safing-portbase/api/router.go
2021-01-19 15:37:55 +01:00

148 lines
3.6 KiB
Go

package api
import (
"context"
"errors"
"net/http"
"sync"
"time"
"github.com/gorilla/mux"
"github.com/safing/portbase/log"
)
var (
// gorilla mux
mainMux = mux.NewRouter()
// middlewares
middlewareHandler = &mwHandler{
final: mainMux,
handlers: []Middleware{
ModuleWorker,
LogTracer,
RequestLogger,
authMiddleware,
},
}
// main server and lock
server = &http.Server{}
handlerLock sync.RWMutex
)
// RegisterHandler registers a handler with the API endoint.
func RegisterHandler(path string, handler http.Handler) *mux.Route {
handlerLock.Lock()
defer handlerLock.Unlock()
return mainMux.Handle(path, handler)
}
// RegisterHandleFunc registers a handle function with the API endoint.
func RegisterHandleFunc(path string, handleFunc func(http.ResponseWriter, *http.Request)) *mux.Route {
handlerLock.Lock()
defer handlerLock.Unlock()
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.
func Serve() {
// configure server
server.Addr = listenAddressConfig()
server.Handler = &mainHandler{
mux: mainMux,
}
// start serving
log.Infof("api: starting to listen on %s", server.Addr)
backoffDuration := 10 * time.Second
for {
// always returns an error
err := module.RunWorker("http endpoint", func(ctx context.Context) error {
return server.ListenAndServe()
})
// return on shutdown error
if err == http.ErrServerClosed {
return
}
// log error and restart
log.Errorf("api: http endpoint failed: %s - restarting in %s", err, backoffDuration)
time.Sleep(backoffDuration)
}
}
type mainHandler struct {
mux *mux.Router
}
func (mh *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_ = module.RunWorker("http request", func(_ context.Context) error {
return mh.handle(w, r)
})
}
func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
// Setup context trace logging.
ctx, tracer := log.AddTracer(r.Context())
// Add request context.
apiRequest := &Request{
Request: r,
}
ctx = context.WithValue(ctx, requestContextKey, apiRequest)
// Add context back to request.
r = r.WithContext(ctx)
lrw := NewLoggingResponseWriter(w, r)
tracer.Tracef("api request: %s ___ %s", r.RemoteAddr, r.RequestURI)
defer func() {
// Log request status.
if lrw.Status != 0 {
// If lrw.Status is 0, the request may have been hijacked.
tracer.Debugf("api request: %s %d %s", lrw.Request.RemoteAddr, lrw.Status, lrw.Request.RequestURI)
}
tracer.Submit()
}()
// Get handler for request.
// Gorilla does not support handling this on our own very well.
// See github.com/gorilla/mux.ServeHTTP for reference.
var match mux.RouteMatch
var handler http.Handler
if mh.mux.Match(r, &match) {
handler = match.Handler
apiRequest.Route = match.Route
apiRequest.URLVars = match.Vars
}
// Be sure that URLVars always is a map.
if apiRequest.URLVars == nil {
apiRequest.URLVars = make(map[string]string)
}
// Check authentication.
apiRequest.AuthToken = authenticateRequest(lrw, r, handler)
if apiRequest.AuthToken == nil {
// Authenticator already replied.
return nil
}
// Handle request.
switch {
case handler != nil:
handler.ServeHTTP(lrw, r)
case errors.Is(match.MatchErr, mux.ErrMethodMismatch):
http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
default: // handler == nil or other error
http.Error(lrw, "Not found.", http.StatusNotFound)
}
return nil
}