房间内播放背景音乐上传音乐

This commit is contained in:
fengshuo
2022-05-07 19:02:13 +08:00
parent f5042df003
commit db61470a40
82 changed files with 18444 additions and 5 deletions

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,194 @@
#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
{
if (header==nil || header.length == 0) return @"";
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,215 @@
#import <Foundation/Foundation.h>
#import "MyHTTPConnection.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;
UInt32 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.
**/
- (UInt32)port;
- (UInt32)listeningPort;
- (void)setPort:(UInt32)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;
/**
Get the connections in the httpserver
@return the connections with array
*/
- (NSMutableArray *)getConnectionsInServer;
///添加代理使用
@property (nonatomic,weak) UIViewController<MyHTTPConnectionDelegate> * viewcontroller;
@end

View File

@@ -0,0 +1,781 @@
#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;
// port = 12306;
// 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.
**/
- (UInt32)port
{
__block UInt32 result;
dispatch_sync(serverQueue, ^{
result = port;
// result = 12306;
});
return result;
}
- (UInt32)listeningPort
{
__block UInt32 result;
dispatch_sync(serverQueue, ^{
if (isRunning)
result = [asyncSocket localPort];
else
result = 0;
// result = 12306;
});
return result;
}
- (void)setPort:(UInt32)value
{
HTTPLogTrace();
dispatch_async(serverQueue, ^{
port = value;
// port = 12306;
});
}
/**
* 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 {
// port = 12306;
success = [asyncSocket acceptOnInterface:interface port:port error:&err];
if (success)
{
HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]);
//HTTPLogInfo(@"%@: Started HTTP server on port %d", THIS_FILE, 12306);
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]];
((MyHTTPConnection *)newConnection).delegate = self.viewcontroller;
[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 = [[NSNetService alloc] initWithDomain:domain type:type name:name port:12306];
[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];
}
- (NSMutableArray *)getConnectionsInServer {
return connections.count > 0 ? connections : [NSMutableArray array];
}
@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?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

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

View File

@@ -0,0 +1,792 @@
#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]];
//NSString * port = @"12306";
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]];
//NSString * port = @"12306";
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

View File

@@ -0,0 +1,58 @@
#import "HTTPConnection.h"
@class MultipartFormDataParser;
@class MyHTTPConnection;
@protocol MyHTTPConnectionDelegate <NSObject>
@required
/**
设置目标地址
@param server 服务器 ftp self
@return 目标地址str
*/
- (NSString *)onSetDestinationPathHttpFileTranSportDestination:(MyHTTPConnection *)server;
@optional
/**
文件传输并存储成功
@param server 服务器 ftp self
@param filePath 文件路径(含文件名)
*/
- (void)onHttpFileTranSportServer:(MyHTTPConnection *)server successWithPath:(NSString *)filePath;
/**
文件判断
@param server 服务器 ftp self
@param filePath 文件路径(含文件名)
@return 如果是重复文件就返回NO
*/
- (BOOL)onHttpFileDataEstimateDuplicateCanPassTranSportServer:(MyHTTPConnection *)server withPath:(NSString *)filePath andFileName:(NSString *)fileName;
@end
@interface MyHTTPConnection : HTTPConnection {
MultipartFormDataParser* parser;
NSFileHandle* storeFile;
NSMutableArray* uploadedFiles;
}
/**
文件路径(含文件名)
*/
@property (nonatomic, strong) NSString *filePath;
/**
文件夹路径(不含文件名)
*/
@property (strong, nonatomic) NSString *destinationPath;
/**
delegate:MyHTTPConnectionDelegate
*/
@property (nonatomic, weak) id <MyHTTPConnectionDelegate> delegate;
@end

View File

@@ -0,0 +1,208 @@
#import "MyHTTPConnection.h"
#import "HTTPMessage.h"
#import "HTTPDataResponse.h"
#import "DDNumber.h"
#import "HTTPLogging.h"
#import "MultipartFormDataParser.h"
#import "MultipartMessageHeaderField.h"
#import "HTTPDynamicFileResponse.h"
#import "HTTPFileResponse.h"
// Log levels : off, error, warn, info, verbose
// Other flags: trace
static const int httpLogLevel = HTTP_LOG_LEVEL_VERBOSE; // | HTTP_LOG_FLAG_TRACE;
/**
* All we have to do is override appropriate methods in HTTPConnection.
**/
@implementation MyHTTPConnection
- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path {
HTTPLogTrace();
// Add support for POST
if ([method isEqualToString:@"POST"])
{
if ([path isEqualToString:@"/upload.html"])
{
return YES;
}
}
return [super supportsMethod:method atPath:path];
}
- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path {
HTTPLogTrace();
// Inform HTTP server that we expect a body to accompany a POST request
if([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.html"]) {
// here we need to make sure, boundary is set in header
NSString* contentType = [request headerField:@"Content-Type"];
NSUInteger paramsSeparator = [contentType rangeOfString:@";"].location;
if( NSNotFound == paramsSeparator ) {
return NO;
}
if( paramsSeparator >= contentType.length - 1 ) {
return NO;
}
NSString* type = [contentType substringToIndex:paramsSeparator];
if( ![type isEqualToString:@"multipart/form-data"] ) {
// we expect multipart/form-data content type
return NO;
}
// enumerate all params in content-type, and find boundary there
NSArray* params = [[contentType substringFromIndex:paramsSeparator + 1] componentsSeparatedByString:@";"];
for( NSString* param in params ) {
paramsSeparator = [param rangeOfString:@"="].location;
if( (NSNotFound == paramsSeparator) || paramsSeparator >= param.length - 1 ) {
continue;
}
NSString* paramName = [param substringWithRange:NSMakeRange(1, paramsSeparator-1)];
NSString* paramValue = [param substringFromIndex:paramsSeparator+1];
if( [paramName isEqualToString: @"boundary"] ) {
// let's separate the boundary from content-type, to make it more handy to handle
[request setHeaderField:@"boundary" value:paramValue];
}
}
// check if boundary specified
if( nil == [request headerField:@"boundary"] ) {
return NO;
}
return YES;
}
return [super expectsRequestBodyFromMethod:method atPath:path];
}
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path {
HTTPLogTrace();
if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.html"]) {
// this method will generate response with links to uploaded file
NSMutableString* filesStr = [[NSMutableString alloc] init];
for( NSString* filePath in uploadedFiles ) {
//generate links
[filesStr appendFormat:@"<a href=\"%@\"> %@ </a><br/>",filePath, [filePath lastPathComponent]];
}
NSString* templatePath = [[config documentRoot] stringByAppendingPathComponent:@"upload.html"];
NSDictionary* replacementDict = [NSDictionary dictionaryWithObject:filesStr forKey:@"MyFiles"];
// use dynamic file response to apply our links to response template
return [[HTTPDynamicFileResponse alloc] initWithFilePath:templatePath forConnection:self separator:@"%" replacementDictionary:replacementDict];
}
if( [method isEqualToString:@"GET"] && [path hasPrefix:@"/upload/"] ) {
// let download the uploaded files
return [[HTTPFileResponse alloc] initWithFilePath: [[config documentRoot] stringByAppendingString:path] forConnection:self];
}
return [super httpResponseForMethod:method URI:path];
}
- (void)prepareForBodyWithSize:(UInt64)contentLength {
HTTPLogTrace();
// set up mime parser
NSString* boundary = [request headerField:@"boundary"];
parser = [[MultipartFormDataParser alloc] initWithBoundary:boundary formEncoding:NSUTF8StringEncoding];
parser.delegate = self;
uploadedFiles = [[NSMutableArray alloc] init];
}
- (void)processBodyData:(NSData *)postDataChunk {
HTTPLogTrace();
// append data to the parser. It will invoke callbacks to let us handle
// parsed data.
[parser appendData:postDataChunk];
}
//-----------------------------------------------------------------
#pragma mark multipart form data parser delegate
- (void) processStartOfPartWithHeader:(MultipartMessageHeader*) header {
// in this sample, we are not interested in parts, other then file parts.
// check content disposition to find out filename
MultipartMessageHeaderField* disposition = [header.fields objectForKey:@"Content-Disposition"];
NSString* filename = [[disposition.params objectForKey:@"filename"] lastPathComponent];
if ( (nil == filename) || [filename isEqualToString: @""] ) {
// it's either not a file part, or
// an empty form sent. we won't handle it.
return;
}
//
NSAssert([self.delegate respondsToSelector:@selector(onSetDestinationPathHttpFileTranSportDestination:)], @"Need to add delegate name onSetDestinationPathHttpFileTranSportDestination to add a destinationPath");
if ([self.delegate respondsToSelector:@selector(onSetDestinationPathHttpFileTranSportDestination:)]) {
self.destinationPath = [self.delegate onSetDestinationPathHttpFileTranSportDestination:self];
NSAssert(self.destinationPath.length > 0 || self.destinationPath, @"destinationPath can not be nil");
}
//
NSString* filePath = [self.destinationPath stringByAppendingPathComponent: filename];
self.filePath = filePath;
//
if ([self.delegate respondsToSelector:@selector(onHttpFileDataEstimateDuplicateCanPassTranSportServer:withPath:andFileName:)]) {
if (![self.delegate onHttpFileDataEstimateDuplicateCanPassTranSportServer:self withPath:self.filePath andFileName:filename]) {
return;
};
}
HTTPLogVerbose(@"Saving file to %@", filePath);
if(![[NSFileManager defaultManager] createDirectoryAtPath:self.destinationPath withIntermediateDirectories:true attributes:nil error:nil]) {
HTTPLogError(@"Could not create directory at path: %@", filePath);
}
if(![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
HTTPLogError(@"Could not create file at path: %@", filePath);
}
storeFile = [NSFileHandle fileHandleForWritingAtPath:filePath];
[uploadedFiles addObject: [NSString stringWithFormat:@"/upload/%@", filename]];
}
- (void) processContent:(NSData*) data WithHeader:(MultipartMessageHeader*) header {
// here we just write the output from parser to the file.
if( storeFile ) {
[storeFile writeData:data];
}
}
- (void) processEndOfPartWithHeader:(MultipartMessageHeader*) header {
// as the file part is over, we close the file.
[storeFile closeFile];
storeFile = nil;
}
- (void) processPreambleData:(NSData*) data {
// if we are interested in preamble data, we could process it here.
}
- (void) processEpilogueData:(NSData*) data {
// if we are interested in epilogue data, we could process it here.
//
[[NSNotificationCenter defaultCenter] postNotificationName:@"processEpilogueData" object:nil];
if ([self.delegate respondsToSelector:@selector(onHttpFileTranSportServer:successWithPath:)]) {
[self.delegate onHttpFileTranSportServer:self successWithPath:self.filePath];
}
}
@end

View File

@@ -0,0 +1,19 @@
//
// SJXCSMIPHelper.h
// LocalReader
//
// Created by shapp on 2017/7/24.
// Copyright © 2017年 sjx. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface SJXCSMIPHelper : NSObject
/** 获取ip地址 */
+ (NSString *)deviceIPAdress;
#pragma mark - 获取设备当前网络IP地址
+ (NSString *)getIPAddress:(BOOL)preferIPv4;
@end

View File

@@ -0,0 +1,155 @@
//
// SJXCSMIPHelper.m
// LocalReader
//
// Created by shapp on 2017/7/24.
// Copyright © 2017 sjx. All rights reserved.
//
#import "SJXCSMIPHelper.h"
#import <ifaddrs.h>
#import <arpa/inet.h>
#import <net/if.h>
@implementation SJXCSMIPHelper
+ (NSString *)deviceIPAdress {
NSString *address = @"an error occurred when obtaining ip address";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
success = getifaddrs(&interfaces);
if (success == 0) { // 0
temp_addr = interfaces;
while (temp_addr != NULL) {
if( temp_addr->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
freeifaddrs(interfaces);
return address;
}
#define IOS_CELLULAR @"pdp_ip0"
#define IOS_WIFI @"en0"
#define IOS_VPN @"utun0"
#define IP_ADDR_IPv4 @"ipv4"
#define IP_ADDR_IPv6 @"ipv6"
#pragma mark - IP
+ (NSString *)getIPAddress:(BOOL)preferIPv4
{
NSArray *searchArray = preferIPv4 ?
@[ IOS_VPN @"/" IP_ADDR_IPv4, IOS_VPN @"/" IP_ADDR_IPv6, IOS_WIFI @"/" IP_ADDR_IPv4, IOS_WIFI @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6 ] :
@[ IOS_VPN @"/" IP_ADDR_IPv6, IOS_VPN @"/" IP_ADDR_IPv4, IOS_WIFI @"/" IP_ADDR_IPv6, IOS_WIFI @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4 ] ;
NSDictionary *addresses = [self getIPAddresses];
NSLog(@"addresses: %@", addresses);
__block NSString *address;
[searchArray enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL *stop)
{
address = addresses[key];
//IP
if([self isValidatIP:address]) *stop = YES;
} ];
return address ? address : @"0.0.0.0";
}
+ (BOOL)isValidatIP:(NSString *)ipAddress {
if (ipAddress.length == 0) {
return NO;
}
NSString *urlRegEx = @"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
NSError *error;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:urlRegEx options:0 error:&error];
if (regex != nil) {
NSTextCheckingResult *firstMatch=[regex firstMatchInString:ipAddress options:0 range:NSMakeRange(0, [ipAddress length])];
if (firstMatch) {
NSRange resultRange = [firstMatch rangeAtIndex:0];
NSString *result=[ipAddress substringWithRange:resultRange];
//
NSLog(@"%@",result);
return YES;
}
}
return NO;
}
+ (NSDictionary *)getIPAddresses
{
NSMutableDictionary *addresses = [NSMutableDictionary dictionaryWithCapacity:8];
// retrieve the current interfaces - returns 0 on success
struct ifaddrs *interfaces;
if(!getifaddrs(&interfaces)) {
// Loop through linked list of interfaces
struct ifaddrs *interface;
for(interface=interfaces; interface; interface=interface->ifa_next) {
if(!(interface->ifa_flags & IFF_UP) /* || (interface->ifa_flags & IFF_LOOPBACK) */ ) {
continue; // deeply nested code harder to read
}
const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr;
char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ];
if(addr && (addr->sin_family==AF_INET || addr->sin_family==AF_INET6)) {
NSString *name = [NSString stringWithUTF8String:interface->ifa_name];
NSString *type;
if(addr->sin_family == AF_INET) {
if(inet_ntop(AF_INET, &addr->sin_addr, addrBuf, INET_ADDRSTRLEN)) {
type = IP_ADDR_IPv4;
}
} else {
const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*)interface->ifa_addr;
if(inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, INET6_ADDRSTRLEN)) {
type = IP_ADDR_IPv6;
}
}
if(type) {
NSString *key = [NSString stringWithFormat:@"%@/%@", name, type];
addresses[key] = [NSString stringWithUTF8String:addrBuf];
}
}
}
// Free memory
freeifaddrs(interfaces);
}
return [addresses count] ? addresses : nil;
}
@end

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,101 @@
img {
width: 100%;
vertical-align: top;
}
html,
body {
width: 100%;
height: 100%;
}
.container {
margin: 0 auto;
height: 100%;
background: #f5f5f5;
}
.header {
height: 80px;
text-align: center;
background: #2f2f2f;
}
.header img {
margin: 0 auto;
height: 100%;
width: 900px;
}
.section {
padding: 20px 24px;
width: 800px;
margin: 0 auto;
}
.section .uploadBtn {
margin: 0 auto 20px;
width: 40%;
cursor: pointer;
}
.section #upload,
.section #submit {
display: none;
}
.section .songList {
height: 600px;
background: #fff;
}
.section .songList .title {
padding: 16px 15px;
background: #e7e5e5;
}
.section .songList .title span {
border-left: 5px solid #FF894F;
padding-left: 10px;
line-height: 16px;
}
.section .songList ul li {
height: 60px;
display: flex;
border-bottom: 1px solid #f5f5f5;
align-items: center;
padding: 0 10px;
}
.section .songList ul li .attention {
flex: 0 0 20px;
height: 20px;
width: 20px;
background: url('../images/attention.png') no-repeat;
background-size: 100% 100%;
margin-right: 8px;
}
.section .songList ul li .progress {
flex: 0 0 180px;
width: 180px;
height: 14px;
background: #e7e5e5;
border-radius: 7px;
overflow: hidden;
}
.section .songList ul li .progress .bar {
width: 0;
background: #ffb400;
height: 100%;
transition: all 0.2s;
}
.section .songList ul li .download {
flex: 0 0 20px;
height: 20px;
width: 20px;
background: url('../images/download.png') no-repeat;
background-size: 100% 100%;
margin-right: 8px;
}
.section .songList ul li .delete {
flex: 0 0 20px;
height: 20px;
width: 20px;
background: url('../images/delete.png') no-repeat;
background-size: 100% 100%;
}
.section .songList ul li .songName {
flex: 4;
}
.section .songList ul li .songSize {
flex: 1;
}

View File

@@ -0,0 +1,90 @@
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header,
menu, nav, output, ruby, section, summary,
time, mark, audio, video, input {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a {
color: #7e8c8d;
text-decoration: none;
-webkit-backface-visibility: hidden;
}
li {
list-style: none;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece {
background-color: rgba(0, 0, 0, 0.2);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
-webkit-border-radius: 6px;
}
html, body {
width: 100%;
}
body {
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,54 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
<title>一键上传歌曲</title>
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div class="container">
<div class="header">
<img src="images/header.png" alt="">
</div>
<div class="section">
<form method="post" action="index.html" enctype="multipart/form-data" accept-charset="utf-8">
<div class="uploadBtn">
<img src="images/button.png" alt="">
</div>
<input type="file" name="upload" id="upload" accept="audio/mpeg" multiple>
<input type="submit" value="Submit" id="submit">
</form>
<div class="songList">
<div class="title"><span>歌曲列表</span></div>
<ul id="uploadingList">
<!-- <li>
<i class="attention"></i>
<p class="songName">马博-菊花爆满山.mp3</p>
<div class="progress">
<div class="bar"></div>
</div>
</li> -->
</ul>
<ul id="uploadingDone">
<!-- <li>
<i class="download"></i>
<p class="songName">马博-菊花爆满山.mp3</p>
<p class="songSize">4.40MB</p>
<i class="delete"></i>
</li> -->
</ul>
</div>
</div>
</div>
</body>
<script src="js/jquery-3.2.1.min.js"></script>
<script src="js/jquery.iframe-transport.js"></script>
<script src="js/jquery.ui.widget.js"></script>
<script src="js/jquery.fileupload.js"></script>
<script src="js/index.js"></script>
</html>

View File

@@ -0,0 +1,49 @@
$(function(){
$('.uploadBtn').on('click',function(){
$('#upload').click();
})
$('#upload').fileupload({
dropZone: $(document),
pasteZone: null,
autoUpload: true,
sequentialUploads: true,
dataType: 'json',
url:'./upload.html',
type: 'POST',
add: function(e,data){
console.log(data,'添加文件时执行的操作');
var $li = '<li><i class="attention"></i><p class="songName">'+data.files[0].name+'</p><div class="progress"><div class="bar"></div></div></li>';
data.context = $($li).appendTo('#uploadingList');
console.log(data.context);
var jqXHR = data.submit();
},
progress: function (e, data) {//上传进度
var progress = parseInt(data.loaded / data.total * 100, 10);
console.log(progress,data);
// $(".progress .bar").css("width", progress + "%");
data.context.find(".bar").css("width", progress + "%");
},
done:function(e,data){
console.log('上传完毕');
},
always: function(e, data) {
// 每次传输后(包括成功,失败,被拒执行的回调)
data.context.remove();
var $li = $('<li />');
$li.html('<p class="songName">'+data.files[0].name+'</p><p class="songSize">'+formatFileSize(data.files[0].size)+'</p>');
$('#uploadingDone').append($li);
}
})
})
function formatFileSize(bytes) {
if (bytes >= 1000000000) {
return (bytes / 1000000000).toFixed(2) + ' GB';
}
if (bytes >= 1000000) {
return (bytes / 1000000).toFixed(2) + ' MB';
}
return (bytes / 1000).toFixed(2) + ' KB';
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,224 @@
/*
* jQuery Iframe Transport Plugin
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*/
/* global define, require, window, document, JSON */
;(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS:
factory(require('jquery'));
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0,
jsonAPI = $,
jsonParse = 'parseJSON';
if ('JSON' in window && 'parse' in JSON) {
jsonAPI = JSON;
jsonParse = 'parse';
}
// The iframe transport accepts four additional options:
// options.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s),
// can be a string or an array of strings.
// options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
// options.initialIframeSrc: the URL of the initial iframe src,
// by default set to "javascript:false;"
$.ajaxTransport('iframe', function (options) {
if (options.async) {
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6:
/*jshint scripturl: true */
var initialIframeSrc = options.initialIframeSrc || 'javascript:false;',
/*jshint scripturl: false */
form,
iframe,
addParamChar;
return {
send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset);
addParamChar = /\?/.test(options.url) ? '&' : '?';
// XDomainRequest only supports GET and POST:
if (options.type === 'DELETE') {
options.url = options.url + addParamChar + '_method=DELETE';
options.type = 'POST';
} else if (options.type === 'PUT') {
options.url = options.url + addParamChar + '_method=PUT';
options.type = 'POST';
} else if (options.type === 'PATCH') {
options.url = options.url + addParamChar + '_method=PATCH';
options.type = 'POST';
}
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
counter += 1;
iframe = $(
'<iframe src="' + initialIframeSrc +
'" name="iframe-transport-' + counter + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="' + initialIframeSrc + '"></iframe>')
.appendTo(form);
window.setTimeout(function () {
// Removing the form in a setTimeout call
// allows Chrome's developer tools to display
// the response result
form.remove();
}, 0);
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
// Remove the HTML5 form attribute from the input(s):
options.fileInput.removeAttr('form');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
// Restore the original name and form properties:
$(input)
.prop('name', clone.prop('name'))
.attr('form', clone.attr('form'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', initialIframeSrc);
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, xml
// and script.
// Please note that the Content-Type for JSON responses has to be text/plain
// or text/html, if the browser doesn't include application/json in the
// Accept header, else IE will show a download dialog.
// The Content-Type for XML responses on the other hand has to be always
// application/xml or text/xml, so IE properly parses the XML response.
// See also
// https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && jsonAPI[jsonParse]($(iframe[0].body).text());
},
'iframe html': function (iframe) {
return iframe && $(iframe[0].body).html();
},
'iframe xml': function (iframe) {
var xmlDoc = iframe && iframe[0];
return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc :
$.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) ||
$(xmlDoc.body).html());
},
'iframe script': function (iframe) {
return iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));

View File

@@ -0,0 +1,572 @@
/*! jQuery UI - v1.11.4+CommonJS - 2015-08-28
* http://jqueryui.com
* Includes: widget.js
* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */
(function( factory ) {
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
define([ "jquery" ], factory );
} else if ( typeof exports === "object" ) {
// Node/CommonJS
factory( require( "jquery" ) );
} else {
// Browser globals
factory( jQuery );
}
}(function( $ ) {
/*!
* jQuery UI Widget 1.11.4
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/jQuery.widget/
*/
var widget_uuid = 0,
widget_slice = Array.prototype.slice;
$.cleanData = (function( orig ) {
return function( elems ) {
var events, elem, i;
for ( i = 0; (elem = elems[i]) != null; i++ ) {
try {
// Only trigger remove when necessary to save time
events = $._data( elem, "events" );
if ( events && events.remove ) {
$( elem ).triggerHandler( "remove" );
}
// http://bugs.jquery.com/ticket/8235
} catch ( e ) {}
}
orig( elems );
};
})( $.cleanData );
$.widget = function( name, base, prototype ) {
var fullName, existingConstructor, constructor, basePrototype,
// proxiedPrototype allows the provided prototype to remain unmodified
// so that it can be used as a mixin for multiple widgets (#8876)
proxiedPrototype = {},
namespace = name.split( "." )[ 0 ];
name = name.split( "." )[ 1 ];
fullName = namespace + "-" + name;
if ( !prototype ) {
prototype = base;
base = $.Widget;
}
// create selector for plugin
$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
return !!$.data( elem, fullName );
};
$[ namespace ] = $[ namespace ] || {};
existingConstructor = $[ namespace ][ name ];
constructor = $[ namespace ][ name ] = function( options, element ) {
// allow instantiation without "new" keyword
if ( !this._createWidget ) {
return new constructor( options, element );
}
// allow instantiation without initializing for simple inheritance
// must use "new" keyword (the code above always passes args)
if ( arguments.length ) {
this._createWidget( options, element );
}
};
// extend with the existing constructor to carry over any static properties
$.extend( constructor, existingConstructor, {
version: prototype.version,
// copy the object used to create the prototype in case we need to
// redefine the widget later
_proto: $.extend( {}, prototype ),
// track widgets that inherit from this widget in case this widget is
// redefined after a widget inherits from it
_childConstructors: []
});
basePrototype = new base();
// we need to make the options hash a property directly on the new instance
// otherwise we'll modify the options hash on the prototype that we're
// inheriting from
basePrototype.options = $.widget.extend( {}, basePrototype.options );
$.each( prototype, function( prop, value ) {
if ( !$.isFunction( value ) ) {
proxiedPrototype[ prop ] = value;
return;
}
proxiedPrototype[ prop ] = (function() {
var _super = function() {
return base.prototype[ prop ].apply( this, arguments );
},
_superApply = function( args ) {
return base.prototype[ prop ].apply( this, args );
};
return function() {
var __super = this._super,
__superApply = this._superApply,
returnValue;
this._super = _super;
this._superApply = _superApply;
returnValue = value.apply( this, arguments );
this._super = __super;
this._superApply = __superApply;
return returnValue;
};
})();
});
constructor.prototype = $.widget.extend( basePrototype, {
// TODO: remove support for widgetEventPrefix
// always use the name + a colon as the prefix, e.g., draggable:start
// don't prefix for widgets that aren't DOM-based
widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name
}, proxiedPrototype, {
constructor: constructor,
namespace: namespace,
widgetName: name,
widgetFullName: fullName
});
// If this widget is being redefined then we need to find all widgets that
// are inheriting from it and redefine all of them so that they inherit from
// the new version of this widget. We're essentially trying to replace one
// level in the prototype chain.
if ( existingConstructor ) {
$.each( existingConstructor._childConstructors, function( i, child ) {
var childPrototype = child.prototype;
// redefine the child widget using the same prototype that was
// originally used, but inherit from the new version of the base
$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
});
// remove the list of existing child constructors from the old constructor
// so the old child constructors can be garbage collected
delete existingConstructor._childConstructors;
} else {
base._childConstructors.push( constructor );
}
$.widget.bridge( name, constructor );
return constructor;
};
$.widget.extend = function( target ) {
var input = widget_slice.call( arguments, 1 ),
inputIndex = 0,
inputLength = input.length,
key,
value;
for ( ; inputIndex < inputLength; inputIndex++ ) {
for ( key in input[ inputIndex ] ) {
value = input[ inputIndex ][ key ];
if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
// Clone objects
if ( $.isPlainObject( value ) ) {
target[ key ] = $.isPlainObject( target[ key ] ) ?
$.widget.extend( {}, target[ key ], value ) :
// Don't extend strings, arrays, etc. with objects
$.widget.extend( {}, value );
// Copy everything else by reference
} else {
target[ key ] = value;
}
}
}
}
return target;
};
$.widget.bridge = function( name, object ) {
var fullName = object.prototype.widgetFullName || name;
$.fn[ name ] = function( options ) {
var isMethodCall = typeof options === "string",
args = widget_slice.call( arguments, 1 ),
returnValue = this;
if ( isMethodCall ) {
this.each(function() {
var methodValue,
instance = $.data( this, fullName );
if ( options === "instance" ) {
returnValue = instance;
return false;
}
if ( !instance ) {
return $.error( "cannot call methods on " + name + " prior to initialization; " +
"attempted to call method '" + options + "'" );
}
if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
return $.error( "no such method '" + options + "' for " + name + " widget instance" );
}
methodValue = instance[ options ].apply( instance, args );
if ( methodValue !== instance && methodValue !== undefined ) {
returnValue = methodValue && methodValue.jquery ?
returnValue.pushStack( methodValue.get() ) :
methodValue;
return false;
}
});
} else {
// Allow multiple hashes to be passed on init
if ( args.length ) {
options = $.widget.extend.apply( null, [ options ].concat(args) );
}
this.each(function() {
var instance = $.data( this, fullName );
if ( instance ) {
instance.option( options || {} );
if ( instance._init ) {
instance._init();
}
} else {
$.data( this, fullName, new object( options, this ) );
}
});
}
return returnValue;
};
};
$.Widget = function( /* options, element */ ) {};
$.Widget._childConstructors = [];
$.Widget.prototype = {
widgetName: "widget",
widgetEventPrefix: "",
defaultElement: "<div>",
options: {
disabled: false,
// callbacks
create: null
},
_createWidget: function( options, element ) {
element = $( element || this.defaultElement || this )[ 0 ];
this.element = $( element );
this.uuid = widget_uuid++;
this.eventNamespace = "." + this.widgetName + this.uuid;
this.bindings = $();
this.hoverable = $();
this.focusable = $();
if ( element !== this ) {
$.data( element, this.widgetFullName, this );
this._on( true, this.element, {
remove: function( event ) {
if ( event.target === element ) {
this.destroy();
}
}
});
this.document = $( element.style ?
// element within the document
element.ownerDocument :
// element is window or document
element.document || element );
this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
}
this.options = $.widget.extend( {},
this.options,
this._getCreateOptions(),
options );
this._create();
this._trigger( "create", null, this._getCreateEventData() );
this._init();
},
_getCreateOptions: $.noop,
_getCreateEventData: $.noop,
_create: $.noop,
_init: $.noop,
destroy: function() {
this._destroy();
// we can probably remove the unbind calls in 2.0
// all event bindings should go through this._on()
this.element
.unbind( this.eventNamespace )
.removeData( this.widgetFullName )
// support: jquery <1.6.3
// http://bugs.jquery.com/ticket/9413
.removeData( $.camelCase( this.widgetFullName ) );
this.widget()
.unbind( this.eventNamespace )
.removeAttr( "aria-disabled" )
.removeClass(
this.widgetFullName + "-disabled " +
"ui-state-disabled" );
// clean up events and states
this.bindings.unbind( this.eventNamespace );
this.hoverable.removeClass( "ui-state-hover" );
this.focusable.removeClass( "ui-state-focus" );
},
_destroy: $.noop,
widget: function() {
return this.element;
},
option: function( key, value ) {
var options = key,
parts,
curOption,
i;
if ( arguments.length === 0 ) {
// don't return a reference to the internal hash
return $.widget.extend( {}, this.options );
}
if ( typeof key === "string" ) {
// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
options = {};
parts = key.split( "." );
key = parts.shift();
if ( parts.length ) {
curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
for ( i = 0; i < parts.length - 1; i++ ) {
curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
curOption = curOption[ parts[ i ] ];
}
key = parts.pop();
if ( arguments.length === 1 ) {
return curOption[ key ] === undefined ? null : curOption[ key ];
}
curOption[ key ] = value;
} else {
if ( arguments.length === 1 ) {
return this.options[ key ] === undefined ? null : this.options[ key ];
}
options[ key ] = value;
}
}
this._setOptions( options );
return this;
},
_setOptions: function( options ) {
var key;
for ( key in options ) {
this._setOption( key, options[ key ] );
}
return this;
},
_setOption: function( key, value ) {
this.options[ key ] = value;
if ( key === "disabled" ) {
this.widget()
.toggleClass( this.widgetFullName + "-disabled", !!value );
// If the widget is becoming disabled, then nothing is interactive
if ( value ) {
this.hoverable.removeClass( "ui-state-hover" );
this.focusable.removeClass( "ui-state-focus" );
}
}
return this;
},
enable: function() {
return this._setOptions({ disabled: false });
},
disable: function() {
return this._setOptions({ disabled: true });
},
_on: function( suppressDisabledCheck, element, handlers ) {
var delegateElement,
instance = this;
// no suppressDisabledCheck flag, shuffle arguments
if ( typeof suppressDisabledCheck !== "boolean" ) {
handlers = element;
element = suppressDisabledCheck;
suppressDisabledCheck = false;
}
// no element argument, shuffle and use this.element
if ( !handlers ) {
handlers = element;
element = this.element;
delegateElement = this.widget();
} else {
element = delegateElement = $( element );
this.bindings = this.bindings.add( element );
}
$.each( handlers, function( event, handler ) {
function handlerProxy() {
// allow widgets to customize the disabled handling
// - disabled as an array instead of boolean
// - disabled class as method for disabling individual parts
if ( !suppressDisabledCheck &&
( instance.options.disabled === true ||
$( this ).hasClass( "ui-state-disabled" ) ) ) {
return;
}
return ( typeof handler === "string" ? instance[ handler ] : handler )
.apply( instance, arguments );
}
// copy the guid so direct unbinding works
if ( typeof handler !== "string" ) {
handlerProxy.guid = handler.guid =
handler.guid || handlerProxy.guid || $.guid++;
}
var match = event.match( /^([\w:-]*)\s*(.*)$/ ),
eventName = match[1] + instance.eventNamespace,
selector = match[2];
if ( selector ) {
delegateElement.delegate( selector, eventName, handlerProxy );
} else {
element.bind( eventName, handlerProxy );
}
});
},
_off: function( element, eventName ) {
eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) +
this.eventNamespace;
element.unbind( eventName ).undelegate( eventName );
// Clear the stack to avoid memory leaks (#10056)
this.bindings = $( this.bindings.not( element ).get() );
this.focusable = $( this.focusable.not( element ).get() );
this.hoverable = $( this.hoverable.not( element ).get() );
},
_delay: function( handler, delay ) {
function handlerProxy() {
return ( typeof handler === "string" ? instance[ handler ] : handler )
.apply( instance, arguments );
}
var instance = this;
return setTimeout( handlerProxy, delay || 0 );
},
_hoverable: function( element ) {
this.hoverable = this.hoverable.add( element );
this._on( element, {
mouseenter: function( event ) {
$( event.currentTarget ).addClass( "ui-state-hover" );
},
mouseleave: function( event ) {
$( event.currentTarget ).removeClass( "ui-state-hover" );
}
});
},
_focusable: function( element ) {
this.focusable = this.focusable.add( element );
this._on( element, {
focusin: function( event ) {
$( event.currentTarget ).addClass( "ui-state-focus" );
},
focusout: function( event ) {
$( event.currentTarget ).removeClass( "ui-state-focus" );
}
});
},
_trigger: function( type, event, data ) {
var prop, orig,
callback = this.options[ type ];
data = data || {};
event = $.Event( event );
event.type = ( type === this.widgetEventPrefix ?
type :
this.widgetEventPrefix + type ).toLowerCase();
// the original event may come from any element
// so we need to reset the target on the new event
event.target = this.element[ 0 ];
// copy original event properties over to the new event
orig = event.originalEvent;
if ( orig ) {
for ( prop in orig ) {
if ( !( prop in event ) ) {
event[ prop ] = orig[ prop ];
}
}
}
this.element.trigger( event, data );
return !( $.isFunction( callback ) &&
callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
event.isDefaultPrevented() );
}
};
$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
if ( typeof options === "string" ) {
options = { effect: options };
}
var hasOptions,
effectName = !options ?
method :
options === true || typeof options === "number" ?
defaultEffect :
options.effect || defaultEffect;
options = options || {};
if ( typeof options === "number" ) {
options = { duration: options };
}
hasOptions = !$.isEmptyObject( options );
options.complete = callback;
if ( options.delay ) {
element.delay( options.delay );
}
if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
element[ method ]( options );
} else if ( effectName !== method && element[ effectName ] ) {
element[ effectName ]( options.duration, options.easing, callback );
} else {
element.queue(function( next ) {
$( this )[ method ]();
if ( callback ) {
callback.call( element[ 0 ] );
}
next();
});
}
};
});
var widget = $.widget;
}));

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
</head>
<body>
%MyFiles%
</body>
</html>