mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-05-06 20:02:54 +08:00
- Introduced `.env.example` for environment variable configuration. - Updated `docker-compose.yml` to utilize environment variables for proxy settings. - Implemented proxy authentication with Basic Auth and geo-blocking based on country codes. - Added `GEO_FILTER.md` for documentation on geo-filtering configuration. - Enhanced logging to indicate authentication status and blocked countries during proxy server startup.
279 lines
6.7 KiB
Go
279 lines
6.7 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
|
||
}
|
||
|
||
// handleHTTP 处理普通 HTTP 请求(带自动重试)
|
||
func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
|
||
var tried []string
|
||
for attempt := 0; attempt <= s.cfg.MaxRetry; attempt++ {
|
||
var p *storage.Proxy
|
||
var err error
|
||
|
||
// 根据模式选择代理
|
||
if s.mode == "lowest-latency" {
|
||
p, err = s.storage.GetLowestLatencyExclude(tried)
|
||
} else {
|
||
p, err = s.storage.GetRandomExclude(tried)
|
||
}
|
||
|
||
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 {
|
||
s.storage.Delete(p.Address)
|
||
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.Delete(p.Address)
|
||
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)
|
||
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++ {
|
||
var p *storage.Proxy
|
||
var err error
|
||
|
||
// 根据模式选择代理
|
||
if s.mode == "lowest-latency" {
|
||
p, err = s.storage.GetLowestLatencyExclude(tried)
|
||
} else {
|
||
p, err = s.storage.GetRandomExclude(tried)
|
||
}
|
||
|
||
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.Delete(p.Address)
|
||
continue
|
||
}
|
||
|
||
// 告知客户端隧道建立
|
||
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)
|
||
}
|