@marcel91200 / caddy Mirror · read-only
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 11, 01:00 UTC

master
go 350 lines 12 KB
Raw
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 timeouts
16
17 import (
18 "io"
19 "net/http"
20 "net/http/httptest"
21 "strings"
22 "testing"
23 "time"
24
25 "github.com/stretchr/testify/assert"
26 "github.com/stretchr/testify/require"
27 "go.uber.org/zap"
28
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
30 )
31
32 type readDeadlineRecorder struct {
33 *httptest.ResponseRecorder
34 deadlines []time.Time
35 }
36
37 func (w *readDeadlineRecorder) SetReadDeadline(deadline time.Time) error {
38 w.deadlines = append(w.deadlines, deadline)
39 return nil
40 }
41
42 // pacedReader emits chunkCount chunks of chunkSize bytes, sleeping delay
43 // before each one, simulating a client that trickles a request body.
44 type pacedReader struct {
45 delay time.Duration
46 chunkSize int
47 chunkCount int
48 }
49
50 func (p *pacedReader) Read(b []byte) (int, error) {
51 if p.chunkCount <= 0 {
52 return 0, io.EOF
53 }
54 time.Sleep(p.delay)
55 p.chunkCount--
56 n := copy(b, make([]byte, p.chunkSize))
57 return n, nil
58 }
59
60 // noError adapts a caddyhttp.Handler to a plain http.Handler for httptest.NewServer.
61 func noError(h caddyhttp.Handler) http.HandlerFunc {
62 return func(w http.ResponseWriter, r *http.Request) {
63 if err := h.ServeHTTP(w, r); err != nil {
64 http.Error(w, err.Error(), http.StatusInternalServerError)
65 }
66 }
67 }
68
69 func TestTimeouts_ReadTimeoutIsIdleReset(t *testing.T) {
70 const timeout = 150 * time.Millisecond
71
72 tm := Timeouts{ReadTimeout: timeout}
73 tm.logger = zap.NewNop()
74
75 srv := httptest.NewServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
76 return tm.ServeHTTP(w, r, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
77 _, err := io.Copy(io.Discard, r.Body)
78 if err != nil {
79 http.Error(w, err.Error(), http.StatusRequestTimeout)
80 return nil
81 }
82 w.WriteHeader(http.StatusOK)
83 return nil
84 }))
85 })))
86 defer srv.Close()
87
88 // each gap is well under timeout, but the cumulative transfer time
89 // is well over it; a hard (non-idle-reset) deadline would kill this
90 body := &pacedReader{delay: timeout / 4, chunkSize: 8, chunkCount: 8}
91 resp, err := http.Post(srv.URL, "application/octet-stream", body)
92 require.NoError(t, err)
93 defer resp.Body.Close()
94 assert.Equal(t, http.StatusOK, resp.StatusCode)
95 }
96
97 func TestTimeoutsStopsReadDeadlineBeforeReturning(t *testing.T) {
98 tm := Timeouts{ReadTimeout: time.Second, logger: zap.NewNop()}
99 w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
100 req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("body"))
101 var body io.Reader
102
103 err := tm.ServeHTTP(w, req, caddyhttp.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) error {
104 body = r.Body
105 _, _ = body.Read(make([]byte, 1))
106 return nil
107 }))
108 require.NoError(t, err)
109 require.Len(t, w.deadlines, 1)
110 assert.False(t, w.deadlines[0].IsZero())
111
112 _, _ = io.Copy(io.Discard, body)
113 assert.Len(t, w.deadlines, 1)
114 }
115
116 func TestTimeoutsSkipsDrainDeadlineWithoutBody(t *testing.T) {
117 tm := Timeouts{ReadTimeout: time.Second, logger: zap.NewNop()}
118 w := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
119 req := httptest.NewRequest(http.MethodGet, "/", nil)
120
121 err := tm.ServeHTTP(w, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
122 return nil
123 }))
124 require.NoError(t, err)
125 assert.Empty(t, w.deadlines)
126 }
127
128 func TestTimeouts_WriteMaxChunkOverride(t *testing.T) {
129 const size = 10000
130 const maxChunk = 100
131
132 tm := Timeouts{WriteTimeout: time.Second, MaxWriteChunk: maxChunk}
133 tm.logger = zap.NewNop()
134
135 srv := httptest.NewServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
136 return tm.ServeHTTP(w, r, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
137 _, err := w.Write(make([]byte, size))
138 return err
139 }))
140 })))
141 defer srv.Close()
142
143 resp, err := http.Get(srv.URL)
144 require.NoError(t, err)
145 defer resp.Body.Close()
146
147 n, err := io.Copy(io.Discard, resp.Body)
148 require.NoError(t, err)
149 assert.EqualValues(t, size, n)
150 }
151
152 // TestTimeoutsTakePrecedence checks that the read timeout of the most
153 // specific scope applies: a timeouts handler over the server-wide idle
154 // timeout, and an inner timeouts handler over an outer one.
155 func TestTimeoutsTakePrecedence(t *testing.T) {
156 const short = 100 * time.Millisecond
157 const long = 2 * time.Second
158 const pause = 300 * time.Millisecond
159
160 for i, tc := range []struct {
161 name string
162 server time.Duration
163 outer time.Duration
164 inner time.Duration
165 expected int
166 }{
167 {name: "route extends server-wide", server: short, inner: long, expected: http.StatusOK},
168 {name: "route shortens server-wide", server: long, inner: short, expected: http.StatusRequestTimeout},
169 {name: "inner route extends outer", outer: short, inner: long, expected: http.StatusOK},
170 {name: "inner route shortens outer", outer: long, inner: short, expected: http.StatusRequestTimeout},
171 {name: "inner route extends outer and server-wide", server: short, outer: short, inner: long, expected: http.StatusOK},
172 } {
173 var handler caddyhttp.Handler = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
174 if _, err := io.Copy(io.Discard, r.Body); err != nil {
175 w.WriteHeader(http.StatusRequestTimeout)
176 return nil
177 }
178 w.WriteHeader(http.StatusOK)
179 return nil
180 })
181 for _, timeout := range []time.Duration{tc.inner, tc.outer} {
182 if timeout == 0 {
183 continue
184 }
185 tm := Timeouts{ReadTimeout: timeout, logger: zap.NewNop()}
186 next := handler
187 handler = caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
188 return tm.ServeHTTP(w, r, next)
189 })
190 }
191
192 srv := httptest.NewServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
193 // stand in for the server-wide idle timeout
194 if tc.server > 0 {
195 reader := &caddyhttp.IdleTimeoutReader{
196 ReadCloser: r.Body,
197 Ctrl: http.NewResponseController(w),
198 Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: tc.server},
199 Logger: zap.NewNop(),
200 }
201 defer reader.HandlerDone()
202 r.Body = reader
203 r = r.WithContext(caddyhttp.ContextWithIdleTimeouts(r.Context(), reader, nil))
204 }
205 return handler.ServeHTTP(w, r)
206 })))
207
208 body := &pacedReader{delay: pause, chunkSize: 8, chunkCount: 2}
209 resp, err := http.Post(srv.URL, "application/octet-stream", body)
210 if err != nil {
211 // a timed out read may close the connection before a response
212 if tc.expected != http.StatusRequestTimeout {
213 t.Errorf("Test %d (%s): unexpected error: %v", i, tc.name, err)
214 }
215 } else {
216 resp.Body.Close()
217 assert.Equal(t, tc.expected, resp.StatusCode, "Test %d (%s)", i, tc.name)
218 }
219 srv.Close()
220 }
221 }
222
223 func TestTimeoutsOverrideServerWide(t *testing.T) {
224 const serverTimeout = time.Hour
225 const routeTimeout = time.Minute
226
227 rec := &readDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
228 req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("body"))
229 reader := &caddyhttp.IdleTimeoutReader{
230 ReadCloser: req.Body,
231 Ctrl: http.NewResponseController(rec),
232 Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
233 Logger: zap.NewNop(),
234 }
235 req.Body = reader
236 writer := &caddyhttp.IdleTimeoutWriter{
237 ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: rec},
238 Ctrl: http.NewResponseController(rec),
239 Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
240 MaxChunk: 1000,
241 Logger: zap.NewNop(),
242 }
243 req = req.WithContext(caddyhttp.ContextWithIdleTimeouts(req.Context(), reader, writer))
244
245 tm := Timeouts{ReadTimeout: routeTimeout, WriteTimeout: routeTimeout, MaxWriteChunk: 100, logger: zap.NewNop()}
246 err := tm.ServeHTTP(writer, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
247 // overridden in place rather than wrapped again
248 assert.Same(t, writer, w)
249 assert.Same(t, reader, r.Body)
250 assert.Equal(t, routeTimeout, writer.Deadline.Timeout)
251 assert.Equal(t, 100, writer.MaxChunk)
252
253 before := time.Now()
254 _, err := r.Body.Read(make([]byte, 1))
255 require.NoError(t, err)
256 require.Len(t, rec.deadlines, 1)
257 assert.WithinRange(t, rec.deadlines[0], before.Add(routeTimeout), time.Now().Add(routeTimeout))
258 return nil
259 }))
260 require.NoError(t, err)
261
262 // the server-wide settings are back once the handler returns, and
263 // the armed read deadline was moved to them
264 assert.Equal(t, serverTimeout, writer.Deadline.Timeout)
265 assert.Equal(t, 1000, writer.MaxChunk)
266 require.Len(t, rec.deadlines, 2)
267 assert.WithinRange(t, rec.deadlines[1], time.Now().Add(serverTimeout-time.Second), time.Now().Add(serverTimeout))
268
269 before := time.Now()
270 _, err = reader.Read(make([]byte, 1))
271 require.NoError(t, err)
272 require.Len(t, rec.deadlines, 3)
273 assert.WithinRange(t, rec.deadlines[2], before.Add(serverTimeout), time.Now().Add(serverTimeout))
274 }
275
276 func TestTimeoutsKeepServerWideMaxWriteChunk(t *testing.T) {
277 rec := httptest.NewRecorder()
278 writer := &caddyhttp.IdleTimeoutWriter{
279 ResponseWriterWrapper: &caddyhttp.ResponseWriterWrapper{ResponseWriter: rec},
280 Ctrl: http.NewResponseController(rec),
281 Deadline: caddyhttp.IdleDeadline{Timeout: time.Hour},
282 MaxChunk: 1000,
283 Logger: zap.NewNop(),
284 }
285 req := httptest.NewRequest(http.MethodGet, "/", nil)
286 req = req.WithContext(caddyhttp.ContextWithIdleTimeouts(req.Context(), nil, writer))
287
288 tm := Timeouts{WriteTimeout: time.Minute, logger: zap.NewNop()}
289 err := tm.ServeHTTP(writer, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
290 assert.Equal(t, 1000, writer.MaxChunk)
291 return nil
292 }))
293 require.NoError(t, err)
294 }
295
296 // TestTimeoutsRearmReadDeadlineHTTP2 checks that a read deadline armed
297 // before a timeouts handler starts is moved to its timeout. On HTTP/2
298 // the deadline is a timer that fails the body when it fires, even with
299 // no read in flight, so it can't wait for the next read to be reset.
300 func TestTimeoutsRearmReadDeadlineHTTP2(t *testing.T) {
301 const serverTimeout = 100 * time.Millisecond
302 const routeTimeout = 2 * time.Second
303 const pause = 300 * time.Millisecond
304
305 tm := Timeouts{ReadTimeout: routeTimeout, logger: zap.NewNop()}
306 srv := httptest.NewUnstartedServer(noError(caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
307 // stand in for the server-wide idle timeout
308 reader := &caddyhttp.IdleTimeoutReader{
309 ReadCloser: r.Body,
310 Ctrl: http.NewResponseController(w),
311 Deadline: caddyhttp.IdleDeadline{Start: time.Now(), Timeout: serverTimeout},
312 Logger: zap.NewNop(),
313 }
314 defer reader.HandlerDone()
315 r.Body = reader
316 r = r.WithContext(caddyhttp.ContextWithIdleTimeouts(r.Context(), reader, nil))
317
318 // read before the timeouts handler, arming the server-wide deadline
319 if _, err := io.ReadFull(r.Body, make([]byte, 8)); err != nil {
320 w.WriteHeader(http.StatusBadRequest)
321 return nil
322 }
323 return tm.ServeHTTP(w, r, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
324 // pause longer than the server-wide timeout, but not the route's
325 time.Sleep(pause)
326 if _, err := io.Copy(io.Discard, r.Body); err != nil {
327 w.WriteHeader(http.StatusRequestTimeout)
328 return nil
329 }
330 w.WriteHeader(http.StatusOK)
331 return nil
332 }))
333 })))
334 srv.EnableHTTP2 = true
335 srv.StartTLS()
336 defer srv.Close()
337
338 pr, pw := io.Pipe()
339 go func() {
340 _, _ = pw.Write(make([]byte, 8))
341 time.Sleep(pause + 100*time.Millisecond)
342 _, _ = pw.Write(make([]byte, 8))
343 _ = pw.Close()
344 }()
345 resp, err := srv.Client().Post(srv.URL, "application/octet-stream", pr)
346 require.NoError(t, err)
347 defer resp.Body.Close()
348 require.Equal(t, 2, resp.ProtoMajor)
349 assert.Equal(t, http.StatusOK, resp.StatusCode)
350 }