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 httpcaddyfile |
| 2 | |
| 3 | import ( |
| 4 | "reflect" |
| 5 | "slices" |
| 6 | "sort" |
| 7 | "strings" |
| 8 | "sync" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" |
| 12 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" |
| 13 | ) |
| 14 | |
| 15 | func TestHostsFromKeys(t *testing.T) { |
| 16 | for i, tc := range []struct { |
| 17 | keys []Address |
| 18 | expectNormalMode []string |
| 19 | expectLoggerMode []string |
| 20 | }{ |
| 21 | { |
| 22 | []Address{ |
| 23 | {Original: "foo", Host: "foo"}, |
| 24 | }, |
| 25 | []string{"foo"}, |
| 26 | []string{"foo"}, |
| 27 | }, |
| 28 | { |
| 29 | []Address{ |
| 30 | {Original: "foo", Host: "foo"}, |
| 31 | {Original: "bar", Host: "bar"}, |
| 32 | }, |
| 33 | []string{"bar", "foo"}, |
| 34 | []string{"bar", "foo"}, |
| 35 | }, |
| 36 | { |
| 37 | []Address{ |
| 38 | {Original: ":2015", Port: "2015"}, |
| 39 | }, |
| 40 | []string{}, |
| 41 | []string{}, |
| 42 | }, |
| 43 | { |
| 44 | []Address{ |
| 45 | {Original: ":443", Port: "443"}, |
| 46 | }, |
| 47 | []string{}, |
| 48 | []string{}, |
| 49 | }, |
| 50 | { |
| 51 | []Address{ |
| 52 | {Original: "foo", Host: "foo"}, |
| 53 | {Original: ":2015", Port: "2015"}, |
| 54 | }, |
| 55 | []string{}, |
| 56 | []string{"foo"}, |
| 57 | }, |
| 58 | { |
| 59 | []Address{ |
| 60 | {Original: "example.com:2015", Host: "example.com", Port: "2015"}, |
| 61 | }, |
| 62 | []string{"example.com"}, |
| 63 | []string{"example.com:2015"}, |
| 64 | }, |
| 65 | { |
| 66 | []Address{ |
| 67 | {Original: "example.com:80", Host: "example.com", Port: "80"}, |
| 68 | }, |
| 69 | []string{"example.com"}, |
| 70 | []string{"example.com"}, |
| 71 | }, |
| 72 | { |
| 73 | []Address{ |
| 74 | {Original: "https://:2015/foo", Scheme: "https", Port: "2015", Path: "/foo"}, |
| 75 | }, |
| 76 | []string{}, |
| 77 | []string{}, |
| 78 | }, |
| 79 | { |
| 80 | []Address{ |
| 81 | {Original: "https://example.com:2015/foo", Scheme: "https", Host: "example.com", Port: "2015", Path: "/foo"}, |
| 82 | }, |
| 83 | []string{"example.com"}, |
| 84 | []string{"example.com:2015"}, |
| 85 | }, |
| 86 | } { |
| 87 | sb := serverBlock{parsedKeys: tc.keys} |
| 88 | |
| 89 | // test in normal mode |
| 90 | actual := sb.hostsFromKeys(false) |
| 91 | sort.Strings(actual) |
| 92 | if !reflect.DeepEqual(tc.expectNormalMode, actual) { |
| 93 | t.Errorf("Test %d (loggerMode=false): Expected: %v Actual: %v", i, tc.expectNormalMode, actual) |
| 94 | } |
| 95 | |
| 96 | // test in logger mode |
| 97 | actual = sb.hostsFromKeys(true) |
| 98 | sort.Strings(actual) |
| 99 | if !reflect.DeepEqual(tc.expectLoggerMode, actual) { |
| 100 | t.Errorf("Test %d (loggerMode=true): Expected: %v Actual: %v", i, tc.expectLoggerMode, actual) |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | // test_ordered_plugin stands in for a plugin directive in the tests below. |
| 106 | // it registers from init() like a real plugin would, which also keeps it to |
| 107 | // once per process, since registering twice panics. |
| 108 | func init() { |
| 109 | RegisterHandlerDirective("test_ordered_plugin", func(h Helper) (caddyhttp.MiddlewareHandler, error) { |
| 110 | h.Next() |
| 111 | return caddyhttp.StaticError{StatusCode: "500"}, nil |
| 112 | }) |
| 113 | RegisterDirectiveOrder("test_ordered_plugin", Before, "respond") |
| 114 | |
| 115 | RegisterHandlerDirective("test_named_route", func(h Helper) (caddyhttp.MiddlewareHandler, error) { |
| 116 | h.Next() |
| 117 | body := "site" |
| 118 | if h.IsNamedRoute() { |
| 119 | body = "named" |
| 120 | } |
| 121 | return caddyhttp.StaticResponse{Body: body}, nil |
| 122 | }) |
| 123 | RegisterDirectiveOrder("test_named_route", Before, "respond") |
| 124 | } |
| 125 | |
| 126 | func TestHelperIsNamedRoute(t *testing.T) { |
| 127 | adapted := adaptCaddyfile(t, `&(shared) { |
| 128 | handle /nested { |
| 129 | test_named_route |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | :8080 { |
| 134 | test_named_route |
| 135 | invoke shared |
| 136 | } |
| 137 | `) |
| 138 | |
| 139 | for _, body := range []string{`"body":"named"`, `"body":"site"`} { |
| 140 | if !strings.Contains(adapted, body) { |
| 141 | t.Errorf("expected %s in the adapted config: %s", body, adapted) |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | // caddyfiles shared by the directive order tests below. the handle block in |
| 147 | // orderTestPlain is there so that the subroute path gets covered too, not |
| 148 | // just the top level of the site block, and orderTestReordered moves respond |
| 149 | // ahead of root so it has to adapt to something different. |
| 150 | const ( |
| 151 | orderTestPlain = `:8080 { |
| 152 | root * /srv |
| 153 | handle /foo { |
| 154 | respond 200 |
| 155 | root * /other |
| 156 | } |
| 157 | } |
| 158 | ` |
| 159 | orderTestReordered = "{\n\torder respond first\n}\n" + orderTestPlain |
| 160 | |
| 161 | // covers every positional, since first and last build the new order in |
| 162 | // their own way and last can append in place |
| 163 | orderTestConcurrent = `{ |
| 164 | order redir before respond |
| 165 | order vars after root |
| 166 | order root first |
| 167 | order respond last |
| 168 | } |
| 169 | :8080 { |
| 170 | respond 200 |
| 171 | } |
| 172 | ` |
| 173 | ) |
| 174 | |
| 175 | // adaptCaddyfile adapts the given Caddyfile with a fresh options map. |
| 176 | func adaptCaddyfile(t *testing.T, input string) string { |
| 177 | t.Helper() |
| 178 | return adaptWithOptions(t, input, nil) |
| 179 | } |
| 180 | |
| 181 | // adaptWithOptions adapts the given Caddyfile with the caller's options map. |
| 182 | func adaptWithOptions(t *testing.T, input string, options map[string]any) string { |
| 183 | t.Helper() |
| 184 | adapter := caddyfile.Adapter{ServerType: ServerType{}} |
| 185 | out, _, err := adapter.Adapt([]byte(input), options) |
| 186 | if err != nil { |
| 187 | t.Fatalf("adapting: %v", err) |
| 188 | } |
| 189 | return string(out) |
| 190 | } |
| 191 | |
| 192 | // adaptConcurrently adapts a Caddyfile using the order global option from |
| 193 | // several goroutines at once. A non-nil options map is shared by all of |
| 194 | // them; nil gives each adaptation its own. Run with -race. |
| 195 | func adaptConcurrently(t *testing.T, options map[string]any) { |
| 196 | t.Helper() |
| 197 | |
| 198 | var wg sync.WaitGroup |
| 199 | for range 8 { |
| 200 | wg.Add(1) |
| 201 | go func() { |
| 202 | defer wg.Done() |
| 203 | for range 200 { |
| 204 | adapter := caddyfile.Adapter{ServerType: ServerType{}} |
| 205 | if _, _, err := adapter.Adapt([]byte(orderTestConcurrent), options); err != nil { |
| 206 | t.Errorf("adapting concurrently: %v", err) |
| 207 | return |
| 208 | } |
| 209 | } |
| 210 | }() |
| 211 | } |
| 212 | wg.Wait() |
| 213 | } |
| 214 | |
| 215 | // handlerComesFirst reports whether handler a appears before handler b in |
| 216 | // the adapted JSON. |
| 217 | func handlerComesFirst(adapted, a, b string) bool { |
| 218 | ai := strings.Index(adapted, `"handler":"`+a+`"`) |
| 219 | bi := strings.Index(adapted, `"handler":"`+b+`"`) |
| 220 | return ai >= 0 && bi >= 0 && ai < bi |
| 221 | } |
| 222 | |
| 223 | // TestAdaptDirectiveOrderIsolation makes sure the order global option only |
| 224 | // applies to the adaptation it was written in. |
| 225 | func TestAdaptDirectiveOrderIsolation(t *testing.T) { |
| 226 | baseline := adaptCaddyfile(t, orderTestPlain) |
| 227 | |
| 228 | // this one moves respond to the front, so it has to produce something |
| 229 | // different than the default order does |
| 230 | if out := adaptCaddyfile(t, orderTestReordered); out == baseline { |
| 231 | t.Fatal("the order option had no effect, so this test proves nothing") |
| 232 | } |
| 233 | |
| 234 | if out := adaptCaddyfile(t, orderTestPlain); out != baseline { |
| 235 | t.Errorf("order option leaked into the next adaptation:\nExpected: %s\nActual: %s", baseline, out) |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | // TestAdaptDirectiveOrderAfterError makes sure a failed adaptation leaves |
| 240 | // nothing behind for the next one. |
| 241 | func TestAdaptDirectiveOrderAfterError(t *testing.T) { |
| 242 | baseline := adaptCaddyfile(t, orderTestPlain) |
| 243 | |
| 244 | const broken = `{ |
| 245 | order respond before not_a_directive |
| 246 | } |
| 247 | :8080 { |
| 248 | respond 200 |
| 249 | } |
| 250 | ` |
| 251 | adapter := caddyfile.Adapter{ServerType: ServerType{}} |
| 252 | if _, _, err := adapter.Adapt([]byte(broken), nil); err == nil { |
| 253 | t.Fatal("expected an error for an order option pointing at an unknown directive") |
| 254 | } |
| 255 | |
| 256 | if out := adaptCaddyfile(t, orderTestPlain); out != baseline { |
| 257 | t.Errorf("failed adaptation left the order broken:\nExpected: %s\nActual: %s", baseline, out) |
| 258 | } |
| 259 | } |
| 260 | |
| 261 | // TestAdaptDirectiveOrderWithReusedOptions makes sure that handing the same |
| 262 | // options map to one adaptation after another doesn't carry the first one's |
| 263 | // order into the second, and that the caller's map is left alone. |
| 264 | func TestAdaptDirectiveOrderWithReusedOptions(t *testing.T) { |
| 265 | baseline := adaptCaddyfile(t, orderTestPlain) |
| 266 | |
| 267 | options := map[string]any{} |
| 268 | |
| 269 | if out := adaptWithOptions(t, orderTestReordered, options); out == baseline { |
| 270 | t.Fatal("the order option had no effect, so this test proves nothing") |
| 271 | } |
| 272 | if out := adaptWithOptions(t, orderTestPlain, options); out != baseline { |
| 273 | t.Errorf("reused options map carried the order into the next adaptation:\nExpected: %s\nActual: %s", baseline, out) |
| 274 | } |
| 275 | if len(options) > 0 { |
| 276 | t.Errorf("adapting wrote into the caller's options map: %v", options) |
| 277 | } |
| 278 | |
| 279 | // an order the caller put in the map itself is ignored as well, and the |
| 280 | // slice behind it is never written to; if it were used here, root would |
| 281 | // not be an ordered handler and adapting would fail |
| 282 | callerOrder := []string{"respond"} |
| 283 | options["order"] = callerOrder |
| 284 | if out := adaptWithOptions(t, orderTestPlain, options); out != baseline { |
| 285 | t.Errorf("an order from the caller's map was used:\nExpected: %s\nActual: %s", baseline, out) |
| 286 | } |
| 287 | if !slices.Equal(callerOrder, []string{"respond"}) { |
| 288 | t.Errorf("adapting modified the caller's order: %v", callerOrder) |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | // TestConcurrentAdaptDirectiveOrder makes sure that adapting several |
| 293 | // Caddyfiles at once, all of them using the order global option, doesn't |
| 294 | // race or corrupt the order shared by the whole process. |
| 295 | // See https://github.com/caddyserver/caddy/issues/7994 |
| 296 | func TestConcurrentAdaptDirectiveOrder(t *testing.T) { |
| 297 | baseline := slices.Clone(directiveOrder) |
| 298 | defaultBaseline := slices.Clone(defaultDirectiveOrder) |
| 299 | |
| 300 | adaptConcurrently(t, nil) |
| 301 | |
| 302 | if !slices.Equal(directiveOrder, baseline) { |
| 303 | t.Errorf("adaptation changed the process-wide order:\nExpected: %v\nActual: %v", baseline, directiveOrder) |
| 304 | } |
| 305 | if !slices.Equal(defaultDirectiveOrder, defaultBaseline) { |
| 306 | t.Errorf("adaptation changed the default order:\nExpected: %v\nActual: %v", defaultBaseline, defaultDirectiveOrder) |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | // TestConcurrentAdaptDirectiveOrderWithReusedOptions is the same, except |
| 311 | // every goroutine is handed the same options map. |
| 312 | func TestConcurrentAdaptDirectiveOrderWithReusedOptions(t *testing.T) { |
| 313 | baseline := slices.Clone(directiveOrder) |
| 314 | defaultBaseline := slices.Clone(defaultDirectiveOrder) |
| 315 | options := map[string]any{} |
| 316 | |
| 317 | adaptConcurrently(t, options) |
| 318 | |
| 319 | if !slices.Equal(directiveOrder, baseline) { |
| 320 | t.Errorf("adaptation changed the process-wide order:\nExpected: %v\nActual: %v", baseline, directiveOrder) |
| 321 | } |
| 322 | if !slices.Equal(defaultDirectiveOrder, defaultBaseline) { |
| 323 | t.Errorf("adaptation changed the default order:\nExpected: %v\nActual: %v", defaultBaseline, defaultDirectiveOrder) |
| 324 | } |
| 325 | if len(options) > 0 { |
| 326 | t.Errorf("adapting wrote into the caller's options map: %v", options) |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | // TestRegisterDirectiveOrderWithOrderOption makes sure an order registered |
| 331 | // by a plugin is still honored, and can still be overridden by the order |
| 332 | // global option without the override sticking around afterwards. |
| 333 | func TestRegisterDirectiveOrderWithOrderOption(t *testing.T) { |
| 334 | if !slices.Contains(directiveOrder, "test_ordered_plugin") { |
| 335 | t.Fatal("plugin directive was not added to the order") |
| 336 | } |
| 337 | if !slices.Contains(defaultDirectiveOrder, "respond") || slices.Contains(defaultDirectiveOrder, "test_ordered_plugin") { |
| 338 | t.Errorf("registering a plugin order corrupted the default order: %v", defaultDirectiveOrder) |
| 339 | } |
| 340 | |
| 341 | const site = `:8080 { |
| 342 | respond 200 |
| 343 | test_ordered_plugin |
| 344 | } |
| 345 | ` |
| 346 | // with no order option, the registered position wins |
| 347 | if out := adaptCaddyfile(t, site); !handlerComesFirst(out, "error", "static_response") { |
| 348 | t.Errorf("expected the plugin directive before respond, got: %s", out) |
| 349 | } |
| 350 | |
| 351 | // the order option overrides it |
| 352 | overridden := adaptCaddyfile(t, "{\n\torder test_ordered_plugin after respond\n}\n"+site) |
| 353 | if !handlerComesFirst(overridden, "static_response", "error") { |
| 354 | t.Errorf("expected respond before the plugin directive, got: %s", overridden) |
| 355 | } |
| 356 | |
| 357 | // and the override is gone again |
| 358 | if out := adaptCaddyfile(t, site); !handlerComesFirst(out, "error", "static_response") { |
| 359 | t.Errorf("the order option leaked past its own adaptation, got: %s", out) |
| 360 | } |
| 361 | } |