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