package store import ( "fmt" "net/url" "strings" "github.com/wormhole/wormhole/internal/forwardplugin" ) func normalizeDomainForwardRoute(host, location string) (string, string) { host = strings.TrimSpace(strings.ToLower(host)) location = strings.TrimSpace(location) if location == "" { location = "/" } if !strings.HasPrefix(location, "/") { location = "/" + location } return host, location } func (s *Store) ListDomainForwards() ([]DomainForward, error) { var items []DomainForward err := s.db.Order("id asc").Find(&items).Error return items, err } func (s *Store) GetDomainForwardByID(id uint) (*DomainForward, error) { var df DomainForward if err := s.db.First(&df, id).Error; err != nil { return nil, err } return &df, nil } func (s *Store) CreateDomainForward(df *DomainForward) error { df.SourceHost, df.SourceLocation = normalizeDomainForwardRoute(df.SourceHost, df.SourceLocation) return s.db.Create(df).Error } func (s *Store) UpdateDomainForward(df *DomainForward) error { df.SourceHost, df.SourceLocation = normalizeDomainForwardRoute(df.SourceHost, df.SourceLocation) return s.db.Save(df).Error } func (s *Store) DeleteDomainForward(id uint) error { return s.db.Delete(&DomainForward{}, id).Error } func (s *Store) DomainForwardRouteInUse(host, location string, excludeID uint) (bool, error) { host, location = normalizeDomainForwardRoute(host, location) q := s.db.Model(&DomainForward{}).Where("source_host = ? AND source_location = ?", host, location) if excludeID > 0 { q = q.Where("id <> ?", excludeID) } var count int64 if err := q.Count(&count).Error; err != nil { return false, err } return count > 0, nil } // RouteConflict checks domain forward vs penetration host on same host+location. func (s *Store) RouteConflict(host, location string, excludeForwardID, excludeHostID uint) error { host, location = normalizeDomainForwardRoute(host, location) inUse, err := s.DomainForwardRouteInUse(host, location, excludeForwardID) if err != nil { return err } if inUse { return fmt.Errorf("域名与路径前缀已被其他转发规则占用") } hostInUse, err := s.HostRouteInUse(host, location, excludeHostID) if err != nil { return err } if hostInUse { return fmt.Errorf("域名与路径前缀已被穿透域名占用") } return nil } func ValidateDomainForward(df *DomainForward) error { if strings.TrimSpace(df.SourceHost) == "" { return fmt.Errorf("源域名不能为空") } target := strings.TrimSpace(df.TargetURL) if target == "" { return fmt.Errorf("目标 URL 不能为空") } u, err := url.Parse(target) if err != nil || u.Scheme == "" || u.Host == "" { return fmt.Errorf("目标 URL 格式无效,需包含协议和主机,如 https://example.com") } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("目标 URL 仅支持 http/https") } df.TargetURL = u.String() switch df.Mode { case "redirect", "proxy": default: return fmt.Errorf("模式无效") } if df.Mode == "redirect" { if df.RedirectMode == "" { df.RedirectMode = "preserve" } if df.RedirectMode != "preserve" && df.RedirectMode != "fixed" { return fmt.Errorf("跳转策略无效") } if df.RedirectCode == 0 { df.RedirectCode = 302 } if df.RedirectCode != 301 && df.RedirectCode != 302 { return fmt.Errorf("跳转状态码仅支持 301 或 302") } } if df.Mode == "proxy" { if df.ProxyType == "" { df.ProxyType = "http" } if df.ProxyType != "http" && df.ProxyType != "socks5" && df.ProxyType != "http_proxy" { return fmt.Errorf("代理类型无效") } if df.ProxyType == "socks5" && strings.TrimSpace(df.Socks5Addr) == "" { return fmt.Errorf("SOCKS5 代理地址不能为空") } if df.ProxyType == "http_proxy" && strings.TrimSpace(df.HTTPProxyAddr) == "" { return fmt.Errorf("HTTP 代理地址不能为空") } normalized, err := forwardplugin.ValidateAndNormalizePlugins(toPluginEntries(df.Plugins)) if err != nil { return err } df.Plugins = fromPluginEntries(normalized) } if df.SourceScheme == "" { df.SourceScheme = "all" } if df.SourceLocation == "" { df.SourceLocation = "/" } return nil } func toPluginEntries(plugins ForwardPlugins) []forwardplugin.PluginEntry { if len(plugins) == 0 { return nil } out := make([]forwardplugin.PluginEntry, len(plugins)) for i, p := range plugins { out[i] = forwardplugin.PluginEntry{ Type: p.Type, Enabled: p.Enabled, Config: p.Config, } } return out } func fromPluginEntries(entries []forwardplugin.PluginEntry) ForwardPlugins { if len(entries) == 0 { return nil } out := make(ForwardPlugins, len(entries)) for i, e := range entries { out[i] = ForwardPluginEntry{ Type: e.Type, Enabled: e.Enabled, Config: e.Config, } } return out }