Skip to content
Open
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
25 changes: 20 additions & 5 deletions packages/bun-uws/src/WebSocketContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ struct WebSocketContext {
us_socket_close((us_socket_t *) s, (int) reason.length(), (void *) reason.data());
}

/* RFC 6455 7.1.7 "Fail the WebSocket Connection": send a Close frame
* carrying the status code before dropping TCP. end() queues the Close,
* sets isShuttingDown so later inbound bytes are ignored, then half-closes
* (FIN) once drained. forceClose() remains for idle-timeout only.
*
* noinline: called from a dozen error branches inside the templated
* consume()/consumeMessage()/handleFragment() hot path. Letting end()
* (and transitively send()) inline at every site bloats the parser by
* hundreds of KB; one out-of-line copy per instantiation is enough for
* a cold failure path. */
__attribute__((noinline))
static void failConnection(WebSocketState<isServer> */*wState*/, void *s, uint16_t code, std::string_view reason) {
((WebSocket<SSL, isServer, USERDATA> *) s)->end(code, reason);
}

/* Returns true on breakage */
static bool handleFragment(char *data, size_t length, unsigned int remainingBytes, int opCode, bool fin, WebSocketState<isServer> *webSocketState, void *s) {
/* WebSocketData and WebSocketContextData */
Expand All @@ -113,7 +128,7 @@ struct WebSocketContext {
}

if (!inflatedFrame.has_value()) {
forceClose(webSocketState, s, ERR_TOO_BIG_MESSAGE_INFLATION);
failConnection(webSocketState, s, CLOSE_MESSAGE_TOO_BIG, ERR_TOO_BIG_MESSAGE_INFLATION);
return true;
Comment thread
robobun marked this conversation as resolved.
} else {
data = (char *) inflatedFrame->data();
Expand All @@ -123,7 +138,7 @@ struct WebSocketContext {

/* Check text messages for Utf-8 validity */
if (opCode == 1 && !protocol::isValidUtf8((unsigned char *) data, length)) {
forceClose(webSocketState, s, ERR_INVALID_TEXT);
failConnection(webSocketState, s, CLOSE_INVALID_DATA, ERR_INVALID_TEXT);
return true;
}

Expand All @@ -141,7 +156,7 @@ struct WebSocketContext {
}
/* Fragments forming a big message are not caught until appending them */
if (refusePayloadLength(length + webSocketData->fragmentBuffer.length(), webSocketState, s)) {
forceClose(webSocketState, s, ERR_TOO_BIG_MESSAGE);
failConnection(webSocketState, s, CLOSE_MESSAGE_TOO_BIG, ERR_TOO_BIG_MESSAGE);
return true;
}
webSocketData->fragmentBuffer.append(data, length);
Expand All @@ -168,7 +183,7 @@ struct WebSocketContext {
}

if (!inflatedFrame.has_value()) {
forceClose(webSocketState, s, ERR_TOO_BIG_MESSAGE_INFLATION);
failConnection(webSocketState, s, CLOSE_MESSAGE_TOO_BIG, ERR_TOO_BIG_MESSAGE_INFLATION);
return true;
} else {
data = (char *) inflatedFrame->data();
Expand All @@ -184,7 +199,7 @@ struct WebSocketContext {

/* Check text messages for Utf-8 validity */
if (opCode == 1 && !protocol::isValidUtf8((unsigned char *) data, length)) {
forceClose(webSocketState, s, ERR_INVALID_TEXT);
failConnection(webSocketState, s, CLOSE_INVALID_DATA, ERR_INVALID_TEXT);
return true;
}

Expand Down
57 changes: 35 additions & 22 deletions packages/bun-uws/src/WebSocketProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ inline constexpr std::string_view ERR_TOO_BIG_MESSAGE_INFLATION("Received too bi
inline constexpr std::string_view ERR_INVALID_CLOSE_PAYLOAD("Received invalid close payload");
inline constexpr std::string_view ERR_INVALID_MASKING("Received an incorrectly masked frame");
inline constexpr std::string_view ERR_INVALID_RSV1("Received unexpected RSV1 bit");
inline constexpr std::string_view ERR_PROTOCOL("WebSocket protocol error");

/* RFC 6455 7.4.1 status codes an endpoint sends when it Fails the connection. */
inline constexpr uint16_t CLOSE_PROTOCOL_ERROR = 1002;
inline constexpr uint16_t CLOSE_INVALID_DATA = 1007;
inline constexpr uint16_t CLOSE_MESSAGE_TOO_BIG = 1009;

enum OpCode : unsigned char {
CONTINUATION = 0,
Expand Down Expand Up @@ -121,25 +127,32 @@ static bool isValidUtf8(unsigned char *s, size_t length)

struct CloseFrame {
uint16_t code;
char *message;
const char *message;
size_t length;
};

static inline CloseFrame parseClosePayload(char *src, size_t length) {
/* If we get no code or message, default to reporting 1005 no status code present */
CloseFrame cf = {1005, nullptr, 0};
if (length >= 2) {
memcpy(&cf.code, src, 2);
cf = {cond_byte_swap<uint16_t>(cf.code), src + 2, length - 2};
// RFC 6455 §7.4: 1000-1015 defined, 1016-2999 reserved (MUST NOT be
// used), 3000-3999 IANA-registered for libraries/frameworks, 4000-4999
// private use. 1004/1005/1006/1015 are not valid on the wire.
if (cf.code < 1000 || cf.code > 4999 || (cf.code > 1015 && cf.code < 3000) ||
(cf.code >= 1004 && cf.code <= 1006) || cf.code == 1015 ||
!isValidUtf8((unsigned char *) cf.message, cf.length)) {
/* Even though we got a WebSocket close frame, it in itself is abnormal */
return {1006, nullptr, 0};
}
/* No body: report 1005 "no status code present". An empty Close is echoed back empty. */
if (length == 0) {
return {1005, nullptr, 0};
}
/* RFC 6455 5.5.1: if there is a body, its first two bytes MUST be a 2-byte
* status code. A 1-byte body is a protocol error, not "no status present". */
if (length < 2) {
return {CLOSE_PROTOCOL_ERROR, ERR_INVALID_CLOSE_PAYLOAD.data(), ERR_INVALID_CLOSE_PAYLOAD.length()};
}
CloseFrame cf;
memcpy(&cf.code, src, 2);
cf = {cond_byte_swap<uint16_t>(cf.code), src + 2, length - 2};
// RFC 6455 §7.4: 1000-1015 defined, 1016-2999 reserved (MUST NOT be
// used), 3000-3999 IANA-registered for libraries/frameworks, 4000-4999
// private use. 1004/1005/1006/1015 are not valid on the wire.
if (cf.code < 1000 || cf.code > 4999 || (cf.code > 1015 && cf.code < 3000) ||
(cf.code >= 1004 && cf.code <= 1006) || cf.code == 1015) {
return {CLOSE_PROTOCOL_ERROR, ERR_INVALID_CLOSE_PAYLOAD.data(), ERR_INVALID_CLOSE_PAYLOAD.length()};
}
if (!isValidUtf8((unsigned char *) cf.message, cf.length)) {
return {CLOSE_INVALID_DATA, ERR_INVALID_TEXT.data(), ERR_INVALID_TEXT.length()};
}
return cf;
}
Expand Down Expand Up @@ -285,18 +298,18 @@ struct WebSocketProtocol {
static inline bool consumeMessage(T payLength, char *&src, unsigned int &length, WebSocketState<isServer> *wState, void *user) {
if (getOpCode(src)) {
if (wState->state.opStack == 1 || (!wState->state.lastFin && getOpCode(src) < 2)) {
Impl::forceClose(wState, user);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_PROTOCOL);
return true;
}
wState->state.opCode[++wState->state.opStack] = (OpCode) getOpCode(src);
} else if (wState->state.opStack == -1) {
Impl::forceClose(wState, user);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_PROTOCOL);
return true;
}
wState->state.lastFin = isFin(src);

if (Impl::refusePayloadLength(payLength, wState, user)) {
Impl::forceClose(wState, user, ERR_TOO_BIG_MESSAGE);
Impl::failConnection(wState, user, CLOSE_MESSAGE_TOO_BIG, ERR_TOO_BIG_MESSAGE);
return true;
}

Expand Down Expand Up @@ -411,22 +424,22 @@ struct WebSocketProtocol {
* The MESSAGE_HEADER constants assume the mask bit matches our role, so a
* mismatched frame would otherwise desync the parser. */
if (isMasked(src) != isServer) {
Impl::forceClose(wState, user, ERR_INVALID_MASKING);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_INVALID_MASKING);
return;
}

/* RSV1 (compression) is only valid on the first frame of a data message, and
* only if negotiated (RFC 7692 6.1). A control frame or continuation must never
* reach setCompressed(): the armed flag would inflate the next data frame. */
if (rsv1(src) && (getOpCode(src) == 0 || getOpCode(src) > 2 || !Impl::setCompressed(wState, user))) {
Impl::forceClose(wState, user, ERR_INVALID_RSV1);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_INVALID_RSV1);
return;
}

// invalid reserved bits / invalid opcodes / invalid control frames
if (rsv23(src) || (getOpCode(src) > 2 && getOpCode(src) < 8) ||
getOpCode(src) > 10 || (getOpCode(src) > 2 && (!isFin(src) || payloadLength(src) > 125))) {
Impl::forceClose(wState, user);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_PROTOCOL);
return;
}

Expand All @@ -450,7 +463,7 @@ struct WebSocketProtocol {
* bit is already visible once the 2-byte base header is in: an unmasked frame
* must be refused now, not spilled until enough of a masked header arrives. */
if (isServer && length >= 2 && !isMasked(src)) {
Impl::forceClose(wState, user, ERR_INVALID_MASKING);
Impl::failConnection(wState, user, CLOSE_PROTOCOL_ERROR, ERR_INVALID_MASKING);
return;
}
if (length) {
Expand Down
Loading
Loading