diff --git a/docs/doc.md b/docs/doc.md index d1c33b87..8bb2f0b5 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -2091,6 +2091,18 @@ See a complete example in the `https://github.com/gin-gonic/examples/tree/master > Configure HTTP servers, TLS, proxies, and runtime settings. +### Default Read header timeout + +You can change the default read header timeout by `engine.ReadHeaderTimeout` if its necessary. + +```go +func main() { + router := gin.Default() + router.ReadHeaderTimeout = 5 * time.Second + http.ListenAndServe(":8000", router) +} +``` + ### Custom HTTP configuration Use `http.ListenAndServe()` directly, like this: diff --git a/gin.go b/gin.go index 2e033bf3..c08cd929 100644 --- a/gin.go +++ b/gin.go @@ -13,6 +13,7 @@ import ( "path" "strings" "sync" + "time" "github.com/gin-gonic/gin/internal/bytesconv" filesystem "github.com/gin-gonic/gin/internal/fs" @@ -24,6 +25,7 @@ import ( const ( defaultMultipartMemory = 32 << 20 // 32 MB + defaultReadHeaderTimeout = 10 * time.Second escapedColon = "\\:" colon = ":" backslash = "\\" @@ -114,6 +116,17 @@ type Engine struct { // RedirectTrailingSlash is independent of this option. RedirectFixedPath bool + // ReadHeaderTimeout is the maximum duration for reading the entire + // request header, including the body. A zero or negative value means + // there will be no timeout. + // + // This setting helps protect against Slowloris attacks by limiting + // the time a client can take to send headers. If the timeout expires + // before the full header is received, the server closes the connection. + // It corresponds directly to http.Server.ReadHeaderTimeout in the + // standard library. + ReadHeaderTimeout time.Duration + // HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the // current route, if the current request can not be routed. // If this is the case, the request is answered with 'Method Not Allowed' @@ -210,6 +223,7 @@ func New(opts ...OptionFunc) *Engine { FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, + ReadHeaderTimeout: defaultReadHeaderTimeout, HandleMethodNotAllowed: false, ForwardedByClientIP: true, RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, @@ -550,6 +564,7 @@ func (engine *Engine) Run(addr ...string) (err error) { server := &http.Server{ // #nosec G112 Addr: address, Handler: engine.Handler(), + ReadHeaderTimeout: engine.ReadHeaderTimeout, } err = server.ListenAndServe() return diff --git a/gin_integration_test.go b/gin_integration_test.go index 720b140f..bf2173e8 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -605,3 +605,56 @@ func TestEscapedColon(t *testing.T) { testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r") testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r") } + +// Tests the ReadHeaderTimeout fail +func TestEngineReadHeaderTimeout(t *testing.T) { + const timeout = 200 * time.Millisecond + + router := New() + router.ReadHeaderTimeout = timeout + router.GET("/test", func(c *Context) { + c.String(http.StatusOK, "ok") + }) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + go func() { + _ = http.Serve(ln, router.Handler()) + }() + + conn, err := net.Dial("tcp", ln.Addr().String()) + require.NoError(t, err) + defer conn.Close() + + _, err = conn.Write([]byte("GET /test HTTP/1.1\r\nHost: localhost\r\n")) + require.NoError(t, err) + + conn.SetReadDeadline(time.Now().Add(timeout + 500*time.Millisecond)) + + buf := make([]byte, 1024) + _, err = conn.Read(buf) + + assert.Error(t, err, "expected connection to be closed by server due to ReadHeaderTimeout") + + if err == nil { + t.Fatalf("expected error but got response: %s", string(buf)) + } +} + +// Tests the ReadHeaderTimeout success case +func TestEngineReadHeaderTimeoutSuccess(t *testing.T) { + router := New() + router.ReadHeaderTimeout = 5 * time.Second + router.GET("/test", func(c *Context) { + c.String(http.StatusOK, "success") + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/test", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "success", w.Body.String()) +}