612d68f56f
Go + Vue 管理台,SQLite 持久化;支持隧道/域名穿透、域名转发插件链、访客认证、 IP 规则、API Key、Docker 单镜像部署;配置通过 Web 系统设置管理,无需 YAML 文件。 Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
4.7 KiB
Go
177 lines
4.7 KiB
Go
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
|
|
}
|