add .gitignore

This commit is contained in:
JoyWayer
2023-12-27 20:38:37 +08:00
parent b106a628a5
commit f6343426d6
515 changed files with 104217 additions and 199 deletions

View File

@@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>
@interface NSData (DDData)
- (NSData *)md5Digest;
- (NSData *)sha1Digest;
- (NSString *)hexStringValue;
- (NSString *)base64Encoded;
- (NSData *)base64Decoded;
@end

View File

@@ -0,0 +1,158 @@
#import "DDData.h"
#import <CommonCrypto/CommonDigest.h>
@implementation NSData (DDData)
static char encodingTable[64] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' };
- (NSData *)md5Digest
{
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5([self bytes], (CC_LONG)[self length], result);
return [NSData dataWithBytes:result length:CC_MD5_DIGEST_LENGTH];
}
- (NSData *)sha1Digest
{
unsigned char result[CC_SHA1_DIGEST_LENGTH];
CC_SHA1([self bytes], (CC_LONG)[self length], result);
return [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH];
}
- (NSString *)hexStringValue
{
NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)];
const unsigned char *dataBuffer = [self bytes];
int i;
for (i = 0; i < [self length]; ++i)
{
[stringBuffer appendFormat:@"%02x", (unsigned int)dataBuffer[i]];
}
return [stringBuffer copy];
}
- (NSString *)base64Encoded
{
const unsigned char *bytes = [self bytes];
NSMutableString *result = [NSMutableString stringWithCapacity:[self length]];
unsigned long ixtext = 0;
unsigned long lentext = [self length];
long ctremaining = 0;
unsigned char inbuf[3], outbuf[4];
unsigned short i = 0;
unsigned short charsonline = 0, ctcopy = 0;
unsigned long ix = 0;
while( YES )
{
ctremaining = lentext - ixtext;
if( ctremaining <= 0 ) break;
for( i = 0; i < 3; i++ ) {
ix = ixtext + i;
if( ix < lentext ) inbuf[i] = bytes[ix];
else inbuf [i] = 0;
}
outbuf [0] = (inbuf [0] & 0xFC) >> 2;
outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4);
outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6);
outbuf [3] = inbuf [2] & 0x3F;
ctcopy = 4;
switch( ctremaining )
{
case 1:
ctcopy = 2;
break;
case 2:
ctcopy = 3;
break;
}
for( i = 0; i < ctcopy; i++ )
[result appendFormat:@"%c", encodingTable[outbuf[i]]];
for( i = ctcopy; i < 4; i++ )
[result appendString:@"="];
ixtext += 3;
charsonline += 4;
}
return [NSString stringWithString:result];
}
- (NSData *)base64Decoded
{
const unsigned char *bytes = [self bytes];
NSMutableData *result = [NSMutableData dataWithCapacity:[self length]];
unsigned long ixtext = 0;
unsigned long lentext = [self length];
unsigned char ch = 0;
unsigned char inbuf[4] = {0, 0, 0, 0};
unsigned char outbuf[3] = {0, 0, 0};
short i = 0, ixinbuf = 0;
BOOL flignore = NO;
BOOL flendtext = NO;
while( YES )
{
if( ixtext >= lentext ) break;
ch = bytes[ixtext++];
flignore = NO;
if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A';
else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26;
else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52;
else if( ch == '+' ) ch = 62;
else if( ch == '=' ) flendtext = YES;
else if( ch == '/' ) ch = 63;
else flignore = YES;
if( ! flignore )
{
short ctcharsinbuf = 3;
BOOL flbreak = NO;
if( flendtext )
{
if( ! ixinbuf ) break;
if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1;
else ctcharsinbuf = 2;
ixinbuf = 3;
flbreak = YES;
}
inbuf [ixinbuf++] = ch;
if( ixinbuf == 4 )
{
ixinbuf = 0;
outbuf [0] = ( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 );
outbuf [1] = ( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 );
outbuf [2] = ( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F );
for( i = 0; i < ctcharsinbuf; i++ )
[result appendBytes:&outbuf[i] length:1];
}
if( flbreak ) break;
}
}
return [NSData dataWithData:result];
}
@end

View File

@@ -0,0 +1,12 @@
#import <Foundation/Foundation.h>
@interface NSNumber (DDNumber)
+ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum;
+ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum;
+ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum;
+ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum;
@end

View File

@@ -0,0 +1,88 @@
#import "DDNumber.h"
@implementation NSNumber (DDNumber)
+ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum
{
if(str == nil)
{
*pNum = 0;
return NO;
}
errno = 0;
// On both 32-bit and 64-bit machines, long long = 64 bit
*pNum = strtoll([str UTF8String], NULL, 10);
if(errno != 0)
return NO;
else
return YES;
}
+ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum
{
if(str == nil)
{
*pNum = 0;
return NO;
}
errno = 0;
// On both 32-bit and 64-bit machines, unsigned long long = 64 bit
*pNum = strtoull([str UTF8String], NULL, 10);
if(errno != 0)
return NO;
else
return YES;
}
+ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum
{
if(str == nil)
{
*pNum = 0;
return NO;
}
errno = 0;
// On LP64, NSInteger = long = 64 bit
// Otherwise, NSInteger = int = long = 32 bit
*pNum = strtol([str UTF8String], NULL, 10);
if(errno != 0)
return NO;
else
return YES;
}
+ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum
{
if(str == nil)
{
*pNum = 0;
return NO;
}
errno = 0;
// On LP64, NSUInteger = unsigned long = 64 bit
// Otherwise, NSUInteger = unsigned int = unsigned long = 32 bit
*pNum = strtoul([str UTF8String], NULL, 10);
if(errno != 0)
return NO;
else
return YES;
}
@end

View File

@@ -0,0 +1,56 @@
/**
* DDRange is the functional equivalent of a 64 bit NSRange.
* The HTTP Server is designed to support very large files.
* On 32 bit architectures (ppc, i386) NSRange uses unsigned 32 bit integers.
* This only supports a range of up to 4 gigabytes.
* By defining our own variant, we can support a range up to 16 exabytes.
*
* All effort is given such that DDRange functions EXACTLY the same as NSRange.
**/
#import <Foundation/NSValue.h>
#import <Foundation/NSObjCRuntime.h>
@class NSString;
typedef struct _DDRange {
UInt64 location;
UInt64 length;
} DDRange;
typedef DDRange *DDRangePointer;
NS_INLINE DDRange DDMakeRange(UInt64 loc, UInt64 len) {
DDRange r;
r.location = loc;
r.length = len;
return r;
}
NS_INLINE UInt64 DDMaxRange(DDRange range) {
return (range.location + range.length);
}
NS_INLINE BOOL DDLocationInRange(UInt64 loc, DDRange range) {
return (loc - range.location < range.length);
}
NS_INLINE BOOL DDEqualRanges(DDRange range1, DDRange range2) {
return ((range1.location == range2.location) && (range1.length == range2.length));
}
FOUNDATION_EXPORT DDRange DDUnionRange(DDRange range1, DDRange range2);
FOUNDATION_EXPORT DDRange DDIntersectionRange(DDRange range1, DDRange range2);
FOUNDATION_EXPORT NSString *DDStringFromRange(DDRange range);
FOUNDATION_EXPORT DDRange DDRangeFromString(NSString *aString);
NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2);
@interface NSValue (NSValueDDRangeExtensions)
+ (NSValue *)valueWithDDRange:(DDRange)range;
- (DDRange)ddrangeValue;
- (NSInteger)ddrangeCompare:(NSValue *)ddrangeValue;
@end

View File

@@ -0,0 +1,104 @@
#import "DDRange.h"
#import "DDNumber.h"
DDRange DDUnionRange(DDRange range1, DDRange range2)
{
DDRange result;
result.location = MIN(range1.location, range2.location);
result.length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - result.location;
return result;
}
DDRange DDIntersectionRange(DDRange range1, DDRange range2)
{
DDRange result;
if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location))
{
return DDMakeRange(0, 0);
}
result.location = MAX(range1.location, range2.location);
result.length = MIN(DDMaxRange(range1), DDMaxRange(range2)) - result.location;
return result;
}
NSString *DDStringFromRange(DDRange range)
{
return [NSString stringWithFormat:@"{%qu, %qu}", range.location, range.length];
}
DDRange DDRangeFromString(NSString *aString)
{
DDRange result = DDMakeRange(0, 0);
// NSRange will ignore '-' characters, but not '+' characters
NSCharacterSet *cset = [NSCharacterSet characterSetWithCharactersInString:@"+0123456789"];
NSScanner *scanner = [NSScanner scannerWithString:aString];
[scanner setCharactersToBeSkipped:[cset invertedSet]];
NSString *str1 = nil;
NSString *str2 = nil;
BOOL found1 = [scanner scanCharactersFromSet:cset intoString:&str1];
BOOL found2 = [scanner scanCharactersFromSet:cset intoString:&str2];
if(found1) [NSNumber parseString:str1 intoUInt64:&result.location];
if(found2) [NSNumber parseString:str2 intoUInt64:&result.length];
return result;
}
NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2)
{
// Comparison basis:
// Which range would you encouter first if you started at zero, and began walking towards infinity.
// If you encouter both ranges at the same time, which range would end first.
if(pDDRange1->location < pDDRange2->location)
{
return NSOrderedAscending;
}
if(pDDRange1->location > pDDRange2->location)
{
return NSOrderedDescending;
}
if(pDDRange1->length < pDDRange2->length)
{
return NSOrderedAscending;
}
if(pDDRange1->length > pDDRange2->length)
{
return NSOrderedDescending;
}
return NSOrderedSame;
}
@implementation NSValue (NSValueDDRangeExtensions)
+ (NSValue *)valueWithDDRange:(DDRange)range
{
return [NSValue valueWithBytes:&range objCType:@encode(DDRange)];
}
- (DDRange)ddrangeValue
{
DDRange result;
[self getValue:&result];
return result;
}
- (NSInteger)ddrangeCompare:(NSValue *)other
{
DDRange r1 = [self ddrangeValue];
DDRange r2 = [other ddrangeValue];
return DDRangeCompare(&r1, &r2);
}
@end

View File

@@ -0,0 +1,45 @@
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE
// Note: You may need to add the CFNetwork Framework to your project
#import <CFNetwork/CFNetwork.h>
#endif
@class HTTPMessage;
@interface HTTPAuthenticationRequest : NSObject
{
BOOL isBasic;
BOOL isDigest;
NSString *base64Credentials;
NSString *username;
NSString *realm;
NSString *nonce;
NSString *uri;
NSString *qop;
NSString *nc;
NSString *cnonce;
NSString *response;
}
- (id)initWithRequest:(HTTPMessage *)request;
- (BOOL)isBasic;
- (BOOL)isDigest;
// Basic
- (NSString *)base64Credentials;
// Digest
- (NSString *)username;
- (NSString *)realm;
- (NSString *)nonce;
- (NSString *)uri;
- (NSString *)qop;
- (NSString *)nc;
- (NSString *)cnonce;
- (NSString *)response;
@end

View File

@@ -0,0 +1,195 @@
#import "HTTPAuthenticationRequest.h"
#import "HTTPMessage.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@interface HTTPAuthenticationRequest (PrivateAPI)
- (NSString *)quotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header;
- (NSString *)nonquotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header;
@end
@implementation HTTPAuthenticationRequest
- (id)initWithRequest:(HTTPMessage *)request
{
if ((self = [super init]))
{
NSString *authInfo = [request headerField:@"Authorization"];
isBasic = NO;
if ([authInfo length] >= 6)
{
isBasic = [[authInfo substringToIndex:6] caseInsensitiveCompare:@"Basic "] == NSOrderedSame;
}
isDigest = NO;
if ([authInfo length] >= 7)
{
isDigest = [[authInfo substringToIndex:7] caseInsensitiveCompare:@"Digest "] == NSOrderedSame;
}
if (isBasic)
{
NSMutableString *temp = [[authInfo substringFromIndex:6] mutableCopy];
CFStringTrimWhitespace((__bridge CFMutableStringRef)temp);
base64Credentials = [temp copy];
}
if (isDigest)
{
username = [self quotedSubHeaderFieldValue:@"username" fromHeaderFieldValue:authInfo];
realm = [self quotedSubHeaderFieldValue:@"realm" fromHeaderFieldValue:authInfo];
nonce = [self quotedSubHeaderFieldValue:@"nonce" fromHeaderFieldValue:authInfo];
uri = [self quotedSubHeaderFieldValue:@"uri" fromHeaderFieldValue:authInfo];
// It appears from RFC 2617 that the qop is to be given unquoted
// Tests show that Firefox performs this way, but Safari does not
// Thus we'll attempt to retrieve the value as nonquoted, but we'll verify it doesn't start with a quote
qop = [self nonquotedSubHeaderFieldValue:@"qop" fromHeaderFieldValue:authInfo];
if(qop && ([qop characterAtIndex:0] == '"'))
{
qop = [self quotedSubHeaderFieldValue:@"qop" fromHeaderFieldValue:authInfo];
}
nc = [self nonquotedSubHeaderFieldValue:@"nc" fromHeaderFieldValue:authInfo];
cnonce = [self quotedSubHeaderFieldValue:@"cnonce" fromHeaderFieldValue:authInfo];
response = [self quotedSubHeaderFieldValue:@"response" fromHeaderFieldValue:authInfo];
}
}
return self;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Accessors:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)isBasic {
return isBasic;
}
- (BOOL)isDigest {
return isDigest;
}
- (NSString *)base64Credentials {
return base64Credentials;
}
- (NSString *)username {
return username;
}
- (NSString *)realm {
return realm;
}
- (NSString *)nonce {
return nonce;
}
- (NSString *)uri {
return uri;
}
- (NSString *)qop {
return qop;
}
- (NSString *)nc {
return nc;
}
- (NSString *)cnonce {
return cnonce;
}
- (NSString *)response {
return response;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Private API:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Retrieves a "Sub Header Field Value" from a given header field value.
* The sub header field is expected to be quoted.
*
* In the following header field:
* Authorization: Digest username="Mufasa", qop=auth, response="6629fae4939"
* The sub header field titled 'username' is quoted, and this method would return the value @"Mufasa".
**/
- (NSString *)quotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header
{
NSRange startRange = [header rangeOfString:[NSString stringWithFormat:@"%@=\"", param]];
if(startRange.location == NSNotFound)
{
// The param was not found anywhere in the header
return nil;
}
NSUInteger postStartRangeLocation = startRange.location + startRange.length;
NSUInteger postStartRangeLength = [header length] - postStartRangeLocation;
NSRange postStartRange = NSMakeRange(postStartRangeLocation, postStartRangeLength);
NSRange endRange = [header rangeOfString:@"\"" options:0 range:postStartRange];
if(endRange.location == NSNotFound)
{
// The ending double-quote was not found anywhere in the header
return nil;
}
NSRange subHeaderRange = NSMakeRange(postStartRangeLocation, endRange.location - postStartRangeLocation);
return [header substringWithRange:subHeaderRange];
}
/**
* Retrieves a "Sub Header Field Value" from a given header field value.
* The sub header field is expected to not be quoted.
*
* In the following header field:
* Authorization: Digest username="Mufasa", qop=auth, response="6629fae4939"
* The sub header field titled 'qop' is nonquoted, and this method would return the value @"auth".
**/
- (NSString *)nonquotedSubHeaderFieldValue:(NSString *)param fromHeaderFieldValue:(NSString *)header
{
NSRange startRange = [header rangeOfString:[NSString stringWithFormat:@"%@=", param]];
if(startRange.location == NSNotFound)
{
// The param was not found anywhere in the header
return nil;
}
NSUInteger postStartRangeLocation = startRange.location + startRange.length;
NSUInteger postStartRangeLength = [header length] - postStartRangeLocation;
NSRange postStartRange = NSMakeRange(postStartRangeLocation, postStartRangeLength);
NSRange endRange = [header rangeOfString:@"," options:0 range:postStartRange];
if(endRange.location == NSNotFound)
{
// The ending comma was not found anywhere in the header
// However, if the nonquoted param is at the end of the string, there would be no comma
// This is only possible if there are no spaces anywhere
NSRange endRange2 = [header rangeOfString:@" " options:0 range:postStartRange];
if(endRange2.location != NSNotFound)
{
return nil;
}
else
{
return [header substringWithRange:postStartRange];
}
}
else
{
NSRange subHeaderRange = NSMakeRange(postStartRangeLocation, endRange.location - postStartRangeLocation);
return [header substringWithRange:subHeaderRange];
}
}
@end

View File

@@ -0,0 +1,119 @@
#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@class HTTPMessage;
@class HTTPServer;
@class WebSocket;
@protocol HTTPResponse;
#define HTTPConnectionDidDieNotification @"HTTPConnectionDidDie"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface HTTPConfig : NSObject
{
HTTPServer __unsafe_unretained *server;
NSString __strong *documentRoot;
dispatch_queue_t queue;
}
- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot;
- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot queue:(dispatch_queue_t)q;
@property (nonatomic, unsafe_unretained, readonly) HTTPServer *server;
@property (nonatomic, strong, readonly) NSString *documentRoot;
@property (nonatomic, readonly) dispatch_queue_t queue;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface HTTPConnection : NSObject
{
dispatch_queue_t connectionQueue;
GCDAsyncSocket *asyncSocket;
HTTPConfig *config;
BOOL started;
HTTPMessage *request;
unsigned int numHeaderLines;
BOOL sentResponseHeaders;
NSString *nonce;
long lastNC;
NSObject<HTTPResponse> *httpResponse;
NSMutableArray *ranges;
NSMutableArray *ranges_headers;
NSString *ranges_boundry;
int rangeIndex;
UInt64 requestContentLength;
UInt64 requestContentLengthReceived;
UInt64 requestChunkSize;
UInt64 requestChunkSizeReceived;
NSMutableArray *responseDataSizes;
}
- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig;
- (void)start;
- (void)stop;
- (void)startConnection;
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path;
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path;
- (BOOL)isSecureServer;
- (NSArray *)sslIdentityAndCertificates;
- (BOOL)isPasswordProtected:(NSString *)path;
- (BOOL)useDigestAccessAuthentication;
- (NSString *)realm;
- (NSString *)passwordForUser:(NSString *)username;
- (NSDictionary *)parseParams:(NSString *)query;
- (NSDictionary *)parseGetParams;
- (NSString *)requestURI;
- (NSArray *)directoryIndexFileNames;
- (NSString *)filePathForURI:(NSString *)path;
- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory;
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path;
- (WebSocket *)webSocketForURI:(NSString *)path;
- (void)prepareForBodyWithSize:(UInt64)contentLength;
- (void)processBodyData:(NSData *)postDataChunk;
- (void)finishBody;
- (void)handleVersionNotSupported:(NSString *)version;
- (void)handleAuthenticationFailed;
- (void)handleResourceNotFound;
- (void)handleInvalidRequest:(NSData *)data;
- (void)handleUnknownMethod:(NSString *)method;
- (NSData *)preprocessResponse:(HTTPMessage *)response;
- (NSData *)preprocessErrorResponse:(HTTPMessage *)response;
- (void)finishResponse;
- (BOOL)shouldDie;
- (void)die;
@end
@interface HTTPConnection (AsynchronousHTTPResponse)
- (void)responseHasAvailableData:(NSObject<HTTPResponse> *)sender;
- (void)responseDidAbort:(NSObject<HTTPResponse> *)sender;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
/**
* In order to provide fast and flexible logging, this project uses Cocoa Lumberjack.
*
* The Google Code page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* Here's what you need to know concerning how logging is setup for CocoaHTTPServer:
*
* There are 4 log levels:
* - Error
* - Warning
* - Info
* - Verbose
*
* In addition to this, there is a Trace flag that can be enabled.
* When tracing is enabled, it spits out the methods that are being called.
*
* Please note that tracing is separate from the log levels.
* For example, one could set the log level to warning, and enable tracing.
*
* All logging is asynchronous, except errors.
* To use logging within your own custom files, follow the steps below.
*
* Step 1:
* Import this header in your implementation file:
*
* #import "HTTPLogging.h"
*
* Step 2:
* Define your logging level in your implementation file:
*
* // Log levels: off, error, warn, info, verbose
* static const int httpLogLevel = HTTP_LOG_LEVEL_VERBOSE;
*
* If you wish to enable tracing, you could do something like this:
*
* // Debug levels: off, error, warn, info, verbose
* static const int httpLogLevel = HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_TRACE;
*
* Step 3:
* Replace your NSLog statements with HTTPLog statements according to the severity of the message.
*
* NSLog(@"Fatal error, no dohickey found!"); -> HTTPLogError(@"Fatal error, no dohickey found!");
*
* HTTPLog works exactly the same as NSLog.
* This means you can pass it multiple variables just like NSLog.
**/
#import "DDLog.h"
// Define logging context for every log message coming from the HTTP server.
// The logging context can be extracted from the DDLogMessage from within the logging framework,
// which gives loggers, formatters, and filters the ability to optionally process them differently.
#define HTTP_LOG_CONTEXT 80
// Configure log levels.
#define HTTP_LOG_FLAG_ERROR (1 << 0) // 0...00001
#define HTTP_LOG_FLAG_WARN (1 << 1) // 0...00010
#define HTTP_LOG_FLAG_INFO (1 << 2) // 0...00100
#define HTTP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000
#define HTTP_LOG_LEVEL_OFF 0 // 0...00000
#define HTTP_LOG_LEVEL_ERROR (HTTP_LOG_LEVEL_OFF | HTTP_LOG_FLAG_ERROR) // 0...00001
#define HTTP_LOG_LEVEL_WARN (HTTP_LOG_LEVEL_ERROR | HTTP_LOG_FLAG_WARN) // 0...00011
#define HTTP_LOG_LEVEL_INFO (HTTP_LOG_LEVEL_WARN | HTTP_LOG_FLAG_INFO) // 0...00111
#define HTTP_LOG_LEVEL_VERBOSE (HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_VERBOSE) // 0...01111
// Setup fine grained logging.
// The first 4 bits are being used by the standard log levels (0 - 3)
//
// We're going to add tracing, but NOT as a log level.
// Tracing can be turned on and off independently of log level.
#define HTTP_LOG_FLAG_TRACE (1 << 4) // 0...10000
// Setup the usual boolean macros.
#define HTTP_LOG_ERROR (httpLogLevel & HTTP_LOG_FLAG_ERROR)
#define HTTP_LOG_WARN (httpLogLevel & HTTP_LOG_FLAG_WARN)
#define HTTP_LOG_INFO (httpLogLevel & HTTP_LOG_FLAG_INFO)
#define HTTP_LOG_VERBOSE (httpLogLevel & HTTP_LOG_FLAG_VERBOSE)
#define HTTP_LOG_TRACE (httpLogLevel & HTTP_LOG_FLAG_TRACE)
// Configure asynchronous logging.
// We follow the default configuration,
// but we reserve a special macro to easily disable asynchronous logging for debugging purposes.
#define HTTP_LOG_ASYNC_ENABLED YES
#define HTTP_LOG_ASYNC_ERROR ( NO && HTTP_LOG_ASYNC_ENABLED)
#define HTTP_LOG_ASYNC_WARN (YES && HTTP_LOG_ASYNC_ENABLED)
#define HTTP_LOG_ASYNC_INFO (YES && HTTP_LOG_ASYNC_ENABLED)
#define HTTP_LOG_ASYNC_VERBOSE (YES && HTTP_LOG_ASYNC_ENABLED)
#define HTTP_LOG_ASYNC_TRACE (YES && HTTP_LOG_ASYNC_ENABLED)
// Define logging primitives.
#define HTTPLogError(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_ERROR, httpLogLevel, HTTP_LOG_FLAG_ERROR, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogWarn(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_WARN, httpLogLevel, HTTP_LOG_FLAG_WARN, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogInfo(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_INFO, httpLogLevel, HTTP_LOG_FLAG_INFO, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogVerbose(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_VERBOSE, httpLogLevel, HTTP_LOG_FLAG_VERBOSE, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogTrace() LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \
HTTP_LOG_CONTEXT, @"%@[%p]: %@", THIS_FILE, self, THIS_METHOD)
#define HTTPLogTrace2(frmt, ...) LOG_OBJC_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogCError(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_ERROR, httpLogLevel, HTTP_LOG_FLAG_ERROR, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogCWarn(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_WARN, httpLogLevel, HTTP_LOG_FLAG_WARN, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogCInfo(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_INFO, httpLogLevel, HTTP_LOG_FLAG_INFO, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogCVerbose(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_VERBOSE, httpLogLevel, HTTP_LOG_FLAG_VERBOSE, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)
#define HTTPLogCTrace() LOG_C_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \
HTTP_LOG_CONTEXT, @"%@[%p]: %@", THIS_FILE, self, __FUNCTION__)
#define HTTPLogCTrace2(frmt, ...) LOG_C_MAYBE(HTTP_LOG_ASYNC_TRACE, httpLogLevel, HTTP_LOG_FLAG_TRACE, \
HTTP_LOG_CONTEXT, frmt, ##__VA_ARGS__)

View File

@@ -0,0 +1,48 @@
/**
* The HTTPMessage class is a simple Objective-C wrapper around Apple's CFHTTPMessage class.
**/
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE
// Note: You may need to add the CFNetwork Framework to your project
#import <CFNetwork/CFNetwork.h>
#endif
#define HTTPVersion1_0 ((NSString *)kCFHTTPVersion1_0)
#define HTTPVersion1_1 ((NSString *)kCFHTTPVersion1_1)
@interface HTTPMessage : NSObject
{
CFHTTPMessageRef message;
}
- (id)initEmptyRequest;
- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version;
- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version;
- (BOOL)appendData:(NSData *)data;
- (BOOL)isHeaderComplete;
- (NSString *)version;
- (NSString *)method;
- (NSURL *)url;
- (NSInteger)statusCode;
- (NSDictionary *)allHeaderFields;
- (NSString *)headerField:(NSString *)headerField;
- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue;
- (NSData *)messageData;
- (NSData *)body;
- (void)setBody:(NSData *)body;
@end

View File

@@ -0,0 +1,113 @@
#import "HTTPMessage.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@implementation HTTPMessage
- (id)initEmptyRequest
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateEmpty(NULL, YES);
}
return self;
}
- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateRequest(NULL,
(__bridge CFStringRef)method,
(__bridge CFURLRef)url,
(__bridge CFStringRef)version);
}
return self;
}
- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateResponse(NULL,
(CFIndex)code,
(__bridge CFStringRef)description,
(__bridge CFStringRef)version);
}
return self;
}
- (void)dealloc
{
if (message)
{
CFRelease(message);
}
}
- (BOOL)appendData:(NSData *)data
{
return CFHTTPMessageAppendBytes(message, [data bytes], [data length]);
}
- (BOOL)isHeaderComplete
{
return CFHTTPMessageIsHeaderComplete(message);
}
- (NSString *)version
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message);
}
- (NSString *)method
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message);
}
- (NSURL *)url
{
return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message);
}
- (NSInteger)statusCode
{
return (NSInteger)CFHTTPMessageGetResponseStatusCode(message);
}
- (NSDictionary *)allHeaderFields
{
return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message);
}
- (NSString *)headerField:(NSString *)headerField
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)headerField);
}
- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue
{
CFHTTPMessageSetHeaderFieldValue(message,
(__bridge CFStringRef)headerField,
(__bridge CFStringRef)headerFieldValue);
}
- (NSData *)messageData
{
return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message);
}
- (NSData *)body
{
return (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message);
}
- (void)setBody:(NSData *)body
{
CFHTTPMessageSetBody(message, (__bridge CFDataRef)body);
}
@end

View File

@@ -0,0 +1,149 @@
#import <Foundation/Foundation.h>
@protocol HTTPResponse
/**
* Returns the length of the data in bytes.
* If you don't know the length in advance, implement the isChunked method and have it return YES.
**/
- (UInt64)contentLength;
/**
* The HTTP server supports range requests in order to allow things like
* file download resumption and optimized streaming on mobile devices.
**/
- (UInt64)offset;
- (void)setOffset:(UInt64)offset;
/**
* Returns the data for the response.
* You do not have to return data of the exact length that is given.
* You may optionally return data of a lesser length.
* However, you must never return data of a greater length than requested.
* Doing so could disrupt proper support for range requests.
*
* To support asynchronous responses, read the discussion at the bottom of this header.
**/
- (NSData *)readDataOfLength:(NSUInteger)length;
/**
* Should only return YES after the HTTPConnection has read all available data.
* That is, all data for the response has been returned to the HTTPConnection via the readDataOfLength method.
**/
- (BOOL)isDone;
@optional
/**
* If you need time to calculate any part of the HTTP response headers (status code or header fields),
* this method allows you to delay sending the headers so that you may asynchronously execute the calculations.
* Simply implement this method and return YES until you have everything you need concerning the headers.
*
* This method ties into the asynchronous response architecture of the HTTPConnection.
* You should read the full discussion at the bottom of this header.
*
* If you return YES from this method,
* the HTTPConnection will wait for you to invoke the responseHasAvailableData method.
* After you do, the HTTPConnection will again invoke this method to see if the response is ready to send the headers.
*
* You should only delay sending the headers until you have everything you need concerning just the headers.
* Asynchronously generating the body of the response is not an excuse to delay sending the headers.
* Instead you should tie into the asynchronous response architecture, and use techniques such as the isChunked method.
*
* Important: You should read the discussion at the bottom of this header.
**/
- (BOOL)delayResponseHeaders;
/**
* Status code for response.
* Allows for responses such as redirect (301), etc.
**/
- (NSInteger)status;
/**
* If you want to add any extra HTTP headers to the response,
* simply return them in a dictionary in this method.
**/
- (NSDictionary *)httpHeaders;
/**
* If you don't know the content-length in advance,
* implement this method in your custom response class and return YES.
*
* Important: You should read the discussion at the bottom of this header.
**/
- (BOOL)isChunked;
/**
* This method is called from the HTTPConnection class when the connection is closed,
* or when the connection is finished with the response.
* If your response is asynchronous, you should implement this method so you know not to
* invoke any methods on the HTTPConnection after this method is called (as the connection may be deallocated).
**/
- (void)connectionDidClose;
@end
/**
* Important notice to those implementing custom asynchronous and/or chunked responses:
*
* HTTPConnection supports asynchronous responses. All you have to do in your custom response class is
* asynchronously generate the response, and invoke HTTPConnection's responseHasAvailableData method.
* You don't have to wait until you have all of the response ready to invoke this method. For example, if you
* generate the response in incremental chunks, you could call responseHasAvailableData after generating
* each chunk. Please see the HTTPAsyncFileResponse class for an example of how to do this.
*
* The normal flow of events for an HTTPConnection while responding to a request is like this:
* - Send http resopnse headers
* - Get data from response via readDataOfLength method.
* - Add data to asyncSocket's write queue.
* - Wait for asyncSocket to notify it that the data has been sent.
* - Get more data from response via readDataOfLength method.
* - ... continue this cycle until the entire response has been sent.
*
* With an asynchronous response, the flow is a little different.
*
* First the HTTPResponse is given the opportunity to postpone sending the HTTP response headers.
* This allows the response to asynchronously execute any code needed to calculate a part of the header.
* An example might be the response needs to generate some custom header fields,
* or perhaps the response needs to look for a resource on network-attached storage.
* Since the network-attached storage may be slow, the response doesn't know whether to send a 200 or 404 yet.
* In situations such as this, the HTTPResponse simply implements the delayResponseHeaders method and returns YES.
* After returning YES from this method, the HTTPConnection will wait until the response invokes its
* responseHasAvailableData method. After this occurs, the HTTPConnection will again query the delayResponseHeaders
* method to see if the response is ready to send the headers.
* This cycle will continue until the delayResponseHeaders method returns NO.
*
* You should only delay sending the response headers until you have everything you need concerning just the headers.
* Asynchronously generating the body of the response is not an excuse to delay sending the headers.
*
* After the response headers have been sent, the HTTPConnection calls your readDataOfLength method.
* You may or may not have any available data at this point. If you don't, then simply return nil.
* You should later invoke HTTPConnection's responseHasAvailableData when you have data to send.
*
* You don't have to keep track of when you return nil in the readDataOfLength method, or how many times you've invoked
* responseHasAvailableData. Just simply call responseHasAvailableData whenever you've generated new data, and
* return nil in your readDataOfLength whenever you don't have any available data in the requested range.
* HTTPConnection will automatically detect when it should be requesting new data and will act appropriately.
*
* It's important that you also keep in mind that the HTTP server supports range requests.
* The setOffset method is mandatory, and should not be ignored.
* Make sure you take into account the offset within the readDataOfLength method.
* You should also be aware that the HTTPConnection automatically sorts any range requests.
* So if your setOffset method is called with a value of 100, then you can safely release bytes 0-99.
*
* HTTPConnection can also help you keep your memory footprint small.
* Imagine you're dynamically generating a 10 MB response. You probably don't want to load all this data into
* RAM, and sit around waiting for HTTPConnection to slowly send it out over the network. All you need to do
* is pay attention to when HTTPConnection requests more data via readDataOfLength. This is because HTTPConnection
* will never allow asyncSocket's write queue to get much bigger than READ_CHUNKSIZE bytes. You should
* consider how you might be able to take advantage of this fact to generate your asynchronous response on demand,
* while at the same time keeping your memory footprint small, and your application lightning fast.
*
* If you don't know the content-length in advanced, you should also implement the isChunked method.
* This means the response will not include a Content-Length header, and will instead use "Transfer-Encoding: chunked".
* There's a good chance that if your response is asynchronous and dynamic, it's also chunked.
* If your response is chunked, you don't need to worry about range requests.
**/

View File

@@ -0,0 +1,205 @@
#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@class WebSocket;
#if TARGET_OS_IPHONE
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 40000 // iPhone 4.0
#define IMPLEMENTED_PROTOCOLS <NSNetServiceDelegate>
#else
#define IMPLEMENTED_PROTOCOLS
#endif
#else
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1060 // Mac OS X 10.6
#define IMPLEMENTED_PROTOCOLS <NSNetServiceDelegate>
#else
#define IMPLEMENTED_PROTOCOLS
#endif
#endif
@interface HTTPServer : NSObject IMPLEMENTED_PROTOCOLS
{
// Underlying asynchronous TCP/IP socket
GCDAsyncSocket *asyncSocket;
// Dispatch queues
dispatch_queue_t serverQueue;
dispatch_queue_t connectionQueue;
void *IsOnServerQueueKey;
void *IsOnConnectionQueueKey;
// HTTP server configuration
NSString *documentRoot;
Class connectionClass;
NSString *interface;
UInt16 port;
// NSNetService and related variables
NSNetService *netService;
NSString *domain;
NSString *type;
NSString *name;
NSString *publishedName;
NSDictionary *txtRecordDictionary;
// Connection management
NSMutableArray *connections;
NSMutableArray *webSockets;
NSLock *connectionsLock;
NSLock *webSocketsLock;
BOOL isRunning;
}
/**
* Specifies the document root to serve files from.
* For example, if you set this to "/Users/<your_username>/Sites",
* then it will serve files out of the local Sites directory (including subdirectories).
*
* The default value is nil.
* The default server configuration will not serve any files until this is set.
*
* If you change the documentRoot while the server is running,
* the change will affect future incoming http connections.
**/
- (NSString *)documentRoot;
- (void)setDocumentRoot:(NSString *)value;
/**
* The connection class is the class used to handle incoming HTTP connections.
*
* The default value is [HTTPConnection class].
* You can override HTTPConnection, and then set this to [MyHTTPConnection class].
*
* If you change the connectionClass while the server is running,
* the change will affect future incoming http connections.
**/
- (Class)connectionClass;
- (void)setConnectionClass:(Class)value;
/**
* Set what interface you'd like the server to listen on.
* By default this is nil, which causes the server to listen on all available interfaces like en1, wifi etc.
*
* The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34").
* You may also use the special strings "localhost" or "loopback" to specify that
* the socket only accept connections from the local machine.
**/
- (NSString *)interface;
- (void)setInterface:(NSString *)value;
/**
* The port number to run the HTTP server on.
*
* The default port number is zero, meaning the server will automatically use any available port.
* This is the recommended port value, as it avoids possible port conflicts with other applications.
* Technologies such as Bonjour can be used to allow other applications to automatically discover the port number.
*
* Note: As is common on most OS's, you need root privledges to bind to port numbers below 1024.
*
* You can change the port property while the server is running, but it won't affect the running server.
* To actually change the port the server is listening for connections on you'll need to restart the server.
*
* The listeningPort method will always return the port number the running server is listening for connections on.
* If the server is not running this method returns 0.
**/
- (UInt16)port;
- (UInt16)listeningPort;
- (void)setPort:(UInt16)value;
/**
* Bonjour domain for publishing the service.
* The default value is "local.".
*
* Note: Bonjour publishing requires you set a type.
*
* If you change the domain property after the bonjour service has already been published (server already started),
* you'll need to invoke the republishBonjour method to update the broadcasted bonjour service.
**/
- (NSString *)domain;
- (void)setDomain:(NSString *)value;
/**
* Bonjour name for publishing the service.
* The default value is "".
*
* If using an empty string ("") for the service name when registering,
* the system will automatically use the "Computer Name".
* Using an empty string will also handle name conflicts
* by automatically appending a digit to the end of the name.
*
* Note: Bonjour publishing requires you set a type.
*
* If you change the name after the bonjour service has already been published (server already started),
* you'll need to invoke the republishBonjour method to update the broadcasted bonjour service.
*
* The publishedName method will always return the actual name that was published via the bonjour service.
* If the service is not running this method returns nil.
**/
- (NSString *)name;
- (NSString *)publishedName;
- (void)setName:(NSString *)value;
/**
* Bonjour type for publishing the service.
* The default value is nil.
* The service will not be published via bonjour unless the type is set.
*
* If you wish to publish the service as a traditional HTTP server, you should set the type to be "_http._tcp.".
*
* If you change the type after the bonjour service has already been published (server already started),
* you'll need to invoke the republishBonjour method to update the broadcasted bonjour service.
**/
- (NSString *)type;
- (void)setType:(NSString *)value;
/**
* Republishes the service via bonjour if the server is running.
* If the service was not previously published, this method will publish it (if the server is running).
**/
- (void)republishBonjour;
/**
*
**/
- (NSDictionary *)TXTRecordDictionary;
- (void)setTXTRecordDictionary:(NSDictionary *)dict;
/**
* Attempts to starts the server on the configured port, interface, etc.
*
* If an error occurs, this method returns NO and sets the errPtr (if given).
* Otherwise returns YES on success.
*
* Some examples of errors that might occur:
* - You specified the server listen on a port which is already in use by another application.
* - You specified the server listen on a port number below 1024, which requires root priviledges.
*
* Code Example:
*
* NSError *err = nil;
* if (![httpServer start:&err])
* {
* NSLog(@"Error starting http server: %@", err);
* }
**/
- (BOOL)start:(NSError **)errPtr;
/**
* Stops the server, preventing it from accepting any new connections.
* You may specify whether or not you want to close the existing client connections.
*
* The default stop method (with no arguments) will close any existing connections. (It invokes [self stop:NO])
**/
- (void)stop;
- (void)stop:(BOOL)keepExistingConnections;
- (BOOL)isRunning;
- (void)addWebSocket:(WebSocket *)ws;
- (NSUInteger)numberOfHTTPConnections;
- (NSUInteger)numberOfWebSocketConnections;
@end

View File

@@ -0,0 +1,772 @@
#import "HTTPServer.h"
#import "GCDAsyncSocket.h"
#import "HTTPConnection.h"
#import "WebSocket.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_INFO; // | HTTP_LOG_FLAG_TRACE;
@interface HTTPServer (PrivateAPI)
- (void)unpublishBonjour;
- (void)publishBonjour;
+ (void)startBonjourThreadIfNeeded;
+ (void)performBonjourBlock:(dispatch_block_t)block;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation HTTPServer
/**
* Standard Constructor.
* Instantiates an HTTP server, but does not start it.
**/
- (id)init
{
if ((self = [super init]))
{
HTTPLogTrace();
// Setup underlying dispatch queues
serverQueue = dispatch_queue_create("HTTPServer", NULL);
connectionQueue = dispatch_queue_create("HTTPConnection", NULL);
IsOnServerQueueKey = &IsOnServerQueueKey;
IsOnConnectionQueueKey = &IsOnConnectionQueueKey;
void *nonNullUnusedPointer = (__bridge void *)self; // Whatever, just not null
dispatch_queue_set_specific(serverQueue, IsOnServerQueueKey, nonNullUnusedPointer, NULL);
dispatch_queue_set_specific(connectionQueue, IsOnConnectionQueueKey, nonNullUnusedPointer, NULL);
// Initialize underlying GCD based tcp socket
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:serverQueue];
// Use default connection class of HTTPConnection
connectionClass = [HTTPConnection self];
// By default bind on all available interfaces, en1, wifi etc
interface = nil;
// Use a default port of 0
// This will allow the kernel to automatically pick an open port for us
port = 0;
// Configure default values for bonjour service
// Bonjour domain. Use the local domain by default
domain = @"local.";
// If using an empty string ("") for the service name when registering,
// the system will automatically use the "Computer Name".
// Passing in an empty string will also handle name conflicts
// by automatically appending a digit to the end of the name.
name = @"";
// Initialize arrays to hold all the HTTP and webSocket connections
connections = [[NSMutableArray alloc] init];
webSockets = [[NSMutableArray alloc] init];
connectionsLock = [[NSLock alloc] init];
webSocketsLock = [[NSLock alloc] init];
// Register for notifications of closed connections
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(connectionDidDie:)
name:HTTPConnectionDidDieNotification
object:nil];
// Register for notifications of closed websocket connections
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(webSocketDidDie:)
name:WebSocketDidDieNotification
object:nil];
isRunning = NO;
}
return self;
}
/**
* Standard Deconstructor.
* Stops the server, and clients, and releases any resources connected with this instance.
**/
- (void)dealloc
{
HTTPLogTrace();
// Remove notification observer
[[NSNotificationCenter defaultCenter] removeObserver:self];
// Stop the server if it's running
[self stop];
// Release all instance variables
#if !OS_OBJECT_USE_OBJC
dispatch_release(serverQueue);
dispatch_release(connectionQueue);
#endif
[asyncSocket setDelegate:nil delegateQueue:NULL];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The document root is filesystem root for the webserver.
* Thus requests for /index.html will be referencing the index.html file within the document root directory.
* All file requests are relative to this document root.
**/
- (NSString *)documentRoot
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
result = documentRoot;
});
return result;
}
- (void)setDocumentRoot:(NSString *)value
{
HTTPLogTrace();
// Document root used to be of type NSURL.
// Add type checking for early warning to developers upgrading from older versions.
if (value && ![value isKindOfClass:[NSString class]])
{
HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter",
THIS_FILE, THIS_METHOD, NSStringFromClass([value class]));
return;
}
NSString *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
documentRoot = valueCopy;
});
}
/**
* The connection class is the class that will be used to handle connections.
* That is, when a new connection is created, an instance of this class will be intialized.
* The default connection class is HTTPConnection.
* If you use a different connection class, it is assumed that the class extends HTTPConnection
**/
- (Class)connectionClass
{
__block Class result;
dispatch_sync(serverQueue, ^{
result = connectionClass;
});
return result;
}
- (void)setConnectionClass:(Class)value
{
HTTPLogTrace();
dispatch_async(serverQueue, ^{
connectionClass = value;
});
}
/**
* What interface to bind the listening socket to.
**/
- (NSString *)interface
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
result = interface;
});
return result;
}
- (void)setInterface:(NSString *)value
{
NSString *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
interface = valueCopy;
});
}
/**
* The port to listen for connections on.
* By default this port is initially set to zero, which allows the kernel to pick an available port for us.
* After the HTTP server has started, the port being used may be obtained by this method.
**/
- (UInt16)port
{
__block UInt16 result;
dispatch_sync(serverQueue, ^{
result = port;
});
return result;
}
- (UInt16)listeningPort
{
__block UInt16 result;
dispatch_sync(serverQueue, ^{
if (isRunning)
result = [asyncSocket localPort];
else
result = 0;
});
return result;
}
- (void)setPort:(UInt16)value
{
HTTPLogTrace();
dispatch_async(serverQueue, ^{
port = value;
});
}
/**
* Domain on which to broadcast this service via Bonjour.
* The default domain is @"local".
**/
- (NSString *)domain
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
result = domain;
});
return result;
}
- (void)setDomain:(NSString *)value
{
HTTPLogTrace();
NSString *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
domain = valueCopy;
});
}
/**
* The name to use for this service via Bonjour.
* The default name is an empty string,
* which should result in the published name being the host name of the computer.
**/
- (NSString *)name
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
result = name;
});
return result;
}
- (NSString *)publishedName
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
if (netService == nil)
{
result = nil;
}
else
{
dispatch_block_t bonjourBlock = ^{
result = [[netService name] copy];
};
[[self class] performBonjourBlock:bonjourBlock];
}
});
return result;
}
- (void)setName:(NSString *)value
{
NSString *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
name = valueCopy;
});
}
/**
* The type of service to publish via Bonjour.
* No type is set by default, and one must be set in order for the service to be published.
**/
- (NSString *)type
{
__block NSString *result;
dispatch_sync(serverQueue, ^{
result = type;
});
return result;
}
- (void)setType:(NSString *)value
{
NSString *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
type = valueCopy;
});
}
/**
* The extra data to use for this service via Bonjour.
**/
- (NSDictionary *)TXTRecordDictionary
{
__block NSDictionary *result;
dispatch_sync(serverQueue, ^{
result = txtRecordDictionary;
});
return result;
}
- (void)setTXTRecordDictionary:(NSDictionary *)value
{
HTTPLogTrace();
NSDictionary *valueCopy = [value copy];
dispatch_async(serverQueue, ^{
txtRecordDictionary = valueCopy;
// Update the txtRecord of the netService if it has already been published
if (netService)
{
NSNetService *theNetService = netService;
NSData *txtRecordData = nil;
if (txtRecordDictionary)
txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
dispatch_block_t bonjourBlock = ^{
[theNetService setTXTRecordData:txtRecordData];
};
[[self class] performBonjourBlock:bonjourBlock];
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Control
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)start:(NSError **)errPtr
{
HTTPLogTrace();
__block BOOL success = YES;
__block NSError *err = nil;
dispatch_sync(serverQueue, ^{ @autoreleasepool {
success = [asyncSocket acceptOnInterface:interface port:port error:&err];
if (success)
{
HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]);
isRunning = YES;
[self publishBonjour];
}
else
{
HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err);
}
}});
if (errPtr)
*errPtr = err;
return success;
}
- (void)stop
{
[self stop:NO];
}
- (void)stop:(BOOL)keepExistingConnections
{
HTTPLogTrace();
dispatch_sync(serverQueue, ^{ @autoreleasepool {
// First stop publishing the service via bonjour
[self unpublishBonjour];
// Stop listening / accepting incoming connections
[asyncSocket disconnect];
isRunning = NO;
if (!keepExistingConnections)
{
// Stop all HTTP connections the server owns
[connectionsLock lock];
for (HTTPConnection *connection in connections)
{
[connection stop];
}
[connections removeAllObjects];
[connectionsLock unlock];
// Stop all WebSocket connections the server owns
[webSocketsLock lock];
for (WebSocket *webSocket in webSockets)
{
[webSocket stop];
}
[webSockets removeAllObjects];
[webSocketsLock unlock];
}
}});
}
- (BOOL)isRunning
{
__block BOOL result;
dispatch_sync(serverQueue, ^{
result = isRunning;
});
return result;
}
- (void)addWebSocket:(WebSocket *)ws
{
[webSocketsLock lock];
HTTPLogTrace();
[webSockets addObject:ws];
[webSocketsLock unlock];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Server Status
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Returns the number of http client connections that are currently connected to the server.
**/
- (NSUInteger)numberOfHTTPConnections
{
NSUInteger result = 0;
[connectionsLock lock];
result = [connections count];
[connectionsLock unlock];
return result;
}
/**
* Returns the number of websocket client connections that are currently connected to the server.
**/
- (NSUInteger)numberOfWebSocketConnections
{
NSUInteger result = 0;
[webSocketsLock lock];
result = [webSockets count];
[webSocketsLock unlock];
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Incoming Connections
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (HTTPConfig *)config
{
// Override me if you want to provide a custom config to the new connection.
//
// Generally this involves overriding the HTTPConfig class to include any custom settings,
// and then having this method return an instance of 'MyHTTPConfig'.
// Note: Think you can make the server faster by putting each connection on its own queue?
// Then benchmark it before and after and discover for yourself the shocking truth!
//
// Try the apache benchmark tool (already installed on your Mac):
// $ ab -n 1000 -c 1 http://localhost:<port>/some_path.html
return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:connectionQueue];
}
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket
configuration:[self config]];
[connectionsLock lock];
[connections addObject:newConnection];
[connectionsLock unlock];
[newConnection start];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Bonjour
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)publishBonjour
{
HTTPLogTrace();
NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue");
if (type)
{
netService = [[NSNetService alloc] initWithDomain:domain type:type name:name port:[asyncSocket localPort]];
[netService setDelegate:self];
NSNetService *theNetService = netService;
NSData *txtRecordData = nil;
if (txtRecordDictionary)
txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDictionary];
dispatch_block_t bonjourBlock = ^{
[theNetService removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[theNetService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[theNetService publish];
// Do not set the txtRecordDictionary prior to publishing!!!
// This will cause the OS to crash!!!
if (txtRecordData)
{
[theNetService setTXTRecordData:txtRecordData];
}
};
[[self class] startBonjourThreadIfNeeded];
[[self class] performBonjourBlock:bonjourBlock];
}
}
- (void)unpublishBonjour
{
HTTPLogTrace();
NSAssert(dispatch_get_specific(IsOnServerQueueKey) != NULL, @"Must be on serverQueue");
if (netService)
{
NSNetService *theNetService = netService;
dispatch_block_t bonjourBlock = ^{
[theNetService stop];
};
[[self class] performBonjourBlock:bonjourBlock];
netService = nil;
}
}
/**
* Republishes the service via bonjour if the server is running.
* If the service was not previously published, this method will publish it (if the server is running).
**/
- (void)republishBonjour
{
HTTPLogTrace();
dispatch_async(serverQueue, ^{
[self unpublishBonjour];
[self publishBonjour];
});
}
/**
* Called when our bonjour service has been successfully published.
* This method does nothing but output a log message telling us about the published service.
**/
- (void)netServiceDidPublish:(NSNetService *)ns
{
// Override me to do something here...
//
// Note: This method is invoked on our bonjour thread.
HTTPLogInfo(@"Bonjour Service Published: domain(%@) type(%@) name(%@)", [ns domain], [ns type], [ns name]);
}
/**
* Called if our bonjour service failed to publish itself.
* This method does nothing but output a log message telling us about the published service.
**/
- (void)netService:(NSNetService *)ns didNotPublish:(NSDictionary *)errorDict
{
// Override me to do something here...
//
// Note: This method in invoked on our bonjour thread.
HTTPLogWarn(@"Failed to Publish Service: domain(%@) type(%@) name(%@) - %@",
[ns domain], [ns type], [ns name], errorDict);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Notifications
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted.
* It allows us to remove the connection from our array.
**/
- (void)connectionDidDie:(NSNotification *)notification
{
// Note: This method is called on the connection queue that posted the notification
[connectionsLock lock];
HTTPLogTrace();
[connections removeObject:[notification object]];
[connectionsLock unlock];
}
/**
* This method is automatically called when a notification of type WebSocketDidDieNotification is posted.
* It allows us to remove the websocket from our array.
**/
- (void)webSocketDidDie:(NSNotification *)notification
{
// Note: This method is called on the connection queue that posted the notification
[webSocketsLock lock];
HTTPLogTrace();
[webSockets removeObject:[notification object]];
[webSocketsLock unlock];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Bonjour Thread
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* NSNetService is runloop based, so it requires a thread with a runloop.
* This gives us two options:
*
* - Use the main thread
* - Setup our own dedicated thread
*
* Since we have various blocks of code that need to synchronously access the netservice objects,
* using the main thread becomes troublesome and a potential for deadlock.
**/
static NSThread *bonjourThread;
+ (void)startBonjourThreadIfNeeded
{
HTTPLogTrace();
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
HTTPLogVerbose(@"%@: Starting bonjour thread...", THIS_FILE);
bonjourThread = [[NSThread alloc] initWithTarget:self
selector:@selector(bonjourThread)
object:nil];
[bonjourThread start];
});
}
+ (void)bonjourThread
{
@autoreleasepool {
HTTPLogVerbose(@"%@: BonjourThread: Started", THIS_FILE);
// We can't run the run loop unless it has an associated input source or a timer.
// So we'll just create a timer that will never fire - unless the server runs for 10,000 years.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(donothingatall:)
userInfo:nil
repeats:YES];
#pragma clang diagnostic pop
[[NSRunLoop currentRunLoop] run];
HTTPLogVerbose(@"%@: BonjourThread: Aborted", THIS_FILE);
}
}
+ (void)executeBonjourBlock:(dispatch_block_t)block
{
HTTPLogTrace();
NSAssert([NSThread currentThread] == bonjourThread, @"Executed on incorrect thread");
block();
}
+ (void)performBonjourBlock:(dispatch_block_t)block
{
HTTPLogTrace();
[self performSelector:@selector(executeBonjourBlock:)
onThread:bonjourThread
withObject:block
waitUntilDone:YES];
}
@end

View File

@@ -0,0 +1,65 @@
#import "MultipartMessageHeader.h"
/*
Part one: http://tools.ietf.org/html/rfc2045 (Format of Internet Message Bodies)
Part two: http://tools.ietf.org/html/rfc2046 (Media Types)
Part three: http://tools.ietf.org/html/rfc2047 (Message Header Extensions for Non-ASCII Text)
Part four: http://tools.ietf.org/html/rfc4289 (Registration Procedures)
Part five: http://tools.ietf.org/html/rfc2049 (Conformance Criteria and Examples)
Internet message format: http://tools.ietf.org/html/rfc2822
Multipart/form-data http://tools.ietf.org/html/rfc2388
*/
@class MultipartFormDataParser;
//-----------------------------------------------------------------
// protocol MultipartFormDataParser
//-----------------------------------------------------------------
@protocol MultipartFormDataParserDelegate <NSObject>
@optional
- (void) processContent:(NSData*) data WithHeader:(MultipartMessageHeader*) header;
- (void) processEndOfPartWithHeader:(MultipartMessageHeader*) header;
- (void) processPreambleData:(NSData*) data;
- (void) processEpilogueData:(NSData*) data;
- (void) processStartOfPartWithHeader:(MultipartMessageHeader*) header;
@end
//-----------------------------------------------------------------
// interface MultipartFormDataParser
//-----------------------------------------------------------------
@interface MultipartFormDataParser : NSObject {
NSMutableData* pendingData;
NSData* boundaryData;
MultipartMessageHeader* currentHeader;
BOOL waitingForCRLF;
BOOL reachedEpilogue;
BOOL processedPreamble;
BOOL checkForContentEnd;
#if __has_feature(objc_arc_weak)
__weak id<MultipartFormDataParserDelegate> delegate;
#else
__unsafe_unretained id<MultipartFormDataParserDelegate> delegate;
#endif
int currentEncoding;
NSStringEncoding formEncoding;
}
- (BOOL) appendData:(NSData*) data;
- (id) initWithBoundary:(NSString*) boundary formEncoding:(NSStringEncoding) formEncoding;
#if __has_feature(objc_arc_weak)
@property(weak, readwrite) id delegate;
#else
@property(unsafe_unretained, readwrite) id delegate;
#endif
@property(readwrite) NSStringEncoding formEncoding;
@end

View File

@@ -0,0 +1,529 @@
#import "MultipartFormDataParser.h"
#import "DDData.h"
#import "HTTPLogging.h"
#pragma mark log level
#ifdef DEBUG
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#else
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#endif
#ifdef __x86_64__
#define FMTNSINT "li"
#else
#define FMTNSINT "i"
#endif
//-----------------------------------------------------------------
// interface MultipartFormDataParser (private)
//-----------------------------------------------------------------
@interface MultipartFormDataParser (private)
+ (NSData*) decodedDataFromData:(NSData*) data encoding:(int) encoding;
- (int) findHeaderEnd:(NSData*) workingData fromOffset:(int) offset;
- (int) findContentEnd:(NSData*) data fromOffset:(int) offset;
- (int) numberOfBytesToLeavePendingWithData:(NSData*) data length:(NSUInteger) length encoding:(int) encoding;
- (int) offsetTillNewlineSinceOffset:(int) offset inData:(NSData*) data;
- (int) processPreamble:(NSData*) workingData;
@end
//-----------------------------------------------------------------
// implementation MultipartFormDataParser
//-----------------------------------------------------------------
@implementation MultipartFormDataParser
@synthesize delegate,formEncoding;
- (id) initWithBoundary:(NSString*) boundary formEncoding:(NSStringEncoding) _formEncoding {
if( nil == (self = [super init]) ){
return self;
}
if( nil == boundary ) {
HTTPLogWarn(@"MultipartFormDataParser: init with zero boundary");
return nil;
}
boundaryData = [[@"\r\n--" stringByAppendingString:boundary] dataUsingEncoding:NSASCIIStringEncoding];
pendingData = [[NSMutableData alloc] init];
currentEncoding = contentTransferEncoding_binary;
currentHeader = nil;
formEncoding = _formEncoding;
reachedEpilogue = NO;
processedPreamble = NO;
return self;
}
- (BOOL) appendData:(NSData *)data {
// Can't parse without boundary;
if( nil == boundaryData ) {
HTTPLogError(@"MultipartFormDataParser: Trying to parse multipart without specifying a valid boundary");
assert(false);
return NO;
}
NSData* workingData = data;
if( pendingData.length ) {
[pendingData appendData:data];
workingData = pendingData;
}
// the parser saves parse stat in the offset variable, which indicates offset of unhandled part in
// currently received chunk. Before returning, we always drop all data up to offset, leaving
// only unhandled for the next call
int offset = 0;
// don't parse data unless its size is greater then boundary length, so we couldn't
// misfind the boundary, if it got split into different data chunks
NSUInteger sizeToLeavePending = boundaryData.length;
if( !reachedEpilogue && workingData.length <= sizeToLeavePending ) {
// not enough data even to start parsing.
// save to pending data.
if( !pendingData.length ) {
[pendingData appendData:data];
}
if( checkForContentEnd ) {
if( pendingData.length >= 2 ) {
if( *(uint16_t*)(pendingData.bytes + offset) == 0x2D2D ) {
// we found the multipart end. all coming next is an epilogue.
HTTPLogVerbose(@"MultipartFormDataParser: End of multipart message");
waitingForCRLF = YES;
reachedEpilogue = YES;
offset+= 2;
}
else {
checkForContentEnd = NO;
waitingForCRLF = YES;
return YES;
}
} else {
return YES;
}
}
else {
return YES;
}
}
while( true ) {
if( checkForContentEnd ) {
// the flag will be raised to check if the last part was the last one.
if( offset < workingData.length -1 ) {
char* bytes = (char*) workingData.bytes;
if( *(uint16_t*)(bytes + offset) == 0x2D2D ) {
// we found the multipart end. all coming next is an epilogue.
HTTPLogVerbose(@"MultipartFormDataParser: End of multipart message");
checkForContentEnd = NO;
reachedEpilogue = YES;
// still wait for CRLF, that comes after boundary, but before epilogue.
waitingForCRLF = YES;
offset += 2;
}
else {
// it's not content end, we have to wait till separator line end before next part comes
waitingForCRLF = YES;
checkForContentEnd = NO;
}
}
else {
// we haven't got enough data to check for content end.
// save current unhandled data (it may be 1 byte) to pending and recheck on next chunk received
if( offset < workingData.length ) {
[pendingData setData:[NSData dataWithBytes:workingData.bytes + workingData.length-1 length:1]];
}
else {
// there is no unhandled data now, wait for more chunks
[pendingData setData:[NSData data]];
}
return YES;
}
}
if( waitingForCRLF ) {
// the flag will be raised in the code below, meaning, we've read the boundary, but
// didnt find the end of boundary line yet.
offset = [self offsetTillNewlineSinceOffset:offset inData:workingData];
if( -1 == offset ) {
// didnt find the endl again.
if( offset ) {
// we still have to save the unhandled data (maybe it's 1 byte CR)
if( *((char*)workingData.bytes + workingData.length -1) == '\r' ) {
[pendingData setData:[NSData dataWithBytes:workingData.bytes + workingData.length-1 length:1]];
}
else {
// or save nothing if it wasnt
[pendingData setData:[NSData data]];
}
}
return YES;
}
waitingForCRLF = NO;
}
if( !processedPreamble ) {
// got to find the first boundary before the actual content begins.
offset = [self processPreamble:workingData];
// wait for more data for preamble
if( -1 == offset )
return YES;
// invoke continue to skip newline after boundary.
continue;
}
if( reachedEpilogue ) {
// parse all epilogue data to delegate.
if( [delegate respondsToSelector:@selector(processEpilogueData:)] ) {
NSData* epilogueData = [NSData dataWithBytesNoCopy: (char*) workingData.bytes + offset length: workingData.length - offset freeWhenDone:NO];
[delegate processEpilogueData: epilogueData];
}
return YES;
}
if( nil == currentHeader ) {
// nil == currentHeader is a state flag, indicating we are waiting for header now.
// whenever part is over, currentHeader is set to nil.
// try to find CRLFCRLF bytes in the data, which indicates header end.
// we won't parse header parts, as they won't be too large.
int headerEnd = [self findHeaderEnd:workingData fromOffset:offset];
if( -1 == headerEnd ) {
// didn't recieve the full header yet.
if( !pendingData.length) {
// store the unprocessed data till next chunks come
[pendingData appendBytes:data.bytes + offset length:data.length - offset];
}
else {
if( offset ) {
// save the current parse state; drop all handled data and save unhandled only.
pendingData = [[NSMutableData alloc] initWithBytes: (char*) workingData.bytes + offset length:workingData.length - offset];
}
}
return YES;
}
else {
// let the header parser do it's job from now on.
NSData * headerData = [NSData dataWithBytesNoCopy: (char*) workingData.bytes + offset length:headerEnd + 2 - offset freeWhenDone:NO];
currentHeader = [[MultipartMessageHeader alloc] initWithData:headerData formEncoding:formEncoding];
if( nil == currentHeader ) {
// we've found the data is in wrong format.
HTTPLogError(@"MultipartFormDataParser: MultipartFormDataParser: wrong input format, coulnd't get a valid header");
return NO;
}
if( [delegate respondsToSelector:@selector(processStartOfPartWithHeader:)] ) {
[delegate processStartOfPartWithHeader:currentHeader];
}
HTTPLogVerbose(@"MultipartFormDataParser: MultipartFormDataParser: Retrieved part header.");
}
// skip the two trailing \r\n, in addition to the whole header.
offset = headerEnd + 4;
}
// after we've got the header, we try to
// find the boundary in the data.
int contentEnd = [self findContentEnd:workingData fromOffset:offset];
if( contentEnd == -1 ) {
// this case, we didn't find the boundary, so the data is related to the current part.
// we leave the sizeToLeavePending amount of bytes to make sure we don't include
// boundary part in processed data.
NSUInteger sizeToPass = workingData.length - offset - sizeToLeavePending;
// if we parse BASE64 encoded data, or Quoted-Printable data, we will make sure we don't break the format
int leaveTrailing = [self numberOfBytesToLeavePendingWithData:data length:sizeToPass encoding:currentEncoding];
sizeToPass -= leaveTrailing;
if( sizeToPass <= 0 ) {
// wait for more data!
if( offset ) {
[pendingData setData:[NSData dataWithBytes:(char*) workingData.bytes + offset length:workingData.length - offset]];
}
return YES;
}
// decode the chunk and let the delegate use it (store in a file, for example)
NSData* decodedData = [MultipartFormDataParser decodedDataFromData:[NSData dataWithBytesNoCopy:(char*)workingData.bytes + offset length:workingData.length - offset - sizeToLeavePending freeWhenDone:NO] encoding:currentEncoding];
if( [delegate respondsToSelector:@selector(processContent:WithHeader:)] ) {
HTTPLogVerbose(@"MultipartFormDataParser: Processed %"FMTNSINT" bytes of body",sizeToPass);
[delegate processContent: decodedData WithHeader:currentHeader];
}
// store the unprocessed data till the next chunks come.
[pendingData setData:[NSData dataWithBytes:(char*)workingData.bytes + workingData.length - sizeToLeavePending length:sizeToLeavePending]];
return YES;
}
else {
// Here we found the boundary.
// let the delegate process it, and continue going to the next parts.
if( [delegate respondsToSelector:@selector(processContent:WithHeader:)] ) {
[delegate processContent:[NSData dataWithBytesNoCopy:(char*) workingData.bytes + offset length:contentEnd - offset freeWhenDone:NO] WithHeader:currentHeader];
}
if( [delegate respondsToSelector:@selector(processEndOfPartWithHeader:)] ){
[delegate processEndOfPartWithHeader:currentHeader];
HTTPLogVerbose(@"MultipartFormDataParser: End of body part");
}
currentHeader = nil;
// set up offset to continue with the remaining data (if any)
// cast to int because above comment suggests a small number
offset = contentEnd + (int)boundaryData.length;
checkForContentEnd = YES;
// setting the flag tells the parser to skip all the data till CRLF
}
}
return YES;
}
//-----------------------------------------------------------------
#pragma mark private methods
- (int) offsetTillNewlineSinceOffset:(int) offset inData:(NSData*) data {
char* bytes = (char*) data.bytes;
NSUInteger length = data.length;
if( offset >= length - 1 )
return -1;
while ( *(uint16_t*)(bytes + offset) != 0x0A0D ) {
// find the trailing \r\n after the boundary. The boundary line might have any number of whitespaces before CRLF, according to rfc2046
// in debug, we might also want to know, if the file is somehow misformatted.
#ifdef DEBUG
if( !isspace(*(bytes+offset)) ) {
HTTPLogWarn(@"MultipartFormDataParser: Warning, non-whitespace character '%c' between boundary bytes and CRLF in boundary line",*(bytes+offset) );
}
if( !isspace(*(bytes+offset+1)) ) {
HTTPLogWarn(@"MultipartFormDataParser: Warning, non-whitespace character '%c' between boundary bytes and CRLF in boundary line",*(bytes+offset+1) );
}
#endif
offset++;
if( offset >= length ) {
// no endl found within current data
return -1;
}
}
offset += 2;
return offset;
}
- (int) processPreamble:(NSData*) data {
int offset = 0;
char* boundaryBytes = (char*) boundaryData.bytes + 2; // the first boundary won't have CRLF preceding.
char* dataBytes = (char*) data.bytes;
NSUInteger boundaryLength = boundaryData.length - 2;
NSUInteger dataLength = data.length;
// find the boundary without leading CRLF.
while( offset < dataLength - boundaryLength +1 ) {
int i;
for( i = 0;i < boundaryLength; i++ ) {
if( boundaryBytes[i] != dataBytes[offset + i] )
break;
}
if( i == boundaryLength ) {
break;
}
offset++;
}
if( offset == dataLength ) {
// the end of preamble wasn't found in this chunk
NSUInteger sizeToProcess = dataLength - boundaryLength;
if( sizeToProcess > 0) {
if( [delegate respondsToSelector:@selector(processPreambleData:)] ) {
NSData* preambleData = [NSData dataWithBytesNoCopy: (char*) data.bytes length: data.length - offset - boundaryLength freeWhenDone:NO];
[delegate processPreambleData:preambleData];
HTTPLogVerbose(@"MultipartFormDataParser: processed preamble");
}
pendingData = [NSMutableData dataWithBytes: data.bytes + data.length - boundaryLength length:boundaryLength];
}
return -1;
}
else {
if ( offset && [delegate respondsToSelector:@selector(processPreambleData:)] ) {
NSData* preambleData = [NSData dataWithBytesNoCopy: (char*) data.bytes length: offset freeWhenDone:NO];
[delegate processPreambleData:preambleData];
}
offset +=boundaryLength;
// tells to skip CRLF after the boundary.
processedPreamble = YES;
waitingForCRLF = YES;
}
return offset;
}
- (int) findHeaderEnd:(NSData*) workingData fromOffset:(int)offset {
char* bytes = (char*) workingData.bytes;
NSUInteger inputLength = workingData.length;
uint16_t separatorBytes = 0x0A0D;
while( true ) {
if(inputLength < offset + 3 ) {
// wait for more data
return -1;
}
if( (*((uint16_t*) (bytes+offset)) == separatorBytes) && (*((uint16_t*) (bytes+offset)+1) == separatorBytes) ) {
return offset;
}
offset++;
}
return -1;
}
- (int) findContentEnd:(NSData*) data fromOffset:(int) offset {
char* boundaryBytes = (char*) boundaryData.bytes;
char* dataBytes = (char*) data.bytes;
NSUInteger boundaryLength = boundaryData.length;
NSUInteger dataLength = data.length;
while( offset < dataLength - boundaryLength +1 ) {
int i;
for( i = 0;i < boundaryLength; i++ ) {
if( boundaryBytes[i] != dataBytes[offset + i] )
break;
}
if( i == boundaryLength ) {
return offset;
}
offset++;
}
return -1;
}
- (int) numberOfBytesToLeavePendingWithData:(NSData*) data length:(int) length encoding:(int) encoding {
// If we have BASE64 or Quoted-Printable encoded data, we have to be sure
// we don't break the format.
int sizeToLeavePending = 0;
if( encoding == contentTransferEncoding_base64 ) {
char* bytes = (char*) data.bytes;
int i;
for( i = length - 1; i > 0; i++ ) {
if( * (uint16_t*) (bytes + i) == 0x0A0D ) {
break;
}
}
// now we've got to be sure that the length of passed data since last line
// is multiplier of 4.
sizeToLeavePending = (length - i) & ~0x11; // size to leave pending = length-i - (length-i) %4;
return sizeToLeavePending;
}
if( encoding == contentTransferEncoding_quotedPrintable ) {
// we don't pass more less then 3 bytes anyway.
if( length <= 2 )
return length;
// check the last bytes to be start of encoded symbol.
const char* bytes = data.bytes + length - 2;
if( bytes[0] == '=' )
return 2;
if( bytes[1] == '=' )
return 1;
return 0;
}
return 0;
}
//-----------------------------------------------------------------
#pragma mark decoding
+ (NSData*) decodedDataFromData:(NSData*) data encoding:(int) encoding {
switch (encoding) {
case contentTransferEncoding_base64: {
return [data base64Decoded];
} break;
case contentTransferEncoding_quotedPrintable: {
return [self decodedDataFromQuotedPrintableData:data];
} break;
default: {
return data;
} break;
}
}
+ (NSData*) decodedDataFromQuotedPrintableData:(NSData *)data {
// http://tools.ietf.org/html/rfc2045#section-6.7
const char hex [] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', };
NSMutableData* result = [[NSMutableData alloc] initWithLength:data.length];
const char* bytes = (const char*) data.bytes;
int count = 0;
NSUInteger length = data.length;
while( count < length ) {
if( bytes[count] == '=' ) {
[result appendBytes:bytes length:count];
bytes = bytes + count + 1;
length -= count + 1;
count = 0;
if( length < 3 ) {
HTTPLogWarn(@"MultipartFormDataParser: warning, trailing '=' in quoted printable data");
}
// soft newline
if( bytes[0] == '\r' ) {
bytes += 1;
if(bytes[1] == '\n' ) {
bytes += 2;
}
continue;
}
char encodedByte = 0;
for( int i = 0; i < sizeof(hex); i++ ) {
if( hex[i] == bytes[0] ) {
encodedByte += i << 4;
}
if( hex[i] == bytes[1] ) {
encodedByte += i;
}
}
[result appendBytes:&encodedByte length:1];
bytes += 2;
}
#ifdef DEBUG
if( (unsigned char) bytes[count] > 126 ) {
HTTPLogWarn(@"MultipartFormDataParser: Warning, character with code above 126 appears in quoted printable encoded data");
}
#endif
count++;
}
return result;
}
@end

View File

@@ -0,0 +1,33 @@
//
// MultipartMessagePart.h
// HttpServer
//
// Created by Валерий Гаврилов on 29.03.12.
// Copyright (c) 2012 LLC "Online Publishing Partners" (onlinepp.ru). All rights reserved.
//
#import <Foundation/Foundation.h>
//-----------------------------------------------------------------
// interface MultipartMessageHeader
//-----------------------------------------------------------------
enum {
contentTransferEncoding_unknown,
contentTransferEncoding_7bit,
contentTransferEncoding_8bit,
contentTransferEncoding_binary,
contentTransferEncoding_base64,
contentTransferEncoding_quotedPrintable,
};
@interface MultipartMessageHeader : NSObject {
NSMutableDictionary* fields;
int encoding;
NSString* contentDispositionName;
}
@property (strong,readonly) NSDictionary* fields;
@property (readonly) int encoding;
- (id) initWithData:(NSData*) data formEncoding:(NSStringEncoding) encoding;
@end

View File

@@ -0,0 +1,86 @@
//
// MultipartMessagePart.m
// HttpServer
//
// Created by Валерий Гаврилов on 29.03.12.
// Copyright (c) 2012 LLC "Online Publishing Partners" (onlinepp.ru). All rights reserved.
#import "MultipartMessageHeader.h"
#import "MultipartMessageHeaderField.h"
#import "HTTPLogging.h"
//-----------------------------------------------------------------
#pragma mark log level
#ifdef DEBUG
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#else
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#endif
//-----------------------------------------------------------------
// implementation MultipartMessageHeader
//-----------------------------------------------------------------
@implementation MultipartMessageHeader
@synthesize fields,encoding;
- (id) initWithData:(NSData *)data formEncoding:(NSStringEncoding) formEncoding {
if( nil == (self = [super init]) ) {
return self;
}
fields = [[NSMutableDictionary alloc] initWithCapacity:1];
// In case encoding is not mentioned,
encoding = contentTransferEncoding_unknown;
char* bytes = (char*)data.bytes;
NSUInteger length = data.length;
int offset = 0;
// split header into header fields, separated by \r\n
uint16_t fields_separator = 0x0A0D; // \r\n
while( offset < length - 2 ) {
// the !isspace condition is to support header unfolding
if( (*(uint16_t*) (bytes+offset) == fields_separator) && ((offset == length - 2) || !(isspace(bytes[offset+2])) )) {
NSData* fieldData = [NSData dataWithBytesNoCopy:bytes length:offset freeWhenDone:NO];
MultipartMessageHeaderField* field = [[MultipartMessageHeaderField alloc] initWithData: fieldData contentEncoding:formEncoding];
if( field ) {
[fields setObject:field forKey:field.name];
HTTPLogVerbose(@"MultipartFormDataParser: Processed Header field '%@'",field.name);
}
else {
NSString* fieldStr = [[NSString alloc] initWithData:fieldData encoding:NSASCIIStringEncoding];
HTTPLogWarn(@"MultipartFormDataParser: Failed to parse MIME header field. Input ASCII string:%@",fieldStr);
}
// move to the next header field
bytes += offset + 2;
length -= offset + 2;
offset = 0;
continue;
}
++ offset;
}
if( !fields.count ) {
// it was an empty header.
// we have to set default values.
// default header.
[fields setObject:@"text/plain" forKey:@"Content-Type"];
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@",fields];
}
@end

View File

@@ -0,0 +1,23 @@
#import <Foundation/Foundation.h>
//-----------------------------------------------------------------
// interface MultipartMessageHeaderField
//-----------------------------------------------------------------
@interface MultipartMessageHeaderField : NSObject {
NSString* name;
NSString* value;
NSMutableDictionary* params;
}
@property (strong, readonly) NSString* value;
@property (strong, readonly) NSDictionary* params;
@property (strong, readonly) NSString* name;
//- (id) initWithLine:(NSString*) line;
//- (id) initWithName:(NSString*) paramName value:(NSString*) paramValue;
- (id) initWithData:(NSData*) data contentEncoding:(NSStringEncoding) encoding;
@end

View File

@@ -0,0 +1,211 @@
#import "MultipartMessageHeaderField.h"
#import "HTTPLogging.h"
//-----------------------------------------------------------------
#pragma mark log level
#ifdef DEBUG
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#else
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN;
#endif
// helpers
int findChar(const char* str,NSUInteger length, char c);
NSString* extractParamValue(const char* bytes, NSUInteger length, NSStringEncoding encoding);
//-----------------------------------------------------------------
// interface MultipartMessageHeaderField (private)
//-----------------------------------------------------------------
@interface MultipartMessageHeaderField (private)
-(BOOL) parseHeaderValueBytes:(char*) bytes length:(NSUInteger) length encoding:(NSStringEncoding) encoding;
@end
//-----------------------------------------------------------------
// implementation MultipartMessageHeaderField
//-----------------------------------------------------------------
@implementation MultipartMessageHeaderField
@synthesize name,value,params;
- (id) initWithData:(NSData *)data contentEncoding:(NSStringEncoding)encoding {
params = [[NSMutableDictionary alloc] initWithCapacity:1];
char* bytes = (char*)data.bytes;
NSUInteger length = data.length;
int separatorOffset = findChar(bytes, length, ':');
if( (-1 == separatorOffset) || (separatorOffset >= length-2) ) {
HTTPLogError(@"MultipartFormDataParser: Bad format.No colon in field header.");
// tear down
return nil;
}
// header name is always ascii encoded;
name = [[NSString alloc] initWithBytes: bytes length: separatorOffset encoding: NSASCIIStringEncoding];
if( nil == name ) {
HTTPLogError(@"MultipartFormDataParser: Bad MIME header name.");
// tear down
return nil;
}
// skip the separator and the next ' ' symbol
bytes += separatorOffset + 2;
length -= separatorOffset + 2;
separatorOffset = findChar(bytes, length, ';');
if( separatorOffset == -1 ) {
// couldn't find ';', means we don't have extra params here.
value = [[NSString alloc] initWithBytes:bytes length: length encoding:encoding];
if( nil == value ) {
HTTPLogError(@"MultipartFormDataParser: Bad MIME header value for header name: '%@'",name);
// tear down
return nil;
}
return self;
}
value = [[NSString alloc] initWithBytes:bytes length: separatorOffset encoding:encoding];
HTTPLogVerbose(@"MultipartFormDataParser: Processing header field '%@' : '%@'",name,value);
// skipe the separator and the next ' ' symbol
bytes += separatorOffset + 2;
length -= separatorOffset + 2;
// parse the "params" part of the header
if( ![self parseHeaderValueBytes:bytes length:length encoding:encoding] ) {
NSString* paramsStr = [[NSString alloc] initWithBytes:bytes length:length encoding:NSASCIIStringEncoding];
HTTPLogError(@"MultipartFormDataParser: Bad params for header with name '%@' and value '%@'",name,value);
HTTPLogError(@"MultipartFormDataParser: Params str: %@",paramsStr);
return nil;
}
return self;
}
-(BOOL) parseHeaderValueBytes:(char*) bytes length:(NSUInteger) length encoding:(NSStringEncoding) encoding {
int offset = 0;
NSString* currentParam = nil;
BOOL insideQuote = NO;
while( offset < length ) {
if( bytes[offset] == '\"' ) {
if( !offset || bytes[offset-1] != '\\' ) {
insideQuote = !insideQuote;
}
}
// skip quoted symbols
if( insideQuote ) {
++ offset;
continue;
}
if( bytes[offset] == '=' ) {
if( currentParam ) {
// found '=' before terminating previous param.
return NO;
}
currentParam = [[NSString alloc] initWithBytes:bytes length:offset encoding:NSASCIIStringEncoding];
bytes+=offset + 1;
length -= offset + 1;
offset = 0;
continue;
}
if( bytes[offset] == ';' ) {
if( !currentParam ) {
// found ; before stating '='.
HTTPLogError(@"MultipartFormDataParser: Unexpected ';' when parsing header");
return NO;
}
NSString* paramValue = extractParamValue(bytes, offset,encoding);
if( nil == paramValue ) {
HTTPLogWarn(@"MultipartFormDataParser: Failed to exctract paramValue for key %@ in header %@",currentParam,name);
}
else {
#ifdef DEBUG
if( [params objectForKey:currentParam] ) {
HTTPLogWarn(@"MultipartFormDataParser: param %@ mentioned more then once in header %@",currentParam,name);
}
#endif
[params setObject:paramValue forKey:currentParam];
HTTPLogVerbose(@"MultipartFormDataParser: header param: %@ = %@",currentParam,paramValue);
}
currentParam = nil;
// ';' separator has ' ' following, skip them.
bytes+=offset + 2;
length -= offset + 2;
offset = 0;
}
++ offset;
}
// add last param
if( insideQuote ) {
HTTPLogWarn(@"MultipartFormDataParser: unterminated quote in header %@",name);
// return YES;
}
if( currentParam ) {
NSString* paramValue = extractParamValue(bytes, length, encoding);
if( nil == paramValue ) {
HTTPLogError(@"MultipartFormDataParser: Failed to exctract paramValue for key %@ in header %@",currentParam,name);
}
#ifdef DEBUG
if( [params objectForKey:currentParam] ) {
HTTPLogWarn(@"MultipartFormDataParser: param %@ mentioned more then once in one header",currentParam);
}
#endif
[params setObject:paramValue forKey:currentParam];
HTTPLogVerbose(@"MultipartFormDataParser: header param: %@ = %@",currentParam,paramValue);
currentParam = nil;
}
return YES;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@:%@\n params: %@",name,value,params];
}
@end
int findChar(const char* str, NSUInteger length, char c) {
int offset = 0;
while( offset < length ) {
if( str[offset] == c )
return offset;
++ offset;
}
return -1;
}
NSString* extractParamValue(const char* bytes, NSUInteger length, NSStringEncoding encoding) {
if( !length )
return nil;
NSMutableString* value = nil;
if( bytes[0] == '"' ) {
// values may be quoted. Strip the quotes to get what we need.
value = [[NSMutableString alloc] initWithBytes:bytes + 1 length: length - 2 encoding:encoding];
}
else {
value = [[NSMutableString alloc] initWithBytes:bytes length: length encoding:encoding];
}
// restore escaped symbols
NSRange range= [value rangeOfString:@"\\"];
while ( range.length ) {
[value deleteCharactersInRange:range];
range.location ++;
range = [value rangeOfString:@"\\" options:NSLiteralSearch range: range];
}
return value;
}

View File

@@ -0,0 +1,75 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@class HTTPConnection;
/**
* This is an asynchronous version of HTTPFileResponse.
* It reads data from the given file asynchronously via GCD.
*
* It may be overriden to allow custom post-processing of the data that has been read from the file.
* An example of this is the HTTPDynamicFileResponse class.
**/
@interface HTTPAsyncFileResponse : NSObject <HTTPResponse>
{
HTTPConnection *connection;
NSString *filePath;
UInt64 fileLength;
UInt64 fileOffset; // File offset as pertains to data given to connection
UInt64 readOffset; // File offset as pertains to data read from file (but maybe not returned to connection)
BOOL aborted;
NSData *data;
int fileFD;
void *readBuffer;
NSUInteger readBufferSize; // Malloced size of readBuffer
NSUInteger readBufferOffset; // Offset within readBuffer where the end of existing data is
NSUInteger readRequestLength;
dispatch_queue_t readQueue;
dispatch_source_t readSource;
BOOL readSourceSuspended;
}
- (id)initWithFilePath:(NSString *)filePath forConnection:(HTTPConnection *)connection;
- (NSString *)filePath;
@end
/**
* Explanation of Variables (excluding those that are obvious)
*
* fileOffset
* This is the number of bytes that have been returned to the connection via the readDataOfLength method.
* If 1KB of data has been read from the file, but none of that data has yet been returned to the connection,
* then the fileOffset variable remains at zero.
* This variable is used in the calculation of the isDone method.
* Only after all data has been returned to the connection are we actually done.
*
* readOffset
* Represents the offset of the file descriptor.
* In other words, the file position indidcator for our read stream.
* It might be easy to think of it as the total number of bytes that have been read from the file.
* However, this isn't entirely accurate, as the setOffset: method may have caused us to
* jump ahead in the file (lseek).
*
* readBuffer
* Malloc'd buffer to hold data read from the file.
*
* readBufferSize
* Total allocation size of malloc'd buffer.
*
* readBufferOffset
* Represents the position in the readBuffer where we should store new bytes.
*
* readRequestLength
* The total number of bytes that were requested from the connection.
* It's OK if we return a lesser number of bytes to the connection.
* It's NOT OK if we return a greater number of bytes to the connection.
* Doing so would disrupt proper support for range requests.
* If, however, the response is chunked then we don't need to worry about this.
* Chunked responses inheritly don't support range requests.
**/

View File

@@ -0,0 +1,405 @@
#import "HTTPAsyncFileResponse.h"
#import "HTTPConnection.h"
#import "HTTPLogging.h"
#import <unistd.h>
#import <fcntl.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 NULL_FD -1
/**
* Architecure overview:
*
* HTTPConnection will invoke our readDataOfLength: method to fetch data.
* We will return nil, and then proceed to read the data via our readSource on our readQueue.
* Once the requested amount of data has been read, we then pause our readSource,
* and inform the connection of the available data.
*
* While our read is in progress, we don't have to worry about the connection calling any other methods,
* except the connectionDidClose method, which would be invoked if the remote end closed the socket connection.
* To safely handle this, we do a synchronous dispatch on the readQueue,
* and nilify the connection as well as cancel our readSource.
*
* In order to minimize resource consumption during a HEAD request,
* we don't open the file until we have to (until the connection starts requesting data).
**/
@implementation HTTPAsyncFileResponse
- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent
{
if ((self = [super init]))
{
HTTPLogTrace();
connection = parent; // Parents retain children, children do NOT retain parents
fileFD = NULL_FD;
filePath = [fpath copy];
if (filePath == nil)
{
HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE);
return nil;
}
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL];
if (fileAttributes == nil)
{
HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath);
return nil;
}
fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
fileOffset = 0;
aborted = NO;
// We don't bother opening the file here.
// If this is a HEAD request we only need to know the fileLength.
}
return self;
}
- (void)abort
{
HTTPLogTrace();
[connection responseDidAbort:self];
aborted = YES;
}
- (void)processReadBuffer
{
// This method is here to allow superclasses to perform post-processing of the data.
// For an example, see the HTTPDynamicFileResponse class.
//
// At this point, the readBuffer has readBufferOffset bytes available.
// This method is in charge of updating the readBufferOffset.
// Failure to do so will cause the readBuffer to grow to fileLength. (Imagine a 1 GB file...)
// Copy the data out of the temporary readBuffer.
data = [[NSData alloc] initWithBytes:readBuffer length:readBufferOffset];
// Reset the read buffer.
readBufferOffset = 0;
// Notify the connection that we have data available for it.
[connection responseHasAvailableData:self];
}
- (void)pauseReadSource
{
if (!readSourceSuspended)
{
HTTPLogVerbose(@"%@[%p]: Suspending readSource", THIS_FILE, self);
readSourceSuspended = YES;
dispatch_suspend(readSource);
}
}
- (void)resumeReadSource
{
if (readSourceSuspended)
{
HTTPLogVerbose(@"%@[%p]: Resuming readSource", THIS_FILE, self);
readSourceSuspended = NO;
dispatch_resume(readSource);
}
}
- (void)cancelReadSource
{
HTTPLogVerbose(@"%@[%p]: Canceling readSource", THIS_FILE, self);
dispatch_source_cancel(readSource);
// Cancelling a dispatch source doesn't
// invoke the cancel handler if the dispatch source is paused.
if (readSourceSuspended)
{
readSourceSuspended = NO;
dispatch_resume(readSource);
}
}
- (BOOL)openFileAndSetupReadSource
{
HTTPLogTrace();
fileFD = open([filePath UTF8String], (O_RDONLY | O_NONBLOCK));
if (fileFD == NULL_FD)
{
HTTPLogError(@"%@: Unable to open file. filePath: %@", THIS_FILE, filePath);
return NO;
}
HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath);
readQueue = dispatch_queue_create("HTTPAsyncFileResponse", NULL);
readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fileFD, 0, readQueue);
dispatch_source_set_event_handler(readSource, ^{
HTTPLogTrace2(@"%@: eventBlock - fd[%i]", THIS_FILE, fileFD);
// Determine how much data we should read.
//
// It is OK if we ask to read more bytes than exist in the file.
// It is NOT OK to over-allocate the buffer.
unsigned long long _bytesAvailableOnFD = dispatch_source_get_data(readSource);
UInt64 _bytesLeftInFile = fileLength - readOffset;
NSUInteger bytesAvailableOnFD;
NSUInteger bytesLeftInFile;
bytesAvailableOnFD = (_bytesAvailableOnFD > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesAvailableOnFD;
bytesLeftInFile = (_bytesLeftInFile > NSUIntegerMax) ? NSUIntegerMax : (NSUInteger)_bytesLeftInFile;
NSUInteger bytesLeftInRequest = readRequestLength - readBufferOffset;
NSUInteger bytesLeft = MIN(bytesLeftInRequest, bytesLeftInFile);
NSUInteger bytesToRead = MIN(bytesAvailableOnFD, bytesLeft);
// Make sure buffer is big enough for read request.
// Do not over-allocate.
if (readBuffer == NULL || bytesToRead > (readBufferSize - readBufferOffset))
{
readBufferSize = bytesToRead;
readBuffer = reallocf(readBuffer, (size_t)bytesToRead);
if (readBuffer == NULL)
{
HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self);
[self pauseReadSource];
[self abort];
return;
}
}
// Perform the read
HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead);
ssize_t result = read(fileFD, readBuffer + readBufferOffset, (size_t)bytesToRead);
// Check the results
if (result < 0)
{
HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath);
[self pauseReadSource];
[self abort];
}
else if (result == 0)
{
HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath);
[self pauseReadSource];
[self abort];
}
else // (result > 0)
{
HTTPLogVerbose(@"%@[%p]: Read %lu bytes from file", THIS_FILE, self, (unsigned long)result);
readOffset += result;
readBufferOffset += result;
[self pauseReadSource];
[self processReadBuffer];
}
});
int theFileFD = fileFD;
#if !OS_OBJECT_USE_OBJC
dispatch_source_t theReadSource = readSource;
#endif
dispatch_source_set_cancel_handler(readSource, ^{
// Do not access self from within this block in any way, shape or form.
//
// Note: You access self if you reference an iVar.
HTTPLogTrace2(@"%@: cancelBlock - Close fd[%i]", THIS_FILE, theFileFD);
#if !OS_OBJECT_USE_OBJC
dispatch_release(theReadSource);
#endif
close(theFileFD);
});
readSourceSuspended = YES;
return YES;
}
- (BOOL)openFileIfNeeded
{
if (aborted)
{
// The file operation has been aborted.
// This could be because we failed to open the file,
// or the reading process failed.
return NO;
}
if (fileFD != NULL_FD)
{
// File has already been opened.
return YES;
}
return [self openFileAndSetupReadSource];
}
- (UInt64)contentLength
{
HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, fileLength);
return fileLength;
}
- (UInt64)offset
{
HTTPLogTrace();
return fileOffset;
}
- (void)setOffset:(UInt64)offset
{
HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset);
if (![self openFileIfNeeded])
{
// File opening failed,
// or response has been aborted due to another error.
return;
}
fileOffset = offset;
readOffset = offset;
off_t result = lseek(fileFD, (off_t)offset, SEEK_SET);
if (result == -1)
{
HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath);
[self abort];
}
}
- (NSData *)readDataOfLength:(NSUInteger)length
{
HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length);
if (data)
{
NSUInteger dataLength = [data length];
HTTPLogVerbose(@"%@[%p]: Returning data of length %lu", THIS_FILE, self, (unsigned long)dataLength);
fileOffset += dataLength;
NSData *result = data;
data = nil;
return result;
}
else
{
if (![self openFileIfNeeded])
{
// File opening failed,
// or response has been aborted due to another error.
return nil;
}
dispatch_sync(readQueue, ^{
NSAssert(readSourceSuspended, @"Invalid logic - perhaps HTTPConnection has changed.");
readRequestLength = length;
[self resumeReadSource];
});
return nil;
}
}
- (BOOL)isDone
{
BOOL result = (fileOffset == fileLength);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
- (NSString *)filePath
{
return filePath;
}
- (BOOL)isAsynchronous
{
HTTPLogTrace();
return YES;
}
- (void)connectionDidClose
{
HTTPLogTrace();
if (fileFD != NULL_FD)
{
dispatch_sync(readQueue, ^{
// Prevent any further calls to the connection
connection = nil;
// Cancel the readSource.
// We do this here because the readSource's eventBlock has retained self.
// In other words, if we don't cancel the readSource, we will never get deallocated.
[self cancelReadSource];
});
}
}
- (void)dealloc
{
HTTPLogTrace();
#if !OS_OBJECT_USE_OBJC
if (readQueue) dispatch_release(readQueue);
#endif
if (readBuffer)
free(readBuffer);
}
@end

View File

@@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@interface HTTPDataResponse : NSObject <HTTPResponse>
{
NSUInteger offset;
NSData *data;
}
- (id)initWithData:(NSData *)data;
@end

View File

@@ -0,0 +1,79 @@
#import "HTTPDataResponse.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_OFF; // | HTTP_LOG_FLAG_TRACE;
@implementation HTTPDataResponse
- (id)initWithData:(NSData *)dataParam
{
if((self = [super init]))
{
HTTPLogTrace();
offset = 0;
data = dataParam;
}
return self;
}
- (void)dealloc
{
HTTPLogTrace();
}
- (UInt64)contentLength
{
UInt64 result = (UInt64)[data length];
HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, result);
return result;
}
- (UInt64)offset
{
HTTPLogTrace();
return offset;
}
- (void)setOffset:(UInt64)offsetParam
{
HTTPLogTrace2(@"%@[%p]: setOffset:%lu", THIS_FILE, self, (unsigned long)offset);
offset = (NSUInteger)offsetParam;
}
- (NSData *)readDataOfLength:(NSUInteger)lengthParameter
{
HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)lengthParameter);
NSUInteger remaining = [data length] - offset;
NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining;
void *bytes = (void *)([data bytes] + offset);
offset += length;
return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
}
- (BOOL)isDone
{
BOOL result = (offset == [data length]);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
@end

View File

@@ -0,0 +1,52 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
#import "HTTPAsyncFileResponse.h"
/**
* This class is designed to assist with dynamic content.
* Imagine you have a file that you want to make dynamic:
*
* <html>
* <body>
* <h1>ComputerName Control Panel</h1>
* ...
* <li>System Time: SysTime</li>
* </body>
* </html>
*
* Now you could generate the entire file in Objective-C,
* but this would be a horribly tedious process.
* Beside, you want to design the file with professional tools to make it look pretty.
*
* So all you have to do is escape your dynamic content like this:
*
* ...
* <h1>%%ComputerName%% Control Panel</h1>
* ...
* <li>System Time: %%SysTime%%</li>
*
* And then you create an instance of this class with:
*
* - separator = @"%%"
* - replacementDictionary = { "ComputerName"="Black MacBook", "SysTime"="2010-04-30 03:18:24" }
*
* This class will then perform the replacements for you, on the fly, as it reads the file data.
* This class is also asynchronous, so it will perform the file IO using its own GCD queue.
*
* All keys for the replacementDictionary must be NSString's.
* Values for the replacementDictionary may be NSString's, or any object that
* returns what you want when its description method is invoked.
**/
@interface HTTPDynamicFileResponse : HTTPAsyncFileResponse
{
NSData *separator;
NSDictionary *replacementDict;
}
- (id)initWithFilePath:(NSString *)filePath
forConnection:(HTTPConnection *)connection
separator:(NSString *)separatorStr
replacementDictionary:(NSDictionary *)dictionary;
@end

View File

@@ -0,0 +1,292 @@
#import "HTTPDynamicFileResponse.h"
#import "HTTPConnection.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 NULL_FD -1
@implementation HTTPDynamicFileResponse
- (id)initWithFilePath:(NSString *)fpath
forConnection:(HTTPConnection *)parent
separator:(NSString *)separatorStr
replacementDictionary:(NSDictionary *)dict
{
if ((self = [super initWithFilePath:fpath forConnection:parent]))
{
HTTPLogTrace();
separator = [separatorStr dataUsingEncoding:NSUTF8StringEncoding];
replacementDict = dict;
}
return self;
}
- (BOOL)isChunked
{
HTTPLogTrace();
return YES;
}
- (UInt64)contentLength
{
// This method shouldn't be called since we're using a chunked response.
// We override it just to be safe.
HTTPLogTrace();
return 0;
}
- (void)setOffset:(UInt64)offset
{
// This method shouldn't be called since we're using a chunked response.
// We override it just to be safe.
HTTPLogTrace();
}
- (BOOL)isDone
{
BOOL result = (readOffset == fileLength) && (readBufferOffset == 0);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
- (void)processReadBuffer
{
HTTPLogTrace();
// At this point, the readBuffer has readBufferOffset bytes available.
// This method is in charge of updating the readBufferOffset.
NSUInteger bufLen = readBufferOffset;
NSUInteger sepLen = [separator length];
// We're going to start looking for the separator at the beginning of the buffer,
// and stop when we get to the point where the separator would no longer fit in the buffer.
NSUInteger offset = 0;
NSUInteger stopOffset = (bufLen > sepLen) ? bufLen - sepLen + 1 : 0;
// In order to do the replacement, we need to find the starting and ending separator.
// For example:
//
// %%USER_NAME%%
//
// Where "%%" is the separator.
BOOL found1 = NO;
BOOL found2 = NO;
NSUInteger s1 = 0;
NSUInteger s2 = 0;
const void *sep = [separator bytes];
while (offset < stopOffset)
{
const void *subBuffer = readBuffer + offset;
if (memcmp(subBuffer, sep, sepLen) == 0)
{
if (!found1)
{
// Found the first separator
found1 = YES;
s1 = offset;
offset += sepLen;
HTTPLogVerbose(@"%@[%p]: Found s1 at %lu", THIS_FILE, self, (unsigned long)s1);
}
else
{
// Found the second separator
found2 = YES;
s2 = offset;
offset += sepLen;
HTTPLogVerbose(@"%@[%p]: Found s2 at %lu", THIS_FILE, self, (unsigned long)s2);
}
if (found1 && found2)
{
// We found our separators.
// Now extract the string between the two separators.
NSRange fullRange = NSMakeRange(s1, (s2 - s1 + sepLen));
NSRange strRange = NSMakeRange(s1 + sepLen, (s2 - s1 - sepLen));
// Wish we could use the simple subdataWithRange method.
// But that method copies the bytes...
// So for performance reasons, we need to use the methods that don't copy the bytes.
void *strBuf = readBuffer + strRange.location;
NSUInteger strLen = strRange.length;
NSString *key = [[NSString alloc] initWithBytes:strBuf length:strLen encoding:NSUTF8StringEncoding];
if (key)
{
// Is there a given replacement for this key?
id value = [replacementDict objectForKey:key];
if (value)
{
// Found the replacement value.
// Now perform the replacement in the buffer.
HTTPLogVerbose(@"%@[%p]: key(%@) -> value(%@)", THIS_FILE, self, key, value);
NSData *v = [[value description] dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger vLength = [v length];
if (fullRange.length == vLength)
{
// Replacement is exactly the same size as what it is replacing
// memcpy(void *restrict dst, const void *restrict src, size_t n);
memcpy(readBuffer + fullRange.location, [v bytes], vLength);
}
else // (fullRange.length != vLength)
{
NSInteger diff = (NSInteger)vLength - (NSInteger)fullRange.length;
if (diff > 0)
{
// Replacement is bigger than what it is replacing.
// Make sure there is room in the buffer for the replacement.
if (diff > (readBufferSize - bufLen))
{
NSUInteger inc = MAX(diff, 256);
readBufferSize += inc;
readBuffer = reallocf(readBuffer, readBufferSize);
}
}
// Move the data that comes after the replacement.
//
// If replacement is smaller than what it is replacing,
// then we are shifting the data toward the beginning of the buffer.
//
// If replacement is bigger than what it is replacing,
// then we are shifting the data toward the end of the buffer.
//
// memmove(void *dst, const void *src, size_t n);
//
// The memmove() function copies n bytes from src to dst.
// The two areas may overlap; the copy is always done in a non-destructive manner.
void *src = readBuffer + fullRange.location + fullRange.length;
void *dst = readBuffer + fullRange.location + vLength;
NSUInteger remaining = bufLen - (fullRange.location + fullRange.length);
memmove(dst, src, remaining);
// Now copy the replacement into its location.
//
// memcpy(void *restrict dst, const void *restrict src, size_t n)
//
// The memcpy() function copies n bytes from src to dst.
// If the two areas overlap, behavior is undefined.
memcpy(readBuffer + fullRange.location, [v bytes], vLength);
// And don't forget to update our indices.
bufLen += diff;
offset += diff;
stopOffset += diff;
}
}
}
found1 = found2 = NO;
}
}
else
{
offset++;
}
}
// We've gone through our buffer now, and performed all the replacements that we could.
// It's now time to update the amount of available data we have.
if (readOffset == fileLength)
{
// We've read in the entire file.
// So there can be no more replacements.
data = [[NSData alloc] initWithBytes:readBuffer length:bufLen];
readBufferOffset = 0;
}
else
{
// There are a couple different situations that we need to take into account here.
//
// Imagine the following file:
// My name is %%USER_NAME%%
//
// Situation 1:
// The first chunk of data we read was "My name is %%".
// So we found the first separator, but not the second.
// In this case we can only return the data that precedes the first separator.
//
// Situation 2:
// The first chunk of data we read was "My name is %".
// So we didn't find any separators, but part of a separator may be included in our buffer.
NSUInteger available;
if (found1)
{
// Situation 1
available = s1;
}
else
{
// Situation 2
available = stopOffset;
}
// Copy available data
data = [[NSData alloc] initWithBytes:readBuffer length:available];
// Remove the copied data from the buffer.
// We do this by shifting the remaining data toward the beginning of the buffer.
NSUInteger remaining = bufLen - available;
memmove(readBuffer, readBuffer + available, remaining);
readBufferOffset = remaining;
}
[connection responseHasAvailableData:self];
}
- (void)dealloc
{
HTTPLogTrace();
}
@end

View File

@@ -0,0 +1,9 @@
#import "HTTPResponse.h"
@interface HTTPErrorResponse : NSObject <HTTPResponse> {
NSInteger _status;
}
- (id)initWithErrorCode:(int)httpErrorCode;
@end

View File

@@ -0,0 +1,38 @@
#import "HTTPErrorResponse.h"
@implementation HTTPErrorResponse
-(id)initWithErrorCode:(int)httpErrorCode
{
if ((self = [super init]))
{
_status = httpErrorCode;
}
return self;
}
- (UInt64) contentLength {
return 0;
}
- (UInt64) offset {
return 0;
}
- (void)setOffset:(UInt64)offset {
;
}
- (NSData*) readDataOfLength:(NSUInteger)length {
return nil;
}
- (BOOL) isDone {
return YES;
}
- (NSInteger) status {
return _status;
}
@end

View File

@@ -0,0 +1,25 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@class HTTPConnection;
@interface HTTPFileResponse : NSObject <HTTPResponse>
{
HTTPConnection *connection;
NSString *filePath;
UInt64 fileLength;
UInt64 fileOffset;
BOOL aborted;
int fileFD;
void *buffer;
NSUInteger bufferSize;
}
- (id)initWithFilePath:(NSString *)filePath forConnection:(HTTPConnection *)connection;
- (NSString *)filePath;
@end

View File

@@ -0,0 +1,237 @@
#import "HTTPFileResponse.h"
#import "HTTPConnection.h"
#import "HTTPLogging.h"
#import <unistd.h>
#import <fcntl.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 NULL_FD -1
@implementation HTTPFileResponse
- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent
{
if((self = [super init]))
{
HTTPLogTrace();
connection = parent; // Parents retain children, children do NOT retain parents
fileFD = NULL_FD;
filePath = [[fpath copy] stringByResolvingSymlinksInPath];
if (filePath == nil)
{
HTTPLogWarn(@"%@: Init failed - Nil filePath", THIS_FILE);
return nil;
}
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
if (fileAttributes == nil)
{
HTTPLogWarn(@"%@: Init failed - Unable to get file attributes. filePath: %@", THIS_FILE, filePath);
return nil;
}
fileLength = (UInt64)[[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
fileOffset = 0;
aborted = NO;
// We don't bother opening the file here.
// If this is a HEAD request we only need to know the fileLength.
}
return self;
}
- (void)abort
{
HTTPLogTrace();
[connection responseDidAbort:self];
aborted = YES;
}
- (BOOL)openFile
{
HTTPLogTrace();
fileFD = open([filePath UTF8String], O_RDONLY);
if (fileFD == NULL_FD)
{
HTTPLogError(@"%@[%p]: Unable to open file. filePath: %@", THIS_FILE, self, filePath);
[self abort];
return NO;
}
HTTPLogVerbose(@"%@[%p]: Open fd[%i] -> %@", THIS_FILE, self, fileFD, filePath);
return YES;
}
- (BOOL)openFileIfNeeded
{
if (aborted)
{
// The file operation has been aborted.
// This could be because we failed to open the file,
// or the reading process failed.
return NO;
}
if (fileFD != NULL_FD)
{
// File has already been opened.
return YES;
}
return [self openFile];
}
- (UInt64)contentLength
{
HTTPLogTrace();
return fileLength;
}
- (UInt64)offset
{
HTTPLogTrace();
return fileOffset;
}
- (void)setOffset:(UInt64)offset
{
HTTPLogTrace2(@"%@[%p]: setOffset:%llu", THIS_FILE, self, offset);
if (![self openFileIfNeeded])
{
// File opening failed,
// or response has been aborted due to another error.
return;
}
fileOffset = offset;
off_t result = lseek(fileFD, (off_t)offset, SEEK_SET);
if (result == -1)
{
HTTPLogError(@"%@[%p]: lseek failed - errno(%i) filePath(%@)", THIS_FILE, self, errno, filePath);
[self abort];
}
}
- (NSData *)readDataOfLength:(NSUInteger)length
{
HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)length);
if (![self openFileIfNeeded])
{
// File opening failed,
// or response has been aborted due to another error.
return nil;
}
// Determine how much data we should read.
//
// It is OK if we ask to read more bytes than exist in the file.
// It is NOT OK to over-allocate the buffer.
UInt64 bytesLeftInFile = fileLength - fileOffset;
NSUInteger bytesToRead = (NSUInteger)MIN(length, bytesLeftInFile);
// Make sure buffer is big enough for read request.
// Do not over-allocate.
if (buffer == NULL || bufferSize < bytesToRead)
{
bufferSize = bytesToRead;
buffer = reallocf(buffer, (size_t)bufferSize);
if (buffer == NULL)
{
HTTPLogError(@"%@[%p]: Unable to allocate buffer", THIS_FILE, self);
[self abort];
return nil;
}
}
// Perform the read
HTTPLogVerbose(@"%@[%p]: Attempting to read %lu bytes from file", THIS_FILE, self, (unsigned long)bytesToRead);
ssize_t result = read(fileFD, buffer, bytesToRead);
// Check the results
if (result < 0)
{
HTTPLogError(@"%@: Error(%i) reading file(%@)", THIS_FILE, errno, filePath);
[self abort];
return nil;
}
else if (result == 0)
{
HTTPLogError(@"%@: Read EOF on file(%@)", THIS_FILE, filePath);
[self abort];
return nil;
}
else // (result > 0)
{
HTTPLogVerbose(@"%@[%p]: Read %ld bytes from file", THIS_FILE, self, (long)result);
fileOffset += result;
return [NSData dataWithBytes:buffer length:result];
}
}
- (BOOL)isDone
{
BOOL result = (fileOffset == fileLength);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
- (NSString *)filePath
{
return filePath;
}
- (void)dealloc
{
HTTPLogTrace();
if (fileFD != NULL_FD)
{
HTTPLogVerbose(@"%@[%p]: Close fd[%i]", THIS_FILE, self, fileFD);
close(fileFD);
}
if (buffer)
free(buffer);
}
@end

View File

@@ -0,0 +1,12 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@interface HTTPRedirectResponse : NSObject <HTTPResponse>
{
NSString *redirectPath;
}
- (id)initWithPath:(NSString *)redirectPath;
@end

View File

@@ -0,0 +1,73 @@
#import "HTTPRedirectResponse.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_OFF; // | HTTP_LOG_FLAG_TRACE;
@implementation HTTPRedirectResponse
- (id)initWithPath:(NSString *)path
{
if ((self = [super init]))
{
HTTPLogTrace();
redirectPath = [path copy];
}
return self;
}
- (UInt64)contentLength
{
return 0;
}
- (UInt64)offset
{
return 0;
}
- (void)setOffset:(UInt64)offset
{
// Nothing to do
}
- (NSData *)readDataOfLength:(NSUInteger)length
{
HTTPLogTrace();
return nil;
}
- (BOOL)isDone
{
return YES;
}
- (NSDictionary *)httpHeaders
{
HTTPLogTrace();
return [NSDictionary dictionaryWithObject:redirectPath forKey:@"Location"];
}
- (NSInteger)status
{
HTTPLogTrace();
return 302;
}
- (void)dealloc
{
HTTPLogTrace();
}
@end

105
msext/Class/http/Core/WebSocket.h Executable file
View File

@@ -0,0 +1,105 @@
#import <Foundation/Foundation.h>
@class HTTPMessage;
@class GCDAsyncSocket;
#define WebSocketDidDieNotification @"WebSocketDidDie"
@interface WebSocket : NSObject
{
dispatch_queue_t websocketQueue;
HTTPMessage *request;
GCDAsyncSocket *asyncSocket;
NSData *term;
BOOL isStarted;
BOOL isOpen;
BOOL isVersion76;
id __unsafe_unretained delegate;
}
+ (BOOL)isWebSocketRequest:(HTTPMessage *)request;
- (id)initWithRequest:(HTTPMessage *)request socket:(GCDAsyncSocket *)socket;
/**
* Delegate option.
*
* In most cases it will be easier to subclass WebSocket,
* but some circumstances may lead one to prefer standard delegate callbacks instead.
**/
@property (/* atomic */ unsafe_unretained) id delegate;
/**
* The WebSocket class is thread-safe, generally via it's GCD queue.
* All public API methods are thread-safe,
* and the subclass API methods are thread-safe as they are all invoked on the same GCD queue.
**/
@property (nonatomic, readonly) dispatch_queue_t websocketQueue;
/**
* Public API
*
* These methods are automatically called by the HTTPServer.
* You may invoke the stop method yourself to close the WebSocket manually.
**/
- (void)start;
- (void)stop;
/**
* Public API
*
* Sends a message over the WebSocket.
* This method is thread-safe.
**/
- (void)sendMessage:(NSString *)msg;
/**
* Public API
*
* Sends a message over the WebSocket.
* This method is thread-safe.
**/
- (void)sendData:(NSData *)msg;
/**
* Subclass API
*
* These methods are designed to be overriden by subclasses.
**/
- (void)didOpen;
- (void)didReceiveMessage:(NSString *)msg;
- (void)didClose;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* There are two ways to create your own custom WebSocket:
*
* - Subclass it and override the methods you're interested in.
* - Use traditional delegate paradigm along with your own custom class.
*
* They both exist to allow for maximum flexibility.
* In most cases it will be easier to subclass WebSocket.
* However some circumstances may lead one to prefer standard delegate callbacks instead.
* One such example, you're already subclassing another class, so subclassing WebSocket isn't an option.
**/
@protocol WebSocketDelegate
@optional
- (void)webSocketDidOpen:(WebSocket *)ws;
- (void)webSocket:(WebSocket *)ws didReceiveMessage:(NSString *)msg;
- (void)webSocketDidClose:(WebSocket *)ws;
@end

791
msext/Class/http/Core/WebSocket.m Executable file
View File

@@ -0,0 +1,791 @@
#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