package http import ( "bufio" "crypto/subtle" "errors" "log/slog" "net" stdhttp "net/http" "time" ) type statusRecorder struct { stdhttp.ResponseWriter status int } func (r *statusRecorder) WriteHeader(status int) { r.status = status r.ResponseWriter.WriteHeader(status) } // Hijack lets WebSocket upgrades (e.g. the proto-socket route) take over the // connection even though the response writer is wrapped for status logging. func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker, ok := r.ResponseWriter.(stdhttp.Hijacker) if !ok { return nil, nil, errors.New("underlying ResponseWriter does not implement http.Hijacker") } return hijacker.Hijack() } func loggingMiddleware(logger *slog.Logger) func(stdhttp.Handler) stdhttp.Handler { return func(next stdhttp.Handler) stdhttp.Handler { return stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { start := time.Now() recorder := &statusRecorder{ResponseWriter: w, status: stdhttp.StatusOK} next.ServeHTTP(recorder, r) if logger != nil { logger.Info( "http request", "method", r.Method, "path", r.URL.Path, "status", recorder.status, "duration", time.Since(start).String(), ) } }) } } func basicAuthMiddleware(auth AuthConfig) func(stdhttp.Handler) stdhttp.Handler { if auth.Password == "" { return func(next stdhttp.Handler) stdhttp.Handler { return next } } if auth.Username == "" { auth.Username = "nomadcode" } return func(next stdhttp.Handler) stdhttp.Handler { return stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { username, password, ok := r.BasicAuth() if !ok || !secureCompare(username, auth.Username) || !secureCompare(password, auth.Password) { w.Header().Set("WWW-Authenticate", `Basic realm="nomadcode-core", charset="UTF-8"`) writeError(w, stdhttp.StatusUnauthorized, "authentication required") return } next.ServeHTTP(w, r) }) } } func secureCompare(got, want string) bool { return subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1 }