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