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