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 | "fmt" |
| 5 | "io" |
| 6 | "net" |
| 7 | "net/http" |
| 8 | "net/http/fcgi" |
| 9 | "os" |
| 10 | "runtime" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | |
| 14 | "github.com/caddyserver/caddy/v2/caddytest" |
| 15 | ) |
| 16 | |
| 17 | // TestFastCGIChunkedRequestBody drives the case from caddyserver/caddy#7386: a |
| 18 | // client sending a body with Transfer-Encoding: chunked through a fastcgi |
| 19 | // reverse_proxy over a unix socket. |
| 20 | // |
| 21 | // FastCGI cannot express a body of unknown length — CGI/1.1 requires |
| 22 | // CONTENT_LENGTH, and php-fpm hangs when it is absent or wrong and the body is |
| 23 | // non-empty — so such a body is only forwardable once it has been buffered in |
| 24 | // full. request_buffers is therefore what decides the outcome, and the default |
| 25 | // of 4096 for this transport is far below a git push or a file upload. |
| 26 | func TestFastCGIChunkedRequestBody(t *testing.T) { |
| 27 | if runtime.GOOS == "windows" { |
| 28 | t.SkipNow() |
| 29 | } |
| 30 | |
| 31 | socketName := tempSocketName(t) |
| 32 | |
| 33 | // Report how many body bytes actually arrived, so a truncated body shows up |
| 34 | // as a failure instead of passing quietly. |
| 35 | ln, err := net.Listen("unix", socketName) |
| 36 | if err != nil { |
| 37 | t.Fatalf("failed to listen on the socket: %s", err) |
| 38 | } |
| 39 | go fcgi.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //nolint:errcheck |
| 40 | n, _ := io.Copy(io.Discard, r.Body) |
| 41 | fmt.Fprintf(w, "received %d", n) |
| 42 | })) |
| 43 | t.Cleanup(func() { ln.Close() }) |
| 44 | runtime.Gosched() |
| 45 | |
| 46 | const requestBuffers = 4096 |
| 47 | |
| 48 | tester := caddytest.NewTester(t) |
| 49 | tester.InitServer(fmt.Sprintf(` |
| 50 | { |
| 51 | "admin": {"listen": "localhost:2999"}, |
| 52 | "apps": { |
| 53 | "pki": {"certificate_authorities": {"local": {"install_trust": false}}}, |
| 54 | "http": { |
| 55 | "grace_period": 1, |
| 56 | "servers": { |
| 57 | "srv0": { |
| 58 | "listen": [":18080"], |
| 59 | "routes": [ |
| 60 | { |
| 61 | "match": [{"path": ["/limited"]}], |
| 62 | "handle": [{ |
| 63 | "handler": "reverse_proxy", |
| 64 | "request_buffers": %d, |
| 65 | "transport": {"protocol": "fastcgi"}, |
| 66 | "upstreams": [{"dial": "unix/%s"}] |
| 67 | }] |
| 68 | }, |
| 69 | { |
| 70 | "match": [{"path": ["/unlimited"]}], |
| 71 | "handle": [{ |
| 72 | "handler": "reverse_proxy", |
| 73 | "request_buffers": -1, |
| 74 | "transport": {"protocol": "fastcgi"}, |
| 75 | "upstreams": [{"dial": "unix/%s"}] |
| 76 | }] |
| 77 | } |
| 78 | ] |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | `, requestBuffers, socketName, socketName), "json") |
| 85 | |
| 86 | tests := []struct { |
| 87 | name string |
| 88 | path string |
| 89 | size int |
| 90 | wantStatus int |
| 91 | }{ |
| 92 | {name: "small body fits the buffer", path: "/limited", size: 16, wantStatus: http.StatusOK}, |
| 93 | {name: "one below the buffer", path: "/limited", size: requestBuffers - 1, wantStatus: http.StatusOK}, |
| 94 | // The body is buffered up to the limit and no further, so its length |
| 95 | // stays unknown and FastCGI has nothing to put in CONTENT_LENGTH. |
| 96 | {name: "exactly the buffer", path: "/limited", size: requestBuffers, wantStatus: http.StatusLengthRequired}, |
| 97 | {name: "above the buffer", path: "/limited", size: requestBuffers * 4, wantStatus: http.StatusLengthRequired}, |
| 98 | // Unlimited buffering is the remedy: the whole body is read, so its |
| 99 | // length is known and the request goes through at any size. |
| 100 | {name: "above the buffer, buffering unlimited", path: "/unlimited", size: requestBuffers * 4, wantStatus: http.StatusOK}, |
| 101 | } |
| 102 | |
| 103 | for _, tc := range tests { |
| 104 | t.Run(tc.name, func(t *testing.T) { |
| 105 | body := strings.Repeat("x", tc.size) |
| 106 | // A strings.Reader would let net/http size the body; wrapping it in |
| 107 | // a bare io.Reader is what makes the request chunked. |
| 108 | req, err := http.NewRequest(http.MethodPost, "http://localhost:18080"+tc.path, struct{ io.Reader }{strings.NewReader(body)}) |
| 109 | if err != nil { |
| 110 | t.Fatalf("building request: %s", err) |
| 111 | } |
| 112 | |
| 113 | resp := tester.AssertResponseCode(req, tc.wantStatus) |
| 114 | defer resp.Body.Close() |
| 115 | if tc.wantStatus != http.StatusOK { |
| 116 | return |
| 117 | } |
| 118 | got, err := io.ReadAll(resp.Body) |
| 119 | if err != nil { |
| 120 | t.Fatalf("reading response: %s", err) |
| 121 | } |
| 122 | // Every byte has to reach the upstream, not just a buffer's worth. |
| 123 | if want := fmt.Sprintf("received %d", tc.size); string(got) != want { |
| 124 | t.Errorf("upstream got %q, want %q", got, want) |
| 125 | } |
| 126 | }) |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // tempSocketName borrows a name inside a valid path to bind a unix socket on. |
| 131 | func tempSocketName(t *testing.T) string { |
| 132 | t.Helper() |
| 133 | f, err := os.CreateTemp("", "*.sock") |
| 134 | if err != nil { |
| 135 | t.Fatalf("failed to create TempFile: %s", err) |
| 136 | } |
| 137 | name := f.Name() |
| 138 | f.Close() |
| 139 | os.Remove(name) |
| 140 | t.Cleanup(func() { os.Remove(name) }) |
| 141 | return name |
| 142 | } |