// // MBIMPingPongWebSocket.m // kordophoned // // Created by James Magahern on 6/13/25. // Copyright © 2025 James Magahern. All rights reserved. // #import "MBIMPingPongWebSocket.h" #import "GCDAsyncSocket.h" // WebSocket opcodes (from the base class) #define TIMEOUT_NONE -1 #define TIMEOUT_REQUEST_BODY 10 #define TAG_HTTP_REQUEST_BODY 100 #define TAG_HTTP_RESPONSE_HEADERS 200 #define TAG_HTTP_RESPONSE_BODY 201 #define TAG_PREFIX 300 #define TAG_MSG_PLUS_SUFFIX 301 #define TAG_MSG_WITH_LENGTH 302 #define TAG_MSG_MASKING_KEY 303 #define TAG_PAYLOAD_PREFIX 304 #define TAG_PAYLOAD_LENGTH 305 #define TAG_PAYLOAD_LENGTH16 306 #define TAG_PAYLOAD_LENGTH64 307 #define WS_OP_CONTINUATION_FRAME 0 #define WS_OP_TEXT_FRAME 1 #define WS_OP_BINARY_FRAME 2 #define WS_OP_CONNECTION_CLOSE 8 #define WS_OP_PING 9 #define WS_OP_PONG 10 // Additional tags for ping/pong handling #define TAG_PING_PAYLOAD 400 #define TAG_PONG_SENT 401 @interface WebSocket () - (void)socket:(GCDAsyncSocket *)asyncSocket didReadData:(NSData *)data withTag:(long)tag; @end @implementation MBIMPingPongWebSocket { NSUInteger currentPayloadLength; } #pragma mark - Ping/Pong Frame Construction - (NSData *)createPongFrameWithPayload:(NSData *)pingPayload { NSMutableData *frame = [NSMutableData data]; // First byte: FIN (1) + RSV (000) + Opcode (1010 = 0xA for Pong) uint8_t firstByte = 0x8A; // 10001010 in binary [frame appendBytes:&firstByte length:1]; // Second byte: MASK (0) + Payload Length NSUInteger payloadLength = [pingPayload length]; if (payloadLength <= 125) { // Short payload length (0-125 bytes) uint8_t secondByte = (uint8_t)payloadLength; // MASK bit is 0 for server-to-client [frame appendBytes:&secondByte length:1]; } else if (payloadLength <= 65535) { // Medium payload length (126-65535 bytes) uint8_t secondByte = 126; [frame appendBytes:&secondByte length:1]; // Add 16-bit length in network byte order (big-endian) uint16_t extendedLength = htons((uint16_t)payloadLength); [frame appendBytes:&extendedLength length:2]; } else { // Large payload length (65536+ bytes) - rarely needed for pings uint8_t secondByte = 127; [frame appendBytes:&secondByte length:1]; // Add 64-bit length in network byte order (big-endian) uint64_t extendedLength = OSSwapHostToBigInt64(payloadLength); [frame appendBytes:&extendedLength length:8]; } // Append the payload (copied exactly from ping) if (payloadLength > 0) { [frame appendData:pingPayload]; } return [frame copy]; } - (void)sendPongWithPayload:(NSData *)payload { NSData *pongFrame = [self createPongFrameWithPayload:payload]; // Send the pong frame directly through the socket [asyncSocket writeData:pongFrame withTimeout:TIMEOUT_NONE tag:TAG_PONG_SENT]; NSLog(@"Sent pong frame with payload length: %lu", (unsigned long)[payload length]); } #pragma mark - Override AsyncSocket Delegate - (void)socket:(GCDAsyncSocket *)asyncSocket didReadData:(NSData *)data withTag:(long)tag { // xxx: sheesh: really need to get off of CococaHTTPServer. NSData *maskingKey = [self valueForKey:@"maskingKey"]; BOOL nextFrameMasked = [[self valueForKey:@"nextFrameMasked"] boolValue]; NSUInteger nextOpCode = [[self valueForKey:@"nextOpCode"] unsignedIntValue]; NSLog(@"tag: %ld, nextOpCode: %lu", tag, (unsigned long)nextOpCode); // Handle our custom ping payload tag if (tag == TAG_PING_PAYLOAD) { // We've read the ping payload, now send the pong response NSData *payload = data; // If the frame was masked, we need to unmask it if (nextFrameMasked && maskingKey) { NSMutableData *unmaskedPayload = [data mutableCopy]; UInt8 *payloadBytes = [unmaskedPayload mutableBytes]; UInt8 *maskBytes = (UInt8 *)[maskingKey bytes]; for (NSUInteger i = 0; i < [data length]; i++) { payloadBytes[i] ^= maskBytes[i % 4]; } payload = [unmaskedPayload copy]; } NSLog(@"Sending pong response to ping with payload length: %lu", (unsigned long)[payload length]); [self sendPongWithPayload:payload]; // Continue reading the next frame [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; return; } // Let parent handle TAG_PAYLOAD_PREFIX first to set nextOpCode if (tag == TAG_PAYLOAD_PREFIX) { [super socket:asyncSocket didReadData:data withTag:tag]; return; } // Now intercept ping/pong handling after nextOpCode is set if (nextOpCode == WS_OP_PING || nextOpCode == WS_OP_PONG) { if (tag == TAG_PAYLOAD_LENGTH) { UInt8 frame = *(UInt8 *)[data bytes]; BOOL masked = (frame & 0x80) != 0; NSUInteger length = frame & 0x7F; nextFrameMasked = masked; currentPayloadLength = length; NSLog(@"Processing ping/pong frame, masked: %d, length: %lu", masked, (unsigned long)length); if (length <= 125) { if (nextFrameMasked) { [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; } else if (length > 0) { [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD]; } else { // Empty payload, no masking - handle immediately NSLog(@"Handling empty unmasked ping payload"); [self sendPongWithPayload:[NSData data]]; [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; } } else if (length == 126) { [asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16]; } else { [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64]; } return; } if (tag == TAG_MSG_MASKING_KEY) { // Store the masking key maskingKey = [data copy]; NSLog(@"Stored masking key for ping/pong frame"); // Now read the payload (or handle empty payload) if (currentPayloadLength > 0) { [asyncSocket readDataToLength:currentPayloadLength withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD]; } else { // Empty payload - send pong immediately NSLog(@"Sending pong for empty masked ping payload"); [self sendPongWithPayload:[NSData data]]; [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; } return; } if (tag == TAG_PAYLOAD_LENGTH16) { UInt8 *pFrame = (UInt8 *)[data bytes]; NSUInteger length = ((NSUInteger)pFrame[0] << 8) | (NSUInteger)pFrame[1]; if (nextFrameMasked) { [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; } [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD]; return; } if (tag == TAG_PAYLOAD_LENGTH64) { // For 64-bit length, this is too complex for ping frames - just close [self didClose]; return; } } // For all other cases, call the parent implementation [super socket:asyncSocket didReadData:data withTag:tag]; } #pragma mark - Helper Methods // Expose the isValidWebSocketFrame method since it's private in the parent - (BOOL)isValidWebSocketFrame:(UInt8)frame { NSUInteger rsv = frame & 0x70; NSUInteger opcode = frame & 0x0F; if (rsv || (3 <= opcode && opcode <= 7) || (0xB <= opcode && opcode <= 0xF)) { return NO; } return YES; } @end