robbiehanson / CocoaHTTPServer

A small, lightweight, embeddable HTTP server for Mac OS X or iOS applications
Other
5.6k stars 1.31k forks source link

HTTPProxyResponse #70

Open SamQuest opened 11 years ago

SamQuest commented 11 years ago

Hello Robbiehanson,

To start, I am really really new to IOS / Objective C.

I am given a project which already uses your server. The enhancement needed to decrypt a progressive download video. In my research a couple of people have asked you and implemented this, but I could not find any code.

So I went on and mucked HTTPAsyncFileResponse myself. Now while waiting for my hairs to grow back, the code works fine. At this state (my knowledge about the language and framework is almost nil) I am satisfied with this code.

This code is for anyone who don't want to start clean. and over the top of your head if anyone see any big issues/leaks please comment.

HTTPConnection.m

- (void)sendResponseHeadersAndBody
{
...
        if ([ranges count] == 1)
        {
            if(remote){
            HTTPProxyResponse * proxyResponse = (HTTPProxyResponse *)httpResponse;
            response = [proxyResponse remoteResponse];
            } 
            else 
            {
            response = [self newUniRangeResponse:contentLength];
            }
        }
        else
        {
            response = [self newMultiRangeResponse:contentLength];
        }
...
}

HTTPProxyResponse.h

#import <Foundation/Foundation.h>
#import "HTTPConnection.h"
#import "HTTPResponse.h"
#import "HTTPMessage.h"
#import "GCDAsyncSocket.h"

@class HTTPConnection;
@class GCDAsyncSocket;

@interface HTTPProxyResponse : NSObject <HTTPResponse>
{
    dispatch_queue_t socketQueue;
    GCDAsyncSocket *asyncSocket;

    HTTPMessage *localRequest;
    HTTPMessage __strong *remoteResponse;
    unsigned int numHeaderLines;

    HTTPConnection *connection;

    UInt64 fileLength;  // Actual lwngth of the file
    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 connected;
    BOOL aborted;

    NSMutableData __strong *buffer;
    NSData __strong *data;

    void *readBuffer;
    NSUInteger readBufferSize;     // Malloced size of readBuffer
    NSUInteger readBufferOffset;   // Offset within readBuffer where the end of existing data is
    NSUInteger readRequestLength;
}

@property (nonatomic, strong) HTTPMessage *remoteResponse;

- (id)initWithLocalRequest:(HTTPMessage *)request forConnection:(HTTPConnection *)connection;

@end

HTTPProxyResponse.m


#import "HTTPProxyResponse.h"
#import "MyAppDelegate.h"
#import "HTTPLogging.h"

#import <unistd.h>
#import <fcntl.h>

#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonDigest.h>

// Log levels : off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_FLAG_TRACE;

// Define chunk size used to read in data for responses
// This is how much data will be read from disk into RAM at a time
#if TARGET_OS_IPHONE
    #define READ_CHUNKSIZE  (1024 * 128)
#else
    #define READ_CHUNKSIZE  (1024 * 512)
#endif

// Define the various timeouts (in seconds) for various parts of the HTTP process
#define TIMEOUT_READ_FIRST_HEADER_LINE       30
#define TIMEOUT_READ_SUBSEQUENT_HEADER_LINE  30
#define TIMEOUT_READ_BODY                    -1
#define TIMEOUT_WRITE_HEAD                   30
#define TIMEOUT_WRITE_ERROR                  30

// Define the various limits
// LIMIT_MAX_HEADER_LINE_LENGTH: Max length (in bytes) of any single line in a header (including \r\n)
// LIMIT_MAX_HEADER_LINES      : Max number of lines in a single header (including first GET line)
#define LIMIT_MAX_HEADER_LINE_LENGTH  8190
#define LIMIT_MAX_HEADER_LINES         100

// Define the various tags we'll use to differentiate what it is we're currently doing
#define HTTP_REQUEST_HEADER                10

#define HTTP_RESPONSE_HEADER                 92
#define HTTP_RESPONSE_BODY                   93
#define HTTP_FINAL_RESPONSE                  91

@implementation HTTPProxyResponse

@synthesize remoteResponse;

- (NSURL *)remoteRequestURI
{
    return [[NSURL alloc] initWithString:@"http://www.example.com/path/to/my/video.mp4"];
}

- (id)initWithLocalRequest:(HTTPMessage *)request forConnection:(HTTPConnection *)parent
{
    if ((self = [super init]))
    {
        HTTPLogTrace();

        connection = parent; // Parents retain children, children do NOT retain parents
        localRequest = request;

        NSURL *remoteURI = [self remoteRequestURI];

        MyAppDelegate * appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
        fileLength = [appDelegate currentVideoLength];

        fileOffset = 0;

        socketQueue = dispatch_queue_create("AsyncSocket", NULL);
        asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:socketQueue];

        uint16_t port = [[remoteURI port] unsignedLongLongValue];
        if(port==0) port = 80;

        NSError *error = nil;
        bool res = [asyncSocket connectToHost:[remoteURI host] onPort:port error:&error];
        if(!res || error) return nil;

        connected = NO;
        aborted = NO;
    }
    return self;
}

- (void)abort
{
    HTTPLogTrace();
    [asyncSocket disconnect];
    [connection responseDidAbort:self];
    aborted = YES;
}

- (BOOL)delayResponseHeaders
{
    return !(connected && [remoteResponse isHeaderComplete]);
}

- (void)processReadBuffer:(int)read
{
    // 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];
}

- (BOOL)canContinue
{
    if (aborted || asyncSocket == nil || [remoteResponse statusCode] == 304)
    {
        return NO;
    }
    return [asyncSocket isConnected];
}

- (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);
    fileOffset = offset;
    readOffset = offset;
}

- (void)startHeadRequest
{
    HTTPLogTrace();

    NSURL *remoteUrl = [self remoteRequestURI];

    HTTPMessage *remoteRequest = [[HTTPMessage alloc] initRequestWithMethod:@"HEAD" URL:remoteUrl version:HTTPVersion1_1];
    // Add HOST headers¸
    [remoteRequest setHeaderField:@"HOST" value:[remoteUrl host]];
    [remoteRequest setHeaderField:@"User-Agent" value:[localRequest headerField:@"User-Agent"]];
    [remoteRequest setHeaderField:@"Range" value:[NSString stringWithFormat:@"%lld-",fileOffset]];
    [remoteRequest setHeaderField:@"Connection" value:@"Keep-Alive"];

    NSData *requestData = [remoteRequest messageData];
    [asyncSocket writeData:requestData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_REQUEST_HEADER];
}
- (void)startContentRequest
{
    HTTPLogTrace();

    NSURL *remoteUrl = [self remoteRequestURI];

    HTTPMessage *remoteRequest = [[HTTPMessage alloc] initRequestWithMethod:[localRequest method] URL:remoteUrl version:HTTPVersion1_1];
    // Add HOST headers¸
    [remoteRequest setHeaderField:@"HOST" value:[remoteUrl host]];

    // Add standart headers from orignal request
    NSMutableDictionary *requestHeaders = [[localRequest allHeaderFields] mutableCopy];
    [requestHeaders removeObjectForKey:@"HOST"];

    NSEnumerator *keyEnumerator = [requestHeaders keyEnumerator];
    NSString *key;

    while ((key = [keyEnumerator nextObject]))
    {
        NSString *value = [requestHeaders objectForKey:key];
        [remoteRequest setHeaderField:key value:value];
    }

    NSData *requestData = [remoteRequest messageData];
    [asyncSocket writeData:requestData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_REQUEST_HEADER];
}
- (NSData *)readDataOfLength:(NSUInteger)length
{
    if (data)
    {
        NSUInteger dataLength = [data length];
        fileOffset += dataLength;
        NSData *result = data;
        data = nil;
        return result;
    }
    else
    {
        if(!connected){
            return nil;
        }
        if (![self canContinue])
        {
            [self abort];
            return nil;
        }
        HTTPLogVerbose(@"%@[%p]: Reading data of length %d", THIS_FILE, self, length);
        [asyncSocket readDataToLength:length withTimeout:TIMEOUT_READ_BODY buffer:buffer bufferOffset:readBufferOffset tag:HTTP_RESPONSE_BODY];
        return nil;
    }
}

- (BOOL)isDone
{
    BOOL result = (fileOffset == fileLength);
    HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
    return result;
}

- (BOOL)isAsynchronous
{
    HTTPLogTrace();
    return YES;
}

- (void)connectionDidClose
{
    HTTPLogTrace();
    [asyncSocket  disconnect];
}

- (void)dealloc
{
    HTTPLogTrace();
    //if (readBuffer)
    //  free(readBuffer);
    if(socketQueue)
        dispatch_release(socketQueue);
    //[super dealloc];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark GCDAsyncSocket Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
    connected = true;
    if(fileLength >0)
    {
        [self startContentRequest];
    } else {
        [self startHeadRequest];
    }
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
    connected = false;
    if(err) {
        HTTPLogTrace2(@"%@[%p]: socketDidDisconnect:\n %@", THIS_FILE, self, err);
    }
}
/**
 * This method is called after the socket has successfully written data to the stream.
 **/
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    if (tag == HTTP_REQUEST_HEADER)
    {
        remoteResponse = [[HTTPMessage alloc] initEmptyResponse];

        [asyncSocket readDataToData:[GCDAsyncSocket CRLFData]
                        withTimeout:TIMEOUT_READ_FIRST_HEADER_LINE
                          maxLength:LIMIT_MAX_HEADER_LINE_LENGTH
                                tag:HTTP_RESPONSE_HEADER];
    }
}
/**
 * This method is called after the socket has successfully read data from the stream.
 * Remember that this method will only be called after the socket reaches a CRLF, or after it's read the proper length.
 **/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)dat withTag:(long)tag
{
    if (tag == HTTP_RESPONSE_HEADER)
    {
        NSString *log = [[NSString alloc]initWithData:dat encoding:NSUTF8StringEncoding];
        HTTPLogTrace2(@"%@[%p]: didReadData\n %@", THIS_FILE, self, log);

        // Append the header line to the http message
        BOOL result = [remoteResponse appendData:dat];

        if (!result)
        {
            HTTPLogWarn(@"%@[%p]: Malformed request", THIS_FILE, self);
            [self abort];
        }
        else if (![remoteResponse isHeaderComplete])
        {
            // We don't have a complete header yet
            // That is, we haven't yet received a CRLF on a line by itself, indicating the end of the header
            if (++numHeaderLines > LIMIT_MAX_HEADER_LINES)
            {
                // Reached the maximum amount of header lines in a single HTTP request
                // This could be an attempted DOS attack
                [self abort];

                // Explictly return to ensure we don't do anything after the socket disconnect
                return;
            }
            else
            {
                [asyncSocket readDataToData:[GCDAsyncSocket CRLFData]
                                withTimeout:TIMEOUT_READ_SUBSEQUENT_HEADER_LINE
                                  maxLength:LIMIT_MAX_HEADER_LINE_LENGTH
                                        tag:HTTP_RESPONSE_HEADER];
            }
        }
        else 
        {
            // We have an entire HTTP request header from the client

            //NSRange isHeadRequest = [[remoteResponse method] rangeOfString:@"HEAD" options:NSCaseInsensitiveSearch];
            //if(isHeadRequest.location == NSNotFound)
            if(fileLength > 0)
            {
                // Now we need to reply to the local request
                // Not able to pass my buffer dont know why
                //buffer = [[NSMutableData alloc] initWithCapacity:READ_CHUNKSIZE];
                //Kick start reading the body from the connections getDataOfLength
                [connection responseHasAvailableData:self];
            }
            else
            {
                NSString *fLength = [remoteResponse headerField:@"Content-Length"];
                fileLength = [fLength longLongValue];
                if(fileLength > 0)
                {
                    MyAppDelegate * appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
                    appDelegate.currentVideoLength = fileLength;

                    [self startContentRequest];
                }
                else
                {
                    [self abort];
                }
            }
        }
    }
    else if (tag == HTTP_RESPONSE_BODY)
    {
        // Handle a chunk of data from the remote response body
        NSUInteger read = [dat length];
        readBuffer = (void *)[dat bytes];
        readOffset += read;
        readBufferOffset += read;

        [self processReadBuffer:read];
    }
}

@end
CodeStrumpet commented 10 years ago

This is awesome (albeit a little intimidating). Any chance you could point to a working example where you proxy a URL request?

egormerkushev commented 7 years ago

@CodeStrumpet you need to implement your own NSObject<HTTPResponse> but not work with sockets - just use NSURLSession and data tasks to serve request from connection and use data task's response to create HTTPMessage response.

Core functionality may look like this:

- (id)initWithLocalRequest:(HTTPMessage *)request forConnection:(HTTPConnection *)parent targetHost:(NSURL*)targetHost
{
    if ((self = [super init]))
    {
        _connection = parent;
        _localRequest = request;
        _done = NO;
        NSURL *remoteURI = [targetHost copy];

... <some URL changes>

        NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        NSMutableURLRequest *urlrequest = [NSMutableURLRequest requestWithURL:remoteURI];
        [urlrequest setHTTPMethod:[_localRequest method]];
        // Body
        NSData *bodyData = [_localRequest body];
        if (bodyData.length > 0) {
            NSLog(@"bodyData: %@", [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding]);
            [urlrequest setHTTPBody:bodyData];
        }
        // Headers
        NSDictionary *originHeaders = [_localRequest allHeaderFields];
        [urlrequest setAllHTTPHeaderFields:originHeaders];
        [urlrequest setValue:remoteURI.host forHTTPHeaderField:@"Host"];
        // Handler
        NSURLSessionTask *task;
        task = [session dataTaskWithRequest:urlrequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSURL *url = [response URL];
            _remoteResponse = [self responseForResponse:response data:data];
            _done = YES;
            [_connection responseHasAvailableData:self];
            _connection = nil;
        }];
        // Run
        [task resume];
    }
    return self;
}