Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions example/__tests__/nitrowebsockets.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,54 @@ describe('NitroWebSocket - Server-initiated close', () => {
});
});

describe('NitroWebSocket - Handshake headers', () => {
function handshake(
protocols?: string[],
extraHeaders?: Record<string, string>
): Promise<{ ws: NitroWebSocket; headers: Record<string, string> }> {
return withTimeout(
new Promise<{ ws: NitroWebSocket; headers: Record<string, string> }>(
(resolve, reject) => {
const ws = new NitroWebSocket(
`${WS_BASE}/ws/headers`,
protocols,
extraHeaders
);
ws.onmessage = (e) => resolve({ ws, headers: JSON.parse(e.data) });
ws.onerror = (err) => reject(new Error(`Connection error: ${err}`));
}
),
5_000,
'handshake header echo'
);
}

it('sends no Sec-WebSocket-Protocol and no Origin when no protocols requested', async () => {
const { ws, headers } = await handshake();
expect(headers['sec-websocket-protocol']).toBe(undefined);
expect(headers.origin).toBe(undefined);
expect(ws.protocol).toBe('');
await closeAndWait(ws);
});

it('offers requested subprotocols and reports the negotiated one', async () => {
const { ws, headers } = await handshake(['chat', 'superchat']);
expect(headers['sec-websocket-protocol']).toContain('chat');
expect(headers['sec-websocket-protocol']).toContain('superchat');
expect(ws.protocol).toBe('chat');
await closeAndWait(ws);
});

it('still sends caller-supplied custom headers', async () => {
const { ws, headers } = await handshake(undefined, {
'x-nitro-test': 'handshake',
});
expect(headers['x-nitro-test']).toBe('handshake');
expect(headers['sec-websocket-protocol']).toBe(undefined);
await closeAndWait(ws);
});
});

// ─── Error Handling ───────────────────────────────────────────────────────────

describe('NitroWebSocket - Error Handling', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ int nitroWsCallback(lws* wsi, enum lws_callback_reasons reason,
auto* conn = static_cast<WebSocketConnection*>(lws_wsi_user(wsi));

switch (reason) {
case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH:
if (conn) conn->handleFilterPreEstablish(wsi);
break;

case LWS_CALLBACK_CLIENT_ESTABLISHED:
if (conn) conn->handleEstablished(wsi);
break;
Expand Down Expand Up @@ -139,6 +143,7 @@ void WebSocketConnection::connect(

_url = url;
_state = State::CONNECTING;
_negotiatedProtocol.clear();

#if defined(NITRO_WS_TRACING)
ATrace_beginSection(("NitroWS connect " + url).c_str());
Expand Down Expand Up @@ -180,8 +185,8 @@ void WebSocketConnection::connect(
i.port = port;
i.path = path.c_str();
i.host = host.c_str();
i.origin = host.c_str();
i.protocol = protoStr.empty() ? "nitro-ws" : protoStr.c_str();
i.protocol = protoStr.empty() ? nullptr : protoStr.c_str();
i.local_protocol_name = "nitro-ws";
i.userdata = self.get();
i.ssl_connection = isWss ? LCCSCF_USE_SSL : 0;

Expand Down Expand Up @@ -295,6 +300,14 @@ void WebSocketConnection::setOnError(OnError cb) {



// Server-selected subprotocol must be read here: lws detaches the header
// table before LWS_CALLBACK_CLIENT_ESTABLISHED fires.
void WebSocketConnection::handleFilterPreEstablish(lws* wsi) {
char buf[256];
int n = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_PROTOCOL);
if (n > 0) _negotiatedProtocol.assign(buf, static_cast<size_t>(n));
}

void WebSocketConnection::handleEstablished(lws* wsi) {
#if defined(NITRO_WS_TRACING)
ATrace_beginSection("NitroWS established");
Expand All @@ -303,9 +316,6 @@ void WebSocketConnection::handleEstablished(lws* wsi) {
_state = State::OPEN;
_redirectCount = 0;

const lws_protocols* proto = lws_get_protocol(wsi);
if (proto && proto->name) _negotiatedProtocol = proto->name;

if (_onOpen) {
_onOpen();
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class WebSocketConnection : public WebSocketConnectionBase {
void setOnError(OnError cb) override;

// lws callback handlers (internal, not part of the base interface)
void handleFilterPreEstablish(lws* wsi);
void handleEstablished(lws* wsi);
void handleReceive(const void* in, size_t len, bool isBinary);
void handleReceiveFragment(lws* wsi, const void* in, size_t len);
Expand Down
5 changes: 4 additions & 1 deletion test-server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ const server = app.listen(PORT, '0.0.0.0', () => {

// WebSocket endpoints for the nitrowebsockets harness.
// /ws/echo -> echoes every frame back
// /ws/headers -> first message is the handshake headers as JSON
// /ws/close?code=1011&reason=x&delay=200 -> server-initiated close handshake
// /ws/kill?delay=200 -> socket destroyed, no close frame
const wss = new WebSocketServer({ server });
Expand All @@ -236,7 +237,9 @@ wss.on('connection', (ws, req) => {

ws.on('message', (data, isBinary) => ws.send(data, { binary: isBinary }));

if (url.pathname === '/ws/close') {
if (url.pathname === '/ws/headers') {
ws.send(JSON.stringify(req.headers));
} else if (url.pathname === '/ws/close') {
const code = Number(url.searchParams.get('code')) || 1011;
const reason = url.searchParams.get('reason') ?? 'server shutdown';
setTimeout(() => ws.close(code, reason), delay);
Expand Down
Loading