@marcel91200 / caddy Mirror · read-only / commits / a379fa4c
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 16:45 UTC

reverseproxy: Implement read & write timeouts for HTTP transport (#4905)

Matt Holt committed Jul 23, 2022 at 22:38 UTC a379fa4c6c5d58aa6b812bb21a879ce0944c1ccb
2 files changed +72 -2
modules/caddyhttp/reverseproxy/caddyfile.go
+20
@@ -875,6 +875,26 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
875 }
876 h.WriteBufferSize = int(size)
877
878 + case "read_timeout":
879 + if !d.NextArg() {
880 + return d.ArgErr()
881 + }
882 + timeout, err := caddy.ParseDuration(d.Val())
883 + if err != nil {
884 + return d.Errf("invalid read timeout duration '%s': %v", d.Val(), err)
885 + }
886 + h.ReadTimeout = caddy.Duration(timeout)
887 +
888 + case "write_timeout":
889 + if !d.NextArg() {
890 + return d.ArgErr()
891 + }
892 + timeout, err := caddy.ParseDuration(d.Val())
893 + if err != nil {
894 + return d.Errf("invalid write timeout duration '%s': %v", d.Val(), err)
895 + }
896 + h.WriteTimeout = caddy.Duration(timeout)
897 +
898 case "max_response_header":
899 if !d.NextArg() {
900 return d.ArgErr()
modules/caddyhttp/reverseproxy/httptransport.go
+52 -2
@@ -30,6 +30,7 @@ import (
30
31 "github.com/caddyserver/caddy/v2"
32 "github.com/caddyserver/caddy/v2/modules/caddytls"
33 + "go.uber.org/zap"
34 "golang.org/x/net/http2"
35 )
36
@@ -88,6 +89,12 @@ type HTTPTransport struct {
89 // The size of the read buffer in bytes. Default: `4KiB`.
90 ReadBufferSize int `json:"read_buffer_size,omitempty"`
91
92 + // The maximum time to wait for next read from backend. Default: no timeout.
93 + ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
94 +
95 + // The maximum time to wait for next write to backend. Default: no timeout.
96 + WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
97 +
98 // The versions of HTTP to support. As a special case, "h2c"
99 // can be specified to use H2C (HTTP/2 over Cleartext) to the
100 // upstream (this feature is experimental and subject to
@@ -147,7 +154,7 @@ func (h *HTTPTransport) Provision(ctx caddy.Context) error {
154 }
155
156 // NewTransport builds a standard-lib-compatible http.Transport value from h.
150 -func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error) {
157 +func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, error) {
158 // Set keep-alive defaults if it wasn't otherwise configured
159 if h.KeepAlive == nil {
160 h.KeepAlive = &KeepAlive{
@@ -194,6 +201,7 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
201 network = dialInfo.Network
202 address = dialInfo.Address
203 }
204 +
205 conn, err := dialer.DialContext(ctx, network, address)
206 if err != nil {
207 // identify this error as one that occurred during
@@ -201,6 +209,18 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
209 // decide whether to retry a request
210 return nil, DialError{err}
211 }
212 +
213 + // if read/write timeouts are configured and this is a TCP connection, enforce the timeouts
214 + // by wrapping the connection with our own type
215 + if tcpConn, ok := conn.(*net.TCPConn); ok && (h.ReadTimeout > 0 || h.WriteTimeout > 0) {
216 + conn = &tcpRWTimeoutConn{
217 + TCPConn: tcpConn,
218 + readTimeout: time.Duration(h.ReadTimeout),
219 + writeTimeout: time.Duration(h.WriteTimeout),
220 + logger: caddyCtx.Logger(h),
221 + }
222 + }
223 +
224 return conn, nil
225 },
226 MaxConnsPerHost: h.MaxConnsPerHost,
@@ -214,7 +234,7 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
234 if h.TLS != nil {
235 rt.TLSHandshakeTimeout = time.Duration(h.TLS.HandshakeTimeout)
236 var err error
217 - rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(ctx)
237 + rt.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx)
238 if err != nil {
239 return nil, fmt.Errorf("making TLS client config: %v", err)
240 }
@@ -510,6 +530,36 @@ type KeepAlive struct {
530 IdleConnTimeout caddy.Duration `json:"idle_timeout,omitempty"`
531 }
532
533 +// tcpRWTimeoutConn enforces read/write timeouts for a TCP connection.
534 +// If it fails to set deadlines, the error is logged but does not abort
535 +// the read/write attempt (ignoring the error is consistent with what
536 +// the standard library does: https://github.com/golang/go/blob/c5da4fb7ac5cb7434b41fc9a1df3bee66c7f1a4d/src/net/http/server.go#L981-L986)
537 +type tcpRWTimeoutConn struct {
538 + *net.TCPConn
539 + readTimeout, writeTimeout time.Duration
540 + logger *zap.Logger
541 +}
542 +
543 +func (c *tcpRWTimeoutConn) Read(b []byte) (int, error) {
544 + if c.readTimeout > 0 {
545 + err := c.TCPConn.SetReadDeadline(time.Now().Add(c.readTimeout))
546 + if err != nil {
547 + c.logger.Error("failed to set read deadline", zap.Error(err))
548 + }
549 + }
550 + return c.TCPConn.Read(b)
551 +}
552 +
553 +func (c *tcpRWTimeoutConn) Write(b []byte) (int, error) {
554 + if c.writeTimeout > 0 {
555 + err := c.TCPConn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
556 + if err != nil {
557 + c.logger.Error("failed to set write deadline", zap.Error(err))
558 + }
559 + }
560 + return c.TCPConn.Write(b)
561 +}
562 +
563 // decodeBase64DERCert base64-decodes, then DER-decodes, certStr.
564 func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
565 // decode base64