Skip to content

Commit 3ff3432

Browse files
authored
Merge pull request #124 from SenseUnit/optimism
Optimistic dialer
2 parents 29ed7e2 + ed1c095 commit 3ff3432

File tree

4 files changed

+152
-1
lines changed

4 files changed

+152
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,8 @@ Supported proxy schemes are:
340340
* `curves` - colon-separated list of enabled TLS key exchange curves.
341341
* `min-tls-version` - minimum TLS version.
342342
* `max-tls-version` - maximum TLS version.
343+
* `http+optimistic` - (EXPERIMENTAL) HTTP proxy dialer which reads the connection success response asynchronously.
344+
* `https+optimistic` - (EXPERIMENTAL) HTTP proxy over TLS dialer which reads the connection success response asynchronously. Options are same as for `https` dialer.
343345
* `h2` - HTTP/2 proxy over TLS connection. Examples: `h2://user:[email protected]`, `h2://example.org?cert=cert.pem&key=key.pem`. This method also supports additional parameters passed in query string:
344346
* `cafile` - file with CA certificates in PEM format used to verify TLS peer.
345347
* `sni` - override value of ServerName Indication extension.

dialer/dialer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
func init() {
1313
xproxy.RegisterDialerType("http", HTTPProxyDialerFromURL)
1414
xproxy.RegisterDialerType("https", HTTPProxyDialerFromURL)
15+
xproxy.RegisterDialerType("http+optimistic", OptimisticHTTPProxyDialerFromURL)
16+
xproxy.RegisterDialerType("https+optimistic", OptimisticHTTPProxyDialerFromURL)
1517
xproxy.RegisterDialerType("h2", H2ProxyDialerFromURL)
1618
xproxy.RegisterDialerType("h2c", H2ProxyDialerFromURL)
1719
xproxy.RegisterDialerType("set-src-hints", NewHintsSettingDialerFromURL)

dialer/optimistic.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package dialer
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/tls"
7+
"errors"
8+
"fmt"
9+
"net"
10+
"net/http"
11+
"net/url"
12+
"strings"
13+
14+
xproxy "golang.org/x/net/proxy"
15+
16+
"github.com/SenseUnit/dumbproxy/tlsutil"
17+
)
18+
19+
type OptimisticHTTPProxyDialer struct {
20+
address string
21+
tlsConfig *tls.Config
22+
userinfo *url.Userinfo
23+
next Dialer
24+
}
25+
26+
func NewOptimisticHTTPProxyDialer(address string, tlsConfig *tls.Config, userinfo *url.Userinfo, next LegacyDialer) *OptimisticHTTPProxyDialer {
27+
return &OptimisticHTTPProxyDialer{
28+
address: address,
29+
tlsConfig: tlsConfig,
30+
next: MaybeWrapWithContextDialer(next),
31+
userinfo: userinfo,
32+
}
33+
}
34+
35+
func OptimisticHTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
36+
host := u.Hostname()
37+
port := u.Port()
38+
39+
var tlsConfig *tls.Config
40+
var err error
41+
switch strings.ToLower(u.Scheme) {
42+
case "http+optimistic":
43+
if port == "" {
44+
port = "80"
45+
}
46+
case "https+optimistic":
47+
if port == "" {
48+
port = "443"
49+
}
50+
tlsConfig, err = tlsutil.TLSConfigFromURL(u)
51+
if err != nil {
52+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
53+
}
54+
default:
55+
return nil, errors.New("unsupported proxy type")
56+
}
57+
58+
address := net.JoinHostPort(host, port)
59+
60+
return NewOptimisticHTTPProxyDialer(address, tlsConfig, u.User, next), nil
61+
}
62+
63+
func (d *OptimisticHTTPProxyDialer) Dial(network, address string) (net.Conn, error) {
64+
return d.DialContext(context.Background(), network, address)
65+
}
66+
67+
func (d *OptimisticHTTPProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
68+
switch network {
69+
case "tcp", "tcp4", "tcp6":
70+
default:
71+
return nil, errors.New("only \"tcp\" network is supported")
72+
}
73+
conn, err := d.next.DialContext(ctx, "tcp", d.address)
74+
if err != nil {
75+
return nil, fmt.Errorf("proxy dialer is unable to make connection: %w", err)
76+
}
77+
if d.tlsConfig != nil {
78+
conn = tls.Client(conn, d.tlsConfig)
79+
}
80+
81+
return &futureH1ProxiedConn{
82+
Conn: conn,
83+
address: address,
84+
userinfo: d.userinfo,
85+
}, nil
86+
}
87+
88+
type futureH1ProxiedConn struct {
89+
net.Conn
90+
address string
91+
userinfo *url.Userinfo
92+
rDone bool
93+
wDone bool
94+
rErr error
95+
wErr error
96+
}
97+
98+
func (c *futureH1ProxiedConn) Write(b []byte) (n int, err error) {
99+
if c.wErr != nil {
100+
return 0, c.wErr
101+
}
102+
if !c.wDone {
103+
buf := new(bytes.Buffer)
104+
fmt.Fprintf(buf, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n", c.address, c.address)
105+
if c.userinfo != nil {
106+
fmt.Fprintf(buf, "Proxy-Authorization: %s\r\n", basicAuthHeader(c.userinfo))
107+
}
108+
fmt.Fprintf(buf, "User-Agent: dumbproxy\r\n\r\n")
109+
prologueBytes := buf.Len()
110+
buf.Write(b)
111+
n, err := c.Conn.Write(buf.Bytes())
112+
if err != nil {
113+
c.wErr = err
114+
}
115+
c.wDone = true
116+
c.address = ""
117+
c.userinfo = nil
118+
return max(0, n-prologueBytes), err
119+
}
120+
return c.Conn.Write(b)
121+
}
122+
123+
func (c *futureH1ProxiedConn) Read(b []byte) (n int, err error) {
124+
if c.rErr != nil {
125+
return 0, c.rErr
126+
}
127+
if !c.rDone {
128+
resp, err := readResponse(c.Conn)
129+
if err != nil {
130+
c.rErr = fmt.Errorf("reading proxy response failed: %w", err)
131+
return 0, c.rErr
132+
}
133+
if resp.StatusCode != http.StatusOK {
134+
c.rErr = fmt.Errorf("bad status code from proxy: %d", resp.StatusCode)
135+
return 0, c.rErr
136+
}
137+
c.rDone = true
138+
}
139+
return c.Conn.Read(b)
140+
}

handler/proxy.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,20 @@ type pendingWriteConn struct {
7474
net.Conn
7575
data []byte
7676
done bool
77+
wErr error
7778
}
7879

7980
func (p *pendingWriteConn) Write(b []byte) (n int, err error) {
81+
if p.wErr != nil {
82+
return 0, p.wErr
83+
}
8084
if !p.done {
8185
buf := append(append(make([]byte, 0, len(p.data)+len(b)), p.data...), b...)
8286
n, err := p.Conn.Write(buf)
83-
n = max(0, n - len(p.data))
87+
if err != nil {
88+
p.wErr = err
89+
}
90+
n = max(0, n-len(p.data))
8491
p.done = true
8592
p.data = nil
8693
return n, err

0 commit comments

Comments
 (0)