| @@ -5,7 +5,6 @@ import ( | |||||
| "flag" | "flag" | ||||
| "fmt" | "fmt" | ||||
| "log" | "log" | ||||
| "net/http" | |||||
| "os" | "os" | ||||
| "os/signal" | "os/signal" | ||||
| "syscall" | "syscall" | ||||
| @@ -109,8 +108,9 @@ func main() { | |||||
| // --- default: HTTP only --- | // --- default: HTTP only --- | ||||
| srv := ctrlpkg.NewServer(cfg) | srv := ctrlpkg.NewServer(cfg) | ||||
| log.Printf("fm-rds-tx listening on %s (TX default: off, use --tx for hardware)", cfg.Control.ListenAddress) | |||||
| log.Fatal(http.ListenAndServe(cfg.Control.ListenAddress, srv.Handler())) | |||||
| server := ctrlpkg.NewHTTPServer(cfg, srv.Handler()) | |||||
| log.Printf("fm-rds-tx listening on %s (TX default: off, use --tx for hardware)", server.Addr) | |||||
| log.Fatal(server.ListenAndServe()) | |||||
| } | } | ||||
| // selectDriver picks the best available driver based on config and build tags. | // selectDriver picks the best available driver based on config and build tags. | ||||
| @@ -228,9 +228,10 @@ func runTXMode(cfg cfgpkg.Config, driver platform.SoapyDriver, autoStart bool, a | |||||
| log.Println("TX ready (idle) — POST /tx/start to begin") | log.Println("TX ready (idle) — POST /tx/start to begin") | ||||
| } | } | ||||
| ctrlServer := ctrlpkg.NewHTTPServer(cfg, srv.Handler()) | |||||
| go func() { | go func() { | ||||
| log.Printf("control plane on %s", cfg.Control.ListenAddress) | |||||
| if err := http.ListenAndServe(cfg.Control.ListenAddress, srv.Handler()); err != nil { | |||||
| log.Printf("control plane on %s (read=%s write=%s idle=%s)", ctrlServer.Addr, ctrlServer.ReadTimeout, ctrlServer.WriteTimeout, ctrlServer.IdleTimeout) | |||||
| if err := ctrlServer.ListenAndServe(); err != nil { | |||||
| log.Printf("http: %v", err) | log.Printf("http: %v", err) | ||||
| } | } | ||||
| }() | }() | ||||
| @@ -87,6 +87,8 @@ All major TX parameters are hot-reloadable via `POST /config` during live transm | |||||
| Available endpoints: `/healthz`, `/status`, `/runtime`, `/config` (GET/POST), `/dry-run`, `/tx/start`, `/tx/stop` | Available endpoints: `/healthz`, `/status`, `/runtime`, `/config` (GET/POST), `/dry-run`, `/tx/start`, `/tx/stop` | ||||
| Control-plane HTTP server is configured with 5s read, 10s write, and 60s idle timeouts plus a 1 MiB header limit to reduce slow-client abuse. | |||||
| ### Internal DSP module | ### Internal DSP module | ||||
| - `cd internal` | - `cd internal` | ||||
| - `go test ./...` | - `go test ./...` | ||||
| @@ -0,0 +1,27 @@ | |||||
| package control | |||||
| import ( | |||||
| "net/http" | |||||
| "time" | |||||
| "github.com/jan/fm-rds-tx/internal/config" | |||||
| ) | |||||
| const ( | |||||
| defaultReadTimeout = 5 * time.Second | |||||
| defaultWriteTimeout = 10 * time.Second | |||||
| defaultIdleTimeout = 60 * time.Second | |||||
| defaultMaxHeaderBytes = 1 << 20 // 1 MiB | |||||
| ) | |||||
| // NewHTTPServer returns a configured HTTP server for the control plane. | |||||
| func NewHTTPServer(cfg config.Config, handler http.Handler) *http.Server { | |||||
| return &http.Server{ | |||||
| Addr: cfg.Control.ListenAddress, | |||||
| Handler: handler, | |||||
| ReadTimeout: defaultReadTimeout, | |||||
| WriteTimeout: defaultWriteTimeout, | |||||
| IdleTimeout: defaultIdleTimeout, | |||||
| MaxHeaderBytes: defaultMaxHeaderBytes, | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,33 @@ | |||||
| package control | |||||
| import ( | |||||
| "net/http" | |||||
| "testing" | |||||
| cfgpkg "github.com/jan/fm-rds-tx/internal/config" | |||||
| ) | |||||
| func TestNewHTTPServerConfig(t *testing.T) { | |||||
| cfg := cfgpkg.Default() | |||||
| handler := http.NewServeMux() | |||||
| srv := NewHTTPServer(cfg, handler) | |||||
| if srv.Addr != cfg.Control.ListenAddress { | |||||
| t.Fatalf("expected server address %q, got %q", cfg.Control.ListenAddress, srv.Addr) | |||||
| } | |||||
| if srv.Handler != handler { | |||||
| t.Fatalf("expected handler to be preserved") | |||||
| } | |||||
| if srv.ReadTimeout != defaultReadTimeout { | |||||
| t.Fatalf("expected read timeout %s, got %s", defaultReadTimeout, srv.ReadTimeout) | |||||
| } | |||||
| if srv.WriteTimeout != defaultWriteTimeout { | |||||
| t.Fatalf("expected write timeout %s, got %s", defaultWriteTimeout, srv.WriteTimeout) | |||||
| } | |||||
| if srv.IdleTimeout != defaultIdleTimeout { | |||||
| t.Fatalf("expected idle timeout %s, got %s", defaultIdleTimeout, srv.IdleTimeout) | |||||
| } | |||||
| if srv.MaxHeaderBytes != defaultMaxHeaderBytes { | |||||
| t.Fatalf("expected max header bytes %d, got %d", defaultMaxHeaderBytes, srv.MaxHeaderBytes) | |||||
| } | |||||
| } | |||||