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