TCP/TCPConnection.m
author Jens Alfke <jens@mooseyard.com>
Mon Jun 23 14:02:31 2008 -0700 (2008-06-23)
changeset 19 16454d63d4c2
parent 18 3be241de1630
child 20 02224e981209
permissions -rw-r--r--
Implemented BLIP 1.1 (explicit 'bye' message)
     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         [_reader disconnect];
   211         self.status = kTCP_Disconnected;
   212     }
   213     [self _stopOpenTimer];
   214 }
   215 
   216 
   217 - (void) close
   218 {
   219     [self closeWithTimeout: 60.0];
   220 }
   221 
   222 - (void) closeWithTimeout: (NSTimeInterval)timeout
   223 {
   224     [self _stopOpenTimer];
   225     if( _status == kTCP_Opening ) {
   226         LogTo(TCP,@"%@ canceling open",self);
   227         [self _closed];
   228     } else if( _status == kTCP_Open ) {
   229         LogTo(TCP,@"%@ closing",self);
   230         self.status = kTCP_Closing;
   231         [self retain];
   232         [self _beginClose];
   233         if( ! [self _checkIfClosed] ) {
   234             if( timeout <= 0.0 )
   235                 [self disconnect];
   236             else if( timeout != INFINITY )
   237                 [self performSelector: @selector(_closeTimeoutExpired)
   238                            withObject: nil afterDelay: timeout];
   239         }
   240         [self release];
   241     }
   242 }
   243 
   244 - (void) _closeTimeoutExpired
   245 {
   246     if( _status==kTCP_Closing )
   247         [self disconnect];
   248 }
   249 
   250 - (void) _stopCloseTimer
   251 {
   252     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_closeTimeoutExpired) object: nil];
   253 }
   254 
   255 - (void) _unclose
   256 {
   257     if( _status == kTCP_Closing ) {
   258         LogTo(TCP,@"%@: _unclose!",self);
   259         [_reader _unclose];
   260         [_writer _unclose];
   261         [self _stopCloseTimer];
   262         self.status = kTCP_Open;
   263     }
   264 }
   265 
   266 
   267 /** Subclasses can override this to customize what happens when -close is called. */
   268 - (void) _beginClose
   269 {
   270     [_reader close];
   271     [_writer close];
   272 }
   273 
   274 
   275 - (BOOL) _checkIfClosed
   276 {
   277     if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
   278         [self _closed];
   279         return YES;
   280     } else
   281         return NO;
   282 }
   283 
   284 
   285 // called by my streams when they close (after my -close is called)
   286 - (void) _closed
   287 {
   288     [[self retain] autorelease];
   289     if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
   290         LogTo(TCP,@"%@ is now closed",self);
   291         TCPConnectionStatus prevStatus = _status;
   292         self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
   293         if( prevStatus==kTCP_Opening )
   294             [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error];
   295         else
   296             [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
   297     }
   298     [self _stopCloseTimer];
   299     [self _stopOpenTimer];
   300     [sAllConnections removeObjectIdenticalTo: self];
   301 }
   302 
   303 
   304 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
   305 {
   306     NSArray *connections = [sAllConnections copy];
   307     for( TCPConnection *conn in connections )
   308         [conn closeWithTimeout: timeout];
   309     [connections release];
   310 }
   311 
   312 + (void) waitTillAllClosed
   313 {
   314     while( sAllConnections.count ) {
   315         if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
   316                                        beforeDate: [NSDate distantFuture]] )
   317             break;
   318     }
   319 }
   320 
   321 
   322 #pragma mark -
   323 #pragma mark STREAM CALLBACKS:
   324 
   325 
   326 - (void) _streamOpened: (TCPStream*)stream
   327 {
   328     if( ! _address )
   329         self.address = stream.peerAddress;
   330     if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
   331         LogTo(TCP,@"%@ opened; address=%@",self,_address);
   332         [self _stopOpenTimer];
   333         self.status = kTCP_Open;
   334         [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
   335     }
   336 }
   337 
   338 
   339 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
   340 {
   341     BOOL allow = YES;
   342     if( ! _checkedPeerCert ) {
   343         @try{
   344             _checkedPeerCert = YES;
   345             if( stream.securityLevel != nil ) {
   346                 NSArray *certs = stream.peerSSLCerts;
   347                 if( ! certs && ! _isIncoming )
   348                     allow = NO; // Server MUST have a cert!
   349                 else {
   350                     SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
   351                     LogTo(TCP,@"%@: Peer cert = %@",self,cert);
   352                     if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
   353                         allow = [_delegate connection: self authorizeSSLPeer: cert];
   354                 }
   355             }
   356         }@catch( NSException *x ) {
   357             MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
   358             _checkedPeerCert = NO;
   359             allow = NO;
   360         }
   361         if( ! allow )
   362             [self _stream: stream 
   363                  gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
   364                                                code: errSSLClosedAbort
   365                                            userInfo: nil]];
   366     }
   367     return allow;
   368 }
   369 
   370 
   371 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
   372 {
   373     LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
   374     Assert(error);
   375     [[self retain] autorelease];
   376     setObj(&_error,error);
   377     [_reader disconnect];
   378     [_writer disconnect];
   379     [self _closed];
   380 }
   381 
   382 - (void) _streamGotEOF: (TCPStream*)stream
   383 {
   384     LogTo(TCP,@"%@ got EOF on %@",self,stream);
   385     [stream disconnect];
   386     if( _status == kTCP_Closing ) {
   387         [self _streamCanClose: stream];
   388         [self _checkIfClosed];
   389     } else {
   390         [self _stream: stream 
   391              gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
   392     }
   393 }
   394 
   395 
   396 // Called as soon as a stream is ready to close, after its -close method has been called.
   397 - (void) _streamCanClose: (TCPStream*)stream
   398 {
   399     if( ! _reader.isActive && !_writer.isActive ) {
   400         LogTo(TCPVerbose,@"Both streams are ready to close now!");
   401         [_reader disconnect];
   402         [_writer disconnect];
   403     }
   404 }
   405 
   406 
   407 // Called after I called -close on a stream and it finished closing:
   408 - (void) _streamDisconnected: (TCPStream*)stream
   409 {
   410     LogTo(TCP,@"%@: disconnected %@",self,stream);
   411     if( stream == _reader )
   412         setObj(&_reader,nil);
   413     else if( stream == _writer )
   414         setObj(&_writer,nil);
   415     else
   416         return;
   417     if( !_reader.isOpen && !_writer.isOpen )
   418         [self _closed];
   419 }
   420 
   421 
   422 @end
   423 
   424 
   425 /*
   426  Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   427  
   428  Redistribution and use in source and binary forms, with or without modification, are permitted
   429  provided that the following conditions are met:
   430  
   431  * Redistributions of source code must retain the above copyright notice, this list of conditions
   432  and the following disclaimer.
   433  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   434  and the following disclaimer in the documentation and/or other materials provided with the
   435  distribution.
   436  
   437  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   438  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   439  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   440  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   441  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   442   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   443  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   444  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   445  */