diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bb2678..bc4960e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,6 @@ deploy_preconditions: &deploy_preconditions - script - auditorium - packages - - shared filters: branches: only: /^master$/ @@ -68,20 +67,6 @@ jobs: cp ~/offen/bootstrap.yml . make test-ci - shared: - docker: - - image: circleci/golang:1.12 - working_directory: ~/offen/shared - steps: - - checkout: - path: ~/offen - - run: - name: Install dependencies - command: go get ./... - - run: - name: Run tests - command: make test - vault: docker: - image: circleci/node:10-browsers diff --git a/Makefile b/Makefile index aa9a1ab..5a317c7 100644 --- a/Makefile +++ b/Makefile @@ -25,4 +25,8 @@ bootstrap: @echo "Bootstrapping Accounts service ..." @docker-compose run accounts make bootstrap -.PHONY: setup bootstrap +build: + @docker build -t offen-server:latest -f build/server/Dockerfile . + @docker build -t offen-proxy:latest -f build/proxy/Dockerfile . + +.PHONY: setup bootstrap build diff --git a/build/docker-compose.yml b/build/docker-compose.yml index b45db47..0eef43d 100644 --- a/build/docker-compose.yml +++ b/build/docker-compose.yml @@ -2,15 +2,23 @@ version: '3' services: proxy: - build: - context: './..' - dockerfile: build/proxy/Dockerfile + image: offen-proxy:latest ports: - 3000:80 depends_on: - server server: - build: - context: './..' - dockerfile: build/server/Dockerfile + image: offen-server:latest + environment: + POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable + COOKIE_EXCHANGE_SECRET: Wsttdo4Z3mXV5sTc + EVENT_RETENTION_PERIOD: 4464h + ACCOUNT_USER_EMAIL_SALT: UwBkP24HCUYqy0Eq + depends_on: + - server_database + + server_database: + image: postgres:11.2 + environment: + POSTGRES_PASSWORD: develop diff --git a/build/proxy/Dockerfile b/build/proxy/Dockerfile index 40dd5b2..6d4d0da 100644 --- a/build/proxy/Dockerfile +++ b/build/proxy/Dockerfile @@ -50,8 +50,8 @@ RUN make publish FROM nginx:1.17-alpine COPY --from=homepage /code/homepage/output /www/data +COPY --from=script /code/script/dist /www/data COPY --from=auditorium /code/auditorium/dist /www/data/auditorium -COPY --from=script /code/script/dist /www/data/script COPY --from=vault /code/vault/dist /www/data/vault COPY ./build/proxy/nginx.conf /etc/nginx/nginx.conf diff --git a/build/proxy/nginx.conf b/build/proxy/nginx.conf index ae70f49..0e658c5 100644 --- a/build/proxy/nginx.conf +++ b/build/proxy/nginx.conf @@ -1,8 +1,12 @@ events {} http { - include /etc/nginx/mime.types; + gzip on; + gzip_comp_level 2; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + include mime.types; upstream server { server server:3000; } @@ -16,6 +20,12 @@ http { proxy_pass http://server; proxy_redirect off; rewrite ^/api(.*)$ $1 break; + proxy_hide_header Content-Type; + add_header Content-Type "application/json"; + } + + location /auditorium/ { + try_files $uri $uri/ /auditorium/index.html; } } } diff --git a/docker-compose.yml b/docker-compose.yml index ed4d640..fc95272 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,6 @@ services: - ./styles:/code/styles ports: - 8080:80 - - 8081:81 depends_on: - homepage - server @@ -26,7 +25,6 @@ services: - ./bootstrap.yml:/offen/server/bootstrap.yml - serverdeps:/go/pkg/mod environment: - CORS_ORIGIN: http://localhost:9977 POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable PORT: 8080 DEVELOPMENT: '1' @@ -61,8 +59,6 @@ services: - .:/offen - scriptdeps:/offen/script/node_modules command: npm start -- --port 9966 - environment: - - VAULT_HOST=/vault/ auditorium: build: @@ -73,8 +69,6 @@ services: - .:/offen - auditoriumdeps:/offen/auditorium/node_modules command: npm start -- --port 9955 - environment: - - VAULT_HOST=/vault/ homepage: build: diff --git a/homepage/content/pages/auditorium.md b/homepage/content/pages/auditorium.md deleted file mode 100644 index 463761a..0000000 --- a/homepage/content/pages/auditorium.md +++ /dev/null @@ -1,5 +0,0 @@ -Title: Auditorium | offen -description: offen is a free and open source analytics software for websites and web applications that allows respectful handling of data. -save_as: auditorium/index.html -href: /auditorium/ -template: auditorium diff --git a/homepage/publishconf.py b/homepage/publishconf.py index e442015..b28fdae 100644 --- a/homepage/publishconf.py +++ b/homepage/publishconf.py @@ -11,8 +11,8 @@ sys.path.append(os.curdir) from pelicanconf import * # If your site is available via HTTPS, make sure SITEURL begins with https:// -SITEURL = 'https://www.offen.dev' -RELATIVE_URLS = False +SITEURL = os.environ.get('PELICAN_SITEURL', 'https://www.offen.dev') +# RELATIVE_URLS = True FEED_ALL_ATOM = 'feeds/all.atom.xml' CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml' diff --git a/homepage/theme/templates/auditorium.html b/homepage/theme/templates/auditorium.html deleted file mode 100644 index 997789b..0000000 --- a/homepage/theme/templates/auditorium.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-{% endblock %} - -{% block scripts %} - {{ super () }} - -{% endblock %} diff --git a/homepage/theme/templates/base.html b/homepage/theme/templates/base.html index 26b3792..160e6bd 100644 --- a/homepage/theme/templates/base.html +++ b/homepage/theme/templates/base.html @@ -20,7 +20,7 @@ {% assets filters="cssmin", output="css/style.min.css", "css/normalize.css", "css/fonts.css", "css/style.css" %} - + {% endassets %} {% if OFFEN_ACCOUNT_ID %} @@ -138,7 +138,7 @@ {% block scripts %} {% assets filters="rjsmin", output="scripts/packed.js", "scripts/jquery-3.4.1.min.js", "scripts/menu.js", "scripts/fade.js" %} - + {% endassets %} {% endblock %} diff --git a/shared/Makefile b/shared/Makefile deleted file mode 100644 index c56d5e2..0000000 --- a/shared/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - @go test -race -cover ./... diff --git a/shared/README.md b/shared/README.md deleted file mode 100644 index c124d27..0000000 --- a/shared/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# shared - -`shared` contains Go packages shared across applications. Consumers need to symlink the folder into their app directory. - ---- - -## Development - -### Commands - -#### Run the tests - -``` -make test -``` diff --git a/shared/http/errors.go b/shared/http/errors.go deleted file mode 100644 index f095011..0000000 --- a/shared/http/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" -) - -type errorResponse struct { - Error string `json:"error"` - Status int `json:"status"` -} - -func jsonError(err error, status int) []byte { - r := errorResponse{err.Error(), status} - b, _ := json.Marshal(r) - return b -} - -// RespondWithJSONError writes the given error to the given response writer -// while wrapping it into a JSON payload. -func RespondWithJSONError(w http.ResponseWriter, err error, status int) { - w.WriteHeader(status) - w.Write(jsonError(err, status)) -} diff --git a/shared/http/errors_test.go b/shared/http/errors_test.go deleted file mode 100644 index 6923218..0000000 --- a/shared/http/errors_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package http - -import ( - "errors" - "net/http" - "net/http/httptest" - "testing" -) - -func TestRespondWithJSONError(t *testing.T) { - w := httptest.NewRecorder() - RespondWithJSONError(w, errors.New("does not work"), http.StatusInternalServerError) - if w.Code != http.StatusInternalServerError { - t.Errorf("Unexpected status code %d", w.Code) - } - if w.Body.String() != `{"error":"does not work","status":500}` { - t.Errorf("Unexpected response body %s", w.Body.String()) - } -} diff --git a/shared/http/jwt.go b/shared/http/jwt.go deleted file mode 100644 index bb2dfdd..0000000 --- a/shared/http/jwt.go +++ /dev/null @@ -1,174 +0,0 @@ -package http - -import ( - "context" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/lestrrat-go/jwx/jwa" - "github.com/lestrrat-go/jwx/jwt" -) - -type contextKey string - -// ClaimsContextKey will be used to attach a JWT claim to a request context -const ClaimsContextKey contextKey = "claims" - -// JWTProtect uses the public key located at the given URL to check if the -// cookie value is signed properly. In case yes, the JWT claims will be added -// to the request context -func JWTProtect(keyURL, cookieName, headerName string, authorizer func(*http.Request, map[string]interface{}) error, cache Cache) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var jwtValue string - if authCookie, err := r.Cookie(cookieName); err == nil { - jwtValue = authCookie.Value - } else { - if header := r.Header.Get(headerName); header != "" { - jwtValue = header - } - } - if jwtValue == "" { - RespondWithJSONError(w, errors.New("jwt: could not infer JWT value from cookie or header"), http.StatusForbidden) - return - } - - var keys [][]byte - if cache != nil { - lookup, lookupErr := cache.Get() - if lookupErr == nil { - keys = lookup - } - } - if keys == nil { - var keysErr error - keys, keysErr = fetchKeys(keyURL) - if keysErr != nil { - RespondWithJSONError(w, fmt.Errorf("jwt: error fetching keys: %v", keysErr), http.StatusInternalServerError) - return - } - if cache != nil { - cache.Set(keys) - } - } - - var token *jwt.Token - var tokenErr error - // the response can contain multiple keys to try as some of them - // might have been retired with signed tokens still in use until - // their expiry - for _, key := range keys { - token, tokenErr = tryParse(key, jwtValue) - if tokenErr == nil { - break - } - } - - if tokenErr != nil { - RespondWithJSONError(w, fmt.Errorf("jwt: error verifying token signature: %v", tokenErr), http.StatusForbidden) - return - } - - if err := token.Verify(jwt.WithAcceptableSkew(0)); err != nil { - RespondWithJSONError(w, fmt.Errorf("jwt: error verifying token claims: %v", err), http.StatusForbidden) - return - } - - privKey, _ := token.Get("priv") - claims, _ := privKey.(map[string]interface{}) - if err := authorizer(r, claims); err != nil { - RespondWithJSONError(w, fmt.Errorf("jwt: token claims do not allow the requested operation: %v", err), http.StatusForbidden) - return - } - r = r.WithContext(context.WithValue(r.Context(), ClaimsContextKey, claims)) - next.ServeHTTP(w, r) - }) - } -} - -func tryParse(key []byte, tokenValue string) (*jwt.Token, error) { - keyBytes, _ := pem.Decode([]byte(key)) - if keyBytes == nil { - return nil, errors.New("jwt: no PEM block found in given key") - } - - pubKey, parseErr := x509.ParsePKCS1PublicKey(keyBytes.Bytes) - if parseErr != nil { - return nil, fmt.Errorf("jwt: error parsing key: %v", parseErr) - } - - token, jwtErr := jwt.ParseVerify(strings.NewReader(tokenValue), jwa.RS256, pubKey) - if jwtErr != nil { - return nil, fmt.Errorf("jwt: error parsing token: %v", jwtErr) - } - return token, nil -} - -type keyResponse struct { - Keys []string `json:"keys"` -} - -func fetchKeys(keyURL string) ([][]byte, error) { - fetchRes, fetchErr := http.Get(keyURL) - if fetchErr != nil { - return nil, fetchErr - } - defer fetchRes.Body.Close() - payload := keyResponse{} - if err := json.NewDecoder(fetchRes.Body).Decode(&payload); err != nil { - return nil, err - } - - asBytes := [][]byte{} - for _, key := range payload.Keys { - asBytes = append(asBytes, []byte(key)) - } - return asBytes, nil -} - -// Cache can be implemented by consumers in order to define how requests -// for public keys are being cached. For most use cases, the default cache -// supplied by this package will suffice. -type Cache interface { - Get() ([][]byte, error) - Set([][]byte) -} - -type defaultCache struct { - value *[][]byte - expires time.Duration - deadline time.Time -} - -// DefaultCacheExpiry should be used by cache instantiations without -// any particular requirements. -const DefaultCacheExpiry = time.Minute * 15 - -// ErrNoCache is returned on a cache lookup that did not yield a result -var ErrNoCache = errors.New("nothing found in cache") - -func (c *defaultCache) Get() ([][]byte, error) { - if c.value != nil && time.Now().Before(c.deadline) { - return *c.value, nil - } - return nil, ErrNoCache -} - -func (c *defaultCache) Set(value [][]byte) { - c.deadline = time.Now().Add(c.expires) - c.value = &value -} - -// NewDefaultKeyCache creates a simple cache that will hold a single -// value for the given expiration time -func NewDefaultKeyCache(expires time.Duration) Cache { - return &defaultCache{ - expires: expires, - } -} diff --git a/shared/http/jwt_test.go b/shared/http/jwt_test.go deleted file mode 100644 index 4826efd..0000000 --- a/shared/http/jwt_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package http - -import ( - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/lestrrat-go/jwx/jwa" - - "github.com/lestrrat-go/jwx/jwt" -) - -const publicKey = ` ------BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAl2ifzOsh6AMRHZBe8xycvk01s3EAGT12WbgV9Z7b420Dj3NXrkns -N/jvBbtO9cjQg4WM7NPZLs+ZutRkHCtMxt7vB0kjOYetLPcGdObsVBB5k1jvwvsJ -HkcfmSsZdrV0Lz2Yxuf6ADWkxBqAY3GsS0zW0A2nIMc+41ZxqsZa3ProsKJxecRX -SSZMpZtqCGt/S83Rek4eAahllcWfZpQmoEk7usLuUl5tH2TmaW3e5lo0JNfdwcq5 -PMCa8WSZBFH3YzVttB8rbe7a7336wL2NJQFz6dswL5X1dECYpZ5TRtNgzQYa4V0W -AeICq+EzigaTxrjDHc5urHqEosz1le7O4QIDAQAB ------END RSA PUBLIC KEY----- -` - -const privateKey = ` ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXaJ/M6yHoAxEd -kF7zHJy+TTWzcQAZPXZZuBX1ntvjbQOPc1euSew3+O8Fu071yNCDhYzs09kuz5m6 -1GQcK0zG3u8HSSM5h60s9wZ05uxUEHmTWO/C+wkeRx+ZKxl2tXQvPZjG5/oANaTE -GoBjcaxLTNbQDacgxz7jVnGqxlrc+uiwonF5xFdJJkylm2oIa39LzdF6Th4BqGWV -xZ9mlCagSTu6wu5SXm0fZOZpbd7mWjQk193Byrk8wJrxZJkEUfdjNW20Hytt7trv -ffrAvY0lAXPp2zAvlfV0QJilnlNG02DNBhrhXRYB4gKr4TOKBpPGuMMdzm6seoSi -zPWV7s7hAgMBAAECggEAYoPOxjSP4ThtoIDZZvHNAv2V3WW/HK0jHolqsGBmznmW -AXaZLGwo6NpuG5qea8n38jupUEcfXxfw/OFJKhL6Z8OSX3k1FC+1fDZW2yWNy7zU -fg02I/XXHv5EDxM+BEFYkYxQpcs2nYBJ7tcXhpzl8DDU7JaVkfxSbPVIDEf3wyP2 -k6DjYEeAj7uAsp50/32H9zhlJP/cFZaPiyFYy9/gOmDenrPVyJ/f7iQYNwYAwdbt -yfp11Wd5BpePR58+YXICE8oBtzHvB50akK6RZULC3xHVxLQQ1bSxx6vnttxw5RW+ -QRHTVWtRyWiKe/l5jMvVSUo5XCLqsL2iXfR4bz6hyQKBgQDJQVWEGHyD6JyaN/6i -5M5+O/YTbzMBgt1JAuVR2c0HYE4LpgrX4cA4kT3Bsa/Z9o0uuWVuxVab5gLsjsDu -EI4o+HJQ3pl4LF9xqrndTdybwmZAT6jv3rM/VGfaCDzXCPzVx169I+WsfkyGW7Tr -Cj4KDZyykruG/9OrpN1Aeq9a3wKBgQDAmCnQHLEPvZdezJGBc34HjZntrbW67iFB -L0waCGWydyunYmzfja1FSvlSoziZdqoq0N4+uBQFPZIlERvq0zMgIzNFvxt/WlFV -kQcBV24MNa8dtd+P7GDY8TzTfYBeXwJoi5c59sWLzSwpTlw0rI+ZvuA66eEF7xih -eWw3k1jOPwKBgQCKf8DHGEbQTEtBQlmlZkrIyqDs/PCgEJwSe8Cu1HFpqxfqokkC -CiTLiQB0BMEdAbRlPEcWtQ2GWgMXIqKY8qGyhk+9YYNCFV9VjQU9zDCOrHjLt0Zu -VNcMNR0HCfY8kb3VrM+A4GxVidFGAWR+/9xz9KwqpBoTrIjRrbJphkSZBwKBgBMs -0zTqNmLH0JNasL3/vrOH0KSOYAKdhOgVinEpFt7+6HTA4vAbDf5RKaOlppP48ZZT -t1ztPOkMqUlRe8MUhgmUF53BGj7CwkhPqS/kAYvrqGS/3+NXeIkA87pmy2oZ8YZx -J3xY6nAx3Ey8hYelCqMXEwIqmQHbPUuOaEzcOcJHAoGANw0TRha2YSbUqS3HiGR/ -Jm0lNfeLc3cYlEq0ggRDPLD11485rxVKnaKVHGPYW28OQ4jA+GCc7VZCfPV8VQXW -6b+jUnnBwu/KuYvGMee/xJv1c4MTG54mR9UrUt+R80S0OplpcYkcQnft2Bi+AZ1h -5aZTE7XIouXCYiMKPl4AMtI= ------END PRIVATE KEY----- -` - -func TestJWTProtect(t *testing.T) { - tests := []struct { - name string - cookie *http.Cookie - headers *http.Header - server *httptest.Server - authorizer func(r *http.Request, claims map[string]interface{}) error - expectedStatusCode int - }{ - { - "no cookie", - nil, - nil, - nil, - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - { - "bad server", - &http.Cookie{ - Name: "auth", - Value: "irrelevantgibberish", - }, - nil, - nil, - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusInternalServerError, - }, - { - "non-json response", - &http.Cookie{ - Name: "auth", - Value: "irrelevantgibberish", - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("here's some bytes 4 y'all")) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusInternalServerError, - }, - { - "bad key value", - &http.Cookie{ - Name: "auth", - Value: "irrelevantgibberish", - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"keys":["not really a key","me neither"]}`)) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - { - "invalid key", - &http.Cookie{ - Name: "auth", - Value: "irrelevantgibberish", - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`{"keys":["-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCATZAMIIBCgKCAQEA2yUfHH6SRYKvBTemrefi\nHk4L4qkcc4skl4QCaHOkfgA4VcGKG2nXysYuZK7AzNOcHQVi+e4BwN+BfIZtwEU5\n7Ogctb5eg8ksxxLjS7eSRfQIvPGfAbJ12R9OoOWcue/CdUy/YMec4R/o4+tZ45S6\nQQWIMhLqYljw+s1Runda3K8Q8lOdJ4yEZckXaZr1waNJikC7oGpT7ClAgdbvWIbo\nN18G1OluRn+3WNdcN6V+vIj8c9dGs92bgTPX4cn3RmB/80BDfzeFiPMRw5xaq66F\n42zXzllkTqukQPk2wmO5m9pFy0ciRve+awfgbTtZRZOEpTSWLbbpOfd4RQ5YqDWJ\nmQIDAQAB\n-----END PUBLIC KEY-----"]}`)) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - { - "valid key, bad auth token", - &http.Cookie{ - Name: "auth", - Value: "irrelevantgibberish", - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - { - "valid key, valid token", - &http.Cookie{ - Name: "auth", - Value: (func() string { - token := jwt.New() - token.Set("exp", time.Now().Add(time.Hour)) - keyBytes, _ := pem.Decode([]byte(privateKey)) - privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes) - b, _ := token.Sign(jwa.RS256, privKey) - return string(b) - })(), - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusOK, - }, - { - "ok token in headers", - nil, - (func() *http.Header { - token := jwt.New() - token.Set("exp", time.Now().Add(time.Hour)) - keyBytes, _ := pem.Decode([]byte(privateKey)) - privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes) - b, _ := token.Sign(jwa.RS256, privKey) - return &http.Header{ - "X-RPC-Authentication": []string{string(b)}, - } - })(), - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusOK, - }, - { - "bad token in headers", - nil, - (func() *http.Header { - return &http.Header{ - "X-RPC-Authentication": []string{"nilly willy"}, - } - })(), - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - { - "authorizer rejects", - &http.Cookie{ - Name: "auth", - Value: (func() string { - token := jwt.New() - token.Set("exp", time.Now().Add(time.Hour)) - token.Set("priv", map[string]interface{}{"ok": false}) - keyBytes, _ := pem.Decode([]byte(privateKey)) - privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes) - b, _ := token.Sign(jwa.RS256, privKey) - return string(b) - })(), - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { - if claims["ok"] == true { - return nil - } - return errors.New("expected ok to be true") - }, - http.StatusForbidden, - }, - { - "valid key, expired token", - &http.Cookie{ - Name: "auth", - Value: (func() string { - token := jwt.New() - token.Set("exp", time.Now().Add(-time.Hour)) - keyBytes, _ := pem.Decode([]byte(privateKey)) - privKey, _ := x509.ParsePKCS8PrivateKey(keyBytes.Bytes) - b, _ := token.Sign(jwa.RS256, privKey) - return string(b) - })(), - }, - nil, - httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte( - fmt.Sprintf(`{"keys":["%s"]}`, strings.ReplaceAll(publicKey, "\n", `\n`)), - )) - })), - func(r *http.Request, claims map[string]interface{}) error { return nil }, - http.StatusForbidden, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var url string - if test.server != nil { - url = test.server.URL - } - wrappedHandler := JWTProtect(url, "auth", "X-RPC-Authentication", test.authorizer, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK")) - })) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - if test.cookie != nil { - r.AddCookie(test.cookie) - } - if test.headers != nil { - for key, value := range *test.headers { - r.Header.Add(key, value[0]) - } - } - wrappedHandler.ServeHTTP(w, r) - if w.Code != test.expectedStatusCode { - t.Errorf("Unexpected status code %v", w.Code) - } - }) - } -} diff --git a/shared/http/middleware.go b/shared/http/middleware.go deleted file mode 100644 index d1dc3ee..0000000 --- a/shared/http/middleware.go +++ /dev/null @@ -1,64 +0,0 @@ -package http - -import ( - "context" - "errors" - "net/http" -) - -// CorsMiddleware ensures the wrapped handler will respond with proper CORS -// headers using the given origin. -func CorsMiddleware(origin string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Access-Control-Allow-Methods", "POST,GET") - w.Header().Set("Access-Control-Allow-Origin", origin) - next.ServeHTTP(w, r) - }) - } -} - -// ContentTypeMiddleware ensuresthe wrapped handler will respond with a -// content type header of the given value. -func ContentTypeMiddleware(contentType string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", contentType) - next.ServeHTTP(w, r) - }) - } -} - -// OptoutMiddleware drops all requests to the given handler that are sent with -// a cookie of the given name, -func OptoutMiddleware(cookieName string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if _, err := r.Cookie(cookieName); err == nil { - w.WriteHeader(http.StatusNoContent) - return - } - next.ServeHTTP(w, r) - }) - } -} - -// UserCookieMiddleware ensures a cookie of the given name is present and -// attaches its value to the request's context using the given key, before -// passing it on to the wrapped handler. -func UserCookieMiddleware(cookieKey string, contextKey interface{}) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := r.Cookie(cookieKey) - if err != nil { - RespondWithJSONError(w, errors.New("user cookie: received no or blank identifier"), http.StatusBadRequest) - return - } - r = r.WithContext( - context.WithValue(r.Context(), contextKey, c.Value), - ) - next.ServeHTTP(w, r) - }) - } -} diff --git a/shared/http/middleware_test.go b/shared/http/middleware_test.go deleted file mode 100644 index 633d44b..0000000 --- a/shared/http/middleware_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package http - -import ( - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestCorsMiddleware(t *testing.T) { - t.Run("default", func(t *testing.T) { - wrapped := CorsMiddleware("https://www.example.net")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK")) - })) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - wrapped.ServeHTTP(w, r) - if h := w.Header().Get("Access-Control-Allow-Origin"); h != "https://www.example.net" { - t.Errorf("Unexpected header value %v", h) - } - }) -} - -func TestContentTypeMiddleware(t *testing.T) { - t.Run("default", func(t *testing.T) { - wrapped := ContentTypeMiddleware("application/json")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("OK")) - })) - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - wrapped.ServeHTTP(w, r) - if h := w.Header().Get("Content-Type"); h != "application/json" { - t.Errorf("Unexpected header value %v", h) - } - }) -} - -func TestOptoutMiddleware(t *testing.T) { - wrapped := OptoutMiddleware("optout")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("hey there")) - })) - t.Run("with header", func(t *testing.T) { - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.AddCookie(&http.Cookie{ - Name: "optout", - }) - wrapped.ServeHTTP(w, r) - - if w.Code != http.StatusNoContent { - t.Errorf("Unexpected status code %d", w.Code) - } - - if w.Body.String() != "" { - t.Errorf("Unexpected response body %s", w.Body.String()) - } - }) - t.Run("without header", func(t *testing.T) { - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - wrapped.ServeHTTP(w, r) - - if w.Code != http.StatusOK { - t.Errorf("Unexpected status code %d", w.Code) - } - - if w.Body.String() != "hey there" { - t.Errorf("Unexpected response body %s", w.Body.String()) - } - }) -} - -func TestUserCookieMiddleware(t *testing.T) { - wrapped := UserCookieMiddleware("user", 1)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - value := r.Context().Value(1) - fmt.Fprintf(w, "value is %v", value) - })) - t.Run("no cookie", func(t *testing.T) { - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - wrapped.ServeHTTP(w, r) - if w.Code != http.StatusBadRequest { - t.Errorf("Unexpected status code %v", w.Code) - } - if !strings.Contains(w.Body.String(), "received no or blank identifier") { - t.Errorf("Unexpected body %s", w.Body.String()) - } - }) - - t.Run("no value", func(t *testing.T) { - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - wrapped.ServeHTTP(w, r) - r.AddCookie(&http.Cookie{ - Name: "user", - Value: "", - }) - if w.Code != http.StatusBadRequest { - t.Errorf("Unexpected status code %v", w.Code) - } - if !strings.Contains(w.Body.String(), "received no or blank identifier") { - t.Errorf("Unexpected body %s", w.Body.String()) - } - }) - - t.Run("ok", func(t *testing.T) { - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.AddCookie(&http.Cookie{ - Name: "user", - Value: "token", - }) - wrapped.ServeHTTP(w, r) - if w.Code != http.StatusOK { - t.Errorf("Unexpected status code %v", w.Code) - } - if w.Body.String() != "value is token" { - t.Errorf("Unexpected body %s", w.Body.String()) - } - }) -} diff --git a/styles/index.css b/styles/index.css deleted file mode 100644 index e69de29..0000000