mirror of
https://github.com/safing/portbase
synced 2025-09-02 10:40:39 +00:00
Merge pull request #117 from safing/feature/api-security-headers
Add security headers to API
This commit is contained in:
commit
7364562211
12 changed files with 92 additions and 97 deletions
|
@ -133,22 +133,6 @@ func SetAuthenticator(fn AuthenticatorFunc) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
token := authenticateRequest(w, r, next)
|
|
||||||
if token == nil {
|
|
||||||
// Authenticator already replied.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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())
|
||||||
|
|
||||||
|
@ -258,6 +242,14 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
|
func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
|
||||||
|
// Return highest possible permissions in dev mode.
|
||||||
|
if devMode() {
|
||||||
|
return &AuthToken{
|
||||||
|
Read: PermitSelf,
|
||||||
|
Write: PermitSelf,
|
||||||
|
}, false
|
||||||
|
}
|
||||||
|
|
||||||
// Check for valid API key.
|
// Check for valid API key.
|
||||||
token = checkAPIKey(r)
|
token = checkAPIKey(r)
|
||||||
if token != nil {
|
if token != nil {
|
||||||
|
@ -478,7 +470,12 @@ func deleteSession(sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isReadMethod(method string) bool {
|
func isReadMethod(method string) bool {
|
||||||
return method == http.MethodGet || method == http.MethodHead
|
switch method {
|
||||||
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAPIPermission(s string) (Permission, error) {
|
func parseAPIPermission(s string) (Permission, error) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
// message types
|
// Message Types.
|
||||||
const (
|
const (
|
||||||
msgRequestGet = "get"
|
msgRequestGet = "get"
|
||||||
msgRequestQuery = "query"
|
msgRequestQuery = "query"
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/tevino/abool"
|
"github.com/tevino/abool"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client errors
|
// Client errors.
|
||||||
var (
|
var (
|
||||||
ErrMalformedMessage = errors.New("malformed message")
|
ErrMalformedMessage = errors.New("malformed message")
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config Keys
|
// Config Keys.
|
||||||
const (
|
const (
|
||||||
CfgDefaultListenAddressKey = "core/listenAddress"
|
CfgDefaultListenAddressKey = "core/listenAddress"
|
||||||
CfgAPIKeys = "core/apiKeys"
|
CfgAPIKeys = "core/apiKeys"
|
||||||
|
@ -19,6 +19,8 @@ var (
|
||||||
defaultListenAddress string
|
defaultListenAddress string
|
||||||
|
|
||||||
configuredAPIKeys config.StringArrayOption
|
configuredAPIKeys config.StringArrayOption
|
||||||
|
|
||||||
|
devMode config.BoolOption
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -79,6 +81,8 @@ func registerConfig() error {
|
||||||
}
|
}
|
||||||
configuredAPIKeys = config.GetAsStringArray(CfgAPIKeys, []string{})
|
configuredAPIKeys = config.GetAsStringArray(CfgAPIKeys, []string{})
|
||||||
|
|
||||||
|
devMode = config.Concurrent.GetAsBool(config.CfgDevModeKey, false)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ type (
|
||||||
RecordFunc func(ar *Request) (r record.Record, err error)
|
RecordFunc func(ar *Request) (r record.Record, err error)
|
||||||
)
|
)
|
||||||
|
|
||||||
// MIME Types
|
// MIME Types.
|
||||||
const (
|
const (
|
||||||
MimeTypeJSON string = "application/json"
|
MimeTypeJSON string = "application/json"
|
||||||
MimeTypeText string = "text/plain"
|
MimeTypeText string = "text/plain"
|
||||||
|
@ -256,6 +256,9 @@ func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
apiRequest.InputData = inputData
|
apiRequest.InputData = inputData
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// Nothing special to do here.
|
// Nothing special to do here.
|
||||||
|
case http.MethodOptions:
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Unsupported method for the actions API.", http.StatusMethodNotAllowed)
|
http.Error(w, "Unsupported method for the actions API.", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
@ -308,7 +311,6 @@ 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 {
|
||||||
|
|
|
@ -17,7 +17,7 @@ var (
|
||||||
exportEndpoints bool
|
exportEndpoints bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// API Errors
|
// API Errors.
|
||||||
var (
|
var (
|
||||||
ErrAuthenticationAlreadySet = errors.New("the authentication function has already been set")
|
ErrAuthenticationAlreadySet = errors.New("the authentication function has already been set")
|
||||||
ErrAuthenticationImmutable = errors.New("the authentication function can only be set before the api has started")
|
ErrAuthenticationImmutable = errors.New("the authentication function can only be set before the api has started")
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Middleware is a function that can be added as a middleware to the API endpoint.
|
|
||||||
type Middleware func(next http.Handler) http.Handler
|
|
||||||
|
|
||||||
type mwHandler struct {
|
|
||||||
handlers []Middleware
|
|
||||||
final http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mwh *mwHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
handlerLock.RLock()
|
|
||||||
defer handlerLock.RUnlock()
|
|
||||||
|
|
||||||
// final handler
|
|
||||||
handler := mwh.final
|
|
||||||
|
|
||||||
// build middleware chain
|
|
||||||
// loop in reverse to build the handler chain in the correct order
|
|
||||||
for i := len(mwh.handlers) - 1; i >= 0; i-- {
|
|
||||||
handler = mwh.handlers[i](handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModuleWorker is an http middleware that wraps the request in a module worker.
|
|
||||||
func ModuleWorker(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_ = module.RunWorker("http request", func(_ context.Context) error {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogTracer is an http middleware that attaches a log tracer to the request context.
|
|
||||||
func LogTracer(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, tracer := log.AddTracer(r.Context())
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
tracer.Submit()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -16,17 +16,6 @@ var (
|
||||||
// gorilla mux
|
// gorilla mux
|
||||||
mainMux = mux.NewRouter()
|
mainMux = mux.NewRouter()
|
||||||
|
|
||||||
// middlewares
|
|
||||||
middlewareHandler = &mwHandler{
|
|
||||||
final: mainMux,
|
|
||||||
handlers: []Middleware{
|
|
||||||
ModuleWorker,
|
|
||||||
LogTracer,
|
|
||||||
RequestLogger,
|
|
||||||
authMiddleware,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// main server and lock
|
// main server and lock
|
||||||
server = &http.Server{}
|
server = &http.Server{}
|
||||||
handlerLock sync.RWMutex
|
handlerLock sync.RWMutex
|
||||||
|
@ -46,18 +35,12 @@ func RegisterHandleFunc(path string, handleFunc func(http.ResponseWriter, *http.
|
||||||
return mainMux.HandleFunc(path, handleFunc)
|
return mainMux.HandleFunc(path, handleFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterMiddleware registers a middle function with the API endoint.
|
|
||||||
func RegisterMiddleware(middleware Middleware) {
|
|
||||||
handlerLock.Lock()
|
|
||||||
defer handlerLock.Unlock()
|
|
||||||
middlewareHandler.handlers = append(middlewareHandler.handlers, middleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve starts serving the API endpoint.
|
// Serve starts serving the API endpoint.
|
||||||
func Serve() {
|
func Serve() {
|
||||||
// configure server
|
// configure server
|
||||||
server.Addr = listenAddressConfig()
|
server.Addr = listenAddressConfig()
|
||||||
server.Handler = &mainHandler{
|
server.Handler = &mainHandler{
|
||||||
|
// TODO: mainMux should not be modified anymore.
|
||||||
mux: mainMux,
|
mux: mainMux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +117,22 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add security headers.
|
||||||
|
if !devMode() {
|
||||||
|
w.Header().Set(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
"default-src 'self'; "+
|
||||||
|
"style-src 'self' 'unsafe-inline'; "+
|
||||||
|
"img-src 'self' data:",
|
||||||
|
)
|
||||||
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Set("X-Frame-Options", "deny")
|
||||||
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
}
|
||||||
|
|
||||||
// Handle request.
|
// Handle request.
|
||||||
switch {
|
switch {
|
||||||
case handler != nil:
|
case handler != nil:
|
||||||
|
|
39
config/devmode.go
Normal file
39
config/devmode.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Keys.
|
||||||
|
var (
|
||||||
|
CfgDevModeKey = "core/devMode"
|
||||||
|
defaultDevMode bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&defaultDevMode, "devmode", false, "enable development mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logDevModeOverride() {
|
||||||
|
if defaultDevMode {
|
||||||
|
log.Warning("config: development mode is enabled by default by the -devmode flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDevModeOption() error {
|
||||||
|
return Register(&Option{
|
||||||
|
Name: "Development Mode",
|
||||||
|
Key: CfgDevModeKey,
|
||||||
|
Description: "In Development Mode, security restrictions are lifted/softened to enable unrestricted access for debugging and testing purposes.",
|
||||||
|
OptType: OptTypeBool,
|
||||||
|
ExpertiseLevel: ExpertiseLevelDeveloper,
|
||||||
|
ReleaseLevel: ReleaseLevelStable,
|
||||||
|
DefaultValue: defaultDevMode,
|
||||||
|
Annotations: Annotations{
|
||||||
|
DisplayOrderAnnotation: 512,
|
||||||
|
CategoryAnnotation: "Development",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import (
|
||||||
// to change deep configuration settings.
|
// to change deep configuration settings.
|
||||||
type ExpertiseLevel uint8
|
type ExpertiseLevel uint8
|
||||||
|
|
||||||
// Expertise Level constants
|
// Expertise Level constants.
|
||||||
const (
|
const (
|
||||||
ExpertiseLevelUser ExpertiseLevel = 0
|
ExpertiseLevelUser ExpertiseLevel = 0
|
||||||
ExpertiseLevelExpert ExpertiseLevel = 1
|
ExpertiseLevelExpert ExpertiseLevel = 1
|
||||||
|
|
|
@ -47,6 +47,12 @@ func prep() error {
|
||||||
modules.SetCmdLineOperation(exportConfigCmd)
|
modules.SetCmdLineOperation(exportConfigCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logDevModeOverride()
|
||||||
|
err := registerDevModeOption()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ type PossibleValue struct {
|
||||||
// future well-known annotation additions do not conflict
|
// future well-known annotation additions do not conflict
|
||||||
// with vendor/product/package specific annoations.
|
// with vendor/product/package specific annoations.
|
||||||
//
|
//
|
||||||
// Format: <vendor/package>:<scope>:<identifier>
|
// Format: <vendor/package>:<scope>:<identifier> //.
|
||||||
type Annotations map[string]interface{}
|
type Annotations map[string]interface{}
|
||||||
|
|
||||||
// Well known annotations defined by this package.
|
// Well known annotations defined by this package.
|
||||||
|
@ -144,7 +144,7 @@ type ValueRequirement struct {
|
||||||
Value interface{}
|
Value interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Values for the DisplayHintAnnotation
|
// Values for the DisplayHintAnnotation.
|
||||||
const (
|
const (
|
||||||
// DisplayHintOneOf is used to mark an option
|
// DisplayHintOneOf is used to mark an option
|
||||||
// as a "select"-style option. That is, only one of
|
// as a "select"-style option. That is, only one of
|
||||||
|
@ -263,7 +263,7 @@ func (option *Option) SetAnnotation(key string, value interface{}) {
|
||||||
option.Annotations[key] = value
|
option.Annotations[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnnotation returns the value of the annotation key
|
// GetAnnotation returns the value of the annotation key.
|
||||||
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
|
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
|
||||||
option.Lock()
|
option.Lock()
|
||||||
defer option.Unlock()
|
defer option.Unlock()
|
||||||
|
|
Loading…
Add table
Reference in a new issue