forked from tulir/whatsmeow
-
-
Notifications
You must be signed in to change notification settings - Fork 10
Race WhatsApp chat sockets like the web client #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6fb8486
socket: race both chat endpoints and close the loser the way the clie...
purpshell 9c6bf2c
fix(socket): initialize race cancellation before dialing
purpshell 639a4b8
test(socket): distinguish canceled in-flight racers
purpshell ff99cde
fix(socket): separate racer dial lifetimes
purpshell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
socket/race.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| //go:build !js | ||
|
|
||
| package socket | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "sync" | ||
|
|
||
| "github.com/coder/websocket" | ||
|
|
||
| waLog "github.com/polymorfa/hypermeow/util/log" | ||
| ) | ||
|
|
||
| // LoserSocketCloseReason is the close reason WhatsApp Web sends on the socket | ||
| // that loses the race (WAWebOpenSocket.js:47). The code is 1000. | ||
| const LoserSocketCloseReason = "loser socket" | ||
|
|
||
| // RaceURLs are the two endpoints WhatsApp Web opens concurrently on every | ||
| // connect (WAWebOpenSocket.js:10). The second exists because some networks | ||
| // block or throttle 443 for long-lived upgrades, and which one wins is a | ||
| // property of the network the client is on. | ||
| var RaceURLs = []string{ | ||
| "wss://web.whatsapp.com/ws/chat", | ||
| "wss://web.whatsapp.com:5222/ws/chat", | ||
| } | ||
|
|
||
| // ConnectRace opens every URL concurrently and returns the frame socket that | ||
| // connected first, closing the others the way the client closes them: code | ||
| // 1000 with the reason "loser socket". It mirrors | ||
| // openWebSocketsConcurrently (WAWebOpenSocket.js:44-64) — the first success | ||
| // wins, the outstanding dials are aborted, and the call fails only when every | ||
| // URL failed. | ||
| // | ||
| // A client that makes one attempt per connect and never closes a second socket | ||
| // is distinguishable from the real one by nothing more than counting, which is | ||
| // why this is worth having. | ||
| // | ||
| // The returned socket is connected and its read pump is running; the caller | ||
| // owns it exactly as it owns one from NewFrameSocket plus Connect. | ||
| func ConnectRace( | ||
| ctx context.Context, | ||
| log waLog.Logger, | ||
| httpClient *http.Client, | ||
| urls []string, | ||
| headers http.Header, | ||
| ) (*FrameSocket, error) { | ||
| return connectRace(ctx, log, httpClient, urls, headers, nil) | ||
| } | ||
|
|
||
| func connectRace( | ||
| ctx context.Context, | ||
| log waLog.Logger, | ||
| httpClient *http.Client, | ||
| urls []string, | ||
| headers http.Header, | ||
| afterConnect func(int), | ||
| ) (*FrameSocket, error) { | ||
| if len(urls) == 0 { | ||
| return nil, ErrDialFailed | ||
| } | ||
| if len(urls) == 1 { | ||
| fs := newRacer(log, httpClient, urls[0], headers) | ||
| if err := fs.Connect(ctx); err != nil { | ||
| fs.Close(0) | ||
| return nil, err | ||
| } | ||
| return fs, nil | ||
| } | ||
|
|
||
| // The decision is taken inside the racer goroutines under one lock, so a | ||
| // socket that opens at the same instant as the winner is chosen either | ||
| // becomes the winner or closes itself as the loser — there is no window | ||
| // in which an already-open loser has its context cancelled from outside | ||
| // and is torn down before it can send the close frame. | ||
| var ( | ||
| mu sync.Mutex | ||
| winner *FrameSocket | ||
| errs []error | ||
| ) | ||
| racerContexts := make([]context.Context, len(urls)) | ||
| cancels := make([]context.CancelFunc, len(urls)) | ||
| for i := range urls { | ||
| racerContexts[i], cancels[i] = context.WithCancel(ctx) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| var wg sync.WaitGroup | ||
| for i, url := range urls { | ||
| wg.Add(1) | ||
| go func(i int, url string, racerCtx context.Context) { | ||
| defer wg.Done() | ||
| defer cancels[i]() | ||
| fs := newRacer(log, httpClient, url, headers) | ||
| if err := fs.connect(ctx, racerCtx); err != nil { | ||
| fs.Close(0) | ||
| mu.Lock() | ||
| errs = append(errs, err) | ||
| mu.Unlock() | ||
| return | ||
| } | ||
| if afterConnect != nil { | ||
| afterConnect(i) | ||
| } | ||
| mu.Lock() | ||
| if winner == nil { | ||
| winner = fs | ||
| mu.Unlock() | ||
| log.Debugf("Opened socket with %s (race winner)", url) | ||
| // Abort every dial still outstanding, exactly as the client | ||
| // aborts its AbortController on the first success. The | ||
| // dial contexts do not own sockets after their upgrades complete. | ||
| for j, cancel := range cancels { | ||
| if j != i { | ||
| cancel() | ||
|
purpshell marked this conversation as resolved.
|
||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| return | ||
| } | ||
| mu.Unlock() | ||
| // A socket that opened after the winner is closed with the | ||
| // client's own code and reason. | ||
| fs.CloseWithReason(websocket.StatusNormalClosure, LoserSocketCloseReason) | ||
| }(i, url, racerContexts[i]) | ||
| } | ||
| wg.Wait() | ||
|
|
||
| if winner == nil { | ||
| for _, cancel := range cancels { | ||
| cancel() | ||
| } | ||
| if len(errs) == 0 { | ||
| return nil, ErrDialFailed | ||
| } | ||
| return nil, errors.Join(errs...) | ||
| } | ||
| return winner, nil | ||
| } | ||
|
|
||
| func newRacer(log waLog.Logger, httpClient *http.Client, url string, headers http.Header) *FrameSocket { | ||
| fs := NewFrameSocket(log, httpClient) | ||
| fs.URL = url | ||
| for name, values := range headers { | ||
| if len(values) == 0 { | ||
| continue | ||
| } | ||
| fs.HTTPHeaders[name] = append([]string(nil), values...) | ||
| } | ||
| return fs | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.