package webdevice import ( "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func TestRewriteLocation(t *testing.T) { t.Parallel() targetURL, _ := url.Parse("http://192.168.1.124:80") got := RewriteLocation("192.168.1.124", targetURL, "http://192.168.1.124/ISAPI/Security") if got != "/proxy/web/192.168.1.124/ISAPI/Security" { t.Fatalf("RewriteLocation() = %q", got) } } func TestRewriteSetCookie(t *testing.T) { t.Parallel() got := RewriteSetCookie("SID=1; Path=/; Domain=192.168.1.124; HttpOnly", "/proxy/web/192.168.1.124") if strings.Contains(strings.ToLower(got), "domain=") { t.Fatalf("RewriteSetCookie() kept domain: %q", got) } if !strings.Contains(got, "Path=/proxy/web/192.168.1.124/") { t.Fatalf("RewriteSetCookie() path = %q", got) } } func TestRewriteText(t *testing.T) { t.Parallel() targetURL, _ := url.Parse("http://192.168.1.124:80") body := `x` got := RewriteText(body, "/proxy/web/192.168.1.124", targetURL, "text/html") if !strings.Contains(got, `/proxy/web/192.168.1.124/doc/logo.png`) { t.Fatalf("rewritten body missing proxied relative URL: %s", got) } if !strings.Contains(got, `data-web-proxy-runtime`) { t.Fatalf("rewritten body missing runtime injection: %s", got) } } func TestRewriteTextRewritesScriptAbsoluteURLs(t *testing.T) { t.Parallel() targetURL, _ := url.Parse("http://192.168.1.124:80") body := `window.location.href="/doc/page/login.asp";fetch("/ISAPI/System/time")` got := RewriteText(body, "/proxy/web/192.168.1.124", targetURL, "application/javascript") if !strings.Contains(got, `"/proxy/web/192.168.1.124/doc/page/login.asp"`) { t.Fatalf("script missing rewritten login URL: %s", got) } if !strings.Contains(got, `"/proxy/web/192.168.1.124/ISAPI/System/time"`) { t.Fatalf("script missing rewritten API URL: %s", got) } if strings.Contains(got, "data-web-proxy-runtime") { t.Fatalf("script should not receive HTML runtime: %s", got) } } func TestProxyHTTPRejectsUnscannedIP(t *testing.T) { t.Parallel() svc := NewService() req := httptest.NewRequest(http.MethodGet, "http://portal/proxy/web/192.168.1.124/", nil) rec := httptest.NewRecorder() err := svc.ProxyHTTP(rec, req, "192.168.1.124", "/") if err != ErrTargetNotAllowed { t.Fatalf("ProxyHTTP() error = %v, want ErrTargetNotAllowed", err) } } func TestProxyHTTPServesAllowedTarget(t *testing.T) { t.Parallel() upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "http://192.168.1.124/ISAPI/test") w.Header().Add("Set-Cookie", "SID=1; Path=/") w.Header().Set("Content-Type", "text/html") _, _ = w.Write([]byte(``)) })) defer upstream.Close() svc := NewService() svc.allowIP("192.168.1.124") svc.proxyTarget = func(ip string) string { return upstream.URL } req := httptest.NewRequest(http.MethodGet, "http://portal/proxy/web/192.168.1.124/", nil) rec := httptest.NewRecorder() if err := svc.ProxyHTTP(rec, req, "192.168.1.124", "/"); err != nil { t.Fatalf("ProxyHTTP() error = %v", err) } if rec.Code != http.StatusOK { t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Location"); got != "http://192.168.1.124/ISAPI/test" { t.Fatalf("Location = %q", got) } if !strings.Contains(rec.Body.String(), "/proxy/web/192.168.1.124/doc/logo.png") { t.Fatalf("body = %s", rec.Body.String()) } } func TestProxyHTTPClosesUpstreamConnection(t *testing.T) { t.Parallel() closeSeen := make(chan bool, 1) upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { closeSeen <- r.Close w.Header().Set("Content-Type", "text/plain") _, _ = w.Write([]byte("ok")) })) defer upstream.Close() svc := NewService() svc.allowIP("192.168.1.124") svc.proxyTarget = func(ip string) string { return upstream.URL } req := httptest.NewRequest(http.MethodGet, "http://portal/proxy/web/192.168.1.124/", nil) rec := httptest.NewRecorder() if err := svc.ProxyHTTP(rec, req, "192.168.1.124", "/"); err != nil { t.Fatalf("ProxyHTTP() error = %v", err) } if rec.Code != http.StatusOK { t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String()) } select { case got := <-closeSeen: if !got { t.Fatal("upstream request Close = false, want true") } case <-time.After(time.Second): t.Fatal("timed out waiting for upstream request") } } func TestRetryTransportHoldsProxySlotUntilBodyClose(t *testing.T) { limiter := make(chan struct{}, 1) transport := retryTransport{ base: roundTripperFunc(func(req *http.Request) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("ok")), Header: http.Header{}, Request: req, }, nil }), limiter: limiter, attempts: 1, acquireTimeout: time.Second, } req := httptest.NewRequest(http.MethodGet, "http://portal/test", nil) resp, err := transport.RoundTrip(req) if err != nil { t.Fatalf("RoundTrip() error = %v", err) } if got := len(limiter); got != 1 { t.Fatalf("limiter len after RoundTrip = %d, want 1", got) } if err := resp.Body.Close(); err != nil { t.Fatalf("Body.Close() error = %v", err) } if got := len(limiter); got != 0 { t.Fatalf("limiter len after Body.Close = %d, want 0", got) } } type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func TestSanitizeProxyRequestHeaderDropsLoginCookie(t *testing.T) { source := http.Header{} source.Set("User-Agent", "browser") source.Set("Cookie", "SID=1") source.Set("Referer", "http://10.8.0.18:13000/proxy/web/192.168.0.108/") source.Set("Sessiontag", "abc123") source.Set("If-Modified-Since", "0") source.Set("Accept-Encoding", "gzip, deflate") source.Set("X-Forwarded-For", "10.8.0.1") loginHeader := sanitizeProxyRequestHeader(source, "/doc/page/login.asp") if got := loginHeader.Get("Cookie"); got != "" { t.Fatalf("login Cookie = %q, want empty", got) } if got := loginHeader.Get("Referer"); got != "" { t.Fatalf("login Referer = %q, want empty", got) } if got := loginHeader.Get("X-Forwarded-For"); got != "" { t.Fatalf("login X-Forwarded-For = %q, want empty", got) } if got := loginHeader.Get("Accept-Encoding"); got != "" { t.Fatalf("login Accept-Encoding = %q, want empty", got) } if got := loginHeader.Get("Sessiontag"); got != "abc123" { t.Fatalf("login Sessiontag = %q, want abc123", got) } apiHeader := sanitizeProxyRequestHeader(source, "/ISAPI/Security/userCheck") if got := apiHeader.Get("Cookie"); got != "SID=1" { t.Fatalf("api Cookie = %q, want SID=1", got) } if got := apiHeader.Get("Sessiontag"); got != "abc123" { t.Fatalf("api Sessiontag = %q, want abc123", got) } if got := apiHeader.Get("If-Modified-Since"); got != "0" { t.Fatalf("api If-Modified-Since = %q, want 0", got) } } func TestSanitizeProxyRequestHeaderPreservesWebSocketUpgrade(t *testing.T) { source := http.Header{} source.Set("User-Agent", "browser") source.Set("Connection", "keep-alive, Upgrade") source.Set("Upgrade", "websocket") source.Set("Sec-Websocket-Key", "abc") source.Set("Sec-Websocket-Version", "13") source.Set("Accept-Encoding", "gzip") header := sanitizeProxyRequestHeader(source, "/webSocket") if got := header.Get("Connection"); got != "Upgrade" { t.Fatalf("Connection = %q, want Upgrade", got) } if got := header.Get("Upgrade"); got != "websocket" { t.Fatalf("Upgrade = %q, want websocket", got) } if got := header.Get("Sec-Websocket-Key"); got != "abc" { t.Fatalf("Sec-Websocket-Key = %q, want abc", got) } if got := header.Get("Accept-Encoding"); got != "" { t.Fatalf("Accept-Encoding = %q, want empty", got) } }