Initial commit

This commit is contained in:
jonasen1988
2026-03-26 21:14:16 +08:00
commit d2e679401e
18 changed files with 2055 additions and 0 deletions

197
proxy/server.go Normal file
View File

@@ -0,0 +1,197 @@
package proxy
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"time"
"golang.org/x/net/proxy"
"proxy-pool/config"
"proxy-pool/storage"
)
type Server struct {
storage *storage.Storage
cfg *config.Config
}
func New(s *storage.Storage, cfg *config.Config) *Server {
return &Server{
storage: s,
cfg: cfg,
}
}
func (s *Server) Start() error {
log.Printf("proxy server listening on %s", s.cfg.ProxyPort)
return http.ListenAndServe(s.cfg.ProxyPort, s)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
s.handleTunnel(w, r)
} else {
s.handleHTTP(w, r)
}
}
// 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.storage.GetRandomExclude(tried)
if err != nil {
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
return
}
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++ {
p, err := s.storage.GetRandomExclude(tried)
if err != nil {
http.Error(w, "no available proxy", http.StatusServiceUnavailable)
return
}
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)
}