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
| 1 | package integration |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/caddyserver/caddy/v2/caddytest" |
| 13 | ) |
| 14 | |
| 15 | // A timed-out upload must get an error response without sending the remainder |
| 16 | // of its body. HTTP/1 drains unread bodies smaller than 256 KiB before replying; |
| 17 | // clearing the expired read deadline makes that drain wait for the client. |
| 18 | func TestReverseProxyRequestBodyTimeout(t *testing.T) { |
| 19 | for _, proto := range []int{1, 2} { |
| 20 | for _, tc := range []struct { |
| 21 | name string |
| 22 | timeouts string |
| 23 | trickle bool |
| 24 | handler string |
| 25 | }{ |
| 26 | {"hard stalled", `"read_timeout": 300000000, "read_idle_timeout": 60000000000`, false, ""}, |
| 27 | {"hard trickled", `"read_timeout": 300000000, "read_idle_timeout": 60000000000`, true, ""}, |
| 28 | {"idle stalled", `"read_idle_timeout": 300000000`, false, ""}, |
| 29 | {"minimum rate trickled", `"read_idle_timeout": 300000000, "read_min_rate": 65536`, true, ""}, |
| 30 | {"route stalled", `"read_idle_timeout": 60000000000`, false, `{"handler": "timeouts", "read_timeout": 300000000},`}, |
| 31 | } { |
| 32 | t.Run(fmt.Sprintf("HTTP%d/%s", proto, tc.name), func(t *testing.T) { |
| 33 | type uploadResult struct { |
| 34 | n int64 |
| 35 | err error |
| 36 | } |
| 37 | upstreamRead := make(chan uploadResult, 1) |
| 38 | upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 39 | n, err := io.Copy(io.Discard, r.Body) |
| 40 | upstreamRead <- uploadResult{n, err} |
| 41 | w.WriteHeader(http.StatusNoContent) |
| 42 | })) |
| 43 | defer upstream.Close() |
| 44 | tester := caddytest.NewTester(t) |
| 45 | tester.InitServer(fmt.Sprintf(`{ |
| 46 | "admin": {"listen": "localhost:2999", "config": {"persist": false}}, |
| 47 | "apps": { |
| 48 | "tls": {"certificates": {"load_files": [{ |
| 49 | "certificate": "/caddy.localhost.crt", "key": "/caddy.localhost.key" |
| 50 | }]}}, |
| 51 | "http": {"grace_period": 1, "servers": {"srv0": { |
| 52 | "listen": ["127.0.0.1:9443"], |
| 53 | "automatic_https": {"disable": true}, |
| 54 | "tls_connection_policies": [{}], |
| 55 | %s, |
| 56 | "routes": [{"handle": [%s{"handler": "reverse_proxy", |
| 57 | "upstreams": [{"dial": %q}] |
| 58 | }]}] |
| 59 | }}} |
| 60 | } |
| 61 | }`, tc.timeouts, tc.handler, upstream.Listener.Addr().String()), "json") |
| 62 | if t.Failed() { |
| 63 | t.FailNow() |
| 64 | } |
| 65 | transport := tester.Client.Transport.(*http.Transport) |
| 66 | if proto == 1 { |
| 67 | transport.ForceAttemptHTTP2 = false |
| 68 | } |
| 69 | defer transport.CloseIdleConnections() |
| 70 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) |
| 71 | defer cancel() |
| 72 | pr, pw := io.Pipe() |
| 73 | defer pr.Close() |
| 74 | defer pw.Close() |
| 75 | stopClose := context.AfterFunc(ctx, func() { pr.CloseWithError(ctx.Err()) }) |
| 76 | defer stopClose() |
| 77 | writerDone := make(chan struct{}) |
| 78 | go func() { |
| 79 | defer close(writerDone) |
| 80 | if _, err := pw.Write(make([]byte, 1024)); err != nil { |
| 81 | return |
| 82 | } |
| 83 | if !tc.trickle { |
| 84 | return // Leave the pipe open so the request body stalls. |
| 85 | } |
| 86 | ticker := time.NewTicker(25 * time.Millisecond) |
| 87 | defer ticker.Stop() |
| 88 | for { |
| 89 | select { |
| 90 | case <-ctx.Done(): |
| 91 | return |
| 92 | case <-ticker.C: |
| 93 | if _, err := pw.Write([]byte("x")); err != nil { |
| 94 | return |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | }() |
| 99 | defer func() { |
| 100 | cancel() |
| 101 | pr.Close() |
| 102 | <-writerDone |
| 103 | }() |
| 104 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://a.caddy.localhost:9443", pr) |
| 105 | if err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | req.ContentLength = 100 << 10 // Below net/http's post-handler drain limit. |
| 109 | resp, err := tester.Client.Do(req) |
| 110 | if err != nil { |
| 111 | t.Fatalf("did not receive a response to the timed-out upload: %v", err) |
| 112 | } |
| 113 | defer resp.Body.Close() |
| 114 | if resp.ProtoMajor != proto { |
| 115 | t.Fatalf("got HTTP/%d, want HTTP/%d", resp.ProtoMajor, proto) |
| 116 | } |
| 117 | // A read timeout can also cancel the request context, so the proxy |
| 118 | // can classify it as a canceled request rather than a gateway error. |
| 119 | if resp.StatusCode != 499 && resp.StatusCode != http.StatusBadGateway && resp.StatusCode != http.StatusGatewayTimeout { |
| 120 | t.Errorf("unexpected timeout response status: %d", resp.StatusCode) |
| 121 | } |
| 122 | select { |
| 123 | case result := <-upstreamRead: |
| 124 | if result.err == nil || result.n < 1024 || result.n >= req.ContentLength { |
| 125 | t.Errorf("upstream upload was not interrupted: read %d bytes, error %v", result.n, result.err) |
| 126 | } |
| 127 | case <-ctx.Done(): |
| 128 | t.Fatal("upstream upload was not interrupted") |
| 129 | } |
| 130 | }) |
| 131 | } |
| 132 | } |
| 133 | } |