fix: name the proxy schemes that work, and stop panicking on the rest
"Which proxy does this support?" has a narrower answer than the docs
gave. `golang.org/x/net/proxy.FromURL` resolves `socks5` and `socks5h`
and nothing else, yet the README and `Proxy`'s doc comment both offered
"socks5, socks4 and socks4a". A `socks4://` URI fails every dial with
`proxy: unknown scheme: socks4`, which is what taking the docs at their
word looks like from the caller's side.
Corrected both, and pointed at `proxy.RegisterDialerType` -- the way
x/net/proxy expects a caller to add a scheme it does not carry.
That escape hatch had a second problem waiting. `establishProxyConnection`
asserted the dialer to `proxy.ContextDialer` unconditionally, but a
registered dialer need only implement `Dial`, and the assertion panicked
on those:
interface conversion: T is not proxy.ContextDialer: missing method
DialContext
It now falls back to dialing on a goroutine and abandoning the attempt
when the context expires, which is what x/net/proxy itself does for the
same case.
Closes: #95
This commit is contained in:
1 parent
74c6cde98a
commit
87744aa7c1
4 files changed
+80
-3
No files matched your search
@@ -135,7 +135,10 @@ If you want to disable catchAll checking, use the `DisableCatchAllCheck()` switc
|
|||||||
|
|
||||||
Support setting a SOCKS5 proxy to verify the email, proxyURI should be in the format: `socks5://user:password@127.0.0.1:1080`
|
Support setting a SOCKS5 proxy to verify the email, proxyURI should be in the format: `socks5://user:password@127.0.0.1:1080`
|
||||||
|
|
||||||
The protocol could be socks5, socks4 and socks4a.
|
The scheme must be `socks5` or `socks5h`, the only two `golang.org/x/net/proxy` resolves.
|
||||||
|
Anything else, `socks4` and `socks4a` included, fails the dial with `proxy: unknown scheme`;
|
||||||
|
add it with [`proxy.RegisterDialerType`](https://pkg.go.dev/golang.org/x/net/proxy#RegisterDialerType)
|
||||||
|
before calling `Proxy()`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -326,5 +326,37 @@ func establishProxyConnection(addr, proxyURI string, timeout time.Duration) (net
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", addr)
|
if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
|
||||||
|
return contextDialer.DialContext(ctx, "tcp", addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dialer added with proxy.RegisterDialerType need not be a ContextDialer.
|
||||||
|
return dialWithTimeout(ctx, dialer, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialWithTimeout dials through a Dialer that cannot take a context, abandoning
|
||||||
|
// the attempt -- though not the goroutine -- when ctx is done, as x/net/proxy does.
|
||||||
|
func dialWithTimeout(ctx context.Context, dialer proxy.Dialer, addr string) (net.Conn, error) {
|
||||||
|
type result struct {
|
||||||
|
conn net.Conn
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
done := make(chan result, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
conn, err := dialer.Dial("tcp", addr)
|
||||||
|
done <- result{conn, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
go func() {
|
||||||
|
if r := <-done; r.conn != nil {
|
||||||
|
r.conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case r := <-done:
|
||||||
|
return r.conn, r.err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckSMTPUnSupportedVendor(t *testing.T) {
|
func TestCheckSMTPUnSupportedVendor(t *testing.T) {
|
||||||
@@ -231,6 +233,43 @@ func TestDialSMTP_ProxyBypassesCustomResolver(t *testing.T) {
|
|||||||
assert.False(t, called.Load())
|
assert.False(t, called.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pins the schemes x/net/proxy actually resolves; socks4 and socks4a never worked.
|
||||||
|
func TestEstablishProxyConnection_Schemes(t *testing.T) {
|
||||||
|
// Nothing listens here: a resolved scheme reaches a refused connection.
|
||||||
|
const proxyAddr = "127.0.0.1:1"
|
||||||
|
|
||||||
|
for _, scheme := range []string{"socks5", "socks5h"} {
|
||||||
|
t.Run(scheme+" is supported", func(tt *testing.T) {
|
||||||
|
_, err := establishProxyConnection("example.com:25", scheme+"://"+proxyAddr, time.Second)
|
||||||
|
require.Error(tt, err)
|
||||||
|
assert.NotContains(tt, err.Error(), "unknown scheme")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scheme := range []string{"socks4", "socks4a", "http", "https"} {
|
||||||
|
t.Run(scheme+" is not supported", func(tt *testing.T) {
|
||||||
|
_, err := establishProxyConnection("example.com:25", scheme+"://"+proxyAddr, time.Second)
|
||||||
|
require.ErrorContains(tt, err, "proxy: unknown scheme: "+scheme)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dialerWithoutContext struct{ err error }
|
||||||
|
|
||||||
|
func (d dialerWithoutContext) Dial(_, _ string) (net.Conn, error) { return nil, d.err }
|
||||||
|
|
||||||
|
func TestEstablishProxyConnection_DialerWithoutContext(t *testing.T) {
|
||||||
|
wantErr := errors.New("registered dialer reached")
|
||||||
|
proxy.RegisterDialerType("emailverifiertest", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) {
|
||||||
|
return dialerWithoutContext{err: wantErr}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Used to panic on the proxy.ContextDialer assertion.
|
||||||
|
conn, err := establishProxyConnection("example.com:25", "emailverifiertest://127.0.0.1:1080", time.Second)
|
||||||
|
assert.Nil(t, conn)
|
||||||
|
require.ErrorIs(t, err, wantErr)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreferredDialError(t *testing.T) {
|
func TestPreferredDialError(t *testing.T) {
|
||||||
network := errors.New("dial tcp 1.2.3.4:25: connect: connection refused")
|
network := errors.New("dial tcp 1.2.3.4:25: connect: connection refused")
|
||||||
dns := &net.DNSError{Err: "no such host", Name: "mx.example.invalid", IsNotFound: true}
|
dns := &net.DNSError{Err: "no such host", Name: "mx.example.invalid", IsNotFound: true}
|
||||||
|
|||||||
+4
-1
@@ -226,7 +226,10 @@ func (v *Verifier) HelloName(domain string) *Verifier {
|
|||||||
|
|
||||||
// Proxy sets a SOCKS5 proxy to verify the email,
|
// Proxy sets a SOCKS5 proxy to verify the email,
|
||||||
// proxyURI should be in the format: "socks5://user:password@127.0.0.1:1080".
|
// proxyURI should be in the format: "socks5://user:password@127.0.0.1:1080".
|
||||||
// The protocol could be socks5, socks4 and socks4a.
|
//
|
||||||
|
// The scheme must be socks5 or socks5h -- the only two golang.org/x/net/proxy
|
||||||
|
// resolves. Anything else, socks4 and socks4a included, fails the dial with
|
||||||
|
// "proxy: unknown scheme"; add it with proxy.RegisterDialerType.
|
||||||
//
|
//
|
||||||
// Only the scheme, credentials and host are read; a query string is ignored, so
|
// Only the scheme, credentials and host are read; a query string is ignored, so
|
||||||
// a "?timeout=5s" has no effect. Use ConnectTimeout and OperationTimeout.
|
// a "?timeout=5s" has no effect. Use ConnectTimeout and OperationTimeout.
|
||||||
|
|||||||
Reference in new issue
Block a user