diff --git a/README.md b/README.md index d696e18..5c5e4d6 100644 --- a/README.md +++ b/README.md @@ -107,12 +107,15 @@ Example configuration file can be found in [config.yml](config.yml) Proxies file should be in the following format: ``` -scheme://ip:port +scheme://ip:port or scheme://username:password@ip:port Examples: socks5://192.111.137.37:18762 http://192.111.137.37:9911 https://192.111.137.37:9911 +socks5://admin:admin@192.111.137.37:18762 +http://admin:admin@192.111.137.37:8080 +https://admin:admin@192.111.137.37:8081 ``` # Quick Start diff --git a/cmd/rota/main.go b/cmd/rota/main.go index 4d3a8b6..d772329 100644 --- a/cmd/rota/main.go +++ b/cmd/rota/main.go @@ -16,7 +16,7 @@ import ( ) const ( - msgVersion = "rota proxy v1.2.0" + msgVersion = "rota proxy v1.2.1" msgConfigPathRequired = "config file path is required" msgFailedToLoadConfig = "failed to load config" msgConfigLoadedSuccess = "config loaded successfully" diff --git a/internal/proxy/loader.go b/internal/proxy/loader.go index 6e7f032..5f6c4af 100644 --- a/internal/proxy/loader.go +++ b/internal/proxy/loader.go @@ -1,13 +1,16 @@ package proxy import ( + "context" "crypto/tls" "fmt" "log/slog" + "net" "net/http" "net/url" "os" "strings" + "time" "github.com/alpkeskin/rota/internal/config" "h12.io/socks" @@ -64,29 +67,58 @@ func (pl *ProxyLoader) CreateProxy(proxyURL string) (*Proxy, error) { return nil, err } + var username, password string + if parsedUrl.User != nil { + username = parsedUrl.User.Username() + password, _ = parsedUrl.User.Password() + } + p := Proxy{ - Scheme: parsedUrl.Scheme, - Host: proxyURL, - Url: parsedUrl, + Scheme: parsedUrl.Scheme, + Host: proxyURL, + Url: parsedUrl, + Username: username, + Password: password, + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }, + } + + tr := &http.Transport{ + TLSClientConfig: tlsConfig, + DisableKeepAlives: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + DisableCompression: true, + TLSHandshakeTimeout: 10 * time.Second, } - tr := &http.Transport{} switch p.Scheme { case "socks4", "socks4a", "socks5": - tr = &http.Transport{ - Dial: socks.Dial(p.Host), + proxyAddrURL := &url.URL{ + Host: parsedUrl.Host, + Scheme: parsedUrl.Scheme, + User: url.UserPassword(username, password), } - case "http", "https": - tr = &http.Transport{ - Proxy: http.ProxyURL(p.Url), + dialSocks := socks.Dial(proxyAddrURL.String()) + tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialSocks(network, addr) } + case "http", "https": + tr.Proxy = http.ProxyURL(p.Url) default: return nil, fmt.Errorf("%s. URL: %s", msgUnsupportedProxyScheme, proxyURL) } - tr.DisableKeepAlives = true - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - p.Transport = tr return &p, nil } diff --git a/internal/proxy/proxy_handler.go b/internal/proxy/proxy_handler.go index d6940cf..5685c31 100644 --- a/internal/proxy/proxy_handler.go +++ b/internal/proxy/proxy_handler.go @@ -20,7 +20,9 @@ func (ps *ProxyServer) handleRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*h if r.URL.Scheme == "http" && ps.cfg.Proxy.Authentication.Enabled { if err := ps.authenticateHttp(ctx, reqInfo); err != nil { - return ps.unauthorizedResponse(reqInfo) + return nil, goproxy.NewResponse(reqInfo.request, + goproxy.ContentTypeText, StatusProxyAuthRequired, + fmt.Sprintf(msgUnauthorized, reqInfo.id)) } } @@ -61,12 +63,6 @@ func (ps *ProxyServer) authenticateHttps(host string, ctx *goproxy.ProxyCtx) (*g return goproxy.MitmConnect, host } -func (ps *ProxyServer) unauthorizedResponse(reqInfo requestInfo) (*http.Request, *http.Response) { - return nil, goproxy.NewResponse(reqInfo.request, - goproxy.ContentTypeText, StatusProxyAuthRequired, - fmt.Sprintf(msgUnauthorized, reqInfo.id)) -} - func (ps *ProxyServer) badGatewayResponse(reqInfo requestInfo, err error) (*http.Request, *http.Response) { slog.Error(msgReqRotationError, "error", err, "request_id", reqInfo.id, "url", reqInfo.url) return nil, goproxy.NewResponse(reqInfo.request, diff --git a/internal/proxy/proxy_rotation.go b/internal/proxy/proxy_rotation.go index a5804ae..89e57ee 100644 --- a/internal/proxy/proxy_rotation.go +++ b/internal/proxy/proxy_rotation.go @@ -11,6 +11,10 @@ import ( ) func (ps *ProxyServer) getProxy() *Proxy { + if len(ps.Proxies) == 0 { + return nil + } + method := ps.cfg.Proxy.Rotation.Method switch method { case "random": @@ -76,13 +80,25 @@ func (ps *ProxyServer) tryProxy(proxy *Proxy, reqInfo requestInfo) (*http.Respon client := &http.Client{ Transport: proxy.Transport, Timeout: time.Duration(ps.cfg.Proxy.Rotation.Timeout) * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return errors.New(msgStoppedAfter10Redirects) + } + return nil + }, } defer client.CloseIdleConnections() ps.removeHopHeaders(reqInfo.request) reqInfo.request.RequestURI = "" + + if reqInfo.request.URL != nil { + reqInfo.request.Host = reqInfo.request.URL.Host + } + response, err := client.Do(reqInfo.request) duration := time.Since(reqInfo.startAt) + if err == nil && response != nil { go ps.updateProxyUsage(proxy, reqInfo, duration, "success") slog.Info(msgReqRotationSuccess, @@ -93,6 +109,7 @@ func (ps *ProxyServer) tryProxy(proxy *Proxy, reqInfo requestInfo) (*http.Respon ) return response, nil } + go ps.updateProxyUsage(proxy, reqInfo, duration, "failed") slog.Error(msgReqRotationError, "error", err, diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 7a312b7..3b1b0fb 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -128,21 +128,6 @@ func TestRemoveUnhealthyProxy(t *testing.T) { assert.Equal(t, proxy3, ps.Proxies[1]) } -func TestUnauthorizedResponse(t *testing.T) { - ps := NewProxyServer(&config.Config{}) - req, _ := http.NewRequest("GET", "http://example.com", nil) - - reqInfo := requestInfo{ - id: "test-id", - request: req, - } - - _, resp := ps.unauthorizedResponse(reqInfo) - - assert.Equal(t, StatusProxyAuthRequired, resp.StatusCode) - assert.Equal(t, "Proxy Authentication Required", resp.Status) -} - func TestBadGatewayResponse(t *testing.T) { ps := NewProxyServer(&config.Config{}) req, _ := http.NewRequest("GET", "http://example.com", nil) diff --git a/internal/proxy/proxy_types.go b/internal/proxy/proxy_types.go index c674306..b8ee470 100644 --- a/internal/proxy/proxy_types.go +++ b/internal/proxy/proxy_types.go @@ -13,6 +13,8 @@ type Proxy struct { Host string Url *url.URL Transport *http.Transport + Username string + Password string LatestUsageStatus string LatestUsageAt string LatestUsageDuration string @@ -65,6 +67,7 @@ const ( msgDeadProxy = "dead proxy" msgAliveProxy = "alive proxy" msgFailedToWriteOutputFile = "failed to write output file" + msgStoppedAfter10Redirects = "stopped after 10 redirects" ) var hopHeaders = []string{