2
0
mirror of https://github.com/offen/website.git synced 2024-11-22 01:00:26 +01:00

normalize local urls throughout

This commit is contained in:
Frederik Ring 2019-09-05 11:18:42 +02:00
parent b2fe172681
commit 460e2a77ac
19 changed files with 35 additions and 748 deletions

View File

@ -17,7 +17,6 @@ deploy_preconditions: &deploy_preconditions
- script - script
- auditorium - auditorium
- packages - packages
- shared
filters: filters:
branches: branches:
only: /^master$/ only: /^master$/
@ -68,20 +67,6 @@ jobs:
cp ~/offen/bootstrap.yml . cp ~/offen/bootstrap.yml .
make test-ci 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: vault:
docker: docker:
- image: circleci/node:10-browsers - image: circleci/node:10-browsers

View File

@ -25,4 +25,8 @@ bootstrap:
@echo "Bootstrapping Accounts service ..." @echo "Bootstrapping Accounts service ..."
@docker-compose run accounts make bootstrap @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

View File

@ -2,15 +2,23 @@ version: '3'
services: services:
proxy: proxy:
build: image: offen-proxy:latest
context: './..'
dockerfile: build/proxy/Dockerfile
ports: ports:
- 3000:80 - 3000:80
depends_on: depends_on:
- server - server
server: server:
build: image: offen-server:latest
context: './..' environment:
dockerfile: build/server/Dockerfile 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

View File

@ -50,8 +50,8 @@ RUN make publish
FROM nginx:1.17-alpine FROM nginx:1.17-alpine
COPY --from=homepage /code/homepage/output /www/data 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=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 --from=vault /code/vault/dist /www/data/vault
COPY ./build/proxy/nginx.conf /etc/nginx/nginx.conf COPY ./build/proxy/nginx.conf /etc/nginx/nginx.conf

View File

@ -1,8 +1,12 @@
events {} events {}
http { 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 { upstream server {
server server:3000; server server:3000;
} }
@ -16,6 +20,12 @@ http {
proxy_pass http://server; proxy_pass http://server;
proxy_redirect off; proxy_redirect off;
rewrite ^/api(.*)$ $1 break; rewrite ^/api(.*)$ $1 break;
proxy_hide_header Content-Type;
add_header Content-Type "application/json";
}
location /auditorium/ {
try_files $uri $uri/ /auditorium/index.html;
} }
} }
} }

View File

@ -8,7 +8,6 @@ services:
- ./styles:/code/styles - ./styles:/code/styles
ports: ports:
- 8080:80 - 8080:80
- 8081:81
depends_on: depends_on:
- homepage - homepage
- server - server
@ -26,7 +25,6 @@ services:
- ./bootstrap.yml:/offen/server/bootstrap.yml - ./bootstrap.yml:/offen/server/bootstrap.yml
- serverdeps:/go/pkg/mod - serverdeps:/go/pkg/mod
environment: environment:
CORS_ORIGIN: http://localhost:9977
POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable POSTGRES_CONNECTION_STRING: postgres://postgres:develop@server_database:5432/postgres?sslmode=disable
PORT: 8080 PORT: 8080
DEVELOPMENT: '1' DEVELOPMENT: '1'
@ -61,8 +59,6 @@ services:
- .:/offen - .:/offen
- scriptdeps:/offen/script/node_modules - scriptdeps:/offen/script/node_modules
command: npm start -- --port 9966 command: npm start -- --port 9966
environment:
- VAULT_HOST=/vault/
auditorium: auditorium:
build: build:
@ -73,8 +69,6 @@ services:
- .:/offen - .:/offen
- auditoriumdeps:/offen/auditorium/node_modules - auditoriumdeps:/offen/auditorium/node_modules
command: npm start -- --port 9955 command: npm start -- --port 9955
environment:
- VAULT_HOST=/vault/
homepage: homepage:
build: build:

View File

@ -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

View File

@ -11,8 +11,8 @@ sys.path.append(os.curdir)
from pelicanconf import * from pelicanconf import *
# If your site is available via HTTPS, make sure SITEURL begins with https:// # If your site is available via HTTPS, make sure SITEURL begins with https://
SITEURL = 'https://www.offen.dev' SITEURL = os.environ.get('PELICAN_SITEURL', 'https://www.offen.dev')
RELATIVE_URLS = False # RELATIVE_URLS = True
FEED_ALL_ATOM = 'feeds/all.atom.xml' FEED_ALL_ATOM = 'feeds/all.atom.xml'
CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml' CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml'

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div id="app-host"></div>
{% endblock %}
{% block scripts %}
{{ super () }}
<script src="{{AUDITORIUM_SCRIPT}}"></script>
{% endblock %}

View File

@ -20,7 +20,7 @@
<link rel="canonical" href="{{ SITEURL }}/{{ page.save_as }}"> <link rel="canonical" href="{{ SITEURL }}/{{ page.save_as }}">
<link rel="shortcut icon" type="image/x-icon" href="/theme/images/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="/theme/images/favicon.ico">
{% assets filters="cssmin", output="css/style.min.css", "css/normalize.css", "css/fonts.css", "css/style.css" %} {% assets filters="cssmin", output="css/style.min.css", "css/normalize.css", "css/fonts.css", "css/style.css" %}
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}"> <link rel="stylesheet" href="/{{ ASSET_URL }}">
{% endassets %} {% endassets %}
{% if OFFEN_ACCOUNT_ID %} {% if OFFEN_ACCOUNT_ID %}
<script async src="https://script-alpha.offen.dev/script.js" data-account-id="{{ OFFEN_ACCOUNT_ID }}"></script> <script async src="https://script-alpha.offen.dev/script.js" data-account-id="{{ OFFEN_ACCOUNT_ID }}"></script>
@ -138,7 +138,7 @@
{% block scripts %} {% block scripts %}
{% assets filters="rjsmin", output="scripts/packed.js", "scripts/jquery-3.4.1.min.js", "scripts/menu.js", "scripts/fade.js" %} {% assets filters="rjsmin", output="scripts/packed.js", "scripts/jquery-3.4.1.min.js", "scripts/menu.js", "scripts/fade.js" %}
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script> <script src="/{{ ASSET_URL }}"></script>
{% endassets %} {% endassets %}
{% endblock %} {% endblock %}
</body> </body>

View File

@ -1,2 +0,0 @@
test:
@go test -race -cover ./...

View File

@ -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
```

View File

@ -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))
}

View File

@ -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())
}
}

View File

@ -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,
}
}

View File

@ -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)
}
})
}
}

View File

@ -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)
})
}
}

View File

@ -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())
}
})
}

View File