TCP/TCPConnection.m
author Jens Alfke <jens@mooseyard.com>
Wed Apr 29 13:57:10 2009 -0700 (2009-04-29)
changeset 32 b3254a2f6d6c
parent 26 cb9cdf247239
child 33 a9c59b0acbbc
permissions -rw-r--r--
Tweaked docs
     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 && !defined(__SEC_TYPES__)
    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*)CFMakeCollectable(readStream)
   115                      outputStream: (NSOutputStream*)CFMakeCollectable(writeStream)];
   116     if( self ) {
   117         _isIncoming = YES;
   118         _server = [listener retain];
   119         CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
   120         CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
   121     }
   122     if(readStream) CFRelease(readStream);
   123     if(writeStream) CFRelease(writeStream);
   124     return self;
   125 }    
   126 
   127 
   128 - (void) dealloc
   129 {
   130     LogTo(TCP,@"DEALLOC %@",self);
   131     [_reader release];
   132     [_writer release];
   133     [_address release];
   134     [super dealloc];
   135 }
   136 
   137 
   138 - (NSString*) description
   139 {
   140     return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address);
   141 }
   142 
   143 
   144 @synthesize address=_address, isIncoming=_isIncoming, status=_status, delegate=_delegate,
   145             reader=_reader, writer=_writer, server=_server, openTimeout=_openTimeout;
   146 
   147 
   148 - (NSError*) error
   149 {
   150     return _error;
   151 }
   152 
   153 
   154 - (NSString*) actualSecurityLevel
   155 {
   156     return _reader.securityLevel;
   157 
   158 }
   159 
   160 - (NSArray*) peerSSLCerts
   161 {
   162     return _reader.peerSSLCerts ?: _writer.peerSSLCerts;
   163 }
   164 
   165 
   166 - (void) _setStreamProperty: (id)value forKey: (NSString*)key
   167 {
   168     [_reader setProperty: value forKey: (CFStringRef)key];
   169     [_writer setProperty: value forKey: (CFStringRef)key];
   170 }
   171 
   172 
   173 #pragma mark -
   174 #pragma mark OPENING / CLOSING:
   175 
   176 
   177 - (void) open
   178 {
   179     if( _status<=kTCP_Closed && _reader ) {
   180         _reader.SSLProperties = _sslProperties;
   181         _writer.SSLProperties = _sslProperties;
   182         [_reader open];
   183         [_writer open];
   184         if( ! [sAllConnections my_containsObjectIdenticalTo: self] )
   185             [sAllConnections addObject: self];
   186         self.status = kTCP_Opening;
   187         if( _openTimeout > 0 )
   188             [self performSelector: @selector(_openTimeoutExpired) withObject: nil afterDelay: _openTimeout];
   189     }
   190 }
   191 
   192 - (void) _stopOpenTimer
   193 {
   194     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_openTimeoutExpired) object: nil];
   195 }
   196 
   197 - (void) _openTimeoutExpired
   198 {
   199     if( _status == kTCP_Opening ) {
   200         LogTo(TCP,@"%@: timed out waiting to open",self);
   201         [self _stream: _reader gotError: [NSError errorWithDomain: NSPOSIXErrorDomain
   202                                                              code: ETIMEDOUT userInfo: nil]];
   203     }
   204 }
   205 
   206 
   207 - (void) disconnect
   208 {
   209     if( _status > kTCP_Closed ) {
   210         LogTo(TCP,@"%@ disconnecting",self);
   211         [_writer disconnect];
   212         [_reader disconnect];
   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         [self _beginClose];
   235         if( ! [self _checkIfClosed] ) {
   236             if( timeout <= 0.0 )
   237                 [self disconnect];
   238             else if( timeout != INFINITY )
   239                 [self performSelector: @selector(_closeTimeoutExpired)
   240                            withObject: nil afterDelay: timeout];
   241         }
   242         [self release];
   243     }
   244 }
   245 
   246 - (void) _closeTimeoutExpired
   247 {
   248     if( _status==kTCP_Closing )
   249         [self disconnect];
   250 }
   251 
   252 - (void) _stopCloseTimer
   253 {
   254     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_closeTimeoutExpired) object: nil];
   255 }
   256 
   257 - (void) _unclose
   258 {
   259     if( _status == kTCP_Closing ) {
   260         LogTo(TCP,@"%@: _unclose!",self);
   261         [_reader _unclose];
   262         [_writer _unclose];
   263         [self _stopCloseTimer];
   264         self.status = kTCP_Open;
   265     }
   266 }
   267 
   268 
   269 /** Subclasses can override this to customize what happens when -close is called. */
   270 - (void) _beginClose
   271 {
   272     [_reader close];
   273     [_writer close];
   274 }
   275 
   276 
   277 - (BOOL) _checkIfClosed
   278 {
   279     if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
   280         [self _closed];
   281         return YES;
   282     } else
   283         return NO;
   284 }
   285 
   286 
   287 // called by my streams when they close (after my -close is called)
   288 - (void) _closed
   289 {
   290     [[self retain] autorelease];
   291     if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
   292         LogTo(TCP,@"%@ is now closed",self);
   293         TCPConnectionStatus prevStatus = _status;
   294         self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
   295         if( prevStatus==kTCP_Opening )
   296             [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error];
   297         else
   298             [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
   299     }
   300     [self _stopCloseTimer];
   301     [self _stopOpenTimer];
   302     [sAllConnections removeObjectIdenticalTo: self];
   303 }
   304 
   305 
   306 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
   307 {
   308     NSArray *connections = [sAllConnections copy];
   309     for( TCPConnection *conn in connections )
   310         [conn closeWithTimeout: timeout];
   311     [connections release];
   312 }
   313 
   314 + (void) waitTillAllClosed
   315 {
   316     while( sAllConnections.count ) {
   317         if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
   318                                        beforeDate: [NSDate distantFuture]] )
   319             break;
   320     }
   321 }
   322 
   323 
   324 #pragma mark -
   325 #pragma mark STREAM CALLBACKS:
   326 
   327 
   328 - (void) _streamOpened: (TCPStream*)stream
   329 {
   330     if( ! _address )
   331         self.address = stream.peerAddress;
   332     if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
   333         LogTo(TCP,@"%@ opened; address=%@",self,_address);
   334         [self _stopOpenTimer];
   335         self.status = kTCP_Open;
   336         [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
   337     }
   338 }
   339 
   340 
   341 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
   342 {
   343     BOOL allow = YES;
   344     if( ! _checkedPeerCert ) {
   345         @try{
   346             _checkedPeerCert = YES;
   347             if( stream.securityLevel != nil ) {
   348                 NSArray *certs = stream.peerSSLCerts;
   349                 if( ! certs && ! _isIncoming )
   350                     allow = NO; // Server MUST have a cert!
   351                 else {
   352                     SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
   353                     LogTo(TCP,@"%@: Peer cert = %@",self,[TCPEndpoint describeCert: cert]);
   354                     if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
   355                         allow = [_delegate connection: self authorizeSSLPeer: cert];
   356                 }
   357             }
   358         }@catch( NSException *x ) {
   359             MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
   360             _checkedPeerCert = NO;
   361             allow = NO;
   362         }
   363         if( ! allow )
   364             [self _stream: stream 
   365                  gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
   366                                                code: errSSLClosedAbort
   367                                            userInfo: nil]];
   368     }
   369     return allow;
   370 }
   371 
   372 
   373 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
   374 {
   375     LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
   376     Assert(error);
   377     [[self retain] autorelease];
   378     setObj(&_error,error);
   379     [_reader disconnect];
   380     [_writer disconnect];
   381     [self _closed];
   382 }
   383 
   384 - (void) _streamGotEOF: (TCPStream*)stream
   385 {
   386     LogTo(TCP,@"%@ got EOF on %@",self,stream);
   387     [stream disconnect];
   388     if( _status == kTCP_Closing ) {
   389         [self _streamCanClose: stream];
   390         [self _checkIfClosed];
   391     } else {
   392         [self _stream: stream 
   393              gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
   394     }
   395 }
   396 
   397 
   398 // Called as soon as a stream is ready to close, after its -close method has been called.
   399 - (void) _streamCanClose: (TCPStream*)stream
   400 {
   401     if( ! _reader.isActive && !_writer.isActive ) {
   402         LogTo(TCPVerbose,@"Both streams are ready to close now!");
   403         [_reader disconnect];
   404         [_writer disconnect];
   405     }
   406 }
   407 
   408 
   409 // Called after I called -close on a stream and it finished closing:
   410 - (void) _streamDisconnected: (TCPStream*)stream
   411 {
   412     LogTo(TCP,@"%@: disconnected %@",self,stream);
   413     if( stream == _reader )
   414         setObj(&_reader,nil);
   415     else if( stream == _writer )
   416         setObj(&_writer,nil);
   417     else
   418         return;
   419     if( !_reader.isOpen && !_writer.isOpen )
   420         [self _closed];
   421 }
   422 
   423 
   424 @end
   425 
   426 
   427 /*
   428  Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   429  
   430  Redistribution and use in source and binary forms, with or without modification, are permitted
   431  provided that the following conditions are met:
   432  
   433  * Redistributions of source code must retain the above copyright notice, this list of conditions
   434  and the following disclaimer.
   435  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   436  and the following disclaimer in the documentation and/or other materials provided with the
   437  distribution.
   438  
   439  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   440  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   441  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   442  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   443  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   444   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   445  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   446  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   447  */