TCP/TCPConnection.m
author Dan Preston <danpreston@codechemistry.com>
Tue May 05 15:40:36 2009 -0700 (2009-05-05)
changeset 41 0fa1fcf07d87
parent 33 a9c59b0acbbc
child 47 60f2b46d9a3b
permissions -rw-r--r--
Fixed a small leak of an NSString object.
     1 //
     2 //  TCPConnection.m
     3 //  MYNetwork
     4 //
     5 //  Created by Jens Alfke on 5/18/08.
     6 //  Copyright 2008 Jens Alfke. All rights reserved.
     7 //
     8 
     9 #import "TCP_Internal.h"
    10 #import "IPAddress.h"
    11 #import "MYBonjourService.h"
    12 
    13 #import "Logging.h"
    14 #import "Test.h"
    15 #import "ExceptionUtils.h"
    16 
    17 
    18 #if TARGET_OS_IPHONE && !defined(__SEC_TYPES__)
    19 // SecureTransport.h is missing on iPhone, with its SSL constants:
    20 enum{
    21     errSSLClosedAbort 			= -9806,	/* connection closed via error */
    22 };
    23 #endif
    24 
    25 
    26 
    27 NSString* const TCPErrorDomain = @"TCP";
    28 
    29 
    30 @interface TCPConnection ()
    31 @property TCPConnectionStatus status;
    32 @property (retain) IPAddress *address;
    33 - (BOOL) _checkIfClosed;
    34 - (void) _closed;
    35 @end
    36 
    37 
    38 @implementation TCPConnection
    39 
    40 
    41 static NSMutableArray *sAllConnections;
    42 
    43 
    44 - (Class) readerClass   {return [TCPReader class];}
    45 - (Class) writerClass   {return [TCPWriter class];}
    46 
    47 
    48 - (id) _initWithAddress: (IPAddress*)address
    49             inputStream: (NSInputStream*)input
    50            outputStream: (NSOutputStream*)output
    51 {
    52     self = [super init];
    53     if (self != nil) {
    54         if( !input || !output ) {
    55             LogTo(TCP,@"Failed to create %@: addr=%@, in=%@, out=%@",
    56                   self.class,address,input,output);
    57             [self release];
    58             return nil;
    59         }
    60         _address = [address copy];
    61         _reader = [[[self readerClass] alloc] initWithConnection: self stream: input];
    62         _writer = [[[self writerClass] alloc] initWithConnection: self stream: output];
    63         LogTo(TCP,@"%@ initialized, address=%@",self,address);
    64     }
    65     return self;
    66 }
    67 
    68 
    69 
    70 - (id) initToAddress: (IPAddress*)address
    71 {
    72     NSInputStream *input = nil;
    73     NSOutputStream *output = nil;
    74 #if TARGET_OS_IPHONE
    75     // +getStreamsToHost: is missing for some stupid reason on iPhone. Grrrrrrrrrr.
    76     CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)address.hostname, address.port,
    77                                        (CFReadStreamRef*)&input, (CFWriteStreamRef*)&output);
    78     if( input )  [NSMakeCollectable(input) autorelease];
    79     if( output ) [NSMakeCollectable(output) autorelease];
    80 #else
    81     [NSStream getStreamsToHost: [NSHost hostWithAddress: address.ipv4name]
    82                           port: address.port 
    83                    inputStream: &input 
    84                   outputStream: &output];
    85 #endif
    86     return [self _initWithAddress: address inputStream: input outputStream: output];
    87 }
    88 
    89 - (id) initToNetService: (NSNetService*)service
    90 {
    91     IPAddress *address = nil;
    92     NSInputStream *input;
    93     NSOutputStream *output;
    94     if( [service getInputStream: &input outputStream: &output] ) {
    95         NSArray *addresses = service.addresses;
    96         if( addresses.count > 0 )
    97             address = [[[IPAddress alloc] initWithSockAddr: [[addresses objectAtIndex: 0] bytes]] autorelease];
    98     } else {
    99         input = nil;
   100         output = nil;
   101     }
   102     return [self _initWithAddress: address inputStream: input outputStream: output];
   103 }
   104 
   105 - (id) initToBonjourService: (MYBonjourService*)service;
   106 {
   107     NSNetService *netService = [[NSNetService alloc] initWithDomain: service.domain
   108                                                                type: service.type name: service.name];
   109     self = [self initToNetService: netService];
   110     [service release];
   111     return self;
   112 }
   113 
   114 
   115 - (id) initIncomingFromSocket: (CFSocketNativeHandle)socket
   116                      listener: (TCPListener*)listener
   117 {
   118     CFReadStreamRef readStream = NULL;
   119     CFWriteStreamRef writeStream = NULL;
   120     CFStreamCreatePairWithSocket(kCFAllocatorDefault, socket, &readStream, &writeStream);
   121 	if( readStream )  [NSMakeCollectable(readStream) autorelease];
   122     if( writeStream ) [NSMakeCollectable(writeStream) autorelease];
   123 	
   124     self = [self _initWithAddress: [IPAddress addressOfSocket: socket] 
   125                       inputStream: (NSInputStream*)readStream
   126                      outputStream: (NSOutputStream*)writeStream];
   127     if( self ) {
   128         _isIncoming = YES;
   129         _server = [listener retain];
   130         CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
   131         CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
   132     }
   133     return self;
   134 }    
   135 
   136 
   137 - (void) dealloc
   138 {
   139     LogTo(TCP,@"DEALLOC %@",self);
   140     [_reader release];
   141     [_writer release];
   142     [_address release];
   143     [super dealloc];
   144 }
   145 
   146 
   147 - (NSString*) description
   148 {
   149     return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address);
   150 }
   151 
   152 
   153 @synthesize address=_address, isIncoming=_isIncoming, status=_status, delegate=_delegate,
   154             reader=_reader, writer=_writer, server=_server, openTimeout=_openTimeout;
   155 
   156 
   157 - (NSError*) error
   158 {
   159     return _error;
   160 }
   161 
   162 
   163 - (NSString*) actualSecurityLevel
   164 {
   165     return _reader.securityLevel;
   166 
   167 }
   168 
   169 - (NSArray*) peerSSLCerts
   170 {
   171     return _reader.peerSSLCerts ?: _writer.peerSSLCerts;
   172 }
   173 
   174 
   175 - (void) _setStreamProperty: (id)value forKey: (NSString*)key
   176 {
   177     [_reader setProperty: value forKey: (CFStringRef)key];
   178     [_writer setProperty: value forKey: (CFStringRef)key];
   179 }
   180 
   181 
   182 #pragma mark -
   183 #pragma mark OPENING / CLOSING:
   184 
   185 
   186 - (void) open
   187 {
   188     if( _status<=kTCP_Closed && _reader ) {
   189         _reader.SSLProperties = _sslProperties;
   190         _writer.SSLProperties = _sslProperties;
   191         [_reader open];
   192         [_writer open];
   193         if( ! [sAllConnections my_containsObjectIdenticalTo: self] )
   194             [sAllConnections addObject: self];
   195         self.status = kTCP_Opening;
   196         if( _openTimeout > 0 )
   197             [self performSelector: @selector(_openTimeoutExpired) withObject: nil afterDelay: _openTimeout];
   198     }
   199 }
   200 
   201 - (void) _stopOpenTimer
   202 {
   203     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_openTimeoutExpired) object: nil];
   204 }
   205 
   206 - (void) _openTimeoutExpired
   207 {
   208     if( _status == kTCP_Opening ) {
   209         LogTo(TCP,@"%@: timed out waiting to open",self);
   210         [self _stream: _reader gotError: [NSError errorWithDomain: NSPOSIXErrorDomain
   211                                                              code: ETIMEDOUT userInfo: nil]];
   212     }
   213 }
   214 
   215 
   216 - (void) disconnect
   217 {
   218     if( _status > kTCP_Closed ) {
   219         LogTo(TCP,@"%@ disconnecting",self);
   220         [_writer disconnect];
   221         [_reader disconnect];
   222         self.status = kTCP_Disconnected;
   223     }
   224     [self _stopOpenTimer];
   225 }
   226 
   227 
   228 - (void) close
   229 {
   230     [self closeWithTimeout: 60.0];
   231 }
   232 
   233 - (void) closeWithTimeout: (NSTimeInterval)timeout
   234 {
   235     [self _stopOpenTimer];
   236     if( _status == kTCP_Opening ) {
   237         LogTo(TCP,@"%@ canceling open",self);
   238         [self _closed];
   239     } else if( _status == kTCP_Open ) {
   240         LogTo(TCP,@"%@ closing",self);
   241         self.status = kTCP_Closing;
   242         [self retain];
   243         [self _beginClose];
   244         if( ! [self _checkIfClosed] ) {
   245             if( timeout <= 0.0 )
   246                 [self disconnect];
   247             else if( timeout != INFINITY )
   248                 [self performSelector: @selector(_closeTimeoutExpired)
   249                            withObject: nil afterDelay: timeout];
   250         }
   251         [self release];
   252     }
   253 }
   254 
   255 - (void) _closeTimeoutExpired
   256 {
   257     if( _status==kTCP_Closing )
   258         [self disconnect];
   259 }
   260 
   261 - (void) _stopCloseTimer
   262 {
   263     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_closeTimeoutExpired) object: nil];
   264 }
   265 
   266 - (void) _unclose
   267 {
   268     if( _status == kTCP_Closing ) {
   269         LogTo(TCP,@"%@: _unclose!",self);
   270         [_reader _unclose];
   271         [_writer _unclose];
   272         [self _stopCloseTimer];
   273         self.status = kTCP_Open;
   274     }
   275 }
   276 
   277 
   278 /** Subclasses can override this to customize what happens when -close is called. */
   279 - (void) _beginClose
   280 {
   281     [_reader close];
   282     [_writer close];
   283 }
   284 
   285 
   286 - (BOOL) _checkIfClosed
   287 {
   288     if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
   289         [self _closed];
   290         return YES;
   291     } else
   292         return NO;
   293 }
   294 
   295 
   296 // called by my streams when they close (after my -close is called)
   297 - (void) _closed
   298 {
   299     [[self retain] autorelease];
   300     if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
   301         LogTo(TCP,@"%@ is now closed",self);
   302         TCPConnectionStatus prevStatus = _status;
   303         self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
   304         if( prevStatus==kTCP_Opening )
   305             [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error];
   306         else
   307             [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
   308     }
   309     [self _stopCloseTimer];
   310     [self _stopOpenTimer];
   311     [sAllConnections removeObjectIdenticalTo: self];
   312 }
   313 
   314 
   315 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
   316 {
   317     NSArray *connections = [sAllConnections copy];
   318     for( TCPConnection *conn in connections )
   319         [conn closeWithTimeout: timeout];
   320     [connections release];
   321 }
   322 
   323 + (void) waitTillAllClosed
   324 {
   325     while( sAllConnections.count ) {
   326         if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
   327                                        beforeDate: [NSDate distantFuture]] )
   328             break;
   329     }
   330 }
   331 
   332 
   333 #pragma mark -
   334 #pragma mark STREAM CALLBACKS:
   335 
   336 
   337 - (void) _streamOpened: (TCPStream*)stream
   338 {
   339     if( ! _address )
   340         self.address = stream.peerAddress;
   341     if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
   342         LogTo(TCP,@"%@ opened; address=%@",self,_address);
   343         [self _stopOpenTimer];
   344         self.status = kTCP_Open;
   345         [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
   346     }
   347 }
   348 
   349 
   350 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
   351 {
   352     BOOL allow = YES;
   353     if( ! _checkedPeerCert ) {
   354         @try{
   355             _checkedPeerCert = YES;
   356             if( stream.securityLevel != nil ) {
   357                 NSArray *certs = stream.peerSSLCerts;
   358                 if( ! certs && ! _isIncoming )
   359                     allow = NO; // Server MUST have a cert!
   360                 else {
   361                     SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
   362                     LogTo(TCP,@"%@: Peer cert = %@",self,[TCPEndpoint describeCert: cert]);
   363                     if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
   364                         allow = [_delegate connection: self authorizeSSLPeer: cert];
   365                 }
   366             }
   367         }@catch( NSException *x ) {
   368             MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
   369             _checkedPeerCert = NO;
   370             allow = NO;
   371         }
   372         if( ! allow )
   373             [self _stream: stream 
   374                  gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
   375                                                code: errSSLClosedAbort
   376                                            userInfo: nil]];
   377     }
   378     return allow;
   379 }
   380 
   381 
   382 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
   383 {
   384     LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
   385     Assert(error);
   386     [[self retain] autorelease];
   387     setObj(&_error,error);
   388     [_reader disconnect];
   389     [_writer disconnect];
   390     [self _closed];
   391 }
   392 
   393 - (void) _streamGotEOF: (TCPStream*)stream
   394 {
   395     LogTo(TCP,@"%@ got EOF on %@",self,stream);
   396     [stream disconnect];
   397     if( _status == kTCP_Closing ) {
   398         [self _streamCanClose: stream];
   399         [self _checkIfClosed];
   400     } else {
   401         [self _stream: stream 
   402              gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
   403     }
   404 }
   405 
   406 
   407 // Called as soon as a stream is ready to close, after its -close method has been called.
   408 - (void) _streamCanClose: (TCPStream*)stream
   409 {
   410     if( ! _reader.isActive && !_writer.isActive ) {
   411         LogTo(TCPVerbose,@"Both streams are ready to close now!");
   412         [_reader disconnect];
   413         [_writer disconnect];
   414     }
   415 }
   416 
   417 
   418 // Called after I called -close on a stream and it finished closing:
   419 - (void) _streamDisconnected: (TCPStream*)stream
   420 {
   421     LogTo(TCP,@"%@: disconnected %@",self,stream);
   422     if( stream == _reader )
   423         setObj(&_reader,nil);
   424     else if( stream == _writer )
   425         setObj(&_writer,nil);
   426     else
   427         return;
   428     if( !_reader.isOpen && !_writer.isOpen )
   429         [self _closed];
   430 }
   431 
   432 
   433 @end
   434 
   435 
   436 /*
   437  Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   438  
   439  Redistribution and use in source and binary forms, with or without modification, are permitted
   440  provided that the following conditions are met:
   441  
   442  * Redistributions of source code must retain the above copyright notice, this list of conditions
   443  and the following disclaimer.
   444  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   445  and the following disclaimer in the documentation and/or other materials provided with the
   446  distribution.
   447  
   448  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   449  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   450  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   451  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   452  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   453   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   454  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   455  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   456  */