package edge import ( "bytes" "crypto/sha256" "encoding/hex" "log" "net" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" ) // sha256Hex is the test-side mirror of what FleetChannelAuth stores and // SetDynamicAuth consumes: the sha256 hex of a bearer token, never the // plaintext. func sha256Hex(s string) string { sum := sha256.Sum256([]byte(s)) return hex.EncodeToString(sum[:]) } // 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) } } } // Scheme-less credential: "Authorization: sekrit" must not authenticate even // though "sekrit" is the correct configured token — TrimPrefix would have let // this through unchanged (nothing to strip), which is the bug this pins. func TestTunnelRegisterRejectsSchemelessAuthorization(t *testing.T) { hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}) req := tunnelReq("pi-box", "") req.Header.Set("Authorization", "sekrit") // no "Bearer " prefix rec := httptest.NewRecorder() hub.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("scheme-less Authorization: got %d, want 401", 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 _, 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.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.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) rec := httptest.NewRecorder() hub.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("got %d, want 404", rec.Code) } } // TestTunnelRegisterAcceptsDynamicToken proves the second credential source: // a channel name containing "/" (the "/