jens@0: // jens@0: // TCPConnection.m jens@0: // MYNetwork jens@0: // jens@0: // Created by Jens Alfke on 5/18/08. jens@0: // Copyright 2008 Jens Alfke. All rights reserved. jens@0: // jens@0: jens@0: #import "TCP_Internal.h" jens@0: #import "IPAddress.h" jens@33: #import "MYBonjourService.h" jens@0: jens@1: #import "Logging.h" jens@1: #import "Test.h" jens@0: #import "ExceptionUtils.h" jens@0: jens@0: jens@26: #if TARGET_OS_IPHONE && !defined(__SEC_TYPES__) jens@8: // SecureTransport.h is missing on iPhone, with its SSL constants: jens@8: enum{ jens@8: errSSLClosedAbort = -9806, /* connection closed via error */ jens@8: }; jens@8: #endif jens@8: jens@8: jens@8: jens@0: NSString* const TCPErrorDomain = @"TCP"; jens@0: jens@0: jens@0: @interface TCPConnection () jens@0: @property TCPConnectionStatus status; jens@7: @property (retain) IPAddress *address; jens@0: - (BOOL) _checkIfClosed; jens@0: - (void) _closed; jens@0: @end jens@0: jens@0: jens@0: @implementation TCPConnection jens@0: jens@0: jens@0: static NSMutableArray *sAllConnections; jens@0: jens@0: jens@0: - (Class) readerClass {return [TCPReader class];} jens@0: - (Class) writerClass {return [TCPWriter class];} jens@0: jens@0: jens@0: - (id) _initWithAddress: (IPAddress*)address jens@0: inputStream: (NSInputStream*)input jens@0: outputStream: (NSOutputStream*)output jens@0: { jens@0: self = [super init]; jens@0: if (self != nil) { jens@7: if( !input || !output ) { jens@0: LogTo(TCP,@"Failed to create %@: addr=%@, in=%@, out=%@", jens@0: self.class,address,input,output); jens@0: [self release]; jens@0: return nil; jens@0: } jens@0: _address = [address copy]; jens@0: _reader = [[[self readerClass] alloc] initWithConnection: self stream: input]; jens@0: _writer = [[[self writerClass] alloc] initWithConnection: self stream: output]; jens@7: LogTo(TCP,@"%@ initialized, address=%@",self,address); jens@0: } jens@0: return self; jens@0: } jens@0: jens@0: jens@0: jens@0: - (id) initToAddress: (IPAddress*)address jens@0: { jens@0: NSInputStream *input = nil; jens@0: NSOutputStream *output = nil; jens@8: #if TARGET_OS_IPHONE jens@8: // +getStreamsToHost: is missing for some stupid reason on iPhone. Grrrrrrrrrr. jens@8: CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)address.hostname, address.port, jens@8: (CFReadStreamRef*)&input, (CFWriteStreamRef*)&output); jens@8: if( input ) jens@8: [(id)CFMakeCollectable(input) autorelease]; jens@8: if( output ) jens@8: [(id)CFMakeCollectable(output) autorelease]; jens@8: #else jens@0: [NSStream getStreamsToHost: [NSHost hostWithAddress: address.ipv4name] jens@0: port: address.port jens@0: inputStream: &input jens@0: outputStream: &output]; jens@8: #endif jens@0: return [self _initWithAddress: address inputStream: input outputStream: output]; jens@0: } jens@0: jens@7: - (id) initToNetService: (NSNetService*)service jens@7: { jens@7: IPAddress *address = nil; jens@7: NSInputStream *input; jens@7: NSOutputStream *output; jens@7: if( [service getInputStream: &input outputStream: &output] ) { jens@7: NSArray *addresses = service.addresses; jens@7: if( addresses.count > 0 ) jens@7: address = [[[IPAddress alloc] initWithSockAddr: [[addresses objectAtIndex: 0] bytes]] autorelease]; jens@7: } else { jens@7: input = nil; jens@7: output = nil; jens@7: } jens@7: return [self _initWithAddress: address inputStream: input outputStream: output]; jens@7: } jens@7: jens@33: - (id) initToBonjourService: (MYBonjourService*)service; jens@33: { jens@33: NSNetService *netService = [[NSNetService alloc] initWithDomain: service.domain jens@33: type: service.type name: service.name]; jens@33: self = [self initToNetService: netService]; jens@33: [service release]; jens@33: return self; jens@33: } jens@33: jens@0: jens@0: - (id) initIncomingFromSocket: (CFSocketNativeHandle)socket jens@0: listener: (TCPListener*)listener jens@0: { jens@0: CFReadStreamRef readStream = NULL; jens@0: CFWriteStreamRef writeStream = NULL; jens@0: CFStreamCreatePairWithSocket(kCFAllocatorDefault, socket, &readStream, &writeStream); jens@0: self = [self _initWithAddress: [IPAddress addressOfSocket: socket] jens@29: inputStream: (NSInputStream*)CFMakeCollectable(readStream) jens@29: outputStream: (NSOutputStream*)CFMakeCollectable(writeStream)]; jens@0: if( self ) { jens@0: _isIncoming = YES; jens@0: _server = [listener retain]; jens@0: CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); jens@0: CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); jens@0: } jens@29: if(readStream) CFRelease(readStream); jens@29: if(writeStream) CFRelease(writeStream); jens@0: return self; jens@0: } jens@0: jens@0: jens@0: - (void) dealloc jens@0: { jens@0: LogTo(TCP,@"DEALLOC %@",self); jens@0: [_reader release]; jens@0: [_writer release]; jens@0: [_address release]; jens@0: [super dealloc]; jens@0: } jens@0: jens@0: jens@0: - (NSString*) description jens@0: { jens@0: return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address); jens@0: } jens@0: jens@0: jens@0: @synthesize address=_address, isIncoming=_isIncoming, status=_status, delegate=_delegate, jens@16: reader=_reader, writer=_writer, server=_server, openTimeout=_openTimeout; jens@0: jens@0: jens@0: - (NSError*) error jens@0: { jens@0: return _error; jens@0: } jens@0: jens@0: jens@0: - (NSString*) actualSecurityLevel jens@0: { jens@0: return _reader.securityLevel; jens@0: jens@0: } jens@0: jens@0: - (NSArray*) peerSSLCerts jens@0: { jens@0: return _reader.peerSSLCerts ?: _writer.peerSSLCerts; jens@0: } jens@0: jens@0: jens@0: - (void) _setStreamProperty: (id)value forKey: (NSString*)key jens@0: { jens@0: [_reader setProperty: value forKey: (CFStringRef)key]; jens@0: [_writer setProperty: value forKey: (CFStringRef)key]; jens@0: } jens@0: jens@0: jens@0: #pragma mark - jens@0: #pragma mark OPENING / CLOSING: jens@0: jens@0: jens@0: - (void) open jens@0: { jens@0: if( _status<=kTCP_Closed && _reader ) { jens@0: _reader.SSLProperties = _sslProperties; jens@0: _writer.SSLProperties = _sslProperties; jens@0: [_reader open]; jens@0: [_writer open]; jens@0: if( ! [sAllConnections my_containsObjectIdenticalTo: self] ) jens@0: [sAllConnections addObject: self]; jens@0: self.status = kTCP_Opening; jens@16: if( _openTimeout > 0 ) jens@16: [self performSelector: @selector(_openTimeoutExpired) withObject: nil afterDelay: _openTimeout]; jens@16: } jens@16: } jens@16: jens@16: - (void) _stopOpenTimer jens@16: { jens@16: [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_openTimeoutExpired) object: nil]; jens@16: } jens@16: jens@16: - (void) _openTimeoutExpired jens@16: { jens@16: if( _status == kTCP_Opening ) { jens@16: LogTo(TCP,@"%@: timed out waiting to open",self); jens@16: [self _stream: _reader gotError: [NSError errorWithDomain: NSPOSIXErrorDomain jens@16: code: ETIMEDOUT userInfo: nil]]; jens@0: } jens@0: } jens@0: jens@0: jens@0: - (void) disconnect jens@0: { jens@0: if( _status > kTCP_Closed ) { jens@0: LogTo(TCP,@"%@ disconnecting",self); jens@0: [_writer disconnect]; jens@0: [_reader disconnect]; jens@0: self.status = kTCP_Disconnected; jens@0: } jens@16: [self _stopOpenTimer]; jens@0: } jens@0: jens@0: jens@0: - (void) close jens@0: { jens@0: [self closeWithTimeout: 60.0]; jens@0: } jens@0: jens@0: - (void) closeWithTimeout: (NSTimeInterval)timeout jens@0: { jens@16: [self _stopOpenTimer]; jens@0: if( _status == kTCP_Opening ) { jens@0: LogTo(TCP,@"%@ canceling open",self); jens@0: [self _closed]; jens@0: } else if( _status == kTCP_Open ) { jens@0: LogTo(TCP,@"%@ closing",self); jens@0: self.status = kTCP_Closing; jens@0: [self retain]; jens@18: [self _beginClose]; jens@0: if( ! [self _checkIfClosed] ) { jens@0: if( timeout <= 0.0 ) jens@0: [self disconnect]; jens@0: else if( timeout != INFINITY ) jens@0: [self performSelector: @selector(_closeTimeoutExpired) jens@0: withObject: nil afterDelay: timeout]; jens@0: } jens@0: [self release]; jens@0: } jens@0: } jens@0: jens@0: - (void) _closeTimeoutExpired jens@0: { jens@0: if( _status==kTCP_Closing ) jens@0: [self disconnect]; jens@0: } jens@0: jens@19: - (void) _stopCloseTimer jens@19: { jens@19: [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_closeTimeoutExpired) object: nil]; jens@19: } jens@0: jens@19: - (void) _unclose jens@19: { jens@19: if( _status == kTCP_Closing ) { jens@19: LogTo(TCP,@"%@: _unclose!",self); jens@19: [_reader _unclose]; jens@19: [_writer _unclose]; jens@19: [self _stopCloseTimer]; jens@19: self.status = kTCP_Open; jens@19: } jens@19: } jens@19: jens@19: jens@19: /** Subclasses can override this to customize what happens when -close is called. */ jens@18: - (void) _beginClose jens@18: { jens@18: [_reader close]; jens@18: [_writer close]; jens@18: } jens@18: jens@18: jens@0: - (BOOL) _checkIfClosed jens@0: { jens@0: if( _status == kTCP_Closing && _writer==nil && _reader==nil ) { jens@0: [self _closed]; jens@0: return YES; jens@0: } else jens@0: return NO; jens@0: } jens@0: jens@0: jens@0: // called by my streams when they close (after my -close is called) jens@0: - (void) _closed jens@0: { jens@17: [[self retain] autorelease]; jens@0: if( _status != kTCP_Closed && _status != kTCP_Disconnected ) { jens@0: LogTo(TCP,@"%@ is now closed",self); jens@15: TCPConnectionStatus prevStatus = _status; jens@15: self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected); jens@15: if( prevStatus==kTCP_Opening ) jens@15: [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error]; jens@15: else jens@15: [self tellDelegate: @selector(connectionDidClose:) withObject: nil]; jens@0: } jens@19: [self _stopCloseTimer]; jens@16: [self _stopOpenTimer]; jens@0: [sAllConnections removeObjectIdenticalTo: self]; jens@0: } jens@0: jens@0: jens@0: + (void) closeAllWithTimeout: (NSTimeInterval)timeout jens@0: { jens@0: NSArray *connections = [sAllConnections copy]; jens@0: for( TCPConnection *conn in connections ) jens@0: [conn closeWithTimeout: timeout]; jens@0: [connections release]; jens@0: } jens@0: jens@0: + (void) waitTillAllClosed jens@0: { jens@0: while( sAllConnections.count ) { jens@0: if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode jens@0: beforeDate: [NSDate distantFuture]] ) jens@0: break; jens@0: } jens@0: } jens@0: jens@0: jens@0: #pragma mark - jens@0: #pragma mark STREAM CALLBACKS: jens@0: jens@0: jens@0: - (void) _streamOpened: (TCPStream*)stream jens@0: { jens@7: if( ! _address ) jens@7: self.address = stream.peerAddress; jens@0: if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) { jens@7: LogTo(TCP,@"%@ opened; address=%@",self,_address); jens@16: [self _stopOpenTimer]; jens@0: self.status = kTCP_Open; jens@0: [self tellDelegate: @selector(connectionDidOpen:) withObject: nil]; jens@0: } jens@0: } jens@0: jens@0: jens@0: - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream jens@0: { jens@0: BOOL allow = YES; jens@0: if( ! _checkedPeerCert ) { jens@0: @try{ jens@0: _checkedPeerCert = YES; jens@0: if( stream.securityLevel != nil ) { jens@0: NSArray *certs = stream.peerSSLCerts; jens@0: if( ! certs && ! _isIncoming ) jens@0: allow = NO; // Server MUST have a cert! jens@0: else { jens@0: SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL; jens@26: LogTo(TCP,@"%@: Peer cert = %@",self,[TCPEndpoint describeCert: cert]); jens@0: if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] ) jens@0: allow = [_delegate connection: self authorizeSSLPeer: cert]; jens@0: } jens@0: } jens@0: }@catch( NSException *x ) { jens@0: MYReportException(x,@"TCPConnection _streamPeerCertAvailable"); jens@0: _checkedPeerCert = NO; jens@0: allow = NO; jens@0: } jens@0: if( ! allow ) jens@0: [self _stream: stream jens@0: gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain jens@0: code: errSSLClosedAbort jens@0: userInfo: nil]]; jens@0: } jens@0: return allow; jens@0: } jens@0: jens@0: jens@0: - (void) _stream: (TCPStream*)stream gotError: (NSError*)error jens@0: { jens@0: LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class); jens@0: Assert(error); jens@17: [[self retain] autorelease]; jens@0: setObj(&_error,error); jens@0: [_reader disconnect]; jens@0: [_writer disconnect]; jens@0: [self _closed]; jens@0: } jens@0: jens@0: - (void) _streamGotEOF: (TCPStream*)stream jens@0: { jens@0: LogTo(TCP,@"%@ got EOF on %@",self,stream); jens@18: [stream disconnect]; jens@0: if( _status == kTCP_Closing ) { jens@18: [self _streamCanClose: stream]; jens@0: [self _checkIfClosed]; jens@0: } else { jens@0: [self _stream: stream jens@0: gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]]; jens@0: } jens@0: } jens@0: jens@0: jens@18: // Called as soon as a stream is ready to close, after its -close method has been called. jens@18: - (void) _streamCanClose: (TCPStream*)stream jens@18: { jens@18: if( ! _reader.isActive && !_writer.isActive ) { jens@18: LogTo(TCPVerbose,@"Both streams are ready to close now!"); jens@18: [_reader disconnect]; jens@18: [_writer disconnect]; jens@18: } jens@18: } jens@18: jens@18: jens@0: // Called after I called -close on a stream and it finished closing: jens@18: - (void) _streamDisconnected: (TCPStream*)stream jens@0: { jens@18: LogTo(TCP,@"%@: disconnected %@",self,stream); jens@0: if( stream == _reader ) jens@0: setObj(&_reader,nil); jens@0: else if( stream == _writer ) jens@0: setObj(&_writer,nil); jens@18: else jens@18: return; jens@0: if( !_reader.isOpen && !_writer.isOpen ) jens@0: [self _closed]; jens@0: } jens@0: jens@0: jens@0: @end jens@0: jens@0: jens@0: /* jens@0: Copyright (c) 2008, Jens Alfke . All rights reserved. jens@0: jens@0: Redistribution and use in source and binary forms, with or without modification, are permitted jens@0: provided that the following conditions are met: jens@0: jens@0: * Redistributions of source code must retain the above copyright notice, this list of conditions jens@0: and the following disclaimer. jens@0: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions jens@0: and the following disclaimer in the documentation and/or other materials provided with the jens@0: distribution. jens@0: jens@0: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR jens@0: IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND jens@0: FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- jens@0: BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES jens@0: (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR jens@0: PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN jens@0: CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF jens@0: THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. jens@0: */