Files
GoProxy/proxy/server.go
isboyjc f03c3300b4 feat: implement custom proxy subscription management and enhance configuration
- Added support for importing Clash/V2ray subscriptions, including automatic format detection and integration with sing-box for protocol conversion.
- Introduced five proxy usage modes in the configuration, allowing flexible selection between mixed, custom-only, and free-only modes.
- Enhanced `.env.example` and `docker-compose.yml` to include new environment variables for custom proxy settings.
- Updated `CHANGELOG.md` to document new features and improvements related to subscription management.
- Improved WebUI for managing subscriptions and displaying proxy statistics.
- Implemented a background process for refreshing subscriptions and probing disabled proxies for reactivation.
2026-04-04 22:25:54 +08:00

321 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package proxy
import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
"goproxy/config"
"goproxy/storage"
)
type Server struct {
storage *storage.Storage
cfg *config.Config
mode string // "random" 或 "lowest-latency"
port string
}
func New(s *storage.Storage, cfg *config.Config, mode string, port string) *Server {
return &Server{
storage: s,
cfg: cfg,
mode: mode,
port: port,
}
}
func (s *Server) Start() error {
modeDesc := "随机轮换"
if s.mode == "lowest-latency" {
modeDesc = "最低延迟"
}
authStatus := "无认证"
if s.cfg.ProxyAuthEnabled {
authStatus = fmt.Sprintf("需认证 (用户: %s)", s.cfg.ProxyAuthUsername)
}
log.Printf("proxy server listening on %s [%s] [%s]", s.port, modeDesc, authStatus)
return http.ListenAndServe(s.port, s)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 认证检查(如果启用)
if s.cfg.ProxyAuthEnabled {
if !s.checkAuth(r) {
w.Header().Set("Proxy-Authenticate", `Basic realm="GoProxy"`)
http.Error(w, "Proxy Authentication Required", http.StatusProxyAuthRequired)
return
}
}
if r.Method == http.MethodConnect {
s.handleTunnel(w, r)
} else {
s.handleHTTP(w, r)
}
}
// checkAuth 验证代理 Basic Auth
func (s *Server) checkAuth(r *http.Request) bool {
auth := r.Header.Get("Proxy-Authorization")
if auth == "" {
return false
}
// 解析 Basic Auth
const prefix = "Basic "
if !strings.HasPrefix(auth, prefix) {
return false
}
decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return false
}
credentials := strings.SplitN(string(decoded), ":", 2)
if len(credentials) != 2 {
return false
}
username := credentials[0]
password := credentials[1]
// 验证用户名和密码
usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(s.cfg.ProxyAuthUsername)) == 1
passwordHash := fmt.Sprintf("%x", sha256.Sum256([]byte(password)))
passwordMatch := subtle.ConstantTimeCompare([]byte(passwordHash), []byte(s.cfg.ProxyAuthPasswordHash)) == 1
return usernameMatch && passwordMatch
}
// selectProxy 根据使用模式和选择策略获取代理
func (s *Server) selectProxy(tried []string, lowestLatency bool) (*storage.Proxy, error) {
cfg := config.Get()
sourceFilter := sourceFilterFromMode(cfg.CustomProxyMode)
// 混用 + 优先模式:先尝试优先源,无可用则 fallback 全部
if cfg.CustomProxyMode == "mixed" && (cfg.CustomPriority || cfg.CustomFreePriority) {
preferSource := "custom"
if cfg.CustomFreePriority {
preferSource = "free"
}
var p *storage.Proxy
var err error
if lowestLatency {
p, err = s.storage.GetLowestLatencyExcludeFiltered(tried, preferSource)
} else {
p, err = s.storage.GetRandomExcludeFiltered(tried, preferSource)
}
if err == nil {
return p, nil
}
// fallback 到全部
if lowestLatency {
return s.storage.GetLowestLatencyExcludeFiltered(tried, "")
}
return s.storage.GetRandomExcludeFiltered(tried, "")
}
if lowestLatency {
return s.storage.GetLowestLatencyExcludeFiltered(tried, sourceFilter)
}
return s.storage.GetRandomExcludeFiltered(tried, sourceFilter)
}
// removeOrDisableProxy 根据代理来源决定删除或禁用
func removeOrDisableProxy(store *storage.Storage, p *storage.Proxy) {
if p.Source == "custom" {
store.DisableProxy(p.Address)
} else {
store.Delete(p.Address)
}
}
// sourceFilterFromMode 根据使用模式返回来源过滤值
func sourceFilterFromMode(mode string) string {
switch mode {
case "custom_only":
return "custom"
case "free_only":
return "free"
default:
return "" // mixed
}
}
// handleHTTP 处理普通 HTTP 请求(带自动重试)
func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
var tried []string
for attempt := 0; attempt <= s.cfg.MaxRetry; attempt++ {
p, err := s.selectProxy(tried, s.mode == "lowest-latency")
if err != nil {
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
return
}
tried = append(tried, p.Address)
client, err := s.buildClient(p)
if err != nil {
removeOrDisableProxy(s.storage, p)
continue
}
// 转发请求(使用完整 URL上游代理通过 client transport 设置)
req, err := http.NewRequest(r.Method, r.URL.String(), r.Body)
if err != nil {
continue
}
req.Header = r.Header.Clone()
req.Header.Del("Proxy-Connection")
resp, err := client.Do(req)
if err != nil {
log.Printf("[proxy] %s via %s failed, removing", r.RequestURI, p.Address)
s.storage.RecordProxyUse(p.Address, false)
removeOrDisableProxy(s.storage, p)
continue
}
defer resp.Body.Close()
// 写回响应
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
s.storage.RecordProxyUse(p.Address, true)
if resp.StatusCode == 429 {
log.Printf("[proxy] ⚠️ 429 %s via %s (protocol=%s)", r.RequestURI, p.Address, p.Protocol)
} else {
log.Printf("[proxy] %s via %s -> %d", r.RequestURI, p.Address, resp.StatusCode)
}
return
}
http.Error(w, "all proxies failed", http.StatusBadGateway)
}
// handleTunnel 处理 HTTPS CONNECT 隧道(带自动重试)
func (s *Server) handleTunnel(w http.ResponseWriter, r *http.Request) {
var tried []string
for attempt := 0; attempt <= s.cfg.MaxRetry; attempt++ {
p, err := s.selectProxy(tried, s.mode == "lowest-latency")
if err != nil {
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
return
}
tried = append(tried, p.Address)
conn, err := s.dialViaProxy(p, r.Host)
if err != nil {
log.Printf("[tunnel] dial %s via %s failed, removing", r.Host, p.Address)
s.storage.RecordProxyUse(p.Address, false)
removeOrDisableProxy(s.storage, p)
continue
}
s.storage.RecordProxyUse(p.Address, true)
// 告知客户端隧道建立
hijacker, ok := w.(http.Hijacker)
if !ok {
conn.Close()
http.Error(w, "hijack not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
conn.Close()
return
}
fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection Established\r\n\r\n")
log.Printf("[tunnel] %s via %s established", r.Host, p.Address)
// 双向转发
go transfer(conn, clientConn)
go transfer(clientConn, conn)
return
}
http.Error(w, "all proxies failed", http.StatusBadGateway)
}
func (s *Server) dialViaProxy(p *storage.Proxy, host string) (net.Conn, error) {
timeout := time.Duration(s.cfg.ValidateTimeout) * time.Second
switch p.Protocol {
case "http":
conn, err := net.DialTimeout("tcp", p.Address, timeout)
if err != nil {
return nil, err
}
// 发送 CONNECT 请求给上游 HTTP 代理
fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", host, host)
buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
return nil, err
}
if n < 12 {
conn.Close()
return nil, fmt.Errorf("short response from proxy")
}
return conn, nil
case "socks5":
dialer, err := proxy.SOCKS5("tcp", p.Address, nil, proxy.Direct)
if err != nil {
return nil, err
}
return dialer.Dial("tcp", host)
default:
return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol)
}
}
func (s *Server) buildClient(p *storage.Proxy) (*http.Client, error) {
timeout := time.Duration(s.cfg.ValidateTimeout) * time.Second
switch p.Protocol {
case "http":
proxyURL, err := url.Parse(fmt.Sprintf("http://%s", p.Address))
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: timeout,
}, nil
case "socks5":
dialer, err := proxy.SOCKS5("tcp", p.Address, nil, proxy.Direct)
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{Dial: dialer.Dial},
Timeout: timeout,
}, nil
default:
return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol)
}
}
func transfer(dst io.WriteCloser, src io.ReadCloser) {
defer dst.Close()
defer src.Close()
io.Copy(dst, src)
}