nextcloud-desktop/shell_integration/MacOSX/OwnCloudFinder/GCDAsyncSocket.m

6970 lines
182 KiB
Mathematica
Raw Normal View History

//
// GCDAsyncSocket.m
//
// This class is in the public domain.
// Originally created by Robbie Hanson in Q4 2010.
// Updated and maintained by Deusty LLC and the Mac development community.
//
// http://code.google.com/p/cocoaasyncsocket/
//
#import "GCDAsyncSocket.h"
#if TARGET_OS_IPHONE
#import <CFNetwork/CFNetwork.h>
#endif
#import <arpa/inet.h>
#import <fcntl.h>
#import <ifaddrs.h>
#import <netdb.h>
#import <netinet/in.h>
#import <net/if.h>
#import <sys/socket.h>
#import <sys/types.h>
#import <sys/ioctl.h>
#import <sys/poll.h>
#import <sys/uio.h>
#import <unistd.h>
#if 0
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// http://code.google.com/p/cocoalumberjack/
//
// It allows us to do a lot of logging without significantly slowing down the code.
#import "DDLog.h"
#define LogAsync YES
#define LogContext 65535
#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
// Log levels : off, error, warn, info, verbose
static const int logLevel = LOG_LEVEL_VERBOSE;
#else
// Logging Disabled
#define LogError(frmt, ...) {}
#define LogWarn(frmt, ...) {}
#define LogInfo(frmt, ...) {}
#define LogVerbose(frmt, ...) {}
#define LogCError(frmt, ...) {}
#define LogCWarn(frmt, ...) {}
#define LogCInfo(frmt, ...) {}
#define LogCVerbose(frmt, ...) {}
#define LogTrace() {}
#define LogCTrace(frmt, ...) {}
#endif
/**
* Seeing a return statements within an inner block
* can sometimes be mistaken for a return point of the enclosing method.
* This makes inline blocks a bit easier to read.
**/
#define return_from_block return
/**
* A socket file descriptor is really just an integer.
* It represents the index of the socket within the kernel.
* This makes invalid file descriptor comparisons easier to read.
**/
#define SOCKET_NULL -1
NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException";
NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain";
#if !TARGET_OS_IPHONE
NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites";
NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters";
#endif
enum GCDAsyncSocketFlags
{
kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting)
kConnected = 1 << 1, // If set, the socket is connected
kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed
kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout
kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout
kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued
kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued
kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown.
kReadSourceSuspended = 1 << 8, // If set, the read source is suspended
kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended
kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS
kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete
kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete
kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS
kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket
kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained
#if TARGET_OS_IPHONE
kAddedStreamListener = 1 << 16, // If set, CFStreams have been added to listener thread
kSecureSocketHasBytesAvailable = 1 << 17, // If set, CFReadStream has notified us of bytes available
#endif
};
enum GCDAsyncSocketConfig
{
kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled
kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled
kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4
kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes
};
#if TARGET_OS_IPHONE
static NSThread *listenerThread; // Used for CFStreams
#endif
@interface GCDAsyncSocket (Private)
// Accepting
- (BOOL)doAccept:(int)socketFD;
// Connecting
- (void)startConnectTimeout:(NSTimeInterval)timeout;
- (void)endConnectTimeout;
- (void)doConnectTimeout;
- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port;
- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6;
- (void)lookup:(int)aConnectIndex didFail:(NSError *)error;
- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr;
- (void)didConnect:(int)aConnectIndex;
- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error;
// Disconnect
- (void)closeWithError:(NSError *)error;
- (void)close;
- (void)maybeClose;
// Errors
- (NSError *)badConfigError:(NSString *)msg;
- (NSError *)badParamError:(NSString *)msg;
- (NSError *)gaiError:(int)gai_error;
- (NSError *)errnoError;
- (NSError *)errnoErrorWithReason:(NSString *)reason;
- (NSError *)connectTimeoutError;
- (NSError *)otherError:(NSString *)msg;
// Diagnostics
- (NSString *)connectedHost4;
- (NSString *)connectedHost6;
- (uint16_t)connectedPort4;
- (uint16_t)connectedPort6;
- (NSString *)localHost4;
- (NSString *)localHost6;
- (uint16_t)localPort4;
- (uint16_t)localPort6;
- (NSString *)connectedHostFromSocket4:(int)socketFD;
- (NSString *)connectedHostFromSocket6:(int)socketFD;
- (uint16_t)connectedPortFromSocket4:(int)socketFD;
- (uint16_t)connectedPortFromSocket6:(int)socketFD;
- (NSString *)localHostFromSocket4:(int)socketFD;
- (NSString *)localHostFromSocket6:(int)socketFD;
- (uint16_t)localPortFromSocket4:(int)socketFD;
- (uint16_t)localPortFromSocket6:(int)socketFD;
// Utilities
- (void)getInterfaceAddress4:(NSMutableData **)addr4Ptr
address6:(NSMutableData **)addr6Ptr
fromDescription:(NSString *)interfaceDescription
port:(uint16_t)port;
- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD;
- (void)suspendReadSource;
- (void)resumeReadSource;
- (void)suspendWriteSource;
- (void)resumeWriteSource;
// Reading
- (void)maybeDequeueRead;
- (void)flushSSLBuffers;
- (void)doReadData;
- (void)doReadEOF;
- (void)completeCurrentRead;
- (void)endCurrentRead;
- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout;
- (void)doReadTimeout;
- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension;
// Writing
- (void)maybeDequeueWrite;
- (void)doWriteData;
- (void)completeCurrentWrite;
- (void)endCurrentWrite;
- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout;
- (void)doWriteTimeout;
- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension;
// Security
- (void)maybeStartTLS;
#if !TARGET_OS_IPHONE
- (void)continueSSLHandshake;
#endif
// CFStream
#if TARGET_OS_IPHONE
+ (void)startListenerThreadIfNeeded;
- (BOOL)createReadAndWriteStream;
- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite;
- (BOOL)addStreamsToRunLoop;
- (BOOL)openStreams;
- (void)removeStreamsFromRunLoop;
#endif
// Class Methods
+ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
+ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
+ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
+ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The GCDAsyncReadPacket encompasses the instructions for any given read.
* The content of a read packet allows the code to determine if we're:
* - reading to a certain length
* - reading to a certain separator
* - or simply reading the first chunk of available data
**/
@interface GCDAsyncReadPacket : NSObject
{
@public
NSMutableData *buffer;
NSUInteger startOffset;
NSUInteger bytesDone;
NSUInteger maxLength;
NSTimeInterval timeout;
NSUInteger readLength;
NSData *term;
BOOL bufferOwner;
NSUInteger originalBufferLength;
long tag;
}
- (id)initWithData:(NSMutableData *)d
startOffset:(NSUInteger)s
maxLength:(NSUInteger)m
timeout:(NSTimeInterval)t
readLength:(NSUInteger)l
terminator:(NSData *)e
tag:(long)i;
- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;
- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
- (NSUInteger)readLengthForTermWithPreBuffer:(NSData *)preBuffer found:(BOOL *)foundPtr;
- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;
@end
@implementation GCDAsyncReadPacket
- (id)initWithData:(NSMutableData *)d
startOffset:(NSUInteger)s
maxLength:(NSUInteger)m
timeout:(NSTimeInterval)t
readLength:(NSUInteger)l
terminator:(NSData *)e
tag:(long)i
{
if((self = [super init]))
{
bytesDone = 0;
maxLength = m;
timeout = t;
readLength = l;
term = [e copy];
tag = i;
if (d)
{
buffer = [d retain];
startOffset = s;
bufferOwner = NO;
originalBufferLength = [d length];
}
else
{
if (readLength > 0)
buffer = [[NSMutableData alloc] initWithLength:readLength];
else
buffer = [[NSMutableData alloc] initWithLength:0];
startOffset = 0;
bufferOwner = YES;
originalBufferLength = 0;
}
}
return self;
}
/**
* Increases the length of the buffer (if needed) to ensure a read of the given size will fit.
**/
- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead
{
NSUInteger buffSize = [buffer length];
NSUInteger buffUsed = startOffset + bytesDone;
NSUInteger buffSpace = buffSize - buffUsed;
if (bytesToRead > buffSpace)
{
NSUInteger buffInc = bytesToRead - buffSpace;
[buffer increaseLengthBy:buffInc];
}
}
/**
* This method is used when we do NOT know how much data is available to be read from the socket.
* This method returns the default value unless it exceeds the specified readLength or maxLength.
*
* Furthermore, the shouldPreBuffer decision is based upon the packet type,
* and whether the returned value would fit in the current buffer without requiring a resize of the buffer.
**/
- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr
{
NSUInteger result;
if (readLength > 0)
{
// Read a specific length of data
result = MIN(defaultValue, (readLength - bytesDone));
// There is no need to prebuffer since we know exactly how much data we need to read.
// Even if the buffer isn't currently big enough to fit this amount of data,
// it would have to be resized eventually anyway.
if (shouldPreBufferPtr)
*shouldPreBufferPtr = NO;
}
else
{
// Either reading until we find a specified terminator,
// or we're simply reading all available data.
//
// In other words, one of:
//
// - readDataToData packet
// - readDataWithTimeout packet
if (maxLength > 0)
result = MIN(defaultValue, (maxLength - bytesDone));
else
result = defaultValue;
// Since we don't know the size of the read in advance,
// the shouldPreBuffer decision is based upon whether the returned value would fit
// in the current buffer without requiring a resize of the buffer.
//
// This is because, in all likelyhood, the amount read from the socket will be less than the default value.
// Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead.
if (shouldPreBufferPtr)
{
NSUInteger buffSize = [buffer length];
NSUInteger buffUsed = startOffset + bytesDone;
NSUInteger buffSpace = buffSize - buffUsed;
if (buffSpace >= result)
*shouldPreBufferPtr = NO;
else
*shouldPreBufferPtr = YES;
}
}
return result;
}
/**
* For read packets without a set terminator, returns the amount of data
* that can be read without exceeding the readLength or maxLength.
*
* The given parameter indicates the number of bytes estimated to be available on the socket,
* which is taken into consideration during the calculation.
*
* The given hint MUST be greater than zero.
**/
- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable
{
NSAssert(term == nil, @"This method does not apply to term reads");
NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable");
if (readLength > 0)
{
// Read a specific length of data
return MIN(bytesAvailable, (readLength - bytesDone));
// No need to avoid resizing the buffer.
// If the user provided their own buffer,
// and told us to read a certain length of data that exceeds the size of the buffer,
// then it is clear that our code will resize the buffer during the read operation.
//
// This method does not actually do any resizing.
// The resizing will happen elsewhere if needed.
}
else
{
// Read all available data
NSUInteger result = bytesAvailable;
if (maxLength > 0)
{
result = MIN(result, (maxLength - bytesDone));
}
// No need to avoid resizing the buffer.
// If the user provided their own buffer,
// and told us to read all available data without giving us a maxLength,
// then it is clear that our code might resize the buffer during the read operation.
//
// This method does not actually do any resizing.
// The resizing will happen elsewhere if needed.
return result;
}
}
/**
* For read packets with a set terminator, returns the amount of data
* that can be read without exceeding the maxLength.
*
* The given parameter indicates the number of bytes estimated to be available on the socket,
* which is taken into consideration during the calculation.
*
* To optimize memory allocations, mem copies, and mem moves
* the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first,
* or if the data can be read directly into the read packet's buffer.
**/
- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr
{
NSAssert(term != nil, @"This method does not apply to non-term reads");
NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable");
NSUInteger result = bytesAvailable;
if (maxLength > 0)
{
result = MIN(result, (maxLength - bytesDone));
}
// Should the data be read into the read packet's buffer, or into a pre-buffer first?
//
// One would imagine the preferred option is the faster one.
// So which one is faster?
//
// Reading directly into the packet's buffer requires:
// 1. Possibly resizing packet buffer (malloc/realloc)
// 2. Filling buffer (read)
// 3. Searching for term (memcmp)
// 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy)
//
// Reading into prebuffer first:
// 1. Possibly resizing prebuffer (malloc/realloc)
// 2. Filling buffer (read)
// 3. Searching for term (memcmp)
// 4. Copying underflow into packet buffer (malloc/realloc, memcpy)
// 5. Removing underflow from prebuffer (memmove)
//
// Comparing the performance of the two we can see that reading
// data into the prebuffer first is slower due to the extra memove.
//
// However:
// The implementation of NSMutableData is open source via core foundation's CFMutableData.
// Decreasing the length of a mutable data object doesn't cause a realloc.
// In other words, the capacity of a mutable data object can grow, but doesn't shrink.
//
// This means the prebuffer will rarely need a realloc.
// The packet buffer, on the other hand, may often need a realloc.
// This is especially true if we are the buffer owner.
// Furthermore, if we are constantly realloc'ing the packet buffer,
// and then moving the overflow into the prebuffer,
// then we're consistently over-allocating memory for each term read.
// And now we get into a bit of a tradeoff between speed and memory utilization.
//
// The end result is that the two perform very similarly.
// And we can answer the original question very simply by another means.
//
// If we can read all the data directly into the packet's buffer without resizing it first,
// then we do so. Otherwise we use the prebuffer.
if (shouldPreBufferPtr)
{
NSUInteger buffSize = [buffer length];
NSUInteger buffUsed = startOffset + bytesDone;
if ((buffSize - buffUsed) >= result)
*shouldPreBufferPtr = NO;
else
*shouldPreBufferPtr = YES;
}
return result;
}
/**
* For read packets with a set terminator,
* returns the amount of data that can be read from the given preBuffer,
* without going over a terminator or the maxLength.
*
* It is assumed the terminator has not already been read.
**/
- (NSUInteger)readLengthForTermWithPreBuffer:(NSData *)preBuffer found:(BOOL *)foundPtr
{
NSAssert(term != nil, @"This method does not apply to non-term reads");
NSAssert([preBuffer length] > 0, @"Invoked with empty pre buffer!");
// We know that the terminator, as a whole, doesn't exist in our own buffer.
// But it is possible that a portion of it exists in our buffer.
// So we're going to look for the terminator starting with a portion of our own buffer.
//
// Example:
//
// term length = 3 bytes
// bytesDone = 5 bytes
// preBuffer length = 5 bytes
//
// If we append the preBuffer to our buffer,
// it would look like this:
//
// ---------------------
// |B|B|B|B|B|P|P|P|P|P|
// ---------------------
//
// So we start our search here:
//
// ---------------------
// |B|B|B|B|B|P|P|P|P|P|
// -------^-^-^---------
//
// And move forwards...
//
// ---------------------
// |B|B|B|B|B|P|P|P|P|P|
// ---------^-^-^-------
//
// Until we find the terminator or reach the end.
//
// ---------------------
// |B|B|B|B|B|P|P|P|P|P|
// ---------------^-^-^-
BOOL found = NO;
NSUInteger termLength = [term length];
NSUInteger preBufferLength = [preBuffer length];
if ((bytesDone + preBufferLength) < termLength)
{
// Not enough data for a full term sequence yet
return preBufferLength;
}
NSUInteger maxPreBufferLength;
if (maxLength > 0) {
maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone));
// Note: maxLength >= termLength
}
else {
maxPreBufferLength = preBufferLength;
}
uint8_t seq[termLength];
const void *termBuf = [term bytes];
NSUInteger bufLen = MIN(bytesDone, (termLength - 1));
uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen;
NSUInteger preLen = termLength - bufLen;
const uint8_t *pre = [preBuffer bytes];
NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above.
NSUInteger result = preBufferLength;
NSUInteger i;
for (i = 0; i < loopCount; i++)
{
if (bufLen > 0)
{
// Combining bytes from buffer and preBuffer
memcpy(seq, buf, bufLen);
memcpy(seq + bufLen, pre, preLen);
if (memcmp(seq, termBuf, termLength) == 0)
{
result = preLen;
found = YES;
break;
}
buf++;
bufLen--;
preLen++;
}
else
{
// Comparing directly from preBuffer
if (memcmp(pre, termBuf, termLength) == 0)
{
NSUInteger preOffset = pre - (const uint8_t *)[preBuffer bytes]; // pointer arithmetic
result = preOffset + termLength;
found = YES;
break;
}
pre++;
}
}
// There is no need to avoid resizing the buffer in this particular situation.
if (foundPtr) *foundPtr = found;
return result;
}
/**
* For read packets with a set terminator, scans the packet buffer for the term.
* It is assumed the terminator had not been fully read prior to the new bytes.
*
* If the term is found, the number of excess bytes after the term are returned.
* If the term is not found, this method will return -1.
*
* Note: A return value of zero means the term was found at the very end.
*
* Prerequisites:
* The given number of bytes have been added to the end of our buffer.
* Our bytesDone variable has NOT been changed due to the prebuffered bytes.
**/
- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes
{
NSAssert(term != nil, @"This method does not apply to non-term reads");
// The implementation of this method is very similar to the above method.
// See the above method for a discussion of the algorithm used here.
uint8_t *buff = [buffer mutableBytes];
NSUInteger buffLength = bytesDone + numBytes;
const void *termBuff = [term bytes];
NSUInteger termLength = [term length];
// Note: We are dealing with unsigned integers,
// so make sure the math doesn't go below zero.
NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0;
while (i + termLength <= buffLength)
{
uint8_t *subBuffer = buff + startOffset + i;
if (memcmp(subBuffer, termBuff, termLength) == 0)
{
return buffLength - (i + termLength);
}
i++;
}
return -1;
}
- (void)dealloc
{
[buffer release];
[term release];
[super dealloc];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The GCDAsyncWritePacket encompasses the instructions for any given write.
**/
@interface GCDAsyncWritePacket : NSObject
{
@public
NSData *buffer;
NSUInteger bytesDone;
long tag;
NSTimeInterval timeout;
}
- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i;
@end
@implementation GCDAsyncWritePacket
- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i
{
if((self = [super init]))
{
buffer = [d retain]; // Retain not copy. For performance as documented in header file.
bytesDone = 0;
timeout = t;
tag = i;
}
return self;
}
- (void)dealloc
{
[buffer release];
[super dealloc];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues.
* This class my be altered to support more than just TLS in the future.
**/
@interface GCDAsyncSpecialPacket : NSObject
{
@public
NSDictionary *tlsSettings;
}
- (id)initWithTLSSettings:(NSDictionary *)settings;
@end
@implementation GCDAsyncSpecialPacket
- (id)initWithTLSSettings:(NSDictionary *)settings
{
if((self = [super init]))
{
tlsSettings = [settings copy];
}
return self;
}
- (void)dealloc
{
[tlsSettings release];
[super dealloc];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation GCDAsyncSocket
- (id)init
{
return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL];
}
- (id)initWithSocketQueue:(dispatch_queue_t)sq
{
return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq];
}
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
{
return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL];
}
- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
{
if((self = [super init]))
{
delegate = aDelegate;
if (dq)
{
dispatch_retain(dq);
delegateQueue = dq;
}
socket4FD = SOCKET_NULL;
socket6FD = SOCKET_NULL;
connectIndex = 0;
if (sq)
{
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
@"The given socketQueue parameter must not be a concurrent queue.");
dispatch_retain(sq);
socketQueue = sq;
}
else
{
socketQueue = dispatch_queue_create("GCDAsyncSocket", NULL);
}
readQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentRead = nil;
writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentWrite = nil;
partialReadBuffer = [[NSMutableData alloc] init];
}
return self;
}
- (void)dealloc
{
LogInfo(@"%@ - %@ (start)", THIS_METHOD, self);
if (dispatch_get_current_queue() == socketQueue)
{
[self closeWithError:nil];
}
else
{
dispatch_sync(socketQueue, ^{
[self closeWithError:nil];
});
}
delegate = nil;
if (delegateQueue)
dispatch_release(delegateQueue);
delegateQueue = NULL;
dispatch_release(socketQueue);
socketQueue = NULL;
[readQueue release];
[writeQueue release];
[partialReadBuffer release];
#if !TARGET_OS_IPHONE
[sslReadBuffer release];
#endif
[userData release];
LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self);
[super dealloc];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id)delegate
{
if (dispatch_get_current_queue() == socketQueue)
{
return delegate;
}
else
{
__block id result;
dispatch_sync(socketQueue, ^{
result = delegate;
});
return result;
}
}
- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously
{
dispatch_block_t block = ^{
delegate = newDelegate;
};
if (dispatch_get_current_queue() == socketQueue) {
block();
}
else {
if (synchronously)
dispatch_sync(socketQueue, block);
else
dispatch_async(socketQueue, block);
}
}
- (void)setDelegate:(id)newDelegate
{
[self setDelegate:newDelegate synchronously:NO];
}
- (void)synchronouslySetDelegate:(id)newDelegate
{
[self setDelegate:newDelegate synchronously:YES];
}
- (dispatch_queue_t)delegateQueue
{
if (dispatch_get_current_queue() == socketQueue)
{
return delegateQueue;
}
else
{
__block dispatch_queue_t result;
dispatch_sync(socketQueue, ^{
result = delegateQueue;
});
return result;
}
}
- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
{
dispatch_block_t block = ^{
if (delegateQueue)
dispatch_release(delegateQueue);
if (newDelegateQueue)
dispatch_retain(newDelegateQueue);
delegateQueue = newDelegateQueue;
};
if (dispatch_get_current_queue() == socketQueue) {
block();
}
else {
if (synchronously)
dispatch_sync(socketQueue, block);
else
dispatch_async(socketQueue, block);
}
}
- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue
{
[self setDelegateQueue:newDelegateQueue synchronously:NO];
}
- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue
{
[self setDelegateQueue:newDelegateQueue synchronously:YES];
}
- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr
{
if (dispatch_get_current_queue() == socketQueue)
{
if (delegatePtr) *delegatePtr = delegate;
if (delegateQueuePtr) *delegateQueuePtr = delegateQueue;
}
else
{
__block id dPtr = NULL;
__block dispatch_queue_t dqPtr = NULL;
dispatch_sync(socketQueue, ^{
dPtr = delegate;
dqPtr = delegateQueue;
});
if (delegatePtr) *delegatePtr = dPtr;
if (delegateQueuePtr) *delegateQueuePtr = dqPtr;
}
}
- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
{
dispatch_block_t block = ^{
delegate = newDelegate;
if (delegateQueue)
dispatch_release(delegateQueue);
if (newDelegateQueue)
dispatch_retain(newDelegateQueue);
delegateQueue = newDelegateQueue;
};
if (dispatch_get_current_queue() == socketQueue) {
block();
}
else {
if (synchronously)
dispatch_sync(socketQueue, block);
else
dispatch_async(socketQueue, block);
}
}
- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
{
[self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO];
}
- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
{
[self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES];
}
- (BOOL)autoDisconnectOnClosedReadStream
{
// Note: YES means kAllowHalfDuplexConnection is OFF
if (dispatch_get_current_queue() == socketQueue)
{
return ((config & kAllowHalfDuplexConnection) == 0);
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = ((config & kAllowHalfDuplexConnection) == 0);
});
return result;
}
}
- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag
{
// Note: YES means kAllowHalfDuplexConnection is OFF
dispatch_block_t block = ^{
if (flag)
config &= ~kAllowHalfDuplexConnection;
else
config |= kAllowHalfDuplexConnection;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_async(socketQueue, block);
}
- (BOOL)isIPv4Enabled
{
// Note: YES means kIPv4Disabled is OFF
if (dispatch_get_current_queue() == socketQueue)
{
return ((config & kIPv4Disabled) == 0);
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = ((config & kIPv4Disabled) == 0);
});
return result;
}
}
- (void)setIPv4Enabled:(BOOL)flag
{
// Note: YES means kIPv4Disabled is OFF
dispatch_block_t block = ^{
if (flag)
config &= ~kIPv4Disabled;
else
config |= kIPv4Disabled;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_async(socketQueue, block);
}
- (BOOL)isIPv6Enabled
{
// Note: YES means kIPv6Disabled is OFF
if (dispatch_get_current_queue() == socketQueue)
{
return ((config & kIPv6Disabled) == 0);
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = ((config & kIPv6Disabled) == 0);
});
return result;
}
}
- (void)setIPv6Enabled:(BOOL)flag
{
// Note: YES means kIPv6Disabled is OFF
dispatch_block_t block = ^{
if (flag)
config &= ~kIPv6Disabled;
else
config |= kIPv6Disabled;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_async(socketQueue, block);
}
- (BOOL)isIPv4PreferredOverIPv6
{
// Note: YES means kPreferIPv6 is OFF
if (dispatch_get_current_queue() == socketQueue)
{
return ((config & kPreferIPv6) == 0);
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = ((config & kPreferIPv6) == 0);
});
return result;
}
}
- (void)setPreferIPv4OverIPv6:(BOOL)flag
{
// Note: YES means kPreferIPv6 is OFF
dispatch_block_t block = ^{
if (flag)
config &= ~kPreferIPv6;
else
config |= kPreferIPv6;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_async(socketQueue, block);
}
- (id)userData
{
__block id result = nil;
dispatch_block_t block = ^{
result = [userData retain];
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
return [result autorelease];
}
- (void)setUserData:(id)arbitraryUserData
{
dispatch_block_t block = ^{
if (userData != arbitraryUserData)
{
[userData release];
userData = [arbitraryUserData retain];
}
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_async(socketQueue, block);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Accepting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr
{
return [self acceptOnInterface:nil port:port error:errPtr];
}
- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr
{
LogTrace();
// Just in-case interface parameter is immutable.
NSString *interface = [[inInterface copy] autorelease];
__block BOOL result = NO;
__block NSError *err = nil;
// CreateSocket Block
// This block will be invoked within the dispatch block below.
int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
int socketFD = socket(domain, SOCK_STREAM, 0);
if (socketFD == SOCKET_NULL)
{
NSString *reason = @"Error in socket() function";
err = [[self errnoErrorWithReason:reason] retain];
return SOCKET_NULL;
}
int status;
// Set socket options
status = fcntl(socketFD, F_SETFL, O_NONBLOCK);
if (status == -1)
{
NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)";
err = [[self errnoErrorWithReason:reason] retain];
close(socketFD);
return SOCKET_NULL;
}
int reuseOn = 1;
status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
if (status == -1)
{
NSString *reason = @"Error enabling address reuse (setsockopt)";
err = [[self errnoErrorWithReason:reason] retain];
close(socketFD);
return SOCKET_NULL;
}
// Bind socket
status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);
if (status == -1)
{
NSString *reason = @"Error in bind() function";
err = [[self errnoErrorWithReason:reason] retain];
close(socketFD);
return SOCKET_NULL;
}
// Listen
status = listen(socketFD, 1024);
if (status == -1)
{
NSString *reason = @"Error in listen() function";
err = [[self errnoErrorWithReason:reason] retain];
close(socketFD);
return SOCKET_NULL;
}
return socketFD;
};
// Create dispatch block and run on socketQueue
dispatch_block_t block = ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (delegate == nil) // Must have delegate set
{
NSString *msg = @"Attempting to accept without a delegate. Set a delegate first.";
err = [[self badConfigError:msg] retain];
[pool drain];
return_from_block;
}
if (delegateQueue == NULL) // Must have delegate queue set
{
NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first.";
err = [[self badConfigError:msg] retain];
[pool drain];
return_from_block;
}
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
err = [[self badConfigError:msg] retain];
[pool drain];
return_from_block;
}
if (![self isDisconnected]) // Must be disconnected
{
NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first.";
err = [[self badConfigError:msg] retain];
[pool drain];
return_from_block;
}
// Clear queues (spurious read/write requests post disconnect)
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
// Resolve interface from description
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
if ((interface4 == nil) && (interface6 == nil))
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
if (isIPv4Disabled && (interface6 == nil))
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
if (isIPv6Disabled && (interface4 == nil))
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil);
BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil);
// Create sockets, configure, bind, and listen
if (enableIPv4)
{
LogVerbose(@"Creating IPv4 socket");
socket4FD = createSocket(AF_INET, interface4);
if (socket4FD == SOCKET_NULL)
{
[pool drain];
return_from_block;
}
}
if (enableIPv6)
{
LogVerbose(@"Creating IPv6 socket");
if (enableIPv4 && (port == 0))
{
// No specific port was specified, so we allowed the OS to pick an available port for us.
// Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket.
struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes];
addr6->sin6_port = htons([self localPort4]);
}
socket6FD = createSocket(AF_INET6, interface6);
if (socket6FD == SOCKET_NULL)
{
if (socket4FD != SOCKET_NULL)
{
close(socket4FD);
}
[pool drain];
return_from_block;
}
}
// Create accept sources
if (enableIPv4)
{
accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
int socketFD = socket4FD;
dispatch_source_t acceptSource = accept4Source;
dispatch_source_set_event_handler(accept4Source, ^{
NSAutoreleasePool *eventPool = [[NSAutoreleasePool alloc] init];
LogVerbose(@"event4Block");
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
while ([self doAccept:socketFD] && (++i < numPendingConnections));
[eventPool drain];
});
dispatch_source_set_cancel_handler(accept4Source, ^{
LogVerbose(@"dispatch_release(accept4Source)");
dispatch_release(acceptSource);
LogVerbose(@"close(socket4FD)");
close(socketFD);
});
LogVerbose(@"dispatch_resume(accept4Source)");
dispatch_resume(accept4Source);
}
if (enableIPv6)
{
accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue);
int socketFD = socket6FD;
dispatch_source_t acceptSource = accept6Source;
dispatch_source_set_event_handler(accept6Source, ^{
NSAutoreleasePool *eventPool = [[NSAutoreleasePool alloc] init];
LogVerbose(@"event6Block");
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
while ([self doAccept:socketFD] && (++i < numPendingConnections));
[eventPool drain];
});
dispatch_source_set_cancel_handler(accept6Source, ^{
LogVerbose(@"dispatch_release(accept6Source)");
dispatch_release(acceptSource);
LogVerbose(@"close(socket6FD)");
close(socketFD);
});
LogVerbose(@"dispatch_resume(accept6Source)");
dispatch_resume(accept6Source);
}
flags |= kSocketStarted;
result = YES;
[pool drain];
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
LogInfo(@"Error in accept: %@", err);
if (errPtr)
*errPtr = [err autorelease];
else
[err release];
}
return result;
}
- (BOOL)doAccept:(int)parentSocketFD
{
LogTrace();
BOOL isIPv4;
int childSocketFD;
NSData *childSocketAddress;
if (parentSocketFD == socket4FD)
{
isIPv4 = YES;
struct sockaddr_in addr;
socklen_t addrLen = sizeof(addr);
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
if (childSocketFD == -1)
{
LogWarn(@"Accept failed with error: %@", [self errnoError]);
return NO;
}
childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
}
else // if (parentSocketFD == socket6FD)
{
isIPv4 = NO;
struct sockaddr_in6 addr;
socklen_t addrLen = sizeof(addr);
childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
if (childSocketFD == -1)
{
LogWarn(@"Accept failed with error: %@", [self errnoError]);
return NO;
}
childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
}
// Enable non-blocking IO on the socket
int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK);
if (result == -1)
{
LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)");
return NO;
}
// Prevent SIGPIPE signals
int nosigpipe = 1;
setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
// Notify delegate
if (delegateQueue)
{
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *delegatePool = [[NSAutoreleasePool alloc] init];
// Query delegate for custom socket queue
dispatch_queue_t childSocketQueue = NULL;
if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)])
{
childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress
onSocket:self];
}
// Create GCDAsyncSocket instance for accepted socket
GCDAsyncSocket *acceptedSocket = [[GCDAsyncSocket alloc] initWithDelegate:delegate
delegateQueue:delegateQueue
socketQueue:childSocketQueue];
if (isIPv4)
acceptedSocket->socket4FD = childSocketFD;
else
acceptedSocket->socket6FD = childSocketFD;
acceptedSocket->flags = (kSocketStarted | kConnected);
// Setup read and write sources for accepted socket
dispatch_async(acceptedSocket->socketQueue, ^{
NSAutoreleasePool *socketPool = [[NSAutoreleasePool alloc] init];
[acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
[socketPool drain];
});
// Notify delegate
if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)])
{
[theDelegate socket:self didAcceptNewSocket:acceptedSocket];
}
// Release the socket queue returned from the delegate (it was retained by acceptedSocket)
if (childSocketQueue)
dispatch_release(childSocketQueue);
// Release the accepted socket (it should have been retained by the delegate)
[acceptedSocket release];
[delegatePool drain];
});
}
return YES;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Connecting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* This method runs through the various checks required prior to a connection attempt.
* It is shared between the connectToHost and connectToAddress methods.
*
**/
- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
{
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
if (delegate == nil) // Must have delegate set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
if (delegateQueue == NULL) // Must have delegate queue set
{
if (errPtr)
{
NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
if (![self isDisconnected]) // Must be disconnected
{
if (errPtr)
{
NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
{
if (errPtr)
{
NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
*errPtr = [self badConfigError:msg];
}
return NO;
}
if (interface)
{
NSMutableData *interface4 = nil;
NSMutableData *interface6 = nil;
[self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
if ((interface4 == nil) && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv4Disabled && (interface6 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
*errPtr = [self badParamError:msg];
}
return NO;
}
if (isIPv6Disabled && (interface4 == nil))
{
if (errPtr)
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
*errPtr = [self badParamError:msg];
}
return NO;
}
connectInterface4 = [interface4 retain];
connectInterface6 = [interface6 retain];
}
// Clear queues (spurious read/write requests post disconnect)
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
return YES;
}
- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr];
}
- (BOOL)connectToHost:(NSString *)host
onPort:(uint16_t)port
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr];
}
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
LogTrace();
// Just in case immutable objects were passed
NSString *host = [[inHost copy] autorelease];
NSString *interface = [[inInterface copy] autorelease];
__block BOOL result = NO;
__block NSError *err = nil;
dispatch_block_t block = ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Check for problems with host parameter
if ([host length] == 0)
{
NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
// Run through standard pre-connect checks
if (![self preConnectWithInterface:interface error:&err])
{
[err retain];
[pool drain];
return_from_block;
}
// We've made it past all the checks.
// It's time to start the connection process.
flags |= kSocketStarted;
LogVerbose(@"Dispatching DNS lookup...");
// It's possible that the given host parameter is actually a NSMutableString.
// So we want to copy it now, within this block that will be executed synchronously.
// This way the asynchronous lookup block below doesn't have to worry about it changing.
int aConnectIndex = connectIndex;
NSString *hostCpy = [[host copy] autorelease];
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{
NSAutoreleasePool *lookupPool = [[NSAutoreleasePool alloc] init];
[self lookup:aConnectIndex host:hostCpy port:port];
[lookupPool drain];
});
[self startConnectTimeout:timeout];
result = YES;
[pool drain];
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
if (errPtr)
*errPtr = [err autorelease];
else
[err release];
}
return result;
}
- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr
{
return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr];
}
- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr
{
return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr];
}
- (BOOL)connectToAddress:(NSData *)inRemoteAddr
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr
{
LogTrace();
// Just in case immutable objects were passed
NSData *remoteAddr = [[inRemoteAddr copy] autorelease];
NSString *interface = [[inInterface copy] autorelease];
__block BOOL result = NO;
__block NSError *err = nil;
dispatch_block_t block = ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Check for problems with remoteAddr parameter
NSData *address4 = nil;
NSData *address6 = nil;
if ([remoteAddr length] >= sizeof(struct sockaddr))
{
const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes];
if (sockaddr->sa_family == AF_INET)
{
if ([remoteAddr length] == sizeof(struct sockaddr_in))
{
address4 = remoteAddr;
}
}
else if (sockaddr->sa_family == AF_INET6)
{
if ([remoteAddr length] == sizeof(struct sockaddr_in6))
{
address6 = remoteAddr;
}
}
}
if ((address4 == nil) && (address6 == nil))
{
NSString *msg = @"A valid IPv4 or IPv6 address was not given";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
if (isIPv4Disabled && (address4 != nil))
{
NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
if (isIPv6Disabled && (address6 != nil))
{
NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed.";
err = [[self badParamError:msg] retain];
[pool drain];
return_from_block;
}
// Run through standard pre-connect checks
if (![self preConnectWithInterface:interface error:&err])
{
[err retain];
[pool drain];
return_from_block;
}
// We've made it past all the checks.
// It's time to start the connection process.
if (![self connectWithAddress4:address4 address6:address6 error:&err])
{
[err retain];
[pool drain];
return_from_block;
}
flags |= kSocketStarted;
[self startConnectTimeout:timeout];
result = YES;
[pool drain];
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
if (errPtr)
*errPtr = [err autorelease];
else
[err release];
}
return result;
}
- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port
{
LogTrace();
// This method is executed on a global concurrent queue.
// It posts the results back to the socket queue.
// The lookupIndex is used to ignore the results if the connect operation was cancelled or timed out.
NSError *error = nil;
NSData *address4 = nil;
NSData *address6 = nil;
if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"])
{
// Use LOOPBACK address
struct sockaddr_in nativeAddr;
nativeAddr.sin_len = sizeof(struct sockaddr_in);
nativeAddr.sin_family = AF_INET;
nativeAddr.sin_port = htons(port);
nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero));
struct sockaddr_in6 nativeAddr6;
nativeAddr6.sin6_len = sizeof(struct sockaddr_in6);
nativeAddr6.sin6_family = AF_INET6;
nativeAddr6.sin6_port = htons(port);
nativeAddr6.sin6_flowinfo = 0;
nativeAddr6.sin6_addr = in6addr_loopback;
nativeAddr6.sin6_scope_id = 0;
// Wrap the native address structures
address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)];
address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
else
{
NSString *portStr = [NSString stringWithFormat:@"%hu", port];
struct addrinfo hints, *res, *res0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
if (gai_error)
{
error = [self gaiError:gai_error];
}
else
{
for(res = res0; res; res = res->ai_next)
{
if ((address4 == nil) && (res->ai_family == AF_INET))
{
// Found IPv4 address
// Wrap the native address structure
address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
}
else if ((address6 == nil) && (res->ai_family == AF_INET6))
{
// Found IPv6 address
// Wrap the native address structure
address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
}
}
freeaddrinfo(res0);
if ((address4 == nil) && (address6 == nil))
{
error = [self gaiError:EAI_FAIL];
}
}
}
if (error)
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self lookup:aConnectIndex didFail:error];
[pool drain];
});
}
else
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self lookup:aConnectIndex didSucceedWithAddress4:address4 address6:address6];
[pool drain];
});
}
}
- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
NSAssert(address4 || address6, @"Expected at least one valid address");
if (aConnectIndex != connectIndex)
{
LogInfo(@"Ignoring lookupDidSucceed, already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
// Check for problems
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
if (isIPv4Disabled && (address6 == nil))
{
NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
[self closeWithError:[self otherError:msg]];
return;
}
if (isIPv6Disabled && (address4 == nil))
{
NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address.";
[self closeWithError:[self otherError:msg]];
return;
}
// Start the normal connection process
NSError *err = nil;
if (![self connectWithAddress4:address4 address6:address6 error:&err])
{
[self closeWithError:err];
}
}
/**
* This method is called if the DNS lookup fails.
* This method is executed on the socketQueue.
*
* Since the DNS lookup executed synchronously on a global concurrent queue,
* the original connection request may have already been cancelled or timed-out by the time this method is invoked.
* The lookupIndex tells us whether the lookup is still valid or not.
**/
- (void)lookup:(int)aConnectIndex didFail:(NSError *)error
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
if (aConnectIndex != connectIndex)
{
LogInfo(@"Ignoring lookup:didFail: - already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
[self endConnectTimeout];
[self closeWithError:error];
}
- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]);
LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]);
// Determine socket type
BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil));
// Create the socket
int socketFD;
NSData *address;
NSData *connectInterface;
if (useIPv6)
{
LogVerbose(@"Creating IPv6 socket");
socket6FD = socket(AF_INET6, SOCK_STREAM, 0);
socketFD = socket6FD;
address = address6;
connectInterface = connectInterface6;
}
else
{
LogVerbose(@"Creating IPv4 socket");
socket4FD = socket(AF_INET, SOCK_STREAM, 0);
socketFD = socket4FD;
address = address4;
connectInterface = connectInterface4;
}
if (socketFD == SOCKET_NULL)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
return NO;
}
// Bind the socket to the desired interface (if needed)
if (connectInterface)
{
LogVerbose(@"Binding socket...");
if ([[self class] portFromAddress:connectInterface] > 0)
{
// Since we're going to be binding to a specific port,
// we should turn on reuseaddr to allow us to override sockets in time_wait.
int reuseOn = 1;
setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
}
const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes];
int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
if (result != 0)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error in bind() function"];
return NO;
}
}
// Start the connection process in a background queue
int aConnectIndex = connectIndex;
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{
int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
if (result == 0)
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self didConnect:aConnectIndex];
[pool drain];
});
}
else
{
NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self didNotConnect:aConnectIndex error:error];
[pool drain];
});
}
});
LogVerbose(@"Connecting...");
return YES;
}
- (void)didConnect:(int)aConnectIndex
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
if (aConnectIndex != connectIndex)
{
LogInfo(@"Ignoring didConnect, already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
flags |= kConnected;
[self endConnectTimeout];
#if TARGET_OS_IPHONE
// The endConnectTimeout method executed above incremented the connectIndex.
aConnectIndex = connectIndex;
#endif
// Setup read/write streams (as workaround for specific shortcomings in the iOS platform)
//
// Note:
// There may be configuration options that must be set by the delegate before opening the streams.
// The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream.
//
// Thus we wait until after the socket:didConnectToHost:port: delegate method has completed.
// This gives the delegate time to properly configure the streams if needed.
dispatch_block_t SetupStreamsPart1 = ^{
#if TARGET_OS_IPHONE
if (![self createReadAndWriteStream])
{
[self closeWithError:[self otherError:@"Error creating CFStreams"]];
return;
}
if (![self registerForStreamCallbacksIncludingReadWrite:NO])
{
[self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
return;
}
#endif
};
dispatch_block_t SetupStreamsPart2 = ^{
#if TARGET_OS_IPHONE
if (aConnectIndex != connectIndex)
{
// The socket has been disconnected.
return;
}
if (![self addStreamsToRunLoop])
{
[self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
return;
}
if (![self openStreams])
{
[self closeWithError:[self otherError:@"Error creating CFStreams"]];
return;
}
#endif
};
// Notify delegate
NSString *host = [self connectedHost];
uint16_t port = [self connectedPort];
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didConnectToHost:port:)])
{
SetupStreamsPart1();
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *delegatePool = [[NSAutoreleasePool alloc] init];
[theDelegate socket:self didConnectToHost:host port:port];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *callbackPool = [[NSAutoreleasePool alloc] init];
SetupStreamsPart2();
[callbackPool drain];
});
[delegatePool drain];
});
}
else
{
SetupStreamsPart1();
SetupStreamsPart2();
}
// Get the connected socket
int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : socket6FD;
// Enable non-blocking IO on the socket
int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
if (result == -1)
{
NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)";
[self closeWithError:[self otherError:errMsg]];
return;
}
// Prevent SIGPIPE signals
int nosigpipe = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
// Setup our read/write sources
[self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD];
// Dequeue any pending read/write requests
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
if (aConnectIndex != connectIndex)
{
LogInfo(@"Ignoring didNotConnect, already disconnected");
// The connect operation has been cancelled.
// That is, socket was disconnected, or connection has already timed out.
return;
}
[self endConnectTimeout];
[self closeWithError:error];
}
- (void)startConnectTimeout:(NSTimeInterval)timeout
{
if (timeout >= 0.0)
{
connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
dispatch_source_set_event_handler(connectTimer, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self doConnectTimeout];
[pool drain];
});
dispatch_source_t theConnectTimer = connectTimer;
dispatch_source_set_cancel_handler(connectTimer, ^{
LogVerbose(@"dispatch_release(connectTimer)");
dispatch_release(theConnectTimer);
});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(connectTimer);
}
}
- (void)endConnectTimeout
{
LogTrace();
if (connectTimer)
{
dispatch_source_cancel(connectTimer);
connectTimer = NULL;
}
// Increment connectIndex.
// This will prevent us from processing results from any related background asynchronous operations.
//
// Note: This should be called from close method even if connectTimer is NULL.
// This is because one might disconnect a socket prior to a successful connection which had no timeout.
connectIndex++;
if (connectInterface4)
{
[connectInterface4 release];
connectInterface4 = nil;
}
if (connectInterface6)
{
[connectInterface6 release];
connectInterface6 = nil;
}
}
- (void)doConnectTimeout
{
LogTrace();
[self endConnectTimeout];
[self closeWithError:[self connectTimeoutError]];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Disconnecting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)closeWithError:(NSError *)error
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
[self endConnectTimeout];
if (currentRead != nil) [self endCurrentRead];
if (currentWrite != nil) [self endCurrentWrite];
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
[partialReadBuffer setLength:0];
#if TARGET_OS_IPHONE
{
if (readStream || writeStream)
{
[self removeStreamsFromRunLoop];
if (readStream)
{
CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
CFReadStreamClose(readStream);
CFRelease(readStream);
readStream = NULL;
}
if (writeStream)
{
CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL);
CFWriteStreamClose(writeStream);
CFRelease(writeStream);
writeStream = NULL;
}
}
}
#else
{
[sslReadBuffer setLength:0];
if (sslContext)
{
// Getting a linker error here about SSLDisposeContext?
// You need to add the Security Framework to your application.
SSLDisposeContext(sslContext);
sslContext = NULL;
}
}
#endif
// For some crazy reason (in my opinion), cancelling a dispatch source doesn't
// invoke the cancel handler if the dispatch source is paused.
// So we have to unpause the source if needed.
// This allows the cancel handler to be run, which in turn releases the source and closes the socket.
if (accept4Source)
{
LogVerbose(@"dispatch_source_cancel(accept4Source)");
dispatch_source_cancel(accept4Source);
// We never suspend accept4Source
accept4Source = NULL;
}
if (accept6Source)
{
LogVerbose(@"dispatch_source_cancel(accept6Source)");
dispatch_source_cancel(accept6Source);
// We never suspend accept6Source
accept6Source = NULL;
}
if (!readSource && !writeSource) {
LogVerbose(@"manually closing close");
if (socket4FD) {
close(socket4FD);
}
if (socket6FD) {
close(socket6FD);
}
}
if (readSource)
{
LogVerbose(@"dispatch_source_cancel(readSource)");
dispatch_source_cancel(readSource);
[self resumeReadSource];
readSource = NULL;
}
if (writeSource)
{
LogVerbose(@"dispatch_source_cancel(writeSource)");
dispatch_source_cancel(writeSource);
[self resumeWriteSource];
writeSource = NULL;
}
// The sockets will be closed by the cancel handlers of the corresponding source
socket4FD = SOCKET_NULL;
socket6FD = SOCKET_NULL;
// If the client has passed the connect/accept method, then the connection has at least begun.
// Notify delegate that it is now ending.
BOOL shouldCallDelegate = (flags & kSocketStarted);
// Clear stored socket info and all flags (config remains as is)
socketFDBytesAvailable = 0;
flags = 0;
if (shouldCallDelegate)
{
if (delegateQueue && [delegate respondsToSelector: @selector(socketDidDisconnect:withError:)])
{
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socketDidDisconnect:self withError:error];
[pool drain];
});
}
}
}
- (void)disconnect
{
dispatch_block_t block = ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (flags & kSocketStarted)
{
[self closeWithError:nil];
}
[pool drain];
};
// Synchronous disconnection, as documented in the header file
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
}
- (void)disconnectAfterReading
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterReads);
[self maybeClose];
}
[pool drain];
});
}
- (void)disconnectAfterWriting
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterWrites);
[self maybeClose];
}
[pool drain];
});
}
- (void)disconnectAfterReadingAndWriting
{
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites);
[self maybeClose];
}
[pool drain];
});
}
/**
* Closes the socket if possible.
* That is, if all writes have completed, and we're set to disconnect after writing,
* or if all reads have completed, and we're set to disconnect after reading.
**/
- (void)maybeClose
{
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
BOOL shouldClose = NO;
if (flags & kDisconnectAfterReads)
{
if (([readQueue count] == 0) && (currentRead == nil))
{
if (flags & kDisconnectAfterWrites)
{
if (([writeQueue count] == 0) && (currentWrite == nil))
{
shouldClose = YES;
}
}
else
{
shouldClose = YES;
}
}
}
else if (flags & kDisconnectAfterWrites)
{
if (([writeQueue count] == 0) && (currentWrite == nil))
{
shouldClose = YES;
}
}
if (shouldClose)
{
[self closeWithError:nil];
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Errors
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (NSError *)badConfigError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo];
}
- (NSError *)badParamError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
}
- (NSError *)gaiError:(int)gai_error
{
NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding];
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo];
}
- (NSError *)errnoErrorWithReason:(NSString *)reason
{
NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey,
reason, NSLocalizedFailureReasonErrorKey, nil];
return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo];
}
- (NSError *)errnoError
{
NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)];
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo];
}
- (NSError *)sslError:(OSStatus)ssl_error
{
NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h";
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey];
return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo];
}
- (NSError *)connectTimeoutError
{
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError",
@"GCDAsyncSocket", [NSBundle mainBundle],
@"Attempt to connect to host timed out", nil);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo];
}
/**
* Returns a standard AsyncSocket maxed out error.
**/
- (NSError *)readMaxedOutError
{
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError",
@"GCDAsyncSocket", [NSBundle mainBundle],
@"Read operation reached set maximum length", nil);
NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info];
}
/**
* Returns a standard AsyncSocket write timeout error.
**/
- (NSError *)readTimeoutError
{
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError",
@"GCDAsyncSocket", [NSBundle mainBundle],
@"Read operation timed out", nil);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo];
}
/**
* Returns a standard AsyncSocket write timeout error.
**/
- (NSError *)writeTimeoutError
{
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError",
@"GCDAsyncSocket", [NSBundle mainBundle],
@"Write operation timed out", nil);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo];
}
- (NSError *)connectionClosedError
{
NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError",
@"GCDAsyncSocket", [NSBundle mainBundle],
@"Socket closed by remote peer", nil);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo];
}
- (NSError *)otherError:(NSString *)errMsg
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Diagnostics
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (BOOL)isDisconnected
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = (flags & kSocketStarted) ? NO : YES;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (BOOL)isConnected
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = (flags & kConnected) ? YES : NO;
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (NSString *)connectedHost
{
if (dispatch_get_current_queue() == socketQueue)
{
if (socket4FD != SOCKET_NULL)
return [self connectedHostFromSocket4:socket4FD];
if (socket6FD != SOCKET_NULL)
return [self connectedHostFromSocket6:socket6FD];
return nil;
}
else
{
__block NSString *result = nil;
dispatch_sync(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (socket4FD != SOCKET_NULL)
result = [[self connectedHostFromSocket4:socket4FD] retain];
else if (socket6FD != SOCKET_NULL)
result = [[self connectedHostFromSocket6:socket6FD] retain];
[pool drain];
});
return [result autorelease];
}
}
- (uint16_t)connectedPort
{
if (dispatch_get_current_queue() == socketQueue)
{
if (socket4FD != SOCKET_NULL)
return [self connectedPortFromSocket4:socket4FD];
if (socket6FD != SOCKET_NULL)
return [self connectedPortFromSocket6:socket6FD];
return 0;
}
else
{
__block uint16_t result = 0;
dispatch_sync(socketQueue, ^{
// No need for autorelease pool
if (socket4FD != SOCKET_NULL)
result = [self connectedPortFromSocket4:socket4FD];
else if (socket6FD != SOCKET_NULL)
result = [self connectedPortFromSocket6:socket6FD];
});
return result;
}
}
- (NSString *)localHost
{
if (dispatch_get_current_queue() == socketQueue)
{
if (socket4FD != SOCKET_NULL)
return [self localHostFromSocket4:socket4FD];
if (socket6FD != SOCKET_NULL)
return [self localHostFromSocket6:socket6FD];
return nil;
}
else
{
__block NSString *result = nil;
dispatch_sync(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (socket4FD != SOCKET_NULL)
result = [[self localHostFromSocket4:socket4FD] retain];
else if (socket6FD != SOCKET_NULL)
result = [[self localHostFromSocket6:socket6FD] retain];
[pool drain];
});
return [result autorelease];
}
}
- (uint16_t)localPort
{
if (dispatch_get_current_queue() == socketQueue)
{
if (socket4FD != SOCKET_NULL)
return [self localPortFromSocket4:socket4FD];
if (socket6FD != SOCKET_NULL)
return [self localPortFromSocket6:socket6FD];
return 0;
}
else
{
__block uint16_t result = 0;
dispatch_sync(socketQueue, ^{
// No need for autorelease pool
if (socket4FD != SOCKET_NULL)
result = [self localPortFromSocket4:socket4FD];
else if (socket6FD != SOCKET_NULL)
result = [self localPortFromSocket6:socket6FD];
});
return result;
}
}
- (NSString *)connectedHost4
{
if (socket4FD != SOCKET_NULL)
return [self connectedHostFromSocket4:socket4FD];
return nil;
}
- (NSString *)connectedHost6
{
if (socket6FD != SOCKET_NULL)
return [self connectedHostFromSocket6:socket6FD];
return nil;
}
- (uint16_t)connectedPort4
{
if (socket4FD != SOCKET_NULL)
return [self connectedPortFromSocket4:socket4FD];
return 0;
}
- (uint16_t)connectedPort6
{
if (socket6FD != SOCKET_NULL)
return [self connectedPortFromSocket6:socket6FD];
return 0;
}
- (NSString *)localHost4
{
if (socket4FD != SOCKET_NULL)
return [self localHostFromSocket4:socket4FD];
return nil;
}
- (NSString *)localHost6
{
if (socket6FD != SOCKET_NULL)
return [self localHostFromSocket6:socket6FD];
return nil;
}
- (uint16_t)localPort4
{
if (socket4FD != SOCKET_NULL)
return [self localPortFromSocket4:socket4FD];
return 0;
}
- (uint16_t)localPort6
{
if (socket6FD != SOCKET_NULL)
return [self localPortFromSocket6:socket6FD];
return 0;
}
- (NSString *)connectedHostFromSocket4:(int)socketFD
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
{
return nil;
}
return [[self class] hostFromSockaddr4:&sockaddr4];
}
- (NSString *)connectedHostFromSocket6:(int)socketFD
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
{
return nil;
}
return [[self class] hostFromSockaddr6:&sockaddr6];
}
- (uint16_t)connectedPortFromSocket4:(int)socketFD
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
{
return 0;
}
return [[self class] portFromSockaddr4:&sockaddr4];
}
- (uint16_t)connectedPortFromSocket6:(int)socketFD
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
{
return 0;
}
return [[self class] portFromSockaddr6:&sockaddr6];
}
- (NSString *)localHostFromSocket4:(int)socketFD
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
{
return nil;
}
return [[self class] hostFromSockaddr4:&sockaddr4];
}
- (NSString *)localHostFromSocket6:(int)socketFD
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
{
return nil;
}
return [[self class] hostFromSockaddr6:&sockaddr6];
}
- (uint16_t)localPortFromSocket4:(int)socketFD
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
{
return 0;
}
return [[self class] portFromSockaddr4:&sockaddr4];
}
- (uint16_t)localPortFromSocket6:(int)socketFD
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
{
return 0;
}
return [[self class] portFromSockaddr6:&sockaddr6];
}
- (NSData *)connectedAddress
{
__block NSData *result = nil;
dispatch_block_t block = ^{
if (socket4FD != SOCKET_NULL)
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
{
result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len];
}
}
if (socket6FD != SOCKET_NULL)
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
{
result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len];
}
}
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
return [result autorelease];
}
- (NSData *)localAddress
{
__block NSData *result = nil;
dispatch_block_t block = ^{
if (socket4FD != SOCKET_NULL)
{
struct sockaddr_in sockaddr4;
socklen_t sockaddr4len = sizeof(sockaddr4);
if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
{
result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len];
}
}
if (socket6FD != SOCKET_NULL)
{
struct sockaddr_in6 sockaddr6;
socklen_t sockaddr6len = sizeof(sockaddr6);
if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
{
result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len];
}
}
};
if (dispatch_get_current_queue() == socketQueue)
block();
else
dispatch_sync(socketQueue, block);
return [result autorelease];
}
- (BOOL)isIPv4
{
if (dispatch_get_current_queue() == socketQueue)
{
return (socket4FD != SOCKET_NULL);
}
else
{
__block BOOL result = NO;
dispatch_sync(socketQueue, ^{
result = (socket4FD != SOCKET_NULL);
});
return result;
}
}
- (BOOL)isIPv6
{
if (dispatch_get_current_queue() == socketQueue)
{
return (socket6FD != SOCKET_NULL);
}
else
{
__block BOOL result = NO;
dispatch_sync(socketQueue, ^{
result = (socket6FD != SOCKET_NULL);
});
return result;
}
}
- (BOOL)isSecure
{
if (dispatch_get_current_queue() == socketQueue)
{
return (flags & kSocketSecure) ? YES : NO;
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = (flags & kSocketSecure) ? YES : NO;
});
return result;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Utilities
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Finds the address of an interface description.
* An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34).
*
* The interface description may optionally contain a port number at the end, separated by a colon.
* If a non-zero port parameter is provided, any port number in the interface description is ignored.
*
* The returned value is a 'struct sockaddr' wrapped in an NSMutableData object.
**/
- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
address6:(NSMutableData **)interfaceAddr6Ptr
fromDescription:(NSString *)interfaceDescription
port:(uint16_t)port
{
NSMutableData *addr4 = nil;
NSMutableData *addr6 = nil;
NSString *interface = nil;
NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
if ([components count] > 0)
{
NSString *temp = [components objectAtIndex:0];
if ([temp length] > 0)
{
interface = temp;
}
}
if ([components count] > 1 && port == 0)
{
long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
if (portL > 0 && portL <= UINT16_MAX)
{
port = (uint16_t)portL;
}
}
if (interface == nil)
{
// ANY address
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));
sockaddr6.sin6_len = sizeof(sockaddr6);
sockaddr6.sin6_family = AF_INET6;
sockaddr6.sin6_port = htons(port);
sockaddr6.sin6_addr = in6addr_any;
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"])
{
// LOOPBACK address
struct sockaddr_in sockaddr4;
memset(&sockaddr4, 0, sizeof(sockaddr4));
sockaddr4.sin_len = sizeof(sockaddr4);
sockaddr4.sin_family = AF_INET;
sockaddr4.sin_port = htons(port);
sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
struct sockaddr_in6 sockaddr6;
memset(&sockaddr6, 0, sizeof(sockaddr6));
sockaddr6.sin6_len = sizeof(sockaddr6);
sockaddr6.sin6_family = AF_INET6;
sockaddr6.sin6_port = htons(port);
sockaddr6.sin6_addr = in6addr_loopback;
addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
}
else
{
const char *iface = [interface UTF8String];
struct ifaddrs *addrs;
const struct ifaddrs *cursor;
if ((getifaddrs(&addrs) == 0))
{
cursor = addrs;
while (cursor != NULL)
{
if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
{
// IPv4
struct sockaddr_in nativeAddr4;
memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
nativeAddr4.sin_port = htons(port);
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
else
{
char ip[INET_ADDRSTRLEN];
const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
nativeAddr4.sin_port = htons(port);
addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
}
}
}
else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6))
{
// IPv6
struct sockaddr_in6 nativeAddr6;
memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6));
if (strcmp(cursor->ifa_name, iface) == 0)
{
// Name match
nativeAddr6.sin6_port = htons(port);
addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
else
{
char ip[INET6_ADDRSTRLEN];
const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip));
if ((conversion != NULL) && (strcmp(ip, iface) == 0))
{
// IP match
nativeAddr6.sin6_port = htons(port);
addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
}
}
cursor = cursor->ifa_next;
}
freeifaddrs(addrs);
}
}
if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;
}
- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD
{
readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue);
// Setup event handlers
dispatch_source_set_event_handler(readSource, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogVerbose(@"readEventBlock");
socketFDBytesAvailable = dispatch_source_get_data(readSource);
LogVerbose(@"socketFDBytesAvailable: %lu", socketFDBytesAvailable);
if (socketFDBytesAvailable > 0)
[self doReadData];
else
[self doReadEOF];
[pool drain];
});
dispatch_source_set_event_handler(writeSource, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogVerbose(@"writeEventBlock");
flags |= kSocketCanAcceptBytes;
[self doWriteData];
[pool drain];
});
// Setup cancel handlers
__block int socketFDRefCount = 2;
dispatch_source_t theReadSource = readSource;
dispatch_source_t theWriteSource = writeSource;
dispatch_source_set_cancel_handler(readSource, ^{
LogVerbose(@"readCancelBlock");
LogVerbose(@"dispatch_release(readSource)");
dispatch_release(theReadSource);
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
close(socketFD);
}
});
dispatch_source_set_cancel_handler(writeSource, ^{
LogVerbose(@"writeCancelBlock");
LogVerbose(@"dispatch_release(writeSource)");
dispatch_release(theWriteSource);
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
close(socketFD);
}
});
// We will not be able to read until data arrives.
// But we should be able to write immediately.
socketFDBytesAvailable = 0;
flags &= ~kReadSourceSuspended;
LogVerbose(@"dispatch_resume(readSource)");
dispatch_resume(readSource);
flags |= kSocketCanAcceptBytes;
flags |= kWriteSourceSuspended;
}
- (BOOL)usingCFStream
{
#if TARGET_OS_IPHONE
if (flags & kSocketSecure)
{
// Due to the fact that Apple doesn't give us the full power of SecureTransport on iOS,
// we are relegated to using the slower, less powerful, and RunLoop based CFStream API. :( Boo!
//
// Thus we're not able to use the GCD read/write sources in this particular scenario.
return YES;
}
#endif
return NO;
}
- (void)suspendReadSource
{
if (!(flags & kReadSourceSuspended))
{
LogVerbose(@"dispatch_suspend(readSource)");
dispatch_suspend(readSource);
flags |= kReadSourceSuspended;
}
}
- (void)resumeReadSource
{
if (flags & kReadSourceSuspended)
{
LogVerbose(@"dispatch_resume(readSource)");
dispatch_resume(readSource);
flags &= ~kReadSourceSuspended;
}
}
- (void)suspendWriteSource
{
if (!(flags & kWriteSourceSuspended))
{
LogVerbose(@"dispatch_suspend(writeSource)");
dispatch_suspend(writeSource);
flags |= kWriteSourceSuspended;
}
}
- (void)resumeWriteSource
{
if (flags & kWriteSourceSuspended)
{
LogVerbose(@"dispatch_resume(writeSource)");
dispatch_resume(writeSource);
flags &= ~kWriteSourceSuspended;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Reading
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
{
[self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
}
- (void)readDataWithTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
tag:(long)tag
{
[self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
}
- (void)readDataWithTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
maxLength:(NSUInteger)length
tag:(long)tag
{
if (offset > [buffer length]) {
LogWarn(@"Cannot read: offset > [buffer length]");
return;
}
GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
startOffset:offset
maxLength:length
timeout:timeout
readLength:0
terminator:nil
tag:tag];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
[pool drain];
});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
[packet release];
}
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag
{
[self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag];
}
- (void)readDataToLength:(NSUInteger)length
withTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
tag:(long)tag
{
if (length == 0) {
LogWarn(@"Cannot read: length == 0");
return;
}
if (offset > [buffer length]) {
LogWarn(@"Cannot read: offset > [buffer length]");
return;
}
GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
startOffset:offset
maxLength:0
timeout:timeout
readLength:length
terminator:nil
tag:tag];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
[pool drain];
});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
[packet release];
}
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
{
[self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
}
- (void)readDataToData:(NSData *)data
withTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
tag:(long)tag
{
[self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
}
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag
{
[self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag];
}
- (void)readDataToData:(NSData *)data
withTimeout:(NSTimeInterval)timeout
buffer:(NSMutableData *)buffer
bufferOffset:(NSUInteger)offset
maxLength:(NSUInteger)maxLength
tag:(long)tag
{
if ([data length] == 0) {
LogWarn(@"Cannot read: [data length] == 0");
return;
}
if (offset > [buffer length]) {
LogWarn(@"Cannot read: offset > [buffer length]");
return;
}
if (maxLength > 0 && maxLength < [data length]) {
LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]");
return;
}
GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
startOffset:offset
maxLength:maxLength
timeout:timeout
readLength:0
terminator:data
tag:tag];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
[pool drain];
});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
[packet release];
}
/**
* This method starts a new read, if needed.
*
* It is called when:
* - a user requests a read
* - after a read request has finished (to handle the next request)
* - immediately after the socket opens to handle any pending requests
*
* This method also handles auto-disconnect post read/write completion.
**/
- (void)maybeDequeueRead
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
// If we're not currently processing a read AND we have an available read stream
if ((currentRead == nil) && (flags & kConnected))
{
if ([readQueue count] > 0)
{
// Dequeue the next object in the write queue
currentRead = [[readQueue objectAtIndex:0] retain];
[readQueue removeObjectAtIndex:0];
if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
{
LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
// Attempt to start TLS
flags |= kStartingReadTLS;
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
[self maybeStartTLS];
}
else
{
LogVerbose(@"Dequeued GCDAsyncReadPacket");
// Setup read timer (if needed)
[self setupReadTimerWithTimeout:currentRead->timeout];
// Immediately read, if possible
[self doReadData];
}
}
else if (flags & kDisconnectAfterReads)
{
if (flags & kDisconnectAfterWrites)
{
if (([writeQueue count] == 0) && (currentWrite == nil))
{
[self closeWithError:nil];
}
}
else
{
[self closeWithError:nil];
}
}
else if (flags & kSocketSecure)
{
[self flushSSLBuffers];
// Edge case:
//
// We just drained all data from the ssl buffers,
// and all known data from the socket (socketFDBytesAvailable).
//
// If we didn't get any data from this process,
// then we may have reached the end of the TCP stream.
//
// Be sure callbacks are enabled so we're notified about a disconnection.
if ([partialReadBuffer length] == 0)
{
if ([self usingCFStream]) {
// Callbacks never disabled
}
else {
[self resumeReadSource];
}
}
}
}
}
- (void)flushSSLBuffers
{
LogTrace();
NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket");
if ([partialReadBuffer length] > 0)
{
// Only flush the ssl buffers if the partialReadBuffer is empty.
// This is to avoid growing the partialReadBuffer inifinitely large.
return;
}
#if TARGET_OS_IPHONE
if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
{
LogVerbose(@"%@ - Flushing ssl buffers into partialReadBuffer...", THIS_METHOD);
CFIndex defaultBytesToRead = (1024 * 16);
NSUInteger partialReadBufferOffset = [partialReadBuffer length];
[partialReadBuffer increaseLengthBy:defaultBytesToRead];
uint8_t *buffer = [partialReadBuffer mutableBytes] + partialReadBufferOffset;
CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result);
if (result <= 0)
{
[partialReadBuffer setLength:partialReadBufferOffset];
}
else
{
[partialReadBuffer setLength:(partialReadBufferOffset + result)];
}
flags &= ~kSecureSocketHasBytesAvailable;
}
#else
__block NSUInteger estimatedBytesAvailable = 0;
dispatch_block_t updateEstimatedBytesAvailable = ^{
// Figure out if there is any data available to be read
//
// socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket
// [sslReadBuffer length] <- Number of encrypted bytes we've buffered from bsd socket
// sslInternalBufSize <- Number od decrypted bytes SecureTransport has buffered
//
// We call the variable "estimated" because we don't know how many decrypted bytes we'll get
// from the encrypted bytes in the sslReadBuffer.
// However, we do know this is an upper bound on the estimation.
estimatedBytesAvailable = socketFDBytesAvailable + [sslReadBuffer length];
size_t sslInternalBufSize = 0;
SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
estimatedBytesAvailable += sslInternalBufSize;
};
updateEstimatedBytesAvailable();
if (estimatedBytesAvailable > 0)
{
LogVerbose(@"%@ - Flushing ssl buffers into partialReadBuffer...", THIS_METHOD);
BOOL done = NO;
do
{
LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable);
// Make room in the partialReadBuffer
NSUInteger partialReadBufferOffset = [partialReadBuffer length];
[partialReadBuffer increaseLengthBy:estimatedBytesAvailable];
uint8_t *buffer = (uint8_t *)[partialReadBuffer mutableBytes] + partialReadBufferOffset;
size_t bytesRead = 0;
// Read data into partialReadBuffer
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead);
[partialReadBuffer setLength:(partialReadBufferOffset + bytesRead)];
LogVerbose(@"%@ - partialReadBuffer.length = %lu", THIS_METHOD, (unsigned long)[partialReadBuffer length]);
if (result != noErr)
{
done = YES;
}
else
{
updateEstimatedBytesAvailable();
}
} while (!done && estimatedBytesAvailable > 0);
}
#endif
}
- (void)doReadData
{
LogTrace();
// This method is called on the socketQueue.
// It might be called directly, or via the readSource when data is available to be read.
if ((currentRead == nil) || (flags & kReadsPaused))
{
LogVerbose(@"No currentRead or kReadsPaused");
// Unable to read at this time
if (flags & kSocketSecure)
{
// Here's the situation:
//
// We have an established secure connection.
// There may not be a currentRead, but there might be encrypted data sitting around for us.
// When the user does get around to issuing a read, that encrypted data will need to be decrypted.
//
// So why make the user wait?
// We might as well get a head start on decrypting some data now.
//
// The other reason we do this has to do with detecting a socket disconnection.
// The SSL/TLS protocol has it's own disconnection handshake.
// So when a secure socket is closed, a "goodbye" packet comes across the wire.
// We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection.
[self flushSSLBuffers];
}
if ([self usingCFStream])
{
// CFReadStream only fires once when there is available data.
// It won't fire again until we've invoked CFReadStreamRead.
}
else
{
// If the readSource is firing, we need to pause it
// or else it will continue to fire over and over again.
//
// If the readSource is not firing,
// we want it to continue monitoring the socket.
if (socketFDBytesAvailable > 0)
{
[self suspendReadSource];
}
}
return;
}
BOOL hasBytesAvailable;
unsigned long estimatedBytesAvailable;
#if TARGET_OS_IPHONE
{
if (flags & kSocketSecure)
{
// Relegated to using CFStream... :( Boo! Give us SecureTransport Apple!
estimatedBytesAvailable = 0;
if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
hasBytesAvailable = YES;
else
hasBytesAvailable = NO;
}
else
{
estimatedBytesAvailable = socketFDBytesAvailable;
hasBytesAvailable = (estimatedBytesAvailable > 0);
}
}
#else
{
estimatedBytesAvailable = socketFDBytesAvailable;
if (flags & kSocketSecure)
{
// There are 2 buffers to be aware of here.
//
// We are using SecureTransport, a TLS/SSL security layer which sits atop TCP.
// We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction.
// Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport.
// SecureTransport then decrypts the data, and finally returns the decrypted data back to us.
//
// The first buffer is one we create.
// SecureTransport often requests small amounts of data.
// This has to do with the encypted packets that are coming across the TCP stream.
// But it's non-optimal to do a bunch of small reads from the BSD socket.
// So our SSLReadFunction reads all available data from the socket (optimizing the sys call)
// and may store excess in the sslReadBuffer.
estimatedBytesAvailable += [sslReadBuffer length];
// The second buffer is within SecureTransport.
// As mentioned earlier, there are encrypted packets coming across the TCP stream.
// SecureTransport needs the entire packet to decrypt it.
// But if the entire packet produces X bytes of decrypted data,
// and we only asked SecureTransport for X/2 bytes of data,
// it must store the extra X/2 bytes of decrypted data for the next read.
//
// The SSLGetBufferedReadSize function will tell us the size of this internal buffer.
// From the documentation:
//
// "This function does not block or cause any low-level read operations to occur."
size_t sslInternalBufSize = 0;
SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
estimatedBytesAvailable += sslInternalBufSize;
}
hasBytesAvailable = (estimatedBytesAvailable > 0);
}
#endif
if ((hasBytesAvailable == NO) && ([partialReadBuffer length] == 0))
{
LogVerbose(@"No data available to read...");
// No data available to read.
if (![self usingCFStream])
{
// Need to wait for readSource to fire and notify us of
// available data in the socket's internal read buffer.
[self resumeReadSource];
}
return;
}
if (flags & kStartingReadTLS)
{
LogVerbose(@"Waiting for SSL/TLS handshake to complete");
// The readQueue is waiting for SSL/TLS handshake to complete.
if (flags & kStartingWriteTLS)
{
#if !TARGET_OS_IPHONE
{
// We are in the process of a SSL Handshake.
// We were waiting for incoming data which has just arrived.
[self continueSSLHandshake];
}
#endif
}
else
{
// We are still waiting for the writeQueue to drain and start the SSL/TLS process.
// We now know data is available to read.
if (![self usingCFStream])
{
// Suspend the read source or else it will continue to fire nonstop.
[self suspendReadSource];
}
}
return;
}
BOOL done = NO; // Completed read operation
NSError *error = nil; // Error occured
NSUInteger totalBytesReadForCurrentRead = 0;
//
// STEP 1 - READ FROM PREBUFFER
//
NSUInteger partialReadBufferLength = [partialReadBuffer length];
if (partialReadBufferLength > 0)
{
// There are 3 types of read packets:
//
// 1) Read all available data.
// 2) Read a specific length of data.
// 3) Read up to a particular terminator.
NSUInteger bytesToCopy;
if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
bytesToCopy = [currentRead readLengthForTermWithPreBuffer:partialReadBuffer found:&done];
}
else
{
// Read type #1 or #2
bytesToCopy = [currentRead readLengthForNonTermWithHint:partialReadBufferLength];
}
// Make sure we have enough room in the buffer for our read.
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];
// Copy bytes from prebuffer into packet buffer
uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset +
currentRead->bytesDone;
memcpy(buffer, [partialReadBuffer bytes], bytesToCopy);
// Remove the copied bytes from the partial read buffer
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToCopy) withBytes:NULL length:0];
partialReadBufferLength -= bytesToCopy;
LogVerbose(@"copied(%lu) partialReadBufferLength(%lu)", bytesToCopy, partialReadBufferLength);
// Update totals
currentRead->bytesDone += bytesToCopy;
totalBytesReadForCurrentRead += bytesToCopy;
// Check to see if the read operation is done
if (currentRead->readLength > 0)
{
// Read type #2 - read a specific length of data
done = (currentRead->bytesDone == currentRead->readLength);
}
else if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
// Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method
if (!done && currentRead->maxLength > 0)
{
// We're not done and there's a set maxLength.
// Have we reached that maxLength yet?
if (currentRead->bytesDone >= currentRead->maxLength)
{
error = [self readMaxedOutError];
}
}
}
else
{
// Read type #1 - read all available data
//
// We're done as soon as
// - we've read all available data (in prebuffer and socket)
// - we've read the maxLength of read packet.
done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength));
}
}
//
// STEP 2 - READ FROM SOCKET
//
BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to via socket (end of file)
BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more
if (!done && !error && !socketEOF && !waiting && hasBytesAvailable)
{
NSAssert((partialReadBufferLength == 0), @"Invalid logic");
// There are 3 types of read packets:
//
// 1) Read all available data.
// 2) Read a specific length of data.
// 3) Read up to a particular terminator.
BOOL readIntoPartialReadBuffer = NO;
NSUInteger bytesToRead;
if ([self usingCFStream])
{
// Since Apple has neglected to make SecureTransport available on iOS,
// we are relegated to using the slower, less powerful, RunLoop based CFStream API.
//
// This API doesn't tell us how much data is available on the socket to be read.
// If we had that information we could optimize our memory allocations, and sys calls.
//
// But alas...
// So we do it old school, and just read as much data from the socket as we can.
NSUInteger defaultReadLength = (1024 * 32);
bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength
shouldPreBuffer:&readIntoPartialReadBuffer];
}
else
{
if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable
shouldPreBuffer:&readIntoPartialReadBuffer];
}
else
{
// Read type #1 or #2
bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable];
}
}
if (bytesToRead > SIZE_MAX) // NSUInteger may be bigger than size_t (read param 3)
{
bytesToRead = SIZE_MAX;
}
// Make sure we have enough room in the buffer for our read.
//
// We are either reading directly into the currentRead->buffer,
// or we're reading into the temporary partialReadBuffer.
uint8_t *buffer;
if (readIntoPartialReadBuffer)
{
if (bytesToRead > partialReadBufferLength)
{
[partialReadBuffer setLength:bytesToRead];
}
buffer = [partialReadBuffer mutableBytes];
}
else
{
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone;
}
// Read data into buffer
size_t bytesRead = 0;
if (flags & kSocketSecure)
{
#if TARGET_OS_IPHONE
{
CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead);
LogVerbose(@"CFReadStreamRead(): result = %i", (int)result);
if (result < 0)
{
error = [NSMakeCollectable(CFReadStreamCopyError(readStream)) autorelease];
if (readIntoPartialReadBuffer)
[partialReadBuffer setLength:0];
}
else if (result == 0)
{
socketEOF = YES;
if (readIntoPartialReadBuffer)
[partialReadBuffer setLength:0];
}
else
{
waiting = YES;
bytesRead = (size_t)result;
}
// We only know how many decrypted bytes were read.
// The actual number of bytes read was likely more due to the overhead of the encryption.
// So we reset our flag, and rely on the next callback to alert us of more data.
flags &= ~kSecureSocketHasBytesAvailable;
}
#else
{
// The documentation from Apple states:
//
// "a read operation might return errSSLWouldBlock,
// indicating that less data than requested was actually transferred"
//
// However, starting around 10.7, the function will sometimes return noErr,
// even if it didn't read as much data as requested. So we need to watch out for that.
OSStatus result;
do
{
void *loop_buffer = buffer + bytesRead;
size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead;
size_t loop_bytesRead = 0;
result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead);
LogVerbose(@"read from secure socket = %u", (unsigned)bytesRead);
bytesRead += loop_bytesRead;
} while ((result == noErr) && (bytesRead < bytesToRead));
if (result != noErr)
{
if (result == errSSLWouldBlock)
waiting = YES;
else
error = [self sslError:result];
// It's possible that bytesRead > 0, yet the result is errSSLWouldBlock.
// This happens when the SSLRead function is able to read some data,
// but not the entire amount we requested.
if (bytesRead <= 0)
{
bytesRead = 0;
if (readIntoPartialReadBuffer)
[partialReadBuffer setLength:0];
}
}
// Do not modify socketFDBytesAvailable.
// It will be updated via the SSLReadFunction().
}
#endif
}
else
{
int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
LogVerbose(@"read from socket = %i", (int)result);
if (result < 0)
{
if (errno == EWOULDBLOCK)
waiting = YES;
else
error = [self errnoErrorWithReason:@"Error in read() function"];
socketFDBytesAvailable = 0;
if (readIntoPartialReadBuffer)
[partialReadBuffer setLength:0];
}
else if (result == 0)
{
socketEOF = YES;
socketFDBytesAvailable = 0;
if (readIntoPartialReadBuffer)
[partialReadBuffer setLength:0];
}
else
{
bytesRead = result;
if (socketFDBytesAvailable <= bytesRead)
socketFDBytesAvailable = 0;
else
socketFDBytesAvailable -= bytesRead;
if (socketFDBytesAvailable == 0)
{
waiting = YES;
}
}
}
if (bytesRead > 0)
{
// Check to see if the read operation is done
if (currentRead->readLength > 0)
{
// Read type #2 - read a specific length of data
//
// Note: We should never be using a prebuffer when we're reading a specific length of data.
NSAssert(readIntoPartialReadBuffer == NO, @"Invalid logic");
currentRead->bytesDone += bytesRead;
totalBytesReadForCurrentRead += bytesRead;
done = (currentRead->bytesDone == currentRead->readLength);
}
else if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
if (readIntoPartialReadBuffer)
{
// We just read a big chunk of data into the partialReadBuffer.
// Search for the terminating sequence.
//
// Note: We are depending upon [partialReadBuffer length] to tell us how much data is
// available in the partialReadBuffer. So we need to be sure this matches how many bytes
// have actually been read into said buffer.
[partialReadBuffer setLength:bytesRead];
bytesToRead = [currentRead readLengthForTermWithPreBuffer:partialReadBuffer found:&done];
// Ensure there's room on the read packet's buffer
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
// Copy bytes from prebuffer into read buffer
uint8_t *preBuf = [partialReadBuffer mutableBytes];
uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ currentRead->bytesDone;
memcpy(readBuf, preBuf, bytesToRead);
// Remove the copied bytes from the prebuffer
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToRead) withBytes:NULL length:0];
// Update totals
currentRead->bytesDone += bytesToRead;
totalBytesReadForCurrentRead += bytesToRead;
// Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above
}
else
{
// We just read a big chunk of data directly into the packet's buffer.
// We need to move any overflow into the prebuffer.
NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead];
if (overflow == 0)
{
// Perfect match!
// Every byte we read stays in the read buffer,
// and the last byte we read was the last byte of the term.
currentRead->bytesDone += bytesRead;
totalBytesReadForCurrentRead += bytesRead;
done = YES;
}
else if (overflow > 0)
{
// The term was found within the data that we read,
// and there are extra bytes that extend past the end of the term.
// We need to move these excess bytes out of the read packet and into the prebuffer.
NSInteger underflow = bytesRead - overflow;
// Copy excess data into partialReadBuffer
void *overflowBuffer = buffer + currentRead->bytesDone + underflow;
[partialReadBuffer appendBytes:overflowBuffer length:overflow];
// Note: The completeCurrentRead method will trim the buffer for us.
currentRead->bytesDone += underflow;
totalBytesReadForCurrentRead += underflow;
done = YES;
}
else
{
// The term was not found within the data that we read.
currentRead->bytesDone += bytesRead;
totalBytesReadForCurrentRead += bytesRead;
done = NO;
}
}
if (!done && currentRead->maxLength > 0)
{
// We're not done and there's a set maxLength.
// Have we reached that maxLength yet?
if (currentRead->bytesDone >= currentRead->maxLength)
{
error = [self readMaxedOutError];
}
}
}
else
{
// Read type #1 - read all available data
if (readIntoPartialReadBuffer)
{
// We just read a chunk of data into the partialReadBuffer.
// Copy the data into the read packet.
//
// Recall that we didn't read directly into the packet's buffer to avoid
// over-allocating memory since we had no clue how much data was available to be read.
//
// Note: We are depending upon [partialReadBuffer length] to tell us how much data is
// available in the partialReadBuffer. So we need to be sure this matches how many bytes
// have actually been read into said buffer.
[partialReadBuffer setLength:bytesRead];
// Ensure there's room on the read packet's buffer
[currentRead ensureCapacityForAdditionalDataOfLength:bytesRead];
// Copy bytes from prebuffer into read buffer
uint8_t *preBuf = [partialReadBuffer mutableBytes];
uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ currentRead->bytesDone;
memcpy(readBuf, preBuf, bytesRead);
// Remove the copied bytes from the prebuffer
[partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesRead) withBytes:NULL length:0];
// Update totals
currentRead->bytesDone += bytesRead;
totalBytesReadForCurrentRead += bytesRead;
}
else
{
currentRead->bytesDone += bytesRead;
totalBytesReadForCurrentRead += bytesRead;
}
done = YES;
}
} // if (bytesRead > 0)
} // if (!done && !error && !socketEOF && !waiting && hasBytesAvailable)
if (!done && currentRead->readLength == 0 && currentRead->term == nil)
{
// Read type #1 - read all available data
//
// We might arrive here if we read data from the prebuffer but not from the socket.
done = (totalBytesReadForCurrentRead > 0);
}
// Check to see if we're done, or if we've made progress
if (done)
{
[self completeCurrentRead];
if (!error && (!socketEOF || partialReadBufferLength > 0))
{
[self maybeDequeueRead];
}
}
else if (totalBytesReadForCurrentRead > 0)
{
// We're not done read type #2 or #3 yet, but we have read in some bytes
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)])
{
id theDelegate = delegate;
GCDAsyncReadPacket *theRead = currentRead;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theRead->tag];
[pool drain];
});
}
}
// Check for errors
if (error)
{
[self closeWithError:error];
}
else if (socketEOF)
{
[self doReadEOF];
}
else if (waiting)
{
if (![self usingCFStream])
{
// Monitor the socket for readability (if we're not already doing so)
[self resumeReadSource];
}
}
// Do not add any code here without first adding return statements in the error cases above.
}
- (void)doReadEOF
{
LogTrace();
// This method may be called more than once.
// If the EOF is read while there is still data in the partialReadBuffer,
// then this method may be called continually after invocations of doReadData to see if it's time to disconnect.
flags |= kSocketHasReadEOF;
if (flags & kSocketSecure)
{
// If the SSL layer has any buffered data, flush it into the partialReadBuffer now.
[self flushSSLBuffers];
}
BOOL shouldDisconnect;
NSError *error = nil;
if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS))
{
// We received an EOF during or prior to startTLS.
// The SSL/TLS handshake is now impossible, so this is an unrecoverable situation.
shouldDisconnect = YES;
#if !TARGET_OS_IPHONE
error = [self sslError:errSSLClosedAbort];
#endif
}
else if (flags & kReadStreamClosed)
{
// The partialReadBuffer has already been drained.
// The config allows half-duplex connections.
// We've previously checked the socket, and it appeared writeable.
// So we marked the read stream as closed and notified the delegate.
//
// As per the half-duplex contract, the socket will be closed when a write fails,
// or when the socket is manually closed.
shouldDisconnect = NO;
}
else if ([partialReadBuffer length] > 0)
{
LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer");
// Although we won't be able to read any more data from the socket,
// there is existing data that has been prebuffered that we can read.
shouldDisconnect = NO;
}
else if (config & kAllowHalfDuplexConnection)
{
// We just received an EOF (end of file) from the socket's read stream.
// This means the remote end of the socket (the peer we're connected to)
// has explicitly stated that it will not be sending us any more data.
//
// Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us)
int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
struct pollfd pfd[1];
pfd[0].fd = socketFD;
pfd[0].events = POLLOUT;
pfd[0].revents = 0;
poll(pfd, 1, 0);
if (pfd[0].revents & POLLOUT)
{
// Socket appears to still be writeable
shouldDisconnect = NO;
flags |= kReadStreamClosed;
// Notify the delegate that we're going half-duplex
if (delegateQueue && [delegate respondsToSelector:@selector(socketDidCloseReadStream:)])
{
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socketDidCloseReadStream:self];
[pool drain];
});
}
}
else
{
shouldDisconnect = YES;
}
}
else
{
shouldDisconnect = YES;
}
if (shouldDisconnect)
{
if (error == nil)
{
error = [self connectionClosedError];
}
[self closeWithError:error];
}
else
{
if (![self usingCFStream])
{
// Suspend the read source (if needed)
[self suspendReadSource];
}
}
}
- (void)completeCurrentRead
{
LogTrace();
NSAssert(currentRead, @"Trying to complete current read when there is no current read.");
NSData *result;
if (currentRead->bufferOwner)
{
// We created the buffer on behalf of the user.
// Trim our buffer to be the proper size.
[currentRead->buffer setLength:currentRead->bytesDone];
result = currentRead->buffer;
}
else
{
// We did NOT create the buffer.
// The buffer is owned by the caller.
// Only trim the buffer if we had to increase its size.
if ([currentRead->buffer length] > currentRead->originalBufferLength)
{
NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone;
NSUInteger origSize = currentRead->originalBufferLength;
NSUInteger buffSize = MAX(readSize, origSize);
[currentRead->buffer setLength:buffSize];
}
uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset;
result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO];
}
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadData:withTag:)])
{
id theDelegate = delegate;
GCDAsyncReadPacket *theRead = currentRead;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socket:self didReadData:result withTag:theRead->tag];
[pool drain];
});
}
[self endCurrentRead];
}
- (void)endCurrentRead
{
if (readTimer)
{
dispatch_source_cancel(readTimer);
readTimer = NULL;
}
[currentRead release];
currentRead = nil;
}
- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout
{
if (timeout >= 0.0)
{
readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
dispatch_source_set_event_handler(readTimer, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self doReadTimeout];
[pool drain];
});
dispatch_source_t theReadTimer = readTimer;
dispatch_source_set_cancel_handler(readTimer, ^{
LogVerbose(@"dispatch_release(readTimer)");
dispatch_release(theReadTimer);
});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(readTimer);
}
}
- (void)doReadTimeout
{
// This is a little bit tricky.
// Ideally we'd like to synchronously query the delegate about a timeout extension.
// But if we do so synchronously we risk a possible deadlock.
// So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.
flags |= kReadsPaused;
if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)])
{
id theDelegate = delegate;
GCDAsyncReadPacket *theRead = currentRead;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *delegatePool = [[NSAutoreleasePool alloc] init];
NSTimeInterval timeoutExtension = 0.0;
timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag
elapsed:theRead->timeout
bytesDone:theRead->bytesDone];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *callbackPool = [[NSAutoreleasePool alloc] init];
[self doReadTimeoutWithExtension:timeoutExtension];
[callbackPool drain];
});
[delegatePool drain];
});
}
else
{
[self doReadTimeoutWithExtension:0.0];
}
}
- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension
{
if (currentRead)
{
if (timeoutExtension > 0.0)
{
currentRead->timeout += timeoutExtension;
// Reschedule the timer
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC));
dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
// Unpause reads, and continue
flags &= ~kReadsPaused;
[self doReadData];
}
else
{
LogVerbose(@"ReadTimeout");
[self closeWithError:[self readTimeoutError]];
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Writing
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
{
if ([data length] == 0) return;
GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[writeQueue addObject:packet];
[self maybeDequeueWrite];
}
[pool drain];
});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
[packet release];
}
/**
* Conditionally starts a new write.
*
* It is called when:
* - a user requests a write
* - after a write request has finished (to handle the next request)
* - immediately after the socket opens to handle any pending requests
*
* This method also handles auto-disconnect post read/write completion.
**/
- (void)maybeDequeueWrite
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
// If we're not currently processing a write AND we have an available write stream
if ((currentWrite == nil) && (flags & kConnected))
{
if ([writeQueue count] > 0)
{
// Dequeue the next object in the write queue
currentWrite = [[writeQueue objectAtIndex:0] retain];
[writeQueue removeObjectAtIndex:0];
if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]])
{
LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
// Attempt to start TLS
flags |= kStartingWriteTLS;
// This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
[self maybeStartTLS];
}
else
{
LogVerbose(@"Dequeued GCDAsyncWritePacket");
// Setup write timer (if needed)
[self setupWriteTimerWithTimeout:currentWrite->timeout];
// Immediately write, if possible
[self doWriteData];
}
}
else if (flags & kDisconnectAfterWrites)
{
if (flags & kDisconnectAfterReads)
{
if (([readQueue count] == 0) && (currentRead == nil))
{
[self closeWithError:nil];
}
}
else
{
[self closeWithError:nil];
}
}
}
}
- (void)doWriteData
{
LogTrace();
// This method is called by the writeSource via the socketQueue
if ((currentWrite == nil) || (flags & kWritesPaused))
{
LogVerbose(@"No currentWrite or kWritesPaused");
// Unable to write at this time
if ([self usingCFStream])
{
// CFWriteStream only fires once when there is available data.
// It won't fire again until we've invoked CFWriteStreamWrite.
}
else
{
// If the writeSource is firing, we need to pause it
// or else it will continue to fire over and over again.
if (flags & kSocketCanAcceptBytes)
{
[self suspendWriteSource];
}
}
return;
}
if (!(flags & kSocketCanAcceptBytes))
{
LogVerbose(@"No space available to write...");
// No space available to write.
if (![self usingCFStream])
{
// Need to wait for writeSource to fire and notify us of
// available space in the socket's internal write buffer.
[self resumeWriteSource];
}
return;
}
if (flags & kStartingWriteTLS)
{
LogVerbose(@"Waiting for SSL/TLS handshake to complete");
// The writeQueue is waiting for SSL/TLS handshake to complete.
if (flags & kStartingReadTLS)
{
#if !TARGET_OS_IPHONE
// We are in the process of a SSL Handshake.
// We were waiting for available space in the socket's internal OS buffer to continue writing.
[self continueSSLHandshake];
#endif
}
else
{
// We are still waiting for the readQueue to drain and start the SSL/TLS process.
// We now know we can write to the socket.
if (![self usingCFStream])
{
// Suspend the write source or else it will continue to fire nonstop.
[self suspendWriteSource];
}
}
return;
}
// Note: This method is not called if theCurrentWrite is an GCDAsyncSpecialPacket (startTLS packet)
BOOL waiting = NO;
NSError *error = nil;
size_t bytesWritten = 0;
if (flags & kSocketSecure)
{
#if TARGET_OS_IPHONE
const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;
NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;
if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
{
bytesToWrite = SIZE_MAX;
}
CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite);
LogVerbose(@"CFWriteStreamWrite(%lu) = %li", bytesToWrite, result);
if (result < 0)
{
error = [NSMakeCollectable(CFWriteStreamCopyError(writeStream)) autorelease];
}
else
{
bytesWritten = (size_t)result;
// We always set waiting to true in this scenario.
// CFStream may have altered our underlying socket to non-blocking.
// Thus if we attempt to write without a callback, we may end up blocking our queue.
waiting = YES;
}
#else
// We're going to use the SSLWrite function.
//
// OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed)
//
// Parameters:
// context - An SSL session context reference.
// data - A pointer to the buffer of data to write.
// dataLength - The amount, in bytes, of data to write.
// processed - On return, the length, in bytes, of the data actually written.
//
// It sounds pretty straight-forward,
// but there are a few caveats you should be aware of.
//
// The SSLWrite method operates in a non-obvious (and rather annoying) manner.
// According to the documentation:
//
// Because you may configure the underlying connection to operate in a non-blocking manner,
// a write operation might return errSSLWouldBlock, indicating that less data than requested
// was actually transferred. In this case, you should repeat the call to SSLWrite until some
// other result is returned.
//
// This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock,
// then the SSLWrite method returns (with the proper errSSLWouldBlock return value),
// but it sets bytesWritten to bytesToWrite !!
//
// In other words, if the SSLWrite function doesn't completely write all the data we tell it to,
// then it doesn't tell us how many bytes were actually written.
//
// You might be wondering:
// If the SSLWrite function doesn't tell us how many bytes were written,
// then how in the world are we supposed to update our parameters (buffer & bytesToWrite)
// for the next time we invoke SSLWrite?
//
// The answer is that SSLWrite cached all the data we told it to write,
// and it will push out that data next time we call SSLWrite.
// If we call SSLWrite with new data, it will push out the cached data first, and then the new data.
// If we call SSLWrite with empty data, then it will simply push out the cached data.
//
// For this purpose we're going to break large writes into a series of smaller writes.
// This allows us to report progress back to the delegate.
OSStatus result;
BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0);
BOOL hasNewDataToWrite = YES;
if (hasCachedDataToWrite)
{
size_t processed = 0;
result = SSLWrite(sslContext, NULL, 0, &processed);
if (result == noErr)
{
bytesWritten = sslWriteCachedLength;
sslWriteCachedLength = 0;
if (currentWrite->bytesDone == [currentWrite->buffer length])
{
// We've written all data for the current write.
hasNewDataToWrite = NO;
}
}
else
{
if (result == errSSLWouldBlock)
{
waiting = YES;
}
else
{
error = [self sslError:result];
}
// Can't write any new data since we were unable to write the cached data.
hasNewDataToWrite = NO;
}
}
if (hasNewDataToWrite)
{
const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone + bytesWritten;
NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten;
if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
{
bytesToWrite = SIZE_MAX;
}
size_t bytesRemaining = bytesToWrite;
BOOL keepLooping = YES;
while (keepLooping)
{
size_t sslBytesToWrite = MIN(bytesRemaining, 32768);
size_t sslBytesWritten = 0;
result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);
if (result == noErr)
{
buffer += sslBytesWritten;
bytesWritten += sslBytesWritten;
bytesRemaining -= sslBytesWritten;
keepLooping = (bytesRemaining > 0);
}
else
{
if (result == errSSLWouldBlock)
{
waiting = YES;
sslWriteCachedLength = sslBytesToWrite;
}
else
{
error = [self sslError:result];
}
keepLooping = NO;
}
} // while (keepLooping)
} // if (hasNewDataToWrite)
#endif
}
else
{
int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;
NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;
if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
{
bytesToWrite = SIZE_MAX;
}
ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
LogVerbose(@"wrote to socket = %zd", result);
// Check results
if (result < 0)
{
if (errno == EWOULDBLOCK)
{
waiting = YES;
}
else
{
error = [self errnoErrorWithReason:@"Error in write() function"];
}
}
else
{
bytesWritten = result;
}
}
// We're done with our writing.
// If we explictly ran into a situation where the socket told us there was no room in the buffer,
// then we immediately resume listening for notifications.
//
// We must do this before we dequeue another write,
// as that may in turn invoke this method again.
//
// Note that if CFStream is involved, it may have maliciously put our socket in blocking mode.
if (waiting)
{
flags &= ~kSocketCanAcceptBytes;
if (![self usingCFStream])
{
[self resumeWriteSource];
}
}
// Check our results
BOOL done = NO;
if (bytesWritten > 0)
{
// Update total amount read for the current write
currentWrite->bytesDone += bytesWritten;
LogVerbose(@"currentWrite->bytesDone = %lu", currentWrite->bytesDone);
// Is packet done?
done = (currentWrite->bytesDone == [currentWrite->buffer length]);
}
if (done)
{
[self completeCurrentWrite];
if (!error)
{
[self maybeDequeueWrite];
}
}
else
{
// We were unable to finish writing the data,
// so we're waiting for another callback to notify us of available space in the lower-level output buffer.
if (!waiting & !error)
{
// This would be the case if our write was able to accept some data, but not all of it.
flags &= ~kSocketCanAcceptBytes;
if (![self usingCFStream])
{
[self resumeWriteSource];
}
}
if (bytesWritten > 0)
{
// We're not done with the entire write, but we have written some bytes
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)])
{
id theDelegate = delegate;
GCDAsyncWritePacket *theWrite = currentWrite;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWrite->tag];
[pool drain];
});
}
}
}
// Check for errors
if (error)
{
[self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]];
}
// Do not add any code here without first adding a return statement in the error case above.
}
- (void)completeCurrentWrite
{
LogTrace();
NSAssert(currentWrite, @"Trying to complete current write when there is no current write.");
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWriteDataWithTag:)])
{
id theDelegate = delegate;
GCDAsyncWritePacket *theWrite = currentWrite;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socket:self didWriteDataWithTag:theWrite->tag];
[pool drain];
});
}
[self endCurrentWrite];
}
- (void)endCurrentWrite
{
if (writeTimer)
{
dispatch_source_cancel(writeTimer);
writeTimer = NULL;
}
[currentWrite release];
currentWrite = nil;
}
- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout
{
if (timeout >= 0.0)
{
writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
dispatch_source_set_event_handler(writeTimer, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self doWriteTimeout];
[pool drain];
});
dispatch_source_t theWriteTimer = writeTimer;
dispatch_source_set_cancel_handler(writeTimer, ^{
LogVerbose(@"dispatch_release(writeTimer)");
dispatch_release(theWriteTimer);
});
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0);
dispatch_resume(writeTimer);
}
}
- (void)doWriteTimeout
{
// This is a little bit tricky.
// Ideally we'd like to synchronously query the delegate about a timeout extension.
// But if we do so synchronously we risk a possible deadlock.
// So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.
flags |= kWritesPaused;
if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)])
{
id theDelegate = delegate;
GCDAsyncWritePacket *theWrite = currentWrite;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *delegatePool = [[NSAutoreleasePool alloc] init];
NSTimeInterval timeoutExtension = 0.0;
timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag
elapsed:theWrite->timeout
bytesDone:theWrite->bytesDone];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *callbackPool = [[NSAutoreleasePool alloc] init];
[self doWriteTimeoutWithExtension:timeoutExtension];
[callbackPool drain];
});
[delegatePool drain];
});
}
else
{
[self doWriteTimeoutWithExtension:0.0];
}
}
- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension
{
if (currentWrite)
{
if (timeoutExtension > 0.0)
{
currentWrite->timeout += timeoutExtension;
// Reschedule the timer
dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC));
dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0);
// Unpause writes, and continue
flags &= ~kWritesPaused;
[self doWriteData];
}
else
{
LogVerbose(@"WriteTimeout");
[self closeWithError:[self writeTimeoutError]];
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Security
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)startTLS:(NSDictionary *)tlsSettings
{
LogTrace();
if (tlsSettings == nil)
{
// Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary,
// but causes problems if we later try to fetch the remote host's certificate.
//
// To be exact, it causes the following to return NULL instead of the normal result:
// CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates)
//
// So we use an empty dictionary instead, which works perfectly.
tlsSettings = [NSDictionary dictionary];
}
GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
dispatch_async(socketQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[writeQueue addObject:packet];
flags |= kQueuedTLS;
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
[pool drain];
});
[packet release];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Security - Mac OS X
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#if !TARGET_OS_IPHONE
- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength
{
LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength);
if ((socketFDBytesAvailable == 0) && ([sslReadBuffer length] == 0))
{
LogVerbose(@"%@ - No data available to read...", THIS_METHOD);
// No data available to read.
//
// Need to wait for readSource to fire and notify us of
// available data in the socket's internal read buffer.
[self resumeReadSource];
*bufferLength = 0;
return errSSLWouldBlock;
}
size_t totalBytesRead = 0;
size_t totalBytesLeftToBeRead = *bufferLength;
BOOL done = NO;
BOOL socketError = NO;
//
// STEP 1 : READ FROM SSL PRE BUFFER
//
NSUInteger sslReadBufferLength = [sslReadBuffer length];
if (sslReadBufferLength > 0)
{
LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD);
size_t bytesToCopy;
if (sslReadBufferLength > totalBytesLeftToBeRead)
bytesToCopy = totalBytesLeftToBeRead;
else
bytesToCopy = (size_t)sslReadBufferLength;
LogVerbose(@"%@: Copying %zu bytes from sslReadBuffer", THIS_METHOD, bytesToCopy);
memcpy(buffer, [sslReadBuffer mutableBytes], bytesToCopy);
[sslReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToCopy) withBytes:NULL length:0];
LogVerbose(@"%@: sslReadBuffer.length = %lu", THIS_METHOD, (unsigned long)[sslReadBuffer length]);
totalBytesRead += bytesToCopy;
totalBytesLeftToBeRead -= bytesToCopy;
done = (totalBytesLeftToBeRead == 0);
if (done) LogVerbose(@"%@: Complete", THIS_METHOD);
}
//
// STEP 2 : READ FROM SOCKET
//
if (!done && (socketFDBytesAvailable > 0))
{
LogVerbose(@"%@: Reading from socket...", THIS_METHOD);
int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD;
BOOL readIntoPreBuffer;
size_t bytesToRead;
uint8_t *buf;
if (socketFDBytesAvailable > totalBytesLeftToBeRead)
{
// Read all available data from socket into sslReadBuffer.
// Then copy requested amount into dataBuffer.
LogVerbose(@"%@: Reading into sslReadBuffer...", THIS_METHOD);
if ([sslReadBuffer length] < socketFDBytesAvailable)
{
[sslReadBuffer setLength:socketFDBytesAvailable];
}
readIntoPreBuffer = YES;
bytesToRead = (size_t)socketFDBytesAvailable;
buf = [sslReadBuffer mutableBytes];
}
else
{
// Read available data from socket directly into dataBuffer.
LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD);
readIntoPreBuffer = NO;
bytesToRead = totalBytesLeftToBeRead;
buf = (uint8_t *)buffer + totalBytesRead;
}
ssize_t result = read(socketFD, buf, bytesToRead);
LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result);
if (result < 0)
{
LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno);
if (errno != EWOULDBLOCK)
{
socketError = YES;
}
socketFDBytesAvailable = 0;
if (readIntoPreBuffer)
{
[sslReadBuffer setLength:0];
}
}
else if (result == 0)
{
LogVerbose(@"%@: read EOF", THIS_METHOD);
socketError = YES;
socketFDBytesAvailable = 0;
if (readIntoPreBuffer)
{
[sslReadBuffer setLength:0];
}
}
else
{
size_t bytesReadFromSocket = result;
if (socketFDBytesAvailable > bytesReadFromSocket)
socketFDBytesAvailable -= bytesReadFromSocket;
else
socketFDBytesAvailable = 0;
if (readIntoPreBuffer)
{
size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket);
LogVerbose(@"%@: Copying %zu bytes out of sslReadBuffer", THIS_METHOD, bytesToCopy);
memcpy((uint8_t *)buffer + totalBytesRead, [sslReadBuffer bytes], bytesToCopy);
[sslReadBuffer setLength:bytesReadFromSocket];
[sslReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToCopy) withBytes:NULL length:0];
totalBytesRead += bytesToCopy;
totalBytesLeftToBeRead -= bytesToCopy;
LogVerbose(@"%@: sslReadBuffer.length = %lu", THIS_METHOD, (unsigned long)[sslReadBuffer length]);
}
else
{
totalBytesRead += bytesReadFromSocket;
totalBytesLeftToBeRead -= bytesReadFromSocket;
}
done = (totalBytesLeftToBeRead == 0);
if (done) LogVerbose(@"%@: Complete", THIS_METHOD);
}
}
*bufferLength = totalBytesRead;
if (done)
return noErr;
if (socketError)
return errSSLClosedAbort;
return errSSLWouldBlock;
}
- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength
{
if (!(flags & kSocketCanAcceptBytes))
{
// Unable to write.
//
// Need to wait for writeSource to fire and notify us of
// available space in the socket's internal write buffer.
[self resumeWriteSource];
*bufferLength = 0;
return errSSLWouldBlock;
}
size_t bytesToWrite = *bufferLength;
size_t bytesWritten = 0;
BOOL done = NO;
BOOL socketError = NO;
int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
ssize_t result = write(socketFD, buffer, bytesToWrite);
if (result < 0)
{
if (errno != EWOULDBLOCK)
{
socketError = YES;
}
flags &= ~kSocketCanAcceptBytes;
}
else if (result == 0)
{
flags &= ~kSocketCanAcceptBytes;
}
else
{
bytesWritten = result;
done = (bytesWritten == bytesToWrite);
}
*bufferLength = bytesWritten;
if (done)
return noErr;
if (socketError)
return errSSLClosedAbort;
return errSSLWouldBlock;
}
static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
{
GCDAsyncSocket *asyncSocket = (GCDAsyncSocket *)connection;
NSCAssert(dispatch_get_current_queue() == asyncSocket->socketQueue, @"What the deuce?");
return [asyncSocket sslReadWithBuffer:data length:dataLength];
}
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
{
GCDAsyncSocket *asyncSocket = (GCDAsyncSocket *)connection;
NSCAssert(dispatch_get_current_queue() == asyncSocket->socketQueue, @"What the deuce?");
return [asyncSocket sslWriteWithBuffer:data length:dataLength];
}
- (void)maybeStartTLS
{
LogTrace();
// We can't start TLS until:
// - All queued reads prior to the user calling startTLS are complete
// - All queued writes prior to the user calling startTLS are complete
//
// We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
LogVerbose(@"Starting TLS...");
OSStatus status;
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
// Create SSLContext, and setup IO callbacks and connection ref
BOOL isServer = [[tlsSettings objectForKey:(NSString *)kCFStreamSSLIsServer] boolValue];
status = SSLNewContext(isServer, &sslContext);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLNewContext"]];
return;
}
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
return;
}
status = SSLSetConnection(sslContext, (SSLConnectionRef)self);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
return;
}
// Configure SSLContext from given settings
//
// Checklist:
// 1. kCFStreamSSLPeerName
// 2. kCFStreamSSLAllowsAnyRoot
// 3. kCFStreamSSLAllowsExpiredRoots
// 4. kCFStreamSSLValidatesCertificateChain
// 5. kCFStreamSSLAllowsExpiredCertificates
// 6. kCFStreamSSLCertificates
// 7. kCFStreamSSLLevel
// 8. GCDAsyncSocketSSLCipherSuites
// 9. GCDAsyncSocketSSLDiffieHellmanParameters
id value;
// 1. kCFStreamSSLPeerName
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLPeerName];
if ([value isKindOfClass:[NSString class]])
{
NSString *peerName = (NSString *)value;
const char *peer = [peerName UTF8String];
size_t peerLen = strlen(peer);
status = SSLSetPeerDomainName(sslContext, peer, peerLen);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
return;
}
}
// 2. kCFStreamSSLAllowsAnyRoot
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot];
if (value)
{
BOOL allowsAnyRoot = [value boolValue];
status = SSLSetAllowsAnyRoot(sslContext, allowsAnyRoot);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsAnyRoot"]];
return;
}
}
// 3. kCFStreamSSLAllowsExpiredRoots
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots];
if (value)
{
BOOL allowsExpiredRoots = [value boolValue];
status = SSLSetAllowsExpiredRoots(sslContext, allowsExpiredRoots);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredRoots"]];
return;
}
}
// 4. kCFStreamSSLValidatesCertificateChain
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain];
if (value)
{
BOOL validatesCertChain = [value boolValue];
status = SSLSetEnableCertVerify(sslContext, validatesCertChain);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
return;
}
}
// 5. kCFStreamSSLAllowsExpiredCertificates
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates];
if (value)
{
BOOL allowsExpiredCerts = [value boolValue];
status = SSLSetAllowsExpiredCerts(sslContext, allowsExpiredCerts);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredCerts"]];
return;
}
}
// 6. kCFStreamSSLCertificates
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLCertificates];
if (value)
{
CFArrayRef certs = (CFArrayRef)value;
status = SSLSetCertificate(sslContext, certs);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetCertificate"]];
return;
}
}
// 7. kCFStreamSSLLevel
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel];
if (value)
{
NSString *sslLevel = (NSString *)value;
if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv2])
{
// kCFStreamSocketSecurityLevelSSLv2:
//
// Specifies that SSL version 2 be set as the security protocol.
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES);
}
else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv3])
{
// kCFStreamSocketSecurityLevelSSLv3:
//
// Specifies that SSL version 3 be set as the security protocol.
// If SSL version 3 is not available, specifies that SSL version 2 be set as the security protocol.
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES);
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol3, YES);
}
else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1])
{
// kCFStreamSocketSecurityLevelTLSv1:
//
// Specifies that TLS version 1 be set as the security protocol.
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
SSLSetProtocolVersionEnabled(sslContext, kTLSProtocol1, YES);
}
else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL])
{
// kCFStreamSocketSecurityLevelNegotiatedSSL:
//
// Specifies that the highest level security protocol that can be negotiated be used.
SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, YES);
}
}
// 8. GCDAsyncSocketSSLCipherSuites
value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites];
if (value)
{
NSArray *cipherSuites = (NSArray *)value;
NSUInteger numberCiphers = [cipherSuites count];
SSLCipherSuite ciphers[numberCiphers];
NSUInteger cipherIndex;
for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++)
{
NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex];
ciphers[cipherIndex] = [cipherObject shortValue];
}
status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]];
return;
}
}
// 9. GCDAsyncSocketSSLDiffieHellmanParameters
value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters];
if (value)
{
NSData *diffieHellmanData = (NSData *)value;
status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]];
return;
}
}
// Setup the sslReadBuffer
//
// If there is any data in the partialReadBuffer,
// this needs to be moved into the sslReadBuffer,
// as this data is now part of the secure read stream.
sslReadBuffer = [[NSMutableData alloc] init];
if ([partialReadBuffer length] > 0)
{
[sslReadBuffer appendData:partialReadBuffer];
[partialReadBuffer setLength:0];
}
// Start the SSL Handshake process
[self continueSSLHandshake];
}
}
- (void)continueSSLHandshake
{
LogTrace();
// If the return value is noErr, the session is ready for normal secure communication.
// If the return value is errSSLWouldBlock, the SSLHandshake function must be called again.
// Otherwise, the return value indicates an error code.
OSStatus status = SSLHandshake(sslContext);
if (status == noErr)
{
LogVerbose(@"SSLHandshake complete");
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
flags |= kSocketSecure;
if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)])
{
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socketDidSecure:self];
[pool drain];
});
}
[self endCurrentRead];
[self endCurrentWrite];
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
else if (status == errSSLWouldBlock)
{
LogVerbose(@"SSLHandshake continues...");
// Handshake continues...
//
// This method will be called again from doReadData or doWriteData.
}
else
{
[self closeWithError:[self sslError:status]];
}
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Security - iOS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#if TARGET_OS_IPHONE
- (void)finishSSLHandshake
{
LogTrace();
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
flags |= kSocketSecure;
if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)])
{
id theDelegate = delegate;
dispatch_async(delegateQueue, ^{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[theDelegate socketDidSecure:self];
[pool drain];
});
}
[self endCurrentRead];
[self endCurrentWrite];
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}
- (void)abortSSLHandshake:(NSError *)error
{
LogTrace();
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
[self closeWithError:error];
}
}
- (void)maybeStartTLS
{
LogTrace();
// We can't start TLS until:
// - All queued reads prior to the user calling startTLS are complete
// - All queued writes prior to the user calling startTLS are complete
//
// We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
LogVerbose(@"Starting TLS...");
if ([partialReadBuffer length] > 0)
{
NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket.";
[self closeWithError:[self otherError:msg]];
return;
}
[self suspendReadSource];
[self suspendWriteSource];
socketFDBytesAvailable = 0;
flags &= ~kSocketCanAcceptBytes;
flags &= ~kSecureSocketHasBytesAvailable;
if (![self createReadAndWriteStream])
{
[self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
return;
}
if (![self registerForStreamCallbacksIncludingReadWrite:YES])
{
[self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
return;
}
if (![self addStreamsToRunLoop])
{
[self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
return;
}
NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS");
NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS");
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
// Getting an error concerning kCFStreamPropertySSLSettings ?
// You need to add the CFNetwork framework to your iOS application.
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, (CFDictionaryRef)tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, (CFDictionaryRef)tlsSettings);
// For some reason, starting around the time of iOS 4.3,
// the first call to set the kCFStreamPropertySSLSettings will return true,
// but the second will return false.
//
// Order doesn't seem to matter.
// So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order.
// Either way, the first call will return true, and the second returns false.
//
// Interestingly, this doesn't seem to affect anything.
// Which is not altogether unusual, as the documentation seems to suggest that (for many settings)
// setting it on one side of the stream automatically sets it for the other side of the stream.
//
// Although there isn't anything in the documentation to suggest that the second attempt would fail.
//
// Furthermore, this only seems to affect streams that are negotiating a security upgrade.
// In other words, the socket gets connected, there is some back-and-forth communication over the unsecure
// connection, and then a startTLS is issued.
// So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS).
if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
{
[self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
return;
}
if (![self openStreams])
{
[self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
return;
}
LogVerbose(@"Waiting for SSL Handshake to complete...");
}
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark CFStream
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#if TARGET_OS_IPHONE
+ (void)startListenerThreadIfNeeded
{
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
listenerThread = [[NSThread alloc] initWithTarget:self
selector:@selector(listenerThread)
object:nil];
[listenerThread start];
});
}
+ (void)listenerThread
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
LogInfo(@"ListenerThread: Started");
// 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 a decades.
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(ignore:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
LogInfo(@"ListenerThread: Stopped");
[pool drain];
}
+ (void)addStreamListener:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread");
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
if (asyncSocket->readStream)
CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
if (asyncSocket->writeStream)
CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}
+ (void)removeStreamListener:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread");
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
if (asyncSocket->readStream)
CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
if (asyncSocket->writeStream)
CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}
static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
{
GCDAsyncSocket *asyncSocket = [(GCDAsyncSocket *)pInfo retain];
switch(type)
{
case kCFStreamEventHasBytesAvailable:
{
dispatch_async(asyncSocket->socketQueue, ^{
LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable");
if (asyncSocket->readStream != stream)
return_from_block;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
// If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie.
// (A callback related to the tcp stream, but not to the SSL layer).
if (CFReadStreamHasBytesAvailable(asyncSocket->readStream))
{
asyncSocket->flags |= kSecureSocketHasBytesAvailable;
[asyncSocket finishSSLHandshake];
}
}
else
{
asyncSocket->flags |= kSecureSocketHasBytesAvailable;
[asyncSocket doReadData];
}
[pool drain];
});
break;
}
default:
{
NSError *error = NSMakeCollectable(CFReadStreamCopyError(stream));
if (error == nil && type == kCFStreamEventEndEncountered)
{
error = [[asyncSocket connectionClosedError] retain];
}
dispatch_async(asyncSocket->socketQueue, ^{
LogCVerbose(@"CFReadStreamCallback - Other");
if (asyncSocket->readStream != stream)
return_from_block;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
[asyncSocket abortSSLHandshake:error];
}
else
{
[asyncSocket closeWithError:error];
}
[pool drain];
});
[error release];
break;
}
}
[asyncSocket release];
}
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
{
GCDAsyncSocket *asyncSocket = [(GCDAsyncSocket *)pInfo retain];
switch(type)
{
case kCFStreamEventCanAcceptBytes:
{
dispatch_async(asyncSocket->socketQueue, ^{
LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes");
if (asyncSocket->writeStream != stream)
return_from_block;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
// If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie.
// (A callback related to the tcp stream, but not to the SSL layer).
if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream))
{
asyncSocket->flags |= kSocketCanAcceptBytes;
[asyncSocket finishSSLHandshake];
}
}
else
{
asyncSocket->flags |= kSocketCanAcceptBytes;
[asyncSocket doWriteData];
}
[pool drain];
});
break;
}
default:
{
NSError *error = NSMakeCollectable(CFWriteStreamCopyError(stream));
if (error == nil && type == kCFStreamEventEndEncountered)
{
error = [[asyncSocket connectionClosedError] retain];
}
dispatch_async(asyncSocket->socketQueue, ^{
LogCVerbose(@"CFWriteStreamCallback - Other");
if (asyncSocket->writeStream != stream)
return_from_block;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
[asyncSocket abortSSLHandshake:error];
}
else
{
[asyncSocket closeWithError:error];
}
[pool drain];
});
[error release];
break;
}
}
[asyncSocket release];
}
- (BOOL)createReadAndWriteStream
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
if (readStream || writeStream)
{
// Streams already created
return YES;
}
int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD;
if (socketFD == SOCKET_NULL)
{
// Cannot create streams without a file descriptor
return NO;
}
if (![self isConnected])
{
// Cannot create streams until file descriptor is connected
return NO;
}
LogVerbose(@"Creating read and write stream...");
CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
// The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case).
// But let's not take any chances.
if (readStream)
CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
if (writeStream)
CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
if ((readStream == NULL) || (writeStream == NULL))
{
LogWarn(@"Unable to create read and write stream...");
if (readStream)
{
CFReadStreamClose(readStream);
CFRelease(readStream);
readStream = NULL;
}
if (writeStream)
{
CFWriteStreamClose(writeStream);
CFRelease(writeStream);
writeStream = NULL;
}
return NO;
}
return YES;
}
- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite
{
LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO"));
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
streamContext.version = 0;
streamContext.info = self;
streamContext.retain = nil;
streamContext.release = nil;
streamContext.copyDescription = nil;
CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
if (includeReadWrite)
readStreamEvents |= kCFStreamEventHasBytesAvailable;
if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext))
{
return NO;
}
CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
if (includeReadWrite)
writeStreamEvents |= kCFStreamEventCanAcceptBytes;
if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext))
{
return NO;
}
return YES;
}
- (BOOL)addStreamsToRunLoop
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
if (!(flags & kAddedStreamListener))
{
LogVerbose(@"Adding streams to runloop...");
[[self class] startListenerThreadIfNeeded];
[[self class] performSelector:@selector(addStreamListener:)
onThread:listenerThread
withObject:self
waitUntilDone:YES];
flags |= kAddedStreamListener;
}
return YES;
}
- (void)removeStreamsFromRunLoop
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
if (flags & kAddedStreamListener)
{
LogVerbose(@"Removing streams from runloop...");
[[self class] performSelector:@selector(removeStreamListener:)
onThread:listenerThread
withObject:self
waitUntilDone:YES];
flags &= ~kAddedStreamListener;
}
}
- (BOOL)openStreams
{
LogTrace();
NSAssert(dispatch_get_current_queue() == socketQueue, @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
CFStreamStatus readStatus = CFReadStreamGetStatus(readStream);
CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream);
if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen))
{
LogVerbose(@"Opening read and write stream...");
BOOL r1 = CFReadStreamOpen(readStream);
BOOL r2 = CFWriteStreamOpen(writeStream);
if (!r1 || !r2)
{
LogError(@"Error in CFStreamOpen");
return NO;
}
}
return YES;
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Advanced
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)performBlock:(dispatch_block_t)block
{
dispatch_sync(socketQueue, block);
}
- (int)socketFD
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return SOCKET_NULL;
}
if (socket4FD != SOCKET_NULL)
return socket4FD;
else
return socket6FD;
}
- (int)socket4FD
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return SOCKET_NULL;
}
return socket4FD;
}
- (int)socket6FD
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return SOCKET_NULL;
}
return socket6FD;
}
#if TARGET_OS_IPHONE
- (CFReadStreamRef)readStream
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NULL;
}
if (readStream == NULL)
[self createReadAndWriteStream];
return readStream;
}
- (CFWriteStreamRef)writeStream
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NULL;
}
if (writeStream == NULL)
[self createReadAndWriteStream];
return writeStream;
}
- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat
{
if (![self createReadAndWriteStream])
{
// Error occured creating streams (perhaps socket isn't open)
return NO;
}
BOOL r1, r2;
LogVerbose(@"Enabling backgrouding on socket");
r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
if (!r1 || !r2)
{
return NO;
}
if (!caveat)
{
if (![self openStreams])
{
return NO;
}
}
return YES;
}
- (BOOL)enableBackgroundingOnSocket
{
LogTrace();
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NO;
}
return [self enableBackgroundingOnSocketWithCaveat:NO];
}
- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.???
{
// This method was created as a workaround for a bug in iOS.
// Apple has since fixed this bug.
// I'm not entirely sure which version of iOS they fixed it in...
LogTrace();
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NO;
}
return [self enableBackgroundingOnSocketWithCaveat:YES];
}
#else
- (SSLContextRef)sslContext
{
if (dispatch_get_current_queue() != socketQueue)
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NULL;
}
return sslContext;
}
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Class Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
{
char addrBuf[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
{
addrBuf[0] = '\0';
}
return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
}
+ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
{
char addrBuf[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
{
addrBuf[0] = '\0';
}
return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
}
+ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
{
return ntohs(pSockaddr4->sin_port);
}
+ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
{
return ntohs(pSockaddr6->sin6_port);
}
+ (NSString *)hostFromAddress:(NSData *)address
{
NSString *host;
if ([self getHost:&host port:NULL fromAddress:address])
return host;
else
return nil;
}
+ (uint16_t)portFromAddress:(NSData *)address
{
uint16_t port;
if ([self getHost:NULL port:&port fromAddress:address])
return port;
else
return 0;
}
+ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address
{
if ([address length] >= sizeof(struct sockaddr))
{
const struct sockaddr *sockaddrX = [address bytes];
if (sockaddrX->sa_family == AF_INET)
{
if ([address length] >= sizeof(struct sockaddr_in))
{
struct sockaddr_in sockaddr4;
memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4));
if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4];
if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4];
return YES;
}
}
else if (sockaddrX->sa_family == AF_INET6)
{
if ([address length] >= sizeof(struct sockaddr_in6))
{
struct sockaddr_in6 sockaddr6;
memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6));
if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6];
if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6];
return YES;
}
}
}
return NO;
}
+ (NSData *)CRLFData
{
return [NSData dataWithBytes:"\x0D\x0A" length:2];
}
+ (NSData *)CRData
{
return [NSData dataWithBytes:"\x0D" length:1];
}
+ (NSData *)LFData
{
return [NSData dataWithBytes:"\x0A" length:1];
}
+ (NSData *)ZeroData
{
return [NSData dataWithBytes:"" length:1];
}
@end