/** * Defines low-level structs and functions for dealing with WebSocket data * frame protocol. Usually, you won't need to use these functions and structs * directly, since abstractions are provided by the websocket connection and * message structs. */ module handy_http_websockets.frame; import streams; import slf4d; import handy_http_websockets.components : WebSocketException; /** * An enumeration of valid opcodes for websocket data frames. * https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 */ enum WebSocketFrameOpcode : ubyte { CONTINUATION = 0, TEXT_FRAME = 1, BINARY_FRAME = 2, // 0x3-7 reserved for future non-control frames. CONNECTION_CLOSE = 8, PING = 9, PONG = 10 // 0xB-F are reserved for further control frames. } /** * An enumeration of possible closing status codes for websocket connections, * as per https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 */ enum WebSocketCloseStatusCode : ushort { NORMAL = 1000, GOING_AWAY = 1001, PROTOCOL_ERROR = 1002, UNACCEPTABLE_DATA = 1003, NO_CODE = 1005, CLOSED_ABNORMALLY = 1006, INCONSISTENT_DATA = 1007, POLICY_VIOLATION = 1008, MESSAGE_TOO_BIG = 1009, EXTENSION_NEGOTIATION_FAILURE = 1010, UNEXPECTED_CONDITION = 1011, TLS_HANDSHAKE_FAILURE = 1015 } /** * Internal intermediary structure used to hold the results of parsing a * websocket frame. */ struct WebSocketFrame { bool finalFragment; WebSocketFrameOpcode opcode; ubyte[] payload; } void sendWebSocketTextFrame(S)(S stream, string text) if (isByteOutputStream!S) { sendWebSocketFrame!S( stream, WebSocketFrame(true, WebSocketFrameOpcode.TEXT_FRAME, cast(ubyte[]) text) ); } void sendWebSocketBinaryFrame(S)(S stream, ubyte[] bytes) if (isByteOutputStream!S) { sendWebSocketFrame!S( stream, WebSocketFrame(true, WebSocketFrameOpcode.BINARY_FRAME, bytes) ); } void sendWebSocketCloseFrame(S)(S stream, WebSocketCloseStatusCode code, string message) { auto bufferOut = byteArrayOutputStream(); auto dOut = dataOutputStreamFor(&bufferOut); dOut.writeToStream!ushort(code); if (message !is null && message.length > 0) { if (message.length > 123) { throw new WebSocketException("Close message is too long! Maximum of 123 bytes allowed."); } bufferOut.writeToStream(cast(ubyte[]) message); } sendWebSocketFrame!S( stream, WebSocketFrame(true, WebSocketFrameOpcode.CONNECTION_CLOSE, bufferOut.toArrayRaw()) ); } void sendWebSocketPingFrame(S)(S stream, ubyte[] payload) if (isByteOutputStream!S) { sendWebSocketFrame!S( stream, WebSocketFrame(true, WebSocketFrameOpcode.PING, payload) ); } void sendWebSocketPongFrame(S)(S stream, ubyte[] pingPayload) if (isByteOutputStream!S) { sendWebSocketFrame!S( stream, WebSocketFrame(true, WebSocketFrameOpcode.PONG, pingPayload) ); } /** * Sends a websocket frame to a byte output stream. * Params: * stream = The stream to write to. * frame = The frame to write. */ void sendWebSocketFrame(S)(S stream, WebSocketFrame frame) if (isByteOutputStream!S) { static if (isPointerToStream!S) { S ptr = stream; } else { S* ptr = &stream; } ubyte finAndOpcode = frame.opcode; if (frame.finalFragment) { finAndOpcode |= 128; } writeDataOrThrow(ptr, finAndOpcode); if (frame.payload.length < 126) { writeDataOrThrow(ptr, cast(ubyte) frame.payload.length); } else if (frame.payload.length <= ushort.max) { writeDataOrThrow(ptr, cast(ubyte) 126); writeDataOrThrow(ptr, cast(ushort) frame.payload.length); } else { writeDataOrThrow(ptr, cast(ubyte) 127); writeDataOrThrow(ptr, cast(ulong) frame.payload.length); } StreamResult result = stream.writeToStream(cast(ubyte[]) frame.payload); if (result.hasError) { throw new WebSocketException(cast(string) result.error.message); } else if (result.count != frame.payload.length) { import std.format : format; throw new WebSocketException(format!"Wrote %d bytes instead of expected %d."( result.count, frame.payload.length )); } } /** * Receives a websocket frame from a byte input stream. * Params: * stream = The stream to receive from. * Returns: The frame that was received. */ WebSocketFrame receiveWebSocketFrame(S)(S stream) if (isByteInputStream!S) { static if (isPointerToStream!S) { S ptr = stream; } else { S* ptr = &stream; } auto finalAndOpcode = parseFinAndOpcode(ptr); immutable bool finalFragment = finalAndOpcode.finalFragment; immutable ubyte opcode = finalAndOpcode.opcode; immutable bool isControlFrame = ( opcode == WebSocketFrameOpcode.CONNECTION_CLOSE || opcode == WebSocketFrameOpcode.PING || opcode == WebSocketFrameOpcode.PONG ); immutable ubyte maskAndLength = readDataOrThrow!(ubyte)(ptr); immutable bool payloadMasked = (maskAndLength & 128) > 0; immutable ubyte initialPayloadLength = maskAndLength & 127; debugF!"Websocket data frame Mask bit = %s, Initial payload length = %d"(payloadMasked, initialPayloadLength); size_t payloadLength = readPayloadLength(initialPayloadLength, ptr); if (isControlFrame && payloadLength > 125) { throw new WebSocketException("Control frame payload is too large."); } ubyte[4] maskingKey; if (payloadMasked) maskingKey = readDataOrThrow!(ubyte[4])(ptr); debugF!"Receiving websocket frame: (FIN=%s,OP=%d,MASK=%s,LENGTH=%d)"( finalFragment, opcode, payloadMasked, payloadLength ); ubyte[] buffer = readPayload(payloadLength, ptr); if (payloadMasked) unmaskData(buffer, maskingKey); return WebSocketFrame( finalFragment, cast(WebSocketFrameOpcode) opcode, buffer ); } /** * Parses the `finalFragment` flag and opcode from a websocket frame's first * header byte. * Params: * stream = The stream to read a byte from. */ private auto parseFinAndOpcode(S)(S stream) if (isByteInputStream!S) { immutable ubyte firstByte = readDataOrThrow!(ubyte)(stream); immutable bool finalFragment = (firstByte & 128) > 0; immutable bool reserved1 = (firstByte & 64) > 0; immutable bool reserved2 = (firstByte & 32) > 0; immutable bool reserved3 = (firstByte & 16) > 0; immutable ubyte opcode = firstByte & 15; if (reserved1 || reserved2 || reserved3) { throw new WebSocketException("Reserved header bits are set."); } if (!validateOpcode(opcode)) { import std.format : format; throw new WebSocketException(format!"Invalid opcode: %d"(opcode)); } import std.typecons : tuple; return tuple!("finalFragment", "opcode")(finalFragment, opcode); } private bool validateOpcode(ubyte opcode) { import std.traits : EnumMembers; static foreach (member; EnumMembers!WebSocketFrameOpcode) { if (opcode == member) return true; } return false; } /** * Reads the payload length of a websocket frame, given an initial 7-bit length * value read from the second byte of the frame's header. This may throw a * websocket exception if the length format is invalid. * Params: * initialLength = The initial 7-bit length value. * stream = The stream to read from. * Returns: The complete payload length. */ private size_t readPayloadLength(S)(ubyte initialLength, S stream) if (isByteInputStream!S) { if (initialLength == 126) { return cast(size_t) readDataOrThrow!(ushort)(stream); } else if (initialLength == 127) { return cast(size_t) readDataOrThrow!(ulong)(stream); } return cast(size_t) initialLength; } /** * Reads the payload of a websocket frame, or throws a websocket exception if * the payload can't be read in its entirety. * Params: * payloadLength = The length of the payload. * stream = The stream to read from. * Returns: The payload data that was read. */ private ubyte[] readPayload(S)(size_t payloadLength, S stream) if (isByteInputStream!S) { ubyte[] buffer = new ubyte[payloadLength]; StreamResult readResult = stream.readFromStream(buffer); if (readResult.hasError) { throw new WebSocketException(cast(string) readResult.error.message); } else if (readResult.count != payloadLength) { import std.format : format; throw new WebSocketException(format!"Read %d bytes instead of expected %d for message payload."( readResult.count, payloadLength )); } return buffer; } /** * Helper function to read data from a byte stream, or throw a websocket * exception if reading fails for any reason. * Params: * stream = The stream to read from. * Returns: The value that was read. */ private T readDataOrThrow(T, S)(S stream) if (isByteInputStream!S) { auto dIn = dataInputStreamFor(stream, Endianness.BigEndian); DataReadResult!T result = dIn.readFromStream!T(); if (result.hasError) { throw new WebSocketException(cast(string) result.error.message); } return result.value; } private void writeDataOrThrow(T, S)(S stream, T data) if (isByteOutputStream!S) { auto dOut = dataOutputStreamFor(stream, Endianness.BigEndian); OptionalStreamError err = dOut.writeToStream(data); if (err.present) { throw new WebSocketException(cast(string) err.value.message); } } /** * Applies a 4-byte mask to a websocket frame's payload bytes. * Params: * buffer = The buffer containing the payload. * mask = The mask to apply. */ private void unmaskData(ubyte[] buffer, ubyte[4] mask) { for (size_t i = 0; i < buffer.length; i++) { buffer[i] = buffer[i] ^ mask[i % 4]; } } /** * The following unit tests are derived from examples provided in * https://datatracker.ietf.org/doc/html/rfc6455#section-5.7 */ unittest { import slf4d; import slf4d.default_provider; import streams; // Note: Un-comment the below two lines to enable TRACE-level log messages. // auto provider = new shared DefaultProvider(true, Levels.TRACE); // configureLoggingProvider(provider); ubyte[] example1 = [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]; WebSocketFrame frame1 = receiveWebSocketFrame(arrayInputStreamFor(example1)); assert(frame1.finalFragment); assert(frame1.opcode == WebSocketFrameOpcode.TEXT_FRAME); assert(cast(string) frame1.payload == "Hello"); ubyte[] example2 = [0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58]; WebSocketFrame frame2 = receiveWebSocketFrame(arrayInputStreamFor(example2)); assert(frame2.finalFragment); assert(frame2.opcode == WebSocketFrameOpcode.TEXT_FRAME); assert(cast(string) frame2.payload == "Hello"); ubyte[] example3 = [0x01, 0x03, 0x48, 0x65, 0x6c]; WebSocketFrame frame3 = receiveWebSocketFrame(arrayInputStreamFor(example3)); assert(!frame3.finalFragment); assert(frame3.opcode == WebSocketFrameOpcode.TEXT_FRAME); assert(cast(string) frame3.payload == "Hel"); ubyte[] example4 = [0x80, 0x02, 0x6c, 0x6f]; WebSocketFrame frame4 = receiveWebSocketFrame(arrayInputStreamFor(example4)); assert(frame4.finalFragment); assert(frame4.opcode == WebSocketFrameOpcode.CONTINUATION); assert(cast(string) frame4.payload == "lo"); ubyte[] pingExample = [0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]; WebSocketFrame pingFrame = receiveWebSocketFrame(arrayInputStreamFor(pingExample)); assert(pingFrame.finalFragment); assert(pingFrame.opcode == WebSocketFrameOpcode.PING); assert(cast(string) pingFrame.payload == "Hello"); ubyte[] pongExample = [0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58]; WebSocketFrame pongFrame = receiveWebSocketFrame(arrayInputStreamFor(pongExample)); assert(pongFrame.finalFragment); assert(pongFrame.opcode == WebSocketFrameOpcode.PONG); assert(cast(string) pongFrame.payload == "Hello"); ubyte[] binaryExample1 = new ubyte[256]; // Populate the data with some expected values. for (int i = 0; i < binaryExample1.length; i++) binaryExample1[i] = cast(ubyte) i % ubyte.max; ubyte[] binaryExample1Full = cast(ubyte[]) [0x82, 0x7E, 0x01, 0x00] ~ binaryExample1; WebSocketFrame binaryFrame1 = receiveWebSocketFrame(arrayInputStreamFor(binaryExample1Full)); assert(binaryFrame1.finalFragment); assert(binaryFrame1.opcode == WebSocketFrameOpcode.BINARY_FRAME); assert(binaryFrame1.payload == binaryExample1); ubyte[] binaryExample2 = new ubyte[65_536]; for (int i = 0; i < binaryExample2.length; i++) binaryExample2[i] = cast(ubyte) i % ubyte.max; ubyte[] binaryExample2Full = cast(ubyte[]) [0x82, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00] ~ binaryExample2; WebSocketFrame binaryFrame2 = receiveWebSocketFrame(arrayInputStreamFor(binaryExample2Full)); assert(binaryFrame2.finalFragment); assert(binaryFrame2.opcode == WebSocketFrameOpcode.BINARY_FRAME); assert(binaryFrame2.payload == binaryExample2); }