1
1
use crate :: relay:: message:: { MessageOut , MessageOutEvent , MessageOutStart } ;
2
+ use crate :: relay:: token:: generate_token;
2
3
use anyhow:: { Context , Result } ;
3
4
use futures_util:: stream:: { SplitSink , SplitStream } ;
4
5
use futures_util:: { SinkExt , StreamExt } ;
5
6
use http:: { HeaderMap , HeaderName , HeaderValue } ;
6
7
use message:: { MessageIn , MessageInEvent } ;
7
8
use std:: collections:: HashMap ;
9
+ use std:: fmt:: { Debug , Display , Formatter } ;
8
10
use std:: time:: Duration ;
9
11
use tokio:: net:: TcpStream ;
10
12
use tokio:: sync:: mpsc:: { UnboundedReceiver , UnboundedSender } ;
11
13
use tokio:: task:: JoinSet ;
12
14
use tokio:: time:: Instant ;
13
15
use tokio_tungstenite:: tungstenite:: client:: IntoClientRequest ;
14
- use tokio_tungstenite:: tungstenite:: Bytes ;
16
+ use tokio_tungstenite:: tungstenite:: protocol:: frame:: coding:: CloseCode :: Policy ;
17
+ use tokio_tungstenite:: tungstenite:: protocol:: CloseFrame ;
18
+ use tokio_tungstenite:: tungstenite:: { Bytes , Utf8Bytes } ;
15
19
use tokio_tungstenite:: {
16
20
connect_async, tungstenite:: protocol:: Message , MaybeTlsStream , WebSocketStream ,
17
21
} ;
@@ -35,6 +39,10 @@ const SERVER_PING_PERIOD: Duration = Duration::from_secs(
35
39
21 ,
36
40
) ;
37
41
42
+ /// When multiple clients try to connect to the Relay server using the same token, one will "win"
43
+ /// and the others will get a Close frame with this message as the reason.
44
+ const SOCKET_IN_USE_REASON : Utf8Bytes = Utf8Bytes :: from_static ( "This socket is already in use" ) ;
45
+
38
46
type HttpClient = reqwest:: Client ;
39
47
type LocalServerResponse = reqwest:: Response ;
40
48
@@ -46,67 +54,129 @@ struct Client {
46
54
logging : bool ,
47
55
}
48
56
57
+ /// Special handling for the errors during establishing a websocket connection.
58
+ ///
59
+ /// In a situation where a relay token is already in use, the server will send a `Close` frame.
60
+ /// When this happens, the caller of `Client::connect` may want to try again with a different token.
61
+ ///
62
+ /// For all other error cases, we report/propagate in the same way as we ever have.
63
+ struct TokenInUse ;
64
+
65
+ impl Debug for TokenInUse {
66
+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
67
+ f. write_str ( "TokenInUse" )
68
+ }
69
+ }
70
+
71
+ impl Display for TokenInUse {
72
+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
73
+ f. write_str ( "TokenInUse" )
74
+ }
75
+ }
76
+
77
+ impl std:: error:: Error for TokenInUse { }
78
+
49
79
impl Client {
50
- async fn connect ( & mut self , announce : bool ) -> Result < ( ) > {
80
+ async fn connect ( & mut self , show_welcome_message : bool ) -> Result < ( ) > {
51
81
let mut set = JoinSet :: new ( ) ;
52
82
let conn = WsConnection :: new ( & self . websocket_url ) . await ?;
53
83
let ( mut ws_tx, mut ws_rx) = conn. stream . split ( ) ;
54
84
55
85
let ( remote_tx, remote_rx) = tokio:: sync:: mpsc:: unbounded_channel :: < MessageOut > ( ) ;
56
86
57
- ws_tx
58
- . send ( Message :: Binary (
87
+ match tokio:: time:: timeout (
88
+ WRITE_WAIT ,
89
+ ws_tx. send ( Message :: Binary (
59
90
serde_json:: to_vec ( & MessageOut :: Start {
60
91
version : message:: VERSION ,
61
92
data : MessageOutStart {
62
93
token : self . token . clone ( ) ,
63
94
} ,
64
95
} ) ?
65
96
. into ( ) ,
66
- ) )
67
- . await ?;
68
- match ws_rx. next ( ) . await {
69
- None => anyhow:: bail!( "no response from server for start message" ) ,
70
- Some ( msg) => {
71
- let data = msg?. into_data ( ) ;
72
-
73
- let parsed = match serde_json:: from_slice :: < MessageIn > ( & data) ? {
74
- MessageIn :: Start { data, .. } => data,
75
- MessageIn :: Event { .. } => {
76
- panic ! ( "unexpected event message during start handshake" )
77
- }
78
- } ;
79
- if announce {
80
- println ! (
81
- r#"
82
- Webhook relay is now listening at
83
- {}
97
+ ) ) ,
98
+ )
99
+ . await
100
+ {
101
+ Ok ( Ok ( _) ) => { /* nothing to do */ }
102
+ // The outer Result is for the timeout, the inner is if there was some other failure during `send`.
103
+ Ok ( Err ( _) ) | Err ( _) => {
104
+ anyhow:: bail!( "failed to complete handshake with Webhook Relay server: remote didn't accept start message" ) ;
105
+ }
106
+ }
84
107
85
- All requests on this endpoint will be forwarded to your local URL:
86
- {}
87
- "# ,
88
- receive_url( & parsed. token) ,
89
- self . local_url,
90
- ) ;
91
- } else {
92
- // Shows that a reconnection attempt succeeded after some failing initial attempts.
93
- println ! ( "Connected!" ) ;
108
+ // The assumption is the very first message we get from the websocket reader will be the
109
+ // response to our `MessageOut::Start` but it could also be any number of control messages.
110
+ // Keep reading until we see a `MessageIn::Start` or give up after some attempts.
111
+ const MAX_ATTEMPTS : u8 = 10 ;
112
+ let mut attempts = 0 ;
113
+ let start_response = loop {
114
+ if attempts > MAX_ATTEMPTS {
115
+ anyhow:: bail!( "failed to complete handshake with Webhook Relay server: no response from remote" ) ;
116
+ }
117
+ attempts += 1 ;
118
+
119
+ match tokio:: time:: timeout ( SERVER_PING_PERIOD , ws_rx. next ( ) ) . await {
120
+ Err ( _timeout) => continue ,
121
+ Ok ( None ) => {
122
+ anyhow:: bail!( "no response from server for start message" ) ;
123
+ }
124
+ Ok ( Some ( msg) ) => {
125
+ let data = match msg? {
126
+ // Control messages.
127
+ Message :: Close ( Some ( CloseFrame { code, reason } ) )
128
+ if code == Policy && reason == SOCKET_IN_USE_REASON =>
129
+ {
130
+ return Err ( TokenInUse . into ( ) )
131
+ }
132
+ Message :: Close ( _) => {
133
+ anyhow:: bail!( "Relay server refused connection" ) ;
134
+ }
135
+ Message :: Ping ( _) | Message :: Pong ( _) | Message :: Frame ( _) => continue ,
136
+
137
+ // Messages that carry data we care to process.
138
+ Message :: Text ( s) => s. into ( ) ,
139
+ Message :: Binary ( bytes) => bytes,
140
+ } ;
141
+
142
+ match serde_json:: from_slice :: < MessageIn > ( & data) ? {
143
+ // This is what we're waiting to see. A `MessageOut::Start` sent to the writer
144
+ // should result in a `MessageInStart` coming back on the reader.
145
+ MessageIn :: Start { data, .. } => break data,
146
+ MessageIn :: Event { .. } => continue ,
147
+ } ;
94
148
}
95
149
}
96
- }
150
+ } ;
97
151
98
- // TL;DR `--no-logging` is broken the same way here as it was in Go.
99
- // Setting `--no-logging` gives a 400 response (invalid token) when you send a webhook to
100
- // Play.
101
- if self . logging && announce {
152
+ if show_welcome_message {
102
153
println ! (
103
154
r#"
104
- View logs and debug information at
155
+ Webhook Relay is now listening at:
156
+ {}
157
+
158
+ All requests on this endpoint will be forwarded to your local URL:
105
159
{}
106
- To disable logging, run `svix listen --no-logging`
107
160
"# ,
108
- view_url( & self . token)
161
+ receive_url( & start_response. token) ,
162
+ self . local_url,
109
163
) ;
164
+ // TL;DR `--no-logging` is broken the same way here as it was in Go.
165
+ // Setting `--no-logging` gives a 400 response (invalid token) when you send a webhook to
166
+ // Play.
167
+ if self . logging {
168
+ println ! (
169
+ r#"
170
+ View logs and debug information at:
171
+ {}
172
+ To disable logging, run `svix listen --no-logging`
173
+ "# ,
174
+ view_url( & self . token)
175
+ ) ;
176
+ }
177
+ } else {
178
+ // Shows that a reconnection attempt succeeded after some failing initial attempts.
179
+ println ! ( "Connected!" ) ;
110
180
}
111
181
112
182
set. spawn ( {
@@ -170,10 +240,26 @@ pub async fn listen(
170
240
let mut attempt_count = 0 ;
171
241
let mut last_attempt = Instant :: now ( ) ;
172
242
243
+ // We may ditch this token, generating a new one on the fly, depending on how the server
244
+ // responds when we connect.
245
+ let orig_token = client. token . clone ( ) ;
173
246
loop {
174
247
// Any termination Ok or Err... try to reconnect.
175
- if let Err ( e) = client. connect ( attempt_count == 0 ) . await {
248
+ let show_welcome_message = attempt_count == 0 || orig_token != client. token ;
249
+
250
+ if let Err ( e) = client. connect ( show_welcome_message) . await {
176
251
eprintln ! ( "Failed to connect to Webhook Relay: {e}" ) ;
252
+ if e. downcast_ref :: < TokenInUse > ( ) . is_some ( ) {
253
+ eprintln ! ( "Generating a new token for this session." ) ;
254
+ client. token = {
255
+ let relay_token = generate_token ( ) ?;
256
+ if logging {
257
+ format ! ( "c_{relay_token}" )
258
+ } else {
259
+ relay_token
260
+ }
261
+ } ;
262
+ }
177
263
} else {
178
264
eprintln ! ( "Failed to connect to Webhook Relay" ) ;
179
265
}
0 commit comments