Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a81bbb2bcf |
@@ -2,16 +2,46 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isLoopback reports whether the address binds to a loopback interface
|
||||||
|
// (127.0.0.1, ::1, localhost). REQ-123: pprof must be loopback-only.
|
||||||
|
func isLoopback(addr string) bool {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
host = addr
|
||||||
|
}
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
if host == "" || host == "localhost" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip != nil {
|
||||||
|
return ip.IsLoopback()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func StartPprof(addr string, log *slog.Logger) (*http.Server, error) {
|
func StartPprof(addr string, log *slog.Logger) (*http.Server, error) {
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
// REQ-123: pprof must bind to loopback only. Non-loopback addresses
|
||||||
|
// require explicit --pprof-allow-public confirmation (which the CLI
|
||||||
|
// passes after a warning). We refuse non-loopback here by default.
|
||||||
|
if !isLoopback(addr) {
|
||||||
|
log.Error("pprof refuses non-loopback bind",
|
||||||
|
slog.String("addr", addr),
|
||||||
|
slog.String("reason", "REQ-123: pprof is unauthenticated; use --pprof-allow-public to override (operator-only)"))
|
||||||
|
return nil, fmt.Errorf("pprof: refusing non-loopback bind %s (REQ-123; unauthenticated; use --pprof-allow-public)", addr)
|
||||||
|
}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||||
|
|||||||
@@ -261,3 +261,28 @@ func TestServer_WithPprof(t *testing.T) {
|
|||||||
t.Error("expected main GET to fail after Shutdown")
|
t.Error("expected main GET to fail after Shutdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- REQ-123 pprof loopback-only test ---
|
||||||
|
|
||||||
|
// TestStartPprof_NonLoopbackRefused verifies pprof refuses non-loopback.
|
||||||
|
func TestStartPprof_NonLoopbackRefused(t *testing.T) {
|
||||||
|
_, err := StartPprof("0.0.0.0:6060", slog.Default())
|
||||||
|
if err == nil {
|
||||||
|
t.Error("StartPprof on 0.0.0.0 should be refused (REQ-123)")
|
||||||
|
}
|
||||||
|
_, err = StartPprof("10.0.0.1:6060", slog.Default())
|
||||||
|
if err == nil {
|
||||||
|
t.Error("StartPprof on 10.0.0.1 should be refused (REQ-123)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStartPprof_LoopbackAccepted verifies loopback addresses are accepted.
|
||||||
|
func TestStartPprof_LoopbackAccepted(t *testing.T) {
|
||||||
|
srv, err := StartPprof("127.0.0.1:0", slog.Default())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StartPprof on 127.0.0.1 should be accepted: %v", err)
|
||||||
|
}
|
||||||
|
if srv != nil {
|
||||||
|
srv.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -64,6 +65,19 @@ type Options struct {
|
|||||||
PprofAddr string
|
PprofAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxBodyBytes is the limit for request bodies on JSON-decoding
|
||||||
|
// endpoints (REQ-124, F24). 1 MiB is generous for orca API calls.
|
||||||
|
const maxBodyBytes int64 = 1 << 20
|
||||||
|
|
||||||
|
// bodyLimitMiddleware wraps the handler with a MaxBytesReader so
|
||||||
|
// oversized request bodies are rejected before decoding (REQ-124).
|
||||||
|
func bodyLimitMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer constructs a Server with the default mux and route table.
|
// NewServer constructs a Server with the default mux and route table.
|
||||||
func NewServer(opts Options) *Server {
|
func NewServer(opts Options) *Server {
|
||||||
if opts.Log == nil {
|
if opts.Log == nil {
|
||||||
@@ -132,7 +146,7 @@ func (s *Server) mux() http.Handler {
|
|||||||
if s.dispatch != nil {
|
if s.dispatch != nil {
|
||||||
s.dispatch.Mount(mux)
|
s.dispatch.Mount(mux)
|
||||||
}
|
}
|
||||||
return loggingMiddleware(s.log, mux)
|
return bodyLimitMiddleware(loggingMiddleware(s.log, mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterDispatch attaches the orca.v1.Dispatch service to the
|
// RegisterDispatch attaches the orca.v1.Dispatch service to the
|
||||||
@@ -151,8 +165,16 @@ func (s *Server) RegisterDispatch(h *DispatchHandlers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start runs the HTTP server. Returns http.ErrServerClosed on clean shutdown.
|
// Start runs the HTTP server. Returns http.ErrServerClosed on clean shutdown.
|
||||||
|
// R-021 / REQ-123: the daemon MUST run in mTLS mode (no plaintext).
|
||||||
|
// If StartMTLS has not been called, Start refuses to run.
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
s.log.Info("daemon starting",
|
if s.mtls == nil {
|
||||||
|
s.log.Error("daemon refuses to start in plaintext mode",
|
||||||
|
slog.String("component", "daemon"),
|
||||||
|
slog.String("reason", "mTLS is required (R-021, REQ-123); call StartMTLS first"))
|
||||||
|
return fmt.Errorf("daemon: mTLS is required (R-021, REQ-123); refusing to start in plaintext mode")
|
||||||
|
}
|
||||||
|
s.log.Info("daemon starting (mTLS required)",
|
||||||
slog.String("addr", s.addr),
|
slog.String("addr", s.addr),
|
||||||
slog.String("component", "daemon"))
|
slog.String("component", "daemon"))
|
||||||
return s.httpServer.ListenAndServe()
|
return s.httpServer.ListenAndServe()
|
||||||
|
|||||||
Reference in New Issue
Block a user