// websocket framing read/write // @date 2026-04-19 06:17:55Z typedef enum { ws_opcode_continuation = 0x0, ws_opcode_text = 0x1, ws_opcode_binary = 0x2, ws_opcode_close = 0x8, ws_opcode_ping = 0x9, ws_opcode_pong = 0xa, } ws_opcode_t; enum { ws_flag_fin = 0x80, ws_flag_mask = 0x80, ws_no_mask = 0x00, ws_opcode_mask = 0x0f, }; enum{ ws_close_code_normal = 1000, ws_close_code_going_away = 1001, ws_close_code_protocol_error = 1002, ws_close_code_no_status_received = 1005, }; typedef struct { bool fin; // 1 = final fragment, 0 = more fragments ws_opcode_t opcode; bool masked; // 1 = client masked, 0 = server unmasked uint64_t payload_len; unsigned char mask[4]; str payload; str remaining; // unconsumed data after this frame } ws_frame_t; // Process a single WebSocket frame from a buffer // Returns 0 on success, -1 on error // On success, frame contains parsed data, remaining points to next frame static bool ws_parse_frame(str buf, ws_frame_t *frame) { if (buf.len < 2) { debug("WebSocket frame too short (need at least 2 bytes)\n"); return false; } // Parse first 2 bytes of header unsigned char h1 = buf.data[0]; unsigned char h2 = buf.data[1]; frame->fin = (h1 & ws_flag_fin) ? 1 : 0; frame->opcode = h1 & ws_opcode_mask; frame->masked = (h2 & ws_flag_mask) ? 1 : 0; ptrdiff_t payload_len = h2 & 0x7F; ptrdiff_t header_len = 2; // Read extended payload length if (payload_len == 126) { if (buf.len < 4) return false; payload_len = (buf.data[2] << 8) | buf.data[3]; header_len = 4; } else if (payload_len == 127) { if (buf.len < 10) return false; payload_len = 0; for (int i = 0; i < 8; i++) { payload_len = (payload_len << 8) | buf.data[2 + i]; } header_len = 10; } frame->payload_len = payload_len; // Read masking key if present if (frame->masked) { if (buf.len < header_len + 4) return false; memcpy(frame->mask, buf.data + header_len, 4); header_len += 4; } // Check if we have the full payload if (buf.len < header_len + payload_len) { debug("WebSocket frame incomplete (need %zu bytes, have %td)\n", header_len + payload_len, buf.len); return false; } // Set up payload view frame->payload.data = buf.data + header_len; frame->payload.len = payload_len; // Unmask payload if needed if (frame->masked) { for (ptrdiff_t i = 0; i < payload_len; i++) { frame->payload.data[i] ^= frame->mask[i % 4]; } } // Set remaining data (for multiple frames in one buffer) frame->remaining.data = buf.data + header_len + payload_len; frame->remaining.len = buf.len - (header_len + payload_len); return true; } // Helper to send a WebSocket frame (server side, unmasked) static bool ws_send_frame(int fd, int opcode, str payload) { uint8_t header_[14]; size_t header_len = 2; header_[0] = ws_flag_fin | opcode; // FIN=1 header_[1] = ws_no_mask; // mask=0 if (payload.len < 126) { header_[1] |= payload.len; } else if (payload.len < 65536) { header_[1] |= 126; header_[2] = (payload.len >> 8) & 0xFF; header_[3] = payload.len & 0xFF; header_len = 4; } else { header_[1] |= 127; uint64_t len = payload.len; for (int i = 7; i >= 0; i--) { header_[2 + i] = len & 0xFF; len >>= 8; } header_len = 10; } str const header = str_from_buf(header_, header_len); return write_header_body(fd, header, payload); } static bool ws_send_close_frame(int fd, uint16_t code, str reason) { // Close frame payload: 2-byte status code (network byte order) + optional reason size_t payload_len = 2 + reason.len; uint8_t *payload = NULL; if (payload_len > 0) { // We'll build it on stack for small payloads uint8_t stack_payload[128]; if (payload_len <= sizeof(stack_payload)) { payload = stack_payload; } else { // Should not happen for normal close reasons return false; } // Set status code (network byte order - big endian) payload[0] = (code >> 8) & 0xFF; payload[1] = code & 0xFF; // Copy reason if any if (reason.len > 0) { memcpy(payload + 2, reason.data, reason.len); } str close_payload = str_from_buf(payload, payload_len); return ws_send_frame(fd, ws_opcode_close, close_payload); } // No reason, just send empty close frame str empty = {0}; return ws_send_frame(fd, ws_opcode_close, empty); }