diff --git a/go-backend/internal/http/handler/tunnel_probe_target.go b/go-backend/internal/http/handler/tunnel_probe_target.go new file mode 100644 index 0000000..80af291 --- /dev/null +++ b/go-backend/internal/http/handler/tunnel_probe_target.go @@ -0,0 +1,75 @@ +package handler + +import ( + "errors" + "fmt" + "net/netip" + "strings" + + "go-backend/internal/store/model" +) + +const ( + defaultTunnelProbeTargetHost = "www.bing.com" + defaultTunnelProbeTargetPort = 443 +) + +type tunnelProbeTarget struct { + Host string + Port int +} + +func defaultTunnelProbeTarget() tunnelProbeTarget { + return tunnelProbeTarget{Host: defaultTunnelProbeTargetHost, Port: defaultTunnelProbeTargetPort} +} + +func normalizeTunnelProbeTarget(host string, port int) (tunnelProbeTarget, bool, error) { + host = strings.TrimSpace(host) + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") + } + + if host == "" && port == 0 { + return defaultTunnelProbeTarget(), false, nil + } + if host == "" { + return tunnelProbeTarget{}, false, errors.New("测试目标 Host 不能为空") + } + 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") { + return tunnelProbeTarget{}, false, errors.New("测试目标 Host 不能包含协议或路径") + } + + return tunnelProbeTarget{Host: host, Port: port}, true, nil +} + +func parseTunnelProbeTargetFromRequest(req map[string]interface{}) (tunnelProbeTarget, bool, error) { + if req == nil { + return defaultTunnelProbeTarget(), false, nil + } + return normalizeTunnelProbeTarget(asString(req["probeTargetHost"]), asInt(req["probeTargetPort"], 0)) +} + +func effectiveTunnelProbeTarget(tunnel *model.Tunnel) tunnelProbeTarget { + if tunnel == nil { + return defaultTunnelProbeTarget() + } + return defaultTunnelProbeTarget() +} + +func effectiveTunnelProbeTargetValues(host string, port int) tunnelProbeTarget { + target, configured, err := normalizeTunnelProbeTarget(host, port) + if err != nil || !configured { + return defaultTunnelProbeTarget() + } + return target +} + +func formatTunnelProbeTarget(target tunnelProbeTarget) string { + if addr, err := netip.ParseAddr(target.Host); err == nil && addr.Is6() { + return fmt.Sprintf("[%s]:%d", target.Host, target.Port) + } + return fmt.Sprintf("%s:%d", target.Host, target.Port) +} diff --git a/go-backend/internal/http/handler/tunnel_probe_target_test.go b/go-backend/internal/http/handler/tunnel_probe_target_test.go new file mode 100644 index 0000000..a39a2ed --- /dev/null +++ b/go-backend/internal/http/handler/tunnel_probe_target_test.go @@ -0,0 +1,69 @@ +package handler + +import "testing" + +func TestNormalizeTunnelProbeTargetDefaultsWhenEmpty(t *testing.T) { + target, configured, err := normalizeTunnelProbeTarget("", 0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if configured { + t.Fatalf("expected empty input to be default, not configured") + } + if target.Host != defaultTunnelProbeTargetHost || target.Port != defaultTunnelProbeTargetPort { + t.Fatalf("unexpected default target: %+v", target) + } +} + +func TestNormalizeTunnelProbeTargetAcceptsHostPortAndIPv6(t *testing.T) { + target, configured, err := normalizeTunnelProbeTarget(" [2001:db8::1] ", 8443) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !configured { + t.Fatalf("expected explicit target") + } + if target.Host != "2001:db8::1" || target.Port != 8443 { + t.Fatalf("unexpected normalized target: %+v", target) + } + if got := formatTunnelProbeTarget(target); got != "[2001:db8::1]:8443" { + t.Fatalf("unexpected formatted target: %s", got) + } +} + +func TestNormalizeTunnelProbeTargetRejectsPartialAndInvalidInputs(t *testing.T) { + tests := []struct { + name string + host string + port int + }{ + {name: "missing host", host: "", port: 443}, + {name: "missing port", host: "example.com", port: 0}, + {name: "port too high", host: "example.com", port: 70000}, + {name: "scheme", host: "https://example.com", port: 443}, + {name: "path", host: "example.com/ping", port: 443}, + {name: "space", host: "example .com", port: 443}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, _, err := normalizeTunnelProbeTarget(tt.host, tt.port); err == nil { + t.Fatalf("expected validation error") + } + }) + } +} + +func TestParseTunnelProbeTargetFromRequest(t *testing.T) { + req := map[string]interface{}{ + "probeTargetHost": "speed.example.com", + "probeTargetPort": float64(1443), + } + target, configured, err := parseTunnelProbeTargetFromRequest(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !configured || target.Host != "speed.example.com" || target.Port != 1443 { + t.Fatalf("unexpected request target: %+v configured=%v", target, configured) + } +}