@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 142 lines 4.34 KB
Raw
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 }