mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-17 14:53:52 +00:00
fix: apply per-request timeouts to script HTTP proxy dials
This commit is contained in:
3 files changed
+86
-53
No files matched your search
+55
-48
@@ -12,7 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
"github.com/valyala/fasthttp/fasthttpproxy"
|
|
||||||
"go.aykhans.me/sarin/internal/types"
|
"go.aykhans.me/sarin/internal/types"
|
||||||
utilsSlice "go.aykhans.me/utils/slice"
|
utilsSlice "go.aykhans.me/utils/slice"
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
@@ -95,40 +94,45 @@ func NewHostClients(
|
|||||||
return []*fasthttp.HostClient{client}, nil
|
return []*fasthttp.HostClient{client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProxyDialFunc creates a dial function for the given proxy URL.
|
// NewProxyDialFunc creates a dial function for the given proxy URL with a fixed timeout.
|
||||||
// It can return the following errors:
|
// It can return the following errors:
|
||||||
// - types.ProxyUnsupportedSchemeError
|
// - types.ProxyUnsupportedSchemeError
|
||||||
|
// - types.ErrProxyNoContextDialer
|
||||||
func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
||||||
var (
|
dial, err := NewProxyDialFuncWithTimeout(ctx, proxyURL)
|
||||||
dialer fasthttp.DialFunc
|
if err != nil {
|
||||||
err error
|
return nil, err
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return func(addr string) (net.Conn, error) {
|
||||||
|
return dial(addr, timeout)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProxyDialFuncWithTimeout creates a dial function for the given proxy URL that takes the timeout per call.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ProxyUnsupportedSchemeError
|
||||||
|
// - types.ErrProxyNoContextDialer
|
||||||
|
func NewProxyDialFuncWithTimeout(ctx context.Context, proxyURL *url.URL) (fasthttp.DialFuncWithTimeout, error) {
|
||||||
switch proxyURL.Scheme {
|
switch proxyURL.Scheme {
|
||||||
case "socks5":
|
case "socks5":
|
||||||
dialer, err = fasthttpSocksDialerDualStackTimeout(ctx, proxyURL, timeout, true)
|
return fasthttpSocksDialer(ctx, proxyURL, true)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "socks5h":
|
case "socks5h":
|
||||||
dialer, err = fasthttpSocksDialerDualStackTimeout(ctx, proxyURL, timeout, false)
|
return fasthttpSocksDialer(ctx, proxyURL, false)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "http":
|
case "http":
|
||||||
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxyURL.String(), timeout)
|
return fasthttpConnectDialer(proxyURL, false), nil
|
||||||
case "https":
|
case "https":
|
||||||
dialer = fasthttpHTTPSDialerDualStackTimeout(proxyURL, timeout)
|
return fasthttpConnectDialer(proxyURL, true), nil
|
||||||
default:
|
default:
|
||||||
return nil, types.NewProxyUnsupportedSchemeError(proxyURL.Scheme)
|
return nil, types.NewProxyUnsupportedSchemeError(proxyURL.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The returned dial function can return the following errors:
|
// fasthttpSocksDialer creates a SOCKS5 dial function that takes the timeout per call.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrProxyNoContextDialer
|
||||||
// - types.ProxyDialError
|
// - types.ProxyDialError
|
||||||
func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL, timeout time.Duration, resolveLocally bool) (fasthttp.DialFunc, error) {
|
func fasthttpSocksDialer(ctx context.Context, proxyURL *url.URL, resolveLocally bool) (fasthttp.DialFuncWithTimeout, error) {
|
||||||
netDialer := &net.Dialer{}
|
netDialer := &net.Dialer{}
|
||||||
|
|
||||||
// Parse auth from proxy URL if present
|
// Parse auth from proxy URL if present
|
||||||
@@ -150,21 +154,14 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
|
|
||||||
proxyStr := proxyURL.String()
|
proxyStr := proxyURL.String()
|
||||||
|
|
||||||
// Assert to ContextDialer for timeout support
|
// Timeouts need DialContext, which proxy.SOCKS5 always provides with a net.Dialer.
|
||||||
contextDialer, ok := socksDialer.(proxy.ContextDialer)
|
contextDialer, ok := socksDialer.(proxy.ContextDialer)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Fallback without timeout (should not happen with net.Dialer)
|
return nil, types.ErrProxyNoContextDialer
|
||||||
return func(addr string) (net.Conn, error) {
|
|
||||||
conn, err := socksDialer.Dial("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return dial function that uses context with timeout
|
// Return dial function that uses context with timeout
|
||||||
return func(addr string) (net.Conn, error) {
|
return func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
|
|
||||||
if resolveLocally {
|
if resolveLocally {
|
||||||
@@ -204,12 +201,20 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fasthttpConnectDialer creates a dial function that tunnels through an HTTP proxy with CONNECT,
|
||||||
|
// reaching the proxy over TLS when useTLS is set.
|
||||||
|
//
|
||||||
// The returned dial function can return the following errors:
|
// The returned dial function can return the following errors:
|
||||||
// - types.ProxyDialError
|
// - types.ProxyDialError
|
||||||
func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duration) fasthttp.DialFunc {
|
func fasthttpConnectDialer(proxyURL *url.URL, useTLS bool) fasthttp.DialFuncWithTimeout {
|
||||||
|
defaultPort := "80"
|
||||||
|
if useTLS {
|
||||||
|
defaultPort = "443"
|
||||||
|
}
|
||||||
|
|
||||||
proxyAddr := proxyURL.Host
|
proxyAddr := proxyURL.Host
|
||||||
if proxyURL.Port() == "" {
|
if proxyURL.Port() == "" {
|
||||||
proxyAddr = net.JoinHostPort(proxyURL.Hostname(), "443")
|
proxyAddr = net.JoinHostPort(proxyURL.Hostname(), defaultPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build Proxy-Authorization header if auth is present
|
// Build Proxy-Authorization header if auth is present
|
||||||
@@ -223,7 +228,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
|
|
||||||
proxyStr := proxyURL.String()
|
proxyStr := proxyURL.String()
|
||||||
|
|
||||||
return func(addr string) (net.Conn, error) {
|
return func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
// Establish TCP connection to proxy with timeout
|
// Establish TCP connection to proxy with timeout
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
|
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
|
||||||
@@ -243,13 +248,15 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade to TLS
|
if useTLS {
|
||||||
tlsConn := tls.Client(conn, &tls.Config{
|
tlsConn := tls.Client(conn, &tls.Config{
|
||||||
ServerName: proxyURL.Hostname(),
|
ServerName: proxyURL.Hostname(),
|
||||||
})
|
})
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
|
}
|
||||||
|
conn = tlsConn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and send CONNECT request
|
// Build and send CONNECT request
|
||||||
@@ -263,35 +270,35 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
connectReq.Header.Set("Proxy-Authorization", proxyAuth)
|
connectReq.Header.Set("Proxy-Authorization", proxyAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := connectReq.Write(tlsConn); err != nil {
|
if err := connectReq.Write(conn); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read response using buffered reader, but return wrapped connection
|
// Read response using buffered reader, but return wrapped connection
|
||||||
// to preserve any buffered data
|
// to preserve any buffered data
|
||||||
bufReader := bufio.NewReader(tlsConn)
|
bufReader := bufio.NewReader(conn)
|
||||||
resp, err := http.ReadResponse(bufReader, connectReq)
|
resp, err := http.ReadResponse(bufReader, connectReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
resp.Body.Close() //nolint:errcheck,gosec
|
resp.Body.Close() //nolint:errcheck,gosec
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, types.NewProxyDialError(proxyStr, types.NewProxyConnectError(resp.Status))
|
return nil, types.NewProxyDialError(proxyStr, types.NewProxyConnectError(resp.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear deadline for the tunneled connection
|
// Clear deadline for the tunneled connection
|
||||||
if err := tlsConn.SetDeadline(time.Time{}); err != nil {
|
if err := conn.SetDeadline(time.Time{}); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return wrapped connection that uses the buffered reader
|
// Return wrapped connection that uses the buffered reader
|
||||||
// to avoid losing any data that was read ahead
|
// to avoid losing any data that was read ahead
|
||||||
return &bufferedConn{Conn: tlsConn, reader: bufReader}, nil
|
return &bufferedConn{Conn: conn, reader: bufReader}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sarin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,24 +29,26 @@ var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
|
|||||||
// - types.ProxyDialError
|
// - types.ProxyDialError
|
||||||
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) {
|
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) {
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return []*scriptHTTPClient{newScriptHTTPClient(nil, scriptHTTPDefaultTimeout, maxConns)}, nil
|
return []*scriptHTTPClient{newScriptHTTPClient(fasthttp.DialTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
clients := make([]*scriptHTTPClient, 0, len(proxies))
|
clients := make([]*scriptHTTPClient, 0, len(proxies))
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
dialFunc, err := NewProxyDialFunc(ctx, &proxy, scriptHTTPDefaultTimeout)
|
dial, err := NewProxyDialFuncWithTimeout(ctx, &proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.NewProxyDialError(proxy.String(), err)
|
return nil, types.NewProxyDialError(proxy.String(), err)
|
||||||
}
|
}
|
||||||
clients = append(clients, newScriptHTTPClient(dialFunc, scriptHTTPDefaultTimeout, maxConns))
|
clients = append(clients, newScriptHTTPClient(dial, scriptHTTPDefaultTimeout, maxConns))
|
||||||
}
|
}
|
||||||
return clients, nil
|
return clients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScriptHTTPClient(dialFunc fasthttp.DialFunc, defaultTimeout time.Duration, maxConns uint) *scriptHTTPClient {
|
func newScriptHTTPClient(dial fasthttp.DialFuncWithTimeout, defaultTimeout time.Duration, maxConns uint) *scriptHTTPClient {
|
||||||
newClient := func(insecure bool) *fasthttp.Client {
|
newClient := func(insecure bool) *fasthttp.Client {
|
||||||
|
// Read and write timeouts stay unset because they would cap the per-call timeout,
|
||||||
|
// so dialWithDeadline is what bounds the TLS handshake.
|
||||||
return &fasthttp.Client{
|
return &fasthttp.Client{
|
||||||
Dial: dialFunc,
|
DialTimeout: dialWithDeadline(dial),
|
||||||
MaxConnsPerHost: safeUintToInt(maxConns),
|
MaxConnsPerHost: safeUintToInt(maxConns),
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
InsecureSkipVerify: insecure, //nolint:gosec
|
InsecureSkipVerify: insecure, //nolint:gosec
|
||||||
@@ -63,6 +66,27 @@ func newScriptHTTPClient(dialFunc fasthttp.DialFunc, defaultTimeout time.Duratio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dialWithDeadline dials within the request timeout and keeps it as the connection deadline,
|
||||||
|
// so the TLS handshake fasthttp runs on the first write cannot outlive the request.
|
||||||
|
func dialWithDeadline(dial fasthttp.DialFuncWithTimeout) fasthttp.DialFuncWithTimeout {
|
||||||
|
return func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = scriptHTTPDefaultTimeout
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
|
||||||
|
conn, err := dial(addr, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := conn.SetDeadline(deadline); err != nil {
|
||||||
|
conn.Close() //nolint:errcheck,gosec
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Do sends the request. With maxRedirects the timeout applies to each hop.
|
// Do sends the request. With maxRedirects the timeout applies to each hop.
|
||||||
// It can return the following errors:
|
// It can return the following errors:
|
||||||
// - types.ScriptHTTPRequestError
|
// - types.ScriptHTTPRequestError
|
||||||
|
|||||||
@@ -318,6 +318,8 @@ func (e ConfigFileReadError) Unwrap() error {
|
|||||||
|
|
||||||
// ======================================== Proxy ========================================
|
// ======================================== Proxy ========================================
|
||||||
|
|
||||||
|
var ErrProxyNoContextDialer = errors.New("SOCKS5 dialer does not support timeouts")
|
||||||
|
|
||||||
type ProxyUnsupportedSchemeError struct {
|
type ProxyUnsupportedSchemeError struct {
|
||||||
Scheme string
|
Scheme string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user