mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-05-07 04:52:56 +08:00
- 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.
321 lines
8.1 KiB
Go
321 lines
8.1 KiB
Go
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)
|
||
}
|