Files
Wormhole/internal/proxy/domain_forward.go
T
rose_cat707 612d68f56f feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台,SQLite 持久化;支持隧道/域名穿透、域名转发插件链、访客认证、
IP 规则、API Key、Docker 单镜像部署;配置通过 Web 系统设置管理,无需 YAML 文件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 12:15:02 +08:00

218 lines
5.5 KiB
Go

package proxy
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/wormhole/wormhole/internal/store"
"golang.org/x/net/proxy"
)
func (m *Manager) handleDomainForward(w http.ResponseWriter, r *http.Request, df *store.DomainForward) {
switch df.Mode {
case "redirect":
m.handleDomainRedirect(w, r, df)
case "proxy":
m.handleDomainProxy(w, r, df)
default:
http.Error(w, "bad forward mode", http.StatusBadGateway)
}
}
func (m *Manager) handleDomainRedirect(w http.ResponseWriter, r *http.Request, df *store.DomainForward) {
target, err := buildRedirectURL(df, r)
if err != nil {
http.Error(w, "bad redirect target", http.StatusBadGateway)
return
}
code := df.RedirectCode
if code != 301 && code != 302 {
code = 302
}
http.Redirect(w, r, target, code)
}
func buildRedirectURL(df *store.DomainForward, r *http.Request) (string, error) {
targetBase, err := url.Parse(df.TargetURL)
if err != nil {
return "", err
}
if df.RedirectMode == "fixed" {
return targetBase.String(), nil
}
loc := df.SourceLocation
if loc == "" {
loc = "/"
}
suffix := strings.TrimPrefix(r.URL.Path, loc)
if suffix == "" {
suffix = "/"
}
if !strings.HasPrefix(suffix, "/") {
suffix = "/" + suffix
}
out := *targetBase
if strings.HasSuffix(targetBase.Path, "/") && suffix == "/" {
// keep target path as-is
} else if targetBase.Path == "" || targetBase.Path == "/" {
out.Path = suffix
} else {
out.Path = strings.TrimSuffix(targetBase.Path, "/") + suffix
}
out.RawQuery = r.URL.RawQuery
return out.String(), nil
}
func (m *Manager) handleDomainProxy(w http.ResponseWriter, r *http.Request, df *store.DomainForward) {
targetBase, err := url.Parse(df.TargetURL)
if err != nil {
http.Error(w, "bad target", http.StatusBadGateway)
return
}
transport, err := m.domainForwardTransport(df)
if err != nil {
http.Error(w, "proxy transport error", http.StatusBadGateway)
return
}
proxy := httputil.NewSingleHostReverseProxy(targetBase)
proxy.Transport = transport
loc := df.SourceLocation
if loc == "" {
loc = "/"
}
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
suffix := strings.TrimPrefix(r.URL.Path, loc)
if suffix == "" {
suffix = "/"
}
if !strings.HasPrefix(suffix, "/") {
suffix = "/" + suffix
}
if targetBase.Path == "" || targetBase.Path == "/" {
req.URL.Path = suffix
} else {
req.URL.Path = strings.TrimSuffix(targetBase.Path, "/") + suffix
}
req.URL.RawQuery = r.URL.RawQuery
req.URL.Scheme = targetBase.Scheme
req.URL.Host = targetBase.Host
if df.PreserveHost {
req.Host = r.Host
} else {
req.Host = targetBase.Host
}
}
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, e error) {
http.Error(rw, "proxy error", http.StatusBadGateway)
}
if isWebSocketUpgrade(r) {
proxy.ServeHTTP(w, r)
return
}
if hasEnabledPlugins(df.Plugins) {
upstreamReq, err := buildDomainForwardUpstreamRequest(r, df, targetBase, loc)
if err != nil {
http.Error(w, "bad target", http.StatusBadGateway)
return
}
if err := runForwardPluginPipeline(w, r, df, upstreamReq, transport); err != nil {
http.Error(w, "proxy error", http.StatusBadGateway)
}
return
}
rec := &responseRecorder{ResponseWriter: w, status: 200}
proxy.ServeHTTP(rec, r)
}
func buildDomainForwardUpstreamRequest(r *http.Request, df *store.DomainForward, targetBase *url.URL, loc string) (*http.Request, error) {
suffix := strings.TrimPrefix(r.URL.Path, loc)
if suffix == "" {
suffix = "/"
}
if !strings.HasPrefix(suffix, "/") {
suffix = "/" + suffix
}
path := suffix
if targetBase.Path != "" && targetBase.Path != "/" {
path = strings.TrimSuffix(targetBase.Path, "/") + suffix
}
upstreamURL := *targetBase
upstreamURL.Path = path
upstreamURL.RawQuery = r.URL.RawQuery
var body io.ReadCloser
if r.Body != nil && r.Method != http.MethodGet && r.Method != http.MethodHead {
body = r.Body
}
outReq, err := http.NewRequestWithContext(r.Context(), r.Method, upstreamURL.String(), body)
if err != nil {
return nil, err
}
outReq.Header = r.Header.Clone()
if df.PreserveHost {
outReq.Host = r.Host
} else {
outReq.Host = targetBase.Host
}
return outReq, nil
}
func (m *Manager) domainForwardTransport(df *store.DomainForward) (*http.Transport, error) {
t := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
if df.ProxyType == "http_proxy" {
addr := strings.TrimSpace(df.HTTPProxyAddr)
if addr == "" {
return nil, fmt.Errorf("http proxy addr required")
}
if !strings.Contains(addr, "://") {
addr = "http://" + addr
}
proxyURL, err := url.Parse(addr)
if err != nil || proxyURL.Host == "" {
return nil, fmt.Errorf("invalid http proxy addr")
}
if df.HTTPProxyUser != "" {
proxyURL.User = url.UserPassword(df.HTTPProxyUser, df.HTTPProxyPass)
}
t.Proxy = http.ProxyURL(proxyURL)
return t, nil
}
if df.ProxyType != "socks5" {
return t, nil
}
addr := strings.TrimSpace(df.Socks5Addr)
if addr == "" {
return nil, fmt.Errorf("socks5 addr required")
}
var auth *proxy.Auth
if df.Socks5User != "" {
auth = &proxy.Auth{User: df.Socks5User, Password: df.Socks5Pass}
}
dialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
if err != nil {
return nil, err
}
t.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
if cd, ok := dialer.(proxy.ContextDialer); ok {
return cd.DialContext(ctx, network, address)
}
return dialer.Dial(network, address)
}
return t, nil
}