| index 554dfe36b..d75e97c31 100644 |
| --- a/internal/edge/tunnel_test.go |
| +++ b/internal/edge/tunnel_test.go |
| @@ -3,28 +3,108 @@ package edge |
| import ( |
| "net/http" |
| "net/http/httptest" |
| + "strings" |
| "testing" |
| ) |
| |
| +// tunnelReq builds a request that is well-formed EXCEPT for the credentials |
| +// under test: valid upgrade headers, so the auth cases below can only fail on |
| +// auth. Pass an empty token to send no Authorization header at all. |
| +func tunnelReq(name, token string) *http.Request { |
| + req := httptest.NewRequest("GET", "/register", nil) |
| + req.Header.Set("Connection", "Upgrade") |
| + req.Header.Set("Upgrade", tunnelUpgradeProto) |
| + req.Header.Set("Carlos-Tunnel-Name", name) |
| + if token != "" { |
| + req.Header.Set("Authorization", "Bearer "+token) |
| + } |
| + return req |
| +} |
| + |
| func TestTunnelRegisterRejectsBadToken(t *testing.T) { |
| + // TWO clients on purpose: with only one configured, no test here can tell |
| + // "every token is bound to its own channel" apart from "any known token |
| + // admits any known channel" — an implementation looping over h.clients |
| + // would pass a single-client table unchanged. |
| + hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit", "hq": "other"}) |
| + for _, tc := range []struct{ desc, name, token string }{ |
| + {"no auth", "", ""}, |
| + {"wrong token", "pi-box", "nope"}, |
| + {"unknown name", "nosuch", "sekrit"}, |
| + {"empty name", "", "sekrit"}, |
| + // The binding property itself: a valid token may only ever claim ITS |
| + // OWN channel name, in either direction. |
| + {"cross-name", "hq", "sekrit"}, |
| + {"cross-name reversed", "pi-box", "other"}, |
| + } { |
| + rec := httptest.NewRecorder() |
| + hub.ServeHTTP(rec, tunnelReq(tc.name, tc.token)) |
| + if rec.Code != http.StatusUnauthorized { |
| + t.Errorf("%s (name=%q token=%q): got %d, want 401", tc.desc, tc.name, tc.token, rec.Code) |
| + } |
| + } |
| +} |
| + |
| +func TestTunnelRegisterAuthPrecedesUpgradeCheck(t *testing.T) { |
| + hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}) |
| + req := httptest.NewRequest("GET", "/register", nil) // no upgrade headers at all |
| + req.Header.Set("Authorization", "Bearer nope") |
| + req.Header.Set("Carlos-Tunnel-Name", "pi-box") |
| + rec := httptest.NewRecorder() |
| + hub.ServeHTTP(rec, req) |
| + if rec.Code != http.StatusUnauthorized { |
| + t.Fatalf("got %d, want 401 — an unauthenticated caller must not learn which upgrade protocols we speak", rec.Code) |
| + } |
| +} |
| + |
| +func TestTunnelRegisterRejectsWrongUpgrade(t *testing.T) { |
| hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}) |
| - for name, hdr := range map[string]http.Header{ |
| - "no auth": {}, |
| - "wrong token": {"Authorization": {"Bearer nope"}, "Carlos-Tunnel-Name": {"pi-box"}}, |
| - "unknown name": {"Authorization": {"Bearer sekrit"}, "Carlos-Tunnel-Name": {"other"}}, |
| - // A valid token may only claim ITS OWN channel name. |
| - "cross-name": {"Authorization": {"Bearer sekrit"}, "Carlos-Tunnel-Name": {""}}, |
| + for _, tc := range []struct{ desc, conn, upgrade string }{ |
| + {"no upgrade headers", "", ""}, |
| + {"missing Upgrade", "Upgrade", ""}, |
| + {"missing Connection", "", tunnelUpgradeProto}, |
| + {"wrong version", "Upgrade", "carlos-tunnel/2"}, |
| + {"other protocol", "Upgrade", "websocket"}, |
| } { |
| req := httptest.NewRequest("GET", "/register", nil) |
| - req.Header = hdr |
| + req.Header.Set("Authorization", "Bearer sekrit") |
| + req.Header.Set("Carlos-Tunnel-Name", "pi-box") |
| + if tc.conn != "" { |
| + req.Header.Set("Connection", tc.conn) |
| + } |
| + if tc.upgrade != "" { |
| + req.Header.Set("Upgrade", tc.upgrade) |
| + } |
| rec := httptest.NewRecorder() |
| hub.ServeHTTP(rec, req) |
| - if rec.Code != http.StatusUnauthorized { |
| - t.Errorf("%s: got %d, want 401", name, rec.Code) |
| + if rec.Code != http.StatusBadRequest { |
| + t.Errorf("%s: got %d, want 400", tc.desc, rec.Code) |
| + continue |
| + } |
| + // A future carlos-tunnel/2 host must get a diagnosable refusal. |
| + if !strings.Contains(rec.Body.String(), tunnelUpgradeProto) { |
| + t.Errorf("%s: body %q should name the expected protocol", tc.desc, rec.Body.String()) |
| } |
| } |
| } |
| |
| +func TestTunnelRegisterAcceptsTokenWiseConnectionHeader(t *testing.T) { |
| + hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}) |
| + req := httptest.NewRequest("GET", "/register", nil) |
| + req.Header.Set("Authorization", "Bearer sekrit") |
| + req.Header.Set("Carlos-Tunnel-Name", "pi-box") |
| + req.Header.Set("Connection", "keep-alive, Upgrade") // token-wise, per RFC 7230 |
| + req.Header.Set("Upgrade", "Carlos-Tunnel/1") // protocol names are case-insensitive |
| + rec := httptest.NewRecorder() |
| + hub.ServeHTTP(rec, req) |
| + // This request passes auth AND the upgrade gate, then dies at the hijack, |
| + // which httptest.NewRecorder cannot do — so 500 (not 400) is how we know |
| + // the gate let it through. The real 101 is Task 10's e2e. |
| + if rec.Code != http.StatusInternalServerError { |
| + t.Fatalf("got %d, want 500 (past the upgrade gate, stopped at the hijack)", rec.Code) |
| + } |
| +} |
| + |
| func TestTunnelRegisterRejectsUnknownPath(t *testing.T) { |
| hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}) |
| req := httptest.NewRequest("GET", "/anything-else", nil) |