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)
}
}