#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 // Log levels: off, error, warn, info, verbose // Other flags : trace static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | 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 (HTTP_LOG_VERBOSE) { 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 !OS_OBJECT_USE_OBJC 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 (HTTP_LOG_VERBOSE) { 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 (HTTP_LOG_VERBOSE) { 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