carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "maps"
5 "net/http"
6 "net/http/httptest"
7 "slices"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/registry"
11)
12
13// upsertAlias writes one route_aliases row, the shape the reconcile pass
14// (internal/domains) derives from a claim record. The edge only ever reads
15// Alias and Host; account/app are carried for realism.
16func upsertAlias(t *testing.T, reg *registry.Registry, alias, host string) {
17 t.Helper()
18 if err := reg.UpsertAlias(registry.RouteAlias{
19 Alias: alias, Host: host, Account: "acct", App: "app", CreatedAt: 1,
20 }); err != nil {
21 t.Fatalf("upsert alias %s -> %s: %v", alias, host, err)
22 }
23}
24
25// TestAliasServesPrimaryBackend is the feature itself: a hostname attached to
26// a ready route reaches that route's backend, with the customer's own Host
27// header preserved end to end (the app must see the name the browser asked
28// for, not the platform hostname).
29func TestAliasServesPrimaryBackend(t *testing.T) {
30 backend, seenHost := hostEchoBackend(t, "hello from the primary")
31 reg := openReg(t)
32 if err := reg.Upsert(registry.Route{
33 Host: "app.acct.oncarlos.com", Kind: "service",
34 Addr: backend.Listener.Addr().String(), Status: "ready",
35 }); err != nil {
36 t.Fatalf("upsert: %v", err)
37 }
38 upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com")
39
40 e, err := New(reg, nil)
41 if err != nil {
42 t.Fatalf("New: %v", err)
43 }
44 srv := httptest.NewServer(e)
45 defer srv.Close()
46
47 code, body := getHost(t, srv.URL, "www.customer.example")
48 if code != http.StatusOK {
49 t.Fatalf("status = %d, want 200 for an attached hostname", code)
50 }
51 if body != "hello from the primary" {
52 t.Fatalf("body = %q, want the primary route's backend body", body)
53 }
54 if *seenHost != "www.customer.example" {
55 t.Fatalf("backend saw Host %q, want www.customer.example (inbound Host must be preserved)", *seenHost)
56 }
57}
58
59// TestAliasSharesUpstreamEntry is the composition pin (#152, multi-upstream
60// HA): the alias must map to the primary's OWN entry — the identical proxy
61// handler value and the primary's route row — never a proxy rebuilt for the
62// alias hostname. Everything downstream is keyed off route.Host (activator
63// entry, lease, live symlink, cgroup) and off the proxy value's per-primary
64// state, so a second handler would silently fork both.
65func TestAliasSharesUpstreamEntry(t *testing.T) {
66 backend, _ := hostEchoBackend(t, "primary")
67 reg := openReg(t)
68 if err := reg.Upsert(registry.Route{
69 Host: "app.acct.oncarlos.com", Kind: "service",
70 Addr: backend.Listener.Addr().String(), Status: "ready",
71 }); err != nil {
72 t.Fatalf("upsert: %v", err)
73 }
74 upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com")
75
76 e, err := New(reg, nil)
77 if err != nil {
78 t.Fatalf("New: %v", err)
79 }
80 m := e.current()
81 primary, ok := m["app.acct.oncarlos.com"]
82 if !ok {
83 t.Fatalf("primary route missing from the upstream table")
84 }
85 alias, ok := m["www.customer.example"]
86 if !ok {
87 t.Fatalf("alias missing from the upstream table")
88 }
89 // Identity, not equivalent behaviour: the route carries no Version, so the
90 // handler is the *httputil.ReverseProxy pointer itself and == compares the
91 // pointer, which is exactly the claim being pinned.
92 if alias.proxy != primary.proxy {
93 t.Fatalf("alias proxy %p is not the primary's proxy %p — an alias must never get a rebuilt handler", alias.proxy, primary.proxy)
94 }
95 if alias.route.Host != "app.acct.oncarlos.com" {
96 t.Fatalf("alias route.Host = %q, want the PRIMARY's host (activator/version/purge keying all read it)", alias.route.Host)
97 }
98 if !alias.alias {
99 t.Fatalf("alias entry not marked alias — wildcard parent derivation would treat a customer domain as a platform host")
100 }
101 if primary.alias {
102 t.Fatalf("primary entry marked alias")
103 }
104}
105
106// TestAliasSharesMultiUpstreamEntry is TestAliasSharesUpstreamEntry against
107// the shape the #152 claim is actually about: a primary carrying an Addrs set
108// and a probe config, i.e. one served by multiProxy rather than the legacy
109// single-addr tcpProxy. Two things must hold, and only this shape can show
110// either.
111//
112// The alias must ride the primary's OWN multiProxy value. That handler is
113// closed over the PRIMARY's host and reads its live set from proberSet under
114// that key, so an alias handed a rebuilt proxy would look up a host nothing
115// probes, find an empty live set, and answer 503 + Retry-After for a route
116// whose upstreams are all healthy.
117//
118// And the alias must add NO prober of its own: probeSpecs is built from the
119// route pass only, before aliases are folded in, so an alias appearing there
120// would double every upstream's probe traffic and put a duplicate host on the
121// dashboard's per-upstream table.
122func TestAliasSharesMultiUpstreamEntry(t *testing.T) {
123 addrA, _ := countingBackend(t, "A")
124 addrB, _ := countingBackend(t, "B")
125 reg := openReg(t)
126 if err := reg.Upsert(registry.Route{
127 Host: "ha.acct.oncarlos.com", Kind: "service", Status: "ready",
128 Addrs: addrA + "," + addrB,
129 Health: healthJSON(t, frozenHealth()),
130 }); err != nil {
131 t.Fatalf("upsert: %v", err)
132 }
133 upsertAlias(t, reg, "www.customer.example", "ha.acct.oncarlos.com")
134
135 e, err := New(reg, nil)
136 if err != nil {
137 t.Fatalf("New: %v", err)
138 }
139 defer e.probers.Stop()
140
141 m := e.current()
142 primary, ok := m["ha.acct.oncarlos.com"]
143 if !ok {
144 t.Fatalf("primary route missing from the upstream table")
145 }
146 alias, ok := m["www.customer.example"]
147 if !ok {
148 t.Fatalf("alias missing from the upstream table")
149 }
150 // Identity, as in the single-addr pin above: no Version on the route, so
151 // the handler is the *httputil.ReverseProxy multiProxy returned and ==
152 // compares that pointer.
153 if alias.proxy != primary.proxy {
154 t.Fatalf("alias proxy %p is not the primary's multiProxy %p — a rebuilt handler would read an unprobed live set and 503 a healthy route", alias.proxy, primary.proxy)
155 }
156 if alias.route.Host != "ha.acct.oncarlos.com" {
157 t.Fatalf("alias route.Host = %q, want the PRIMARY's host", alias.route.Host)
158 }
159 // Exactly one probed host, and it is the primary.
160 st := e.probers.State()
161 if len(st) != 1 {
162 t.Fatalf("probed hosts = %v, want exactly one (the primary) — an alias must never get a prober of its own", slices.Sorted(maps.Keys(st)))
163 }
164 if _, ok := st["ha.acct.oncarlos.com"]; !ok {
165 t.Fatalf("probed hosts = %v, want the primary probed", slices.Sorted(maps.Keys(st)))
166 }
167 // And the whole point of the identity: a request over the attached
168 // hostname reaches a live upstream instead of multiProxy's dark-set 503.
169 srv := httptest.NewServer(e)
170 defer srv.Close()
171 code, body := getHost(t, srv.URL, "www.customer.example")
172 if code != http.StatusOK || (body != "A" && body != "B") {
173 t.Fatalf("alias request = %d %q, want 200 from one of the primary's upstreams", code, body)
174 }
175}
176
177// TestAliasAllowed covers the autocert HostPolicy: an attached hostname is a
178// host this box serves, so it may be certed; a stranger's SNI may not.
179func TestAliasAllowed(t *testing.T) {
180 backend, _ := hostEchoBackend(t, "primary")
181 reg := openReg(t)
182 if err := reg.Upsert(registry.Route{
183 Host: "app.acct.oncarlos.com", Kind: "service",
184 Addr: backend.Listener.Addr().String(), Status: "ready",
185 }); err != nil {
186 t.Fatalf("upsert: %v", err)
187 }
188 upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com")
189
190 e, err := New(reg, nil)
191 if err != nil {
192 t.Fatalf("New: %v", err)
193 }
194 if !e.Allowed("www.customer.example") {
195 t.Fatalf("Allowed(www.customer.example) = false, want true for an attached hostname")
196 }
197 if e.Allowed("stranger.example") {
198 t.Fatalf("Allowed(stranger.example) = true, want false")
199 }
200}
201
202// TestAliasNeverShadowsRoute: routes always win. An alias row whose hostname
203// collides with a real route must not displace it — the route's own backend
204// keeps answering.
205func TestAliasNeverShadowsRoute(t *testing.T) {
206 one, _ := hostEchoBackend(t, "one")
207 two, _ := hostEchoBackend(t, "two")
208 reg := openReg(t)
209 if err := reg.Upsert(registry.Route{
210 Host: "one.example.com", Kind: "service",
211 Addr: one.Listener.Addr().String(), Status: "ready",
212 }); err != nil {
213 t.Fatalf("upsert one: %v", err)
214 }
215 if err := reg.Upsert(registry.Route{
216 Host: "two.example.com", Kind: "service",
217 Addr: two.Listener.Addr().String(), Status: "ready",
218 }); err != nil {
219 t.Fatalf("upsert two: %v", err)
220 }
221 // An alias claiming a hostname that is already a route of its own.
222 upsertAlias(t, reg, "two.example.com", "one.example.com")
223
224 e, err := New(reg, nil)
225 if err != nil {
226 t.Fatalf("New: %v", err)
227 }
228 srv := httptest.NewServer(e)
229 defer srv.Close()
230
231 code, body := getHost(t, srv.URL, "two.example.com")
232 if code != http.StatusOK || body != "two" {
233 t.Fatalf("got %d %q, want 200 \"two\" — a real route must win a collision with an alias", code, body)
234 }
235 if up := e.current()["two.example.com"]; up.alias {
236 t.Fatalf("route entry for two.example.com was overwritten by the alias")
237 }
238}
239
240// TestAliasSkipsMissingPrimary: an alias whose primary is not in the table
241// (not ready here, or not local to this box) is skipped entirely rather than
242// mapped to nothing — it must 404 as an unknown host and must not be certed.
243func TestAliasSkipsMissingPrimary(t *testing.T) {
244 backend, _ := hostEchoBackend(t, "should never be reached")
245 reg := openReg(t)
246 if err := reg.Upsert(registry.Route{
247 Host: "pending.acct.oncarlos.com", Kind: "service",
248 Addr: backend.Listener.Addr().String(), Status: "pending",
249 }); err != nil {
250 t.Fatalf("upsert: %v", err)
251 }
252 upsertAlias(t, reg, "www.customer.example", "pending.acct.oncarlos.com")
253 // And one pointing at a host with no route row at all.
254 upsertAlias(t, reg, "gone.customer.example", "nosuch.acct.oncarlos.com")
255
256 e, err := New(reg, nil)
257 if err != nil {
258 t.Fatalf("New: %v", err)
259 }
260 srv := httptest.NewServer(e)
261 defer srv.Close()
262
263 for _, host := range []string{"www.customer.example", "gone.customer.example"} {
264 if code, _ := getHost(t, srv.URL, host); code != http.StatusNotFound {
265 t.Fatalf("status for %s = %d, want 404 (alias with no ready primary)", host, code)
266 }
267 if e.Allowed(host) {
268 t.Fatalf("Allowed(%s) = true, want false — no certificate for an alias to nowhere", host)
269 }
270 }
271}
272
273// routeRecordingActivator records the route each wake was handed, so a test
274// can prove which host the activator was keyed on.
275type routeRecordingActivator struct {
276 routes []registry.Route
277}
278
279func (a *routeRecordingActivator) ServeHTTP(w http.ResponseWriter, r *http.Request, rt registry.Route, proxy http.Handler) bool {
280 a.routes = append(a.routes, rt)
281 proxy.ServeHTTP(w, r) // "wake" is instant; proxy straight through
282 return true
283}
284
285func (a *routeRecordingActivator) RetireExcept(keep map[string]bool) {}
286
287// TestAliasWakesPrimaryOnce is the #129-class regression pin: a request to an
288// attached hostname wakes the PRIMARY's single activator entry. The activator
289// keys its entry, lease, live symlink and cgroup on route.Host, so an alias
290// arriving with its own host would start a second copy of the app beside the
291// first and fight it for the socket.
292func TestAliasWakesPrimaryOnce(t *testing.T) {
293 backend, _ := hostEchoBackend(t, "woken")
294 reg := openReg(t)
295 if err := reg.Upsert(registry.Route{
296 Host: "hib.acct.oncarlos.com", Kind: "instance",
297 Addr: backend.Listener.Addr().String(), Status: "ready", Hibernate: true,
298 }); err != nil {
299 t.Fatalf("upsert: %v", err)
300 }
301 upsertAlias(t, reg, "www.customer.example", "hib.acct.oncarlos.com")
302
303 act := &routeRecordingActivator{}
304 e, err := New(reg, act)
305 if err != nil {
306 t.Fatalf("New: %v", err)
307 }
308 srv := httptest.NewServer(e)
309 defer srv.Close()
310
311 code, body := getHost(t, srv.URL, "www.customer.example")
312 if code != http.StatusOK || body != "woken" {
313 t.Fatalf("got %d %q, want 200 woken", code, body)
314 }
315 if len(act.routes) != 1 {
316 t.Fatalf("activator saw %d wakes, want exactly 1", len(act.routes))
317 }
318 if got := act.routes[0].Host; got != "hib.acct.oncarlos.com" {
319 t.Fatalf("activator was handed route.Host = %q, want the primary hib.acct.oncarlos.com", got)
320 }
321}
322
323// TestCurrentHostsSkipsAliases: wildcard parent derivation reads currentHosts,
324// so a customer hostname riding a platform-minted primary must never appear
325// there — otherwise a name like x.y.<apps-domain> could mint a wildcard parent
326// and burn an ACME issuance on a domain the wildcard never covered.
327func TestCurrentHostsSkipsAliases(t *testing.T) {
328 backend, _ := hostEchoBackend(t, "primary")
329 reg := openReg(t)
330 if err := reg.Upsert(registry.Route{
331 Host: "app.acct.oncarlos.com", Kind: "service",
332 Addr: backend.Listener.Addr().String(), Status: "ready", Platform: true,
333 }); err != nil {
334 t.Fatalf("upsert: %v", err)
335 }
336 upsertAlias(t, reg, "shop.acct.oncarlos.com", "app.acct.oncarlos.com")
337
338 e, err := New(reg, nil)
339 if err != nil {
340 t.Fatalf("New: %v", err)
341 }
342 got := e.currentHosts()
343 if !slices.Contains(got, "app.acct.oncarlos.com") {
344 t.Fatalf("currentHosts = %v, want it to contain the platform-minted primary", got)
345 }
346 if slices.Contains(got, "shop.acct.oncarlos.com") {
347 t.Fatalf("currentHosts = %v, must NOT contain an attached hostname", got)
348 }
349}
350
351// TestWildcardAliasRequestRouting is Task 4's sub-change 3: a catch-all
352// alias row ("*.<host>", the shape Task 3's reconcile pass derives from a
353// claim's Catchall+Wildcard capability) must resolve one-label subdomains at
354// request time, folding into the SAME upstream map entry the exact primary
355// host resolves to (#151/#152's "identical map value" rule — activator
356// keying, version header and purge tenancy all read off that one entry).
357// Exact hosts always win a collision with the wildcard fallback, a wildcard
358// covers exactly one label (mirroring the TLS cert), and the apex must never
359// match its own wildcard.
360func TestWildcardAliasRequestRouting(t *testing.T) {
361 backend, seenHost := hostEchoBackend(t, "hello from the primary")
362 reg := openReg(t)
363 if err := reg.Upsert(registry.Route{
364 Host: "woodstar-relay.bab.oncarlos.com", Kind: "service",
365 Addr: backend.Listener.Addr().String(), Status: "ready",
366 }); err != nil {
367 t.Fatalf("upsert: %v", err)
368 }
369 upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com")
370
371 e, err := New(reg, nil)
372 if err != nil {
373 t.Fatalf("New: %v", err)
374 }
375
376 // Entry identity: paul.woodstar.app must resolve to the SAME upstream
377 // map value as the primary host — same *httputil.ReverseProxy pointer,
378 // same route row — not a value that merely behaves the same.
379 m := e.current()
380 primary, ok := m["woodstar-relay.bab.oncarlos.com"]
381 if !ok {
382 t.Fatalf("primary route missing from the upstream table")
383 }
384 star, ok := m["*.woodstar.app"]
385 if !ok {
386 t.Fatalf("catch-all alias missing from the upstream table")
387 }
388 if star.proxy != primary.proxy {
389 t.Fatalf("*.woodstar.app proxy %p is not the primary's proxy %p", star.proxy, primary.proxy)
390 }
391 if star.route.Host != primary.route.Host {
392 t.Fatalf("*.woodstar.app route.Host = %q, want the primary's %q", star.route.Host, primary.route.Host)
393 }
394
395 up, ok := e.lookupUpstream("paul.woodstar.app")
396 if !ok {
397 t.Fatalf("lookupUpstream(paul.woodstar.app) missed")
398 }
399 if up.proxy != primary.proxy {
400 t.Fatalf("lookupUpstream(paul.woodstar.app) resolved to a different entry than the primary (proxy %p vs %p)", up.proxy, primary.proxy)
401 }
402
403 srv := httptest.NewServer(e)
404 defer srv.Close()
405 code, body := getHost(t, srv.URL, "paul.woodstar.app")
406 if code != http.StatusOK || body != "hello from the primary" {
407 t.Fatalf("got %d %q, want 200 from the primary's backend", code, body)
408 }
409 if *seenHost != "paul.woodstar.app" {
410 t.Fatalf("backend saw Host %q, want paul.woodstar.app (inbound Host preserved)", *seenHost)
411 }
412
413 // Two labels deep: a wildcard covers exactly one label, mirroring the
414 // TLS cert — a.b.woodstar.app must miss.
415 if _, ok := e.lookupUpstream("a.b.woodstar.app"); ok {
416 t.Fatalf("lookupUpstream(a.b.woodstar.app) hit, want a miss (two labels — wildcard covers one)")
417 }
418
419 // The apex itself must never match its own wildcard: "woodstar.app" has
420 // no dot before "app", so the fallback would (wrongly) probe "*.app",
421 // never "*.woodstar.app" — this pins that the apex is never routed by
422 // its own catch-all.
423 if _, ok := e.lookupUpstream("woodstar.app"); ok {
424 t.Fatalf("lookupUpstream(woodstar.app) hit, want a miss (apex must not match its own wildcard)")
425 }
426
427 // An exact alias row wins over the wildcard fallback.
428 exactBackend, _ := hostEchoBackend(t, "exact wins")
429 if err := reg.Upsert(registry.Route{
430 Host: "other.bab.oncarlos.com", Kind: "service",
431 Addr: exactBackend.Listener.Addr().String(), Status: "ready",
432 }); err != nil {
433 t.Fatalf("upsert other: %v", err)
434 }
435 upsertAlias(t, reg, "paul.woodstar.app", "other.bab.oncarlos.com")
436 if err := e.Reload(); err != nil {
437 t.Fatalf("reload: %v", err)
438 }
439 up, ok = e.lookupUpstream("paul.woodstar.app")
440 if !ok {
441 t.Fatalf("lookupUpstream(paul.woodstar.app) missed after exact alias added")
442 }
443 if up.route.Host != "other.bab.oncarlos.com" {
444 t.Fatalf("lookupUpstream(paul.woodstar.app) route.Host = %q, want the EXACT alias's primary other.bab.oncarlos.com", up.route.Host)
445 }
446}
447
448// TestHostPolicyExcludesWildcardFallback is the CRITICAL-1 review fix: the
449// autocert HostPolicy (Allowed) must stay on the EXACT map only, never
450// lookupUpstream's wildcard fallback. A catch-all alias ("*.<domain>") gets
451// its TLS coverage from the WILDCARD cert (wrapGetCertificate checks certFor
452// before ever reaching autocert) — Allowed only gates a NEW per-host ACME
453// order, which is unbounded (attacker-chosen labels) and shared across every
454// tenant on the box (one ACME account). Before this fix, a stranger's SNI
455// random.woodstar.app would pass Allowed and drive a fresh per-host order
456// for as long as the wildcard cert wasn't yet obtained — the normal state
457// right after an attach, before the customer's CNAME exists.
458//
459// Request-time routing (lookupUpstream, unaffected by this fix) must still
460// resolve the same one-label wildcard host — the policy split is HostPolicy
461// vs. serving, not a routing regression.
462func TestHostPolicyExcludesWildcardFallback(t *testing.T) {
463 backend, _ := hostEchoBackend(t, "primary")
464 reg := openReg(t)
465 if err := reg.Upsert(registry.Route{
466 Host: "woodstar-relay.bab.oncarlos.com", Kind: "service",
467 Addr: backend.Listener.Addr().String(), Status: "ready",
468 }); err != nil {
469 t.Fatalf("upsert: %v", err)
470 }
471 upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com")
472
473 e, err := New(reg, nil)
474 if err != nil {
475 t.Fatalf("New: %v", err)
476 }
477
478 // (a) Request routing is unchanged: the wildcard-alias host still
479 // resolves at request time.
480 if _, ok := e.lookupUpstream("paul.woodstar.app"); !ok {
481 t.Fatalf("lookupUpstream(paul.woodstar.app) missed — routing must still resolve via the wildcard alias")
482 }
483
484 // (b) HostPolicy refuses ANY one-label host under the catch-all that
485 // isn't an exact route/alias row of its own — only exact hosts are
486 // certable via the per-host path.
487 if e.Allowed("random.woodstar.app") {
488 t.Fatalf("Allowed(random.woodstar.app) = true, want false — HostPolicy must not follow the wildcard fallback (unbounded per-host ACME orders from attacker-chosen SNI)")
489 }
490 if e.Allowed("paul.woodstar.app") {
491 t.Fatalf("Allowed(paul.woodstar.app) = true, want false — same rule, no exact row for this host either")
492 }
493
494 // An EXACT alias row is still certable, same as any other routed host.
495 upsertAlias(t, reg, "paul.woodstar.app", "woodstar-relay.bab.oncarlos.com")
496 if err := e.Reload(); err != nil {
497 t.Fatalf("reload: %v", err)
498 }
499 if !e.Allowed("paul.woodstar.app") {
500 t.Fatalf("Allowed(paul.woodstar.app) = false, want true once it has its OWN exact alias row")
501 }
502 // The still-wildcard-only host stays refused.
503 if e.Allowed("random.woodstar.app") {
504 t.Fatalf("Allowed(random.woodstar.app) = true, want false — still no exact row for it")
505 }
506 // The wildcard label itself must never be certable — even though it is
507 // the exact map key backing the catch-all fallback, refused locally
508 // rather than resting on autocert's own IDN rejection of "*".
509 if e.Allowed("*.woodstar.app") {
510 t.Fatalf("Allowed(*.woodstar.app) = true, want false — the wildcard label itself must never be certable")
511 }
512}
513
514// TestHTTPFallbackRedirectsWildcardAlias is the round-2 review fix: the :80
515// handler (httpFallbackHandler) is a THIRD Allowed() consumer the first fix
516// pass missed. It answers a routing question ("does something serve this
517// host on 443?"), not an issuance question, so it must use lookupUpstream —
518// any host that routes via the *.woodstar.app catch-all (paul.woodstar.app,
519// random.woodstar.app, any label) gets the classic 302 to https, even though
520// none of them has its own exact row and Allowed() (HostPolicy) refuses all
521// of them. A host that doesn't route at all still gets unknownHost (404 with
522// no fallback URL configured).
523func TestHTTPFallbackRedirectsWildcardAlias(t *testing.T) {
524 backend, _ := hostEchoBackend(t, "primary")
525 reg := openReg(t)
526 if err := reg.Upsert(registry.Route{
527 Host: "woodstar-relay.bab.oncarlos.com", Kind: "service",
528 Addr: backend.Listener.Addr().String(), Status: "ready",
529 }); err != nil {
530 t.Fatalf("upsert: %v", err)
531 }
532 upsertAlias(t, reg, "*.woodstar.app", "woodstar-relay.bab.oncarlos.com")
533
534 e, err := New(reg, nil)
535 if err != nil {
536 t.Fatalf("New: %v", err)
537 }
538 h := e.httpFallbackHandler()
539
540 for _, host := range []string{"paul.woodstar.app", "random.woodstar.app"} {
541 req := httptest.NewRequest(http.MethodGet, "http://"+host+"/", nil)
542 req.Host = host
543 rec := httptest.NewRecorder()
544 h.ServeHTTP(rec, req)
545 if rec.Code != http.StatusFound {
546 t.Fatalf("%s: status = %d, want 302 — it routes via the *.woodstar.app catch-all even with no exact row", host, rec.Code)
547 }
548 if loc := rec.Header().Get("Location"); loc != "https://"+host+"/" {
549 t.Fatalf("%s: Location = %q, want https://%s/", host, loc, host)
550 }
551 }
552
553 // A host that doesn't route at all (no exact route, no matching
554 // wildcard alias) still hits unknownHost — no fallback URL configured
555 // here, so a plain 404.
556 req := httptest.NewRequest(http.MethodGet, "http://nowhere.example.com/", nil)
557 req.Host = "nowhere.example.com"
558 rec := httptest.NewRecorder()
559 h.ServeHTTP(rec, req)
560 if rec.Code != http.StatusNotFound {
561 t.Fatalf("nowhere.example.com: status = %d, want 404 (unknownHost, no fallback URL)", rec.Code)
562 }
563}
564
565// TestHTTPFallbackRedirectsTunnelHost is the final-review fix: the old inline
566// :80 closure gated on Allowed(host), which special-cases the reserved tunnel
567// hostname (see Allowed's own e.tunnels check); the extracted handler gates
568// on lookupUpstream alone, which has no tunnel branch at all, so plain
569// http://<tunnel-host>/ regressed to unknownHost instead of the classic
570// redirect into the hub's own TLS handshake on 443.
571func TestHTTPFallbackRedirectsTunnelHost(t *testing.T) {
572 e, err := New(openReg(t), nil)
573 if err != nil {
574 t.Fatalf("New: %v", err)
575 }
576 e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}))
577 if err := e.Reload(); err != nil {
578 t.Fatalf("reload: %v", err)
579 }
580 h := e.httpFallbackHandler()
581
582 req := httptest.NewRequest(http.MethodGet, "http://tunnel.test/register", nil)
583 req.Host = "tunnel.test"
584 rec := httptest.NewRecorder()
585 h.ServeHTTP(rec, req)
586 if rec.Code != http.StatusFound {
587 t.Fatalf("tunnel.test: status = %d, want 302 — the reserved tunnel hostname answers on 443 and must redirect like any other routable host", rec.Code)
588 }
589 if loc := rec.Header().Get("Location"); loc != "https://tunnel.test/register" {
590 t.Fatalf("tunnel.test: Location = %q, want https://tunnel.test/register", loc)
591 }
592}
593