diff --git a/MessagesBridge.xcodeproj/project.pbxproj b/MessagesBridge.xcodeproj/project.pbxproj index e63364a..996e108 100644 --- a/MessagesBridge.xcodeproj/project.pbxproj +++ b/MessagesBridge.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ CD936A32289B353F0093A1AC /* MBIMErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */; }; CD936A35289B47D60093A1AC /* MBIMVersionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */; }; CD936A39289B49FC0093A1AC /* MBIMStatusOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */; }; + CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */; }; CDDCF78D283F398C0087ABDF /* MBIMDeleteConversationOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */; }; CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */; }; CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */; }; @@ -260,6 +261,8 @@ CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMVersionOperation.m; sourceTree = ""; }; CD936A37289B49FC0093A1AC /* MBIMStatusOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMStatusOperation.h; sourceTree = ""; }; CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMStatusOperation.m; sourceTree = ""; }; + CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMPingPongWebSocket.h; sourceTree = ""; }; + CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMPingPongWebSocket.m; sourceTree = ""; }; CDDCF78B283F398C0087ABDF /* MBIMDeleteConversationOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDeleteConversationOperation.h; sourceTree = ""; }; CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDeleteConversationOperation.m; sourceTree = ""; }; CDE4556221A3578A0041F5DD /* IMChat+Encoded.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IMChat+Encoded.h"; sourceTree = ""; }; @@ -376,6 +379,8 @@ CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */, 1ACFCFE2219EB45300E2C237 /* MBIMHTTPConnection.h */, 1ACFCFE3219EB45300E2C237 /* MBIMHTTPConnection.m */, + CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */, + CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */, ); path = Bridge; sourceTree = ""; @@ -877,6 +882,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */, CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */, 1AA43E91219EBC2C00EDF1A7 /* MBIMHTTPConnection.m in Sources */, CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */, diff --git a/kordophone/Bridge/MBIMPingPongWebSocket.h b/kordophone/Bridge/MBIMPingPongWebSocket.h new file mode 100644 index 0000000..66a3734 --- /dev/null +++ b/kordophone/Bridge/MBIMPingPongWebSocket.h @@ -0,0 +1,17 @@ +// +// MBIMPingPongWebSocket.h +// kordophoned +// +// Created by James Magahern on 6/13/25. +// Copyright © 2025 James Magahern. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MBIMPingPongWebSocket : WebSocket + +@end + +NS_ASSUME_NONNULL_END diff --git a/kordophone/Bridge/MBIMPingPongWebSocket.m b/kordophone/Bridge/MBIMPingPongWebSocket.m new file mode 100644 index 0000000..223edfc --- /dev/null +++ b/kordophone/Bridge/MBIMPingPongWebSocket.m @@ -0,0 +1,242 @@ +// +// 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 +{ + NSData *pendingPingPayload; +} + +#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 socket:(GCDAsyncSocket *)asyncSocket { + 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 { + NSUInteger nextOpCode = [[self valueForKey:@"nextOpCode"] unsignedIntValue]; // sheesh. + + // Handle our custom ping/pong tags + if (tag == TAG_PING_PAYLOAD) { + // We've read the ping payload, now send the pong response + [self sendPongWithPayload:data socket:asyncSocket]; + + // Continue reading the next frame + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; + return; + } + + if (tag == TAG_PONG_SENT) { + // Pong was sent successfully, continue normal operation + return; + } + + // For TAG_PAYLOAD_PREFIX, we need to intercept and handle ping frames + if (tag == TAG_PAYLOAD_PREFIX) { + UInt8 *pFrame = (UInt8 *)[data bytes]; + UInt8 frame = *pFrame; + + // Check if this is a valid WebSocket frame + if (![self isValidWebSocketFrame:frame]) { + [self didClose]; + return; + } + + NSUInteger opcode = frame & 0x0F; + + // Handle ping frames specially + if (opcode == WS_OP_PING) { + // Read the payload length byte next + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH]; + return; + } + + // Handle pong frames (just log and continue) + if (opcode == WS_OP_PONG) { + NSLog(@"Received pong frame"); + // Read the payload length byte next, but we'll ignore pong payloads + [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH]; + return; + } + } + + // For TAG_PAYLOAD_LENGTH, check if we're handling a ping + if (tag == TAG_PAYLOAD_LENGTH) { + UInt8 frame = *(UInt8 *)[data bytes]; + BOOL masked = (frame & 0x80) != 0; + NSUInteger length = frame & 0x7F; + + // If we were processing a ping frame, handle the payload reading + if (nextOpCode == WS_OP_PING) { + if (length <= 125) { + if (masked) { + // Read masking key first, then payload + [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; + } + // Read ping payload + [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PING_PAYLOAD]; + return; + } + else if (length == 126) { + [asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16]; + return; + } + else { + [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64]; + return; + } + } + + // If we were processing a pong frame, just skip it + if (nextOpCode == WS_OP_PONG) { + if (length <= 125) { + if (masked) { + [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; + } + // Skip pong payload and continue to next frame + [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; + return; + } + else if (length == 126) { + [asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16]; + return; + } + else { + [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64]; + return; + } + } + } + + // Handle masking key reading for ping frames + if (tag == TAG_MSG_MASKING_KEY && nextOpCode == WS_OP_PING) { + // Store the masking key and read the actual payload + pendingPingPayload = [data copy]; + // The payload length was already determined, read it + // Note: This is a simplified version - you'd need to track the actual length + 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; +} + +// Override didReceiveMessage to add logging +- (void)didReceiveMessage:(NSString *)msg { + NSLog(@"Received message: %@", msg); + [super didReceiveMessage:msg]; +} + +// Override didOpen to add logging +- (void)didOpen { + NSLog(@"WebSocket opened with ping/pong support"); + [super didOpen]; +} + +- (void)didClose { + NSLog(@"WebSocket closed"); + [super didClose]; +} + +@end + diff --git a/kordophone/Bridge/MBIMUpdateQueue.m b/kordophone/Bridge/MBIMUpdateQueue.m index e25b40e..3dafdf9 100644 --- a/kordophone/Bridge/MBIMUpdateQueue.m +++ b/kordophone/Bridge/MBIMUpdateQueue.m @@ -11,6 +11,7 @@ #import "IMChat+Encoded.h" #import "MBIMHTTPConnection.h" #import "MBIMURLUtilities.h" +#import "MBIMPingPongWebSocket.h" #import #import @@ -105,7 +106,7 @@ static const NSUInteger kUpdateItemsCullingLength = 100; - (WebSocket *)vendUpdateWebSocketConsumerForRequest:(HTTPMessage *)request socket:(GCDAsyncSocket *)gcdSocket { - WebSocket *socket = [[WebSocket alloc] initWithRequest:request socket:gcdSocket]; + WebSocket *socket = [[MBIMPingPongWebSocket alloc] initWithRequest:request socket:gcdSocket]; socket.delegate = self; MBIMUpdateConsumer consumer = ^(NSArray *updates) {