@@ -5,11 +5,13 @@ import (
55 "context"
66 "crypto/subtle"
77 "encoding/json"
8+ "fmt"
89 "io"
910 "net"
1011 "net/http"
1112 "strings"
1213 "sync"
14+ "time"
1315)
1416
1517const maxRequestBodyBytes = 1 << 20
@@ -74,9 +76,19 @@ func NewWithAPIKey(addr, apiKey string) *Server {
7476
7577// registerRoutes sets up the HTTP endpoints.
7678func (s * Server ) registerRoutes () {
77- s .mux .HandleFunc ("GET /health" , s .handleHealth )
78- s .mux .HandleFunc ("GET /version" , s .handleVersion )
79- s .mux .HandleFunc ("POST /chat" , s .auth (s .handleChat ))
79+ s .mux .HandleFunc ("GET /health" , securityHeaders (s .handleHealth ))
80+ s .mux .HandleFunc ("GET /version" , securityHeaders (s .handleVersion ))
81+ s .mux .HandleFunc ("POST /chat" , securityHeaders (s .auth (s .handleChat )))
82+ }
83+
84+ // securityHeaders sets standard HTTP security headers on every response.
85+ func securityHeaders (next http.HandlerFunc ) http.HandlerFunc {
86+ return func (w http.ResponseWriter , r * http.Request ) {
87+ w .Header ().Set ("X-Content-Type-Options" , "nosniff" )
88+ w .Header ().Set ("X-Frame-Options" , "DENY" )
89+ w .Header ().Set ("Cache-Control" , "no-store" )
90+ next (w , r )
91+ }
8092}
8193
8294func (s * Server ) auth (next http.HandlerFunc ) http.HandlerFunc {
@@ -110,6 +122,35 @@ func constantTimeEqual(a, b string) bool {
110122 return subtle .ConstantTimeCompare ([]byte (a ), []byte (b )) == 1
111123}
112124
125+ // validateAuthConfig refuses to start the server with no API key on a
126+ // non-loopback bind. The auth middleware silently allows every request when
127+ // the API key is empty, so a misconfigured server would be wide open. The
128+ // only safe no-key mode is loopback bind.
129+ func (s * Server ) validateAuthConfig () error {
130+ if s .apiKey != "" {
131+ return nil
132+ }
133+ host , _ , err := net .SplitHostPort (s .addr )
134+ if err != nil {
135+ return fmt .Errorf ("api: invalid bind address %q: %w" , s .addr , err )
136+ }
137+ if ! isLoopbackHost (host ) {
138+ return fmt .Errorf ("api: apiKey is empty and bind address %q is not loopback; refusing to start. Set apiKey or bind to 127.0.0.1" , s .addr )
139+ }
140+ return nil
141+ }
142+
143+ // isLoopbackHost reports whether host is a loopback address.
144+ func isLoopbackHost (host string ) bool {
145+ if host == "" || host == "localhost" {
146+ return host == "localhost" // "" is unsafe; "localhost" is loopback
147+ }
148+ if ip := net .ParseIP (host ); ip != nil {
149+ return ip .IsLoopback ()
150+ }
151+ return false
152+ }
153+
113154func decodeJSONBody (w http.ResponseWriter , r * http.Request , dst any ) bool {
114155 r .Body = http .MaxBytesReader (w , r .Body , maxRequestBodyBytes )
115156 dec := json .NewDecoder (r .Body )
@@ -127,10 +168,17 @@ func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst any) bool {
127168
128169// Start starts the HTTP server. It blocks until the context is cancelled or an error occurs.
129170func (s * Server ) Start (ctx context.Context ) error {
171+ if err := s .validateAuthConfig (); err != nil {
172+ return err
173+ }
130174 s .mu .Lock ()
131175 s .server = & http.Server {
132- Addr : s .addr ,
133- Handler : s .mux ,
176+ Addr : s .addr ,
177+ Handler : s .mux ,
178+ ReadHeaderTimeout : 10 * time .Second ,
179+ ReadTimeout : 30 * time .Second ,
180+ WriteTimeout : 30 * time .Second ,
181+ IdleTimeout : 120 * time .Second ,
134182 }
135183 s .mu .Unlock ()
136184
@@ -151,7 +199,7 @@ func (s *Server) Start(ctx context.Context) error {
151199 return err
152200}
153201
154- // Stop gracefully shuts down the HTTP server.
202+ // Stop gracefully shuts down the HTTP server with a 15-second timeout .
155203func (s * Server ) Stop (ctx context.Context ) error {
156204 s .mu .Lock ()
157205 srv := s .server
@@ -160,7 +208,12 @@ func (s *Server) Stop(ctx context.Context) error {
160208 if srv == nil {
161209 return nil
162210 }
163- return srv .Shutdown (ctx )
211+ // Use a bounded timeout so Stop cannot hang indefinitely if a
212+ // client keeps a connection open. The caller's ctx is respected
213+ // if it has a shorter deadline.
214+ shutdownCtx , cancel := context .WithTimeout (ctx , 15 * time .Second )
215+ defer cancel ()
216+ return srv .Shutdown (shutdownCtx )
164217}
165218
166219// Handler returns the underlying http.Handler for testing purposes.
0 commit comments