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 | // 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 requestbody |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "errors" |
| 20 | "io" |
| 21 | "net/http" |
| 22 | "net/http/httptest" |
| 23 | "strings" |
| 24 | "testing" |
| 25 | |
| 26 | "github.com/caddyserver/caddy/v2" |
| 27 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 28 | ) |
| 29 | |
| 30 | func newRequestBodySetRequest(t *testing.T) (*http.Request, *caddy.Replacer) { |
| 31 | t.Helper() |
| 32 | |
| 33 | req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", nil) |
| 34 | repl := caddy.NewReplacer() |
| 35 | ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) |
| 36 | |
| 37 | return req.WithContext(ctx), repl |
| 38 | } |
| 39 | |
| 40 | // TestRequestBodySetKeepsUnknownPlaceholders is a regression test for the JSON |
| 41 | // case: a set body is user-supplied and commonly JSON, so braces the replacer |
| 42 | // does not recognize must survive the expansion instead of being blanked, the |
| 43 | // same known-placeholder policy the respond body already follows. |
| 44 | func TestRequestBodySetKeepsUnknownPlaceholders(t *testing.T) { |
| 45 | const jsonBody = `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}` |
| 46 | |
| 47 | rb := RequestBody{Set: jsonBody} |
| 48 | |
| 49 | req, _ := newRequestBodySetRequest(t) |
| 50 | |
| 51 | var gotBody string |
| 52 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 53 | body, err := io.ReadAll(r.Body) |
| 54 | if err != nil { |
| 55 | return err |
| 56 | } |
| 57 | gotBody = string(body) |
| 58 | return nil |
| 59 | }) |
| 60 | |
| 61 | err := rb.ServeHTTP(httptest.NewRecorder(), req, next) |
| 62 | if err != nil { |
| 63 | t.Fatalf("did not expect an error, but got: %v", err) |
| 64 | } |
| 65 | |
| 66 | if gotBody != jsonBody { |
| 67 | t.Errorf("set body was mangled by placeholder expansion: got %q, want %q", gotBody, jsonBody) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // TestRequestBodySetStillReplacesKnownPlaceholders guards the other direction: |
| 72 | // keeping unknown braces intact must not stop real placeholders from being |
| 73 | // replaced. |
| 74 | func TestRequestBodySetStillReplacesKnownPlaceholders(t *testing.T) { |
| 75 | rb := RequestBody{Set: `{"token":"{caddy_request_body_test_token}"}`} |
| 76 | want := `{"token":"s3cret"}` |
| 77 | |
| 78 | req, repl := newRequestBodySetRequest(t) |
| 79 | repl.Set("caddy_request_body_test_token", "s3cret") |
| 80 | |
| 81 | var gotBody string |
| 82 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 83 | body, err := io.ReadAll(r.Body) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | gotBody = string(body) |
| 88 | return nil |
| 89 | }) |
| 90 | |
| 91 | err := rb.ServeHTTP(httptest.NewRecorder(), req, next) |
| 92 | if err != nil { |
| 93 | t.Fatalf("did not expect an error, but got: %v", err) |
| 94 | } |
| 95 | |
| 96 | if gotBody != want { |
| 97 | t.Errorf("known placeholder not expanded: got %q, want %q", gotBody, want) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | // TestRequestBodySetHeaderPlaceholders verifies that HTTP request header placeholders |
| 102 | // resolve to an empty string when absent under ReplaceKnown, and expand to the header |
| 103 | // value when present, so the backend never receives literal placeholder braces. |
| 104 | func TestRequestBodySetHeaderPlaceholders(t *testing.T) { |
| 105 | for _, tc := range []struct { |
| 106 | name string |
| 107 | setBody string |
| 108 | setupReq func(req *http.Request) |
| 109 | wantBody string |
| 110 | }{ |
| 111 | { |
| 112 | name: "absent header resolves to empty string", |
| 113 | setBody: `{"api_key":"{http.request.header.X-Api-Key}"}`, |
| 114 | wantBody: `{"api_key":""}`, |
| 115 | }, |
| 116 | { |
| 117 | name: "present header resolves to value", |
| 118 | setBody: `{"api_key":"{http.request.header.X-Api-Key}"}`, |
| 119 | setupReq: func(req *http.Request) { |
| 120 | req.Header.Set("X-Api-Key", "secret-token") |
| 121 | }, |
| 122 | wantBody: `{"api_key":"secret-token"}`, |
| 123 | }, |
| 124 | } { |
| 125 | t.Run(tc.name, func(t *testing.T) { |
| 126 | rb := RequestBody{Set: tc.setBody} |
| 127 | |
| 128 | req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", nil) |
| 129 | if tc.setupReq != nil { |
| 130 | tc.setupReq(req) |
| 131 | } |
| 132 | _ = caddyhttp.NewTestReplacer(req) |
| 133 | |
| 134 | var gotBody string |
| 135 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 136 | body, err := io.ReadAll(r.Body) |
| 137 | if err != nil { |
| 138 | return err |
| 139 | } |
| 140 | gotBody = string(body) |
| 141 | return nil |
| 142 | }) |
| 143 | |
| 144 | err := rb.ServeHTTP(httptest.NewRecorder(), req, next) |
| 145 | if err != nil { |
| 146 | t.Fatalf("did not expect an error, but got: %v", err) |
| 147 | } |
| 148 | |
| 149 | if gotBody != tc.wantBody { |
| 150 | t.Errorf("got %q, want %q", gotBody, tc.wantBody) |
| 151 | } |
| 152 | }) |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | // TestRequestBodyMaxSize verifies that a declared Content-Length over the limit |
| 157 | // is rejected before the next handler runs, while bodies of unknown length still |
| 158 | // fail on read, both with the same 413 error. |
| 159 | func TestRequestBodyMaxSize(t *testing.T) { |
| 160 | const maxSize = 8 |
| 161 | |
| 162 | for _, tc := range []struct { |
| 163 | name string |
| 164 | body string |
| 165 | contentLength int64 |
| 166 | wantNext bool |
| 167 | wantErr bool |
| 168 | }{ |
| 169 | { |
| 170 | name: "declared length over the limit", |
| 171 | body: "0123456789", |
| 172 | contentLength: 10, |
| 173 | wantErr: true, |
| 174 | }, |
| 175 | { |
| 176 | name: "declared length within the limit", |
| 177 | body: "01234567", |
| 178 | contentLength: 8, |
| 179 | wantNext: true, |
| 180 | }, |
| 181 | { |
| 182 | name: "unknown length over the limit", |
| 183 | body: "0123456789", |
| 184 | contentLength: -1, |
| 185 | wantNext: true, |
| 186 | wantErr: true, |
| 187 | }, |
| 188 | } { |
| 189 | t.Run(tc.name, func(t *testing.T) { |
| 190 | rb := RequestBody{MaxSize: maxSize} |
| 191 | |
| 192 | req := httptest.NewRequest(http.MethodPost, "https://example.com/upload", strings.NewReader(tc.body)) |
| 193 | req.ContentLength = tc.contentLength |
| 194 | if tc.contentLength < 0 { |
| 195 | req.TransferEncoding = []string{"chunked"} |
| 196 | } |
| 197 | |
| 198 | var nextCalled bool |
| 199 | next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { |
| 200 | nextCalled = true |
| 201 | _, err := io.ReadAll(r.Body) |
| 202 | return err |
| 203 | }) |
| 204 | |
| 205 | err := rb.ServeHTTP(httptest.NewRecorder(), req, next) |
| 206 | |
| 207 | if nextCalled != tc.wantNext { |
| 208 | t.Errorf("next handler called = %t, want %t", nextCalled, tc.wantNext) |
| 209 | } |
| 210 | |
| 211 | if !tc.wantErr { |
| 212 | if err != nil { |
| 213 | t.Fatalf("did not expect an error, but got: %v", err) |
| 214 | } |
| 215 | return |
| 216 | } |
| 217 | |
| 218 | he, ok := errors.AsType[caddyhttp.HandlerError](err) |
| 219 | if !ok || he.StatusCode != http.StatusRequestEntityTooLarge { |
| 220 | t.Fatalf("want a %d handler error, got: %v", http.StatusRequestEntityTooLarge, err) |
| 221 | } |
| 222 | if mbe, ok := errors.AsType[*http.MaxBytesError](err); !ok || mbe.Limit != maxSize { |
| 223 | t.Errorf("want a *http.MaxBytesError with limit %d, got: %v", maxSize, err) |
| 224 | } |
| 225 | }) |
| 226 | } |
| 227 | } |