git-subtree-dir: server git-subtree-mainline:6a4054c15agit-subtree-split:800090542d
217 lines
7.6 KiB
Objective-C
217 lines
7.6 KiB
Objective-C
//
|
|
// 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];
|
|
}
|
|
|
|
#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];
|
|
|
|
// 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];
|
|
}
|
|
|
|
[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;
|
|
|
|
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
|
|
[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];
|
|
|
|
// 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
|
|
[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
|