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