diff --git a/internal/server/http.go b/internal/server/http.go index cc904ec..bdd1238 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -9,24 +9,40 @@ import ( // HTTP server configuration constants. const ( - HTTPReadTimeout = 30 * time.Second - HTTPWriteTimeout = 60 * time.Second + HTTPReadTimeout = 30 * time.Second + // HTTPReadHeaderTimeout bounds the request-header read on its own, + // short, so a slowloris client dribbling headers is dropped well + // before it ties up a connection for the whole ReadTimeout window. + HTTPReadHeaderTimeout = 10 * time.Second + HTTPWriteTimeout = 60 * time.Second + // HTTPIdleTimeout bounds how long an idle keep-alive connection is + // held open, so idle connections cannot accumulate without limit on a + // service targeting high concurrency. + HTTPIdleTimeout = 120 * time.Second HTTPMaxHeaderBytes = 8 << 10 // 8KB ) -func (s *Server) serveUntilShutdown() { - listenAddr := fmt.Sprintf(":%d", s.config.Port) - s.httpServer = &http.Server{ - Addr: listenAddr, - ReadTimeout: HTTPReadTimeout, - WriteTimeout: HTTPWriteTimeout, - MaxHeaderBytes: HTTPMaxHeaderBytes, - Handler: s, +// newHTTPServer builds the http.Server with the hardening timeouts and +// limits applied. It is separate from serveUntilShutdown so the +// configuration can be asserted in a test without binding a listener. +func (s *Server) newHTTPServer() *http.Server { + return &http.Server{ + Addr: fmt.Sprintf(":%d", s.config.Port), + ReadTimeout: HTTPReadTimeout, + ReadHeaderTimeout: HTTPReadHeaderTimeout, + WriteTimeout: HTTPWriteTimeout, + IdleTimeout: HTTPIdleTimeout, + MaxHeaderBytes: HTTPMaxHeaderBytes, + Handler: s, } +} + +func (s *Server) serveUntilShutdown() { + s.httpServer = s.newHTTPServer() s.SetupRoutes() - s.log.Info("http begin listen", "listenaddr", listenAddr) + s.log.Info("http begin listen", "listenaddr", s.httpServer.Addr) err := s.httpServer.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) {