1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyhttp
16
+
17
+import (
18
+ "bytes"
19
+ "io"
20
+ "net/http"
21
+ "net/http/httptest"
22
+ "testing"
23
+ "time"
24
+
25
+ "github.com/stretchr/testify/assert"
26
+ "github.com/stretchr/testify/require"
27
+ "go.uber.org/zap"
28
+)
29
+
30
+// pacedReader emits chunkCount chunks of chunkSize bytes, sleeping delay
31
+// before each one, simulating a client that trickles a request body.
32
+type pacedReader struct {
33
+ delay time.Duration
34
+ chunkSize int
35
+ chunkCount int
36
+}
37
+
38
+func (p *pacedReader) Read(b []byte) (int, error) {
39
+ if p.chunkCount <= 0 {
40
+ return 0, io.EOF
41
+ }
42
+ time.Sleep(p.delay)
43
+ p.chunkCount--
44
+ n := copy(b, make([]byte, p.chunkSize))
45
+ return n, nil
46
+}
47
+
48
+func TestIdleTimeoutReader(t *testing.T) {
49
+ const timeout = 150 * time.Millisecond
50
+
51
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52
+ wrapped := &IdleTimeoutReader{
53
+ ReadCloser: r.Body,
54
+ Ctrl: http.NewResponseController(w),
55
+ Deadline: IdleDeadline{Timeout: timeout},
56
+ Logger: zap.NewNop(),
57
+ }
58
+ _, err := io.Copy(io.Discard, wrapped)
59
+ if err != nil {
60
+ http.Error(w, err.Error(), http.StatusRequestTimeout)
61
+ return
62
+ }
63
+ w.WriteHeader(http.StatusOK)
64
+ }))
65
+ defer srv.Close()
66
+
67
+ t.Run("slow but steadily progressing upload is not killed", func(t *testing.T) {
68
+ // each gap is well under timeout, but the cumulative transfer
69
+ // time is well over it; a hard deadline would kill this
70
+ body := &pacedReader{delay: timeout / 4, chunkSize: 8, chunkCount: 8}
71
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
72
+ require.NoError(t, err)
73
+ defer resp.Body.Close()
74
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
75
+ })
76
+
77
+ t.Run("stalled upload is aborted", func(t *testing.T) {
78
+ // a single gap that exceeds timeout must trip the deadline
79
+ body := &pacedReader{delay: timeout * 4, chunkSize: 8, chunkCount: 2}
80
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
81
+ if err != nil {
82
+ // the connection may also be reset outright, which is fine
83
+ return
84
+ }
85
+ defer resp.Body.Close()
86
+ assert.NotEqual(t, http.StatusOK, resp.StatusCode)
87
+ })
88
+}
89
+
90
+func TestIdleTimeoutReader_HardDeadlineCapsIdleReset(t *testing.T) {
91
+ const idle = 500 * time.Millisecond
92
+ const hard = 200 * time.Millisecond
93
+
94
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
95
+ wrapped := &IdleTimeoutReader{
96
+ ReadCloser: r.Body,
97
+ Ctrl: http.NewResponseController(w),
98
+ Deadline: IdleDeadline{
99
+ Timeout: idle,
100
+ HardDeadline: time.Now().Add(hard),
101
+ },
102
+ Logger: zap.NewNop(),
103
+ }
104
+ _, err := io.Copy(io.Discard, wrapped)
105
+ if err != nil {
106
+ http.Error(w, err.Error(), http.StatusRequestTimeout)
107
+ return
108
+ }
109
+ w.WriteHeader(http.StatusOK)
110
+ }))
111
+ defer srv.Close()
112
+
113
+ // each gap is under the idle timeout, so the connection is never
114
+ // idle, but the cumulative transfer time exceeds hardDeadline: an
115
+ // explicit hard ceiling must still cut it off despite the ongoing
116
+ // idle-reset activity
117
+ body := &pacedReader{delay: hard, chunkSize: 8, chunkCount: 8}
118
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
119
+ if err != nil {
120
+ // the connection may also be reset outright, which is fine
121
+ return
122
+ }
123
+ defer resp.Body.Close()
124
+ assert.NotEqual(t, http.StatusOK, resp.StatusCode)
125
+}
126
+
127
+func TestIdleTimeoutReader_MinRateCatchesTrickle(t *testing.T) {
128
+ const idle = 250 * time.Millisecond
129
+ const chunkDelay = 150 * time.Millisecond
130
+
131
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132
+ wrapped := &IdleTimeoutReader{
133
+ ReadCloser: r.Body,
134
+ Ctrl: http.NewResponseController(w),
135
+ Deadline: IdleDeadline{
136
+ Start: time.Now(),
137
+ Timeout: idle,
138
+ // a huge min rate means even a full-size read call
139
+ // earns virtually no extra credit, so a byte-sized
140
+ // trickle can't keep the connection alive past idle
141
+ MinRate: 100_000_000,
142
+ },
143
+ Logger: zap.NewNop(),
144
+ }
145
+ _, err := io.Copy(io.Discard, wrapped)
146
+ if err != nil {
147
+ http.Error(w, err.Error(), http.StatusRequestTimeout)
148
+ return
149
+ }
150
+ w.WriteHeader(http.StatusOK)
151
+ }))
152
+ defer srv.Close()
153
+
154
+ // each individual gap (150ms) is under the idle window (250ms), so
155
+ // pure idle-reset alone would never trip; MinRate must still catch
156
+ // this trickle since it never earns meaningful credit
157
+ body := &pacedReader{delay: chunkDelay, chunkSize: 1, chunkCount: 6}
158
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
159
+ if err != nil {
160
+ return
161
+ }
162
+ defer resp.Body.Close()
163
+ assert.NotEqual(t, http.StatusOK, resp.StatusCode)
164
+}
165
+
166
+func TestIdleTimeoutReader_MinRateAllowsSustainedRate(t *testing.T) {
167
+ const idle = 250 * time.Millisecond
168
+ const chunkDelay = 100 * time.Millisecond
169
+ const minRate = 1000 // bytes/second
170
+
171
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
172
+ wrapped := &IdleTimeoutReader{
173
+ ReadCloser: r.Body,
174
+ Ctrl: http.NewResponseController(w),
175
+ Deadline: IdleDeadline{
176
+ Start: time.Now(),
177
+ Timeout: idle,
178
+ MinRate: minRate,
179
+ },
180
+ Logger: zap.NewNop(),
181
+ }
182
+ _, err := io.Copy(io.Discard, wrapped)
183
+ if err != nil {
184
+ http.Error(w, err.Error(), http.StatusRequestTimeout)
185
+ return
186
+ }
187
+ w.WriteHeader(http.StatusOK)
188
+ }))
189
+ defer srv.Close()
190
+
191
+ // 200 bytes every 100ms is 2000 bytes/second, comfortably above the
192
+ // 1000 bytes/second minRate, so credit earned per chunk (200ms)
193
+ // outpaces the real time elapsed per chunk (100ms): this transfer
194
+ // must complete despite exceeding the idle window cumulatively
195
+ body := &pacedReader{delay: chunkDelay, chunkSize: 200, chunkCount: 8}
196
+ resp, err := http.Post(srv.URL, "application/octet-stream", body)
197
+ require.NoError(t, err)
198
+ defer resp.Body.Close()
199
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
200
+}
201
+
202
+func TestIdleTimeoutWriter(t *testing.T) {
203
+ const timeout = 150 * time.Millisecond
204
+
205
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
206
+ wrapped := &IdleTimeoutWriter{
207
+ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
208
+ Ctrl: http.NewResponseController(w),
209
+ Deadline: IdleDeadline{Timeout: timeout},
210
+ Logger: zap.NewNop(),
211
+ }
212
+ flusher, _ := wrapped.ResponseWriterWrapper.ResponseWriter.(http.Flusher)
213
+ for range 8 {
214
+ time.Sleep(timeout / 4)
215
+ _, err := wrapped.Write(make([]byte, 8))
216
+ if err != nil {
217
+ t.Errorf("unexpected write error: %v", err)
218
+ return
219
+ }
220
+ if flusher != nil {
221
+ flusher.Flush()
222
+ }
223
+ }
224
+ }))
225
+ defer srv.Close()
226
+
227
+ // each write gap is well under timeout, but the cumulative streaming
228
+ // time is well over it; a hard deadline would kill this response
229
+ resp, err := http.Get(srv.URL)
230
+ require.NoError(t, err)
231
+ defer resp.Body.Close()
232
+
233
+ n, err := io.Copy(io.Discard, resp.Body)
234
+ require.NoError(t, err)
235
+ assert.EqualValues(t, 64, n)
236
+}
237
+
238
+func TestIdleTimeoutWriter_HardDeadlineCapsIdleReset(t *testing.T) {
239
+ const idle = 500 * time.Millisecond
240
+ const hard = 200 * time.Millisecond
241
+
242
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
243
+ wrapped := &IdleTimeoutWriter{
244
+ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
245
+ Ctrl: http.NewResponseController(w),
246
+ Deadline: IdleDeadline{
247
+ Timeout: idle,
248
+ HardDeadline: time.Now().Add(hard),
249
+ },
250
+ Logger: zap.NewNop(),
251
+ }
252
+ flusher, _ := wrapped.ResponseWriterWrapper.ResponseWriter.(http.Flusher)
253
+ for range 8 {
254
+ time.Sleep(hard)
255
+ if _, err := wrapped.Write(make([]byte, 8)); err != nil {
256
+ return
257
+ }
258
+ if flusher != nil {
259
+ flusher.Flush()
260
+ }
261
+ }
262
+ }))
263
+ defer srv.Close()
264
+
265
+ // each write gap is under the idle timeout, so the connection is
266
+ // never idle, but the cumulative streaming time exceeds
267
+ // hardDeadline: an explicit hard ceiling must still cut it off,
268
+ // whether that surfaces as a failed request, a truncated body, or
269
+ // both
270
+ resp, err := http.Get(srv.URL)
271
+ if err != nil {
272
+ return
273
+ }
274
+ defer resp.Body.Close()
275
+
276
+ n, err := io.Copy(io.Discard, resp.Body)
277
+ if err == nil {
278
+ assert.Less(t, n, int64(64))
279
+ }
280
+}
281
+
282
+// readFromCounter is a fake http.ResponseWriter that records how many
283
+// times ReadFrom was called on it and the size of the largest call, so
284
+// tests can assert on chunking behavior deterministically instead of
285
+// depending on real TCP timing.
286
+type readFromCounter struct {
287
+ *httptest.ResponseRecorder
288
+ calls int
289
+ maxCall int
290
+ total int64
291
+ writeCalls int
292
+ maxWriteCall int
293
+}
294
+
295
+func (c *readFromCounter) SetWriteDeadline(time.Time) error { return nil }
296
+
297
+func (c *readFromCounter) ReadFrom(r io.Reader) (int64, error) {
298
+ c.calls++
299
+ n, err := io.Copy(io.Discard, r)
300
+ c.total += n
301
+ if int(n) > c.maxCall {
302
+ c.maxCall = int(n)
303
+ }
304
+ return n, err
305
+}
306
+
307
+func (c *readFromCounter) Write(p []byte) (int, error) {
308
+ c.writeCalls++
309
+ if len(p) > c.maxWriteCall {
310
+ c.maxWriteCall = len(p)
311
+ }
312
+ return c.ResponseRecorder.Write(p)
313
+}
314
+
315
+func TestIdleTimeoutWriter_ReadFromChunksLargeTransfer(t *testing.T) {
316
+ const size = DefaultMaxWriteChunk*3 + 100 // 3 full chunks plus a partial tail
317
+
318
+ counter := &readFromCounter{ResponseRecorder: httptest.NewRecorder()}
319
+ wrapped := &IdleTimeoutWriter{
320
+ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: counter},
321
+ Ctrl: http.NewResponseController(counter),
322
+ Deadline: IdleDeadline{Timeout: time.Second},
323
+ Logger: zap.NewNop(),
324
+ }
325
+
326
+ n, err := wrapped.ReadFrom(bytes.NewReader(make([]byte, size)))
327
+ require.NoError(t, err)
328
+ assert.EqualValues(t, size, n)
329
+ assert.EqualValues(t, size, counter.total)
330
+ assert.LessOrEqual(t, counter.maxCall, DefaultMaxWriteChunk,
331
+ "no single underlying ReadFrom call should cover more than DefaultMaxWriteChunk bytes, "+
332
+ "since SetWriteDeadline bounds the whole call it precedes, not just a stall within it")
333
+ assert.Equal(t, 4, counter.calls, "expected 3 full chunks plus a partial tail chunk")
334
+}
335
+
336
+func TestIdleTimeoutWriter_WriteChunksLargePayload(t *testing.T) {
337
+ const size = DefaultMaxWriteChunk*2 + 1
338
+
339
+ counter := &readFromCounter{ResponseRecorder: httptest.NewRecorder()}
340
+ wrapped := &IdleTimeoutWriter{
341
+ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: counter},
342
+ Ctrl: http.NewResponseController(counter),
343
+ Deadline: IdleDeadline{Timeout: time.Second},
344
+ Logger: zap.NewNop(),
345
+ }
346
+
347
+ n, err := wrapped.Write(make([]byte, size))
348
+ require.NoError(t, err)
349
+ assert.EqualValues(t, size, n)
350
+ assert.EqualValues(t, size, counter.ResponseRecorder.Body.Len())
351
+ assert.LessOrEqual(t, counter.maxWriteCall, DefaultMaxWriteChunk,
352
+ "no single underlying Write call should cover more than DefaultMaxWriteChunk bytes")
353
+ assert.Equal(t, 3, counter.writeCalls, "expected 2 full chunks plus a 1-byte tail chunk")
354
+}
355
+
356
+func TestIdleTimeoutWriter_MaxChunkOverride(t *testing.T) {
357
+ const size = 1000
358
+ const maxChunk = 100
359
+
360
+ counter := &readFromCounter{ResponseRecorder: httptest.NewRecorder()}
361
+ wrapped := &IdleTimeoutWriter{
362
+ ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: counter},
363
+ Ctrl: http.NewResponseController(counter),
364
+ Deadline: IdleDeadline{Timeout: time.Second},
365
+ MaxChunk: maxChunk,
366
+ Logger: zap.NewNop(),
367
+ }
368
+
369
+ n, err := wrapped.Write(make([]byte, size))
370
+ require.NoError(t, err)
371
+ assert.EqualValues(t, size, n)
372
+ assert.LessOrEqual(t, counter.maxWriteCall, maxChunk,
373
+ "a configured MaxChunk should override DefaultMaxWriteChunk")
374
+ assert.Equal(t, size/maxChunk, counter.writeCalls)
375
+}