add .gitignore
This commit is contained in:
14
msext/Class/http/Core/Categories/DDData.h
Executable file
14
msext/Class/http/Core/Categories/DDData.h
Executable file
@@ -0,0 +1,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSData (DDData)
|
||||
|
||||
- (NSData *)md5Digest;
|
||||
|
||||
- (NSData *)sha1Digest;
|
||||
|
||||
- (NSString *)hexStringValue;
|
||||
|
||||
- (NSString *)base64Encoded;
|
||||
- (NSData *)base64Decoded;
|
||||
|
||||
@end
|
||||
158
msext/Class/http/Core/Categories/DDData.m
Executable file
158
msext/Class/http/Core/Categories/DDData.m
Executable 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
|
||||
12
msext/Class/http/Core/Categories/DDNumber.h
Executable file
12
msext/Class/http/Core/Categories/DDNumber.h
Executable 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
|
||||
88
msext/Class/http/Core/Categories/DDNumber.m
Executable file
88
msext/Class/http/Core/Categories/DDNumber.m
Executable 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
|
||||
56
msext/Class/http/Core/Categories/DDRange.h
Executable file
56
msext/Class/http/Core/Categories/DDRange.h
Executable 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
|
||||
104
msext/Class/http/Core/Categories/DDRange.m
Executable file
104
msext/Class/http/Core/Categories/DDRange.m
Executable 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
|
||||
45
msext/Class/http/Core/HTTPAuthenticationRequest.h
Executable file
45
msext/Class/http/Core/HTTPAuthenticationRequest.h
Executable 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
|
||||
195
msext/Class/http/Core/HTTPAuthenticationRequest.m
Executable file
195
msext/Class/http/Core/HTTPAuthenticationRequest.m
Executable 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
|
||||
119
msext/Class/http/Core/HTTPConnection.h
Executable file
119
msext/Class/http/Core/HTTPConnection.h
Executable 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
|
||||
2708
msext/Class/http/Core/HTTPConnection.m
Executable file
2708
msext/Class/http/Core/HTTPConnection.m
Executable file
File diff suppressed because it is too large
Load Diff
136
msext/Class/http/Core/HTTPLogging.h
Executable file
136
msext/Class/http/Core/HTTPLogging.h
Executable 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__)
|
||||
|
||||
48
msext/Class/http/Core/HTTPMessage.h
Executable file
48
msext/Class/http/Core/HTTPMessage.h
Executable 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
|
||||
113
msext/Class/http/Core/HTTPMessage.m
Executable file
113
msext/Class/http/Core/HTTPMessage.m
Executable 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
|
||||
149
msext/Class/http/Core/HTTPResponse.h
Executable file
149
msext/Class/http/Core/HTTPResponse.h
Executable 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.
|
||||
**/
|
||||
205
msext/Class/http/Core/HTTPServer.h
Executable file
205
msext/Class/http/Core/HTTPServer.h
Executable 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
|
||||
772
msext/Class/http/Core/HTTPServer.m
Executable file
772
msext/Class/http/Core/HTTPServer.m
Executable 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
|
||||
65
msext/Class/http/Core/Mime/MultipartFormDataParser.h
Executable file
65
msext/Class/http/Core/Mime/MultipartFormDataParser.h
Executable 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
|
||||
529
msext/Class/http/Core/Mime/MultipartFormDataParser.m
Executable file
529
msext/Class/http/Core/Mime/MultipartFormDataParser.m
Executable 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
|
||||
33
msext/Class/http/Core/Mime/MultipartMessageHeader.h
Executable file
33
msext/Class/http/Core/Mime/MultipartMessageHeader.h
Executable 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
|
||||
86
msext/Class/http/Core/Mime/MultipartMessageHeader.m
Executable file
86
msext/Class/http/Core/Mime/MultipartMessageHeader.m
Executable 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
|
||||
23
msext/Class/http/Core/Mime/MultipartMessageHeaderField.h
Executable file
23
msext/Class/http/Core/Mime/MultipartMessageHeaderField.h
Executable 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
|
||||
211
msext/Class/http/Core/Mime/MultipartMessageHeaderField.m
Executable file
211
msext/Class/http/Core/Mime/MultipartMessageHeaderField.m
Executable 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;
|
||||
}
|
||||
|
||||
75
msext/Class/http/Core/Responses/HTTPAsyncFileResponse.h
Executable file
75
msext/Class/http/Core/Responses/HTTPAsyncFileResponse.h
Executable 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.
|
||||
**/
|
||||
405
msext/Class/http/Core/Responses/HTTPAsyncFileResponse.m
Executable file
405
msext/Class/http/Core/Responses/HTTPAsyncFileResponse.m
Executable 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
|
||||
13
msext/Class/http/Core/Responses/HTTPDataResponse.h
Executable file
13
msext/Class/http/Core/Responses/HTTPDataResponse.h
Executable 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
|
||||
79
msext/Class/http/Core/Responses/HTTPDataResponse.m
Executable file
79
msext/Class/http/Core/Responses/HTTPDataResponse.m
Executable 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
|
||||
52
msext/Class/http/Core/Responses/HTTPDynamicFileResponse.h
Executable file
52
msext/Class/http/Core/Responses/HTTPDynamicFileResponse.h
Executable 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
|
||||
292
msext/Class/http/Core/Responses/HTTPDynamicFileResponse.m
Executable file
292
msext/Class/http/Core/Responses/HTTPDynamicFileResponse.m
Executable 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
|
||||
9
msext/Class/http/Core/Responses/HTTPErrorResponse.h
Executable file
9
msext/Class/http/Core/Responses/HTTPErrorResponse.h
Executable file
@@ -0,0 +1,9 @@
|
||||
#import "HTTPResponse.h"
|
||||
|
||||
@interface HTTPErrorResponse : NSObject <HTTPResponse> {
|
||||
NSInteger _status;
|
||||
}
|
||||
|
||||
- (id)initWithErrorCode:(int)httpErrorCode;
|
||||
|
||||
@end
|
||||
38
msext/Class/http/Core/Responses/HTTPErrorResponse.m
Executable file
38
msext/Class/http/Core/Responses/HTTPErrorResponse.m
Executable 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
|
||||
25
msext/Class/http/Core/Responses/HTTPFileResponse.h
Executable file
25
msext/Class/http/Core/Responses/HTTPFileResponse.h
Executable 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
|
||||
237
msext/Class/http/Core/Responses/HTTPFileResponse.m
Executable file
237
msext/Class/http/Core/Responses/HTTPFileResponse.m
Executable 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
|
||||
12
msext/Class/http/Core/Responses/HTTPRedirectResponse.h
Executable file
12
msext/Class/http/Core/Responses/HTTPRedirectResponse.h
Executable file
@@ -0,0 +1,12 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HTTPResponse.h"
|
||||
|
||||
|
||||
@interface HTTPRedirectResponse : NSObject <HTTPResponse>
|
||||
{
|
||||
NSString *redirectPath;
|
||||
}
|
||||
|
||||
- (id)initWithPath:(NSString *)redirectPath;
|
||||
|
||||
@end
|
||||
73
msext/Class/http/Core/Responses/HTTPRedirectResponse.m
Executable file
73
msext/Class/http/Core/Responses/HTTPRedirectResponse.m
Executable 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
105
msext/Class/http/Core/WebSocket.h
Executable 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
791
msext/Class/http/Core/WebSocket.m
Executable 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
|
||||
Reference in New Issue
Block a user