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