server: ReadHeaderTimeout on the broker/home/push listeners + the signin loopback (#940)
Closes the class PR #903 started: the three sibling services' shared serve() shape called bare http.ListenAndServe on the TCP branch — the documented container-platform entrypoint — with no header timeout, and the unix-socket branch likewise. All three now build one http.Server with ReadHeaderTimeout: 10s (same as main.go's listeners) and route both branches through it. The lchat signin loopback receiver gets the same cap for uniformity (loopback-only + self-terminating, so low severity there). Found by the Aikido feed triage; verified against the full repo sweep — every other listener (main.go, router, cloud, controld, trycmd) already carries the timeout.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4 files changed,
+24
−7
broker/main.go+7 −2eleven/signin.go+3 −1home/main.go+7 −2push/main.go+7 −2
diff --git a/broker/main.go b/broker/main.go| index cf4df48..3b224ba 100644 |
| --- a/broker/main.go |
| +++ b/broker/main.go |
| @@ -158,8 +158,13 @@ func loadOAuthApps() map[string]*oauthApp { |
| // serve listens on addr: a "/"-prefixed addr is a unix socket (how the router |
| // fronts this service); otherwise a TCP address. Mirrors the push relay. |
| func serve(addr string, h http.Handler) error { |
| + // ReadHeaderTimeout caps how long a client may dribble request headers |
| + // (Slowloris): the TCP branch binds straight to public TCP on a container |
| + // platform. Same 10s the messenger's own listeners use (main.go). |
| + srv := &http.Server{Handler: h, ReadHeaderTimeout: 10 * time.Second} |
| if !strings.HasPrefix(addr, "/") { |
| - return http.ListenAndServe(addr, h) |
| + srv.Addr = addr |
| + return srv.ListenAndServe() |
| } |
| if c, err := net.DialTimeout("unix", addr, 200*time.Millisecond); err == nil { |
| c.Close() |
| @@ -176,7 +181,7 @@ func serve(addr string, h http.Handler) error { |
| if err := os.Chmod(addr, 0o600); err != nil { |
| return fmt.Errorf("chmod socket %s: %w", addr, err) |
| } |
| - return http.Serve(ln, h) |
| + return srv.Serve(ln) |
| } |
| func (s *Server) handler() http.Handler { |
diff --git a/eleven/signin.go b/eleven/signin.go| index 6f90bf1..109fed5 100644 |
| --- a/eleven/signin.go |
| +++ b/eleven/signin.go |
| @@ -174,7 +174,9 @@ func startLoopback() (*loopback, error) { |
| default: // a second hit after success — already handled |
| } |
| }) |
| - lb.srv = &http.Server{Handler: mux} |
| + // Loopback-only and torn down after one hit, but cap header reads anyway — |
| + // same posture as every other listener (main.go). |
| + lb.srv = &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second} |
| go lb.srv.Serve(ln) |
| return lb, nil |
| } |
diff --git a/home/main.go b/home/main.go| index 1157fd8..89466b6 100644 |
| --- a/home/main.go |
| +++ b/home/main.go |
| @@ -150,8 +150,13 @@ func openStore(dbPath string) (vaultStore, string, error) { |
| // it clobbers only a stale socket (nothing listening) and locks it to 0600, |
| // mirroring the main binary's listen(). |
| func serve(addr string, h http.Handler) error { |
| + // ReadHeaderTimeout caps how long a client may dribble request headers |
| + // (Slowloris): the TCP branch binds straight to public TCP on a container |
| + // platform. Same 10s the messenger's own listeners use (main.go). |
| + srv := &http.Server{Handler: h, ReadHeaderTimeout: 10 * time.Second} |
| if !strings.HasPrefix(addr, "/") { |
| - return http.ListenAndServe(addr, h) |
| + srv.Addr = addr |
| + return srv.ListenAndServe() |
| } |
| if c, err := net.DialTimeout("unix", addr, 200*time.Millisecond); err == nil { |
| c.Close() |
| @@ -168,7 +173,7 @@ func serve(addr string, h http.Handler) error { |
| if err := os.Chmod(addr, 0o600); err != nil { |
| return fmt.Errorf("chmod socket %s: %w", addr, err) |
| } |
| - return http.Serve(ln, h) |
| + return srv.Serve(ln) |
| } |
| // newServer builds the server. stateSecret keys the stateless ceremony/session |
diff --git a/push/main.go b/push/main.go| index 0382ca2..209b0bf 100644 |
| --- a/push/main.go |
| +++ b/push/main.go |
| @@ -97,8 +97,13 @@ func main() { |
| // it clobbers only a stale socket (nothing listening) and locks it to 0600, |
| // mirroring the main binary's listen(). |
| func serve(addr string, h http.Handler) error { |
| + // ReadHeaderTimeout caps how long a client may dribble request headers |
| + // (Slowloris): the TCP branch binds straight to public TCP on a container |
| + // platform. Same 10s the messenger's own listeners use (main.go). |
| + srv := &http.Server{Handler: h, ReadHeaderTimeout: 10 * time.Second} |
| if !strings.HasPrefix(addr, "/") { |
| - return http.ListenAndServe(addr, h) |
| + srv.Addr = addr |
| + return srv.ListenAndServe() |
| } |
| if c, err := net.DialTimeout("unix", addr, 200*time.Millisecond); err == nil { |
| c.Close() |
| @@ -115,7 +120,7 @@ func serve(addr string, h http.Handler) error { |
| if err := os.Chmod(addr, 0o600); err != nil { |
| return fmt.Errorf("chmod socket %s: %w", addr, err) |
| } |
| - return http.Serve(ln, h) |
| + return srv.Serve(ln) |
| } |
| // handler wires the routes. A method so tests exercise the real router via |