Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ require (
github.com/jackc/pgx/v5 v5.7.6
github.com/shirou/gopsutil/v4 v4.24.12
github.com/swaggo/swag v1.16.6
golang.org/x/net v0.46.0
golang.org/x/time v0.14.0
h12.io/socks v1.0.3
)
Expand Down Expand Up @@ -45,6 +44,7 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
Expand Down
57 changes: 37 additions & 20 deletions core/internal/models/proxy.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
package models

import "time"
import (
"net/url"
"time"
)

// Proxy represents a proxy server
type Proxy struct {
ID int `json:"id"`
Address string `json:"address"`
Protocol string `json:"protocol"`
Username *string `json:"username,omitempty"`
Password *string `json:"-"` // Never expose password in JSON
Status string `json:"status"`
Requests int64 `json:"requests"`
SuccessfulRequests int64 `json:"-"`
FailedRequests int64 `json:"-"`
AvgResponseTime int `json:"avg_response_time"`
ID int `json:"id"`
Address string `json:"address"`
Protocol string `json:"protocol"`
Username *string `json:"username,omitempty"`
Password *string `json:"-"` // Never expose password in JSON
Status string `json:"status"`
Requests int64 `json:"requests"`
SuccessfulRequests int64 `json:"-"`
FailedRequests int64 `json:"-"`
AvgResponseTime int `json:"avg_response_time"`
LastCheck *time.Time `json:"last_check,omitempty"`
LastError *string `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastError *string `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// ProxyWithStats represents a proxy with calculated statistics
Expand Down Expand Up @@ -63,12 +66,12 @@ type BulkDeleteProxyRequest struct {

// ProxyTestResult represents the result of testing a proxy
type ProxyTestResult struct {
ID int `json:"id"`
Address string `json:"address"`
Status string `json:"status"`
ResponseTime *int `json:"response_time,omitempty"`
Error *string `json:"error,omitempty"`
TestedAt time.Time `json:"tested_at"`
ID int `json:"id"`
Address string `json:"address"`
Status string `json:"status"`
ResponseTime *int `json:"response_time,omitempty"`
Error *string `json:"error,omitempty"`
TestedAt time.Time `json:"tested_at"`
}

// ProxyListResponse represents a paginated list of proxies
Expand All @@ -84,3 +87,17 @@ type PaginationMeta struct {
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}

func (p *Proxy) Url() url.URL {
var u *url.Userinfo

if p.Username != nil && *p.Username != "" {
if p.Password != nil && *p.Password != "" {
u = url.UserPassword(*p.Username, *p.Password)
} else {
u = url.User(*p.Username)
}
}

return url.URL{Scheme: p.Protocol, User: u, Host: p.Address}
}
33 changes: 7 additions & 26 deletions core/internal/proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/alpkeskin/rota/core/pkg/logger"
"github.com/elazarl/goproxy"
"github.com/google/uuid"
proxyDialer "golang.org/x/net/proxy"
"h12.io/socks"
)

// UpstreamProxyHandler handles requests with upstream proxy rotation
Expand Down Expand Up @@ -559,34 +559,15 @@ func (h *UpstreamProxyHandler) tryConnectWithRetries(selectedProxy *models.Proxy
// connectViaProxy establishes a connection through a specific proxy
func (h *UpstreamProxyHandler) connectViaProxy(proxy *models.Proxy, host string) (net.Conn, error) {
switch proxy.Protocol {
case "socks5":
// Create SOCKS5 dialer
var dialer proxyDialer.Dialer
var err error

if proxy.Username != nil && *proxy.Username != "" {
// Username exists, create auth
password := ""
if proxy.Password != nil {
password = *proxy.Password
}
auth := &proxyDialer.Auth{
User: *proxy.Username,
Password: password,
}
dialer, err = proxyDialer.SOCKS5("tcp", proxy.Address, auth, proxyDialer.Direct)
} else {
dialer, err = proxyDialer.SOCKS5("tcp", proxy.Address, nil, proxyDialer.Direct)
}

if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 dialer: %w", err)
}
case "socks4", "socks4a", "socks5":
// Create SOCKS dialer
proxyURL := proxy.Url()
dialer := socks.Dial(proxyURL.String())

// Connect to target host through proxy
conn, err := dialer.Dial("tcp", host)
conn, err := dialer("tcp", host)
if err != nil {
return nil, fmt.Errorf("failed to connect to %s via SOCKS5 proxy %s: %w", host, proxy.Address, err)
return nil, fmt.Errorf("failed to connect to %s via %s proxy %s: %w", host, proxy.Protocol, proxy.Address, err)
}

return conn, nil
Expand Down
62 changes: 7 additions & 55 deletions core/internal/proxy/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"time"

"github.com/alpkeskin/rota/core/internal/models"
"h12.io/socks"
proxyDialer "golang.org/x/net/proxy"
)

// CreateProxyTransport creates an HTTP transport configured for the given proxy
Expand All @@ -20,9 +18,9 @@ func CreateProxyTransport(p *models.Proxy) (*http.Transport, error) {
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // Skip certificate verification for proxy connections
InsecureSkipVerify: true, // Skip certificate verification for proxy connections
MinVersion: tls.VersionTLS10, // Support older TLS versions for compatibility
MaxVersion: 0, // Allow all TLS versions
MaxVersion: 0, // Allow all TLS versions
// Don't specify CipherSuites to accept all available ciphers for maximum compatibility
// This is acceptable since InsecureSkipVerify is already true
},
Expand All @@ -34,61 +32,15 @@ func CreateProxyTransport(p *models.Proxy) (*http.Transport, error) {
ExpectContinueTimeout: 10 * time.Second,
}

// Parse proxy URL
var proxyURL string
var authMasked string // For logging (hide credentials)

if p.Username != nil && *p.Username != "" {
// Username exists, include authentication
if p.Password != nil && *p.Password != "" {
// Both username and password
proxyURL = fmt.Sprintf("%s://%s:%s@%s", p.Protocol, *p.Username, *p.Password, p.Address)
authMasked = fmt.Sprintf("%s://[username]:[password]@%s", p.Protocol, p.Address)
} else {
// Only username (API key), password is empty
proxyURL = fmt.Sprintf("%s://%s:@%s", p.Protocol, *p.Username, p.Address)
authMasked = fmt.Sprintf("%s://[api_key]:@%s", p.Protocol, p.Address)
}
} else {
// No authentication
proxyURL = fmt.Sprintf("%s://%s", p.Protocol, p.Address)
authMasked = proxyURL
}

parsedURL, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy URL %s: %w", authMasked, err)
}
proxyURL := p.Url()

switch p.Protocol {
case "http", "https":
// Set proxy URL - http.Transport will handle authentication headers automatically
transport.Proxy = http.ProxyURL(parsedURL)
case "socks4", "socks4a":
// Create SOCKS4/SOCKS4A dialer using h12.io/socks
// The Dial function accepts URI format: socks4://[user@]host:port
transport.Dial = socks.Dial(proxyURL)
case "socks5":
// Create SOCKS5 dialer
var auth *proxyDialer.Auth
if p.Username != nil && *p.Username != "" {
// Username exists, create auth
password := ""
if p.Password != nil {
password = *p.Password
}
auth = &proxyDialer.Auth{
User: *p.Username,
Password: password,
}
}

dialer, err := proxyDialer.SOCKS5("tcp", p.Address, auth, proxyDialer.Direct)
if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 dialer: %w", err)
}

transport.Dial = dialer.Dial
transport.Proxy = http.ProxyURL(&proxyURL)
case "socks4", "socks4a", "socks5":
// Create SOCKS dialer
transport.Dial = socks.Dial(proxyURL.String())
default:
return nil, fmt.Errorf("unsupported proxy protocol: %s", p.Protocol)
}
Expand Down