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:
parent
b2fe172681
commit
460e2a77ac
@ -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
|
||||
|
6
Makefile
6
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
@ -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'
|
||||
|
@ -1,10 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="app-host"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super () }}
|
||||
<script src="{{AUDITORIUM_SCRIPT}}"></script>
|
||||
{% endblock %}
|
@ -20,7 +20,7 @@
|
||||
<link rel="canonical" href="{{ SITEURL }}/{{ page.save_as }}">
|
||||
<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" %}
|
||||
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||
<link rel="stylesheet" href="/{{ ASSET_URL }}">
|
||||
{% endassets %}
|
||||
{% if OFFEN_ACCOUNT_ID %}
|
||||
<script async src="https://script-alpha.offen.dev/script.js" data-account-id="{{ OFFEN_ACCOUNT_ID }}"></script>
|
||||
@ -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" %}
|
||||
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script>
|
||||
<script src="/{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
@ -1,2 +0,0 @@
|
||||
test:
|
||||
@go test -race -cover ./...
|
@ -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
|
||||
```
|
@ -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))
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user