Skip to content
This repository has been archived by the owner on Jul 19, 2018. It is now read-only.

Commit

Permalink
Adding support for asynchronous http headers. (Issue robbiehanson#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
Robbie Hanson committed Dec 9, 2010
1 parent 647715a commit 09b7b2a
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 165 deletions.
2 changes: 2 additions & 0 deletions HTTPConnection.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
HTTPMessage *request;
unsigned int numHeaderLines;

BOOL sentResponseHeaders;

NSString *nonce;
long lastNC;

Expand Down
292 changes: 157 additions & 135 deletions HTTPConnection.m
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@

@interface HTTPConnection (PrivateAPI)
- (void)startReadingRequest;
- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength;
- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength;
- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length;
- (NSData *)chunkedTransferFooter;
- (void)sendResponseHeadersAndBody;
@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -942,11 +939,6 @@ - (void)replyToHTTPRequest
return;
}

// Extract the method
NSString *method = [request method];

// Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag:

// Check Authentication (if needed)
// If not properly authenticated for resource, issue Unauthorized response
if ([self isPasswordProtected:uri] && ![self isAuthenticated])
Expand All @@ -955,6 +947,11 @@ - (void)replyToHTTPRequest
return;
}

// Extract the method
NSString *method = [request method];

// Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag:

// Respond properly to HTTP 'GET' and 'HEAD' commands
httpResponse = [[self httpResponseForMethod:method URI:uri] retain];

Expand All @@ -964,6 +961,139 @@ - (void)replyToHTTPRequest
return;
}

[self sendResponseHeadersAndBody];
}

/**
* Prepares a single-range response.
*
* Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it.
**/
- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength
{
HTTPLogTrace();

// Status Code 206 - Partial Content
HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1];

DDRange range = [[ranges objectAtIndex:0] ddrangeValue];

NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", range.length];
[response setHeaderField:@"Content-Length" value:contentLengthStr];

NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1];
NSString *contentRangeStr = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength];
[response setHeaderField:@"Content-Range" value:contentRangeStr];

return response;
}

/**
* Prepares a multi-range response.
*
* Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it.
**/
- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength
{
HTTPLogTrace();

// Status Code 206 - Partial Content
HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1];

// We have to send each range using multipart/byteranges
// So each byterange has to be prefix'd and suffix'd with the boundry
// Example:
//
// HTTP/1.1 206 Partial Content
// Content-Length: 220
// Content-Type: multipart/byteranges; boundary=4554d24e986f76dd6
//
//
// --4554d24e986f76dd6
// Content-Range: bytes 0-25/4025
//
// [...]
// --4554d24e986f76dd6
// Content-Range: bytes 3975-4024/4025
//
// [...]
// --4554d24e986f76dd6--

ranges_headers = [[NSMutableArray alloc] initWithCapacity:[ranges count]];

CFUUIDRef theUUID = CFUUIDCreate(NULL);
ranges_boundry = NSMakeCollectable(CFUUIDCreateString(NULL, theUUID));
CFRelease(theUUID);

NSString *startingBoundryStr = [NSString stringWithFormat:@"\r\n--%@\r\n", ranges_boundry];
NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry];

UInt64 actualContentLength = 0;

NSUInteger i;
for (i = 0; i < [ranges count]; i++)
{
DDRange range = [[ranges objectAtIndex:i] ddrangeValue];

NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1];
NSString *contentRangeVal = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength];
NSString *contentRangeStr = [NSString stringWithFormat:@"Content-Range: %@\r\n\r\n", contentRangeVal];

NSString *fullHeader = [startingBoundryStr stringByAppendingString:contentRangeStr];
NSData *fullHeaderData = [fullHeader dataUsingEncoding:NSUTF8StringEncoding];

[ranges_headers addObject:fullHeaderData];

actualContentLength += [fullHeaderData length];
actualContentLength += range.length;
}

NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding];

actualContentLength += [endingBoundryData length];

NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", actualContentLength];
[response setHeaderField:@"Content-Length" value:contentLengthStr];

NSString *contentTypeStr = [NSString stringWithFormat:@"multipart/byteranges; boundary=%@", ranges_boundry];
[response setHeaderField:@"Content-Type" value:contentTypeStr];

return response;
}

/**
* Returns the chunk size line that must precede each chunk of data when using chunked transfer encoding.
* This consists of the size of the data, in hexadecimal, followed by a CRLF.
**/
- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length
{
return [[NSString stringWithFormat:@"%lx\r\n", (unsigned long)length] dataUsingEncoding:NSUTF8StringEncoding];
}

/**
* Returns the data that signals the end of a chunked transfer.
**/
- (NSData *)chunkedTransferFooter
{
// Each data chunk is preceded by a size line (in hex and including a CRLF),
// followed by the data itself, followed by another CRLF.
// After every data chunk has been sent, a zero size line is sent,
// followed by optional footer (which are just more headers),
// and followed by a CRLF on a line by itself.

return [@"\r\n0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
}

- (void)sendResponseHeadersAndBody
{
if ([httpResponse respondsToSelector:@selector(delayResponeHeaders)])
{
if ([httpResponse delayResponeHeaders])
{
return;
}
}

BOOL isChunked = NO;

if ([httpResponse respondsToSelector:@selector(isChunked)])
Expand Down Expand Up @@ -1039,17 +1169,21 @@ - (void)replyToHTTPRequest
// If they issue a 'HEAD' command, we don't have to include the file
// If they issue a 'GET' command, we need to include the file

if ([method isEqual:@"HEAD"] || isZeroLengthResponse)
if ([[request method] isEqualToString:@"HEAD"] || isZeroLengthResponse)
{
NSData *responseData = [self preprocessResponse:response];
[asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE];

sentResponseHeaders = YES;
}
else
{
// Write the header response
NSData *responseData = [self preprocessResponse:response];
[asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER];

sentResponseHeaders = YES;

// Now we need to send the body of the response
if (!isRangeRequest)
{
Expand Down Expand Up @@ -1139,126 +1273,6 @@ - (void)replyToHTTPRequest
[response release];
}

/**
* Prepares a single-range response.
*
* Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it.
**/
- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength
{
HTTPLogTrace();

// Status Code 206 - Partial Content
HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1];

DDRange range = [[ranges objectAtIndex:0] ddrangeValue];

NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", range.length];
[response setHeaderField:@"Content-Length" value:contentLengthStr];

NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1];
NSString *contentRangeStr = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength];
[response setHeaderField:@"Content-Range" value:contentRangeStr];

return response;
}

/**
* Prepares a multi-range response.
*
* Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it.
**/
- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength
{
HTTPLogTrace();

// Status Code 206 - Partial Content
HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1];

// We have to send each range using multipart/byteranges
// So each byterange has to be prefix'd and suffix'd with the boundry
// Example:
//
// HTTP/1.1 206 Partial Content
// Content-Length: 220
// Content-Type: multipart/byteranges; boundary=4554d24e986f76dd6
//
//
// --4554d24e986f76dd6
// Content-Range: bytes 0-25/4025
//
// [...]
// --4554d24e986f76dd6
// Content-Range: bytes 3975-4024/4025
//
// [...]
// --4554d24e986f76dd6--

ranges_headers = [[NSMutableArray alloc] initWithCapacity:[ranges count]];

CFUUIDRef theUUID = CFUUIDCreate(NULL);
ranges_boundry = NSMakeCollectable(CFUUIDCreateString(NULL, theUUID));
CFRelease(theUUID);

NSString *startingBoundryStr = [NSString stringWithFormat:@"\r\n--%@\r\n", ranges_boundry];
NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry];

UInt64 actualContentLength = 0;

NSUInteger i;
for (i = 0; i < [ranges count]; i++)
{
DDRange range = [[ranges objectAtIndex:i] ddrangeValue];

NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1];
NSString *contentRangeVal = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength];
NSString *contentRangeStr = [NSString stringWithFormat:@"Content-Range: %@\r\n\r\n", contentRangeVal];

NSString *fullHeader = [startingBoundryStr stringByAppendingString:contentRangeStr];
NSData *fullHeaderData = [fullHeader dataUsingEncoding:NSUTF8StringEncoding];

[ranges_headers addObject:fullHeaderData];

actualContentLength += [fullHeaderData length];
actualContentLength += range.length;
}

NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding];

actualContentLength += [endingBoundryData length];

NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", actualContentLength];
[response setHeaderField:@"Content-Length" value:contentLengthStr];

NSString *contentTypeStr = [NSString stringWithFormat:@"multipart/byteranges; boundary=%@", ranges_boundry];
[response setHeaderField:@"Content-Type" value:contentTypeStr];

return response;
}

/**
* Returns the chunk size line that must precede each chunk of data when using chunked transfer encoding.
* This consists of the size of the data, in hexadecimal, followed by a CRLF.
**/
- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length
{
return [[NSString stringWithFormat:@"%lx\r\n", (unsigned long)length] dataUsingEncoding:NSUTF8StringEncoding];
}

/**
* Returns the data that signals the end of a chunked transfer.
**/
- (NSData *)chunkedTransferFooter
{
// Each data chunk is preceded by a size line (in hex and including a CRLF),
// followed by the data itself, followed by another CRLF.
// After every data chunk has been sent, a zero size line is sent,
// followed by optional footer (which are just more headers),
// and followed by a CRLF on a line by itself.

return [@"\r\n0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding];
}

/**
* Returns the number of bytes of the http response body that are sitting in asyncSocket's write queue.
*
Expand Down Expand Up @@ -2198,6 +2212,7 @@ - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
request = [[HTTPMessage alloc] initEmptyRequest];

numHeaderLines = 0;
sentResponseHeaders = NO;

// And start listening for more requests
[self startReadingRequest];
Expand Down Expand Up @@ -2246,16 +2261,23 @@ - (void)responseHasAvailableData:(NSObject<HTTPResponse> *)sender

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

if (ranges == nil)
if (!sentResponseHeaders)
{
[self continueSendingStandardResponseBody];
[self sendResponseHeadersAndBody];
}
else
{
if ([ranges count] == 1)
[self continueSendingSingleRangeResponseBody];
if (ranges == nil)
{
[self continueSendingStandardResponseBody];
}
else
[self continueSendingMultiRangeResponseBody];
{
if ([ranges count] == 1)
[self continueSendingSingleRangeResponseBody];
else
[self continueSendingMultiRangeResponseBody];
}
}

[pool release];
Expand Down
Loading

0 comments on commit 09b7b2a

Please sign in to comment.