Skip to content

Instantly share code, notes, and snippets.

@paulb777
Last active June 26, 2018 17:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paulb777/6185885 to your computer and use it in GitHub Desktop.
Save paulb777/6185885 to your computer and use it in GitHub Desktop.
Apportable version of GCDAsync.m that works around an issue with spurious EOF's.
//Apportable version of GCDAsync.m that works around an issue with spurious EOF's.
// Also make this change to GCDAsyncSocket.h :
//--- a/GCDAsyncSocket/GCDAsyncSocket.h
//+++ b/GCDAsyncSocket/GCDAsyncSocket.h
//@@ -21,7 +21,7 @@
//- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 // iOS 5.0 supported
//+ #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 50000) && !defined(APPORTABLE) // iOS 5.0 supported
//
// 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 Apple development community.
//
// https://github.com/robbiehanson/CocoaAsyncSocket
//
#import "GCDAsyncSocket.h"
#ifdef APPORTABLE
#define APPORTABLE_BRIDGED_CAST_GLUE __bridge
#else
#define APPORTABLE_BRIDGED_CAST_GLUE
#endif
#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 ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC
#endif
/**
* Does ARC support support GCD objects?
* It does if the minimum deployment target is iOS 6+ or Mac OS X 10.8+
**/
#if TARGET_OS_IPHONE
// Compiling for iOS
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later
#define NEEDS_DISPATCH_RETAIN_RELEASE 0
#else // iOS 5.X or earlier
#define NEEDS_DISPATCH_RETAIN_RELEASE 1
#endif
#else
// Compiling for Mac OS X
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later
#define NEEDS_DISPATCH_RETAIN_RELEASE 0
#else
#define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier
#endif
#endif
#if 0
// Logging Enabled - See log level below
// Logging uses the CocoaLumberjack framework (which is also GCD based).
// https://github.com/robbiehanson/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
#if defined(DEBUG_NETWORK) && defined(APPORTABLE)
#define THIS_METHOD NSStringFromSelector(_cmd)
#define LogError NSLog
#define LogWarn NSLog
#define LogInfo NSLog
#define LogVerbose NSLog
#define LogCError NSLog
#define LogCWarn NSLog
#define LogCInfo NSLog
#define LogCVerbose NSLog
#define LogTrace DEBUG_LOG
#define LogCTrace DEBUG_LOG
#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
#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";
NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket";
NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream";
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites";
#if TARGET_OS_IPHONE
NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin";
NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax";
#else
NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters";
#endif
#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
kAddedStreamsToRunLoop = 1 << 16, // If set, CFStreams have been added to listener thread
kUsingCFStreamForTLS = 1 << 17, // If set, we're forced to use CFStream instead of SecureTransport
kSecureSocketHasBytesAvailable = 1 << 18, // 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 *cfstreamThread; // Used for CFStreams
#endif
@interface GCDAsyncSocket ()
{
uint32_t flags;
uint16_t config;
#if __has_feature(objc_arc_weak)
__weak id delegate;
#else
__unsafe_unretained id delegate;
#endif
dispatch_queue_t delegateQueue;
int socket4FD;
int socket6FD;
int connectIndex;
NSData * connectInterface4;
NSData * connectInterface6;
dispatch_queue_t socketQueue;
dispatch_source_t accept4Source;
dispatch_source_t accept6Source;
dispatch_source_t connectTimer;
dispatch_source_t readSource;
dispatch_source_t writeSource;
dispatch_source_t readTimer;
dispatch_source_t writeTimer;
NSMutableArray *readQueue;
NSMutableArray *writeQueue;
GCDAsyncReadPacket *currentRead;
GCDAsyncWritePacket *currentWrite;
unsigned long socketFDBytesAvailable;
GCDAsyncSocketPreBuffer *preBuffer;
#if TARGET_OS_IPHONE
CFStreamClientContext streamContext;
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
#endif
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
SSLContextRef sslContext;
GCDAsyncSocketPreBuffer *sslPreBuffer;
size_t sslWriteCachedLength;
OSStatus sslErrCode;
#endif
void *IsOnSocketQueueOrTargetQueueKey;
id userData;
}
// 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)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 SECURE_TRANSPORT_MAYBE_AVAILABLE
- (void)ssl_startTLS;
- (void)ssl_continueSSLHandshake;
#endif
#if TARGET_OS_IPHONE
- (void)cf_startTLS;
#endif
// CFStream
#if TARGET_OS_IPHONE
+ (void)startCFStreamThreadIfNeeded;
- (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 -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* A PreBuffer is used when there is more data available on the socket
* than is being requested by current read request.
* In this case we slurp up all data from the socket (to minimize sys calls),
* and store additional yet unread data in a "prebuffer".
*
* The prebuffer is entirely drained before we read from the socket again.
* In other words, a large chunk of data is written is written to the prebuffer.
* The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)).
*
* A ring buffer was once used for this purpose.
* But a ring buffer takes up twice as much memory as needed (double the size for mirroring).
* In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size.
* And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed.
*
* The current design is very simple and straight-forward, while also keeping memory requirements lower.
**/
@interface GCDAsyncSocketPreBuffer : NSObject
{
uint8_t *preBuffer;
size_t preBufferSize;
uint8_t *readPointer;
uint8_t *writePointer;
}
- (id)initWithCapacity:(size_t)numBytes;
- (void)ensureCapacityForWrite:(size_t)numBytes;
- (size_t)availableBytes;
- (uint8_t *)readBuffer;
- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr;
- (size_t)availableSpace;
- (uint8_t *)writeBuffer;
- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr;
- (void)didRead:(size_t)bytesRead;
- (void)didWrite:(size_t)bytesWritten;
- (void)reset;
@end
@implementation GCDAsyncSocketPreBuffer
- (id)initWithCapacity:(size_t)numBytes
{
if ((self = [super init]))
{
preBufferSize = numBytes;
preBuffer = malloc(preBufferSize);
readPointer = preBuffer;
writePointer = preBuffer;
}
return self;
}
- (void)dealloc
{
if (preBuffer)
free(preBuffer);
}
- (void)ensureCapacityForWrite:(size_t)numBytes
{
size_t availableSpace = preBufferSize - (writePointer - readPointer);
if (numBytes > availableSpace)
{
size_t additionalBytes = numBytes - availableSpace;
size_t newPreBufferSize = preBufferSize + additionalBytes;
uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize);
size_t readPointerOffset = readPointer - preBuffer;
size_t writePointerOffset = writePointer - preBuffer;
preBuffer = newPreBuffer;
preBufferSize = newPreBufferSize;
readPointer = preBuffer + readPointerOffset;
writePointer = preBuffer + writePointerOffset;
}
}
- (size_t)availableBytes
{
return writePointer - readPointer;
}
- (uint8_t *)readBuffer
{
return readPointer;
}
- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr
{
if (bufferPtr) *bufferPtr = readPointer;
if (availableBytesPtr) *availableBytesPtr = writePointer - readPointer;
}
- (void)didRead:(size_t)bytesRead
{
readPointer += bytesRead;
if (readPointer == writePointer)
{
// The prebuffer has been drained. Reset pointers.
readPointer = preBuffer;
writePointer = preBuffer;
}
}
- (size_t)availableSpace
{
return preBufferSize - (writePointer - readPointer);
}
- (uint8_t *)writeBuffer
{
return writePointer;
}
- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr
{
if (bufferPtr) *bufferPtr = writePointer;
if (availableSpacePtr) *availableSpacePtr = preBufferSize - (writePointer - readPointer);
}
- (void)didWrite:(size_t)bytesWritten
{
writePointer += bytesWritten;
}
- (void)reset
{
readPointer = preBuffer;
writePointer = preBuffer;
}
@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:(GCDAsyncSocketPreBuffer *)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;
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:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr
{
NSAssert(term != nil, @"This method does not apply to non-term reads");
NSAssert([preBuffer availableBytes] > 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 availableBytes];
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 readBuffer];
NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above.
NSUInteger result = maxPreBufferLength;
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 - [preBuffer readBuffer]; // 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;
}
@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 not copy. For performance as documented in header file.
bytesDone = 0;
timeout = t;
tag = i;
}
return self;
}
@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;
}
@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;
delegateQueue = dq;
#if NEEDS_DISPATCH_RETAIN_RELEASE
if (dq) dispatch_retain(dq);
#endif
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.");
socketQueue = sq;
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_retain(sq);
#endif
}
else
{
socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
}
// The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
// From the documentation:
//
// > Keys are only compared as pointers and are never dereferenced.
// > Thus, you can use a pointer to a static variable for a specific subsystem or
// > any other value that allows you to identify the value uniquely.
//
// We're just going to use the memory address of an ivar.
// Specifically an ivar that is explicitly named for our purpose to make the code more readable.
//
// However, it feels tedious (and less readable) to include the "&" all the time:
// dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
//
// So we're going to make it so it doesn't matter if we use the '&' or not,
// by assigning the value of the ivar to the address of the ivar.
// Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
void *nonNullUnusedPointer = (__bridge void *)self;
dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
readQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentRead = nil;
writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
currentWrite = nil;
preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
}
return self;
}
- (void)dealloc
{
LogInfo(@"%@ - %@ (start)", THIS_METHOD, self);
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
[self closeWithError:nil];
}
else
{
dispatch_sync(socketQueue, ^{
[self closeWithError:nil];
});
}
delegate = nil;
#if NEEDS_DISPATCH_RETAIN_RELEASE
if (delegateQueue) dispatch_release(delegateQueue);
#endif
delegateQueue = NULL;
#if NEEDS_DISPATCH_RETAIN_RELEASE
if (socketQueue) dispatch_release(socketQueue);
#endif
socketQueue = NULL;
LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id)delegate
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey)) {
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_specific(IsOnSocketQueueOrTargetQueueKey))
{
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 NEEDS_DISPATCH_RETAIN_RELEASE
if (delegateQueue) dispatch_release(delegateQueue);
if (newDelegateQueue) dispatch_retain(newDelegateQueue);
#endif
delegateQueue = newDelegateQueue;
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
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_specific(IsOnSocketQueueOrTargetQueueKey))
{
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 NEEDS_DISPATCH_RETAIN_RELEASE
if (delegateQueue) dispatch_release(delegateQueue);
if (newDelegateQueue) dispatch_retain(newDelegateQueue);
#endif
delegateQueue = newDelegateQueue;
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
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)isIPv4Enabled
{
// Note: YES means kIPv4Disabled is OFF
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_async(socketQueue, block);
}
- (BOOL)isIPv6Enabled
{
// Note: YES means kIPv6Disabled is OFF
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_async(socketQueue, block);
}
- (BOOL)isIPv4PreferredOverIPv6
{
// Note: YES means kPreferIPv6 is OFF
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_async(socketQueue, block);
}
- (id)userData
{
__block id result = nil;
dispatch_block_t block = ^{
result = userData;
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (void)setUserData:(id)arbitraryUserData
{
dispatch_block_t block = ^{
if (userData != arbitraryUserData)
{
userData = arbitraryUserData;
}
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
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];
__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];
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];
LogVerbose(@"close(socketFD)");
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];
LogVerbose(@"close(socketFD)");
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];
LogVerbose(@"close(socketFD)");
close(socketFD);
return SOCKET_NULL;
}
// Listen
status = listen(socketFD, 1024);
if (status == -1)
{
NSString *reason = @"Error in listen() function";
err = [self errnoErrorWithReason:reason];
LogVerbose(@"close(socketFD)");
close(socketFD);
return SOCKET_NULL;
}
return socketFD;
};
// Create dispatch block and run on socketQueue
dispatch_block_t block = ^{ @autoreleasepool {
if (delegate == nil) // Must have delegate set
{
NSString *msg = @"Attempting to accept without a delegate. Set a delegate first.";
err = [self badConfigError:msg];
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];
return_from_block;
}
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
#ifdef APPORTABLE
BOOL isIPv6Disabled = YES;
#else
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
#endif
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];
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];
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];
return_from_block;
}
if (isIPv4Disabled && (interface6 == nil))
{
NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
err = [self badParamError:msg];
return_from_block;
}
if (isIPv6Disabled && (interface4 == nil))
{
NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
err = [self badParamError:msg];
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)
{
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)
{
LogVerbose(@"close(socket4FD)");
close(socket4FD);
}
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, ^{ @autoreleasepool {
LogVerbose(@"event4Block");
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
while ([self doAccept:socketFD] && (++i < numPendingConnections));
}});
dispatch_source_set_cancel_handler(accept4Source, ^{
#if NEEDS_DISPATCH_RETAIN_RELEASE
LogVerbose(@"dispatch_release(accept4Source)");
dispatch_release(acceptSource);
#endif
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, ^{ @autoreleasepool {
LogVerbose(@"event6Block");
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
while ([self doAccept:socketFD] && (++i < numPendingConnections));
}});
dispatch_source_set_cancel_handler(accept6Source, ^{
#if NEEDS_DISPATCH_RETAIN_RELEASE
LogVerbose(@"dispatch_release(accept6Source)");
dispatch_release(acceptSource);
#endif
LogVerbose(@"close(socket6FD)");
close(socketFD);
});
LogVerbose(@"dispatch_resume(accept6Source)");
dispatch_resume(accept6Source);
}
flags |= kSocketStarted;
result = YES;
}};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
LogInfo(@"Error in accept: %@", err);
if (errPtr)
*errPtr = err;
}
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)
{
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
// 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:theDelegate
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, ^{ @autoreleasepool {
[acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
}});
// 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 NEEDS_DISPATCH_RETAIN_RELEASE
if (childSocketQueue) dispatch_release(childSocketQueue);
#endif
// The accepted socket should have been retained by the delegate.
// Otherwise it gets properly released when exiting the block.
}});
}
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_specific(IsOnSocketQueueOrTargetQueueKey), @"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;
#ifdef APPORTABLE
BOOL isIPv6Disabled = YES;
#else
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
#endif
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;
connectInterface6 = interface6;
}
// 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];
NSString *interface = [inInterface copy];
__block BOOL result = NO;
__block NSError *err = nil;
dispatch_block_t block = ^{ @autoreleasepool {
// 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];
return_from_block;
}
// Run through standard pre-connect checks
if (![self preConnectWithInterface:interface error:&err])
{
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];
dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
[self lookup:aConnectIndex host:hostCpy port:port];
}});
[self startConnectTimeout:timeout];
result = YES;
}};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
if (errPtr)
*errPtr = err;
}
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];
NSString *interface = [inInterface copy];
__block BOOL result = NO;
__block NSError *err = nil;
dispatch_block_t block = ^{ @autoreleasepool {
// 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];
return_from_block;
}
BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
#ifdef APPORTABLE
BOOL isIPv6Disabled = YES;
#else
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
#endif
if (isIPv4Disabled && (address4 != nil))
{
NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed.";
err = [self badParamError:msg];
return_from_block;
}
if (isIPv6Disabled && (address6 != nil))
{
NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed.";
err = [self badParamError:msg];
return_from_block;
}
// Run through standard pre-connect checks
if (![self preConnectWithInterface:interface error:&err])
{
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])
{
return_from_block;
}
flags |= kSocketStarted;
[self startConnectTimeout:timeout];
result = YES;
}};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
if (result == NO)
{
if (errPtr)
*errPtr = err;
}
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;
#ifndef ANDROID
nativeAddr.sin_len = sizeof(struct sockaddr_in);
#endif
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;
#ifndef ANDROID
nativeAddr6.sin6_len = sizeof(struct sockaddr_in6);
#endif
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, ^{ @autoreleasepool {
[self lookup:aConnectIndex didFail:error];
}});
}
else
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
[self lookup:aConnectIndex didSucceedWithAddress4:address4 address6:address6];
}});
}
}
- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"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;
#ifdef APPORTABLE
BOOL isIPv6Disabled = YES;
#else
BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
#endif
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_specific(IsOnSocketQueueOrTargetQueueKey), @"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_specific(IsOnSocketQueueOrTargetQueueKey), @"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;
}
}
// Prevent SIGPIPE signals
int nosigpipe = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
// 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, ^{ @autoreleasepool {
[self didConnect:aConnectIndex];
}});
}
else
{
NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];
dispatch_async(socketQueue, ^{ @autoreleasepool {
[self didNotConnect:aConnectIndex error:error];
}});
}
});
LogVerbose(@"Connecting...");
return YES;
}
- (void)didConnect:(int)aConnectIndex
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"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;
}
#ifndef APPORTABLE
if (![self openStreams])
{
[self closeWithError:[self otherError:@"Error creating CFStreams"]];
return;
}
#endif
#endif
};
// Notify delegate
NSString *host = [self connectedHost];
uint16_t port = [self connectedPort];
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didConnectToHost:port:)])
{
SetupStreamsPart1();
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didConnectToHost:host port:port];
dispatch_async(socketQueue, ^{ @autoreleasepool {
SetupStreamsPart2();
}});
}});
}
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;
}
// 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_specific(IsOnSocketQueueOrTargetQueueKey), @"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 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, ^{ @autoreleasepool {
[self doConnectTimeout];
}});
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_source_t theConnectTimer = connectTimer;
dispatch_source_set_cancel_handler(connectTimer, ^{
LogVerbose(@"dispatch_release(connectTimer)");
dispatch_release(theConnectTimer);
});
#endif
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 = nil;
}
if (connectInterface6)
{
connectInterface6 = nil;
}
}
- (void)doConnectTimeout
{
LogTrace();
[self endConnectTimeout];
[self closeWithError:[self connectTimeoutError]];
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Disconnecting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)closeWithError:(NSError *)error
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
[self endConnectTimeout];
if (currentRead != nil) [self endCurrentRead];
if (currentWrite != nil) [self endCurrentWrite];
[readQueue removeAllObjects];
[writeQueue removeAllObjects];
[preBuffer reset];
#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;
}
}
}
#endif
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
{
[sslPreBuffer reset];
sslErrCode = noErr;
if (sslContext)
{
// Getting a linker error here about the SSLx() functions?
// You need to add the Security Framework to your application.
SSLClose(sslContext);
#if TARGET_OS_IPHONE
CFRelease(sslContext);
#else
SSLDisposeContext(sslContext);
#endif
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.
#ifdef APPORTABLE
// dispatch_source_cancel doesn't work to close the sockets
if (YES)
#else
if (!accept4Source && !accept6Source && !readSource && !writeSource)
#endif
{
LogVerbose(@"manually closing close");
if (socket4FD != SOCKET_NULL)
{
LogVerbose(@"close(socket4FD) %d", socket4FD);
close(socket4FD);
socket4FD = SOCKET_NULL;
}
if (socket6FD != SOCKET_NULL)
{
LogVerbose(@"close(socket6FD)");
close(socket6FD);
socket6FD = SOCKET_NULL;
}
}
#ifndef APPORTABLE
else
#endif
{
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)
{
LogVerbose(@"dispatch_source_cancel(readSource)");
dispatch_source_cancel(readSource);
#ifndef APPORTABLE
[self resumeReadSource];
#endif
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:)])
{
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socketDidDisconnect:self withError:error];
}});
}
}
}
- (void)disconnect
{
dispatch_block_t block = ^{ @autoreleasepool {
if (flags & kSocketStarted)
{
[self closeWithError:nil];
}
}};
// Synchronous disconnection, as documented in the header file
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
}
- (void)disconnectAfterReading
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterReads);
[self maybeClose];
}
}});
}
- (void)disconnectAfterWriting
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterWrites);
[self maybeClose];
}
}});
}
- (void)disconnectAfterReadingAndWriting
{
dispatch_async(socketQueue, ^{ @autoreleasepool {
if (flags & kSocketStarted)
{
flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites);
[self maybeClose];
}
}});
}
/**
* 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_specific(IsOnSocketQueueOrTargetQueueKey), @"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_specific(IsOnSocketQueueOrTargetQueueKey))
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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (NSString *)connectedHost
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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, ^{ @autoreleasepool {
if (socket4FD != SOCKET_NULL)
result = [self connectedHostFromSocket4:socket4FD];
else if (socket6FD != SOCKET_NULL)
result = [self connectedHostFromSocket6:socket6FD];
}});
return result;
}
}
- (uint16_t)connectedPort
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
{
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, ^{ @autoreleasepool {
if (socket4FD != SOCKET_NULL)
result = [self localHostFromSocket4:socket4FD];
else if (socket6FD != SOCKET_NULL)
result = [self localHostFromSocket6:socket6FD];
}});
return result;
}
}
- (uint16_t)localPort
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (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_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
- (BOOL)isIPv4
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
return (socket4FD != SOCKET_NULL);
}
else
{
__block BOOL result = NO;
dispatch_sync(socketQueue, ^{
result = (socket4FD != SOCKET_NULL);
});
return result;
}
}
- (BOOL)isIPv6
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
return (socket6FD != SOCKET_NULL);
}
else
{
__block BOOL result = NO;
dispatch_sync(socketQueue, ^{
result = (socket6FD != SOCKET_NULL);
});
return result;
}
}
- (BOOL)isSecure
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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));
#ifndef ANDROID
sockaddr4.sin_len = sizeof(sockaddr4);
#endif
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));
#ifndef ANDROID
sockaddr6.sin6_len = sizeof(sockaddr6);
#endif
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));
#ifndef ANDROID
sockaddr4.sin_len = sizeof(sockaddr4);
#endif
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));
#ifndef ANDROID
sockaddr6.sin6_len = sizeof(sockaddr6);
#endif
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, ^{ @autoreleasepool {
LogVerbose(@"readEventBlock");
socketFDBytesAvailable = dispatch_source_get_data(readSource);
LogVerbose(@"socketFDBytesAvailable: %lu", socketFDBytesAvailable);
#ifdef APPORTABLE
// We can get sporadic EOF's. Make sure we see 10 zero reads in a row before
// causing the connection to be terminated
static int zeroCount = 0;
if (socketFDBytesAvailable > 0)
{
[self doReadData];
zeroCount = 0;
}
else
{
if (++zeroCount == 10)
{
[self doReadEOF];
zeroCount = 0;
}
}
#else
if (socketFDBytesAvailable > 0)
[self doReadData];
else
[self doReadEOF];
#endif
}});
dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
LogVerbose(@"writeEventBlock");
flags |= kSocketCanAcceptBytes;
[self doWriteData];
}});
// Setup cancel handlers
__block int socketFDRefCount = 2;
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_source_t theReadSource = readSource;
dispatch_source_t theWriteSource = writeSource;
#endif
dispatch_source_set_cancel_handler(readSource, ^{
LogVerbose(@"readCancelBlock");
#if NEEDS_DISPATCH_RETAIN_RELEASE
LogVerbose(@"dispatch_release(readSource)");
dispatch_release(theReadSource);
#endif
if (--socketFDRefCount == 0)
{
LogVerbose(@"close(socketFD)");
close(socketFD);
}
});
dispatch_source_set_cancel_handler(writeSource, ^{
LogVerbose(@"writeCancelBlock");
#if NEEDS_DISPATCH_RETAIN_RELEASE
LogVerbose(@"dispatch_release(writeSource)");
dispatch_release(theWriteSource);
#endif
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)usingCFStreamForTLS
{
#if TARGET_OS_IPHONE
{
if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
{
// 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;
}
- (BOOL)usingSecureTransportForTLS
{
#if TARGET_OS_IPHONE
{
return ![self usingCFStreamForTLS];
}
#endif
return YES;
}
- (void)suspendReadSource
{
#ifdef APPORTABLE
return;
#endif
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, ^{ @autoreleasepool {
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
}});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
}
- (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, ^{ @autoreleasepool {
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
}});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
}
- (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, ^{ @autoreleasepool {
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[self maybeDequeueRead];
}
}});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
}
- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr
{
__block float result = 0.0F;
dispatch_block_t block = ^{
if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]])
{
// We're not reading anything right now.
if (tagPtr != NULL) *tagPtr = 0;
if (donePtr != NULL) *donePtr = 0;
if (totalPtr != NULL) *totalPtr = 0;
result = NAN;
}
else
{
// It's only possible to know the progress of our read if we're reading to a certain length.
// If we're reading to data, we of course have no idea when the data will arrive.
// If we're reading to timeout, then we have no idea when the next chunk of data will arrive.
NSUInteger done = currentRead->bytesDone;
NSUInteger total = currentRead->readLength;
if (tagPtr != NULL) *tagPtr = currentRead->tag;
if (donePtr != NULL) *donePtr = done;
if (totalPtr != NULL) *totalPtr = total;
if (total > 0)
result = (float)done / (float)total;
else
result = 1.0F;
}
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
/**
* 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_specific(IsOnSocketQueueOrTargetQueueKey), @"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];
[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 ([preBuffer availableBytes] == 0)
{
if ([self usingCFStreamForTLS]) {
// Callbacks never disabled
}
else {
[self resumeReadSource];
}
}
}
}
}
- (void)flushSSLBuffers
{
LogTrace();
NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket");
if ([preBuffer availableBytes] > 0)
{
// Only flush the ssl buffers if the prebuffer is empty.
// This is to avoid growing the prebuffer inifinitely large.
return;
}
#if TARGET_OS_IPHONE
if ([self usingCFStreamForTLS])
{
if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
{
LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);
CFIndex defaultBytesToRead = (1024 * 4);
[preBuffer ensureCapacityForWrite:defaultBytesToRead];
uint8_t *buffer = [preBuffer writeBuffer];
CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result);
if (result > 0)
{
[preBuffer didWrite:result];
}
flags &= ~kSecureSocketHasBytesAvailable;
}
return;
}
#endif
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
__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
// [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket
// sslInternalBufSize <- Number of 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 sslPreBuffer.
// However, we do know this is an upper bound on the estimation.
estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes];
size_t sslInternalBufSize = 0;
SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
estimatedBytesAvailable += sslInternalBufSize;
};
updateEstimatedBytesAvailable();
if (estimatedBytesAvailable > 0)
{
LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);
BOOL done = NO;
do
{
LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable);
// Make sure there's enough room in the prebuffer
[preBuffer ensureCapacityForWrite:estimatedBytesAvailable];
// Read data into prebuffer
uint8_t *buffer = [preBuffer writeBuffer];
size_t bytesRead = 0;
OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead);
if (bytesRead > 0)
{
[preBuffer didWrite:bytesRead];
}
LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]);
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 usingCFStreamForTLS])
{
// 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;
#ifdef APPORTABLE
estimatedBytesAvailable = MAX(1, socketFDBytesAvailable);
hasBytesAvailable = YES;
#else
if ([self usingCFStreamForTLS])
{
#if TARGET_OS_IPHONE
// Relegated to using CFStream... :( Boo! Give us a full SecureTransport stack Apple!
estimatedBytesAvailable = 0;
if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
hasBytesAvailable = YES;
else
hasBytesAvailable = NO;
#endif
}
else
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
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 sslPreBuffer.
estimatedBytesAvailable += [sslPreBuffer availableBytes];
// 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
}
#endif
if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0))
{
LogVerbose(@"No data available to read...");
// No data available to read.
if (![self usingCFStreamForTLS])
{
// 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 ([self usingSecureTransportForTLS])
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
// We are in the process of a SSL Handshake.
// We were waiting for incoming data which has just arrived.
[self ssl_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 usingCFStreamForTLS])
{
// 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
//
if ([preBuffer availableBytes] > 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:preBuffer found:&done];
}
else
{
// Read type #1 or #2
bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
}
// 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, [preBuffer readBuffer], bytesToCopy);
// Remove the copied bytes from the preBuffer
[preBuffer didRead:bytesToCopy];
LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]);
// 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
#ifdef APPORTABLE
socketEOF = NO; // Reads will permanently block if it gets set to YES
#endif
if (!done && !error && !socketEOF && !waiting && hasBytesAvailable)
{
NSAssert(([preBuffer availableBytes] == 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 readIntoPreBuffer = NO;
NSUInteger bytesToRead;
if ([self usingCFStreamForTLS])
{
// Since Apple hasn't made the full power of 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:&readIntoPreBuffer];
}
else
{
if (currentRead->term != nil)
{
// Read type #3 - read up to a terminator
bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable
shouldPreBuffer:&readIntoPreBuffer];
}
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 preBuffer.
uint8_t *buffer;
if (readIntoPreBuffer)
{
[preBuffer ensureCapacityForWrite:bytesToRead];
buffer = [preBuffer writeBuffer];
}
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 ([self usingCFStreamForTLS])
{
#if TARGET_OS_IPHONE
CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead);
LogVerbose(@"CFReadStreamRead(): result = %i", (int)result);
if (result < 0)
{
error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream);
}
else if (result == 0)
{
socketEOF = YES;
}
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;
#endif
}
else
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
// 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
{
if (result == errSSLClosedGraceful || result == errSSLClosedAbort)
{
// We've reached the end of the stream.
// Handle this the same way we would an EOF from the socket.
socketEOF = YES;
sslErrCode = result;
}
else
{
error = [self sslError:result];
}
}
// It's possible that bytesRead > 0, even if the result was 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;
}
}
// 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 socketFD is %i", (int)result, (int)socketFD);
if (result < 0)
{
LogVerbose(@"errno is %d", errno);
if (errno == EWOULDBLOCK)
waiting = YES;
else
error = [self errnoErrorWithReason:@"Error in read() function"];
socketFDBytesAvailable = 0;
}
else if (result == 0)
{
socketEOF = YES;
socketFDBytesAvailable = 0;
}
else
{
bytesRead = result;
if (bytesRead < bytesToRead)
{
// The read returned less data than requested.
// This means socketFDBytesAvailable was a bit off due to timing,
// because we read from the socket right when the readSource event was firing.
socketFDBytesAvailable = 0;
}
else
{
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(readIntoPreBuffer == 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 (readIntoPreBuffer)
{
// We just read a big chunk of data into the preBuffer
[preBuffer didWrite:bytesRead];
LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]);
// Search for the terminating sequence
bytesToRead = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToRead);
// Ensure there's room on the read packet's buffer
[currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
// Copy bytes from prebuffer into read buffer
uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ currentRead->bytesDone;
memcpy(readBuf, [preBuffer readBuffer], bytesToRead);
// Remove the copied bytes from the prebuffer
[preBuffer didRead:bytesToRead];
LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]);
// 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 preBuffer
LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow);
[preBuffer ensureCapacityForWrite:overflow];
uint8_t *overflowBuffer = buffer + underflow;
memcpy([preBuffer writeBuffer], overflowBuffer, overflow);
[preBuffer didWrite:overflow];
LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]);
// 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 (readIntoPreBuffer)
{
// We just read a chunk of data into the preBuffer
[preBuffer didWrite:bytesRead];
// Now 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.
//
// Ensure there's room on the read packet's buffer
[currentRead ensureCapacityForAdditionalDataOfLength:bytesRead];
// Copy bytes from prebuffer into read buffer
uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ currentRead->bytesDone;
memcpy(readBuf, [preBuffer readBuffer], bytesRead);
// Remove the copied bytes from the prebuffer
[preBuffer didRead:bytesRead];
// 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 || [preBuffer availableBytes] > 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:)])
{
__strong id theDelegate = delegate;
long theReadTag = currentRead->tag;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag];
}});
}
}
// Check for errors
if (error)
{
[self closeWithError:error];
}
else if (socketEOF)
{
[self doReadEOF];
}
else if (waiting)
{
if (![self usingCFStreamForTLS])
{
// 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 preBuffer,
// 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 preBuffer 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 ([self usingSecureTransportForTLS])
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
error = [self sslError:errSSLClosedAbort];
#endif
}
}
else if (flags & kReadStreamClosed)
{
// The preBuffer 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 ([preBuffer availableBytes] > 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:)])
{
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socketDidCloseReadStream:self];
}});
}
}
else
{
shouldDisconnect = YES;
}
}
else
{
shouldDisconnect = YES;
}
if (shouldDisconnect)
{
if (error == nil)
{
if ([self usingSecureTransportForTLS])
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful)
{
error = [self sslError:sslErrCode];
}
else
{
error = [self connectionClosedError];
}
#endif
}
else
{
error = [self connectionClosedError];
}
}
[self closeWithError:error];
}
else
{
if (![self usingCFStreamForTLS])
{
// 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:)])
{
__strong id theDelegate = delegate;
GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didReadData:result withTag:theRead->tag];
}});
}
[self endCurrentRead];
}
- (void)endCurrentRead
{
if (readTimer)
{
dispatch_source_cancel(readTimer);
readTimer = NULL;
}
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, ^{ @autoreleasepool {
[self doReadTimeout];
}});
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_source_t theReadTimer = readTimer;
dispatch_source_set_cancel_handler(readTimer, ^{
LogVerbose(@"dispatch_release(readTimer)");
dispatch_release(theReadTimer);
});
#endif
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:)])
{
__strong id theDelegate = delegate;
GCDAsyncReadPacket *theRead = currentRead;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
NSTimeInterval timeoutExtension = 0.0;
timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag
elapsed:theRead->timeout
bytesDone:theRead->bytesDone];
dispatch_async(socketQueue, ^{ @autoreleasepool {
[self doReadTimeoutWithExtension:timeoutExtension];
}});
}});
}
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, ^{ @autoreleasepool {
LogTrace();
if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
{
[writeQueue addObject:packet];
[self maybeDequeueWrite];
}
}});
// Do not rely on the block being run in order to release the packet,
// as the queue might get released without the block completing.
}
- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr
{
__block float result = 0.0F;
dispatch_block_t block = ^{
if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]])
{
// We're not writing anything right now.
if (tagPtr != NULL) *tagPtr = 0;
if (donePtr != NULL) *donePtr = 0;
if (totalPtr != NULL) *totalPtr = 0;
result = NAN;
}
else
{
NSUInteger done = currentWrite->bytesDone;
NSUInteger total = [currentWrite->buffer length];
if (tagPtr != NULL) *tagPtr = currentWrite->tag;
if (donePtr != NULL) *donePtr = done;
if (totalPtr != NULL) *totalPtr = total;
result = (float)done / (float)total;
}
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
return result;
}
/**
* 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_specific(IsOnSocketQueueOrTargetQueueKey), @"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];
[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 usingCFStreamForTLS])
{
// 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 usingCFStreamForTLS])
{
// 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 ([self usingSecureTransportForTLS])
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
// 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 ssl_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 usingCFStreamForTLS])
{
// Suspend the write source or else it will continue to fire nonstop.
[self suspendWriteSource];
}
}
return;
}
// Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet)
BOOL waiting = NO;
NSError *error = nil;
size_t bytesWritten = 0;
if (flags & kSocketSecure)
{
if ([self usingCFStreamForTLS])
{
#if TARGET_OS_IPHONE
//
// Writing data using CFStream (over internal TLS)
//
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) = %i", (unsigned long)bytesToWrite, result);
if (result < 0)
{
error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream);
}
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;
}
#endif
}
else
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
// 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 processed to dataLength !!
//
// 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. So, for example, if we tell it to
// write 256 bytes then it might actually write 128 bytes, but then report 0 bytes 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->buffer length] == (currentWrite->bytesDone + bytesWritten))
{
// 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
{
//
// Writing data directly over raw socket
//
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 socketFD is %i", result, socketFD);
// 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 usingCFStreamForTLS])
{
[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", (unsigned long)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 usingCFStreamForTLS])
{
[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:)])
{
__strong id theDelegate = delegate;
long theWriteTag = currentWrite->tag;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag];
}});
}
}
}
// 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:)])
{
__strong id theDelegate = delegate;
long theWriteTag = currentWrite->tag;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socket:self didWriteDataWithTag:theWriteTag];
}});
}
[self endCurrentWrite];
}
- (void)endCurrentWrite
{
if (writeTimer)
{
dispatch_source_cancel(writeTimer);
writeTimer = NULL;
}
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, ^{ @autoreleasepool {
[self doWriteTimeout];
}});
#if NEEDS_DISPATCH_RETAIN_RELEASE
dispatch_source_t theWriteTimer = writeTimer;
dispatch_source_set_cancel_handler(writeTimer, ^{
LogVerbose(@"dispatch_release(writeTimer)");
dispatch_release(theWriteTimer);
});
#endif
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:)])
{
__strong id theDelegate = delegate;
GCDAsyncWritePacket *theWrite = currentWrite;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
NSTimeInterval timeoutExtension = 0.0;
timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag
elapsed:theWrite->timeout
bytesDone:theWrite->bytesDone];
dispatch_async(socketQueue, ^{ @autoreleasepool {
[self doWriteTimeoutWithExtension:timeoutExtension];
}});
}});
}
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, ^{ @autoreleasepool {
if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
{
[readQueue addObject:packet];
[writeQueue addObject:packet];
flags |= kQueuedTLS;
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}});
}
- (void)maybeStartTLS
{
// 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))
{
BOOL canUseSecureTransport = YES;
#if TARGET_OS_IPHONE
{
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
NSNumber *value;
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsAnyRoot];
if (value && [value boolValue] == YES)
canUseSecureTransport = NO;
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsExpiredRoots];
if (value && [value boolValue] == YES)
canUseSecureTransport = NO;
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLValidatesCertificateChain];
if (value && [value boolValue] == NO)
canUseSecureTransport = NO;
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsExpiredCertificates];
if (value && [value boolValue] == YES)
canUseSecureTransport = NO;
}
#endif
if (IS_SECURE_TRANSPORT_AVAILABLE && canUseSecureTransport)
{
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
[self ssl_startTLS];
#endif
}
else
{
#if TARGET_OS_IPHONE
[self cf_startTLS];
#endif
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Security via SecureTransport
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength
{
LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength);
if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 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
//
size_t sslPreBufferLength = [sslPreBuffer availableBytes];
if (sslPreBufferLength > 0)
{
LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD);
size_t bytesToCopy;
if (sslPreBufferLength > totalBytesLeftToBeRead)
bytesToCopy = totalBytesLeftToBeRead;
else
bytesToCopy = sslPreBufferLength;
LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy);
memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy);
[sslPreBuffer didRead:bytesToCopy];
LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]);
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 sslPreBuffer.
// Then copy requested amount into dataBuffer.
LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD);
[sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable];
readIntoPreBuffer = YES;
bytesToRead = (size_t)socketFDBytesAvailable;
buf = [sslPreBuffer writeBuffer];
}
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;
}
else if (result == 0)
{
LogVerbose(@"%@: read EOF", THIS_METHOD);
socketError = YES;
socketFDBytesAvailable = 0;
}
else
{
size_t bytesReadFromSocket = result;
if (socketFDBytesAvailable > bytesReadFromSocket)
socketFDBytesAvailable -= bytesReadFromSocket;
else
socketFDBytesAvailable = 0;
if (readIntoPreBuffer)
{
[sslPreBuffer didWrite:bytesReadFromSocket];
size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket);
LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy);
memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy);
[sslPreBuffer didRead:bytesToCopy];
totalBytesRead += bytesToCopy;
totalBytesLeftToBeRead -= bytesToCopy;
LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]);
}
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 = (__bridge GCDAsyncSocket *)connection;
NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
return [asyncSocket sslReadWithBuffer:data length:dataLength];
}
static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
{
GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
return [asyncSocket sslWriteWithBuffer:data length:dataLength];
}
- (void)ssl_startTLS
{
LogTrace();
LogVerbose(@"Starting TLS (via SecureTransport)...");
OSStatus status;
GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
NSDictionary *tlsSettings = tlsPacket->tlsSettings;
// Create SSLContext, and setup IO callbacks and connection ref
BOOL isServer = [[tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLIsServer] boolValue];
#if TARGET_OS_IPHONE
{
if (isServer)
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
else
sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
if (sslContext == NULL)
{
[self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
return;
}
}
#else
{
status = SSLNewContext(isServer, &sslContext);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLNewContext"]];
return;
}
}
#endif
status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
return;
}
status = SSLSetConnection(sslContext, (__bridge 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 (GCDAsyncSocketSSLProtocolVersionMin / GCDAsyncSocketSSLProtocolVersionMax)
// 8. GCDAsyncSocketSSLCipherSuites
// 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
id value;
// 1. kCFStreamSSLPeerName
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE 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:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsAnyRoot];
if (value)
{
#if TARGET_OS_IPHONE
NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsAnyRoot");
#else
BOOL allowsAnyRoot = [value boolValue];
status = SSLSetAllowsAnyRoot(sslContext, allowsAnyRoot);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsAnyRoot"]];
return;
}
#endif
}
// 3. kCFStreamSSLAllowsExpiredRoots
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsExpiredRoots];
if (value)
{
#if TARGET_OS_IPHONE
NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredRoots");
#else
BOOL allowsExpiredRoots = [value boolValue];
status = SSLSetAllowsExpiredRoots(sslContext, allowsExpiredRoots);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredRoots"]];
return;
}
#endif
}
// 4. kCFStreamSSLValidatesCertificateChain
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLValidatesCertificateChain];
if (value)
{
#if TARGET_OS_IPHONE
NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLValidatesCertificateChain");
#else
BOOL validatesCertChain = [value boolValue];
status = SSLSetEnableCertVerify(sslContext, validatesCertChain);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
return;
}
#endif
}
// 5. kCFStreamSSLAllowsExpiredCertificates
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLAllowsExpiredCertificates];
if (value)
{
#if TARGET_OS_IPHONE
NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredCertificates");
#else
BOOL allowsExpiredCerts = [value boolValue];
status = SSLSetAllowsExpiredCerts(sslContext, allowsExpiredCerts);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredCerts"]];
return;
}
#endif
}
// 6. kCFStreamSSLCertificates
value = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLCertificates];
if (value)
{
CFArrayRef certs = (__bridge CFArrayRef)value;
status = SSLSetCertificate(sslContext, certs);
if (status != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetCertificate"]];
return;
}
}
// 7. kCFStreamSSLLevel
#if TARGET_OS_IPHONE
{
NSString *sslLevel = [tlsSettings objectForKey:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSSLLevel];
NSString *sslMinLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin];
NSString *sslMaxLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax];
if (sslLevel)
{
if (sslMinLevel || sslMaxLevel)
{
LogWarn(@"kCFStreamSSLLevel security option ignored. Overriden by "
@"GCDAsyncSocketSSLProtocolVersionMin and/or GCDAsyncSocketSSLProtocolVersionMax");
}
else
{
if ([sslLevel isEqualToString:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSocketSecurityLevelSSLv3])
{
sslMinLevel = sslMaxLevel = @"kSSLProtocol3";
}
else if ([sslLevel isEqualToString:(APPORTABLE_BRIDGED_CAST_GLUE NSString *)kCFStreamSocketSecurityLevelTLSv1])
{
sslMinLevel = sslMaxLevel = @"kTLSProtocol1";
}
else
{
LogWarn(@"Unable to match kCFStreamSSLLevel security option to valid SSL protocol min/max");
}
}
}
if (sslMinLevel || sslMaxLevel)
{
OSStatus status1 = noErr;
OSStatus status2 = noErr;
SSLProtocol (^sslProtocolForString)(NSString*) = ^SSLProtocol (NSString *protocolStr) {
if ([protocolStr isEqualToString:@"kSSLProtocol3"]) return kSSLProtocol3;
if ([protocolStr isEqualToString:@"kTLSProtocol1"]) return kTLSProtocol1;
if ([protocolStr isEqualToString:@"kTLSProtocol11"]) return kTLSProtocol11;
if ([protocolStr isEqualToString:@"kTLSProtocol12"]) return kTLSProtocol12;
return kSSLProtocolUnknown;
};
SSLProtocol minProtocol = sslProtocolForString(sslMinLevel);
SSLProtocol maxProtocol = sslProtocolForString(sslMaxLevel);
if (minProtocol != kSSLProtocolUnknown)
{
status1 = SSLSetProtocolVersionMin(sslContext, minProtocol);
}
if (maxProtocol != kSSLProtocolUnknown)
{
status2 = SSLSetProtocolVersionMax(sslContext, maxProtocol);
}
if (status1 != noErr || status2 != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMinMax"]];
return;
}
}
}
#else
{
value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel];
if (value)
{
NSString *sslLevel = (NSString *)value;
OSStatus status1 = noErr;
OSStatus status2 = noErr;
OSStatus status3 = noErr;
if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv2])
{
// kCFStreamSocketSecurityLevelSSLv2:
//
// Specifies that SSL version 2 be set as the security protocol.
status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
status2 = 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.
status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
status2 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES);
status3 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol3, YES);
}
else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1])
{
// kCFStreamSocketSecurityLevelTLSv1:
//
// Specifies that TLS version 1 be set as the security protocol.
status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
status2 = SSLSetProtocolVersionEnabled(sslContext, kTLSProtocol1, YES);
}
else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL])
{
// kCFStreamSocketSecurityLevelNegotiatedSSL:
//
// Specifies that the highest level security protocol that can be negotiated be used.
status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, YES);
}
if (status1 != noErr || status2 != noErr || status3 != noErr)
{
[self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionEnabled"]];
return;
}
}
}
#endif
// 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
#if !TARGET_OS_IPHONE
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;
}
}
#endif
// Setup the sslPreBuffer
//
// Any data in the preBuffer needs to be moved into the sslPreBuffer,
// as this data is now part of the secure read stream.
sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
size_t preBufferLength = [preBuffer availableBytes];
if (preBufferLength > 0)
{
[sslPreBuffer ensureCapacityForWrite:preBufferLength];
memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
[preBuffer didRead:preBufferLength];
[sslPreBuffer didWrite:preBufferLength];
}
sslErrCode = noErr;
// Start the SSL Handshake process
[self ssl_continueSSLHandshake];
}
- (void)ssl_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:)])
{
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socketDidSecure:self];
}});
}
[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 via CFStream
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#if TARGET_OS_IPHONE
- (void)cf_finishSSLHandshake
{
LogTrace();
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
flags |= kSocketSecure;
if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)])
{
__strong id theDelegate = delegate;
dispatch_async(delegateQueue, ^{ @autoreleasepool {
[theDelegate socketDidSecure:self];
}});
}
[self endCurrentRead];
[self endCurrentWrite];
[self maybeDequeueRead];
[self maybeDequeueWrite];
}
}
- (void)cf_abortSSLHandshake:(NSError *)error
{
LogTrace();
if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
{
flags &= ~kStartingReadTLS;
flags &= ~kStartingWriteTLS;
[self closeWithError:error];
}
}
- (void)cf_startTLS
{
LogTrace();
LogVerbose(@"Starting TLS (via CFStream)...");
if ([preBuffer availableBytes] > 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;
flags |= kUsingCFStreamForTLS;
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;
CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;
// Getting an error concerning kCFStreamPropertySSLSettings ?
// You need to add the CFNetwork framework to your iOS application.
BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, 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)startCFStreamThreadIfNeeded
{
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
cfstreamThread = [[NSThread alloc] initWithTarget:self
selector:@selector(cfstreamThread)
object:nil];
[cfstreamThread start];
});
}
+ (void)cfstreamThread { @autoreleasepool
{
[[NSThread currentThread] setName:GCDAsyncSocketThreadName];
LogInfo(@"CFStreamThread: 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 decades.
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(doNothingAtAll:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
LogInfo(@"CFStreamThread: Stopped");
}}
+ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
if (asyncSocket->readStream)
CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
if (asyncSocket->writeStream)
CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}
+ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == cfstreamThread, @"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 = (__bridge GCDAsyncSocket *)pInfo;
switch(type)
{
case kCFStreamEventHasBytesAvailable:
{
dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable");
if (asyncSocket->readStream != stream)
return_from_block;
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 cf_finishSSLHandshake];
}
}
else
{
asyncSocket->flags |= kSecureSocketHasBytesAvailable;
[asyncSocket doReadData];
}
}});
break;
}
default:
{
NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream);
if (error == nil && type == kCFStreamEventEndEncountered)
{
error = [asyncSocket connectionClosedError];
}
dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
LogCVerbose(@"CFReadStreamCallback - Other");
if (asyncSocket->readStream != stream)
return_from_block;
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
[asyncSocket cf_abortSSLHandshake:error];
}
else
{
[asyncSocket closeWithError:error];
}
}});
break;
}
}
}
static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
{
GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo;
switch(type)
{
case kCFStreamEventCanAcceptBytes:
{
dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes");
if (asyncSocket->writeStream != stream)
return_from_block;
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 cf_finishSSLHandshake];
}
}
else
{
asyncSocket->flags |= kSocketCanAcceptBytes;
[asyncSocket doWriteData];
}
}});
break;
}
default:
{
NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream);
if (error == nil && type == kCFStreamEventEndEncountered)
{
error = [asyncSocket connectionClosedError];
}
dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
LogCVerbose(@"CFWriteStreamCallback - Other");
if (asyncSocket->writeStream != stream)
return_from_block;
if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
{
[asyncSocket cf_abortSSLHandshake:error];
}
else
{
[asyncSocket closeWithError:error];
}
}});
break;
}
}
}
- (BOOL)createReadAndWriteStream
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"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_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
streamContext.version = 0;
streamContext.info = (__bridge void *)(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_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
if (!(flags & kAddedStreamsToRunLoop))
{
LogVerbose(@"Adding streams to runloop...");
[[self class] startCFStreamThreadIfNeeded];
[[self class] performSelector:@selector(scheduleCFStreams:)
onThread:cfstreamThread
withObject:self
waitUntilDone:YES];
flags |= kAddedStreamsToRunLoop;
}
return YES;
}
- (void)removeStreamsFromRunLoop
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
if (flags & kAddedStreamsToRunLoop)
{
LogVerbose(@"Removing streams from runloop...");
[[self class] performSelector:@selector(unscheduleCFStreams:)
onThread:cfstreamThread
withObject:self
waitUntilDone:YES];
flags &= ~kAddedStreamsToRunLoop;
}
}
- (BOOL)openStreams
{
LogTrace();
NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"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
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* See header file for big discussion of this method.
**/
- (BOOL)autoDisconnectOnClosedReadStream
{
// Note: YES means kAllowHalfDuplexConnection is OFF
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
return ((config & kAllowHalfDuplexConnection) == 0);
}
else
{
__block BOOL result;
dispatch_sync(socketQueue, ^{
result = ((config & kAllowHalfDuplexConnection) == 0);
});
return result;
}
}
/**
* See header file for big discussion of this method.
**/
- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag
{
// Note: YES means kAllowHalfDuplexConnection is OFF
dispatch_block_t block = ^{
if (flag)
config &= ~kAllowHalfDuplexConnection;
else
config |= kAllowHalfDuplexConnection;
};
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_async(socketQueue, block);
}
/**
* See header file for big discussion of this method.
**/
- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue
{
void *nonNullUnusedPointer = (__bridge void *)self;
dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
}
/**
* See header file for big discussion of this method.
**/
- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue
{
dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL);
}
/**
* See header file for big discussion of this method.
**/
- (void)performBlock:(dispatch_block_t)block
{
if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
block();
else
dispatch_sync(socketQueue, block);
}
/**
* Questions? Have you read the header file?
**/
- (int)socketFD
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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;
}
/**
* Questions? Have you read the header file?
**/
- (int)socket4FD
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return SOCKET_NULL;
}
return socket4FD;
}
/**
* Questions? Have you read the header file?
**/
- (int)socket6FD
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return SOCKET_NULL;
}
return socket6FD;
}
#if TARGET_OS_IPHONE
/**
* Questions? Have you read the header file?
**/
- (CFReadStreamRef)readStream
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NULL;
}
if (readStream == NULL)
[self createReadAndWriteStream];
return readStream;
}
/**
* Questions? Have you read the header file?
**/
- (CFWriteStreamRef)writeStream
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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;
}
/**
* Questions? Have you read the header file?
**/
- (BOOL)enableBackgroundingOnSocket
{
LogTrace();
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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_specific(IsOnSocketQueueOrTargetQueueKey))
{
LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
return NO;
}
return [self enableBackgroundingOnSocketWithCaveat:YES];
}
#endif
#if SECURE_TRANSPORT_MAYBE_AVAILABLE
- (SSLContextRef)sslContext
{
if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
{
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment