Skip to content

Commit d242f91

Browse files
committed
fix(e2e): stabilize websocket probe handshake
1 parent d391cfc commit d242f91

3 files changed

Lines changed: 66 additions & 10 deletions

File tree

e2e/rust/Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ path = "tests/gpu_device_selection.rs"
5555
required-features = ["e2e-gpu"]
5656

5757
[dependencies]
58+
base64 = "0.22"
5859
tokio = { version = "1.43", features = ["full"] }
5960
tempfile = "3"
61+
sha1 = "0.10"
6062
sha2 = "0.10"
6163
hex = "0.4"
6264
rand = "0.9"

e2e/rust/tests/websocket_conformance.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ use std::io::{self, Error, ErrorKind, Write};
1515
use std::process::Stdio;
1616
use std::sync::Mutex;
1717

18+
use base64::Engine as _;
1819
use openshell_e2e::harness::binary::openshell_cmd;
1920
use openshell_e2e::harness::sandbox::SandboxGuard;
21+
use sha1::{Digest, Sha1};
2022
use tempfile::NamedTempFile;
2123
use tokio::io::{AsyncReadExt, AsyncWriteExt};
2224
use tokio::net::{TcpListener, TcpStream};
2325
use tokio::task::JoinHandle;
2426

27+
const WEBSOCKET_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2528
const PROVIDER_NAME: &str = "e2e-websocket-conformance";
2629
const TEST_SERVER_HOST: &str = "host.openshell.internal";
2730
const 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+
201222
async 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
290313
import os
291314
import socket
292315
import struct
316+
import time
293317
294318
HOST = {host:?}
295319
PORT = {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+
341376
token = os.environ[TOKEN_ENV]
342377
payload = json.dumps({{"authorization": "Bearer " + token}}, sort_keys=True)
343378
key = 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

Comments
 (0)