package webgateway import ( "io" "net" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/gorilla/websocket" ) func TestGatewayServesStaticAndRoutesRESTAndOnlyStreamWebSocketPath(t *testing.T) { api := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { response.Header().Set("X-Upstream", "api") _, _ = io.WriteString(response, request.URL.Path) })) defer api.Close() session := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { response.Header().Set("X-Upstream", "session") _, _ = io.WriteString(response, request.URL.Path) })) defer session.Close() staticRoot := t.TempDir() if err := os.WriteFile(filepath.Join(staticRoot, "index.html"), []byte("

TERA

"), 0o644); err != nil { t.Fatal(err) } handler, err := NewHandler(Options{StaticRoot: staticRoot, APIUpstream: api.URL, SessionUpstream: session.URL}) if err != nil { t.Fatal(err) } server := httptest.NewServer(handler) defer server.Close() assertGatewayResponse(t, server.URL+"/api/v1/system/status", "api") assertGatewayResponse(t, server.URL+"/api/v1/direct/CH01/sessions", "api") assertGatewayResponse(t, server.URL+"/api/v1/direct/CH01/sessions/direct-1/stream", "session") response, err := http.Get(server.URL + "/") if err != nil { t.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if !strings.Contains(string(body), "TERA") || response.Header.Get("X-Content-Type-Options") != "nosniff" { t.Fatalf("static response body=%q headers=%v", body, response.Header) } if policy := response.Header.Get("Content-Security-Policy"); !strings.Contains(policy, "img-src 'self' data:") { t.Fatalf("content security policy does not permit the in-memory raster CAPTCHA: %q", policy) } } func TestGatewayProxiesWebSocketUpgradeToSessiond(t *testing.T) { forwardedSources := make(chan string, 1) upgrader := websocket.Upgrader{ Subprotocols: []string{"tera.direct.v1"}, CheckOrigin: func(*http.Request) bool { return true }, } session := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { forwardedSources <- request.Header.Get("X-Forwarded-For") connection, err := upgrader.Upgrade(response, request, nil) if err != nil { return } defer connection.Close() messageType, payload, err := connection.ReadMessage() if err == nil { _ = connection.WriteMessage(messageType, payload) } })) defer session.Close() api := httptest.NewServer(http.NotFoundHandler()) defer api.Close() staticRoot := t.TempDir() if err := os.WriteFile(filepath.Join(staticRoot, "index.html"), []byte("TERA"), 0o644); err != nil { t.Fatal(err) } handler, err := NewHandler(Options{StaticRoot: staticRoot, APIUpstream: api.URL, SessionUpstream: session.URL}) if err != nil { t.Fatal(err) } gateway := httptest.NewServer(handler) defer gateway.Close() wsURL := "ws" + strings.TrimPrefix(gateway.URL, "http") + "/api/v1/direct/CH01/sessions/direct-1/stream" dialer := websocket.Dialer{Subprotocols: []string{"tera.direct.v1"}} connection, _, err := dialer.Dial(wsURL, http.Header{ "Origin": []string{gateway.URL}, "X-Forwarded-For": []string{"203.0.113.200"}, }) if err != nil { t.Fatal(err) } defer connection.Close() want := []byte{0x00, 0x3a, 0xff} if err := connection.WriteMessage(websocket.BinaryMessage, want); err != nil { t.Fatal(err) } messageType, got, err := connection.ReadMessage() if err != nil || messageType != websocket.BinaryMessage || string(got) != string(want) { t.Fatalf("echo type=%d payload=%v err=%v", messageType, got, err) } forwarded := strings.TrimSpace(<-forwardedSources) if address := net.ParseIP(forwarded); address == nil || !address.IsLoopback() { t.Fatalf("forwarded source = %q, want actual loopback peer without spoofed chain", forwarded) } } func TestGatewayRejectsNonLoopbackUpstreams(t *testing.T) { if _, err := NewHandler(Options{ StaticRoot: t.TempDir(), APIUpstream: "http://example.com", SessionUpstream: "http://127.0.0.1:8081", }); err == nil { t.Fatal("expected external API upstream rejection") } } func assertGatewayResponse(t *testing.T, url, upstream string) { t.Helper() response, err := http.Get(url) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.Header.Get("X-Upstream") != upstream { t.Fatalf("%s routed to %q, want %q", url, response.Header.Get("X-Upstream"), upstream) } }