nomadcode/services/core/internal/http/middleware.go
toki e36db7281b feat: update project rules, roadmap, and core service changes
- Update agent-ops project rules and roadmap files
- Add proto-socket infrastructure communication rail milestone
- Update Flutter pubspec.lock and contracts notes
- Enhance core service: config, HTTP middleware, router
- Add notification module improvements
- Add protosocket internal package
2026-05-30 19:32:48 +09:00

79 lines
2.1 KiB
Go

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
}