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