Files
flvx/go-backend/internal/http/handler/tunnel_probe_target.go
T
2026-05-02 00:12:41 +08:00

223 lines
5.5 KiB
Go

package handler
import (
"errors"
"fmt"
"net/netip"
"strconv"
"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 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") || isTunnelProbeTargetSchemeLikeHost(host) {
return tunnelProbeTarget{}, false, errors.New("测试目标 Host 不能包含协议或路径")
}
if normalized, ok := normalizeTunnelProbeTargetHost(host); ok {
host = normalized
} else {
return tunnelProbeTarget{}, false, errors.New("测试目标 Host 格式无效")
}
return tunnelProbeTarget{Host: host, Port: port}, true, nil
}
func normalizeTunnelProbeTargetHost(host string) (string, bool) {
if strings.HasPrefix(host, "[") || strings.HasSuffix(host, "]") {
if !strings.HasPrefix(host, "[") || !strings.HasSuffix(host, "]") {
return "", false
}
inner := strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
addr, err := netip.ParseAddr(inner)
if err != nil || !addr.Is6() {
return "", false
}
return inner, true
}
if addr, err := netip.ParseAddr(host); err == nil {
return addr.String(), true
}
if strings.Contains(host, ":") || isTunnelProbeTargetIPv4Like(host) {
return "", false
}
if !isValidTunnelProbeTargetHost(host) {
return "", false
}
return host, true
}
func isValidTunnelProbeTargetHost(host string) bool {
if host == "" || len(host) > 253 {
return false
}
for _, label := range strings.Split(host, ".") {
if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
return false
}
for _, r := range label {
if !isASCIILetter(r) && !isASCIIDigit(r) && r != '-' {
return false
}
}
}
return true
}
func isTunnelProbeTargetIPv4Like(host string) bool {
if host == "" {
return false
}
for _, r := range host {
if !isASCIIDigit(r) && r != '.' {
return false
}
}
return strings.Contains(host, ".")
}
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
}
rawHost, hasHost := req["probeTargetHost"]
rawPort, hasPort := req["probeTargetPort"]
if !hasHost && !hasPort {
return defaultTunnelProbeTarget(), false, nil
}
host, err := parseTunnelProbeTargetHostValue(rawHost)
if err != nil {
return tunnelProbeTarget{}, false, err
}
port, err := parseTunnelProbeTargetPortValue(rawPort)
if err != nil {
return tunnelProbeTarget{}, false, err
}
return normalizeTunnelProbeTarget(host, port)
}
func parseTunnelProbeTargetHostValue(raw interface{}) (string, error) {
if raw == nil {
return "", nil
}
host, ok := raw.(string)
if !ok {
return "", errors.New("测试目标 Host 格式无效")
}
if host != strings.TrimSpace(host) {
return "", errors.New("测试目标 Host 不能包含协议或路径")
}
return host, nil
}
func parseTunnelProbeTargetPortValue(raw interface{}) (int, error) {
if raw == nil {
return 0, nil
}
switch v := raw.(type) {
case float64:
if v != float64(int64(v)) {
return 0, errors.New("测试目标端口必须是整数")
}
return int(v), nil
case string:
if v == "" {
return 0, nil
}
if v != strings.TrimSpace(v) {
return 0, errors.New("测试目标端口必须是整数")
}
port, err := strconv.Atoi(v)
if err != nil {
return 0, errors.New("测试目标端口必须是整数")
}
return port, nil
case int:
return v, nil
case int32:
return int(v), nil
case int64:
return int(v), nil
default:
return 0, errors.New("测试目标端口必须是整数")
}
}
func effectiveTunnelProbeTarget(tunnel *model.Tunnel) tunnelProbeTarget {
if tunnel == nil {
return defaultTunnelProbeTarget()
}
return effectiveTunnelProbeTargetValues(tunnel.ProbeTargetHost, tunnel.ProbeTargetPort)
}
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)
}