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