mirror of
https://github.com/safing/portbase
synced 2025-09-01 10:09:50 +00:00
Add API Key auth and improve endpoints
This commit is contained in:
parent
8c6cb758f7
commit
11e8271d41
10 changed files with 529 additions and 164 deletions
|
@ -4,7 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -16,6 +19,28 @@ import (
|
||||||
"github.com/safing/portbase/rng"
|
"github.com/safing/portbase/rng"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionCookieName = "Portmaster-API-Token"
|
||||||
|
sessionCookieTTL = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
apiKeys = make(map[string]*AuthToken)
|
||||||
|
apiKeysLock sync.Mutex
|
||||||
|
|
||||||
|
authFnSet = abool.New()
|
||||||
|
authFn AuthenticatorFunc
|
||||||
|
|
||||||
|
sessions = make(map[string]*session)
|
||||||
|
sessionsLock sync.Mutex
|
||||||
|
|
||||||
|
// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
|
||||||
|
// AuthenticatorFunc in order to signify a blocked request, including a error
|
||||||
|
// message for the user. This is an empty message on purpose, as to allow the
|
||||||
|
// function to define the full text of the error shown to the user.
|
||||||
|
ErrAPIAccessDeniedMessage = errors.New("")
|
||||||
|
)
|
||||||
|
|
||||||
// Permission defines an API requests permission.
|
// Permission defines an API requests permission.
|
||||||
type Permission int8
|
type Permission int8
|
||||||
|
|
||||||
|
@ -23,9 +48,9 @@ const (
|
||||||
// NotFound declares that the operation does not exist.
|
// NotFound declares that the operation does not exist.
|
||||||
NotFound Permission = -2
|
NotFound Permission = -2
|
||||||
|
|
||||||
// Require declares that the operation requires permission to be processed,
|
// Dynamic declares that the operation requires permission to be processed,
|
||||||
// but anyone can execute the operation.
|
// but anyone can execute the operation, as it reacts to permissions itself.
|
||||||
Require Permission = -1
|
Dynamic Permission = -1
|
||||||
|
|
||||||
// NotSupported declares that the operation is not supported.
|
// NotSupported declares that the operation is not supported.
|
||||||
NotSupported Permission = 0
|
NotSupported Permission = 0
|
||||||
|
@ -61,25 +86,29 @@ type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)
|
||||||
type AuthToken struct {
|
type AuthToken struct {
|
||||||
Read Permission
|
Read Permission
|
||||||
Write Permission
|
Write Permission
|
||||||
|
}
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
token *AuthToken
|
||||||
validUntil time.Time
|
validUntil time.Time
|
||||||
validLock sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expired returns whether the token has expired.
|
// Expired returns whether the session has expired.
|
||||||
func (token *AuthToken) Expired() bool {
|
func (sess *session) Expired() bool {
|
||||||
token.validLock.Lock()
|
sess.Lock()
|
||||||
defer token.validLock.Unlock()
|
defer sess.Unlock()
|
||||||
|
|
||||||
return time.Now().After(token.validUntil)
|
return time.Now().After(sess.validUntil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh refreshes the validity of the token with the given TTL.
|
// Refresh refreshes the validity of the session with the given TTL.
|
||||||
func (token *AuthToken) Refresh(ttl time.Duration) {
|
func (sess *session) Refresh(ttl time.Duration) {
|
||||||
token.validLock.Lock()
|
sess.Lock()
|
||||||
defer token.validLock.Unlock()
|
defer sess.Unlock()
|
||||||
|
|
||||||
token.validUntil = time.Now().Add(ttl)
|
sess.validUntil = time.Now().Add(ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatedHandler defines the handler interface to specify custom
|
// AuthenticatedHandler defines the handler interface to specify custom
|
||||||
|
@ -90,25 +119,6 @@ type AuthenticatedHandler interface {
|
||||||
WritePermission(*http.Request) Permission
|
WritePermission(*http.Request) Permission
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
cookieName = "Portmaster-API-Token"
|
|
||||||
cookieTTL = 5 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
authFnSet = abool.New()
|
|
||||||
authFn AuthenticatorFunc
|
|
||||||
|
|
||||||
authTokens = make(map[string]*AuthToken)
|
|
||||||
authTokensLock sync.Mutex
|
|
||||||
|
|
||||||
// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
|
|
||||||
// AuthenticatorFunc in order to signify a blocked request, including a error
|
|
||||||
// message for the user. This is an empty message on purpose, as to allow the
|
|
||||||
// function to define the full text of the error shown to the user.
|
|
||||||
ErrAPIAccessDeniedMessage = errors.New("")
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
|
// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
|
||||||
func SetAuthenticator(fn AuthenticatorFunc) error {
|
func SetAuthenticator(fn AuthenticatorFunc) error {
|
||||||
if module.Online() {
|
if module.Online() {
|
||||||
|
@ -126,27 +136,22 @@ func SetAuthenticator(fn AuthenticatorFunc) error {
|
||||||
func authMiddleware(next http.Handler) http.Handler {
|
func authMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
token := authenticateRequest(w, r, next)
|
token := authenticateRequest(w, r, next)
|
||||||
if token != nil {
|
if token == nil {
|
||||||
if _, apiRequest := getAPIContext(r); apiRequest != nil {
|
// Authenticator already replied.
|
||||||
apiRequest.AuthToken = token
|
return
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add token to request and serve next handler.
|
||||||
|
if _, apiRequest := getAPIContext(r); apiRequest != nil {
|
||||||
|
apiRequest.AuthToken = token
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
|
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
|
||||||
tracer := log.Tracer(r.Context())
|
tracer := log.Tracer(r.Context())
|
||||||
|
|
||||||
// Check if authenticator is set.
|
|
||||||
if !authFnSet.IsSet() {
|
|
||||||
// Return highest available permissions for the request.
|
|
||||||
return &AuthToken{
|
|
||||||
Read: PermitSelf,
|
|
||||||
Write: PermitSelf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if request is read only.
|
// Check if request is read only.
|
||||||
readRequest := isReadMethod(r.Method)
|
readRequest := isReadMethod(r.Method)
|
||||||
|
|
||||||
|
@ -179,7 +184,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
|
||||||
Read: PermitAnyone,
|
Read: PermitAnyone,
|
||||||
Write: PermitAnyone,
|
Write: PermitAnyone,
|
||||||
}
|
}
|
||||||
case Require:
|
case Dynamic:
|
||||||
// Continue processing permissions, but treat as PermitAnyone.
|
// Continue processing permissions, but treat as PermitAnyone.
|
||||||
requiredPermission = PermitAnyone
|
requiredPermission = PermitAnyone
|
||||||
}
|
}
|
||||||
|
@ -196,40 +201,16 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for an existing auth token.
|
// Authenticate request.
|
||||||
token := checkAuthToken(r)
|
token, handled := checkAuth(w, r, requiredPermission > PermitAnyone)
|
||||||
|
switch {
|
||||||
// Get auth token from authenticator if none was in the request.
|
case handled:
|
||||||
if token == nil {
|
return nil
|
||||||
var err error
|
case token == nil:
|
||||||
token, err = authFn(r, server)
|
// Use default permissions.
|
||||||
if err != nil {
|
token = &AuthToken{
|
||||||
// Check for internal error.
|
Read: PermitAnyone,
|
||||||
if !errors.Is(err, ErrAPIAccessDeniedMessage) {
|
Write: PermitAnyone,
|
||||||
tracer.Errorf("api: authenticator failed: %s", err)
|
|
||||||
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If authentication failed and we require authentication, return an
|
|
||||||
// authentication error.
|
|
||||||
if requiredPermission != PermitAnyone {
|
|
||||||
// Return authentication error.
|
|
||||||
tracer.Warningf("api: denying api access to %s", r.RemoteAddr)
|
|
||||||
http.Error(w, err.Error(), http.StatusForbidden)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
token = &AuthToken{
|
|
||||||
Read: PermitAnyone,
|
|
||||||
Write: PermitAnyone,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply auth token to request.
|
|
||||||
err = applyAuthToken(w, token)
|
|
||||||
if err != nil {
|
|
||||||
tracer.Warningf("api: failed to create auth token: %s", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,11 +235,19 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
|
||||||
|
|
||||||
// Check permission.
|
// Check permission.
|
||||||
if requestPermission < requiredPermission {
|
if requestPermission < requiredPermission {
|
||||||
|
// If the token is strictly public, return an authentication request.
|
||||||
|
if token.Read == PermitAnyone && token.Write == PermitAnyone {
|
||||||
|
w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
|
||||||
|
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise just inform of insufficient permissions.
|
||||||
http.Error(w, "Insufficient permissions.", http.StatusForbidden)
|
http.Error(w, "Insufficient permissions.", http.StatusForbidden)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tracer.Tracef("api: granted %s access to authenticated handler", r.RemoteAddr)
|
tracer.Tracef("api: granted %s access to protected handler", r.RemoteAddr)
|
||||||
|
|
||||||
// Make a copy of the AuthToken in order mitigate the handler poisoning the
|
// Make a copy of the AuthToken in order mitigate the handler poisoning the
|
||||||
// token, as changes would apply to future requests.
|
// token, as changes would apply to future requests.
|
||||||
|
@ -268,85 +257,249 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAuthToken(r *http.Request) *AuthToken {
|
func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
|
||||||
// Get auth token from request.
|
// Check for valid API key.
|
||||||
c, err := r.Cookie(cookieName)
|
token = checkAPIKey(r)
|
||||||
|
if token != nil {
|
||||||
|
return token, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid session cookie.
|
||||||
|
token = checkSessionCookie(r)
|
||||||
|
if token != nil {
|
||||||
|
return token, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an external authentication method is available.
|
||||||
|
if !authFnSet.IsSet() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate externally.
|
||||||
|
token, err := authFn(r, server)
|
||||||
|
if err != nil {
|
||||||
|
// Check if the authentication process failed internally.
|
||||||
|
if !errors.Is(err, ErrAPIAccessDeniedMessage) {
|
||||||
|
log.Tracer(r.Context()).Errorf("api: authenticator failed: %s", err)
|
||||||
|
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return authentication failure message if authentication is required.
|
||||||
|
if authRequired {
|
||||||
|
log.Tracer(r.Context()).Warningf("api: denying api access to %s", r.RemoteAddr)
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort if no token is returned.
|
||||||
|
if token == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create session cookie for authenticated request.
|
||||||
|
err = createSession(w, r, token)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(r.Context()).Warningf("api: failed to create session: %s", err)
|
||||||
|
}
|
||||||
|
return token, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAPIKey(r *http.Request) *AuthToken {
|
||||||
|
// Get API key from request.
|
||||||
|
key := r.Header.Get("Authorization")
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse API key.
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(key, "Bearer "):
|
||||||
|
key = strings.TrimPrefix(key, "Bearer ")
|
||||||
|
case strings.HasPrefix(key, "Basic "):
|
||||||
|
user, pass, _ := r.BasicAuth()
|
||||||
|
key = user + pass
|
||||||
|
default:
|
||||||
|
log.Tracer(r.Context()).Tracef(
|
||||||
|
"api: provided api key type %s is unsupported", strings.Split(key, " ")[0],
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeysLock.Lock()
|
||||||
|
defer apiKeysLock.Unlock()
|
||||||
|
|
||||||
|
// Check if the provided API key exists.
|
||||||
|
token, ok := apiKeys[key]
|
||||||
|
if !ok {
|
||||||
|
log.Tracer(r.Context()).Tracef(
|
||||||
|
"api: provided api key %s... is unknown", key[:4],
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAPIKeys(_ context.Context, _ interface{}) error {
|
||||||
|
apiKeysLock.Lock()
|
||||||
|
defer apiKeysLock.Unlock()
|
||||||
|
|
||||||
|
log.Debug("api: importing possibly updated API keys from config")
|
||||||
|
|
||||||
|
// Delete current keys.
|
||||||
|
for k := range apiKeys {
|
||||||
|
delete(apiKeys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse new keys.
|
||||||
|
for _, key := range configuredAPIKeys() {
|
||||||
|
u, err := url.Parse(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("api: failed to parse configured API key %s: %s", key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if u.Path == "" {
|
||||||
|
log.Errorf("api: malformed API key %s: missing path section", key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with default permissions.
|
||||||
|
token := &AuthToken{
|
||||||
|
Read: PermitAnyone,
|
||||||
|
Write: PermitAnyone,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with configured permissions.
|
||||||
|
q := u.Query()
|
||||||
|
// Parse read permission.
|
||||||
|
readPermission, err := parseAPIPermission(q.Get("read"))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("api: invalid API key %s: %s", key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token.Read = readPermission
|
||||||
|
// Parse write permission.
|
||||||
|
writePermission, err := parseAPIPermission(q.Get("write"))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("api: invalid API key %s: %s", key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token.Write = writePermission
|
||||||
|
|
||||||
|
// Save token.
|
||||||
|
apiKeys[u.Path] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSessionCookie(r *http.Request) *AuthToken {
|
||||||
|
// Get session cookie from request.
|
||||||
|
c, err := r.Cookie(sessionCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if auth token is registered.
|
// Check if session cookie is registered.
|
||||||
authTokensLock.Lock()
|
sessionsLock.Lock()
|
||||||
token, ok := authTokens[c.Value]
|
sess, ok := sessions[c.Value]
|
||||||
authTokensLock.Unlock()
|
sessionsLock.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Tracer(r.Context()).Tracef("api: provided auth token %s is unknown", c.Value)
|
log.Tracer(r.Context()).Tracef("api: provided session cookie %s is unknown", c.Value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is still valid.
|
// Check if session is still valid.
|
||||||
if token.Expired() {
|
if sess.Expired() {
|
||||||
log.Tracer(r.Context()).Tracef("api: provided auth token %s has expired", c.Value)
|
log.Tracer(r.Context()).Tracef("api: provided session cookie %s has expired", c.Value)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh token and return.
|
// Refresh session and return.
|
||||||
token.Refresh(cookieTTL)
|
sess.Refresh(sessionCookieTTL)
|
||||||
log.Tracer(r.Context()).Tracef("api: auth token %s is valid, refreshing", c.Value)
|
log.Tracer(r.Context()).Tracef("api: session cookie %s is valid, refreshing", c.Value)
|
||||||
return token
|
return sess.token
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAuthToken(w http.ResponseWriter, token *AuthToken) error {
|
func createSession(w http.ResponseWriter, r *http.Request, token *AuthToken) error {
|
||||||
// Generate new token secret.
|
// Generate new session key.
|
||||||
secret, err := rng.Bytes(32) // 256 bit
|
secret, err := rng.Bytes(32) // 256 bit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
secretHex := base64.RawURLEncoding.EncodeToString(secret)
|
sessionKey := base64.RawURLEncoding.EncodeToString(secret)
|
||||||
|
|
||||||
// Set token cookie in response.
|
// Set token cookie in response.
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: cookieName,
|
Name: sessionCookieName,
|
||||||
Value: secretHex,
|
Value: sessionKey,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set token TTL.
|
// Create session.
|
||||||
token.Refresh(cookieTTL)
|
sess := &session{
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
sess.Refresh(sessionCookieTTL)
|
||||||
|
|
||||||
// Save token.
|
// Save session.
|
||||||
authTokensLock.Lock()
|
sessionsLock.Lock()
|
||||||
defer authTokensLock.Unlock()
|
defer sessionsLock.Unlock()
|
||||||
authTokens[secretHex] = token
|
sessions[sessionKey] = sess
|
||||||
|
log.Tracer(r.Context()).Debug("api: issued session cookie")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanAuthTokens(_ context.Context, _ *modules.Task) error {
|
func cleanSessions(_ context.Context, _ *modules.Task) error {
|
||||||
authTokensLock.Lock()
|
sessionsLock.Lock()
|
||||||
defer authTokensLock.Unlock()
|
defer sessionsLock.Unlock()
|
||||||
|
|
||||||
for secret, token := range authTokens {
|
for sessionKey, sess := range sessions {
|
||||||
if token.Expired() {
|
if sess.Expired() {
|
||||||
delete(authTokens, secret)
|
delete(sessions, sessionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteSession(sessionKey string) {
|
||||||
|
sessionsLock.Lock()
|
||||||
|
defer sessionsLock.Unlock()
|
||||||
|
|
||||||
|
delete(sessions, sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
func isReadMethod(method string) bool {
|
func isReadMethod(method string) bool {
|
||||||
return method == http.MethodGet || method == http.MethodHead
|
return method == http.MethodGet || method == http.MethodHead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAPIPermission(s string) (Permission, error) {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "", "anyone":
|
||||||
|
return PermitAnyone, nil
|
||||||
|
case "user":
|
||||||
|
return PermitUser, nil
|
||||||
|
case "admin":
|
||||||
|
return PermitAdmin, nil
|
||||||
|
default:
|
||||||
|
return PermitAnyone, fmt.Errorf("invalid permission: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p Permission) String() string {
|
func (p Permission) String() string {
|
||||||
switch p {
|
switch p {
|
||||||
case NotSupported:
|
case NotSupported:
|
||||||
return "NotSupported"
|
return "NotSupported"
|
||||||
case Require:
|
case Dynamic:
|
||||||
return "Require"
|
return "Dynamic"
|
||||||
case PermitAnyone:
|
case PermitAnyone:
|
||||||
return "PermitAnyone"
|
return "PermitAnyone"
|
||||||
case PermitUser:
|
case PermitUser:
|
||||||
|
@ -361,3 +514,19 @@ func (p Permission) String() string {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Role returns a string representation of the permission role.
|
||||||
|
func (p Permission) Role() string {
|
||||||
|
switch p {
|
||||||
|
case PermitAnyone:
|
||||||
|
return "Anyone"
|
||||||
|
case PermitUser:
|
||||||
|
return "User"
|
||||||
|
case PermitAdmin:
|
||||||
|
return "Admin"
|
||||||
|
case PermitSelf:
|
||||||
|
return "Self"
|
||||||
|
default:
|
||||||
|
return "Invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ func TestPermissions(t *testing.T) { //nolint:gocognit
|
||||||
PermitUser,
|
PermitUser,
|
||||||
PermitAdmin,
|
PermitAdmin,
|
||||||
PermitSelf,
|
PermitSelf,
|
||||||
Require,
|
Dynamic,
|
||||||
NotFound,
|
NotFound,
|
||||||
100, // Test a too high value.
|
100, // Test a too high value.
|
||||||
-100, // Test a too low value.
|
-100, // Test a too low value.
|
||||||
|
@ -117,7 +117,7 @@ func TestPermissions(t *testing.T) { //nolint:gocognit
|
||||||
case handlerPerm == PermitAnyone:
|
case handlerPerm == PermitAnyone:
|
||||||
// This is fast-tracked. There are not additional checks.
|
// This is fast-tracked. There are not additional checks.
|
||||||
expectSuccess = true
|
expectSuccess = true
|
||||||
case handlerPerm == Require:
|
case handlerPerm == Dynamic:
|
||||||
// This is turned into PermitAnyone in the authenticator.
|
// This is turned into PermitAnyone in the authenticator.
|
||||||
// But authentication is still processed and the result still gets
|
// But authentication is still processed and the result still gets
|
||||||
// sanity checked!
|
// sanity checked!
|
||||||
|
|
|
@ -10,12 +10,15 @@ import (
|
||||||
// Config Keys
|
// Config Keys
|
||||||
const (
|
const (
|
||||||
CfgDefaultListenAddressKey = "core/listenAddress"
|
CfgDefaultListenAddressKey = "core/listenAddress"
|
||||||
|
CfgAPIKeys = "core/apiKeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
listenAddressFlag string
|
listenAddressFlag string
|
||||||
listenAddressConfig config.StringOption
|
listenAddressConfig config.StringOption
|
||||||
defaultListenAddress string
|
defaultListenAddress string
|
||||||
|
|
||||||
|
configuredAPIKeys config.StringArrayOption
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -58,11 +61,28 @@ func registerConfig() error {
|
||||||
}
|
}
|
||||||
listenAddressConfig = config.GetAsString(CfgDefaultListenAddressKey, getDefaultListenAddress())
|
listenAddressConfig = config.GetAsString(CfgDefaultListenAddressKey, getDefaultListenAddress())
|
||||||
|
|
||||||
|
err = config.Register(&config.Option{
|
||||||
|
Name: "API Keys",
|
||||||
|
Key: CfgAPIKeys,
|
||||||
|
Description: "Defined API Keys.",
|
||||||
|
OptType: config.OptTypeStringArray,
|
||||||
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
|
ReleaseLevel: config.ReleaseLevelStable,
|
||||||
|
DefaultValue: []string{},
|
||||||
|
Annotations: config.Annotations{
|
||||||
|
config.DisplayOrderAnnotation: 514,
|
||||||
|
config.CategoryAnnotation: "Development",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configuredAPIKeys = config.GetAsStringArray(CfgAPIKeys, []string{})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaultAPIListenAddress sets the default listen address for the API.
|
// SetDefaultAPIListenAddress sets the default listen address for the API.
|
||||||
func SetDefaultAPIListenAddress(address string) {
|
func SetDefaultAPIListenAddress(address string) {
|
||||||
|
|
||||||
defaultListenAddress = address
|
defaultListenAddress = address
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,13 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterHandleFunc("/api/database/v1", startDatabaseAPI) // net/http pattern matching only this exact path
|
RegisterHandler("/api/database/v1", WrapInAuthHandler(
|
||||||
|
startDatabaseAPI,
|
||||||
|
// Default to admin read/write permissions until the database gets support
|
||||||
|
// for api permissions.
|
||||||
|
PermitAdmin,
|
||||||
|
PermitAdmin,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseAPI is a database API instance.
|
// DatabaseAPI is a database API instance.
|
||||||
|
@ -93,7 +99,7 @@ func startDatabaseAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
go new.handler()
|
go new.handler()
|
||||||
go new.writer()
|
go new.writer()
|
||||||
|
|
||||||
log.Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI)
|
log.Tracer(r.Context()).Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *DatabaseAPI) handler() {
|
func (api *DatabaseAPI) handler() {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -23,13 +24,6 @@ type Endpoint struct {
|
||||||
Read Permission
|
Read Permission
|
||||||
Write Permission
|
Write Permission
|
||||||
|
|
||||||
// TODO: We _could_ expose more metadata to be able to build lists of actions
|
|
||||||
// automatically.
|
|
||||||
// Name string
|
|
||||||
// Description string
|
|
||||||
// Order int
|
|
||||||
// ExpertiseLevel config.ExpertiseLevel
|
|
||||||
|
|
||||||
// ActionFunc is for simple actions with a return message for the user.
|
// ActionFunc is for simple actions with a return message for the user.
|
||||||
ActionFunc ActionFunc `json:"-"`
|
ActionFunc ActionFunc `json:"-"`
|
||||||
|
|
||||||
|
@ -45,6 +39,19 @@ type Endpoint struct {
|
||||||
|
|
||||||
// HandlerFunc is the raw http handler.
|
// HandlerFunc is the raw http handler.
|
||||||
HandlerFunc http.HandlerFunc `json:"-"`
|
HandlerFunc http.HandlerFunc `json:"-"`
|
||||||
|
|
||||||
|
// Documentation Metadata.
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Parameters []Parameter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter describes a parameterized variation of an endpoint.
|
||||||
|
type Parameter struct {
|
||||||
|
Method string
|
||||||
|
Field string
|
||||||
|
Value string
|
||||||
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -141,10 +148,10 @@ func (e *Endpoint) check() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permissions.
|
// Check permissions.
|
||||||
if e.Read < Require || e.Read > PermitSelf {
|
if e.Read < Dynamic || e.Read > PermitSelf {
|
||||||
return errors.New("invalid read permission")
|
return errors.New("invalid read permission")
|
||||||
}
|
}
|
||||||
if e.Write < Require || e.Write > PermitSelf {
|
if e.Write < Dynamic || e.Write > PermitSelf {
|
||||||
return errors.New("invalid write permission")
|
return errors.New("invalid write permission")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +190,28 @@ func (e *Endpoint) check() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportEndpoints exports the registered endpoints. The returned data must be
|
||||||
|
// treated as immutable.
|
||||||
|
func ExportEndpoints() []*Endpoint {
|
||||||
|
endpointsLock.RLock()
|
||||||
|
defer endpointsLock.RUnlock()
|
||||||
|
|
||||||
|
// Copy the map into a slice.
|
||||||
|
eps := make([]*Endpoint, 0, len(endpoints))
|
||||||
|
for _, ep := range endpoints {
|
||||||
|
eps = append(eps, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(sortByPath(eps))
|
||||||
|
return eps
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortByPath []*Endpoint
|
||||||
|
|
||||||
|
func (eps sortByPath) Len() int { return len(eps) }
|
||||||
|
func (eps sortByPath) Less(i, j int) bool { return eps[i].Path < eps[j].Path }
|
||||||
|
func (eps sortByPath) Swap(i, j int) { eps[i], eps[j] = eps[j], eps[i] }
|
||||||
|
|
||||||
type endpointHandler struct{}
|
type endpointHandler struct{}
|
||||||
|
|
||||||
var _ AuthenticatedHandler = &endpointHandler{} // Compile time interface check.
|
var _ AuthenticatedHandler = &endpointHandler{} // Compile time interface check.
|
||||||
|
@ -278,6 +307,7 @@ func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// Write response.
|
// Write response.
|
||||||
w.Header().Set("Content-Type", apiEndpoint.MimeType+"; charset=utf-8")
|
w.Header().Set("Content-Type", apiEndpoint.MimeType+"; charset=utf-8")
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err = w.Write(responseData)
|
_, err = w.Write(responseData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
|
||||||
|
@ -11,25 +12,37 @@ import (
|
||||||
|
|
||||||
func registerDebugEndpoints() error {
|
func registerDebugEndpoints() error {
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "debug/stack",
|
Path: "debug/stack",
|
||||||
Read: PermitAnyone,
|
Read: PermitAnyone,
|
||||||
DataFunc: getStack,
|
DataFunc: getStack,
|
||||||
|
Name: "Get Goroutine Stack",
|
||||||
|
Description: "Returns the current goroutine stack.",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "debug/stack/print",
|
Path: "debug/stack/print",
|
||||||
Read: PermitAnyone,
|
Read: PermitAnyone,
|
||||||
ActionFunc: printStack,
|
ActionFunc: printStack,
|
||||||
|
Name: "Print Goroutine Stack",
|
||||||
|
Description: "Prints the current goroutine stack to stdout.",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "debug/info",
|
Path: "debug/info",
|
||||||
Read: PermitAnyone,
|
Read: PermitAnyone,
|
||||||
DataFunc: debugInfo,
|
DataFunc: debugInfo,
|
||||||
|
Name: "Get Debug Information",
|
||||||
|
Description: "Returns debugging information, including the version and platform info, errors, logs and the current goroutine stack.",
|
||||||
|
Parameters: []Parameter{{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Field: "style",
|
||||||
|
Value: "github",
|
||||||
|
Description: "Specify the formatting style. The default is simple markdown formatting.",
|
||||||
|
}},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,67 @@ package api
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerMetaEndpoints() error {
|
func registerMetaEndpoints() error {
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "endpoints",
|
Path: "ping",
|
||||||
Read: PermitAnyone,
|
Read: PermitAnyone,
|
||||||
MimeType: MimeTypeJSON,
|
ActionFunc: ping,
|
||||||
DataFunc: listEndpoints,
|
Name: "Ping",
|
||||||
|
Description: "Pong.",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "permission",
|
Path: "endpoints",
|
||||||
Read: Require,
|
Read: PermitAnyone,
|
||||||
StructFunc: permissions,
|
MimeType: MimeTypeJSON,
|
||||||
|
DataFunc: listEndpoints,
|
||||||
|
Name: "Export API Endpoints",
|
||||||
|
Description: "Returns a list of all registered endpoints and their metadata.",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := RegisterEndpoint(Endpoint{
|
if err := RegisterEndpoint(Endpoint{
|
||||||
Path: "ping",
|
Path: "auth/permissions",
|
||||||
Read: PermitAnyone,
|
Read: Dynamic,
|
||||||
ActionFunc: ping,
|
StructFunc: permissions,
|
||||||
|
Name: "View Current Permissions",
|
||||||
|
Description: "Returns the current permissions assigned to the request.",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RegisterEndpoint(Endpoint{
|
||||||
|
Path: "auth/bearer",
|
||||||
|
Read: Dynamic,
|
||||||
|
HandlerFunc: authBearer,
|
||||||
|
Name: "Request HTTP Bearer Auth",
|
||||||
|
Description: "Returns an HTTP Bearer Auth request, if not authenticated.",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RegisterEndpoint(Endpoint{
|
||||||
|
Path: "auth/basic",
|
||||||
|
Read: Dynamic,
|
||||||
|
HandlerFunc: authBasic,
|
||||||
|
Name: "Request HTTP Basic Auth",
|
||||||
|
Description: "Returns an HTTP Basic Auth request, if not authenticated.",
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := RegisterEndpoint(Endpoint{
|
||||||
|
Path: "auth/reset",
|
||||||
|
Read: PermitAnyone,
|
||||||
|
HandlerFunc: authReset,
|
||||||
|
Name: "Reset Authenticated Session",
|
||||||
|
Description: "Resets authentication status internally and in the browser.",
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -35,10 +72,7 @@ func registerMetaEndpoints() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func listEndpoints(ar *Request) (data []byte, err error) {
|
func listEndpoints(ar *Request) (data []byte, err error) {
|
||||||
endpointsLock.Lock()
|
data, err = json.Marshal(ExportEndpoints())
|
||||||
defer endpointsLock.Unlock()
|
|
||||||
|
|
||||||
data, err = json.Marshal(endpoints)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,18 +82,67 @@ func permissions(ar *Request) (i interface{}, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return struct {
|
return struct {
|
||||||
Read Permission
|
Read Permission
|
||||||
Write Permission
|
Write Permission
|
||||||
ReadPermName string
|
ReadRole string
|
||||||
WritePermName string
|
WriteRole string
|
||||||
}{
|
}{
|
||||||
Read: ar.AuthToken.Read,
|
Read: ar.AuthToken.Read,
|
||||||
Write: ar.AuthToken.Write,
|
Write: ar.AuthToken.Write,
|
||||||
ReadPermName: ar.AuthToken.Read.String(),
|
ReadRole: ar.AuthToken.Read.Role(),
|
||||||
WritePermName: ar.AuthToken.Write.String(),
|
WriteRole: ar.AuthToken.Write.Role(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ping(ar *Request) (msg string, err error) {
|
func ping(ar *Request) (msg string, err error) {
|
||||||
return "Pong.", nil
|
return "Pong.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func authBearer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check if authenticated by checking read permission.
|
||||||
|
ar := GetAPIRequest(r)
|
||||||
|
if ar.AuthToken.Read != PermitAnyone {
|
||||||
|
TextResponse(w, r, "Authenticated.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with desired authentication header.
|
||||||
|
w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
|
||||||
|
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authBasic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check if authenticated by checking read permission.
|
||||||
|
ar := GetAPIRequest(r)
|
||||||
|
if ar.AuthToken.Read != PermitAnyone {
|
||||||
|
TextResponse(w, r, "Authenticated.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond with desired authentication header.
|
||||||
|
w.Header().Set("WWW-Authenticate", "Basic realm=Portmaster API")
|
||||||
|
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authReset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get session cookie from request and delete session if exists.
|
||||||
|
c, err := r.Cookie(sessionCookieName)
|
||||||
|
if err == nil {
|
||||||
|
deleteSession(c.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete session and cookie.
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: sessionCookieName,
|
||||||
|
MaxAge: -1, // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Request client to also reset all data.
|
||||||
|
w.Header().Set("Clear-Site-Data", "*")
|
||||||
|
|
||||||
|
// Set HTTP Auth Realm without requesting authorization.
|
||||||
|
w.Header().Set("WWW-Authenticate", "None realm=Portmaster API")
|
||||||
|
|
||||||
|
// Reply with 401 Unauthorized in order to clear HTTP Basic Auth data.
|
||||||
|
http.Error(w, "Session deleted.", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
33
api/main.go
33
api/main.go
|
@ -2,7 +2,10 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
|
@ -10,6 +13,8 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
module *modules.Module
|
module *modules.Module
|
||||||
|
|
||||||
|
exportEndpoints bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// API Errors
|
// API Errors
|
||||||
|
@ -20,9 +25,15 @@ var (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
module = modules.Register("api", prep, start, stop, "database", "config")
|
module = modules.Register("api", prep, start, stop, "database", "config")
|
||||||
|
|
||||||
|
flag.BoolVar(&exportEndpoints, "export-api-endpoints", false, "export api endpoint registry and exit")
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
|
if exportEndpoints {
|
||||||
|
modules.SetCmdLineOperation(exportEndpointsCmd)
|
||||||
|
}
|
||||||
|
|
||||||
if getDefaultListenAddress() == "" {
|
if getDefaultListenAddress() == "" {
|
||||||
return errors.New("no default listen address for api available")
|
return errors.New("no default listen address for api available")
|
||||||
}
|
}
|
||||||
|
@ -35,6 +46,10 @@ func prep() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := registerConfigEndpoints(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return registerMetaEndpoints()
|
return registerMetaEndpoints()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +57,15 @@ func start() error {
|
||||||
logFlagOverrides()
|
logFlagOverrides()
|
||||||
go Serve()
|
go Serve()
|
||||||
|
|
||||||
|
_ = updateAPIKeys(module.Ctx, nil)
|
||||||
|
err := module.RegisterEventHook("config", "config change", "update API keys", updateAPIKeys)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// start api auth token cleaner
|
// start api auth token cleaner
|
||||||
if authFnSet.IsSet() {
|
if authFnSet.IsSet() {
|
||||||
module.NewTask("clean api auth tokens", cleanAuthTokens).Repeat(5 * time.Minute)
|
module.NewTask("clean api sessions", cleanSessions).Repeat(5 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -56,3 +77,13 @@ func stop() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportEndpointsCmd() error {
|
||||||
|
data, err := json.MarshalIndent(ExportEndpoints(), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = os.Stdout.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request is a support struct to pool more request related information.
|
// Request is a support struct to pool more request related information.
|
||||||
|
@ -42,3 +44,14 @@ func GetAPIRequest(r *http.Request) *Request {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TextResponse writes a text response.
|
||||||
|
func TextResponse(w http.ResponseWriter, r *http.Request, text string) {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err := fmt.Fprintln(w, text)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(r.Context()).Warningf("api: failed to write text response: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -92,7 +92,6 @@ func (mh *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
|
func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
// Setup context trace logging.
|
// Setup context trace logging.
|
||||||
ctx, tracer := log.AddTracer(r.Context())
|
ctx, tracer := log.AddTracer(r.Context())
|
||||||
lrw := NewLoggingResponseWriter(w, r)
|
|
||||||
// Add request context.
|
// Add request context.
|
||||||
apiRequest := &Request{
|
apiRequest := &Request{
|
||||||
Request: r,
|
Request: r,
|
||||||
|
@ -100,6 +99,7 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx = context.WithValue(ctx, requestContextKey, apiRequest)
|
ctx = context.WithValue(ctx, requestContextKey, apiRequest)
|
||||||
// Add context back to request.
|
// Add context back to request.
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
lrw := NewLoggingResponseWriter(w, r)
|
||||||
|
|
||||||
tracer.Tracef("api request: %s ___ %s", r.RemoteAddr, r.RequestURI)
|
tracer.Tracef("api request: %s ___ %s", r.RemoteAddr, r.RequestURI)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue