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, 02:15 UTC
| 1 | package fastcgi |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "io" |
| 7 | "net" |
| 8 | "net/http" |
| 9 | "net/http/fcgi" |
| 10 | "net/http/httptest" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | |
| 14 | "github.com/caddyserver/caddy/v2" |
| 15 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 16 | "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" |
| 17 | ) |
| 18 | |
| 19 | // fcgiBackend serves h over FastCGI and returns its address. |
| 20 | func fcgiBackend(t *testing.T, h http.Handler) string { |
| 21 | t.Helper() |
| 22 | |
| 23 | ln, err := net.Listen("tcp", "127.0.0.1:0") |
| 24 | if err != nil { |
| 25 | t.Fatalf("listen: %v", err) |
| 26 | } |
| 27 | t.Cleanup(func() { ln.Close() }) |
| 28 | go fcgi.Serve(ln, h) |
| 29 | |
| 30 | return ln.Addr().String() |
| 31 | } |
| 32 | |
| 33 | // fcgiRoundTrip sends req to a FastCGI backend at addr, the way the reverse |
| 34 | // proxy does once it has picked an upstream and decided not to buffer. |
| 35 | func fcgiRoundTrip(t *testing.T, addr string, req *http.Request) (*http.Response, error) { |
| 36 | t.Helper() |
| 37 | |
| 38 | ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) |
| 39 | t.Cleanup(cancel) |
| 40 | |
| 41 | transport := Transport{Root: "/", SplitPath: []string{".php"}} |
| 42 | if err := transport.Provision(ctx); err != nil { |
| 43 | t.Fatalf("provisioning transport: %v", err) |
| 44 | } |
| 45 | |
| 46 | req.URL.Host = addr |
| 47 | req = caddyhttp.PrepareRequest(req, caddy.NewReplacer(), nil, &caddyhttp.Server{}) |
| 48 | |
| 49 | res, err := transport.RoundTrip(req) |
| 50 | if res != nil { |
| 51 | t.Cleanup(func() { res.Body.Close() }) |
| 52 | } |
| 53 | |
| 54 | return res, err |
| 55 | } |
| 56 | |
| 57 | // A request whose length is known carries its own CONTENT_LENGTH, so fastcgi |
| 58 | // streams it without the request buffer it otherwise needs: refusing to |
| 59 | // forward it incrementally would deny a request that works fine unbuffered. |
| 60 | func TestIncrementalUnbufferedRequestWithKnownLength(t *testing.T) { |
| 61 | var gotBody string |
| 62 | var gotLength int64 |
| 63 | addr := fcgiBackend(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 64 | b, _ := io.ReadAll(r.Body) |
| 65 | gotBody, gotLength = string(b), r.ContentLength |
| 66 | })) |
| 67 | |
| 68 | req := httptest.NewRequest(http.MethodPost, "http://example.com/index.php", strings.NewReader("hello")) |
| 69 | req.Header.Set("Incremental", "?1") |
| 70 | |
| 71 | res, err := fcgiRoundTrip(t, addr, req) |
| 72 | if err != nil { |
| 73 | t.Fatalf("RoundTrip() error = %v", err) |
| 74 | } |
| 75 | if res.StatusCode != http.StatusOK { |
| 76 | t.Errorf("status = %d, want 200", res.StatusCode) |
| 77 | } |
| 78 | if gotBody != "hello" { |
| 79 | t.Errorf("upstream body = %q, want %q", gotBody, "hello") |
| 80 | } |
| 81 | if gotLength != 5 { |
| 82 | t.Errorf("upstream CONTENT_LENGTH = %d, want 5", gotLength) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | // A request whose length is unknown, as a chunked request is, has no |
| 87 | // CONTENT_LENGTH to pass on, and the fastcgi client rejects it with 411 rather |
| 88 | // than let the upstream hang. Only the request buffer can supply that length, |
| 89 | // which is why incremental forwarding is refused instead in this case. |
| 90 | func TestIncrementalUnbufferedRequestWithUnknownLength(t *testing.T) { |
| 91 | var reached bool |
| 92 | addr := fcgiBackend(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 93 | reached = true |
| 94 | })) |
| 95 | |
| 96 | req := httptest.NewRequest(http.MethodPost, "http://example.com/index.php", io.NopCloser(strings.NewReader("hello"))) |
| 97 | req.Header.Set("Incremental", "?1") |
| 98 | if req.ContentLength != -1 { |
| 99 | t.Fatalf("ContentLength = %d, want -1", req.ContentLength) |
| 100 | } |
| 101 | |
| 102 | _, err := fcgiRoundTrip(t, addr, req) |
| 103 | |
| 104 | var handlerErr caddyhttp.HandlerError |
| 105 | if !errors.As(err, &handlerErr) { |
| 106 | t.Fatalf("expected a caddyhttp.HandlerError, got %T: %v", err, err) |
| 107 | } |
| 108 | if handlerErr.StatusCode != http.StatusLengthRequired { |
| 109 | t.Errorf("status = %d, want %d", handlerErr.StatusCode, http.StatusLengthRequired) |
| 110 | } |
| 111 | if reached { |
| 112 | t.Error("request reached the upstream despite having no CONTENT_LENGTH") |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | // Declaring that a length is required is what makes the reverse proxy refuse |
| 117 | // to forward a body of unknown length incrementally rather than send it on to |
| 118 | // the 411 above. |
| 119 | func TestRequiresContentLength(t *testing.T) { |
| 120 | if !(Transport{}).RequiresContentLength() { |
| 121 | t.Error("RequiresContentLength() = false, want true") |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestRoundTripDialError(t *testing.T) { |
| 126 | req := httptest.NewRequest(http.MethodPost, "http://example.com/index.php", strings.NewReader("body")) |
| 127 | _, err := fcgiRoundTrip(t, "invalid address", req) |
| 128 | if err == nil { |
| 129 | t.Fatal("RoundTrip() error = nil, want a dial error") |
| 130 | } |
| 131 | |
| 132 | var dialErr reverseproxy.DialError |
| 133 | if !errors.As(err, &dialErr) { |
| 134 | t.Fatalf("RoundTrip() error type = %T, want reverseproxy.DialError", err) |
| 135 | } |
| 136 | if !strings.HasPrefix(err.Error(), "dialing backend:") { |
| 137 | t.Errorf("RoundTrip() error = %q, want dialing context", err) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // The request buffer fastcgi asks for by default is what supplies the |
| 142 | // CONTENT_LENGTH above, so these values matter beyond this package. |
| 143 | func TestDefaultBufferSizes(t *testing.T) { |
| 144 | reqBuffers, respBuffers := Transport{}.DefaultBufferSizes() |
| 145 | if reqBuffers != 4096 { |
| 146 | t.Errorf("request buffers = %d, want 4096", reqBuffers) |
| 147 | } |
| 148 | if respBuffers != 0 { |
| 149 | t.Errorf("response buffers = %d, want 0", respBuffers) |
| 150 | } |
| 151 | } |