初始化提交

This commit is contained in:
2026-02-03 16:52:44 +08:00
commit d2f9806384
512 changed files with 65167 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
#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;
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;
- (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)handleResourceNotFound;
- (void)handleInvalidRequest:(NSData *)data;
- (void)handleUnknownMethod:(NSString *)method;
- (NSData *)preprocessResponse:(HTTPMessage *)response;
- (NSData *)preprocessErrorResponse:(HTTPMessage *)response;
- (void)finishResponse;
- (BOOL)shouldDie;
- (void)die;
@end
@interface HTTPConnection (AsynchronousHTTPResponse)
- (void)responseHasAvailableData:(NSObject<HTTPResponse> *)sender;
- (void)responseDidAbort:(NSObject<HTTPResponse> *)sender;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
/**
* 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.
**/
// 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(...) { }
#define HTTPLogWarn(...) { }
#define HTTPLogInfo(...) { }
#define HTTPLogVerbose(...) { }
#define HTTPLogTrace() { }
#define HTTPLogTrace2(...) { }
#define HTTPLogCError(...) { }
#define HTTPLogCWarn(...) { }
#define HTTPLogCInfo(...) { }
#define HTTPLogCVerbose(...) { }
#define HTTPLogCTrace() { }
#define HTTPLogCTrace2(...) { }

View File

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

View File

@@ -0,0 +1,114 @@
#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
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
@implementation HTTPMessage
- (id)initEmptyRequest
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateEmpty(NULL, YES);
}
return self;
}
- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateRequest(NULL,
(__bridge CFStringRef)method,
(__bridge CFURLRef)url,
(__bridge CFStringRef)version);
}
return self;
}
- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version
{
if ((self = [super init]))
{
message = CFHTTPMessageCreateResponse(NULL,
(CFIndex)code,
(__bridge CFStringRef)description,
(__bridge CFStringRef)version);
}
return self;
}
- (void)dealloc
{
if (message)
{
CFRelease(message);
}
}
- (BOOL)appendData:(NSData *)data
{
return CFHTTPMessageAppendBytes(message, [data bytes], [data length]);
}
- (BOOL)isHeaderComplete
{
return CFHTTPMessageIsHeaderComplete(message);
}
- (NSString *)version
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message);
}
- (NSString *)method
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message);
}
- (NSURL *)url
{
return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message);
}
- (NSInteger)statusCode
{
return (NSInteger)CFHTTPMessageGetResponseStatusCode(message);
}
- (NSDictionary *)allHeaderFields
{
return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message);
}
- (NSString *)headerField:(NSString *)headerField
{
return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)headerField);
}
- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue
{
CFHTTPMessageSetHeaderFieldValue(message,
(__bridge CFStringRef)headerField,
(__bridge CFStringRef)headerFieldValue);
}
- (NSData *)messageData
{
return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message);
}
- (NSData *)body
{
return (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message);
}
- (void)setBody:(NSData *)body
{
CFHTTPMessageSetBody(message, (__bridge CFDataRef)body);
}
@end

View File

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

View File

@@ -0,0 +1,126 @@
#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@class WebSocket;
#if TARGET_OS_IPHONE
#define IMPLEMENTED_PROTOCOLS
#else
#define IMPLEMENTED_PROTOCOLS
#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;
// Connection management
NSMutableArray *connections;
NSLock *connectionsLock;
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;
/**
* 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;
- (NSUInteger)numberOfHTTPConnections;
@end

View File

@@ -0,0 +1,372 @@
#import "HTTPServer.h"
#import "HTTPConnection.h"
#import "HTTPLogging.h"
#import "GCDAsyncSocket.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion"
#pragma clang diagnostic ignored "-Wunused"
// Log levels: off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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:(id<GCDAsyncSocketDelegate>)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;
// Initialize arrays to hold all the HTTP connections
connections = [[NSMutableArray alloc] init];
connectionsLock = [[NSLock alloc] init];
// Register for notifications of closed connections
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(connectionDidDie:)
name:HTTPConnectionDidDieNotification
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;
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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;
}
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 {
// 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];
}
}});
}
- (BOOL)isRunning
{
__block BOOL result;
dispatch_sync(serverQueue, ^{
result = isRunning;
});
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#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 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];
}
@end

View File

@@ -0,0 +1,18 @@
Software License Agreement (BSD License)
Copyright (c) 2011, Deusty, LLC
All rights reserved.
Redistribution and use of this software in source and binary forms,
with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the
following disclaimer.
* Neither the name of Deusty nor the names of its
contributors may be used to endorse or promote products
derived from this software without specific prior
written permission of Deusty, LLC.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

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

View File

@@ -0,0 +1,83 @@
#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
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Wcast-qual"
#pragma clang diagnostic ignored "-Wunused-variable"
// 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 *)(((char*)[data bytes]) + offset);
offset += length;
return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
}
- (BOOL)isDone
{
BOOL result = (offset == [data length]);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
@end

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
// Wraps an HTTPResponse object to allow setting a custom status code
// without needing to create subclasses of every response.
@interface HTTPResponseProxy : NSObject <HTTPResponse>
@property (nonatomic) NSObject<HTTPResponse> *response;
@property (nonatomic) NSInteger status;
- (NSInteger)customStatus;
@end

View File

@@ -0,0 +1,84 @@
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
@implementation HTTPResponseProxy
@synthesize response;
@synthesize status;
- (NSInteger)status {
if (status != 0) {
return status;
} else if ([response respondsToSelector:@selector(status)]) {
return [response status];
}
return 200;
}
- (void)setStatus:(NSInteger)statusCode {
status = statusCode;
}
- (NSInteger)customStatus {
return status;
}
// Implement the required HTTPResponse methods
- (UInt64)contentLength {
if (response) {
return [response contentLength];
} else {
return 0;
}
}
- (UInt64)offset {
if (response) {
return [response offset];
} else {
return 0;
}
}
- (void)setOffset:(UInt64)offset {
if (response) {
[response setOffset:offset];
}
}
- (NSData *)readDataOfLength:(NSUInteger)length {
if (response) {
return [response readDataOfLength:length];
} else {
return nil;
}
}
- (BOOL)isDone {
if (response) {
return [response isDone];
} else {
return YES;
}
}
// Forward all other invocations to the actual response object
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([response respondsToSelector:[invocation selector]]) {
[invocation invokeWithTarget:response];
} else {
[super forwardInvocation:invocation];
}
}
- (BOOL)respondsToSelector:(SEL)selector {
if ([super respondsToSelector:selector])
return YES;
return [response respondsToSelector:selector];
}
@end

View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 Matt Stevens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,18 @@
#import <Foundation/Foundation.h>
#import "RoutingHTTPServer.h"
@interface Route : NSObject
@property (nonatomic) NSRegularExpression *regex;
@property (nonatomic, copy) RequestHandler handler;
#if __has_feature(objc_arc_weak)
@property (nonatomic, weak) id target;
#else
@property (nonatomic, assign) id target;
#endif
@property (nonatomic, assign) SEL selector;
@property (nonatomic) NSArray *keys;
@end

View File

@@ -0,0 +1,11 @@
#import "Route.h"
@implementation Route
@synthesize regex;
@synthesize handler;
@synthesize target;
@synthesize selector;
@synthesize keys;
@end

View File

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
@class HTTPMessage;
@interface RouteRequest : NSObject
@property (nonatomic, readonly) NSDictionary *headers;
@property (nonatomic, readonly) NSDictionary *params;
- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)params;
- (NSString *)header:(NSString *)field;
- (id)param:(NSString *)name;
- (NSString *)method;
- (NSURL *)url;
- (NSData *)body;
@end

View File

@@ -0,0 +1,50 @@
#import "RouteRequest.h"
#import "HTTPMessage.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RouteRequest {
HTTPMessage *message;
}
@synthesize params;
- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)parameters {
if (self = [super init]) {
params = parameters;
message = msg;
}
return self;
}
- (NSDictionary *)headers {
return [message allHeaderFields];
}
- (NSString *)header:(NSString *)field {
return [message headerField:field];
}
- (id)param:(NSString *)name {
return [params objectForKey:name];
}
- (NSString *)method {
return [message method];
}
- (NSURL *)url {
return [message url];
}
- (NSData *)body {
return [message body];
}
- (NSString *)description {
NSData *data = [message messageData];
return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
@end

View File

@@ -0,0 +1,20 @@
#import <Foundation/Foundation.h>
#import "HTTPResponse.h"
@class HTTPConnection;
@class HTTPResponseProxy;
@interface RouteResponse : NSObject
@property (nonatomic, unsafe_unretained, readonly) HTTPConnection *connection;
@property (nonatomic, readonly) NSDictionary *headers;
@property (nonatomic, strong) NSObject<HTTPResponse> *response;
@property (nonatomic, readonly) NSObject<HTTPResponse> *proxiedResponse;
@property (nonatomic) NSInteger statusCode;
- (id)initWithConnection:(HTTPConnection *)theConnection;
- (void)setHeader:(NSString *)field value:(NSString *)value;
- (void)respondWithString:(NSString *)string;
- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding;
- (void)respondWithData:(NSData *)data;
@end

View File

@@ -0,0 +1,66 @@
#import "RouteResponse.h"
#import "HTTPConnection.h"
#import "HTTPDataResponse.h"
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RouteResponse {
NSMutableDictionary *headers;
HTTPResponseProxy *proxy;
}
@synthesize connection;
@synthesize headers;
- (id)initWithConnection:(HTTPConnection *)theConnection {
if (self = [super init]) {
connection = theConnection;
headers = [[NSMutableDictionary alloc] init];
proxy = [[HTTPResponseProxy alloc] init];
}
return self;
}
- (NSObject <HTTPResponse>*)response {
return proxy.response;
}
- (void)setResponse:(NSObject <HTTPResponse>*)response {
proxy.response = response;
}
- (NSObject <HTTPResponse>*)proxiedResponse {
if (proxy.response != nil || proxy.customStatus != 0 || [headers count] > 0) {
return proxy;
}
return nil;
}
- (NSInteger)statusCode {
return proxy.status;
}
- (void)setStatusCode:(NSInteger)status {
proxy.status = status;
}
- (void)setHeader:(NSString *)field value:(NSString *)value {
[headers setObject:value forKey:field];
}
- (void)respondWithString:(NSString *)string {
[self respondWithString:string encoding:NSUTF8StringEncoding];
}
- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding {
[self respondWithData:[string dataUsingEncoding:encoding]];
}
- (void)respondWithData:(NSData *)data {
self.response = [[HTTPDataResponse alloc] initWithData:data];
}
@end

View File

@@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
#import "HTTPConnection.h"
@interface RoutingConnection : HTTPConnection
@end

View File

@@ -0,0 +1,142 @@
#import "RoutingConnection.h"
#import "RoutingHTTPServer.h"
#import "HTTPMessage.h"
#import "HTTPResponseProxy.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
#pragma clang diagnostic ignored "-Wundeclared-selector"
@implementation RoutingConnection {
__unsafe_unretained RoutingHTTPServer *http;
NSDictionary *headers;
}
- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig {
if (self = [super initWithAsyncSocket:newSocket configuration:aConfig]) {
NSAssert([config.server isKindOfClass:[RoutingHTTPServer class]],
@"A RoutingConnection is being used with a server that is not a %@",
NSStringFromClass([RoutingHTTPServer class]));
http = (RoutingHTTPServer *)config.server;
}
return self;
}
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path {
if ([http supportsMethod:method])
return YES;
return [super supportsMethod:method atPath:path];
}
- (BOOL)shouldHandleRequestForMethod:(NSString *)method atPath:(NSString *)path {
// The default implementation is strict about the use of Content-Length. Either
// a given method + path combination must *always* include data or *never*
// include data. The routing connection is lenient, a POST that sometimes does
// not include data or a GET that sometimes does is fine. It is up to the route
// implementations to decide how to handle these situations.
return YES;
}
- (void)processBodyData:(NSData *)postDataChunk {
BOOL result = [request appendData:postDataChunk];
if (!result) {
// TODO: Log
}
}
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path {
NSURL *url = [request url];
NSString *query = nil;
NSDictionary *params = [NSDictionary dictionary];
headers = nil;
if (url) {
path = [url path]; // Strip the query string from the path
query = [url query];
if (query) {
params = [self parseParams:query];
}
}
RouteResponse *response = [http routeMethod:method withPath:path parameters:params request:request connection:self];
if (response != nil) {
headers = response.headers;
return response.proxiedResponse;
}
// Set a MIME type for static files if possible
NSObject<HTTPResponse> *staticResponse = [super httpResponseForMethod:method URI:path];
if (staticResponse && [staticResponse respondsToSelector:@selector(filePath)]) {
NSString *mimeType = [http mimeTypeForPath:[staticResponse performSelector:@selector(filePath)]];
if (mimeType) {
headers = [NSDictionary dictionaryWithObject:mimeType forKey:@"Content-Type"];
}
}
return staticResponse;
}
- (void)responseHasAvailableData:(NSObject<HTTPResponse> *)sender {
HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse;
if (proxy.response == sender) {
[super responseHasAvailableData:httpResponse];
}
}
- (void)responseDidAbort:(NSObject<HTTPResponse> *)sender {
HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse;
if (proxy.response == sender) {
[super responseDidAbort:httpResponse];
}
}
- (void)setHeadersForResponse:(HTTPMessage *)response isError:(BOOL)isError {
[http.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
[response setHeaderField:field value:value];
}];
if (headers && !isError) {
[headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
[response setHeaderField:field value:value];
}];
}
// Set the connection header if not already specified
NSString *connection = [response headerField:@"Connection"];
if (!connection) {
connection = [self shouldDie] ? @"close" : @"keep-alive";
[response setHeaderField:@"Connection" value:connection];
}
}
- (NSData *)preprocessResponse:(HTTPMessage *)response {
[self setHeadersForResponse:response isError:NO];
return [super preprocessResponse:response];
}
- (NSData *)preprocessErrorResponse:(HTTPMessage *)response {
[self setHeadersForResponse:response isError:YES];
return [super preprocessErrorResponse:response];
}
- (BOOL)shouldDie {
__block BOOL shouldDie = [super shouldDie];
// Allow custom headers to determine if the connection should be closed
if (!shouldDie && headers) {
[headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) {
if ([field caseInsensitiveCompare:@"connection"] == NSOrderedSame) {
if ([value caseInsensitiveCompare:@"close"] == NSOrderedSame) {
shouldDie = YES;
}
*stop = YES;
}
}];
}
return shouldDie;
}
@end

View File

@@ -0,0 +1,55 @@
#import <Foundation/Foundation.h>
//! Project version number for Peertalk.
FOUNDATION_EXPORT double RoutingHTTPServerVersionNumber;
//! Project version string for Peertalk.
FOUNDATION_EXPORT const unsigned char RoutingHTTPServerVersionString[];
#import "HTTPServer.h"
#import "HTTPConnection.h"
#import "HTTPResponse.h"
#import "RouteResponse.h"
#import "RouteRequest.h"
#import "RoutingConnection.h"
#import "GCDAsyncSocket.h"
typedef void (^RequestHandler)(RouteRequest *request, RouteResponse *response);
@interface RoutingHTTPServer : HTTPServer
@property (nonatomic, readonly) NSDictionary *defaultHeaders;
// Specifies headers that will be set on every response.
// These headers can be overridden by RouteResponses.
- (void)setDefaultHeaders:(NSDictionary *)headers;
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value;
// Returns the dispatch queue on which routes are processed.
// By default this is NULL and routes are processed on CocoaHTTPServer's
// connection queue. You can specify a queue to process routes on, such as
// dispatch_get_main_queue() to process all routes on the main thread.
- (dispatch_queue_t)routeQueue;
- (void)setRouteQueue:(dispatch_queue_t)queue;
- (NSDictionary *)mimeTypes;
- (void)setMIMETypes:(NSDictionary *)types;
- (void)setMIMEType:(NSString *)type forExtension:(NSString *)ext;
- (NSString *)mimeTypeForPath:(NSString *)path;
// Convenience methods. Yes I know, this is Cocoa and we don't use convenience
// methods because typing lengthy primitives over and over and over again is
// elegant with the beauty and the poetry. These are just, you know, here.
- (void)get:(NSString *)path withBlock:(RequestHandler)block;
- (void)post:(NSString *)path withBlock:(RequestHandler)block;
- (void)put:(NSString *)path withBlock:(RequestHandler)block;
- (void)delete:(NSString *)path withBlock:(RequestHandler)block;
- (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block;
- (void)handleMethod:(NSString *)method withPath:(NSString *)path target:(id)target selector:(SEL)selector;
- (BOOL)supportsMethod:(NSString *)method;
- (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)request connection:(HTTPConnection *)connection;
@end

View File

@@ -0,0 +1,303 @@
#import "RoutingHTTPServer.h"
#import "RoutingConnection.h"
#import "Route.h"
#pragma clang diagnostic ignored "-Wdirect-ivar-access"
#pragma clang diagnostic ignored "-Widiomatic-parentheses"
@implementation RoutingHTTPServer {
NSMutableDictionary *routes;
NSMutableDictionary *defaultHeaders;
NSMutableDictionary *mimeTypes;
dispatch_queue_t routeQueue;
}
@synthesize defaultHeaders;
- (id)init {
if (self = [super init]) {
connectionClass = [RoutingConnection self];
routes = [[NSMutableDictionary alloc] init];
defaultHeaders = [[NSMutableDictionary alloc] init];
[self setupMIMETypes];
}
return self;
}
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
- (void)dealloc {
if (routeQueue)
dispatch_release(routeQueue);
}
#endif
- (void)setDefaultHeaders:(NSDictionary *)headers {
if (headers) {
defaultHeaders = [headers mutableCopy];
} else {
defaultHeaders = [[NSMutableDictionary alloc] init];
}
}
- (void)setDefaultHeader:(NSString *)field value:(NSString *)value {
[defaultHeaders setObject:value forKey:field];
}
- (dispatch_queue_t)routeQueue {
return routeQueue;
}
- (void)setRouteQueue:(dispatch_queue_t)queue {
#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE
if (queue)
dispatch_retain(queue);
if (routeQueue)
dispatch_release(routeQueue);
#endif
routeQueue = queue;
}
- (NSDictionary *)mimeTypes {
return mimeTypes;
}
- (void)setMIMETypes:(NSDictionary *)types {
NSMutableDictionary *newTypes;
if (types) {
newTypes = [types mutableCopy];
} else {
newTypes = [[NSMutableDictionary alloc] init];
}
mimeTypes = newTypes;
}
- (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext {
[mimeTypes setObject:theType forKey:ext];
}
- (NSString *)mimeTypeForPath:(NSString *)path {
NSString *ext = [[path pathExtension] lowercaseString];
if (!ext || [ext length] < 1)
return nil;
return [mimeTypes objectForKey:ext];
}
- (void)get:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"GET" withPath:path block:block];
}
- (void)post:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"POST" withPath:path block:block];
}
- (void)put:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"PUT" withPath:path block:block];
}
- (void)delete:(NSString *)path withBlock:(RequestHandler)block {
[self handleMethod:@"DELETE" withPath:path block:block];
}
- (void)handleMethod:(NSString *)method
withPath:(NSString *)path
block:(RequestHandler)block {
Route *route = [self routeWithPath:path];
route.handler = block;
[self addRoute:route forMethod:method];
}
- (void)handleMethod:(NSString *)method
withPath:(NSString *)path
target:(id)target
selector:(SEL)selector {
Route *route = [self routeWithPath:path];
route.target = target;
route.selector = selector;
[self addRoute:route forMethod:method];
}
- (void)addRoute:(Route *)route forMethod:(NSString *)method {
method = [method uppercaseString];
NSMutableArray *methodRoutes = [routes objectForKey:method];
if (methodRoutes == nil) {
methodRoutes = [NSMutableArray array];
[routes setObject:methodRoutes forKey:method];
}
[methodRoutes addObject:route];
// Define a HEAD route for all GET routes
if ([method isEqualToString:@"GET"]) {
[self addRoute:route forMethod:@"HEAD"];
}
}
- (Route *)routeWithPath:(NSString *)path {
Route *route = [[Route alloc] init];
NSMutableArray *keys = [NSMutableArray array];
if ([path length] > 2 && [path characterAtIndex:0] == '{') {
// This is a custom regular expression, just remove the {}
path = [path substringWithRange:NSMakeRange(1, [path length] - 2)];
} else {
NSRegularExpression *regex = nil;
// Escape regex characters
regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil];
path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"];
// Parse any :parameters and * in the path
regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)"
options:0
error:nil];
NSMutableString *regexPath = [NSMutableString stringWithString:path];
__block NSInteger diff = 0;
[regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length)
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length);
NSString *replacementString;
NSString *capturedString = [path substringWithRange:result.range];
if ([capturedString isEqualToString:@"*"]) {
[keys addObject:@"wildcards"];
replacementString = @"(.*?)";
} else {
NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]];
[keys addObject:keyString];
replacementString = @"([^/]+)";
}
[regexPath replaceCharactersInRange:replacementRange withString:replacementString];
diff += replacementString.length - result.range.length;
}];
path = [NSString stringWithFormat:@"^%@$", regexPath];
}
route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil];
if ([keys count] > 0) {
route.keys = keys;
}
return route;
}
- (BOOL)supportsMethod:(NSString *)method {
return ([routes objectForKey:method] != nil);
}
- (void)handleRoute:(Route *)route
withRequest:(RouteRequest *)request
response:(RouteResponse *)response {
if (route.handler) {
route.handler(request, response);
} else {
id target = route.target;
SEL selector = route.selector;
NSMethodSignature *signature = [target methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:selector];
[invocation setArgument:&request atIndex:2];
[invocation setArgument:&response atIndex:3];
[invocation invokeWithTarget:target];
}
}
- (RouteResponse *)routeMethod:(NSString *)method
withPath:(NSString *)path
parameters:(NSDictionary *)params
request:(HTTPMessage *)httpMessage
connection:(HTTPConnection *)connection {
NSMutableArray *methodRoutes = [routes objectForKey:method];
if (methodRoutes == nil)
return nil;
for (Route *route in methodRoutes) {
NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)];
if (!result)
continue;
// The first range is all of the text matched by the regex.
NSUInteger captureCount = [result numberOfRanges];
if (route.keys) {
// Add the route's parameters to the parameter dictionary, accounting for
// the first range containing the matched text.
if (captureCount == [route.keys count] + 1) {
NSMutableDictionary *newParams = [params mutableCopy];
NSUInteger index = 1;
BOOL firstWildcard = YES;
for (NSString *key in route.keys) {
NSString *capture = [path substringWithRange:[result rangeAtIndex:index]];
if ([key isEqualToString:@"wildcards"]) {
NSMutableArray *wildcards = [newParams objectForKey:key];
if (firstWildcard) {
// Create a new array and replace any existing object with the same key
wildcards = [NSMutableArray array];
[newParams setObject:wildcards forKey:key];
firstWildcard = NO;
}
[wildcards addObject:capture];
} else {
[newParams setObject:capture forKey:key];
}
index++;
}
params = newParams;
}
} else if (captureCount > 1) {
// For custom regular expressions place the anonymous captures in the captures parameter
NSMutableDictionary *newParams = [params mutableCopy];
NSMutableArray *captures = [NSMutableArray array];
for (NSUInteger i = 1; i < captureCount; i++) {
[captures addObject:[path substringWithRange:[result rangeAtIndex:i]]];
}
[newParams setObject:captures forKey:@"captures"];
params = newParams;
}
RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params];
RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection];
if (!routeQueue) {
[self handleRoute:route withRequest:request response:response];
} else {
// Process the route on the specified queue
dispatch_sync(routeQueue, ^{
@autoreleasepool {
[self handleRoute:route withRequest:request response:response];
}
});
}
return response;
}
return nil;
}
- (void)setupMIMETypes {
mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
@"application/x-javascript", @"js",
@"image/gif", @"gif",
@"image/jpeg", @"jpg",
@"image/jpeg", @"jpeg",
@"image/png", @"png",
@"image/svg+xml", @"svg",
@"image/tiff", @"tif",
@"image/tiff", @"tiff",
@"image/x-icon", @"ico",
@"image/x-ms-bmp", @"bmp",
@"text/css", @"css",
@"text/html", @"html",
@"text/html", @"htm",
@"text/plain", @"txt",
@"text/xml", @"xml",
nil];
}
@end