a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
83 lines
2.7 KiB
Go
83 lines
2.7 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// WithAuth wraps h with a bearer-token check. Every request must carry
|
|
// `Authorization: Bearer <token>`; mismatches get a 401 JSON error.
|
|
//
|
|
// When token is empty, WithAuth returns h unchanged — the caller has
|
|
// opted into unauthenticated mode (the server command enforces this is
|
|
// only safe with a localhost bind; see cmd/gortex/server.go).
|
|
//
|
|
// CORS preflights (OPTIONS) bypass the check so browsers on a different
|
|
// origin can negotiate headers before the real request is issued.
|
|
//
|
|
// As a browser-EventSource workaround the middleware also accepts the
|
|
// token via `?token=<t>`. Query-string auth leaks more readily into
|
|
// access logs and referrer headers than a Bearer header, so prefer the
|
|
// header when the client can set it.
|
|
func WithAuth(h http.Handler, token string) http.Handler {
|
|
if token == "" {
|
|
return h
|
|
}
|
|
return WithAuthFunc(h, func() string { return token })
|
|
}
|
|
|
|
// WithAuthFunc is WithAuth with a per-request token source: tokenFn is
|
|
// resolved on every request, so an operator can rotate the expected token
|
|
// (e.g. update $GORTEX_DAEMON_HTTP_TOKEN) without restarting the server.
|
|
// A tokenFn that returns "" serves the request unauthenticated, identical
|
|
// to a static empty token — so the token can also be added or removed at
|
|
// runtime, not only changed.
|
|
func WithAuthFunc(h http.Handler, tokenFn func() string) http.Handler {
|
|
if tokenFn == nil {
|
|
return h
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
token := tokenFn()
|
|
if token == "" {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if r.Method == http.MethodOptions {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if authMatches([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if q := r.URL.Query().Get("token"); q != "" && tokenMatches([]byte(q), []byte(token)) {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="gortex"`)
|
|
WriteJSONError(w, http.StatusUnauthorized, "missing or invalid bearer token")
|
|
})
|
|
}
|
|
|
|
// authMatches does a constant-time comparison to defeat timing attacks
|
|
// on token validation.
|
|
func authMatches(got, expected []byte) bool {
|
|
if !strings.HasPrefix(string(got), "Bearer ") {
|
|
return false
|
|
}
|
|
return tokenMatches(got, expected)
|
|
}
|
|
|
|
func tokenMatches(got, expected []byte) bool {
|
|
// subtle.ConstantTimeCompare needs equal-length slices to return 1.
|
|
// For unequal lengths the answer is obviously "no match" but we
|
|
// still scan a fixed-size buffer so an attacker can't learn the
|
|
// token length from timing.
|
|
if len(got) != len(expected) {
|
|
_ = subtle.ConstantTimeCompare(expected, expected)
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare(got, expected) == 1
|
|
}
|