Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions caddyconfig/httpcaddyfile/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var defaultDirectiveOrder = []string{
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
"request_body",
"timeouts", // wraps the response writer, so keep it close to the real writer, ahead of encode/push/etc.

"redir",

Expand Down
57 changes: 57 additions & 0 deletions caddyconfig/httpcaddyfile/serveroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ type serverOptions struct {
PacketConnWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration
ReadIdleTimeout caddy.Duration
ReadMinRate int64
WriteTimeout caddy.Duration
WriteIdleTimeout caddy.Duration
WriteMinRate int64
MaxWriteChunk int
IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration
KeepAliveIdle caddy.Duration
Expand Down Expand Up @@ -137,6 +142,24 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.ReadTimeout = caddy.Duration(dur)

case "read_body_idle":
args := d.RemainingArgs()
if len(args) < 1 || len(args) > 2 {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(args[0])
if err != nil {
return nil, d.Errf("parsing read_body_idle timeout duration: %v", err)
}
serverOpts.ReadIdleTimeout = caddy.Duration(dur)
if len(args) == 2 {
rate, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
return nil, d.Errf("parsing read_body_idle min_rate bytes/second: %v", err)
}
serverOpts.ReadMinRate = rate
}

case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
Expand All @@ -157,6 +180,35 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.WriteTimeout = caddy.Duration(dur)

case "write_idle":
args := d.RemainingArgs()
if len(args) < 1 || len(args) > 2 {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(args[0])
if err != nil {
return nil, d.Errf("parsing write_idle timeout duration: %v", err)
}
serverOpts.WriteIdleTimeout = caddy.Duration(dur)
if len(args) == 2 {
rate, err := strconv.ParseInt(args[1], 10, 64)
if err != nil {
return nil, d.Errf("parsing write_idle min_rate bytes/second: %v", err)
}
serverOpts.WriteMinRate = rate
}

case "write_max_chunk":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing write_max_chunk: %v", err)
}
serverOpts.MaxWriteChunk = int(size)

case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
Expand Down Expand Up @@ -381,7 +433,12 @@ func applyServerOptions(
server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw
server.ReadTimeout = opts.ReadTimeout
server.ReadHeaderTimeout = opts.ReadHeaderTimeout
server.ReadIdleTimeout = opts.ReadIdleTimeout
server.ReadMinRate = opts.ReadMinRate
server.WriteTimeout = opts.WriteTimeout
server.WriteIdleTimeout = opts.WriteIdleTimeout
server.WriteMinRate = opts.WriteMinRate
server.MaxWriteChunk = opts.MaxWriteChunk
server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval
server.KeepAliveIdle = opts.KeepAliveIdle
Expand Down
17 changes: 17 additions & 0 deletions modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,15 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.ReadHeaderTimeout == 0 {
srv.ReadHeaderTimeout = defaultReadHeaderTimeout // see #6663
}
if srv.ReadIdleTimeout == 0 {
srv.ReadIdleTimeout = defaultReadIdleTimeout
}
if srv.WriteIdleTimeout == 0 {
srv.WriteIdleTimeout = defaultWriteIdleTimeout
}
if srv.MaxWriteChunk == 0 {
srv.MaxWriteChunk = DefaultMaxWriteChunk
}
}
ctx.Context = oldContext
return nil
Expand Down Expand Up @@ -856,6 +865,14 @@ const (
// long time even on legitimately slow connections or
// busy servers to read it.
defaultReadHeaderTimeout = caddy.Duration(time.Minute)

// defaultReadIdleTimeout and defaultWriteIdleTimeout mitigate
// slowloris-style attacks on the request body and response write.
// Unlike a hard deadline, these are safe defaults even for large
// payloads because the deadline is reset on every successful
// read/write; only a stalled connection is affected.
defaultReadIdleTimeout = caddy.Duration(time.Minute)
defaultWriteIdleTimeout = caddy.Duration(time.Minute)
)

// Interface guards
Expand Down
192 changes: 192 additions & 0 deletions modules/caddyhttp/idletimeout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddyhttp

import (
"io"
"net/http"
"time"

"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

// DefaultMaxWriteChunk is used by IdleTimeoutWriter when MaxChunk is zero.
// See IdleTimeoutWriter for why a limit is needed at all; nginx's
// analogous sendfile_max_chunk defaults to 2 MiB and is admin-tunable
// for the same reason this is exposed as a field rather than a constant.
const DefaultMaxWriteChunk = 64 * 1024

// IdleDeadline computes the next read/write deadline for the idle-reset
// mechanism shared by IdleTimeoutReader and IdleTimeoutWriter.
//
// With MinRate == 0, the deadline is simply pushed forward on every
// call (now+Timeout): a slow but steadily-progressing transfer is never
// killed, while a connection that stalls (no bytes for the duration of
// Timeout) is. This alone doesn't bound a transfer that trickles just
// enough data to never go idle.
//
// With MinRate > 0, the allowance instead grows from a fixed Start
// based on bytes transferred so far (1/MinRate seconds of extra
// allowance per byte), matching Apache mod_reqtimeout's MinRate: a
// trickle that doesn't sustain MinRate bytes/sec falls behind real
// time and gets cut, even though no single call ever stalls.
//
// HardDeadline, if non-zero, caps the result either way, so this can't
// silently defeat an explicitly configured ReadTimeout/WriteTimeout
// ceiling.
type IdleDeadline struct {
Start time.Time
Timeout time.Duration
MinRate int64
HardDeadline time.Time

transferred int64
}

func (d *IdleDeadline) next() (deadline time.Time) {
if d.MinRate > 0 {
credit := time.Duration(d.transferred) * time.Second / time.Duration(d.MinRate)
deadline = d.Start.Add(d.Timeout + credit)
} else {
deadline = time.Now().Add(d.Timeout)
}
if !d.HardDeadline.IsZero() && deadline.After(d.HardDeadline) {
deadline = d.HardDeadline
}

return
}

// IdleTimeoutReader wraps a request body with IdleDeadline, resetting
// the read deadline before every Read call instead of bounding the
// whole body transfer with a single hard deadline.
type IdleTimeoutReader struct {
io.ReadCloser
Ctrl *http.ResponseController
Deadline IdleDeadline
Logger *zap.Logger

unsupported bool
}

func (r *IdleTimeoutReader) Read(p []byte) (int, error) {
if !r.unsupported {
if err := r.Ctrl.SetReadDeadline(r.Deadline.next()); err != nil {
r.unsupported = true
if c := r.Logger.Check(zapcore.DebugLevel, "could not set read deadline"); c != nil {
c.Write(zap.Error(err))
}
}
}

n, err := r.ReadCloser.Read(p)
r.Deadline.transferred += int64(n)

return n, err
}

// IdleTimeoutWriter wraps a ResponseWriter with IdleDeadline, resetting
// the write deadline before every Write call, the same way
// IdleTimeoutReader does for reads. A handler that pauses between
// writes (e.g. streaming or SSE) is unaffected, since with MinRate == 0
// the deadline only bounds the duration of the write actually in flight.
//
// MaxChunk bounds how much a single underlying Write/ReadFrom call is
// allowed to cover; zero uses DefaultMaxWriteChunk. SetWriteDeadline
// bounds the whole call it precedes, not just a stall within it:
// net.Conn.Write loops internally until the entire buffer is sent
// (unlike Read, which returns after one syscall), and
// ResponseWriter.ReadFrom hands the entire remaining source to the
// connection in one call, be it via sendfile or an internal buffered
// copy loop. Without chunking, a single large Write or a large body
// copied via io.Copy would have its whole transfer bounded by one
// deadline, silently truncating a slow-but-healthy transfer exactly
// like a hard WriteTimeout would. A 64 KiB default still preserves
// most of the sendfile fast path's benefit (net/sendfile.go
// special-cases *io.LimitedReader to keep using sendfile per chunk).
type IdleTimeoutWriter struct {
*ResponseWriterWrapper
Ctrl *http.ResponseController
Deadline IdleDeadline
MaxChunk int
Logger *zap.Logger

unsupported bool
}

func (w *IdleTimeoutWriter) resetDeadline() {
if w.unsupported {
return
}

if err := w.Ctrl.SetWriteDeadline(w.Deadline.next()); err != nil {
w.unsupported = true
if c := w.Logger.Check(zapcore.DebugLevel, "could not set write deadline"); c != nil {
c.Write(zap.Error(err))
}
}
}

func (w *IdleTimeoutWriter) maxChunk() int {
if w.MaxChunk > 0 {
return w.MaxChunk
}
return DefaultMaxWriteChunk
}

func (w *IdleTimeoutWriter) Write(p []byte) (int, error) {
maxChunk := w.maxChunk()
var total int
for len(p) > 0 {
chunk := p
if len(chunk) > maxChunk {
chunk = chunk[:maxChunk]
}
w.resetDeadline()
n, err := w.ResponseWriterWrapper.Write(chunk)
total += n
w.Deadline.transferred += int64(n)
p = p[n:]
if err != nil {
return total, err
}
}
return total, nil
}

func (w *IdleTimeoutWriter) ReadFrom(r io.Reader) (int64, error) {
maxChunk := w.maxChunk()
var total int64
for {
w.resetDeadline()
n, err := w.ResponseWriterWrapper.ReadFrom(io.LimitReader(r, int64(maxChunk)))
total += n
w.Deadline.transferred += n
if err != nil {
return total, err
}
if n < int64(maxChunk) {
return total, nil
}
}
}

// Interface guards
var (
_ io.ReadCloser = (*IdleTimeoutReader)(nil)
_ http.ResponseWriter = (*IdleTimeoutWriter)(nil)
_ io.ReaderFrom = (*IdleTimeoutWriter)(nil)
)
Loading
Loading