Skip to content

Commit d900d6f

Browse files
committed
Add support for SOCKS proxies and only proxying login websocket
1 parent 01b0547 commit d900d6f

File tree

6 files changed

+104
-30
lines changed

6 files changed

+104
-30
lines changed

client.go

+82-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import (
1919
"sync/atomic"
2020
"time"
2121

22+
"github.com/gorilla/websocket"
2223
"go.mau.fi/util/random"
24+
"golang.org/x/net/proxy"
2325

2426
"go.mau.fi/whatsmeow/appstate"
2527
waBinary "go.mau.fi/whatsmeow/binary"
@@ -155,8 +157,10 @@ type Client struct {
155157
uniqueID string
156158
idCounter atomic.Uint64
157159

158-
proxy socket.Proxy
159-
http *http.Client
160+
proxy Proxy
161+
socksProxy proxy.Dialer
162+
proxyOnlyLogin bool
163+
http *http.Client
160164

161165
// This field changes the client to act like a Messenger client instead of a WhatsApp one.
162166
//
@@ -249,19 +253,35 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
249253
return cli
250254
}
251255

252-
// SetProxyAddress is a helper method that parses a URL string and calls SetProxy.
256+
// SetProxyAddress is a helper method that parses a URL string and calls SetProxy or SetSOCKSProxy based on the URL scheme.
253257
//
254258
// Returns an error if url.Parse fails to parse the given address.
255259
func (cli *Client) SetProxyAddress(addr string) error {
260+
if addr == "" {
261+
cli.SetProxy(nil)
262+
return nil
263+
}
256264
parsed, err := url.Parse(addr)
257265
if err != nil {
258266
return err
259267
}
260-
cli.SetProxy(http.ProxyURL(parsed))
268+
if parsed.Scheme == "http" || parsed.Scheme == "https" {
269+
cli.SetProxy(http.ProxyURL(parsed))
270+
} else if parsed.Scheme == "socks5" {
271+
px, err := proxy.FromURL(parsed, proxy.Direct)
272+
if err != nil {
273+
return err
274+
}
275+
cli.SetSOCKSProxy(px)
276+
} else {
277+
return fmt.Errorf("unsupported proxy scheme %q", parsed.Scheme)
278+
}
261279
return nil
262280
}
263281

264-
// SetProxy sets the proxy to use for WhatsApp web websocket connections and media uploads/downloads.
282+
type Proxy = func(*http.Request) (*url.URL, error)
283+
284+
// SetProxy sets a HTTP proxy to use for WhatsApp web websocket connections and media uploads/downloads.
265285
//
266286
// Must be called before Connect() to take effect in the websocket connection.
267287
// If you want to change the proxy after connecting, you must call Disconnect() and then Connect() again manually.
@@ -281,9 +301,51 @@ func (cli *Client) SetProxyAddress(addr string) error {
281301
// return mediaProxyURL, nil
282302
// }
283303
// })
284-
func (cli *Client) SetProxy(proxy socket.Proxy) {
304+
func (cli *Client) SetProxy(proxy Proxy) {
285305
cli.proxy = proxy
286-
cli.http.Transport.(*http.Transport).Proxy = proxy
306+
cli.socksProxy = nil
307+
transport := cli.http.Transport.(*http.Transport)
308+
transport.Proxy = proxy
309+
transport.Dial = nil
310+
transport.DialContext = nil
311+
}
312+
313+
type SetProxyOptions struct {
314+
// If NoWebsocket is true, the proxy won't be used for the websocket
315+
NoWebsocket bool
316+
// If NoMedia is true, the proxy won't be used for media uploads/downloads
317+
NoMedia bool
318+
}
319+
320+
// SetSOCKSProxy sets a SOCKS5 proxy to use for WhatsApp web websocket connections and media uploads/downloads.
321+
//
322+
// Same details as SetProxy apply, but using a different proxy for the websocket and media is not currently supported.
323+
func (cli *Client) SetSOCKSProxy(px proxy.Dialer, opts ...SetProxyOptions) {
324+
var opt SetProxyOptions
325+
if len(opts) > 0 {
326+
opt = opts[0]
327+
}
328+
if !opt.NoWebsocket {
329+
cli.socksProxy = px
330+
cli.proxy = nil
331+
}
332+
if !opt.NoMedia {
333+
transport := cli.http.Transport.(*http.Transport)
334+
transport.Proxy = nil
335+
transport.Dial = cli.socksProxy.Dial
336+
contextDialer, ok := cli.socksProxy.(proxy.ContextDialer)
337+
if ok {
338+
transport.DialContext = contextDialer.DialContext
339+
} else {
340+
transport.DialContext = nil
341+
}
342+
}
343+
}
344+
345+
// ToggleProxyOnlyForLogin changes whether the proxy set with SetProxy or related methods
346+
// is only used for the pre-login websocket and not authenticated websockets.
347+
func (cli *Client) ToggleProxyOnlyForLogin(only bool) {
348+
cli.proxyOnlyLogin = only
287349
}
288350

289351
func (cli *Client) getSocketWaitChan() <-chan struct{} {
@@ -339,7 +401,19 @@ func (cli *Client) Connect() error {
339401
}
340402

341403
cli.resetExpectedDisconnect()
342-
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), cli.proxy)
404+
wsDialer := websocket.Dialer{}
405+
if !cli.proxyOnlyLogin || cli.Store.ID == nil {
406+
if cli.proxy != nil {
407+
wsDialer.Proxy = cli.proxy
408+
} else if cli.socksProxy != nil {
409+
wsDialer.NetDial = cli.socksProxy.Dial
410+
contextDialer, ok := cli.socksProxy.(proxy.ContextDialer)
411+
if ok {
412+
wsDialer.NetDialContext = contextDialer.DialContext
413+
}
414+
}
415+
}
416+
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), wsDialer)
343417
if cli.MessengerConfig != nil {
344418
fs.URL = "wss://web-chat-e2ee.facebook.com/ws/chat"
345419
fs.HTTPHeaders.Set("Origin", cli.MessengerConfig.BaseURL)

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ require (
88
github.com/rs/zerolog v1.32.0
99
go.mau.fi/libsignal v0.1.0
1010
go.mau.fi/util v0.4.1
11-
golang.org/x/crypto v0.21.0
11+
golang.org/x/crypto v0.23.0
12+
golang.org/x/net v0.25.0
1213
google.golang.org/protobuf v1.33.0
1314
)
1415

1516
require (
1617
filippo.io/edwards25519 v1.0.0 // indirect
1718
github.com/mattn/go-colorable v0.1.13 // indirect
1819
github.com/mattn/go-isatty v0.0.19 // indirect
19-
golang.org/x/sys v0.18.0 // indirect
20+
golang.org/x/sys v0.20.0 // indirect
2021
)

go.sum

+6-4
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
2727
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
2828
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
2929
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
30-
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
31-
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
30+
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
31+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
32+
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
33+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
3234
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3335
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3436
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35-
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
36-
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
37+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
38+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3739
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
3840
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
3941
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=

mdtest/go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ require (
2020
github.com/rs/zerolog v1.32.0 // indirect
2121
go.mau.fi/libsignal v0.1.0 // indirect
2222
go.mau.fi/util v0.4.1 // indirect
23-
golang.org/x/crypto v0.21.0 // indirect
24-
golang.org/x/sys v0.18.0 // indirect
23+
golang.org/x/crypto v0.23.0 // indirect
24+
golang.org/x/net v0.25.0 // indirect
25+
golang.org/x/sys v0.20.0 // indirect
2526
rsc.io/qr v0.2.0 // indirect
2627
)
2728

mdtest/go.sum

+6-4
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
3434
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
3535
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
3636
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
37-
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
38-
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
37+
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
38+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
39+
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
40+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
3941
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4042
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4143
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4244
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43-
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
44-
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
45+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
46+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4547
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
4648
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
4749
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=

socket/framesocket.go

+4-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"errors"
1212
"fmt"
1313
"net/http"
14-
"net/url"
1514
"sync"
1615
"time"
1716

@@ -20,8 +19,6 @@ import (
2019
waLog "go.mau.fi/whatsmeow/util/log"
2120
)
2221

23-
type Proxy = func(*http.Request) (*url.URL, error)
24-
2522
type FrameSocket struct {
2623
conn *websocket.Conn
2724
ctx context.Context
@@ -37,15 +34,15 @@ type FrameSocket struct {
3734
WriteTimeout time.Duration
3835

3936
Header []byte
40-
Proxy Proxy
37+
Dialer websocket.Dialer
4138

4239
incomingLength int
4340
receivedLength int
4441
incoming []byte
4542
partialHeader []byte
4643
}
4744

48-
func NewFrameSocket(log waLog.Logger, proxy Proxy) *FrameSocket {
45+
func NewFrameSocket(log waLog.Logger, dialer websocket.Dialer) *FrameSocket {
4946
return &FrameSocket{
5047
conn: nil,
5148
log: log,
@@ -55,7 +52,7 @@ func NewFrameSocket(log waLog.Logger, proxy Proxy) *FrameSocket {
5552
URL: URL,
5653
HTTPHeaders: http.Header{"Origin": {Origin}},
5754

58-
Proxy: proxy,
55+
Dialer: dialer,
5956
}
6057
}
6158

@@ -104,12 +101,9 @@ func (fs *FrameSocket) Connect() error {
104101
return ErrSocketAlreadyOpen
105102
}
106103
ctx, cancel := context.WithCancel(context.Background())
107-
dialer := websocket.Dialer{
108-
Proxy: fs.Proxy,
109-
}
110104

111105
fs.log.Debugf("Dialing %s", fs.URL)
112-
conn, _, err := dialer.Dial(fs.URL, fs.HTTPHeaders)
106+
conn, _, err := fs.Dialer.Dial(fs.URL, fs.HTTPHeaders)
113107
if err != nil {
114108
cancel()
115109
return fmt.Errorf("couldn't dial whatsapp web websocket: %w", err)

0 commit comments

Comments
 (0)