812 lines
22 KiB
Objective-C
812 lines
22 KiB
Objective-C
#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)didReceiveData:(NSData *)data
|
|
{
|
|
HTTPLogTrace();
|
|
|
|
// Override me to process incoming data.
|
|
// This method is invoked on the websocketQueue.
|
|
//
|
|
// For completeness, you should invoke [super didReceiveData:data] in your method.
|
|
|
|
// Notify delegate
|
|
if ([delegate respondsToSelector:@selector(webSocket:didReceiveData:)])
|
|
{
|
|
[delegate webSocket:self didReceiveData:data];
|
|
}
|
|
}
|
|
|
|
- (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 if (nextOpCode == WS_OP_BINARY_FRAME)
|
|
{
|
|
[self didReceiveData:data];
|
|
}
|
|
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
|