mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-08-30 04:27:26 +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)
|
|
}
|