// // AsyncSocket.m // // This class is in the public domain. // Originally created by Dustin Voss on Wed Jan 29 2003. // Updated and maintained by Deusty Designs and the Mac development community. // // http://code.google.com/p/cocoaasyncsocket/ // #import "AsyncSocket.h" #import #import #import #import #if TARGET_OS_IPHONE // Note: You may need to add the CFNetwork Framework to your project #import #endif #pragma mark Declarations #define DEFAULT_PREBUFFERING YES // Whether pre-buffering is enabled by default #define READQUEUE_CAPACITY 5 // Initial capacity #define WRITEQUEUE_CAPACITY 5 // Initial capacity #define READALL_CHUNKSIZE 256 // Incremental increase in buffer size #define WRITE_CHUNKSIZE (1024 * 4) // Limit on size of each write pass NSString *const AsyncSocketException = @"AsyncSocketException"; NSString *const AsyncSocketErrorDomain = @"AsyncSocketErrorDomain"; // Mutex lock used by all instances of AsyncSocket, to protect getaddrinfo. // The man page says it is not thread-safe. (As of Mac OS X 10.4.7, and possibly earlier) static NSString *getaddrinfoLock = @"lock"; enum AsyncSocketFlags { kEnablePreBuffering = 1 << 0, // If set, pre-buffering is enabled kDidPassConnectMethod = 1 << 1, // If set, disconnection results in delegate call kDidCompleteOpenForRead = 1 << 2, // If set, open callback has been called for read stream kDidCompleteOpenForWrite = 1 << 3, // If set, open callback has been called for write stream kStartingReadTLS = 1 << 4, // If set, we're waiting for TLS negotiation to complete kStartingWriteTLS = 1 << 5, // If set, we're waiting for TLS negotiation to complete kForbidReadsWrites = 1 << 6, // If set, no new reads or writes are allowed kDisconnectAfterReads = 1 << 7, // If set, disconnect after no more reads are queued kDisconnectAfterWrites = 1 << 8, // If set, disconnect after no more writes are queued kClosingWithError = 1 << 9, // If set, the socket is being closed due to an error kDequeueReadScheduled = 1 << 10, // If set, a maybeDequeueRead operation is already scheduled kDequeueWriteScheduled = 1 << 11, // If set, a maybeDequeueWrite operation is already scheduled }; @interface AsyncSocket (Private) // Connecting - (void)startConnectTimeout:(NSTimeInterval)timeout; - (void)endConnectTimeout; // Socket Implementation - (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr; - (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr; - (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; - (BOOL)configureSocketAndReturnError:(NSError **)errPtr; - (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; - (void)doAcceptWithSocket:(CFSocketNativeHandle)newSocket; - (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)err; // Stream Implementation - (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr; - (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; - (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; - (BOOL)configureStreamsAndReturnError:(NSError **)errPtr; - (BOOL)openStreamsAndReturnError:(NSError **)errPtr; - (void)doStreamOpen; - (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr; // Disconnect Implementation - (void)closeWithError:(NSError *)err; - (void)recoverUnreadData; - (void)emptyQueues; - (void)close; // Errors - (NSError *)getErrnoError; - (NSError *)getAbortError; - (NSError *)getStreamError; - (NSError *)getSocketError; - (NSError *)getConnectTimeoutError; - (NSError *)getReadMaxedOutError; - (NSError *)getReadTimeoutError; - (NSError *)getWriteTimeoutError; - (NSError *)errorFromCFStreamError:(CFStreamError)err; // Diagnostics - (BOOL)isSocketConnected; - (BOOL)areStreamsConnected; - (NSString *)connectedHost:(CFSocketRef)socket; - (UInt16)connectedPort:(CFSocketRef)socket; - (NSString *)localHost:(CFSocketRef)socket; - (UInt16)localPort:(CFSocketRef)socket; - (NSString *)addressHost:(CFDataRef)cfaddr; - (UInt16)addressPort:(CFDataRef)cfaddr; // Reading - (void)doBytesAvailable; - (void)completeCurrentRead; - (void)endCurrentRead; - (void)scheduleDequeueRead; - (void)maybeDequeueRead; - (void)doReadTimeout:(NSTimer *)timer; // Writing - (void)doSendBytes; - (void)completeCurrentWrite; - (void)endCurrentWrite; - (void)scheduleDequeueWrite; - (void)maybeDequeueWrite; - (void)maybeScheduleDisconnect; - (void)doWriteTimeout:(NSTimer *)timer; // Run Loop - (void)runLoopAddSource:(CFRunLoopSourceRef)source; - (void)runLoopRemoveSource:(CFRunLoopSourceRef)source; - (void)runLoopAddTimer:(NSTimer *)timer; - (void)runLoopRemoveTimer:(NSTimer *)timer; - (void)runLoopUnscheduleReadStream; - (void)runLoopUnscheduleWriteStream; // Security - (void)maybeStartTLS; - (void)onTLSStarted:(BOOL)flag; // Callbacks - (void)doCFCallback:(CFSocketCallBackType)type forSocket:(CFSocketRef)sock withAddress:(NSData *)address withData:(const void *)pData; - (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream; - (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream; @end static void MyCFSocketCallback(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *); static void MyCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo); static void MyCFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The AsyncReadPacket 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 AsyncReadPacket : NSObject { @public NSMutableData *buffer; CFIndex bytesDone; NSTimeInterval timeout; CFIndex maxLength; long tag; NSData *term; BOOL readAllAvailableData; } - (id)initWithData:(NSMutableData *)d timeout:(NSTimeInterval)t tag:(long)i readAllAvailable:(BOOL)a terminator:(NSData *)e maxLength:(CFIndex)m; - (unsigned)readLengthForTerm; - (unsigned)prebufferReadLengthForTerm; - (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes; @end @implementation AsyncReadPacket - (id)initWithData:(NSMutableData *)d timeout:(NSTimeInterval)t tag:(long)i readAllAvailable:(BOOL)a terminator:(NSData *)e maxLength:(CFIndex)m { if((self = [super init])) { buffer = [d retain]; timeout = t; tag = i; readAllAvailableData = a; term = [e copy]; bytesDone = 0; maxLength = m; } return self; } /** * For read packets with a set terminator, returns the safe length of data that can be read * without going over a terminator, or the maxLength. * * It is assumed the terminator has not already been read. **/ - (unsigned)readLengthForTerm { NSAssert(term != nil, @"Searching for term in data when there is no term."); // What we're going to do is look for a partial sequence of the terminator at the end of the buffer. // If a partial sequence occurs, then we must assume the next bytes to arrive will be the rest of the term, // and we can only read that amount. // Otherwise, we're safe to read the entire length of the term. unsigned result = [term length]; // Shortcut when term is a single byte if(result == 1) return result; // i = index within buffer at which to check data // j = length of term to check against // Note: Beware of implicit casting rules // This could give you -1: MAX(0, (0 - [term length] + 1)); CFIndex i = MAX(0, (CFIndex)(bytesDone - [term length] + 1)); CFIndex j = MIN([term length] - 1, bytesDone); while(i < bytesDone) { const void *subBuffer = [buffer bytes] + i; if(memcmp(subBuffer, [term bytes], j) == 0) { result = [term length] - j; break; } i++; j--; } if(maxLength > 0) return MIN(result, (maxLength - bytesDone)); else return result; } /** * Assuming pre-buffering is enabled, returns the amount of data that can be read * without going over the maxLength. **/ - (unsigned)prebufferReadLengthForTerm { if(maxLength > 0) return MIN(READALL_CHUNKSIZE, (maxLength - bytesDone)); else return READALL_CHUNKSIZE; } /** * 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. **/ - (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes { NSAssert(term != nil, @"Searching for term in data when there is no term."); // We try to start the search such that the first new byte read matches up with the last byte of the term. // We continue searching forward after this until the term no longer fits into the buffer. // Note: Beware of implicit casting rules // This could give you -1: MAX(0, 1 - 1 - [term length] + 1); CFIndex i = MAX(0, (CFIndex)(bytesDone - numBytes - [term length] + 1)); while(i + [term length] <= bytesDone) { const void *subBuffer = [buffer bytes] + i; if(memcmp(subBuffer, [term bytes], [term length]) == 0) { return bytesDone - (i + [term length]); } i++; } return -1; } - (void)dealloc { [buffer release]; [term release]; [super dealloc]; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The AsyncWritePacket encompasses the instructions for any given write. **/ @interface AsyncWritePacket : NSObject { @public NSData *buffer; CFIndex bytesDone; long tag; NSTimeInterval timeout; } - (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; @end @implementation AsyncWritePacket - (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { if((self = [super init])) { buffer = [d retain]; timeout = t; tag = i; bytesDone = 0; } return self; } - (void)dealloc { [buffer release]; [super dealloc]; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The AsyncSpecialPacket 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 AsyncSpecialPacket : NSObject { @public NSDictionary *tlsSettings; } - (id)initWithTLSSettings:(NSDictionary *)settings; @end @implementation AsyncSpecialPacket - (id)initWithTLSSettings:(NSDictionary *)settings { if((self = [super init])) { tlsSettings = [settings copy]; } return self; } - (void)dealloc { [tlsSettings release]; [super dealloc]; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation AsyncSocket - (id)init { return [self initWithDelegate:nil userData:0]; } - (id)initWithDelegate:(id)delegate { return [self initWithDelegate:delegate userData:0]; } // Designated initializer. - (id)initWithDelegate:(id)delegate userData:(long)userData { if((self = [super init])) { theFlags = DEFAULT_PREBUFFERING ? kEnablePreBuffering : 0; theDelegate = delegate; theUserData = userData; theSocket4 = NULL; theSource4 = NULL; theSocket6 = NULL; theSource6 = NULL; theRunLoop = NULL; theReadStream = NULL; theWriteStream = NULL; theConnectTimer = nil; theReadQueue = [[NSMutableArray alloc] initWithCapacity:READQUEUE_CAPACITY]; theCurrentRead = nil; theReadTimer = nil; partialReadBuffer = [[NSMutableData alloc] initWithCapacity:READALL_CHUNKSIZE]; theWriteQueue = [[NSMutableArray alloc] initWithCapacity:WRITEQUEUE_CAPACITY]; theCurrentWrite = nil; theWriteTimer = nil; // Socket context NSAssert(sizeof(CFSocketContext) == sizeof(CFStreamClientContext), @"CFSocketContext != CFStreamClientContext"); theContext.version = 0; theContext.info = self; theContext.retain = nil; theContext.release = nil; theContext.copyDescription = nil; // Default run loop modes theRunLoopModes = [[NSArray arrayWithObject:NSDefaultRunLoopMode] retain]; } return self; } // The socket may been initialized in a connected state and auto-released, so this should close it down cleanly. - (void)dealloc { [self close]; [theReadQueue release]; [theWriteQueue release]; [theRunLoopModes release]; [partialReadBuffer release]; [NSObject cancelPreviousPerformRequestsWithTarget:self]; [super dealloc]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Accessors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (long)userData { return theUserData; } - (void)setUserData:(long)userData { theUserData = userData; } - (id)delegate { return theDelegate; } - (void)setDelegate:(id)delegate { theDelegate = delegate; } - (BOOL)canSafelySetDelegate { return ([theReadQueue count] == 0 && [theWriteQueue count] == 0 && theCurrentRead == nil && theCurrentWrite == nil); } - (CFSocketRef)getCFSocket { if(theSocket4) return theSocket4; else return theSocket6; } - (CFReadStreamRef)getCFReadStream { return theReadStream; } - (CFWriteStreamRef)getCFWriteStream { return theWriteStream; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Progress //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (float)progressOfReadReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total { // Check to make sure we're actually reading something right now if (!theCurrentRead) return NAN; // 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. BOOL hasTotal = (theCurrentRead->readAllAvailableData == NO && theCurrentRead->term == nil); CFIndex d = theCurrentRead->bytesDone; CFIndex t = hasTotal ? [theCurrentRead->buffer length] : 0; if (tag != NULL) *tag = theCurrentRead->tag; if (done != NULL) *done = d; if (total != NULL) *total = t; float ratio = (float)d/(float)t; return isnan(ratio) ? 1.0F : ratio; // 0 of 0 bytes is 100% done. } - (float)progressOfWriteReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total { if (!theCurrentWrite) return NAN; CFIndex d = theCurrentWrite->bytesDone; CFIndex t = [theCurrentWrite->buffer length]; if (tag != NULL) *tag = theCurrentWrite->tag; if (done != NULL) *done = d; if (total != NULL) *total = t; return (float)d/(float)t; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Run Loop //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)runLoopAddSource:(CFRunLoopSourceRef)source { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFRunLoopAddSource(theRunLoop, source, runLoopMode); } } - (void)runLoopRemoveSource:(CFRunLoopSourceRef)source { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFRunLoopRemoveSource(theRunLoop, source, runLoopMode); } } - (void)runLoopAddTimer:(NSTimer *)timer { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFRunLoopAddTimer(theRunLoop, (CFRunLoopTimerRef)timer, runLoopMode); } } - (void)runLoopRemoveTimer:(NSTimer *)timer { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFRunLoopRemoveTimer(theRunLoop, (CFRunLoopTimerRef)timer, runLoopMode); } } - (void)runLoopUnscheduleReadStream { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFReadStreamUnscheduleFromRunLoop(theReadStream, theRunLoop, runLoopMode); } CFReadStreamSetClient(theReadStream, kCFStreamEventNone, NULL, NULL); } - (void)runLoopUnscheduleWriteStream { unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFWriteStreamUnscheduleFromRunLoop(theWriteStream, theRunLoop, runLoopMode); } CFWriteStreamSetClient(theWriteStream, kCFStreamEventNone, NULL, NULL); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * See the header file for a full explanation of pre-buffering. **/ - (void)enablePreBuffering { theFlags |= kEnablePreBuffering; } /** * See the header file for a full explanation of this method. **/ - (BOOL)moveToRunLoop:(NSRunLoop *)runLoop { NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), @"moveToRunLoop must be called from within the current RunLoop!"); if(runLoop == nil) { return NO; } if(theRunLoop == [runLoop getCFRunLoop]) { return YES; } [NSObject cancelPreviousPerformRequestsWithTarget:self]; theFlags &= ~kDequeueReadScheduled; theFlags &= ~kDequeueWriteScheduled; if(theReadStream && theWriteStream) { [self runLoopUnscheduleReadStream]; [self runLoopUnscheduleWriteStream]; } if(theSource4) [self runLoopRemoveSource:theSource4]; if(theSource6) [self runLoopRemoveSource:theSource6]; // We do not retain the timers - they get retained by the runloop when we add them as a source. // Since we're about to remove them as a source, we retain now, and release again below. [theReadTimer retain]; [theWriteTimer retain]; if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; theRunLoop = [runLoop getCFRunLoop]; if(theReadTimer) [self runLoopAddTimer:theReadTimer]; if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; // Release timers since we retained them above [theReadTimer release]; [theWriteTimer release]; if(theSource4) [self runLoopAddSource:theSource4]; if(theSource6) [self runLoopAddSource:theSource6]; if(theReadStream && theWriteStream) { if(![self attachStreamsToRunLoop:runLoop error:nil]) { return NO; } } [runLoop performSelector:@selector(maybeDequeueRead) target:self argument:nil order:0 modes:theRunLoopModes]; [runLoop performSelector:@selector(maybeDequeueWrite) target:self argument:nil order:0 modes:theRunLoopModes]; [runLoop performSelector:@selector(maybeScheduleDisconnect) target:self argument:nil order:0 modes:theRunLoopModes]; return YES; } /** * See the header file for a full explanation of this method. **/ - (BOOL)setRunLoopModes:(NSArray *)runLoopModes { NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), @"setRunLoopModes must be called from within the current RunLoop!"); if([runLoopModes count] == 0) { return NO; } if([theRunLoopModes isEqualToArray:runLoopModes]) { return YES; } [NSObject cancelPreviousPerformRequestsWithTarget:self]; theFlags &= ~kDequeueReadScheduled; theFlags &= ~kDequeueWriteScheduled; if(theReadStream && theWriteStream) { [self runLoopUnscheduleReadStream]; [self runLoopUnscheduleWriteStream]; } if(theSource4) [self runLoopRemoveSource:theSource4]; if(theSource6) [self runLoopRemoveSource:theSource6]; // We do not retain the timers - they get retained by the runloop when we add them as a source. // Since we're about to remove them as a source, we retain now, and release again below. [theReadTimer retain]; [theWriteTimer retain]; if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; [theRunLoopModes release]; theRunLoopModes = [runLoopModes copy]; if(theReadTimer) [self runLoopAddTimer:theReadTimer]; if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; // Release timers since we retained them above [theReadTimer release]; [theWriteTimer release]; if(theSource4) [self runLoopAddSource:theSource4]; if(theSource6) [self runLoopAddSource:theSource6]; if(theReadStream && theWriteStream) { // Note: theRunLoop variable is a CFRunLoop, and NSRunLoop is NOT toll-free bridged with CFRunLoop. // So we cannot pass theRunLoop to the method below, which is expecting a NSRunLoop parameter. // Instead we pass nil, which will result in the method properly using the current run loop. if(![self attachStreamsToRunLoop:nil error:nil]) { return NO; } } [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; [self performSelector:@selector(maybeScheduleDisconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; return YES; } - (NSArray *)runLoopModes { return [[theRunLoopModes retain] autorelease]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Accepting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr { return [self acceptOnAddress:nil port:port error:errPtr]; } /** * To accept on a certain address, pass the address to accept on. * To accept on any address, pass nil or an empty string. * To accept only connections from localhost pass "localhost" or "loopback". **/ - (BOOL)acceptOnAddress:(NSString *)hostaddr port:(UInt16)port error:(NSError **)errPtr { if (theDelegate == NULL) [NSException raise:AsyncSocketException format:@"Attempting to accept without a delegate. Set a delegate first."]; if (theSocket4 != NULL || theSocket6 != NULL) [NSException raise:AsyncSocketException format:@"Attempting to accept while connected or accepting connections. Disconnect first."]; // Set up the listen sockaddr structs if needed. NSData *address4 = nil, *address6 = nil; if(hostaddr == nil || ([hostaddr length] == 0)) { // Accept on ANY address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_ANY); memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_any; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures for CFSocketSetAddress. address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else if([hostaddr isEqualToString:@"localhost"] || [hostaddr isEqualToString:@"loopback"]) { // Accept only on LOOPBACK address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures for CFSocketSetAddress. address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; @synchronized (getaddrinfoLock) { 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; hints.ai_flags = AI_PASSIVE; int error = getaddrinfo([hostaddr UTF8String], [portStr UTF8String], &hints, &res0); if(error) { if(errPtr) { NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; } } for(res = res0; res; res = res->ai_next) { if(!address4 && (res->ai_family == AF_INET)) { // Found IPv4 address // Wrap the native address structures for CFSocketSetAddress. address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; } else if(!address6 && (res->ai_family == AF_INET6)) { // Found IPv6 address // Wrap the native address structures for CFSocketSetAddress. address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; } } freeaddrinfo(res0); } if(!address4 && !address6) return NO; } // Create the sockets. if (address4) { theSocket4 = [self newAcceptSocketForAddress:address4 error:errPtr]; if (theSocket4 == NULL) goto Failed; } if (address6) { theSocket6 = [self newAcceptSocketForAddress:address6 error:errPtr]; // Note: The iPhone doesn't currently support IPv6 #if !TARGET_OS_IPHONE if (theSocket6 == NULL) goto Failed; #endif } // Attach the sockets to the run loop so that callback methods work [self attachSocketsToRunLoop:nil error:nil]; // Set the SO_REUSEADDR flags. int reuseOn = 1; if (theSocket4) setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (theSocket6) setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); // Set the local bindings which causes the sockets to start listening. CFSocketError err; if (theSocket4) { err = CFSocketSetAddress (theSocket4, (CFDataRef)address4); if (err != kCFSocketSuccess) goto Failed; //NSLog(@"theSocket4: %hu", [self localPort:theSocket4]); } if(port == 0 && theSocket4 && theSocket6) { // The user has passed in port 0, which means he wants to allow the kernel to choose the port for them // However, the kernel will choose a different port for both theSocket4 and theSocket6 // So we grab the port the kernel choose for theSocket4, and set it as the port for theSocket6 UInt16 chosenPort = [self localPort:theSocket4]; struct sockaddr_in6 *pSockAddr6 = (struct sockaddr_in6 *)[address6 bytes]; pSockAddr6->sin6_port = htons(chosenPort); } if (theSocket6) { err = CFSocketSetAddress (theSocket6, (CFDataRef)address6); if (err != kCFSocketSuccess) goto Failed; //NSLog(@"theSocket6: %hu", [self localPort:theSocket6]); } theFlags |= kDidPassConnectMethod; return YES; Failed: if(errPtr) *errPtr = [self getSocketError]; if(theSocket4 != NULL) { CFSocketInvalidate(theSocket4); CFRelease(theSocket4); theSocket4 = NULL; } if(theSocket6 != NULL) { CFSocketInvalidate(theSocket6); CFRelease(theSocket6); theSocket6 = NULL; } return NO; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Connecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)connectToHost:(NSString*)hostname onPort:(UInt16)port error:(NSError **)errPtr { return [self connectToHost:hostname onPort:port withTimeout:-1 error:errPtr]; } /** * This method creates an initial CFReadStream and CFWriteStream to the given host on the given port. * The connection is then opened, and the corresponding CFSocket will be extracted after the connection succeeds. * * Thus the delegate will have access to the CFReadStream and CFWriteStream prior to connection, * specifically in the onSocketWillConnect: method. **/ - (BOOL)connectToHost:(NSString *)hostname onPort:(UInt16)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { if(theDelegate == NULL) { NSString *message = @"Attempting to connect without a delegate. Set a delegate first."; [NSException raise:AsyncSocketException format:@"%@", message]; } if(theSocket4 != NULL || theSocket6 != NULL) { NSString *message = @"Attempting to connect while connected or accepting connections. Disconnect first."; [NSException raise:AsyncSocketException format:@"%@", message]; } if(![self createStreamsToHost:hostname onPort:port error:errPtr]) goto Failed; if(![self attachStreamsToRunLoop:nil error:errPtr]) goto Failed; if(![self configureStreamsAndReturnError:errPtr]) goto Failed; if(![self openStreamsAndReturnError:errPtr]) goto Failed; [self startConnectTimeout:timeout]; theFlags |= kDidPassConnectMethod; return YES; Failed: [self close]; return NO; } - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { return [self connectToAddress:remoteAddr withTimeout:-1 error:errPtr]; } /** * This method creates an initial CFSocket to the given address. * The connection is then opened, and the corresponding CFReadStream and CFWriteStream will be * created from the low-level sockets after the connection succeeds. * * Thus the delegate will have access to the CFSocket and CFSocketNativeHandle (BSD socket) prior to connection, * specifically in the onSocketWillConnect: method. * * Note: The NSData parameter is expected to be a sockaddr structure. For example, an NSData object returned from * NSNetservice addresses method. * If you have an existing struct sockaddr you can convert it to an NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; **/ - (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { if (theDelegate == NULL) { NSString *message = @"Attempting to connect without a delegate. Set a delegate first."; [NSException raise:AsyncSocketException format:@"%@", message]; } if (theSocket4 != NULL || theSocket6 != NULL) { NSString *message = @"Attempting to connect while connected or accepting connections. Disconnect first."; [NSException raise:AsyncSocketException format:@"%@", message]; } if(![self createSocketForAddress:remoteAddr error:errPtr]) goto Failed; if(![self attachSocketsToRunLoop:nil error:errPtr]) goto Failed; if(![self configureSocketAndReturnError:errPtr]) goto Failed; if(![self connectSocketToAddress:remoteAddr error:errPtr]) goto Failed; [self startConnectTimeout:timeout]; theFlags |= kDidPassConnectMethod; return YES; Failed: [self close]; return NO; } - (void)startConnectTimeout:(NSTimeInterval)timeout { if(timeout >= 0.0) { theConnectTimer = [NSTimer timerWithTimeInterval:timeout target:self selector:@selector(doConnectTimeout:) userInfo:nil repeats:NO]; [self runLoopAddTimer:theConnectTimer]; } } - (void)endConnectTimeout { [theConnectTimer invalidate]; theConnectTimer = nil; } - (void)doConnectTimeout:(NSTimer *)timer { [self endConnectTimeout]; [self closeWithError:[self getConnectTimeoutError]]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Socket Implementation //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Creates the accept sockets. * Returns true if either IPv4 or IPv6 is created. * If either is missing, an error is returned (even though the method may return true). **/ - (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr { struct sockaddr *pSockAddr = (struct sockaddr *)[addr bytes]; int addressFamily = pSockAddr->sa_family; CFSocketRef theSocket = CFSocketCreate(kCFAllocatorDefault, addressFamily, SOCK_STREAM, 0, kCFSocketAcceptCallBack, // Callback flags (CFSocketCallBack)&MyCFSocketCallback, // Callback method &theContext); if(theSocket == NULL) { if(errPtr) *errPtr = [self getSocketError]; } return theSocket; } - (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr { struct sockaddr *pSockAddr = (struct sockaddr *)[remoteAddr bytes]; if(pSockAddr->sa_family == AF_INET) { theSocket4 = CFSocketCreate(NULL, // Default allocator PF_INET, // Protocol Family SOCK_STREAM, // Socket Type IPPROTO_TCP, // Protocol kCFSocketConnectCallBack, // Callback flags (CFSocketCallBack)&MyCFSocketCallback, // Callback method &theContext); // Socket Context if(theSocket4 == NULL) { if (errPtr) *errPtr = [self getSocketError]; return NO; } } else if(pSockAddr->sa_family == AF_INET6) { theSocket6 = CFSocketCreate(NULL, // Default allocator PF_INET6, // Protocol Family SOCK_STREAM, // Socket Type IPPROTO_TCP, // Protocol kCFSocketConnectCallBack, // Callback flags (CFSocketCallBack)&MyCFSocketCallback, // Callback method &theContext); // Socket Context if(theSocket6 == NULL) { if (errPtr) *errPtr = [self getSocketError]; return NO; } } else { if (errPtr) *errPtr = [self getSocketError]; return NO; } return YES; } /** * Adds the CFSocket's to the run-loop so that callbacks will work properly. **/ - (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr { // Get the CFRunLoop to which the socket should be attached. theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; if(theSocket4) { theSource4 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket4, 0); [self runLoopAddSource:theSource4]; } if(theSocket6) { theSource6 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket6, 0); [self runLoopAddSource:theSource6]; } return YES; } /** * Allows the delegate method to configure the CFSocket or CFNativeSocket as desired before we connect. * Note that the CFReadStream and CFWriteStream will not be available until after the connection is opened. **/ - (BOOL)configureSocketAndReturnError:(NSError **)errPtr { // Call the delegate method for further configuration. if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) { if([theDelegate onSocketWillConnect:self] == NO) { if (errPtr) *errPtr = [self getAbortError]; return NO; } } return YES; } - (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { // Start connecting to the given address in the background // The MyCFSocketCallback method will be called when the connection succeeds or fails if(theSocket4) { CFSocketError err = CFSocketConnectToAddress(theSocket4, (CFDataRef)remoteAddr, -1); if(err != kCFSocketSuccess) { if (errPtr) *errPtr = [self getSocketError]; return NO; } } else if(theSocket6) { CFSocketError err = CFSocketConnectToAddress(theSocket6, (CFDataRef)remoteAddr, -1); if(err != kCFSocketSuccess) { if (errPtr) *errPtr = [self getSocketError]; return NO; } } return YES; } /** * Attempt to make the new socket. * If an error occurs, ignore this event. **/ - (void)doAcceptWithSocket:(CFSocketNativeHandle)newNative { // New socket inherits same delegate and run loop modes. // Note: We use [self class] to support subclassing AsyncSocket. AsyncSocket *newSocket = [[[[self class] alloc] initWithDelegate:theDelegate] autorelease]; [newSocket setRunLoopModes:theRunLoopModes]; if(newSocket) { if ([theDelegate respondsToSelector:@selector(onSocket:didAcceptNewSocket:)]) [theDelegate onSocket:self didAcceptNewSocket:newSocket]; NSRunLoop *runLoop = nil; if ([theDelegate respondsToSelector:@selector(onSocket:wantsRunLoopForNewSocket:)]) runLoop = [theDelegate onSocket:self wantsRunLoopForNewSocket:newSocket]; BOOL pass = YES; if(pass && ![newSocket createStreamsFromNative:newNative error:nil]) pass = NO; if(pass && ![newSocket attachStreamsToRunLoop:runLoop error:nil]) pass = NO; if(pass && ![newSocket configureStreamsAndReturnError:nil]) pass = NO; if(pass && ![newSocket openStreamsAndReturnError:nil]) pass = NO; if(pass) newSocket->theFlags |= kDidPassConnectMethod; else { // No NSError, but errors will still get logged from the above functions. [newSocket close]; } } } /** * Description forthcoming... **/ - (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)socketError { NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); if(socketError == kCFSocketTimeout || socketError == kCFSocketError) { [self closeWithError:[self getSocketError]]; return; } // Get the underlying native (BSD) socket CFSocketNativeHandle nativeSocket = CFSocketGetNative(sock); // Setup the socket so that invalidating the socket will not close the native socket CFSocketSetSocketFlags(sock, 0); // Invalidate and release the CFSocket - All we need from here on out is the nativeSocket // Note: If we don't invalidate the socket (leaving the native socket open) // then theReadStream and theWriteStream won't function properly. // Specifically, their callbacks won't work, with the exception of kCFStreamEventOpenCompleted. // I'm not entirely sure why this is, but I'm guessing that events on the socket fire to the CFSocket we created, // as opposed to the CFReadStream/CFWriteStream. CFSocketInvalidate(sock); CFRelease(sock); theSocket4 = NULL; theSocket6 = NULL; NSError *err; BOOL pass = YES; if(pass && ![self createStreamsFromNative:nativeSocket error:&err]) pass = NO; if(pass && ![self attachStreamsToRunLoop:nil error:&err]) pass = NO; if(pass && ![self openStreamsAndReturnError:&err]) pass = NO; if(!pass) { [self closeWithError:err]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Stream Implementation //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Creates the CFReadStream and CFWriteStream from the given native socket. * The CFSocket may be extracted from either stream after the streams have been opened. * * Note: The given native socket must already be connected! **/ - (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr { // Create the socket & streams. CFStreamCreatePairWithSocket(kCFAllocatorDefault, native, &theReadStream, &theWriteStream); if (theReadStream == NULL || theWriteStream == NULL) { NSError *err = [self getStreamError]; NSLog (@"AsyncSocket %p couldn't create streams from accepted socket: %@", self, err); if (errPtr) *errPtr = err; return NO; } // Ensure the CF & BSD socket is closed when the streams are closed. CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); return YES; } /** * Creates the CFReadStream and CFWriteStream from the given hostname and port number. * The CFSocket may be extracted from either stream after the streams have been opened. **/ - (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr { // Create the socket & streams. CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)hostname, port, &theReadStream, &theWriteStream); if (theReadStream == NULL || theWriteStream == NULL) { if (errPtr) *errPtr = [self getStreamError]; return NO; } // Ensure the CF & BSD socket is closed when the streams are closed. CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); return YES; } - (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr { // Get the CFRunLoop to which the socket should be attached. theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; // Setup read stream callbacks CFOptionFlags readStreamEvents = kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered | kCFStreamEventOpenCompleted; if (!CFReadStreamSetClient(theReadStream, readStreamEvents, (CFReadStreamClientCallBack)&MyCFReadStreamCallback, (CFStreamClientContext *)(&theContext))) { NSError *err = [self getStreamError]; NSLog (@"AsyncSocket %p couldn't attach read stream to run-loop,", self); NSLog (@"Error: %@", err); if (errPtr) *errPtr = err; return NO; } // Setup write stream callbacks CFOptionFlags writeStreamEvents = kCFStreamEventCanAcceptBytes | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered | kCFStreamEventOpenCompleted; if (!CFWriteStreamSetClient (theWriteStream, writeStreamEvents, (CFWriteStreamClientCallBack)&MyCFWriteStreamCallback, (CFStreamClientContext *)(&theContext))) { NSError *err = [self getStreamError]; NSLog (@"AsyncSocket %p couldn't attach write stream to run-loop,", self); NSLog (@"Error: %@", err); if (errPtr) *errPtr = err; return NO; } // Add read and write streams to run loop unsigned i, count = [theRunLoopModes count]; for(i = 0; i < count; i++) { CFStringRef runLoopMode = (CFStringRef)[theRunLoopModes objectAtIndex:i]; CFReadStreamScheduleWithRunLoop(theReadStream, theRunLoop, runLoopMode); CFWriteStreamScheduleWithRunLoop(theWriteStream, theRunLoop, runLoopMode); } return YES; } /** * Allows the delegate method to configure the CFReadStream and/or CFWriteStream as desired before we connect. * Note that the CFSocket and CFNativeSocket will not be available until after the connection is opened. **/ - (BOOL)configureStreamsAndReturnError:(NSError **)errPtr { // Call the delegate method for further configuration. if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) { if([theDelegate onSocketWillConnect:self] == NO) { if (errPtr) *errPtr = [self getAbortError]; return NO; } } return YES; } - (BOOL)openStreamsAndReturnError:(NSError **)errPtr { BOOL pass = YES; if(pass && !CFReadStreamOpen (theReadStream)) { NSLog (@"AsyncSocket %p couldn't open read stream,", self); pass = NO; } if(pass && !CFWriteStreamOpen (theWriteStream)) { NSLog (@"AsyncSocket %p couldn't open write stream,", self); pass = NO; } if(!pass) { if (errPtr) *errPtr = [self getStreamError]; } return pass; } /** * Called when read or write streams open. * When the socket is connected and both streams are open, consider the AsyncSocket instance to be ready. **/ - (void)doStreamOpen { NSError *err = nil; if ((theFlags & kDidCompleteOpenForRead) && (theFlags & kDidCompleteOpenForWrite)) { // Get the socket. if (![self setSocketFromStreamsAndReturnError: &err]) { NSLog (@"AsyncSocket %p couldn't get socket from streams, %@. Disconnecting.", self, err); [self closeWithError:err]; return; } if ([theDelegate respondsToSelector:@selector(onSocket:didConnectToHost:port:)]) { [theDelegate onSocket:self didConnectToHost:[self connectedHost] port:[self connectedPort]]; } // Stop the connection attempt timeout timer [self endConnectTimeout]; // Immediately deal with any already-queued requests. [self maybeDequeueRead]; [self maybeDequeueWrite]; } } - (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr { // Get the CFSocketNativeHandle from theReadStream CFSocketNativeHandle native; CFDataRef nativeProp = CFReadStreamCopyProperty(theReadStream, kCFStreamPropertySocketNativeHandle); if(nativeProp == NULL) { if (errPtr) *errPtr = [self getStreamError]; return NO; } CFDataGetBytes(nativeProp, CFRangeMake(0, CFDataGetLength(nativeProp)), (UInt8 *)&native); CFRelease(nativeProp); CFSocketRef theSocket = CFSocketCreateWithNative(kCFAllocatorDefault, native, 0, NULL, NULL); if(theSocket == NULL) { if (errPtr) *errPtr = [self getSocketError]; return NO; } // Determine whether the connection was IPv4 or IPv6 CFDataRef peeraddr = CFSocketCopyPeerAddress(theSocket); if(peeraddr == NULL) { NSLog(@"AsyncSocket couldn't determine IP version of socket"); CFRelease(theSocket); if (errPtr) *errPtr = [self getSocketError]; return NO; } struct sockaddr *sa = (struct sockaddr *)CFDataGetBytePtr(peeraddr); if(sa->sa_family == AF_INET) { theSocket4 = theSocket; } else { theSocket6 = theSocket; } CFRelease(peeraddr); return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Disconnect Implementation //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Sends error message and disconnects - (void)closeWithError:(NSError *)err { theFlags |= kClosingWithError; if (theFlags & kDidPassConnectMethod) { // Try to salvage what data we can. [self recoverUnreadData]; // Let the delegate know, so it can try to recover if it likes. if ([theDelegate respondsToSelector:@selector(onSocket:willDisconnectWithError:)]) { [theDelegate onSocket:self willDisconnectWithError:err]; } } [self close]; } // Prepare partially read data for recovery. - (void)recoverUnreadData { if(theCurrentRead != nil) { // We never finished the current read. // Check to see if it's a normal read packet (not AsyncSpecialPacket) and if it had read anything yet. if(([theCurrentRead isKindOfClass:[AsyncReadPacket class]]) && (theCurrentRead->bytesDone > 0)) { // We need to move its data into the front of the partial read buffer. [partialReadBuffer replaceBytesInRange:NSMakeRange(0, 0) withBytes:[theCurrentRead->buffer bytes] length:theCurrentRead->bytesDone]; } } [self emptyQueues]; } - (void)emptyQueues { if (theCurrentRead != nil) [self endCurrentRead]; if (theCurrentWrite != nil) [self endCurrentWrite]; [theReadQueue removeAllObjects]; [theWriteQueue removeAllObjects]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueRead) object:nil]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueWrite) object:nil]; theFlags &= ~kDequeueReadScheduled; theFlags &= ~kDequeueWriteScheduled; } /** * Disconnects. This is called for both error and clean disconnections. **/ - (void)close { // Empty queues [self emptyQueues]; // Clear partialReadBuffer (pre-buffer and also unreadData buffer in case of error) [partialReadBuffer replaceBytesInRange:NSMakeRange(0, [partialReadBuffer length]) withBytes:NULL length:0]; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(disconnect) object:nil]; // Stop the connection attempt timeout timer if (theConnectTimer != nil) { [self endConnectTimeout]; } // Close streams. if (theReadStream != NULL) { [self runLoopUnscheduleReadStream]; CFReadStreamClose(theReadStream); CFRelease(theReadStream); theReadStream = NULL; } if (theWriteStream != NULL) { [self runLoopUnscheduleWriteStream]; CFWriteStreamClose(theWriteStream); CFRelease(theWriteStream); theWriteStream = NULL; } // Close sockets. if (theSocket4 != NULL) { CFSocketInvalidate (theSocket4); CFRelease (theSocket4); theSocket4 = NULL; } if (theSocket6 != NULL) { CFSocketInvalidate (theSocket6); CFRelease (theSocket6); theSocket6 = NULL; } if (theSource4 != NULL) { [self runLoopRemoveSource:theSource4]; CFRelease (theSource4); theSource4 = NULL; } if (theSource6 != NULL) { [self runLoopRemoveSource:theSource6]; CFRelease (theSource6); theSource6 = NULL; } theRunLoop = 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 = (theFlags & kDidPassConnectMethod); // Clear all flags (except the pre-buffering flag, which should remain as is) theFlags &= kEnablePreBuffering; if (shouldCallDelegate) { if ([theDelegate respondsToSelector: @selector(onSocketDidDisconnect:)]) { [theDelegate onSocketDidDisconnect:self]; } } // Do not access any instance variables after calling onSocketDidDisconnect. // This gives the delegate freedom to release us without returning here and crashing. } /** * Disconnects immediately. Any pending reads or writes are dropped. **/ - (void)disconnect { [self close]; } /** * Diconnects after all pending reads have completed. **/ - (void)disconnectAfterReading { theFlags |= (kForbidReadsWrites | kDisconnectAfterReads); [self maybeScheduleDisconnect]; } /** * Disconnects after all pending writes have completed. **/ - (void)disconnectAfterWriting { theFlags |= (kForbidReadsWrites | kDisconnectAfterWrites); [self maybeScheduleDisconnect]; } /** * Disconnects after all pending reads and writes have completed. **/ - (void)disconnectAfterReadingAndWriting { theFlags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); [self maybeScheduleDisconnect]; } /** * Schedules a call to disconnect 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)maybeScheduleDisconnect { BOOL shouldDisconnect = NO; if(theFlags & kDisconnectAfterReads) { if(([theReadQueue count] == 0) && (theCurrentRead == nil)) { if(theFlags & kDisconnectAfterWrites) { if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) { shouldDisconnect = YES; } } else { shouldDisconnect = YES; } } } else if(theFlags & kDisconnectAfterWrites) { if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) { shouldDisconnect = YES; } } if(shouldDisconnect) { [self performSelector:@selector(disconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; } } /** * In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read * any data that's left on the socket. **/ - (NSData *)unreadData { // Ensure this method will only return data in the event of an error if(!(theFlags & kClosingWithError)) return nil; if(theReadStream == NULL) return nil; CFIndex totalBytesRead = [partialReadBuffer length]; BOOL error = NO; while(!error && CFReadStreamHasBytesAvailable(theReadStream)) { [partialReadBuffer increaseLengthBy:READALL_CHUNKSIZE]; // Number of bytes to read is space left in packet buffer. CFIndex bytesToRead = [partialReadBuffer length] - totalBytesRead; // Read data into packet buffer UInt8 *packetbuf = (UInt8 *)( [partialReadBuffer mutableBytes] + totalBytesRead ); CFIndex bytesRead = CFReadStreamRead(theReadStream, packetbuf, bytesToRead); // Check results if(bytesRead < 0) { error = YES; } else { totalBytesRead += bytesRead; } } [partialReadBuffer setLength:totalBytesRead]; return partialReadBuffer; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Errors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Returns a standard error object for the current errno value. * Errno is used for low-level BSD socket errors. **/ - (NSError *)getErrnoError { NSString *errorMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errorMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } /** * Returns a standard error message for a CFSocket error. * Unfortunately, CFSocket offers no feedback on its errors. **/ - (NSError *)getSocketError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCFSocketError", @"AsyncSocket", [NSBundle mainBundle], @"General CFSocket error", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; } - (NSError *)getStreamError { CFStreamError err; if (theReadStream != NULL) { err = CFReadStreamGetError (theReadStream); if (err.error != 0) return [self errorFromCFStreamError: err]; } if (theWriteStream != NULL) { err = CFWriteStreamGetError (theWriteStream); if (err.error != 0) return [self errorFromCFStreamError: err]; } return nil; } /** * Returns a standard AsyncSocket abort error. **/ - (NSError *)getAbortError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCanceledError", @"AsyncSocket", [NSBundle mainBundle], @"Connection canceled", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCanceledError userInfo:info]; } /** * Returns a standard AsyncSocket connect timeout error. **/ - (NSError *)getConnectTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketConnectTimeoutError", @"AsyncSocket", [NSBundle mainBundle], @"Attempt to connect to host timed out", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketConnectTimeoutError userInfo:info]; } /** * Returns a standard AsyncSocket maxed out error. **/ - (NSError *)getReadMaxedOutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadMaxedOutError", @"AsyncSocket", [NSBundle mainBundle], @"Read operation reached set maximum length", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadMaxedOutError userInfo:info]; } /** * Returns a standard AsyncSocket read timeout error. **/ - (NSError *)getReadTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadTimeoutError", @"AsyncSocket", [NSBundle mainBundle], @"Read operation timed out", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadTimeoutError userInfo:info]; } /** * Returns a standard AsyncSocket write timeout error. **/ - (NSError *)getWriteTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketWriteTimeoutError", @"AsyncSocket", [NSBundle mainBundle], @"Write operation timed out", nil); NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketWriteTimeoutError userInfo:info]; } - (NSError *)errorFromCFStreamError:(CFStreamError)err { if (err.domain == 0 && err.error == 0) return nil; // Can't use switch; these constants aren't int literals. NSString *domain = @"CFStreamError (unlisted domain)"; NSString *message = nil; if(err.domain == kCFStreamErrorDomainPOSIX) { domain = NSPOSIXErrorDomain; } else if(err.domain == kCFStreamErrorDomainMacOSStatus) { domain = NSOSStatusErrorDomain; } else if(err.domain == kCFStreamErrorDomainMach) { domain = NSMachErrorDomain; } else if(err.domain == kCFStreamErrorDomainNetDB) { domain = @"kCFStreamErrorDomainNetDB"; message = [NSString stringWithCString:gai_strerror(err.error) encoding:NSASCIIStringEncoding]; } else if(err.domain == kCFStreamErrorDomainNetServices) { domain = @"kCFStreamErrorDomainNetServices"; } else if(err.domain == kCFStreamErrorDomainSOCKS) { domain = @"kCFStreamErrorDomainSOCKS"; } else if(err.domain == kCFStreamErrorDomainSystemConfiguration) { domain = @"kCFStreamErrorDomainSystemConfiguration"; } else if(err.domain == kCFStreamErrorDomainSSL) { domain = @"kCFStreamErrorDomainSSL"; } NSDictionary *info = nil; if(message != nil) { info = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; } return [NSError errorWithDomain:domain code:err.error userInfo:info]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Diagnostics //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)isConnected { return [self isSocketConnected] && [self areStreamsConnected]; } - (NSString *)connectedHost { if(theSocket4) return [self connectedHost:theSocket4]; else return [self connectedHost:theSocket6]; } - (UInt16)connectedPort { if(theSocket4) return [self connectedPort:theSocket4]; else return [self connectedPort:theSocket6]; } - (NSString *)localHost { if(theSocket4) return [self localHost:theSocket4]; else return [self localHost:theSocket6]; } - (UInt16)localPort { if(theSocket4) return [self localPort:theSocket4]; else return [self localPort:theSocket6]; } - (NSString *)connectedHost:(CFSocketRef)theSocket { if (theSocket == NULL) return nil; CFDataRef peeraddr; NSString *peerstr = nil; if((peeraddr = CFSocketCopyPeerAddress(theSocket))) { peerstr = [self addressHost:peeraddr]; CFRelease (peeraddr); } return peerstr; } - (UInt16)connectedPort:(CFSocketRef)theSocket { if (theSocket == NULL) return 0; CFDataRef peeraddr; UInt16 peerport = 0; if((peeraddr = CFSocketCopyPeerAddress(theSocket))) { peerport = [self addressPort:peeraddr]; CFRelease (peeraddr); } return peerport; } - (NSString *)localHost:(CFSocketRef)theSocket { if (theSocket == NULL) return nil; CFDataRef selfaddr; NSString *selfstr = nil; if((selfaddr = CFSocketCopyAddress(theSocket))) { selfstr = [self addressHost:selfaddr]; CFRelease (selfaddr); } return selfstr; } - (UInt16)localPort:(CFSocketRef)theSocket { if (theSocket == NULL) return 0; CFDataRef selfaddr; UInt16 selfport = 0; if ((selfaddr = CFSocketCopyAddress(theSocket))) { selfport = [self addressPort:selfaddr]; CFRelease (selfaddr); } return selfport; } - (BOOL)isSocketConnected { if(theSocket4 != NULL) return CFSocketIsValid(theSocket4); else if(theSocket6 != NULL) return CFSocketIsValid(theSocket6); else return NO; } - (BOOL)areStreamsConnected { CFStreamStatus s; if (theReadStream != NULL) { s = CFReadStreamGetStatus (theReadStream); if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusReading || s == kCFStreamStatusError) ) return NO; } else return NO; if (theWriteStream != NULL) { s = CFWriteStreamGetStatus (theWriteStream); if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusWriting || s == kCFStreamStatusError) ) return NO; } else return NO; return YES; } - (NSString *)addressHost:(CFDataRef)cfaddr { if (cfaddr == NULL) return nil; char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ]; struct sockaddr *pSockAddr = (struct sockaddr *) CFDataGetBytePtr (cfaddr); struct sockaddr_in *pSockAddrV4 = (struct sockaddr_in *) pSockAddr; struct sockaddr_in6 *pSockAddrV6 = (struct sockaddr_in6 *)pSockAddr; const void *pAddr = (pSockAddr->sa_family == AF_INET) ? (void *)(&(pSockAddrV4->sin_addr)) : (void *)(&(pSockAddrV6->sin6_addr)); const char *pStr = inet_ntop (pSockAddr->sa_family, pAddr, addrBuf, sizeof(addrBuf)); if (pStr == NULL) [NSException raise: NSInternalInconsistencyException format: @"Cannot convert address to string."]; return [NSString stringWithCString:pStr encoding:NSASCIIStringEncoding]; } - (UInt16)addressPort:(CFDataRef)cfaddr { if (cfaddr == NULL) return 0; struct sockaddr_in *pAddr = (struct sockaddr_in *) CFDataGetBytePtr (cfaddr); return ntohs (pAddr->sin_port); } - (BOOL)isIPv4 { return (theSocket4 != NULL); } - (BOOL)isIPv6 { return (theSocket6 != NULL); } - (NSString *)description { static const char *statstr[] = {"not open","opening","open","reading","writing","at end","closed","has error"}; CFStreamStatus rs = (theReadStream != NULL) ? CFReadStreamGetStatus(theReadStream) : 0; CFStreamStatus ws = (theWriteStream != NULL) ? CFWriteStreamGetStatus(theWriteStream) : 0; NSString *peerstr, *selfstr; CFDataRef peeraddr4 = NULL, peeraddr6 = NULL, selfaddr4 = NULL, selfaddr6 = NULL; if (theSocket4 || theSocket6) { if (theSocket4) peeraddr4 = CFSocketCopyPeerAddress(theSocket4); if (theSocket6) peeraddr6 = CFSocketCopyPeerAddress(theSocket6); if(theSocket4 && theSocket6) { peerstr = [NSString stringWithFormat: @"%@/%@ %u", [self addressHost:peeraddr4], [self addressHost:peeraddr6], [self addressPort:peeraddr4]]; } else if(theSocket4) { peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr4], [self addressPort:peeraddr4]]; } else { peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr6], [self addressPort:peeraddr6]]; } if(peeraddr4) CFRelease(peeraddr4); if(peeraddr6) CFRelease(peeraddr6); peeraddr4 = NULL; peeraddr6 = NULL; } else peerstr = @"nowhere"; if (theSocket4 || theSocket6) { if (theSocket4) selfaddr4 = CFSocketCopyAddress (theSocket4); if (theSocket6) selfaddr6 = CFSocketCopyAddress (theSocket6); if (theSocket4 && theSocket6) { selfstr = [NSString stringWithFormat: @"%@/%@ %u", [self addressHost:selfaddr4], [self addressHost:selfaddr6], [self addressPort:selfaddr4]]; } else if (theSocket4) { selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr4], [self addressPort:selfaddr4]]; } else { selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr6], [self addressPort:selfaddr6]]; } if(selfaddr4) CFRelease(selfaddr4); if(selfaddr6) CFRelease(selfaddr6); selfaddr4 = NULL; selfaddr6 = NULL; } else selfstr = @"nowhere"; NSMutableString *ms = [[NSMutableString alloc] initWithCapacity:150]; [ms appendString:[NSString stringWithFormat:@"buffer length] != 0) percentDone = (float)theCurrentRead->bytesDone / (float)[theCurrentRead->buffer length] * 100.0F; else percentDone = 100.0F; [ms appendString: [NSString stringWithFormat:@"currently read %u bytes (%d%% done), ", (unsigned int)[theCurrentRead->buffer length], theCurrentRead->bytesDone ? percentDone : 0]]; } if (theCurrentWrite == nil) [ms appendString: @"no current write, "]; else { int percentDone; if ([theCurrentWrite->buffer length] != 0) percentDone = (float)theCurrentWrite->bytesDone / (float)[theCurrentWrite->buffer length] * 100.0F; else percentDone = 100.0F; [ms appendString: [NSString stringWithFormat:@"currently written %u (%d%%), ", (unsigned int)[theCurrentWrite->buffer length], theCurrentWrite->bytesDone ? percentDone : 0]]; } [ms appendString:[NSString stringWithFormat:@"read stream %p %s, ", theReadStream, statstr[rs]]]; [ms appendString:[NSString stringWithFormat:@"write stream %p %s", theWriteStream, statstr[ws]]]; if(theFlags & kDisconnectAfterReads) { if(theFlags & kDisconnectAfterWrites) [ms appendString: @", will disconnect after reads & writes"]; else [ms appendString: @", will disconnect after reads"]; } else if(theFlags & kDisconnectAfterWrites) { [ms appendString: @", will disconnect after writes"]; } if (![self isConnected]) [ms appendString: @", not connected"]; [ms appendString:@">"]; return [ms autorelease]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Reading //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)readDataToLength:(CFIndex)length withTimeout:(NSTimeInterval)timeout tag:(long)tag { if(length == 0) return; if(theFlags & kForbidReadsWrites) return; NSMutableData *buffer = [[NSMutableData alloc] initWithLength:length]; AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer timeout:timeout tag:tag readAllAvailable:NO terminator:nil maxLength:length]; [theReadQueue addObject:packet]; [self scheduleDequeueRead]; [packet release]; [buffer release]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataToData:data withTimeout:timeout maxLength:-1 tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(CFIndex)length tag:(long)tag { if(data == nil || [data length] == 0) return; if(length >= 0 && length < [data length]) return; if(theFlags & kForbidReadsWrites) return; NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer timeout:timeout tag:tag readAllAvailable:NO terminator:data maxLength:length]; [theReadQueue addObject:packet]; [self scheduleDequeueRead]; [packet release]; [buffer release]; } - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag { if (theFlags & kForbidReadsWrites) return; NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer timeout:timeout tag:tag readAllAvailable:YES terminator:nil maxLength:-1]; [theReadQueue addObject:packet]; [self scheduleDequeueRead]; [packet release]; [buffer release]; } /** * Puts a maybeDequeueRead on the run loop. * An assumption here is that selectors will be performed consecutively within their priority. **/ - (void)scheduleDequeueRead { if((theFlags & kDequeueReadScheduled) == 0) { theFlags |= kDequeueReadScheduled; [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; } } /** * This method starts a new read, if needed. * It is called when a user requests a read, * or when a stream opens that may have requested reads sitting in the queue, etc. **/ - (void)maybeDequeueRead { // Unset the flag indicating a call to this method is scheduled theFlags &= ~kDequeueReadScheduled; // If we're not currently processing a read AND we have an available read stream if((theCurrentRead == nil) && (theReadStream != NULL)) { if([theReadQueue count] > 0) { // Dequeue the next object in the write queue theCurrentRead = [[theReadQueue objectAtIndex:0] retain]; [theReadQueue removeObjectAtIndex:0]; if([theCurrentRead isKindOfClass:[AsyncSpecialPacket class]]) { // Attempt to start TLS // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are both set theFlags |= kStartingReadTLS; [self maybeStartTLS]; } else { // Start time-out timer if(theCurrentRead->timeout >= 0.0) { theReadTimer = [NSTimer timerWithTimeInterval:theCurrentRead->timeout target:self selector:@selector(doReadTimeout:) userInfo:nil repeats:NO]; [self runLoopAddTimer:theReadTimer]; } // Immediately read, if possible [self doBytesAvailable]; } } else if(theFlags & kDisconnectAfterReads) { if(theFlags & kDisconnectAfterWrites) { if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) { [self disconnect]; } } else { [self disconnect]; } } } } /** * Call this method in doBytesAvailable instead of CFReadStreamHasBytesAvailable(). * This method supports pre-buffering properly. **/ - (BOOL)hasBytesAvailable { return ([partialReadBuffer length] > 0) || CFReadStreamHasBytesAvailable(theReadStream); } /** * Call this method in doBytesAvailable instead of CFReadStreamRead(). * This method support pre-buffering properly. **/ - (CFIndex)readIntoBuffer:(UInt8 *)buffer maxLength:(CFIndex)length { if([partialReadBuffer length] > 0) { // Determine the maximum amount of data to read CFIndex bytesToRead = MIN(length, [partialReadBuffer length]); // Copy the bytes from the buffer memcpy(buffer, [partialReadBuffer bytes], bytesToRead); // Remove the copied bytes from the buffer [partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToRead) withBytes:NULL length:0]; return bytesToRead; } else { return CFReadStreamRead(theReadStream, buffer, length); } } /** * This method is called when a new read is taken from the read queue or when new data becomes available on the stream. **/ - (void)doBytesAvailable { // If data is available on the stream, but there is no read request, then we don't need to process the data yet. // Also, if there is a read request, but no read stream setup yet, we can't process any data yet. if((theCurrentRead != nil) && (theReadStream != NULL)) { // Note: This method is not called if theCurrentRead is an AsyncSpecialPacket (startTLS packet) CFIndex totalBytesRead = 0; BOOL done = NO; BOOL socketError = NO; BOOL maxoutError = NO; while(!done && !socketError && !maxoutError && [self hasBytesAvailable]) { BOOL didPreBuffer = NO; // If reading all available data, make sure there's room in the packet buffer. if(theCurrentRead->readAllAvailableData == YES) { // Make sure there is at least READALL_CHUNKSIZE bytes available. // We don't want to increase the buffer any more than this or we'll waste space. // With prebuffering it's possible to read in a small chunk on the first read. unsigned buffInc = READALL_CHUNKSIZE - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); [theCurrentRead->buffer increaseLengthBy:buffInc]; } // If reading until data, we may only want to read a few bytes. // Just enough to ensure we don't go past our term or over our max limit. // Unless pre-buffering is enabled, in which case we may want to read in a larger chunk. if(theCurrentRead->term != nil) { // If we already have data pre-buffered, we obviously don't want to pre-buffer it again. // So in this case we'll just read as usual. if(([partialReadBuffer length] > 0) || !(theFlags & kEnablePreBuffering)) { unsigned maxToRead = [theCurrentRead readLengthForTerm]; unsigned bufInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); [theCurrentRead->buffer increaseLengthBy:bufInc]; } else { didPreBuffer = YES; unsigned maxToRead = [theCurrentRead prebufferReadLengthForTerm]; unsigned buffInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); [theCurrentRead->buffer increaseLengthBy:buffInc]; } } // Number of bytes to read is space left in packet buffer. CFIndex bytesToRead = [theCurrentRead->buffer length] - theCurrentRead->bytesDone; // Read data into packet buffer UInt8 *subBuffer = (UInt8 *)([theCurrentRead->buffer mutableBytes] + theCurrentRead->bytesDone); CFIndex bytesRead = [self readIntoBuffer:subBuffer maxLength:bytesToRead]; // Check results if(bytesRead < 0) { socketError = YES; } else { // Update total amound read for the current read theCurrentRead->bytesDone += bytesRead; // Update total amount read in this method invocation totalBytesRead += bytesRead; } // Is packet done? if(theCurrentRead->readAllAvailableData != YES) { if(theCurrentRead->term != nil) { if(didPreBuffer) { // Search for the terminating sequence within the big chunk we just read. CFIndex overflow = [theCurrentRead searchForTermAfterPreBuffering:bytesRead]; if(overflow > 0) { // Copy excess data into partialReadBuffer NSMutableData *buffer = theCurrentRead->buffer; const void *overflowBuffer = [buffer bytes] + theCurrentRead->bytesDone - overflow; [partialReadBuffer appendBytes:overflowBuffer length:overflow]; // Update the bytesDone variable. // Note: The completeCurrentRead method will trim the buffer for us. theCurrentRead->bytesDone -= overflow; } done = (overflow >= 0); } else { // Search for the terminating sequence at the end of the buffer int termlen = [theCurrentRead->term length]; if(theCurrentRead->bytesDone >= termlen) { const void *buf = [theCurrentRead->buffer bytes] + (theCurrentRead->bytesDone - termlen); const void *seq = [theCurrentRead->term bytes]; done = (memcmp (buf, seq, termlen) == 0); } } if(!done && theCurrentRead->maxLength >= 0 && theCurrentRead->bytesDone >= theCurrentRead->maxLength) { // There's a set maxLength, and we've reached that maxLength without completing the read maxoutError = YES; } } else { // Done when (sized) buffer is full. done = ([theCurrentRead->buffer length] == theCurrentRead->bytesDone); } } // else readAllAvailable doesn't end until all readable is read. } if(theCurrentRead->readAllAvailableData && theCurrentRead->bytesDone > 0) done = YES; // Ran out of bytes, so the "read-all-data" type packet is done if(done) { [self completeCurrentRead]; if (!socketError) [self scheduleDequeueRead]; } else if(theCurrentRead->bytesDone > 0) { // We're not done with the readToLength or readToData yet, but we have read in some bytes if ([theDelegate respondsToSelector:@selector(onSocket:didReadPartialDataOfLength:tag:)]) { [theDelegate onSocket:self didReadPartialDataOfLength:totalBytesRead tag:theCurrentRead->tag]; } } if(socketError) { CFStreamError err = CFReadStreamGetError(theReadStream); [self closeWithError:[self errorFromCFStreamError:err]]; return; } if(maxoutError) { [self closeWithError:[self getReadMaxedOutError]]; return; } } } // Ends current read and calls delegate. - (void)completeCurrentRead { NSAssert (theCurrentRead, @"Trying to complete current read when there is no current read."); [theCurrentRead->buffer setLength:theCurrentRead->bytesDone]; if([theDelegate respondsToSelector:@selector(onSocket:didReadData:withTag:)]) { [theDelegate onSocket:self didReadData:theCurrentRead->buffer withTag:theCurrentRead->tag]; } if (theCurrentRead != nil) [self endCurrentRead]; // Caller may have disconnected. } // Ends current read. - (void)endCurrentRead { NSAssert (theCurrentRead, @"Trying to end current read when there is no current read."); [theReadTimer invalidate]; theReadTimer = nil; [theCurrentRead release]; theCurrentRead = nil; } - (void)doReadTimeout:(NSTimer *)timer { NSTimeInterval timeoutExtension = 0.0; if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) { timeoutExtension = [theDelegate onSocket:self shouldTimeoutReadWithTag:theCurrentRead->tag elapsed:theCurrentRead->timeout bytesDone:theCurrentRead->bytesDone]; } if(timeoutExtension > 0.0) { theCurrentRead->timeout += timeoutExtension; theReadTimer = [NSTimer timerWithTimeInterval:timeoutExtension target:self selector:@selector(doReadTimeout:) userInfo:nil repeats:NO]; [self runLoopAddTimer:theReadTimer]; } else { [self endCurrentRead]; [self closeWithError:[self getReadTimeoutError]]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Writing //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { if (data == nil || [data length] == 0) return; if (theFlags & kForbidReadsWrites) return; AsyncWritePacket *packet = [[AsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; [theWriteQueue addObject:packet]; [self scheduleDequeueWrite]; [packet release]; } - (void)scheduleDequeueWrite { if((theFlags & kDequeueWriteScheduled) == 0) { theFlags |= kDequeueWriteScheduled; [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; } } // Start a new write. - (void)maybeDequeueWrite { // Unset the flag indicating a call to this method is scheduled theFlags &= ~kDequeueWriteScheduled; // If we're not currently processing a write AND we have an available write stream if((theCurrentWrite == nil) && (theWriteStream != NULL)) { if([theWriteQueue count] > 0) { // Dequeue the next object in the write queue theCurrentWrite = [[theWriteQueue objectAtIndex:0] retain]; [theWriteQueue removeObjectAtIndex:0]; if([theCurrentWrite isKindOfClass:[AsyncSpecialPacket class]]) { // Attempt to start TLS // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are both set theFlags |= kStartingWriteTLS; [self maybeStartTLS]; } else { // Start time-out timer if(theCurrentWrite->timeout >= 0.0) { theWriteTimer = [NSTimer timerWithTimeInterval:theCurrentWrite->timeout target:self selector:@selector(doWriteTimeout:) userInfo:nil repeats:NO]; [self runLoopAddTimer:theWriteTimer]; } // Immediately write, if possible [self doSendBytes]; } } else if(theFlags & kDisconnectAfterWrites) { if(theFlags & kDisconnectAfterReads) { if(([theReadQueue count] == 0) && (theCurrentRead == nil)) { [self disconnect]; } } else { [self disconnect]; } } } } - (void)doSendBytes { if((theCurrentWrite != nil) && (theWriteStream != NULL)) { // Note: This method is not called if theCurrentWrite is an AsyncSpecialPacket (startTLS packet) BOOL done = NO, error = NO; while (!done && !error && CFWriteStreamCanAcceptBytes(theWriteStream)) { // Figure out what to write. CFIndex bytesRemaining = [theCurrentWrite->buffer length] - theCurrentWrite->bytesDone; CFIndex bytesToWrite = (bytesRemaining < WRITE_CHUNKSIZE) ? bytesRemaining : WRITE_CHUNKSIZE; UInt8 *writestart = (UInt8 *)([theCurrentWrite->buffer bytes] + theCurrentWrite->bytesDone); // Write. CFIndex bytesWritten = CFWriteStreamWrite (theWriteStream, writestart, bytesToWrite); // Check results. if (bytesWritten < 0) { bytesWritten = 0; error = YES; } // Is packet done? theCurrentWrite->bytesDone += bytesWritten; done = ([theCurrentWrite->buffer length] == theCurrentWrite->bytesDone); } if(done) { [self completeCurrentWrite]; if (!error) [self scheduleDequeueWrite]; } if(error) { CFStreamError err = CFWriteStreamGetError(theWriteStream); [self closeWithError:[self errorFromCFStreamError:err]]; return; } } } // Ends current write and calls delegate. - (void)completeCurrentWrite { NSAssert (theCurrentWrite, @"Trying to complete current write when there is no current write."); if ([theDelegate respondsToSelector:@selector(onSocket:didWriteDataWithTag:)]) { [theDelegate onSocket:self didWriteDataWithTag:theCurrentWrite->tag]; } if (theCurrentWrite != nil) [self endCurrentWrite]; // Caller may have disconnected. } // Ends current write. - (void)endCurrentWrite { NSAssert (theCurrentWrite, @"Trying to complete current write when there is no current write."); [theWriteTimer invalidate]; theWriteTimer = nil; [theCurrentWrite release]; theCurrentWrite = nil; } - (void)doWriteTimeout:(NSTimer *)timer { NSTimeInterval timeoutExtension = 0.0; if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) { timeoutExtension = [theDelegate onSocket:self shouldTimeoutWriteWithTag:theCurrentWrite->tag elapsed:theCurrentWrite->timeout bytesDone:theCurrentWrite->bytesDone]; } if(timeoutExtension > 0.0) { theCurrentWrite->timeout += timeoutExtension; theWriteTimer = [NSTimer timerWithTimeInterval:timeoutExtension target:self selector:@selector(doWriteTimeout:) userInfo:nil repeats:NO]; [self runLoopAddTimer:theWriteTimer]; } else { [self endCurrentWrite]; [self closeWithError:[self getWriteTimeoutError]]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)startTLS:(NSDictionary *)tlsSettings { 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]; } AsyncSpecialPacket *packet = [[AsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; [theReadQueue addObject:packet]; [self scheduleDequeueRead]; [theWriteQueue addObject:packet]; [self scheduleDequeueWrite]; [packet release]; } - (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((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) { AsyncSpecialPacket *tlsPacket = (AsyncSpecialPacket *)theCurrentRead; BOOL didSecureReadStream = CFReadStreamSetProperty(theReadStream, kCFStreamPropertySSLSettings, (CFDictionaryRef)tlsPacket->tlsSettings); BOOL didSecureWriteStream = CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertySSLSettings, (CFDictionaryRef)tlsPacket->tlsSettings); if(!didSecureReadStream || !didSecureWriteStream) { [self onTLSStarted:NO]; } } } - (void)onTLSStarted:(BOOL)flag { if((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) { theFlags &= ~kStartingReadTLS; theFlags &= ~kStartingWriteTLS; if([theDelegate respondsToSelector:@selector(onSocket:didSecure:)]) { [theDelegate onSocket:self didSecure:flag]; } [self endCurrentRead]; [self endCurrentWrite]; [self scheduleDequeueRead]; [self scheduleDequeueWrite]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark CF Callbacks //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)doCFSocketCallback:(CFSocketCallBackType)type forSocket:(CFSocketRef)sock withAddress:(NSData *)address withData:(const void *)pData { NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); switch (type) { case kCFSocketConnectCallBack: // The data argument is either NULL or a pointer to an SInt32 error code, if the connect failed. if(pData) [self doSocketOpen:sock withCFSocketError:kCFSocketError]; else [self doSocketOpen:sock withCFSocketError:kCFSocketSuccess]; break; case kCFSocketAcceptCallBack: [self doAcceptWithSocket: *((CFSocketNativeHandle *)pData)]; break; default: NSLog (@"AsyncSocket %p received unexpected CFSocketCallBackType %d.", self, type); break; } } - (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream { NSParameterAssert(theReadStream != NULL); CFStreamError err; switch (type) { case kCFStreamEventOpenCompleted: theFlags |= kDidCompleteOpenForRead; [self doStreamOpen]; break; case kCFStreamEventHasBytesAvailable: if(theFlags & kStartingReadTLS) [self onTLSStarted:YES]; else [self doBytesAvailable]; break; case kCFStreamEventErrorOccurred: case kCFStreamEventEndEncountered: err = CFReadStreamGetError (theReadStream); [self closeWithError: [self errorFromCFStreamError:err]]; break; default: NSLog (@"AsyncSocket %p received unexpected CFReadStream callback, CFStreamEventType %d.", self, type); } } - (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream { NSParameterAssert(theWriteStream != NULL); CFStreamError err; switch (type) { case kCFStreamEventOpenCompleted: theFlags |= kDidCompleteOpenForWrite; [self doStreamOpen]; break; case kCFStreamEventCanAcceptBytes: if(theFlags & kStartingWriteTLS) [self onTLSStarted:YES]; else [self doSendBytes]; break; case kCFStreamEventErrorOccurred: case kCFStreamEventEndEncountered: err = CFWriteStreamGetError (theWriteStream); [self closeWithError: [self errorFromCFStreamError:err]]; break; default: NSLog (@"AsyncSocket %p received unexpected CFWriteStream callback, CFStreamEventType %d.", self, type); } } /** * This is the callback we setup for CFSocket. * This method does nothing but forward the call to it's Objective-C counterpart **/ static void MyCFSocketCallback (CFSocketRef sref, CFSocketCallBackType type, CFDataRef address, const void *pData, void *pInfo) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; [theSocket doCFSocketCallback:type forSocket:sref withAddress:(NSData *)address withData:pData]; [pool release]; } /** * This is the callback we setup for CFReadStream. * This method does nothing but forward the call to it's Objective-C counterpart **/ static void MyCFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; [theSocket doCFReadStreamCallback:type forStream:stream]; [pool release]; } /** * This is the callback we setup for CFWriteStream. * This method does nothing but forward the call to it's Objective-C counterpart **/ static void MyCFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; AsyncSocket *theSocket = [[(AsyncSocket *)pInfo retain] autorelease]; [theSocket doCFWriteStreamCallback:type forStream:stream]; [pool release]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Class Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Return line separators. + (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