add .gitignore

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

View File

@@ -0,0 +1,4 @@
The CocoaAsyncSocket project is under Public Domain license.
http://code.google.com/p/cocoaasyncsocket/
The AsyncSocket project has been around since 2001 and is used in many applications and frameworks.

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,41 @@
#import <Foundation/Foundation.h>
#import <asl.h>
#import "DDLog.h"
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides a logger for the Apple System Log facility.
*
* As described in the "Getting Started" page,
* the traditional NSLog() function directs it's output to two places:
*
* - Apple System Log
* - StdErr (if stderr is a TTY) so log statements show up in Xcode console
*
* To duplicate NSLog() functionality you can simply add this logger and a tty logger.
* However, if you instead choose to use file logging (for faster performance),
* you may choose to use a file logger and a tty logger.
**/
@interface DDASLLogger : DDAbstractLogger <DDLogger>
{
aslclient client;
}
+ (DDASLLogger *)sharedInstance;
// Inherited from DDAbstractLogger
// - (id <DDLogFormatter>)logFormatter;
// - (void)setLogFormatter:(id <DDLogFormatter>)formatter;
@end

View File

@@ -0,0 +1,99 @@
#import "DDASLLogger.h"
#import <libkern/OSAtomic.h>
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
**/
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@implementation DDASLLogger
static DDASLLogger *sharedInstance;
/**
* The runtime sends initialize to each class in a program exactly one time just before the class,
* or any class that inherits from it, is sent its first message from within the program. (Thus the
* method may never be invoked if the class is not used.) The runtime sends the initialize message to
* classes in a thread-safe manner. Superclasses receive this message before their subclasses.
*
* This method may also be called directly (assumably by accident), hence the safety mechanism.
**/
+ (void)initialize
{
static BOOL initialized = NO;
if (!initialized)
{
initialized = YES;
sharedInstance = [[DDASLLogger alloc] init];
}
}
+ (DDASLLogger *)sharedInstance
{
return sharedInstance;
}
- (id)init
{
if (sharedInstance != nil)
{
return nil;
}
if ((self = [super init]))
{
// A default asl client is provided for the main thread,
// but background threads need to create their own client.
client = asl_open(NULL, "com.apple.console", 0);
}
return self;
}
- (void)logMessage:(DDLogMessage *)logMessage
{
NSString *logMsg = logMessage->logMsg;
if (formatter)
{
logMsg = [formatter formatLogMessage:logMessage];
}
if (logMsg)
{
const char *msg = [logMsg UTF8String];
int aslLogLevel;
switch (logMessage->logFlag)
{
// Note: By default ASL will filter anything above level 5 (Notice).
// So our mappings shouldn't go above that level.
case LOG_FLAG_ERROR : aslLogLevel = ASL_LEVEL_CRIT; break;
case LOG_FLAG_WARN : aslLogLevel = ASL_LEVEL_ERR; break;
case LOG_FLAG_INFO : aslLogLevel = ASL_LEVEL_WARNING; break;
default : aslLogLevel = ASL_LEVEL_NOTICE; break;
}
asl_log(client, NULL, aslLogLevel, "%s", msg);
}
}
- (NSString *)loggerName
{
return @"cocoa.lumberjack.aslLogger";
}
@end

View File

@@ -0,0 +1,102 @@
#import <Foundation/Foundation.h>
#import "DDLog.h"
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides an abstract implementation of a database logger.
*
* That is, it provides the base implementation for a database logger to build atop of.
* All that is needed for a concrete database logger is to extend this class
* and override the methods in the implementation file that are prefixed with "db_".
**/
@interface DDAbstractDatabaseLogger : DDAbstractLogger {
@protected
NSUInteger saveThreshold;
NSTimeInterval saveInterval;
NSTimeInterval maxAge;
NSTimeInterval deleteInterval;
BOOL deleteOnEverySave;
BOOL saveTimerSuspended;
NSUInteger unsavedCount;
dispatch_time_t unsavedTime;
dispatch_source_t saveTimer;
dispatch_time_t lastDeleteTime;
dispatch_source_t deleteTimer;
}
/**
* Specifies how often to save the data to disk.
* Since saving is an expensive operation (disk io) it is not done after every log statement.
* These properties allow you to configure how/when the logger saves to disk.
*
* A save is done when either (whichever happens first):
*
* - The number of unsaved log entries reaches saveThreshold
* - The amount of time since the oldest unsaved log entry was created reaches saveInterval
*
* You can optionally disable the saveThreshold by setting it to zero.
* If you disable the saveThreshold you are entirely dependent on the saveInterval.
*
* You can optionally disable the saveInterval by setting it to zero (or a negative value).
* If you disable the saveInterval you are entirely dependent on the saveThreshold.
*
* It's not wise to disable both saveThreshold and saveInterval.
*
* The default saveThreshold is 500.
* The default saveInterval is 60 seconds.
**/
@property (assign, readwrite) NSUInteger saveThreshold;
@property (assign, readwrite) NSTimeInterval saveInterval;
/**
* It is likely you don't want the log entries to persist forever.
* Doing so would allow the database to grow infinitely large over time.
*
* The maxAge property provides a way to specify how old a log statement can get
* before it should get deleted from the database.
*
* The deleteInterval specifies how often to sweep for old log entries.
* Since deleting is an expensive operation (disk io) is is done on a fixed interval.
*
* An alternative to the deleteInterval is the deleteOnEverySave option.
* This specifies that old log entries should be deleted during every save operation.
*
* You can optionally disable the maxAge by setting it to zero (or a negative value).
* If you disable the maxAge then old log statements are not deleted.
*
* You can optionally disable the deleteInterval by setting it to zero (or a negative value).
*
* If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted.
*
* It's not wise to enable both deleteInterval and deleteOnEverySave.
*
* The default maxAge is 7 days.
* The default deleteInterval is 5 minutes.
* The default deleteOnEverySave is NO.
**/
@property (assign, readwrite) NSTimeInterval maxAge;
@property (assign, readwrite) NSTimeInterval deleteInterval;
@property (assign, readwrite) BOOL deleteOnEverySave;
/**
* Forces a save of any pending log entries (flushes log entries to disk).
**/
- (void)savePendingLogEntries;
/**
* Removes any log entries that are older than maxAge.
**/
- (void)deleteOldLogEntries;
@end

View File

@@ -0,0 +1,727 @@
#import "DDAbstractDatabaseLogger.h"
#import <math.h>
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
**/
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@interface DDAbstractDatabaseLogger ()
- (void)destroySaveTimer;
- (void)destroyDeleteTimer;
@end
#pragma mark -
@implementation DDAbstractDatabaseLogger
- (id)init
{
if ((self = [super init]))
{
saveThreshold = 500;
saveInterval = 60; // 60 seconds
maxAge = (60 * 60 * 24 * 7); // 7 days
deleteInterval = (60 * 5); // 5 minutes
}
return self;
}
- (void)dealloc
{
[self destroySaveTimer];
[self destroyDeleteTimer];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Override Me
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)db_log:(DDLogMessage *)logMessage
{
// Override me and add your implementation.
//
// Return YES if an item was added to the buffer.
// Return NO if the logMessage was ignored.
return NO;
}
- (void)db_save
{
// Override me and add your implementation.
}
- (void)db_delete
{
// Override me and add your implementation.
}
- (void)db_saveAndDelete
{
// Override me and add your implementation.
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Private API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)performSaveAndSuspendSaveTimer
{
if (unsavedCount > 0)
{
if (deleteOnEverySave)
[self db_saveAndDelete];
else
[self db_save];
}
unsavedCount = 0;
unsavedTime = 0;
if (saveTimer && !saveTimerSuspended)
{
dispatch_suspend(saveTimer);
saveTimerSuspended = YES;
}
}
- (void)performDelete
{
if (maxAge > 0.0)
{
[self db_delete];
lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Timers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)destroySaveTimer
{
if (saveTimer)
{
dispatch_source_cancel(saveTimer);
if (saveTimerSuspended)
{
// Must resume a timer before releasing it (or it will crash)
dispatch_resume(saveTimer);
saveTimerSuspended = NO;
}
#if !OS_OBJECT_USE_OBJC
dispatch_release(saveTimer);
#endif
saveTimer = NULL;
}
}
- (void)updateAndResumeSaveTimer
{
if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0))
{
uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC);
dispatch_time_t startTime = dispatch_time(unsavedTime, interval);
dispatch_source_set_timer(saveTimer, startTime, interval, 1.0);
if (saveTimerSuspended)
{
dispatch_resume(saveTimer);
saveTimerSuspended = NO;
}
}
}
- (void)createSuspendedSaveTimer
{
if ((saveTimer == NULL) && (saveInterval > 0.0))
{
saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue);
dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool {
[self performSaveAndSuspendSaveTimer];
}});
saveTimerSuspended = YES;
}
}
- (void)destroyDeleteTimer
{
if (deleteTimer)
{
dispatch_source_cancel(deleteTimer);
#if !OS_OBJECT_USE_OBJC
dispatch_release(deleteTimer);
#endif
deleteTimer = NULL;
}
}
- (void)updateDeleteTimer
{
if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0))
{
uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC);
dispatch_time_t startTime;
if (lastDeleteTime > 0)
startTime = dispatch_time(lastDeleteTime, interval);
else
startTime = dispatch_time(DISPATCH_TIME_NOW, interval);
dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0);
}
}
- (void)createAndStartDeleteTimer
{
if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0))
{
deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue);
if (deleteTimer != NULL) {
dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool {
[self performDelete];
}});
[self updateDeleteTimer];
dispatch_resume(deleteTimer);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSUInteger)saveThreshold
{
// The design of this method is taken from the DDAbstractLogger implementation.
// For extensive documentation please refer to the DDAbstractLogger implementation.
// Note: The internal implementation MUST access the colorsEnabled variable directly,
// This method is designed explicitly for external access.
//
// Using "self." syntax to go through this method will cause immediate deadlock.
// This is the intended result. Fix it by accessing the ivar directly.
// Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block NSUInteger result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(loggerQueue, ^{
result = saveThreshold;
});
});
return result;
}
- (void)setSaveThreshold:(NSUInteger)threshold
{
dispatch_block_t block = ^{ @autoreleasepool {
if (saveThreshold != threshold)
{
saveThreshold = threshold;
// Since the saveThreshold has changed,
// we check to see if the current unsavedCount has surpassed the new threshold.
//
// If it has, we immediately save the log.
if ((unsavedCount >= saveThreshold) && (saveThreshold > 0))
{
[self performSaveAndSuspendSaveTimer];
}
}
}};
// The design of the setter logic below is taken from the DDAbstractLogger implementation.
// For documentation please refer to the DDAbstractLogger implementation.
if ([self isOnInternalLoggerQueue])
{
block();
}
else
{
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
dispatch_async(globalLoggingQueue, ^{
dispatch_async(loggerQueue, block);
});
}
}
- (NSTimeInterval)saveInterval
{
// The design of this method is taken from the DDAbstractLogger implementation.
// For extensive documentation please refer to the DDAbstractLogger implementation.
// Note: The internal implementation MUST access the colorsEnabled variable directly,
// This method is designed explicitly for external access.
//
// Using "self." syntax to go through this method will cause immediate deadlock.
// This is the intended result. Fix it by accessing the ivar directly.
// Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block NSTimeInterval result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(loggerQueue, ^{
result = saveInterval;
});
});
return result;
}
- (void)setSaveInterval:(NSTimeInterval)interval
{
dispatch_block_t block = ^{ @autoreleasepool {
// C99 recommended floating point comparison macro
// Read: isLessThanOrGreaterThan(floatA, floatB)
if (/* saveInterval != interval */ islessgreater(saveInterval, interval))
{
saveInterval = interval;
// There are several cases we need to handle here.
//
// 1. If the saveInterval was previously enabled and it just got disabled,
// then we need to stop the saveTimer. (And we might as well release it.)
//
// 2. If the saveInterval was previously disabled and it just got enabled,
// then we need to setup the saveTimer. (Plus we might need to do an immediate save.)
//
// 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date.
//
// 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date.
// (Plus we might need to do an immediate save.)
if (saveInterval > 0.0)
{
if (saveTimer == NULL)
{
// Handles #2
//
// Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
// if a save is needed the timer will fire immediately.
[self createSuspendedSaveTimer];
[self updateAndResumeSaveTimer];
}
else
{
// Handles #3
// Handles #4
//
// Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
// if a save is needed the timer will fire immediately.
[self updateAndResumeSaveTimer];
}
}
else if (saveTimer)
{
// Handles #1
[self destroySaveTimer];
}
}
}};
// The design of the setter logic below is taken from the DDAbstractLogger implementation.
// For documentation please refer to the DDAbstractLogger implementation.
if ([self isOnInternalLoggerQueue])
{
block();
}
else
{
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
dispatch_async(globalLoggingQueue, ^{
dispatch_async(loggerQueue, block);
});
}
}
- (NSTimeInterval)maxAge
{
// The design of this method is taken from the DDAbstractLogger implementation.
// For extensive documentation please refer to the DDAbstractLogger implementation.
// Note: The internal implementation MUST access the colorsEnabled variable directly,
// This method is designed explicitly for external access.
//
// Using "self." syntax to go through this method will cause immediate deadlock.
// This is the intended result. Fix it by accessing the ivar directly.
// Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block NSTimeInterval result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(loggerQueue, ^{
result = maxAge;
});
});
return result;
}
- (void)setMaxAge:(NSTimeInterval)interval
{
dispatch_block_t block = ^{ @autoreleasepool {
// C99 recommended floating point comparison macro
// Read: isLessThanOrGreaterThan(floatA, floatB)
if (/* maxAge != interval */ islessgreater(maxAge, interval))
{
NSTimeInterval oldMaxAge = maxAge;
NSTimeInterval newMaxAge = interval;
maxAge = interval;
// There are several cases we need to handle here.
//
// 1. If the maxAge was previously enabled and it just got disabled,
// then we need to stop the deleteTimer. (And we might as well release it.)
//
// 2. If the maxAge was previously disabled and it just got enabled,
// then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
//
// 3. If the maxAge was increased,
// then we don't need to do anything.
//
// 4. If the maxAge was decreased,
// then we should do an immediate delete.
BOOL shouldDeleteNow = NO;
if (oldMaxAge > 0.0)
{
if (newMaxAge <= 0.0)
{
// Handles #1
[self destroyDeleteTimer];
}
else if (oldMaxAge > newMaxAge)
{
// Handles #4
shouldDeleteNow = YES;
}
}
else if (newMaxAge > 0.0)
{
// Handles #2
shouldDeleteNow = YES;
}
if (shouldDeleteNow)
{
[self performDelete];
if (deleteTimer)
[self updateDeleteTimer];
else
[self createAndStartDeleteTimer];
}
}
}};
// The design of the setter logic below is taken from the DDAbstractLogger implementation.
// For documentation please refer to the DDAbstractLogger implementation.
if ([self isOnInternalLoggerQueue])
{
block();
}
else
{
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
dispatch_async(globalLoggingQueue, ^{
dispatch_async(loggerQueue, block);
});
}
}
- (NSTimeInterval)deleteInterval
{
// The design of this method is taken from the DDAbstractLogger implementation.
// For extensive documentation please refer to the DDAbstractLogger implementation.
// Note: The internal implementation MUST access the colorsEnabled variable directly,
// This method is designed explicitly for external access.
//
// Using "self." syntax to go through this method will cause immediate deadlock.
// This is the intended result. Fix it by accessing the ivar directly.
// Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block NSTimeInterval result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(loggerQueue, ^{
result = deleteInterval;
});
});
return result;
}
- (void)setDeleteInterval:(NSTimeInterval)interval
{
dispatch_block_t block = ^{ @autoreleasepool {
// C99 recommended floating point comparison macro
// Read: isLessThanOrGreaterThan(floatA, floatB)
if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval))
{
deleteInterval = interval;
// There are several cases we need to handle here.
//
// 1. If the deleteInterval was previously enabled and it just got disabled,
// then we need to stop the deleteTimer. (And we might as well release it.)
//
// 2. If the deleteInterval was previously disabled and it just got enabled,
// then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
//
// 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date.
//
// 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date.
// (Plus we might need to do an immediate delete.)
if (deleteInterval > 0.0)
{
if (deleteTimer == NULL)
{
// Handles #2
//
// Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
// if a delete is needed the timer will fire immediately.
[self createAndStartDeleteTimer];
}
else
{
// Handles #3
// Handles #4
//
// Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
// if a save is needed the timer will fire immediately.
[self updateDeleteTimer];
}
}
else if (deleteTimer)
{
// Handles #1
[self destroyDeleteTimer];
}
}
}};
// The design of the setter logic below is taken from the DDAbstractLogger implementation.
// For documentation please refer to the DDAbstractLogger implementation.
if ([self isOnInternalLoggerQueue])
{
block();
}
else
{
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
dispatch_async(globalLoggingQueue, ^{
dispatch_async(loggerQueue, block);
});
}
}
- (BOOL)deleteOnEverySave
{
// The design of this method is taken from the DDAbstractLogger implementation.
// For extensive documentation please refer to the DDAbstractLogger implementation.
// Note: The internal implementation MUST access the colorsEnabled variable directly,
// This method is designed explicitly for external access.
//
// Using "self." syntax to go through this method will cause immediate deadlock.
// This is the intended result. Fix it by accessing the ivar directly.
// Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
__block BOOL result;
dispatch_sync(globalLoggingQueue, ^{
dispatch_sync(loggerQueue, ^{
result = deleteOnEverySave;
});
});
return result;
}
- (void)setDeleteOnEverySave:(BOOL)flag
{
dispatch_block_t block = ^{
deleteOnEverySave = flag;
};
// The design of the setter logic below is taken from the DDAbstractLogger implementation.
// For documentation please refer to the DDAbstractLogger implementation.
if ([self isOnInternalLoggerQueue])
{
block();
}
else
{
dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
dispatch_async(globalLoggingQueue, ^{
dispatch_async(loggerQueue, block);
});
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)savePendingLogEntries
{
dispatch_block_t block = ^{ @autoreleasepool {
[self performSaveAndSuspendSaveTimer];
}};
if ([self isOnInternalLoggerQueue])
block();
else
dispatch_async(loggerQueue, block);
}
- (void)deleteOldLogEntries
{
dispatch_block_t block = ^{ @autoreleasepool {
[self performDelete];
}};
if ([self isOnInternalLoggerQueue])
block();
else
dispatch_async(loggerQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark DDLogger
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)didAddLogger
{
// If you override me be sure to invoke [super didAddLogger];
[self createSuspendedSaveTimer];
[self createAndStartDeleteTimer];
}
- (void)willRemoveLogger
{
// If you override me be sure to invoke [super willRemoveLogger];
[self performSaveAndSuspendSaveTimer];
[self destroySaveTimer];
[self destroyDeleteTimer];
}
- (void)logMessage:(DDLogMessage *)logMessage
{
if ([self db_log:logMessage])
{
BOOL firstUnsavedEntry = (++unsavedCount == 1);
if ((unsavedCount >= saveThreshold) && (saveThreshold > 0))
{
[self performSaveAndSuspendSaveTimer];
}
else if (firstUnsavedEntry)
{
unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0);
[self updateAndResumeSaveTimer];
}
}
}
- (void)flush
{
// This method is invoked by DDLog's flushLog method.
//
// It is called automatically when the application quits,
// or if the developer invokes DDLog's flushLog method prior to crashing or something.
[self performSaveAndSuspendSaveTimer];
}
@end

View File

@@ -0,0 +1,334 @@
#import <Foundation/Foundation.h>
#import "DDLog.h"
@class DDLogFileInfo;
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides a logger to write log statements to a file.
**/
// Default configuration and safety/sanity values.
//
// maximumFileSize -> DEFAULT_LOG_MAX_FILE_SIZE
// rollingFrequency -> DEFAULT_LOG_ROLLING_FREQUENCY
// maximumNumberOfLogFiles -> DEFAULT_LOG_MAX_NUM_LOG_FILES
//
// You should carefully consider the proper configuration values for your application.
#define DEFAULT_LOG_MAX_FILE_SIZE (1024 * 1024) // 1 MB
#define DEFAULT_LOG_ROLLING_FREQUENCY (60 * 60 * 24) // 24 Hours
#define DEFAULT_LOG_MAX_NUM_LOG_FILES (5) // 5 Files
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The LogFileManager protocol is designed to allow you to control all aspects of your log files.
//
// The primary purpose of this is to allow you to do something with the log files after they have been rolled.
// Perhaps you want to compress them to save disk space.
// Perhaps you want to upload them to an FTP server.
// Perhaps you want to run some analytics on the file.
//
// A default LogFileManager is, of course, provided.
// The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property.
//
// This protocol provides various methods to fetch the list of log files.
//
// There are two variants: sorted and unsorted.
// If sorting is not necessary, the unsorted variant is obviously faster.
// The sorted variant will return an array sorted by when the log files were created,
// with the most recently created log file at index 0, and the oldest log file at the end of the array.
//
// You can fetch only the log file paths (full path including name), log file names (name only),
// or an array of DDLogFileInfo objects.
// The DDLogFileInfo class is documented below, and provides a handy wrapper that
// gives you easy access to various file attributes such as the creation date or the file size.
@protocol DDLogFileManager <NSObject>
@required
// Public properties
/**
* The maximum number of archived log files to keep on disk.
* For example, if this property is set to 3,
* then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk.
* Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted.
*
* You may optionally disable deleting old/rolled/archived log files by setting this property to zero.
**/
@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles;
// Public methods
- (NSString *)logsDirectory;
- (NSArray *)unsortedLogFilePaths;
- (NSArray *)unsortedLogFileNames;
- (NSArray *)unsortedLogFileInfos;
- (NSArray *)sortedLogFilePaths;
- (NSArray *)sortedLogFileNames;
- (NSArray *)sortedLogFileInfos;
// Private methods (only to be used by DDFileLogger)
- (NSString *)createNewLogFile;
@optional
// Notifications from DDFileLogger
- (void)didArchiveLogFile:(NSString *)logFilePath;
- (void)didRollAndArchiveLogFile:(NSString *)logFilePath;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Default log file manager.
*
* All log files are placed inside the logsDirectory.
* If a specific logsDirectory isn't specified, the default directory is used.
* On Mac, this is in ~/Library/Logs/<Application Name>.
* On iPhone, this is in ~/Library/Caches/Logs.
*
* Log files are named "log-<uuid>.txt",
* where uuid is a 6 character hexadecimal consisting of the set [0123456789ABCDEF].
*
* Archived log files are automatically deleted according to the maximumNumberOfLogFiles property.
**/
@interface DDLogFileManagerDefault : NSObject <DDLogFileManager>
{
NSUInteger maximumNumberOfLogFiles;
NSString *_logsDirectory;
}
- (id)init;
- (id)initWithLogsDirectory:(NSString *)logsDirectory;
/* Inherited from DDLogFileManager protocol:
@property (readwrite, assign) NSUInteger maximumNumberOfLogFiles;
- (NSString *)logsDirectory;
- (NSArray *)unsortedLogFilePaths;
- (NSArray *)unsortedLogFileNames;
- (NSArray *)unsortedLogFileInfos;
- (NSArray *)sortedLogFilePaths;
- (NSArray *)sortedLogFileNames;
- (NSArray *)sortedLogFileInfos;
*/
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Most users will want file log messages to be prepended with the date and time.
* Rather than forcing the majority of users to write their own formatter,
* we will supply a logical default formatter.
* Users can easily replace this formatter with their own by invoking the setLogFormatter method.
* It can also be removed by calling setLogFormatter, and passing a nil parameter.
*
* In addition to the convenience of having a logical default formatter,
* it will also provide a template that makes it easy for developers to copy and change.
**/
@interface DDLogFileFormatterDefault : NSObject <DDLogFormatter>
{
NSDateFormatter *dateFormatter;
}
- (id)init;
- (id)initWithDateFormatter:(NSDateFormatter *)dateFormatter;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface DDFileLogger : DDAbstractLogger <DDLogger>
{
__strong id <DDLogFileManager> logFileManager;
DDLogFileInfo *currentLogFileInfo;
NSFileHandle *currentLogFileHandle;
dispatch_source_t rollingTimer;
unsigned long long maximumFileSize;
NSTimeInterval rollingFrequency;
}
- (id)init;
- (id)initWithLogFileManager:(id <DDLogFileManager>)logFileManager;
/**
* Log File Rolling:
*
* maximumFileSize:
* The approximate maximum size to allow log files to grow.
* If a log file is larger than this value after a log statement is appended,
* then the log file is rolled.
*
* rollingFrequency
* How often to roll the log file.
* The frequency is given as an NSTimeInterval, which is a double that specifies the interval in seconds.
* Once the log file gets to be this old, it is rolled.
*
* Both the maximumFileSize and the rollingFrequency are used to manage rolling.
* Whichever occurs first will cause the log file to be rolled.
*
* For example:
* The rollingFrequency is 24 hours,
* but the log file surpasses the maximumFileSize after only 20 hours.
* The log file will be rolled at that 20 hour mark.
* A new log file will be created, and the 24 hour timer will be restarted.
*
* You may optionally disable rolling due to filesize by setting maximumFileSize to zero.
* If you do so, rolling is based solely on rollingFrequency.
*
* You may optionally disable rolling due to time by setting rollingFrequency to zero (or any non-positive number).
* If you do so, rolling is based solely on maximumFileSize.
*
* If you disable both maximumFileSize and rollingFrequency, then the log file won't ever be rolled.
* This is strongly discouraged.
**/
@property (readwrite, assign) unsigned long long maximumFileSize;
@property (readwrite, assign) NSTimeInterval rollingFrequency;
/**
* The DDLogFileManager instance can be used to retrieve the list of log files,
* and configure the maximum number of archived log files to keep.
*
* @see DDLogFileManager.maximumNumberOfLogFiles
**/
@property (strong, nonatomic, readonly) id <DDLogFileManager> logFileManager;
// You can optionally force the current log file to be rolled with this method.
- (void)rollLogFile;
// Inherited from DDAbstractLogger
// - (id <DDLogFormatter>)logFormatter;
// - (void)setLogFormatter:(id <DDLogFormatter>)formatter;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* DDLogFileInfo is a simple class that provides access to various file attributes.
* It provides good performance as it only fetches the information if requested,
* and it caches the information to prevent duplicate fetches.
*
* It was designed to provide quick snapshots of the current state of log files,
* and to help sort log files in an array.
*
* This class does not monitor the files, or update it's cached attribute values if the file changes on disk.
* This is not what the class was designed for.
*
* If you absolutely must get updated values,
* you can invoke the reset method which will clear the cache.
**/
@interface DDLogFileInfo : NSObject
{
__strong NSString *filePath;
__strong NSString *fileName;
__strong NSDictionary *fileAttributes;
__strong NSDate *creationDate;
__strong NSDate *modificationDate;
unsigned long long fileSize;
}
@property (strong, nonatomic, readonly) NSString *filePath;
@property (strong, nonatomic, readonly) NSString *fileName;
@property (strong, nonatomic, readonly) NSDictionary *fileAttributes;
@property (strong, nonatomic, readonly) NSDate *creationDate;
@property (strong, nonatomic, readonly) NSDate *modificationDate;
@property (nonatomic, readonly) unsigned long long fileSize;
@property (nonatomic, readonly) NSTimeInterval age;
@property (nonatomic, readwrite) BOOL isArchived;
+ (id)logFileWithPath:(NSString *)filePath;
- (id)initWithFilePath:(NSString *)filePath;
- (void)reset;
- (void)renameFile:(NSString *)newFileName;
#if TARGET_IPHONE_SIMULATOR
// So here's the situation.
// Extended attributes are perfect for what we're trying to do here (marking files as archived).
// This is exactly what extended attributes were designed for.
//
// But Apple screws us over on the simulator.
// Everytime you build-and-go, they copy the application into a new folder on the hard drive,
// and as part of the process they strip extended attributes from our log files.
// Normally, a copy of a file preserves extended attributes.
// So obviously Apple has gone to great lengths to piss us off.
//
// Thus we use a slightly different tactic for marking log files as archived in the simulator.
// That way it "just works" and there's no confusion when testing.
//
// The difference in method names is indicative of the difference in functionality.
// On the simulator we add an attribute by appending a filename extension.
//
// For example:
// log-ABC123.txt -> log-ABC123.archived.txt
- (BOOL)hasExtensionAttributeWithName:(NSString *)attrName;
- (void)addExtensionAttributeWithName:(NSString *)attrName;
- (void)removeExtensionAttributeWithName:(NSString *)attrName;
#else
// Normal use of extended attributes used everywhere else,
// such as on Macs and on iPhone devices.
- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName;
- (void)addExtendedAttributeWithName:(NSString *)attrName;
- (void)removeExtendedAttributeWithName:(NSString *)attrName;
#endif
- (NSComparisonResult)reverseCompareByCreationDate:(DDLogFileInfo *)another;
- (NSComparisonResult)reverseCompareByModificationDate:(DDLogFileInfo *)another;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,601 @@
#import <Foundation/Foundation.h>
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
* Otherwise, here is a quick refresher.
* There are three steps to using the macros:
*
* Step 1:
* Import the header in your implementation file:
*
* #import "DDLog.h"
*
* Step 2:
* Define your logging level in your implementation file:
*
* // Log levels: off, error, warn, info, verbose
* static const int ddLogLevel = LOG_LEVEL_VERBOSE;
*
* Step 3:
* Replace your NSLog statements with DDLog statements according to the severity of the message.
*
* NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!");
*
* DDLog works exactly the same as NSLog.
* This means you can pass it multiple variables just like NSLog.
**/
@class DDLogMessage;
@protocol DDLogger;
@protocol DDLogFormatter;
/**
* This is the single macro that all other macros below compile into.
* This big multiline macro makes all the other macros easier to read.
**/
#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \
[DDLog log:isAsynchronous \
level:lvl \
flag:flg \
context:ctx \
file:__FILE__ \
function:fnct \
line:__LINE__ \
tag:atag \
format:(frmt), ##__VA_ARGS__]
/**
* Define the Objective-C and C versions of the macro.
* These automatically inject the proper function name for either an objective-c method or c function.
*
* We also define shorthand versions for asynchronous and synchronous logging.
**/
#define LOG_OBJC_MACRO(async, lvl, flg, ctx, frmt, ...) \
LOG_MACRO(async, lvl, flg, ctx, nil, sel_getName(_cmd), frmt, ##__VA_ARGS__)
#define LOG_C_MACRO(async, lvl, flg, ctx, frmt, ...) \
LOG_MACRO(async, lvl, flg, ctx, nil, __FUNCTION__, frmt, ##__VA_ARGS__)
#define SYNC_LOG_OBJC_MACRO(lvl, flg, ctx, frmt, ...) \
LOG_OBJC_MACRO( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define ASYNC_LOG_OBJC_MACRO(lvl, flg, ctx, frmt, ...) \
LOG_OBJC_MACRO(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define SYNC_LOG_C_MACRO(lvl, flg, ctx, frmt, ...) \
LOG_C_MACRO( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define ASYNC_LOG_C_MACRO(lvl, flg, ctx, frmt, ...) \
LOG_C_MACRO(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__)
/**
* Define version of the macro that only execute if the logLevel is above the threshold.
* The compiled versions essentially look like this:
*
* if (logFlagForThisLogMsg & ddLogLevel) { execute log message }
*
* As shown further below, Lumberjack actually uses a bitmask as opposed to primitive log levels.
* This allows for a great amount of flexibility and some pretty advanced fine grained logging techniques.
*
* Note that when compiler optimizations are enabled (as they are for your release builds),
* the log messages above your logging threshold will automatically be compiled out.
*
* (If the compiler sees ddLogLevel declared as a constant, the compiler simply checks to see if the 'if' statement
* would execute, and if not it strips it from the binary.)
*
* We also define shorthand versions for asynchronous and synchronous logging.
**/
#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \
do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0)
#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \
LOG_MAYBE(async, lvl, flg, ctx, sel_getName(_cmd), frmt, ##__VA_ARGS__)
#define LOG_C_MAYBE(async, lvl, flg, ctx, frmt, ...) \
LOG_MAYBE(async, lvl, flg, ctx, __FUNCTION__, frmt, ##__VA_ARGS__)
#define SYNC_LOG_OBJC_MAYBE(lvl, flg, ctx, frmt, ...) \
LOG_OBJC_MAYBE( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define ASYNC_LOG_OBJC_MAYBE(lvl, flg, ctx, frmt, ...) \
LOG_OBJC_MAYBE(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define SYNC_LOG_C_MAYBE(lvl, flg, ctx, frmt, ...) \
LOG_C_MAYBE( NO, lvl, flg, ctx, frmt, ##__VA_ARGS__)
#define ASYNC_LOG_C_MAYBE(lvl, flg, ctx, frmt, ...) \
LOG_C_MAYBE(YES, lvl, flg, ctx, frmt, ##__VA_ARGS__)
/**
* Define versions of the macros that also accept tags.
*
* The DDLogMessage object includes a 'tag' ivar that may be used for a variety of purposes.
* It may be used to pass custom information to loggers or formatters.
* Or it may be used by 3rd party extensions to the framework.
*
* Thes macros just make it a little easier to extend logging functionality.
**/
#define LOG_OBJC_TAG_MACRO(async, lvl, flg, ctx, tag, frmt, ...) \
LOG_MACRO(async, lvl, flg, ctx, tag, sel_getName(_cmd), frmt, ##__VA_ARGS__)
#define LOG_C_TAG_MACRO(async, lvl, flg, ctx, tag, frmt, ...) \
LOG_MACRO(async, lvl, flg, ctx, tag, __FUNCTION__, frmt, ##__VA_ARGS__)
#define LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, fnct, frmt, ...) \
do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, tag, fnct, frmt, ##__VA_ARGS__); } while(0)
#define LOG_OBJC_TAG_MAYBE(async, lvl, flg, ctx, tag, frmt, ...) \
LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, sel_getName(_cmd), frmt, ##__VA_ARGS__)
#define LOG_C_TAG_MAYBE(async, lvl, flg, ctx, tag, frmt, ...) \
LOG_TAG_MAYBE(async, lvl, flg, ctx, tag, __FUNCTION__, frmt, ##__VA_ARGS__)
/**
* Define the standard options.
*
* We default to only 4 levels because it makes it easier for beginners
* to make the transition to a logging framework.
*
* More advanced users may choose to completely customize the levels (and level names) to suite their needs.
* For more information on this see the "Custom Log Levels" page:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomLogLevels
*
* Advanced users may also notice that we're using a bitmask.
* This is to allow for custom fine grained logging:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/FineGrainedLogging
*
* -- Flags --
*
* Typically you will use the LOG_LEVELS (see below), but the flags may be used directly in certain situations.
* For example, say you have a lot of warning log messages, and you wanted to disable them.
* However, you still needed to see your error and info log messages.
* You could accomplish that with the following:
*
* static const int ddLogLevel = LOG_FLAG_ERROR | LOG_FLAG_INFO;
*
* Flags may also be consulted when writing custom log formatters,
* as the DDLogMessage class captures the individual flag that caused the log message to fire.
*
* -- Levels --
*
* Log levels are simply the proper bitmask of the flags.
*
* -- Booleans --
*
* The booleans may be used when your logging code involves more than one line.
* For example:
*
* if (LOG_VERBOSE) {
* for (id sprocket in sprockets)
* DDLogVerbose(@"sprocket: %@", [sprocket description])
* }
*
* -- Async --
*
* Defines the default asynchronous options.
* The default philosophy for asynchronous logging is very simple:
*
* Log messages with errors should be executed synchronously.
* After all, an error just occurred. The application could be unstable.
*
* All other log messages, such as debug output, are executed asynchronously.
* After all, if it wasn't an error, then it was just informational output,
* or something the application was easily able to recover from.
*
* -- Changes --
*
* You are strongly discouraged from modifying this file.
* If you do, you make it more difficult on yourself to merge future bug fixes and improvements from the project.
* Instead, create your own MyLogging.h or ApplicationNameLogging.h or CompanyLogging.h
*
* For an example of customizing your logging experience, see the "Custom Log Levels" page:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomLogLevels
**/
#define LOG_FLAG_ERROR (1 << 0) // 0...0001
#define LOG_FLAG_WARN (1 << 1) // 0...0010
#define LOG_FLAG_INFO (1 << 2) // 0...0100
#define LOG_FLAG_VERBOSE (1 << 3) // 0...1000
#define LOG_LEVEL_OFF 0
#define LOG_LEVEL_ERROR (LOG_FLAG_ERROR) // 0...0001
#define LOG_LEVEL_WARN (LOG_FLAG_ERROR | LOG_FLAG_WARN) // 0...0011
#define LOG_LEVEL_INFO (LOG_FLAG_ERROR | LOG_FLAG_WARN | LOG_FLAG_INFO) // 0...0111
#define LOG_LEVEL_VERBOSE (LOG_FLAG_ERROR | LOG_FLAG_WARN | LOG_FLAG_INFO | LOG_FLAG_VERBOSE) // 0...1111
#define LOG_ERROR (ddLogLevel & LOG_FLAG_ERROR)
#define LOG_WARN (ddLogLevel & LOG_FLAG_WARN)
#define LOG_INFO (ddLogLevel & LOG_FLAG_INFO)
#define LOG_VERBOSE (ddLogLevel & LOG_FLAG_VERBOSE)
#define LOG_ASYNC_ENABLED YES
#define LOG_ASYNC_ERROR ( NO && LOG_ASYNC_ENABLED)
#define LOG_ASYNC_WARN (YES && LOG_ASYNC_ENABLED)
#define LOG_ASYNC_INFO (YES && LOG_ASYNC_ENABLED)
#define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED)
#define DDLogError(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_ERROR, ddLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__)
#define DDLogWarn(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_WARN, ddLogLevel, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__)
#define DDLogInfo(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_INFO, ddLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__)
#define DDLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, ddLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__)
#define DDLogCError(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_ERROR, ddLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__)
#define DDLogCWarn(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_WARN, ddLogLevel, LOG_FLAG_WARN, 0, frmt, ##__VA_ARGS__)
#define DDLogCInfo(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_INFO, ddLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__)
#define DDLogCVerbose(frmt, ...) LOG_C_MAYBE(LOG_ASYNC_VERBOSE, ddLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__)
/**
* The THIS_FILE macro gives you an NSString of the file name.
* For simplicity and clarity, the file name does not include the full path or file extension.
*
* For example: DDLogWarn(@"%@: Unable to find thingy", THIS_FILE) -> @"MyViewController: Unable to find thingy"
**/
NSString *DDExtractFileNameWithoutExtension(const char *filePath, BOOL copy);
#define THIS_FILE (DDExtractFileNameWithoutExtension(__FILE__, NO))
/**
* The THIS_METHOD macro gives you the name of the current objective-c method.
*
* For example: DDLogWarn(@"%@ - Requires non-nil strings", THIS_METHOD) -> @"setMake:model: requires non-nil strings"
*
* Note: This does NOT work in straight C functions (non objective-c).
* Instead you should use the predefined __FUNCTION__ macro.
**/
#define THIS_METHOD NSStringFromSelector(_cmd)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@interface DDLog : NSObject
/**
* Provides access to the underlying logging queue.
* This may be helpful to Logger classes for things like thread synchronization.
**/
+ (dispatch_queue_t)loggingQueue;
/**
* Logging Primitive.
*
* This method is used by the macros above.
* It is suggested you stick with the macros as they're easier to use.
**/
+ (void)log:(BOOL)synchronous
level:(int)level
flag:(int)flag
context:(int)context
file:(const char *)file
function:(const char *)function
line:(int)line
tag:(id)tag
format:(NSString *)format, ... __attribute__ ((format (__NSString__, 9, 10)));
/**
* Logging Primitive.
*
* This method can be used if you have a prepared va_list.
**/
+ (void)log:(BOOL)asynchronous
level:(int)level
flag:(int)flag
context:(int)context
file:(const char *)file
function:(const char *)function
line:(int)line
tag:(id)tag
format:(NSString *)format
args:(va_list)argList;
/**
* Since logging can be asynchronous, there may be times when you want to flush the logs.
* The framework invokes this automatically when the application quits.
**/
+ (void)flushLog;
/**
* Loggers
*
* If you want your log statements to go somewhere,
* you should create and add a logger.
**/
+ (void)addLogger:(id <DDLogger>)logger;
+ (void)removeLogger:(id <DDLogger>)logger;
+ (void)removeAllLoggers;
/**
* Registered Dynamic Logging
*
* These methods allow you to obtain a list of classes that are using registered dynamic logging,
* and also provides methods to get and set their log level during run time.
**/
+ (NSArray *)registeredClasses;
+ (NSArray *)registeredClassNames;
+ (int)logLevelForClass:(Class)aClass;
+ (int)logLevelForClassWithName:(NSString *)aClassName;
+ (void)setLogLevel:(int)logLevel forClass:(Class)aClass;
+ (void)setLogLevel:(int)logLevel forClassWithName:(NSString *)aClassName;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@protocol DDLogger <NSObject>
@required
- (void)logMessage:(DDLogMessage *)logMessage;
/**
* Formatters may optionally be added to any logger.
*
* If no formatter is set, the logger simply logs the message as it is given in logMessage,
* or it may use its own built in formatting style.
**/
- (id <DDLogFormatter>)logFormatter;
- (void)setLogFormatter:(id <DDLogFormatter>)formatter;
@optional
/**
* Since logging is asynchronous, adding and removing loggers is also asynchronous.
* In other words, the loggers are added and removed at appropriate times with regards to log messages.
*
* - Loggers will not receive log messages that were executed prior to when they were added.
* - Loggers will not receive log messages that were executed after they were removed.
*
* These methods are executed in the logging thread/queue.
* This is the same thread/queue that will execute every logMessage: invocation.
* Loggers may use these methods for thread synchronization or other setup/teardown tasks.
**/
- (void)didAddLogger;
- (void)willRemoveLogger;
/**
* Some loggers may buffer IO for optimization purposes.
* For example, a database logger may only save occasionaly as the disk IO is slow.
* In such loggers, this method should be implemented to flush any pending IO.
*
* This allows invocations of DDLog's flushLog method to be propogated to loggers that need it.
*
* Note that DDLog's flushLog method is invoked automatically when the application quits,
* and it may be also invoked manually by the developer prior to application crashes, or other such reasons.
**/
- (void)flush;
/**
* Each logger is executed concurrently with respect to the other loggers.
* Thus, a dedicated dispatch queue is used for each logger.
* Logger implementations may optionally choose to provide their own dispatch queue.
**/
- (dispatch_queue_t)loggerQueue;
/**
* If the logger implementation does not choose to provide its own queue,
* one will automatically be created for it.
* The created queue will receive its name from this method.
* This may be helpful for debugging or profiling reasons.
**/
- (NSString *)loggerName;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@protocol DDLogFormatter <NSObject>
@required
/**
* Formatters may optionally be added to any logger.
* This allows for increased flexibility in the logging environment.
* For example, log messages for log files may be formatted differently than log messages for the console.
*
* For more information about formatters, see the "Custom Formatters" page:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters
*
* The formatter may also optionally filter the log message by returning nil,
* in which case the logger will not log the message.
**/
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage;
@optional
/**
* A single formatter instance can be added to multiple loggers.
* These methods provides hooks to notify the formatter of when it's added/removed.
*
* This is primarily for thread-safety.
* If a formatter is explicitly not thread-safe, it may wish to throw an exception if added to multiple loggers.
* Or if a formatter has potentially thread-unsafe code (e.g. NSDateFormatter),
* it could possibly use these hooks to switch to thread-safe versions of the code.
**/
- (void)didAddToLogger:(id <DDLogger>)logger;
- (void)willRemoveFromLogger:(id <DDLogger>)logger;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@protocol DDRegisteredDynamicLogging
/**
* Implement these methods to allow a file's log level to be managed from a central location.
*
* This is useful if you'd like to be able to change log levels for various parts
* of your code from within the running application.
*
* Imagine pulling up the settings for your application,
* and being able to configure the logging level on a per file basis.
*
* The implementation can be very straight-forward:
*
* + (int)ddLogLevel
* {
* return ddLogLevel;
* }
*
* + (void)ddSetLogLevel:(int)logLevel
* {
* ddLogLevel = logLevel;
* }
**/
+ (int)ddLogLevel;
+ (void)ddSetLogLevel:(int)logLevel;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The DDLogMessage class encapsulates information about the log message.
* If you write custom loggers or formatters, you will be dealing with objects of this class.
**/
enum {
DDLogMessageCopyFile = 1 << 0,
DDLogMessageCopyFunction = 1 << 1
};
typedef int DDLogMessageOptions;
@interface DDLogMessage : NSObject
{
// The public variables below can be accessed directly (for speed).
// For example: logMessage->logLevel
@public
int logLevel;
int logFlag;
int logContext;
NSString *logMsg;
NSDate *timestamp;
char *file;
char *function;
int lineNumber;
mach_port_t machThreadID;
char *queueLabel;
NSString *threadName;
// For 3rd party extensions to the framework, where flags and contexts aren't enough.
id tag;
// For 3rd party extensions that manually create DDLogMessage instances.
DDLogMessageOptions options;
}
/**
* Standard init method for a log message object.
* Used by the logging primitives. (And the macros use the logging primitives.)
*
* If you find need to manually create logMessage objects, there is one thing you should be aware of:
*
* If no flags are passed, the method expects the file and function parameters to be string literals.
* That is, it expects the given strings to exist for the duration of the object's lifetime,
* and it expects the given strings to be immutable.
* In other words, it does not copy these strings, it simply points to them.
* This is due to the fact that __FILE__ and __FUNCTION__ are usually used to specify these parameters,
* so it makes sense to optimize and skip the unnecessary allocations.
* However, if you need them to be copied you may use the options parameter to specify this.
* Options is a bitmask which supports DDLogMessageCopyFile and DDLogMessageCopyFunction.
**/
- (id)initWithLogMsg:(NSString *)logMsg
level:(int)logLevel
flag:(int)logFlag
context:(int)logContext
file:(const char *)file
function:(const char *)function
line:(int)line
tag:(id)tag
options:(DDLogMessageOptions)optionsMask;
/**
* Returns the threadID as it appears in NSLog.
* That is, it is a hexadecimal value which is calculated from the machThreadID.
**/
- (NSString *)threadID;
/**
* Convenience property to get just the file name, as the file variable is generally the full file path.
* This method does not include the file extension, which is generally unwanted for logging purposes.
**/
- (NSString *)fileName;
/**
* Returns the function variable in NSString form.
**/
- (NSString *)methodName;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The DDLogger protocol specifies that an optional formatter can be added to a logger.
* Most (but not all) loggers will want to support formatters.
*
* However, writting getters and setters in a thread safe manner,
* while still maintaining maximum speed for the logging process, is a difficult task.
*
* To do it right, the implementation of the getter/setter has strict requiremenets:
* - Must NOT require the logMessage method to acquire a lock.
* - Must NOT require the logMessage method to access an atomic property (also a lock of sorts).
*
* To simplify things, an abstract logger is provided that implements the getter and setter.
*
* Logger implementations may simply extend this class,
* and they can ACCESS THE FORMATTER VARIABLE DIRECTLY from within their logMessage method!
**/
@interface DDAbstractLogger : NSObject <DDLogger>
{
id <DDLogFormatter> formatter;
dispatch_queue_t loggerQueue;
}
- (id <DDLogFormatter>)logFormatter;
- (void)setLogFormatter:(id <DDLogFormatter>)formatter;
// For thread-safety assertions
- (BOOL)isOnGlobalLoggingQueue;
- (BOOL)isOnInternalLoggerQueue;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIColor.h>
#else
#import <AppKit/NSColor.h>
#endif
#import "DDLog.h"
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides a logger for Terminal output or Xcode console output,
* depending on where you are running your code.
*
* As described in the "Getting Started" page,
* the traditional NSLog() function directs it's output to two places:
*
* - Apple System Log (so it shows up in Console.app)
* - StdErr (if stderr is a TTY, so log statements show up in Xcode console)
*
* To duplicate NSLog() functionality you can simply add this logger and an asl logger.
* However, if you instead choose to use file logging (for faster performance),
* you may choose to use only a file logger and a tty logger.
**/
@interface DDTTYLogger : DDAbstractLogger <DDLogger>
{
NSCalendar *calendar;
NSUInteger calendarUnitFlags;
NSString *appName;
char *app;
size_t appLen;
NSString *processID;
char *pid;
size_t pidLen;
BOOL colorsEnabled;
NSMutableArray *colorProfilesArray;
NSMutableDictionary *colorProfilesDict;
}
+ (DDTTYLogger *)sharedInstance;
/* Inherited from the DDLogger protocol:
*
* Formatters may optionally be added to any logger.
*
* If no formatter is set, the logger simply logs the message as it is given in logMessage,
* or it may use its own built in formatting style.
*
* More information about formatters can be found here:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters
*
* The actual implementation of these methods is inherited from DDAbstractLogger.
- (id <DDLogFormatter>)logFormatter;
- (void)setLogFormatter:(id <DDLogFormatter>)formatter;
*/
/**
* Want to use different colors for different log levels?
* Enable this property.
*
* If you run the application via the Terminal (not Xcode),
* the logger will map colors to xterm-256color or xterm-color (if available).
*
* Xcode does NOT natively support colors in the Xcode debugging console.
* You'll need to install the XcodeColors plugin to see colors in the Xcode console.
* https://github.com/robbiehanson/XcodeColors
*
* The default value if NO.
**/
@property (readwrite, assign) BOOL colorsEnabled;
/**
* The default color set (foregroundColor, backgroundColor) is:
*
* - LOG_FLAG_ERROR = (red, nil)
* - LOG_FLAG_WARN = (orange, nil)
*
* You can customize the colors however you see fit.
* Please note that you are passing a flag, NOT a level.
*
* GOOD : [ttyLogger setForegroundColor:pink backgroundColor:nil forFlag:LOG_FLAG_INFO]; // <- Good :)
* BAD : [ttyLogger setForegroundColor:pink backgroundColor:nil forFlag:LOG_LEVEL_INFO]; // <- BAD! :(
*
* LOG_FLAG_INFO = 0...00100
* LOG_LEVEL_INFO = 0...00111 <- Would match LOG_FLAG_INFO and LOG_FLAG_WARN and LOG_FLAG_ERROR
*
* If you run the application within Xcode, then the XcodeColors plugin is required.
*
* If you run the application from a shell, then DDTTYLogger will automatically map the given color to
* the closest available color. (xterm-256color or xterm-color which have 256 and 16 supported colors respectively.)
*
* This method invokes setForegroundColor:backgroundColor:forFlag:context: and passes the default context (0).
**/
#if TARGET_OS_IPHONE
- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forFlag:(int)mask;
#else
- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forFlag:(int)mask;
#endif
/**
* Just like setForegroundColor:backgroundColor:flag, but allows you to specify a particular logging context.
*
* A logging context is often used to identify log messages coming from a 3rd party framework,
* although logging context's can be used for many different functions.
*
* Logging context's are explained in further detail here:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomContext
**/
#if TARGET_OS_IPHONE
- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forFlag:(int)mask context:(int)ctxt;
#else
- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forFlag:(int)mask context:(int)ctxt;
#endif
/**
* Similar to the methods above, but allows you to map DDLogMessage->tag to a particular color profile.
* For example, you could do something like this:
*
* static NSString *const PurpleTag = @"PurpleTag";
*
* #define DDLogPurple(frmt, ...) LOG_OBJC_TAG_MACRO(NO, 0, 0, 0, PurpleTag, frmt, ##__VA_ARGS__)
*
* And then in your applicationDidFinishLaunching, or wherever you configure Lumberjack:
*
* #if TARGET_OS_IPHONE
* UIColor *purple = [UIColor colorWithRed:(64/255.0) green:(0/255.0) blue:(128/255.0) alpha:1.0];
* #else
* NSColor *purple = [NSColor colorWithCalibratedRed:(64/255.0) green:(0/255.0) blue:(128/255.0) alpha:1.0];
*
* [[DDTTYLogger sharedInstance] setForegroundColor:purple backgroundColor:nil forTag:PurpleTag];
* [DDLog addLogger:[DDTTYLogger sharedInstance]];
*
* This would essentially give you a straight NSLog replacement that prints in purple:
*
* DDLogPurple(@"I'm a purple log message!");
**/
#if TARGET_OS_IPHONE
- (void)setForegroundColor:(UIColor *)txtColor backgroundColor:(UIColor *)bgColor forTag:(id <NSCopying>)tag;
#else
- (void)setForegroundColor:(NSColor *)txtColor backgroundColor:(NSColor *)bgColor forTag:(id <NSCopying>)tag;
#endif
/**
* Clearing color profiles.
**/
- (void)clearColorsForFlag:(int)mask;
- (void)clearColorsForFlag:(int)mask context:(int)context;
- (void)clearColorsForTag:(id <NSCopying>)tag;
- (void)clearColorsForAllFlags;
- (void)clearColorsForAllTags;
- (void)clearAllColors;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
#import <Foundation/Foundation.h>
#import "DDLog.h"
@class ContextFilterLogFormatter;
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" page.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides a log formatter that filters log statements from a logging context not on the whitelist.
*
* A log formatter can be added to any logger to format and/or filter its output.
* You can learn more about log formatters here:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters
*
* You can learn more about logging context's here:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomContext
*
* But here's a quick overview / refresher:
*
* Every log statement has a logging context.
* These come from the underlying logging macros defined in DDLog.h.
* The default logging context is zero.
* You can define multiple logging context's for use in your application.
* For example, logically separate parts of your app each have a different logging context.
* Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context.
**/
@interface ContextWhitelistFilterLogFormatter : NSObject <DDLogFormatter>
- (id)init;
- (void)addToWhitelist:(int)loggingContext;
- (void)removeFromWhitelist:(int)loggingContext;
- (NSArray *)whitelist;
- (BOOL)isOnWhitelist:(int)loggingContext;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* This class provides a log formatter that filters log statements from a logging context on the blacklist.
**/
@interface ContextBlacklistFilterLogFormatter : NSObject <DDLogFormatter>
- (id)init;
- (void)addToBlacklist:(int)loggingContext;
- (void)removeFromBlacklist:(int)loggingContext;
- (NSArray *)blacklist;
- (BOOL)isOnBlacklist:(int)loggingContext;
@end

View File

@@ -0,0 +1,191 @@
#import "ContextFilterLogFormatter.h"
#import <libkern/OSAtomic.h>
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
**/
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@interface LoggingContextSet : NSObject
- (void)addToSet:(int)loggingContext;
- (void)removeFromSet:(int)loggingContext;
- (NSArray *)currentSet;
- (BOOL)isInSet:(int)loggingContext;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation ContextWhitelistFilterLogFormatter
{
LoggingContextSet *contextSet;
}
- (id)init
{
if ((self = [super init]))
{
contextSet = [[LoggingContextSet alloc] init];
}
return self;
}
- (void)addToWhitelist:(int)loggingContext
{
[contextSet addToSet:loggingContext];
}
- (void)removeFromWhitelist:(int)loggingContext
{
[contextSet removeFromSet:loggingContext];
}
- (NSArray *)whitelist
{
return [contextSet currentSet];
}
- (BOOL)isOnWhitelist:(int)loggingContext
{
return [contextSet isInSet:loggingContext];
}
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
if ([self isOnWhitelist:logMessage->logContext])
return logMessage->logMsg;
else
return nil;
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation ContextBlacklistFilterLogFormatter
{
LoggingContextSet *contextSet;
}
- (id)init
{
if ((self = [super init]))
{
contextSet = [[LoggingContextSet alloc] init];
}
return self;
}
- (void)addToBlacklist:(int)loggingContext
{
[contextSet addToSet:loggingContext];
}
- (void)removeFromBlacklist:(int)loggingContext
{
[contextSet removeFromSet:loggingContext];
}
- (NSArray *)blacklist
{
return [contextSet currentSet];
}
- (BOOL)isOnBlacklist:(int)loggingContext
{
return [contextSet isInSet:loggingContext];
}
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
if ([self isOnBlacklist:logMessage->logContext])
return nil;
else
return logMessage->logMsg;
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation LoggingContextSet
{
OSSpinLock lock;
NSMutableSet *set;
}
- (id)init
{
if ((self = [super init]))
{
set = [[NSMutableSet alloc] init];
}
return self;
}
- (void)addToSet:(int)loggingContext
{
OSSpinLockLock(&lock);
{
[set addObject:@(loggingContext)];
}
OSSpinLockUnlock(&lock);
}
- (void)removeFromSet:(int)loggingContext
{
OSSpinLockLock(&lock);
{
[set removeObject:@(loggingContext)];
}
OSSpinLockUnlock(&lock);
}
- (NSArray *)currentSet
{
NSArray *result = nil;
OSSpinLockLock(&lock);
{
result = [set allObjects];
}
OSSpinLockUnlock(&lock);
return result;
}
- (BOOL)isInSet:(int)loggingContext
{
BOOL result = NO;
OSSpinLockLock(&lock);
{
result = [set containsObject:@(loggingContext)];
}
OSSpinLockUnlock(&lock);
return result;
}
@end

View File

@@ -0,0 +1,116 @@
#import <Foundation/Foundation.h>
#import <libkern/OSAtomic.h>
#import "DDLog.h"
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" page.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
*
*
* This class provides a log formatter that prints the dispatch_queue label instead of the mach_thread_id.
*
* A log formatter can be added to any logger to format and/or filter its output.
* You can learn more about log formatters here:
* https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters
*
* A typical NSLog (or DDTTYLogger) prints detailed info as [<process_id>:<thread_id>].
* For example:
*
* 2011-10-17 20:21:45.435 AppName[19928:5207] Your log message here
*
* Where:
* - 19928 = process id
* - 5207 = thread id (mach_thread_id printed in hex)
*
* When using grand central dispatch (GCD), this information is less useful.
* This is because a single serial dispatch queue may be run on any thread from an internally managed thread pool.
* For example:
*
* 2011-10-17 20:32:31.111 AppName[19954:4d07] Message from my_serial_dispatch_queue
* 2011-10-17 20:32:31.112 AppName[19954:5207] Message from my_serial_dispatch_queue
* 2011-10-17 20:32:31.113 AppName[19954:2c55] Message from my_serial_dispatch_queue
*
* This formatter allows you to replace the standard [box:info] with the dispatch_queue name.
* For example:
*
* 2011-10-17 20:32:31.111 AppName[img-scaling] Message from my_serial_dispatch_queue
* 2011-10-17 20:32:31.112 AppName[img-scaling] Message from my_serial_dispatch_queue
* 2011-10-17 20:32:31.113 AppName[img-scaling] Message from my_serial_dispatch_queue
*
* If the dispatch_queue doesn't have a set name, then it falls back to the thread name.
* If the current thread doesn't have a set name, then it falls back to the mach_thread_id in hex (like normal).
*
* Note: If manually creating your own background threads (via NSThread/alloc/init or NSThread/detachNeThread),
* you can use [[NSThread currentThread] setName:(NSString *)].
**/
@interface DispatchQueueLogFormatter : NSObject <DDLogFormatter> {
@protected
NSString *dateFormatString;
}
/**
* Standard init method.
* Configure using properties as desired.
**/
- (id)init;
/**
* The minQueueLength restricts the minimum size of the [detail box].
* If the minQueueLength is set to 0, there is no restriction.
*
* For example, say a dispatch_queue has a label of "diskIO":
*
* If the minQueueLength is 0: [diskIO]
* If the minQueueLength is 4: [diskIO]
* If the minQueueLength is 5: [diskIO]
* If the minQueueLength is 6: [diskIO]
* If the minQueueLength is 7: [diskIO ]
* If the minQueueLength is 8: [diskIO ]
*
* The default minQueueLength is 0 (no minimum, so [detail box] won't be padded).
*
* If you want every [detail box] to have the exact same width,
* set both minQueueLength and maxQueueLength to the same value.
**/
@property (assign) NSUInteger minQueueLength;
/**
* The maxQueueLength restricts the number of characters that will be inside the [detail box].
* If the maxQueueLength is 0, there is no restriction.
*
* For example, say a dispatch_queue has a label of "diskIO":
*
* If the maxQueueLength is 0: [diskIO]
* If the maxQueueLength is 4: [disk]
* If the maxQueueLength is 5: [diskI]
* If the maxQueueLength is 6: [diskIO]
* If the maxQueueLength is 7: [diskIO]
* If the maxQueueLength is 8: [diskIO]
*
* The default maxQueueLength is 0 (no maximum, so [detail box] won't be truncated).
*
* If you want every [detail box] to have the exact same width,
* set both minQueueLength and maxQueueLength to the same value.
**/
@property (assign) NSUInteger maxQueueLength;
/**
* Sometimes queue labels have long names like "com.apple.main-queue",
* but you'd prefer something shorter like simply "main".
*
* This method allows you to set such preferred replacements.
* The above example is set by default.
*
* To remove/undo a previous replacement, invoke this method with nil for the 'shortLabel' parameter.
**/
- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel;
- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel;
@end

View File

@@ -0,0 +1,251 @@
#import "DispatchQueueLogFormatter.h"
#import <libkern/OSAtomic.h>
/**
* Welcome to Cocoa Lumberjack!
*
* The project page has a wealth of documentation if you have any questions.
* https://github.com/robbiehanson/CocoaLumberjack
*
* If you're new to the project you may wish to read the "Getting Started" wiki.
* https://github.com/robbiehanson/CocoaLumberjack/wiki/GettingStarted
**/
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
@implementation DispatchQueueLogFormatter
{
int32_t atomicLoggerCount;
NSDateFormatter *threadUnsafeDateFormatter; // Use [self stringFromDate]
OSSpinLock lock;
NSUInteger _minQueueLength; // _prefix == Only access via atomic property
NSUInteger _maxQueueLength; // _prefix == Only access via atomic property
NSMutableDictionary *_replacements; // _prefix == Only access from within spinlock
}
- (id)init
{
if ((self = [super init]))
{
dateFormatString = @"yyyy-MM-dd HH:mm:ss:SSS";
atomicLoggerCount = 0;
threadUnsafeDateFormatter = nil;
_minQueueLength = 0;
_maxQueueLength = 0;
_replacements = [[NSMutableDictionary alloc] init];
// Set default replacements:
[_replacements setObject:@"main" forKey:@"com.apple.main-thread"];
}
return self;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize minQueueLength = _minQueueLength;
@synthesize maxQueueLength = _maxQueueLength;
- (NSString *)replacementStringForQueueLabel:(NSString *)longLabel
{
NSString *result = nil;
OSSpinLockLock(&lock);
{
result = [_replacements objectForKey:longLabel];
}
OSSpinLockUnlock(&lock);
return result;
}
- (void)setReplacementString:(NSString *)shortLabel forQueueLabel:(NSString *)longLabel
{
OSSpinLockLock(&lock);
{
if (shortLabel)
[_replacements setObject:shortLabel forKey:longLabel];
else
[_replacements removeObjectForKey:longLabel];
}
OSSpinLockUnlock(&lock);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark DDLogFormatter
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSString *)stringFromDate:(NSDate *)date
{
int32_t loggerCount = OSAtomicAdd32(0, &atomicLoggerCount);
if (loggerCount <= 1)
{
// Single-threaded mode.
if (threadUnsafeDateFormatter == nil)
{
threadUnsafeDateFormatter = [[NSDateFormatter alloc] init];
[threadUnsafeDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[threadUnsafeDateFormatter setDateFormat:dateFormatString];
}
return [threadUnsafeDateFormatter stringFromDate:date];
}
else
{
// Multi-threaded mode.
// NSDateFormatter is NOT thread-safe.
NSString *key = @"DispatchQueueLogFormatter_NSDateFormatter";
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSDateFormatter *dateFormatter = [threadDictionary objectForKey:key];
if (dateFormatter == nil)
{
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setDateFormat:dateFormatString];
[threadDictionary setObject:dateFormatter forKey:key];
}
return [dateFormatter stringFromDate:date];
}
}
- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage
{
// As per the DDLogFormatter contract, this method is always invoked on the same thread/dispatch_queue
NSUInteger minQueueLength = self.minQueueLength;
NSUInteger maxQueueLength = self.maxQueueLength;
// Get the name of the queue, thread, or machID (whichever we are to use).
NSString *queueThreadLabel = nil;
BOOL useQueueLabel = YES;
BOOL useThreadName = NO;
if (logMessage->queueLabel)
{
// If you manually create a thread, it's dispatch_queue will have one of the thread names below.
// Since all such threads have the same name, we'd prefer to use the threadName or the machThreadID.
char *names[] = { "com.apple.root.low-priority",
"com.apple.root.default-priority",
"com.apple.root.high-priority",
"com.apple.root.low-overcommit-priority",
"com.apple.root.default-overcommit-priority",
"com.apple.root.high-overcommit-priority" };
int length = sizeof(names) / sizeof(char *);
int i;
for (i = 0; i < length; i++)
{
if (strcmp(logMessage->queueLabel, names[i]) == 0)
{
useQueueLabel = NO;
useThreadName = [logMessage->threadName length] > 0;
break;
}
}
}
else
{
useQueueLabel = NO;
useThreadName = [logMessage->threadName length] > 0;
}
if (useQueueLabel || useThreadName)
{
NSString *fullLabel;
NSString *abrvLabel;
if (useQueueLabel)
fullLabel = @(logMessage->queueLabel);
else
fullLabel = logMessage->threadName;
OSSpinLockLock(&lock);
{
abrvLabel = [_replacements objectForKey:fullLabel];
}
OSSpinLockUnlock(&lock);
if (abrvLabel)
queueThreadLabel = abrvLabel;
else
queueThreadLabel = fullLabel;
}
else
{
queueThreadLabel = [NSString stringWithFormat:@"%x", logMessage->machThreadID];
}
// Now use the thread label in the output
NSUInteger labelLength = [queueThreadLabel length];
// labelLength > maxQueueLength : truncate
// labelLength < minQueueLength : padding
// : exact
if ((maxQueueLength > 0) && (labelLength > maxQueueLength))
{
// Truncate
return [queueThreadLabel substringToIndex:maxQueueLength];
}
else if (labelLength < minQueueLength)
{
// Padding
NSUInteger numSpaces = minQueueLength - labelLength;
char spaces[numSpaces + 1];
memset(spaces, ' ', numSpaces);
spaces[numSpaces] = '\0';
return [NSString stringWithFormat:@"%@%s", queueThreadLabel, spaces];
}
else
{
// Exact
return queueThreadLabel;
}
}
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
NSString *timestamp = [self stringFromDate:(logMessage->timestamp)];
NSString *queueThreadLabel = [self queueThreadLabelForLogMessage:logMessage];
return [NSString stringWithFormat:@"%@ [%@] %@", timestamp, queueThreadLabel, logMessage->logMsg];
}
- (void)didAddToLogger:(id <DDLogger>)logger
{
OSAtomicIncrement32(&atomicLoggerCount);
}
- (void)willRemoveFromLogger:(id <DDLogger>)logger
{
OSAtomicDecrement32(&atomicLoggerCount);
}
@end

View File

@@ -0,0 +1,7 @@
This folder contains some sample formatters that may be helpful.
Feel free to change them, extend them, or use them as the basis for your own custom formatter(s).
More information about creating your own custom formatters can be found on the wiki:
https://github.com/robbiehanson/CocoaLumberjack/wiki/CustomFormatters

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,79 @@
#import "HTTPDataResponse.h"
#import "HTTPLogging.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
// Log levels : off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE;
@implementation HTTPDataResponse
- (id)initWithData:(NSData *)dataParam
{
if((self = [super init]))
{
HTTPLogTrace();
offset = 0;
data = dataParam;
}
return self;
}
- (void)dealloc
{
HTTPLogTrace();
}
- (UInt64)contentLength
{
UInt64 result = (UInt64)[data length];
HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, result);
return result;
}
- (UInt64)offset
{
HTTPLogTrace();
return offset;
}
- (void)setOffset:(UInt64)offsetParam
{
HTTPLogTrace2(@"%@[%p]: setOffset:%lu", THIS_FILE, self, (unsigned long)offset);
offset = (NSUInteger)offsetParam;
}
- (NSData *)readDataOfLength:(NSUInteger)lengthParameter
{
HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)lengthParameter);
NSUInteger remaining = [data length] - offset;
NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining;
void *bytes = (void *)([data bytes] + offset);
offset += length;
return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO];
}
- (BOOL)isDone
{
BOOL result = (offset == [data length]);
HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO"));
return result;
}
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
#import "HTTPRedirectResponse.h"
#import "HTTPLogging.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
// Log levels : off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE;
@implementation HTTPRedirectResponse
- (id)initWithPath:(NSString *)path
{
if ((self = [super init]))
{
HTTPLogTrace();
redirectPath = [path copy];
}
return self;
}
- (UInt64)contentLength
{
return 0;
}
- (UInt64)offset
{
return 0;
}
- (void)setOffset:(UInt64)offset
{
// Nothing to do
}
- (NSData *)readDataOfLength:(NSUInteger)length
{
HTTPLogTrace();
return nil;
}
- (BOOL)isDone
{
return YES;
}
- (NSDictionary *)httpHeaders
{
HTTPLogTrace();
return [NSDictionary dictionaryWithObject:redirectPath forKey:@"Location"];
}
- (NSInteger)status
{
HTTPLogTrace();
return 302;
}
- (void)dealloc
{
HTTPLogTrace();
}
@end

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

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

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

@@ -0,0 +1,791 @@
#import "WebSocket.h"
#import "HTTPMessage.h"
#import "GCDAsyncSocket.h"
#import "DDNumber.h"
#import "DDData.h"
#import "HTTPLogging.h"
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
// Log levels: off, error, warn, info, verbose
// Other flags : trace
static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE;
#define TIMEOUT_NONE -1
#define TIMEOUT_REQUEST_BODY 10
#define TAG_HTTP_REQUEST_BODY 100
#define TAG_HTTP_RESPONSE_HEADERS 200
#define TAG_HTTP_RESPONSE_BODY 201
#define TAG_PREFIX 300
#define TAG_MSG_PLUS_SUFFIX 301
#define TAG_MSG_WITH_LENGTH 302
#define TAG_MSG_MASKING_KEY 303
#define TAG_PAYLOAD_PREFIX 304
#define TAG_PAYLOAD_LENGTH 305
#define TAG_PAYLOAD_LENGTH16 306
#define TAG_PAYLOAD_LENGTH64 307
#define WS_OP_CONTINUATION_FRAME 0
#define WS_OP_TEXT_FRAME 1
#define WS_OP_BINARY_FRAME 2
#define WS_OP_CONNECTION_CLOSE 8
#define WS_OP_PING 9
#define WS_OP_PONG 10
static inline BOOL WS_OP_IS_FINAL_FRAGMENT(UInt8 frame)
{
return (frame & 0x80) ? YES : NO;
}
static inline BOOL WS_PAYLOAD_IS_MASKED(UInt8 frame)
{
return (frame & 0x80) ? YES : NO;
}
static inline NSUInteger WS_PAYLOAD_LENGTH(UInt8 frame)
{
return frame & 0x7F;
}
@interface WebSocket (PrivateAPI)
- (void)readRequestBody;
- (void)sendResponseBody;
- (void)sendResponseHeaders;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation WebSocket
{
BOOL isRFC6455;
BOOL nextFrameMasked;
NSUInteger nextOpCode;
NSData *maskingKey;
}
+ (BOOL)isWebSocketRequest:(HTTPMessage *)request
{
// Request (Draft 75):
//
// GET /demo HTTP/1.1
// Upgrade: WebSocket
// Connection: Upgrade
// Host: example.com
// Origin: http://example.com
// WebSocket-Protocol: sample
//
//
// Request (Draft 76):
//
// GET /demo HTTP/1.1
// Upgrade: WebSocket
// Connection: Upgrade
// Host: example.com
// Origin: http://example.com
// Sec-WebSocket-Protocol: sample
// Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
// Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
//
// ^n:ds[4U
// Look for Upgrade: and Connection: headers.
// If we find them, and they have the proper value,
// we can safely assume this is a websocket request.
NSString *upgradeHeaderValue = [request headerField:@"Upgrade"];
NSString *connectionHeaderValue = [request headerField:@"Connection"];
BOOL isWebSocket = YES;
if (!upgradeHeaderValue || !connectionHeaderValue) {
isWebSocket = NO;
}
else if (![upgradeHeaderValue caseInsensitiveCompare:@"WebSocket"] == NSOrderedSame) {
isWebSocket = NO;
}
else if ([connectionHeaderValue rangeOfString:@"Upgrade" options:NSCaseInsensitiveSearch].location == NSNotFound) {
isWebSocket = NO;
}
HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isWebSocket ? @"YES" : @"NO"));
return isWebSocket;
}
+ (BOOL)isVersion76Request:(HTTPMessage *)request
{
NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"];
NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"];
BOOL isVersion76;
if (!key1 || !key2) {
isVersion76 = NO;
}
else {
isVersion76 = YES;
}
HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isVersion76 ? @"YES" : @"NO"));
return isVersion76;
}
+ (BOOL)isRFC6455Request:(HTTPMessage *)request
{
NSString *key = [request headerField:@"Sec-WebSocket-Key"];
BOOL isRFC6455 = (key != nil);
HTTPLogTrace2(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, (isRFC6455 ? @"YES" : @"NO"));
return isRFC6455;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Setup and Teardown
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@synthesize websocketQueue;
- (id)initWithRequest:(HTTPMessage *)aRequest socket:(GCDAsyncSocket *)socket
{
HTTPLogTrace();
if (aRequest == nil)
{
return nil;
}
if ((self = [super init]))
{
if (HTTP_LOG_VERBOSE)
{
NSData *requestHeaders = [aRequest messageData];
NSString *temp = [[NSString alloc] initWithData:requestHeaders encoding:NSUTF8StringEncoding];
HTTPLogVerbose(@"%@[%p] Request Headers:\n%@", THIS_FILE, self, temp);
}
websocketQueue = dispatch_queue_create("WebSocket", NULL);
request = aRequest;
asyncSocket = socket;
[asyncSocket setDelegate:self delegateQueue:websocketQueue];
isOpen = NO;
isVersion76 = [[self class] isVersion76Request:request];
isRFC6455 = [[self class] isRFC6455Request:request];
term = [[NSData alloc] initWithBytes:"\xFF" length:1];
}
return self;
}
- (void)dealloc
{
HTTPLogTrace();
#if !OS_OBJECT_USE_OBJC
dispatch_release(websocketQueue);
#endif
[asyncSocket setDelegate:nil delegateQueue:NULL];
[asyncSocket disconnect];
}
- (id)delegate
{
__block id result = nil;
dispatch_sync(websocketQueue, ^{
result = delegate;
});
return result;
}
- (void)setDelegate:(id)newDelegate
{
dispatch_async(websocketQueue, ^{
delegate = newDelegate;
});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Start and Stop
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Starting point for the WebSocket after it has been fully initialized (including subclasses).
* This method is called by the HTTPConnection it is spawned from.
**/
- (void)start
{
// This method is not exactly designed to be overriden.
// Subclasses are encouraged to override the didOpen method instead.
dispatch_async(websocketQueue, ^{ @autoreleasepool {
if (isStarted) return;
isStarted = YES;
if (isVersion76)
{
[self readRequestBody];
}
else
{
[self sendResponseHeaders];
[self didOpen];
}
}});
}
/**
* This method is called by the HTTPServer if it is asked to stop.
* The server, in turn, invokes stop on each WebSocket instance.
**/
- (void)stop
{
// This method is not exactly designed to be overriden.
// Subclasses are encouraged to override the didClose method instead.
dispatch_async(websocketQueue, ^{ @autoreleasepool {
[asyncSocket disconnect];
}});
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark HTTP Response
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)readRequestBody
{
HTTPLogTrace();
NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a request body");
[asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_HTTP_REQUEST_BODY];
}
- (NSString *)originResponseHeaderValue
{
HTTPLogTrace();
NSString *origin = [request headerField:@"Origin"];
if (origin == nil)
{
NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]];
return [NSString stringWithFormat:@"http://localhost:%@", port];
}
else
{
return origin;
}
}
- (NSString *)locationResponseHeaderValue
{
HTTPLogTrace();
NSString *location;
NSString *scheme = [asyncSocket isSecure] ? @"wss" : @"ws";
NSString *host = [request headerField:@"Host"];
NSString *requestUri = [[request url] relativeString];
if (host == nil)
{
NSString *port = [NSString stringWithFormat:@"%hu", [asyncSocket localPort]];
location = [NSString stringWithFormat:@"%@://localhost:%@%@", scheme, port, requestUri];
}
else
{
location = [NSString stringWithFormat:@"%@://%@%@", scheme, host, requestUri];
}
return location;
}
- (NSString *)secWebSocketKeyResponseHeaderValue {
NSString *key = [request headerField: @"Sec-WebSocket-Key"];
NSString *guid = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
return [[key stringByAppendingString: guid] dataUsingEncoding: NSUTF8StringEncoding].sha1Digest.base64Encoded;
}
- (void)sendResponseHeaders
{
HTTPLogTrace();
// Request (Draft 75):
//
// GET /demo HTTP/1.1
// Upgrade: WebSocket
// Connection: Upgrade
// Host: example.com
// Origin: http://example.com
// WebSocket-Protocol: sample
//
//
// Request (Draft 76):
//
// GET /demo HTTP/1.1
// Upgrade: WebSocket
// Connection: Upgrade
// Host: example.com
// Origin: http://example.com
// Sec-WebSocket-Protocol: sample
// Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
// Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
//
// ^n:ds[4U
// Response (Draft 75):
//
// HTTP/1.1 101 Web Socket Protocol Handshake
// Upgrade: WebSocket
// Connection: Upgrade
// WebSocket-Origin: http://example.com
// WebSocket-Location: ws://example.com/demo
// WebSocket-Protocol: sample
//
//
// Response (Draft 76):
//
// HTTP/1.1 101 WebSocket Protocol Handshake
// Upgrade: WebSocket
// Connection: Upgrade
// Sec-WebSocket-Origin: http://example.com
// Sec-WebSocket-Location: ws://example.com/demo
// Sec-WebSocket-Protocol: sample
//
// 8jKS'y:G*Co,Wxa-
HTTPMessage *wsResponse = [[HTTPMessage alloc] initResponseWithStatusCode:101
description:@"Web Socket Protocol Handshake"
version:HTTPVersion1_1];
[wsResponse setHeaderField:@"Upgrade" value:@"WebSocket"];
[wsResponse setHeaderField:@"Connection" value:@"Upgrade"];
// Note: It appears that WebSocket-Origin and WebSocket-Location
// are required for Google's Chrome implementation to work properly.
//
// If we don't send either header, Chrome will never report the WebSocket as open.
// If we only send one of the two, Chrome will immediately close the WebSocket.
//
// In addition to this it appears that Chrome's implementation is very picky of the values of the headers.
// They have to match exactly with what Chrome sent us or it will close the WebSocket.
NSString *originValue = [self originResponseHeaderValue];
NSString *locationValue = [self locationResponseHeaderValue];
NSString *originField = isVersion76 ? @"Sec-WebSocket-Origin" : @"WebSocket-Origin";
NSString *locationField = isVersion76 ? @"Sec-WebSocket-Location" : @"WebSocket-Location";
[wsResponse setHeaderField:originField value:originValue];
[wsResponse setHeaderField:locationField value:locationValue];
NSString *acceptValue = [self secWebSocketKeyResponseHeaderValue];
if (acceptValue) {
[wsResponse setHeaderField: @"Sec-WebSocket-Accept" value: acceptValue];
}
NSData *responseHeaders = [wsResponse messageData];
if (HTTP_LOG_VERBOSE)
{
NSString *temp = [[NSString alloc] initWithData:responseHeaders encoding:NSUTF8StringEncoding];
HTTPLogVerbose(@"%@[%p] Response Headers:\n%@", THIS_FILE, self, temp);
}
[asyncSocket writeData:responseHeaders withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_HEADERS];
}
- (NSData *)processKey:(NSString *)key
{
HTTPLogTrace();
unichar c;
NSUInteger i;
NSUInteger length = [key length];
// Concatenate the digits into a string,
// and count the number of spaces.
NSMutableString *numStr = [NSMutableString stringWithCapacity:10];
long long numSpaces = 0;
for (i = 0; i < length; i++)
{
c = [key characterAtIndex:i];
if (c >= '0' && c <= '9')
{
[numStr appendFormat:@"%C", c];
}
else if (c == ' ')
{
numSpaces++;
}
}
long long num = strtoll([numStr UTF8String], NULL, 10);
long long resultHostNum;
if (numSpaces == 0)
resultHostNum = 0;
else
resultHostNum = num / numSpaces;
HTTPLogVerbose(@"key(%@) -> %qi / %qi = %qi", key, num, numSpaces, resultHostNum);
// Convert result to 4 byte big-endian (network byte order)
// and then convert to raw data.
UInt32 result = OSSwapHostToBigInt32((uint32_t)resultHostNum);
return [NSData dataWithBytes:&result length:4];
}
- (void)sendResponseBody:(NSData *)d3
{
HTTPLogTrace();
NSAssert(isVersion76, @"WebSocket version 75 doesn't contain a response body");
NSAssert([d3 length] == 8, @"Invalid requestBody length");
NSString *key1 = [request headerField:@"Sec-WebSocket-Key1"];
NSString *key2 = [request headerField:@"Sec-WebSocket-Key2"];
NSData *d1 = [self processKey:key1];
NSData *d2 = [self processKey:key2];
// Concatenated d1, d2 & d3
NSMutableData *d0 = [NSMutableData dataWithCapacity:(4+4+8)];
[d0 appendData:d1];
[d0 appendData:d2];
[d0 appendData:d3];
// Hash the data using MD5
NSData *responseBody = [d0 md5Digest];
[asyncSocket writeData:responseBody withTimeout:TIMEOUT_NONE tag:TAG_HTTP_RESPONSE_BODY];
if (HTTP_LOG_VERBOSE)
{
NSString *s1 = [[NSString alloc] initWithData:d1 encoding:NSASCIIStringEncoding];
NSString *s2 = [[NSString alloc] initWithData:d2 encoding:NSASCIIStringEncoding];
NSString *s3 = [[NSString alloc] initWithData:d3 encoding:NSASCIIStringEncoding];
NSString *s0 = [[NSString alloc] initWithData:d0 encoding:NSASCIIStringEncoding];
NSString *sH = [[NSString alloc] initWithData:responseBody encoding:NSASCIIStringEncoding];
HTTPLogVerbose(@"key1 result : raw(%@) str(%@)", d1, s1);
HTTPLogVerbose(@"key2 result : raw(%@) str(%@)", d2, s2);
HTTPLogVerbose(@"key3 passed : raw(%@) str(%@)", d3, s3);
HTTPLogVerbose(@"key0 concat : raw(%@) str(%@)", d0, s0);
HTTPLogVerbose(@"responseBody: raw(%@) str(%@)", responseBody, sH);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Core Functionality
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)didOpen
{
HTTPLogTrace();
// Override me to perform any custom actions once the WebSocket has been opened.
// This method is invoked on the websocketQueue.
//
// Don't forget to invoke [super didOpen] in your method.
// Start reading for messages
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:(isRFC6455 ? TAG_PAYLOAD_PREFIX : TAG_PREFIX)];
// Notify delegate
if ([delegate respondsToSelector:@selector(webSocketDidOpen:)])
{
[delegate webSocketDidOpen:self];
}
}
- (void)sendMessage:(NSString *)msg
{
NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding];
[self sendData:msgData];
}
- (void)sendData:(NSData *)msgData
{
HTTPLogTrace();
NSMutableData *data = nil;
if (isRFC6455)
{
NSUInteger length = msgData.length;
if (length <= 125)
{
data = [NSMutableData dataWithCapacity:(length + 2)];
[data appendBytes: "\x81" length:1];
UInt8 len = (UInt8)length;
[data appendBytes: &len length:1];
[data appendData:msgData];
}
else if (length <= 0xFFFF)
{
data = [NSMutableData dataWithCapacity:(length + 4)];
[data appendBytes: "\x81\x7E" length:2];
UInt16 len = (UInt16)length;
[data appendBytes: (UInt8[]){len >> 8, len & 0xFF} length:2];
[data appendData:msgData];
}
else
{
data = [NSMutableData dataWithCapacity:(length + 10)];
[data appendBytes: "\x81\x7F" length:2];
[data appendBytes: (UInt8[]){0, 0, 0, 0, (UInt8)(length >> 24), (UInt8)(length >> 16), (UInt8)(length >> 8), length & 0xFF} length:8];
[data appendData:msgData];
}
}
else
{
data = [NSMutableData dataWithCapacity:([msgData length] + 2)];
[data appendBytes:"\x00" length:1];
[data appendData:msgData];
[data appendBytes:"\xFF" length:1];
}
// Remember: GCDAsyncSocket is thread-safe
[asyncSocket writeData:data withTimeout:TIMEOUT_NONE tag:0];
}
- (void)didReceiveMessage:(NSString *)msg
{
HTTPLogTrace();
// Override me to process incoming messages.
// This method is invoked on the websocketQueue.
//
// For completeness, you should invoke [super didReceiveMessage:msg] in your method.
// Notify delegate
if ([delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)])
{
[delegate webSocket:self didReceiveMessage:msg];
}
}
- (void)didClose
{
HTTPLogTrace();
// Override me to perform any cleanup when the socket is closed
// This method is invoked on the websocketQueue.
//
// Don't forget to invoke [super didClose] at the end of your method.
// Notify delegate
if ([delegate respondsToSelector:@selector(webSocketDidClose:)])
{
[delegate webSocketDidClose:self];
}
// Notify HTTPServer
[[NSNotificationCenter defaultCenter] postNotificationName:WebSocketDidDieNotification object:self];
}
#pragma mark WebSocket Frame
- (BOOL)isValidWebSocketFrame:(UInt8)frame
{
NSUInteger rsv = frame & 0x70;
NSUInteger opcode = frame & 0x0F;
if (rsv || (3 <= opcode && opcode <= 7) || (0xB <= opcode && opcode <= 0xF))
{
return NO;
}
return YES;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark AsyncSocket Delegate
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
HTTPLogTrace();
if (tag == TAG_HTTP_REQUEST_BODY)
{
[self sendResponseHeaders];
[self sendResponseBody:data];
[self didOpen];
}
else if (tag == TAG_PREFIX)
{
UInt8 *pFrame = (UInt8 *)[data bytes];
UInt8 frame = *pFrame;
if (frame <= 0x7F)
{
[asyncSocket readDataToData:term withTimeout:TIMEOUT_NONE tag:TAG_MSG_PLUS_SUFFIX];
}
else
{
// Unsupported frame type
[self didClose];
}
}
else if (tag == TAG_PAYLOAD_PREFIX)
{
UInt8 *pFrame = (UInt8 *)[data bytes];
UInt8 frame = *pFrame;
if ([self isValidWebSocketFrame: frame])
{
nextOpCode = (frame & 0x0F);
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH];
}
else
{
// Unsupported frame type
[self didClose];
}
}
else if (tag == TAG_PAYLOAD_LENGTH)
{
UInt8 frame = *(UInt8 *)[data bytes];
BOOL masked = WS_PAYLOAD_IS_MASKED(frame);
NSUInteger length = WS_PAYLOAD_LENGTH(frame);
nextFrameMasked = masked;
maskingKey = nil;
if (length <= 125)
{
if (nextFrameMasked)
{
[asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY];
}
[asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_MSG_WITH_LENGTH];
}
else if (length == 126)
{
[asyncSocket readDataToLength:2 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH16];
}
else
{
[asyncSocket readDataToLength:8 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_LENGTH64];
}
}
else if (tag == TAG_PAYLOAD_LENGTH16)
{
UInt8 *pFrame = (UInt8 *)[data bytes];
NSUInteger length = ((NSUInteger)pFrame[0] << 8) | (NSUInteger)pFrame[1];
if (nextFrameMasked) {
[asyncSocket readDataToLength:4 withTimeout:TIMEOUT_NONE tag:TAG_MSG_MASKING_KEY];
}
[asyncSocket readDataToLength:length withTimeout:TIMEOUT_NONE tag:TAG_MSG_WITH_LENGTH];
}
else if (tag == TAG_PAYLOAD_LENGTH64)
{
// FIXME: 64bit data size in memory?
[self didClose];
}
else if (tag == TAG_MSG_WITH_LENGTH)
{
NSUInteger msgLength = [data length];
if (nextFrameMasked && maskingKey) {
NSMutableData *masked = data.mutableCopy;
UInt8 *pData = (UInt8 *)masked.mutableBytes;
UInt8 *pMask = (UInt8 *)maskingKey.bytes;
for (NSUInteger i = 0; i < msgLength; i++)
{
pData[i] = pData[i] ^ pMask[i % 4];
}
data = masked;
}
if (nextOpCode == WS_OP_TEXT_FRAME)
{
NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding];
[self didReceiveMessage:msg];
}
else
{
[self didClose];
return;
}
// Read next frame
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PAYLOAD_PREFIX];
}
else if (tag == TAG_MSG_MASKING_KEY)
{
maskingKey = data.copy;
}
else
{
NSUInteger msgLength = [data length] - 1; // Excluding ending 0xFF frame
NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding];
[self didReceiveMessage:msg];
// Read next message
[asyncSocket readDataToLength:1 withTimeout:TIMEOUT_NONE tag:TAG_PREFIX];
}
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
{
HTTPLogTrace2(@"%@[%p]: socketDidDisconnect:withError: %@", THIS_FILE, self, error);
[self didClose];
}
@end