@@ -15,13 +15,16 @@ use std::io::{self, Error, ErrorKind, Write};
1515use std:: process:: Stdio ;
1616use std:: sync:: Mutex ;
1717
18+ use base64:: Engine as _;
1819use openshell_e2e:: harness:: binary:: openshell_cmd;
1920use openshell_e2e:: harness:: sandbox:: SandboxGuard ;
21+ use sha1:: { Digest , Sha1 } ;
2022use tempfile:: NamedTempFile ;
2123use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
2224use tokio:: net:: { TcpListener , TcpStream } ;
2325use tokio:: task:: JoinHandle ;
2426
27+ const WEBSOCKET_GUID : & str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ;
2528const PROVIDER_NAME : & str = "e2e-websocket-conformance" ;
2629const TEST_SERVER_HOST : & str = "host.openshell.internal" ;
2730const TEST_SECRET : & str = "sk-e2e-websocket-conformance-secret" ;
@@ -198,6 +201,24 @@ async fn send_websocket_text(stream: &mut TcpStream, payload: &str) -> io::Resul
198201 stream. write_all ( & frame) . await
199202}
200203
204+ fn header_value ( request : & str , name : & str ) -> Option < String > {
205+ request. lines ( ) . find_map ( |line| {
206+ let ( header, value) = line. split_once ( ':' ) ?;
207+ if header. trim ( ) . eq_ignore_ascii_case ( name) {
208+ Some ( value. trim ( ) . to_string ( ) )
209+ } else {
210+ None
211+ }
212+ } )
213+ }
214+
215+ fn websocket_accept_for_key ( key : & str ) -> String {
216+ let mut hasher = Sha1 :: new ( ) ;
217+ hasher. update ( key. as_bytes ( ) ) ;
218+ hasher. update ( WEBSOCKET_GUID . as_bytes ( ) ) ;
219+ base64:: engine:: general_purpose:: STANDARD . encode ( hasher. finalize ( ) )
220+ }
221+
201222async fn handle_websocket_probe_connection ( mut stream : TcpStream ) -> io:: Result < ( ) > {
202223 let request_bytes = recv_until ( & mut stream, b"\r \n \r \n " ) . await ?;
203224 let request = String :: from_utf8_lossy ( & request_bytes) ;
@@ -208,15 +229,17 @@ async fn handle_websocket_probe_connection(mut stream: TcpStream) -> io::Result<
208229 return Ok ( ( ) ) ;
209230 }
210231
211- stream
212- . write_all (
213- b"HTTP/1.1 101 Switching Protocols\r \n \
214- Upgrade: websocket\r \n \
215- Connection: Upgrade\r \n \
216- Sec-WebSocket-Accept: test\r \n \
217- \r \n ",
218- )
219- . await ?;
232+ let accept = header_value ( & request, "Sec-WebSocket-Key" )
233+ . map ( |key| websocket_accept_for_key ( & key) )
234+ . ok_or_else ( || Error :: new ( ErrorKind :: InvalidData , "missing Sec-WebSocket-Key" ) ) ?;
235+ let response = format ! (
236+ "HTTP/1.1 101 Switching Protocols\r \n \
237+ Upgrade: websocket\r \n \
238+ Connection: Upgrade\r \n \
239+ Sec-WebSocket-Accept: {accept}\r \n \
240+ \r \n "
241+ ) ;
242+ stream. write_all ( response. as_bytes ( ) ) . await ?;
220243
221244 let text = read_websocket_text ( & mut stream) . await ?;
222245 let response = format ! (
@@ -290,6 +313,7 @@ import json
290313import os
291314import socket
292315import struct
316+ import time
293317
294318HOST = {host:?}
295319PORT = {port}
@@ -338,11 +362,22 @@ def read_frame(sock):
338362 payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
339363 return first, payload
340364
365+ def connect_with_retry(host, port, timeout_seconds=20):
366+ deadline = time.monotonic() + timeout_seconds
367+ last_error = None
368+ while time.monotonic() < deadline:
369+ try:
370+ return socket.create_connection((host, port), timeout=5)
371+ except OSError as error:
372+ last_error = error
373+ time.sleep(0.25)
374+ raise last_error
375+
341376token = os.environ[TOKEN_ENV]
342377payload = json.dumps({{"authorization": "Bearer " + token}}, sort_keys=True)
343378key = base64.b64encode(os.urandom(16)).decode("ascii")
344379
345- with socket.create_connection(( HOST, PORT), timeout=20 ) as sock:
380+ with connect_with_retry( HOST, PORT) as sock:
346381 request = (
347382 f"GET /ws HTTP/1.1\r\n"
348383 f"Host: {{HOST}}:{{PORT}}\r\n"
0 commit comments