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