| #import "WebSocket.h" |
| #import "HTTPMessage.h" |
| #import "GCDAsyncSocket.h" |
| #import "DDNumber.h" |
| #import "DDData.h" |
| #import "HTTPLogging.h" |
| |
| #if ! __has_feature(objc_arc) |
| #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). |
| #endif |
| |
| // Does ARC support support GCD objects? |
| // It does if the minimum deployment target is iOS 6+ or Mac OS X 8+ |
| |
| #if TARGET_OS_IPHONE |
| |
| // Compiling for iOS |
| |
| #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 0 |
| #else // iOS 5.X or earlier |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 1 |
| #endif |
| |
| #else |
| |
| // Compiling for Mac OS X |
| |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 0 |
| #else |
| #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier |
| #endif |
| |
| #endif |
| |
| // Log levels: off, error, warn, info, verbose |
| // Other flags : trace |
| static const DDLogLevel httpLogLevel = DDLogLevelWarning; // | HTTP_LOG_FLAG_TRACE; |
| |
| #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 |
| |
| static inline BOOL WS_OP_IS_FINAL_FRAGMENT(UInt8 frame) |
| { |
| return (frame & 0x80) ? YES : NO; |
| } |
| |
| static inline BOOL WS_PAYLOAD_IS_MASKED(UInt8 frame) |
| { |
| return (frame & 0x80) ? YES : NO; |
| } |
| |
| static inline NSUInteger WS_PAYLOAD_LENGTH(UInt8 frame) |
| { |
| return frame & 0x7F; |
| } |
| |
| @interface WebSocket (PrivateAPI) |
| |
| - (void)readRequestBody; |
| - (void)sendResponseBody; |
| - (void)sendResponseHeaders; |
| |
| @end |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark - |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @implementation WebSocket |
| { |
| BOOL isRFC6455; |
| BOOL nextFrameMasked; |
| NSUInteger nextOpCode; |
| NSData *maskingKey; |
| } |
| |
| + (BOOL)isWebSocketRequest:(HTTPMessage *)request |
| { |
| // Request (Draft 75): |
| // |
| // GET /demo HTTP/1.1 |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // Host: example.com |
| // Origin: http://example.com |
| // WebSocket-Protocol: sample |
| // |
| // |
| // Request (Draft 76): |
| // |
| // GET /demo HTTP/1.1 |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // Host: example.com |
| // Origin: http://example.com |
| // Sec-WebSocket-Protocol: sample |
| // Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 |
| // Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 |
| // |
| // ^n:ds[4U |
| |
| // Look for Upgrade: and Connection: headers. |
| // If we find them, and they have the proper value, |
| // we can safely assume this is a websocket request. |
| |
| NSString *upgradeHeaderValue = [request headerField:@"Upgrade"]; |
| NSString *connectionHeaderValue = [request headerField:@"Connection"]; |
| |
| BOOL isWebSocket = YES; |
| |
| if (!upgradeHeaderValue || !connectionHeaderValue) { |
| isWebSocket = NO; |
| } |
| else if (![upgradeHeaderValue caseInsensitiveCompare:@"WebSocket"] == NSOrderedSame) { |
| isWebSocket = NO; |
| } |
| else if ([connectionHeaderValue rangeOfString:@"Upgrade" options:NSCaseInsensitiveSearch].location == NSNotFound) { |
| isWebSocket = NO; |
| } |
| |
| HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isWebSocket ? @"YES" : @"NO")); |
| |
| return isWebSocket; |
| } |
| |
| + (BOOL)isVersion76Request:(HTTPMessage *)request |
| { |
| NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"]; |
| NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"]; |
| |
| BOOL isVersion76; |
| |
| if (!key1 || !key2) { |
| isVersion76 = NO; |
| } |
| else { |
| isVersion76 = YES; |
| } |
| |
| HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isVersion76 ? @"YES" : @"NO")); |
| |
| return isVersion76; |
| } |
| |
| + (BOOL)isRFC6455Request:(HTTPMessage *)request |
| { |
| NSString *key = [request headerField:@"Sec-WebSocket-Key"]; |
| BOOL isRFC6455 = (key != nil); |
| |
| HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isRFC6455 ? @"YES" : @"NO")); |
| |
| return isRFC6455; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Setup and Teardown |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| @synthesize websocketQueue; |
| |
| - (id)initWithRequest:(HTTPMessage *)aRequest socket:(GCDAsyncSocket *)socket |
| { |
| HTTPLogTrace(); |
| |
| if (aRequest == nil) |
| { |
| return nil; |
| } |
| |
| if ((self = [super init])) |
| { |
| if (httpLogLevel & DDLogFlagVerbose) |
| { |
| NSData *requestHeaders = [aRequest messageData]; |
| |
| NSString *temp = [[NSString alloc] initWithData:requestHeaders encoding:NSUTF8StringEncoding]; |
| HTTPLogVerbose(@"%@[%p] Request Headers:\n%@", THIS_FILE, self, temp); |
| } |
| |
| websocketQueue = dispatch_queue_create("WebSocket", NULL); |
| request = aRequest; |
| |
| asyncSocket = socket; |
| [asyncSocket setDelegate:self delegateQueue:websocketQueue]; |
| |
| isOpen = NO; |
| isVersion76 = [[self class] isVersion76Request:request]; |
| isRFC6455 = [[self class] isRFC6455Request:request]; |
| |
| term = [[NSData alloc] initWithBytes:"\xFF" length:1]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| HTTPLogTrace(); |
| |
| #if NEEDS_DISPATCH_RETAIN_RELEASE |
| dispatch_release(websocketQueue); |
| #endif |
| |
| [asyncSocket setDelegate:nil delegateQueue:NULL]; |
| [asyncSocket disconnect]; |
| } |
| |
| - (id)delegate |
| { |
| __block id result = nil; |
| |
| dispatch_sync(websocketQueue, ^{ |
| result = delegate; |
| }); |
| |
| return result; |
| } |
| |
| - (void)setDelegate:(id)newDelegate |
| { |
| dispatch_async(websocketQueue, ^{ |
| delegate = newDelegate; |
| }); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Start and Stop |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Starting point for the WebSocket after it has been fully initialized (including subclasses). |
| * This method is called by the HTTPConnection it is spawned from. |
| **/ |
| - (void)start |
| { |
| // This method is not exactly designed to be overriden. |
| // Subclasses are encouraged to override the didOpen method instead. |
| |
| dispatch_async(websocketQueue, ^{ @autoreleasepool { |
| |
| if (isStarted) return; |
| isStarted = YES; |
| |
| if (isVersion76) |
| { |
| [self readRequestBody]; |
| } |
| else |
| { |
| [self sendResponseHeaders]; |
| [self didOpen]; |
| } |
| }}); |
| } |
| |
| /** |
| * This method is called by the HTTPServer if it is asked to stop. |
| * The server, in turn, invokes stop on each WebSocket instance. |
| **/ |
| - (void)stop |
| { |
| // This method is not exactly designed to be overriden. |
| // Subclasses are encouraged to override the didClose method instead. |
| |
| dispatch_async(websocketQueue, ^{ @autoreleasepool { |
| |
| [asyncSocket disconnect]; |
| }}); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark HTTP Response |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (void)readRequestBody |
| { |
| HTTPLogTrace(); |
| |
| NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a request body"); |
| |
| [asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_HTTP_REQUEST_BODY]; |
| } |
| |
| - (NSString *)originResponseHeaderValue |
| { |
| HTTPLogTrace(); |
| |
| NSString *origin = [request headerField:@"Origin"]; |
| |
| if (origin == nil) |
| { |
| NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]]; |
| |
| return [NSString stringWithFormat:@"http://localhost:%@", port]; |
| } |
| else |
| { |
| return origin; |
| } |
| } |
| |
| - (NSString *)locationResponseHeaderValue |
| { |
| HTTPLogTrace(); |
| |
| NSString *location; |
| |
| NSString *scheme = [asyncSocket isSecure] ? @"wss" : @"ws"; |
| NSString *host = [request headerField:@"Host"]; |
| |
| NSString *requestUri = [[request url] relativeString]; |
| |
| if (host == nil) |
| { |
| NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]]; |
| |
| location = [NSString stringWithFormat:@"%@://localhost:%@%@", scheme, port, requestUri]; |
| } |
| else |
| { |
| location = [NSString stringWithFormat:@"%@://%@%@", scheme, host, requestUri]; |
| } |
| |
| return location; |
| } |
| |
| - (NSString *)secWebSocketKeyResponseHeaderValue { |
| NSString *key = [request headerField: @"Sec-WebSocket-Key"]; |
| NSString *guid = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; |
| return [[key stringByAppendingString: guid] dataUsingEncoding: NSUTF8StringEncoding].sha1Digest.base64Encoded; |
| } |
| |
| - (void)sendResponseHeaders |
| { |
| HTTPLogTrace(); |
| |
| // Request (Draft 75): |
| // |
| // GET /demo HTTP/1.1 |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // Host: example.com |
| // Origin: http://example.com |
| // WebSocket-Protocol: sample |
| // |
| // |
| // Request (Draft 76): |
| // |
| // GET /demo HTTP/1.1 |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // Host: example.com |
| // Origin: http://example.com |
| // Sec-WebSocket-Protocol: sample |
| // Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 |
| // Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5 |
| // |
| // ^n:ds[4U |
| |
| |
| // Response (Draft 75): |
| // |
| // HTTP/1.1 101 Web Socket Protocol Handshake |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // WebSocket-Origin: http://example.com |
| // WebSocket-Location: ws://example.com/demo |
| // WebSocket-Protocol: sample |
| // |
| // |
| // Response (Draft 76): |
| // |
| // HTTP/1.1 101 WebSocket Protocol Handshake |
| // Upgrade: WebSocket |
| // Connection: Upgrade |
| // Sec-WebSocket-Origin: http://example.com |
| // Sec-WebSocket-Location: ws://example.com/demo |
| // Sec-WebSocket-Protocol: sample |
| // |
| // 8jKS'y:G*Co,Wxa- |
| |
| |
| HTTPMessage *wsResponse = [[HTTPMessage alloc] initResponseWithStatusCode:101 |
| description:@"Web Socket Protocol Handshake" |
| version:HTTPVersion1_1]; |
| |
| [wsResponse setHeaderField:@"Upgrade" value:@"WebSocket"]; |
| [wsResponse setHeaderField:@"Connection" value:@"Upgrade"]; |
| |
| // Note: It appears that WebSocket-Origin and WebSocket-Location |
| // are required for Google's Chrome implementation to work properly. |
| // |
| // If we don't send either header, Chrome will never report the WebSocket as open. |
| // If we only send one of the two, Chrome will immediately close the WebSocket. |
| // |
| // In addition to this it appears that Chrome's implementation is very picky of the values of the headers. |
| // They have to match exactly with what Chrome sent us or it will close the WebSocket. |
| |
| NSString *originValue = [self originResponseHeaderValue]; |
| NSString *locationValue = [self locationResponseHeaderValue]; |
| |
| NSString *originField = isVersion76 ? @"Sec-WebSocket-Origin" : @"WebSocket-Origin"; |
| NSString *locationField = isVersion76 ? @"Sec-WebSocket-Location" : @"WebSocket-Location"; |
| |
| [wsResponse setHeaderField:originField value:originValue]; |
| [wsResponse setHeaderField:locationField value:locationValue]; |
| |
| NSString *acceptValue = [self secWebSocketKeyResponseHeaderValue]; |
| if (acceptValue) { |
| [wsResponse setHeaderField: @"Sec-WebSocket-Accept" value: acceptValue]; |
| } |
| |
| NSData *responseHeaders = [wsResponse messageData]; |
| |
| |
| if (httpLogLevel & DDLogFlagVerbose) |
| { |
| NSString *temp = [[NSString alloc] initWithData:responseHeaders encoding:NSUTF8StringEncoding]; |
| HTTPLogVerbose(@"%@[%p] Response Headers:\n%@", THIS_FILE, self, temp); |
| } |
| |
| [asyncSocket writeData:responseHeaders withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_HEADERS]; |
| } |
| |
| - (NSData *)processKey:(NSString *)key |
| { |
| HTTPLogTrace(); |
| |
| unichar c; |
| NSUInteger i; |
| NSUInteger length = [key length]; |
| |
| // Concatenate the digits into a string, |
| // and count the number of spaces. |
| |
| NSMutableString *numStr = [NSMutableString stringWithCapacity:10]; |
| long long numSpaces = 0; |
| |
| for (i = 0; i < length; i++) |
| { |
| c = [key characterAtIndex:i]; |
| |
| if (c >= '0' && c <= '9') |
| { |
| [numStr appendFormat:@"%C", c]; |
| } |
| else if (c == ' ') |
| { |
| numSpaces++; |
| } |
| } |
| |
| long long num = strtoll([numStr UTF8String], NULL, 10); |
| |
| long long resultHostNum; |
| |
| if (numSpaces == 0) |
| resultHostNum = 0; |
| else |
| resultHostNum = num / numSpaces; |
| |
| HTTPLogVerbose(@"key(%@) -> %qi / %qi = %qi", key, num, numSpaces, resultHostNum); |
| |
| // Convert result to 4 byte big-endian (network byte order) |
| // and then convert to raw data. |
| |
| UInt32 result = OSSwapHostToBigInt32((uint32_t)resultHostNum); |
| |
| return [NSData dataWithBytes:&result length:4]; |
| } |
| |
| - (void)sendResponseBody:(NSData *)d3 |
| { |
| HTTPLogTrace(); |
| |
| NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a response body"); |
| NSAssert([d3 length] == 8, @"Invalid requestBody length"); |
| |
| NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"]; |
| NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"]; |
| |
| NSData *d1 = [self processKey:key1]; |
| NSData *d2 = [self processKey:key2]; |
| |
| // Concatenated d1, d2 & d3 |
| |
| NSMutableData *d0 = [NSMutableData dataWithCapacity:(4+4+8)]; |
| [d0 appendData:d1]; |
| [d0 appendData:d2]; |
| [d0 appendData:d3]; |
| |
| // Hash the data using MD5 |
| |
| NSData *responseBody = [d0 md5Digest]; |
| |
| [asyncSocket writeData:responseBody withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_BODY]; |
| |
| if (httpLogLevel & DDLogFlagVerbose) |
| { |
| NSString *s1 = [[NSString alloc] initWithData:d1 encoding:NSASCIIStringEncoding]; |
| NSString *s2 = [[NSString alloc] initWithData:d2 encoding:NSASCIIStringEncoding]; |
| NSString *s3 = [[NSString alloc] initWithData:d3 encoding:NSASCIIStringEncoding]; |
| |
| NSString *s0 = [[NSString alloc] initWithData:d0 encoding:NSASCIIStringEncoding]; |
| |
| NSString *sH = [[NSString alloc] initWithData:responseBody encoding:NSASCIIStringEncoding]; |
| |
| HTTPLogVerbose(@"key1 result : raw(%@) str(%@)", d1, s1); |
| HTTPLogVerbose(@"key2 result : raw(%@) str(%@)", d2, s2); |
| HTTPLogVerbose(@"key3 passed : raw(%@) str(%@)", d3, s3); |
| HTTPLogVerbose(@"key0 concat : raw(%@) str(%@)", d0, s0); |
| HTTPLogVerbose(@"responseBody: raw(%@) str(%@)", responseBody, sH); |
| |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark Core Functionality |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| - (void)didOpen |
| { |
| HTTPLogTrace(); |
| |
| // Override me to perform any custom actions once the WebSocket has been opened. |
| // This method is invoked on the websocketQueue. |
| // |
| // Don't forget to invoke [super didOpen] in your method. |
| |
| // Start reading for messages |
| [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:(isRFC6455 ? TAG_PAYLOAD_PREFIX : TAG_PREFIX)]; |
| |
| // Notify delegate |
| if ([delegate respondsToSelector:@selector(webSocketDidOpen:)]) |
| { |
| [delegate webSocketDidOpen:self]; |
| } |
| } |
| |
| - (void)sendMessage:(NSString *)msg |
| { |
| NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; |
| [self sendData:msgData]; |
| } |
| |
| - (void)sendData:(NSData *)msgData |
| { |
| HTTPLogTrace(); |
| |
| NSMutableData *data = nil; |
| |
| if (isRFC6455) |
| { |
| NSUInteger length = msgData.length; |
| if (length <= 125) |
| { |
| data = [NSMutableData dataWithCapacity:(length + 2)]; |
| [data appendBytes: "\x81" length:1]; |
| UInt8 len = (UInt8)length; |
| [data appendBytes: &len length:1]; |
| [data appendData:msgData]; |
| } |
| else if (length <= 0xFFFF) |
| { |
| data = [NSMutableData dataWithCapacity:(length + 4)]; |
| [data appendBytes: "\x81\x7E" length:2]; |
| UInt16 len = (UInt16)length; |
| [data appendBytes: (UInt8[]){len >> 8, len & 0xFF} length:2]; |
| [data appendData:msgData]; |
| } |
| else |
| { |
| data = [NSMutableData dataWithCapacity:(length + 10)]; |
| [data appendBytes: "\x81\x7F" length:2]; |
| [data appendBytes: (UInt8[]){0, 0, 0, 0, (UInt8)(length >> 24), (UInt8)(length >> 16), (UInt8)(length >> 8), length & 0xFF} length:8]; |
| [data appendData:msgData]; |
| } |
| } |
| else |
| { |
| data = [NSMutableData dataWithCapacity:([msgData length] + 2)]; |
| |
| [data appendBytes:"\x00" length:1]; |
| [data appendData:msgData]; |
| [data appendBytes:"\xFF" length:1]; |
| } |
| |
| // Remember: GCDAsyncSocket is thread-safe |
| |
| [asyncSocket writeData:data withTimeout:TIMEOUT_NONE tag:0]; |
| } |
| |
| - (void)didReceiveMessage:(NSString *)msg |
| { |
| HTTPLogTrace(); |
| |
| // Override me to process incoming messages. |
| // This method is invoked on the websocketQueue. |
| // |
| // For completeness, you should invoke [super didReceiveMessage:msg] in your method. |
| |
| // Notify delegate |
| if ([delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) |
| { |
| [delegate webSocket:self didReceiveMessage:msg]; |
| } |
| } |
| |
| - (void)didClose |
| { |
| HTTPLogTrace(); |
| |
| // Override me to perform any cleanup when the socket is closed |
| // This method is invoked on the websocketQueue. |
| // |
| // Don't forget to invoke [super didClose] at the end of your method. |
| |
| // Notify delegate |
| if ([delegate respondsToSelector:@selector(webSocketDidClose:)]) |
| { |
| [delegate webSocketDidClose:self]; |
| } |
| |
| // Notify HTTPServer |
| [[NSNotificationCenter defaultCenter] postNotificationName:WebSocketDidDieNotification object:self]; |
| } |
| |
| #pragma mark WebSocket Frame |
| |
| - (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; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| #pragma mark AsyncSocket Delegate |
| //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| // 0 1 2 3 |
| // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 |
| // +-+-+-+-+-------+-+-------------+-------------------------------+ |
| // |F|R|R|R| opcode|M| Payload len | Extended payload length | |
| // |I|S|S|S| (4) |A| (7) | (16/64) | |
| // |N|V|V|V| |S| | (if payload len==126/127) | |
| // | |1|2|3| |K| | | |
| // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + |
| // | Extended payload length continued, if payload len == 127 | |
| // + - - - - - - - - - - - - - - - +-------------------------------+ |
| // | |Masking-key, if MASK set to 1 | |
| // +-------------------------------+-------------------------------+ |
| // | Masking-key (continued) | Payload Data | |
| // +-------------------------------- - - - - - - - - - - - - - - - + |
| // : Payload Data continued ... : |
| // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |
| // | Payload Data continued ... | |
| // +---------------------------------------------------------------+ |
| |
| - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag |
| { |
| HTTPLogTrace(); |
| |
| if (tag == TAG_HTTP_REQUEST_BODY) |
| { |
| [self sendResponseHeaders]; |
| [self sendResponseBody:data]; |
| [self didOpen]; |
| } |
| else if (tag == TAG_PREFIX) |
| { |
| UInt8 *pFrame = (UInt8 *)[data bytes]; |
| UInt8 frame = *pFrame; |
| |
| if (frame <= 0x7F) |
| { |
| [asyncSocket readDataToData:term withTimeout:TIMEOUT_NONE tag:TAG_MSG_PLUS_SUFFIX]; |
| } |
| else |
| { |
| // Unsupported frame type |
| [self didClose]; |
| } |
| } |
| else if (tag == TAG_PAYLOAD_PREFIX) |
| { |
| UInt8 *pFrame = (UInt8 *)[data bytes]; |
| UInt8 frame = *pFrame; |
| |
| if ([self isValidWebSocketFrame: frame]) |
| { |
| nextOpCode = (frame & 0x0F); |
| [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH]; |
| } |
| else |
| { |
| // Unsupported frame type |
| [self didClose]; |
| } |
| } |
| else if (tag == TAG_PAYLOAD_LENGTH) |
| { |
| UInt8 frame = *(UInt8 *)[data bytes]; |
| BOOL masked = WS_PAYLOAD_IS_MASKED(frame); |
| NSUInteger length = WS_PAYLOAD_LENGTH(frame); |
| nextFrameMasked = masked; |
| maskingKey = nil; |
| if (length <= 125) |
| { |
| if (nextFrameMasked) |
| { |
| [asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY]; |
| } |
| [asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_MSG_WITH_LENGTH]; |
| } |
| 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]; |
| } |
| } |
| else 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_MSG_WITH_LENGTH]; |
| } |
| else if (tag == TAG_PAYLOAD_LENGTH64) |
| { |
| // FIXME: 64bit data size in memory? |
| [self didClose]; |
| } |
| else if (tag == TAG_MSG_WITH_LENGTH) |
| { |
| NSUInteger msgLength = [data length]; |
| if (nextFrameMasked && maskingKey) { |
| NSMutableData *masked = data.mutableCopy; |
| UInt8 *pData = (UInt8 *)masked.mutableBytes; |
| UInt8 *pMask = (UInt8 *)maskingKey.bytes; |
| for (NSUInteger i = 0; i < msgLength; i++) |
| { |
| pData[i] = pData[i] ^ pMask[i % 4]; |
| } |
| data = masked; |
| } |
| if (nextOpCode == WS_OP_TEXT_FRAME) |
| { |
| NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding]; |
| [self didReceiveMessage:msg]; |
| } |
| else |
| { |
| [self didClose]; |
| return; |
| } |
| |
| // Read next frame |
| [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX]; |
| } |
| else if (tag == TAG_MSG_MASKING_KEY) |
| { |
| maskingKey = data.copy; |
| } |
| else |
| { |
| NSUInteger msgLength = [data length] - 1; // Excluding ending 0xFF frame |
| |
| NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding]; |
| |
| [self didReceiveMessage:msg]; |
| |
| |
| // Read next message |
| [asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PREFIX]; |
| } |
| } |
| |
| - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error |
| { |
| HTTPLogTrace2(@"%@[%p]: socketDidDisconnect:withError: %@", THIS_FILE, self, error); |
| |
| [self didClose]; |
| } |
| |
| @end |