diff --git a/go-backend/internal/http/handler/tunnel_probe_target.go b/go-backend/internal/http/handler/tunnel_probe_target.go index 80af291..be8e234 100644 --- a/go-backend/internal/http/handler/tunnel_probe_target.go +++ b/go-backend/internal/http/handler/tunnel_probe_target.go @@ -38,13 +38,44 @@ func normalizeTunnelProbeTarget(host string, port int) (tunnelProbeTarget, bool, if port <= 0 || port > 65535 { return tunnelProbeTarget{}, false, errors.New("测试目标端口必须是 1-65535") } - if strings.Contains(host, "://") || strings.ContainsAny(host, "/?#") || strings.ContainsAny(host, " \t\r\n") { + if strings.Contains(host, "://") || isTunnelProbeTargetSchemeLikeHost(host) || strings.ContainsAny(host, "/?#") || strings.ContainsAny(host, " \t\r\n") { return tunnelProbeTarget{}, false, errors.New("测试目标 Host 不能包含协议或路径") } return tunnelProbeTarget{Host: host, Port: port}, true, nil } +func isTunnelProbeTargetSchemeLikeHost(host string) bool { + if _, err := netip.ParseAddr(host); err == nil { + return false + } + + colon := strings.IndexByte(host, ':') + if colon <= 0 { + return false + } + for i, r := range host[:colon] { + if i == 0 { + if !isASCIILetter(r) { + return false + } + continue + } + if !isASCIILetter(r) && !isASCIIDigit(r) && r != '+' && r != '-' && r != '.' { + return false + } + } + return true +} + +func isASCIILetter(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} + +func isASCIIDigit(r rune) bool { + return r >= '0' && r <= '9' +} + func parseTunnelProbeTargetFromRequest(req map[string]interface{}) (tunnelProbeTarget, bool, error) { if req == nil { return defaultTunnelProbeTarget(), false, nil diff --git a/go-backend/internal/http/handler/tunnel_probe_target_test.go b/go-backend/internal/http/handler/tunnel_probe_target_test.go index a39a2ed..df0ecc9 100644 --- a/go-backend/internal/http/handler/tunnel_probe_target_test.go +++ b/go-backend/internal/http/handler/tunnel_probe_target_test.go @@ -54,6 +54,24 @@ func TestNormalizeTunnelProbeTargetRejectsPartialAndInvalidInputs(t *testing.T) } } +func TestNormalizeTunnelProbeTargetRejectsSchemePrefixButAllowsIPv6(t *testing.T) { + for _, host := range []string{"https:example.com", "mailto:ops@example.com"} { + if _, _, err := normalizeTunnelProbeTarget(host, 443); err == nil { + t.Fatalf("expected scheme-like host %q to be rejected", host) + } + } + + for _, host := range []string{"2001:db8::1", "[2001:db8::1]"} { + target, configured, err := normalizeTunnelProbeTarget(host, 443) + if err != nil { + t.Fatalf("expected IPv6 host %q to be accepted: %v", host, err) + } + if !configured || target.Host != "2001:db8::1" { + t.Fatalf("unexpected IPv6 normalization for %q: %+v configured=%v", host, target, configured) + } + } +} + func TestParseTunnelProbeTargetFromRequest(t *testing.T) { req := map[string]interface{}{ "probeTargetHost": "speed.example.com",