TCP/TCPConnection.m
author Jens Alfke <jens@mooseyard.com>
Sat May 24 13:26:02 2008 -0700 (2008-05-24)
changeset 1 8267d5c429c4
parent 0 9d67172bb323
child 7 5936db2c1987
permissions -rw-r--r--
Added #imports of utility headers, so source files will compile without requiring a custom prefix (MYUtilities.pch.)
     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 NSString* const TCPErrorDomain = @"TCP";
    18 
    19 
    20 @interface TCPConnection ()
    21 @property TCPConnectionStatus status;
    22 - (BOOL) _checkIfClosed;
    23 - (void) _closed;
    24 @end
    25 
    26 
    27 @implementation TCPConnection
    28 
    29 
    30 static NSMutableArray *sAllConnections;
    31 
    32 
    33 - (Class) readerClass   {return [TCPReader class];}
    34 - (Class) writerClass   {return [TCPWriter class];}
    35 
    36 
    37 - (id) _initWithAddress: (IPAddress*)address
    38             inputStream: (NSInputStream*)input
    39            outputStream: (NSOutputStream*)output
    40 {
    41     self = [super init];
    42     if (self != nil) {
    43         if( !address || !input || !output ) {
    44             LogTo(TCP,@"Failed to create %@: addr=%@, in=%@, out=%@",
    45                   self.class,address,input,output);
    46             [self release];
    47             return nil;
    48         }
    49         _address = [address copy];
    50         _reader = [[[self readerClass] alloc] initWithConnection: self stream: input];
    51         _writer = [[[self writerClass] alloc] initWithConnection: self stream: output];
    52         LogTo(TCP,@"%@ initialized",self);
    53     }
    54     return self;
    55 }
    56 
    57 
    58 
    59 - (id) initToAddress: (IPAddress*)address
    60            localPort: (UInt16)localPort
    61 {
    62     NSInputStream *input = nil;
    63     NSOutputStream *output = nil;
    64     [NSStream getStreamsToHost: [NSHost hostWithAddress: address.ipv4name]
    65                           port: address.port 
    66                    inputStream: &input 
    67                   outputStream: &output];
    68     return [self _initWithAddress: address inputStream: input outputStream: output];
    69     //FIX: Support localPort!
    70 }
    71 
    72 - (id) initToAddress: (IPAddress*)address
    73 {
    74     return [self initToAddress: address localPort: 0];
    75 }
    76 
    77 
    78 - (id) initIncomingFromSocket: (CFSocketNativeHandle)socket
    79                      listener: (TCPListener*)listener
    80 {
    81     CFReadStreamRef readStream = NULL;
    82     CFWriteStreamRef writeStream = NULL;
    83     CFStreamCreatePairWithSocket(kCFAllocatorDefault, socket, &readStream, &writeStream);
    84     self = [self _initWithAddress: [IPAddress addressOfSocket: socket] 
    85                       inputStream: (NSInputStream*)readStream
    86                      outputStream: (NSOutputStream*)writeStream];
    87     if( self ) {
    88         _isIncoming = YES;
    89         _server = [listener retain];
    90         CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    91         CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    92     }
    93     return self;
    94 }    
    95 
    96 
    97 - (void) dealloc
    98 {
    99     LogTo(TCP,@"DEALLOC %@",self);
   100     [_reader release];
   101     [_writer release];
   102     [_address release];
   103     [super dealloc];
   104 }
   105 
   106 
   107 - (NSString*) description
   108 {
   109     return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address);
   110 }
   111 
   112 
   113 @synthesize address=_address, isIncoming=_isIncoming, status=_status, delegate=_delegate,
   114             reader=_reader, writer=_writer, server=_server;
   115 
   116 
   117 - (NSError*) error
   118 {
   119     return _error;
   120 }
   121 
   122 
   123 - (NSString*) actualSecurityLevel
   124 {
   125     return _reader.securityLevel;
   126 
   127 }
   128 
   129 - (NSArray*) peerSSLCerts
   130 {
   131     return _reader.peerSSLCerts ?: _writer.peerSSLCerts;
   132 }
   133 
   134 
   135 - (void) _setStreamProperty: (id)value forKey: (NSString*)key
   136 {
   137     [_reader setProperty: value forKey: (CFStringRef)key];
   138     [_writer setProperty: value forKey: (CFStringRef)key];
   139 }
   140 
   141 
   142 #pragma mark -
   143 #pragma mark OPENING / CLOSING:
   144 
   145 
   146 - (void) open
   147 {
   148     if( _status<=kTCP_Closed && _reader ) {
   149         _reader.SSLProperties = _sslProperties;
   150         _writer.SSLProperties = _sslProperties;
   151         [_reader open];
   152         [_writer open];
   153         if( ! [sAllConnections my_containsObjectIdenticalTo: self] )
   154             [sAllConnections addObject: self];
   155         self.status = kTCP_Opening;
   156     }
   157 }
   158 
   159 
   160 - (void) disconnect
   161 {
   162     if( _status > kTCP_Closed ) {
   163         LogTo(TCP,@"%@ disconnecting",self);
   164         [_writer disconnect];
   165         setObj(&_writer,nil);
   166         [_reader disconnect];
   167         setObj(&_reader,nil);
   168         self.status = kTCP_Disconnected;
   169     }
   170 }
   171 
   172 
   173 - (void) close
   174 {
   175     [self closeWithTimeout: 60.0];
   176 }
   177 
   178 - (void) closeWithTimeout: (NSTimeInterval)timeout
   179 {
   180     if( _status == kTCP_Opening ) {
   181         LogTo(TCP,@"%@ canceling open",self);
   182         [self _closed];
   183     } else if( _status == kTCP_Open ) {
   184         LogTo(TCP,@"%@ closing",self);
   185         self.status = kTCP_Closing;
   186         [self retain];
   187         [_reader close];
   188         [_writer close];
   189         if( ! [self _checkIfClosed] ) {
   190             if( timeout <= 0.0 )
   191                 [self disconnect];
   192             else if( timeout != INFINITY )
   193                 [self performSelector: @selector(_closeTimeoutExpired)
   194                            withObject: nil afterDelay: timeout];
   195         }
   196         [self release];
   197     }
   198 }
   199 
   200 - (void) _closeTimeoutExpired
   201 {
   202     if( _status==kTCP_Closing )
   203         [self disconnect];
   204 }
   205 
   206 
   207 - (BOOL) _checkIfClosed
   208 {
   209     if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
   210         [self _closed];
   211         return YES;
   212     } else
   213         return NO;
   214 }
   215 
   216 
   217 // called by my streams when they close (after my -close is called)
   218 - (void) _closed
   219 {
   220     if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
   221         LogTo(TCP,@"%@ is now closed",self);
   222         self.status = (_status==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
   223         [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
   224     }
   225     [NSObject cancelPreviousPerformRequestsWithTarget: self
   226                                              selector: @selector(_closeTimeoutExpired)
   227                                                object: nil];
   228     [sAllConnections removeObjectIdenticalTo: self];
   229 }
   230 
   231 
   232 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
   233 {
   234     NSArray *connections = [sAllConnections copy];
   235     for( TCPConnection *conn in connections )
   236         [conn closeWithTimeout: timeout];
   237     [connections release];
   238 }
   239 
   240 + (void) waitTillAllClosed
   241 {
   242     while( sAllConnections.count ) {
   243         if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
   244                                        beforeDate: [NSDate distantFuture]] )
   245             break;
   246     }
   247 }
   248 
   249 
   250 #pragma mark -
   251 #pragma mark STREAM CALLBACKS:
   252 
   253 
   254 - (void) _streamOpened: (TCPStream*)stream
   255 {
   256     if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
   257         LogTo(TCP,@"%@ opened",self);
   258         self.status = kTCP_Open;
   259         [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
   260     }
   261 }
   262 
   263 
   264 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
   265 {
   266     BOOL allow = YES;
   267     if( ! _checkedPeerCert ) {
   268         @try{
   269             _checkedPeerCert = YES;
   270             if( stream.securityLevel != nil ) {
   271                 NSArray *certs = stream.peerSSLCerts;
   272                 if( ! certs && ! _isIncoming )
   273                     allow = NO; // Server MUST have a cert!
   274                 else {
   275                     SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
   276                     LogTo(TCP,@"%@: Peer cert = %@",self,cert);
   277                     if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
   278                         allow = [_delegate connection: self authorizeSSLPeer: cert];
   279                 }
   280             }
   281         }@catch( NSException *x ) {
   282             MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
   283             _checkedPeerCert = NO;
   284             allow = NO;
   285         }
   286         if( ! allow )
   287             [self _stream: stream 
   288                  gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
   289                                                code: errSSLClosedAbort
   290                                            userInfo: nil]];
   291     }
   292     return allow;
   293 }
   294 
   295 
   296 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
   297 {
   298     LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
   299     Assert(error);
   300     setObj(&_error,error);
   301     [_reader disconnect];
   302     setObj(&_reader,nil);
   303     [_writer disconnect];
   304     setObj(&_writer,nil);
   305     [self _closed];
   306 }
   307 
   308 - (void) _streamGotEOF: (TCPStream*)stream
   309 {
   310     LogTo(TCP,@"%@ got EOF on %@",self,stream);
   311     if( stream == _reader ) {
   312         setObj(&_reader,nil);
   313         // This is the expected way for he peer to initiate closing the connection.
   314         if( _status==kTCP_Open ) {
   315             [self closeWithTimeout: INFINITY];
   316             return;
   317         }
   318     } else if( stream == _writer ) {
   319         setObj(&_writer,nil);
   320     }
   321     
   322     if( _status == kTCP_Closing ) {
   323         [self _checkIfClosed];
   324     } else {
   325         [self _stream: stream 
   326              gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
   327     }
   328 }
   329 
   330 
   331 // Called after I called -close on a stream and it finished closing:
   332 - (void) _streamClosed: (TCPStream*)stream
   333 {
   334     LogTo(TCP,@"%@ finished closing %@",self,stream);
   335     if( stream == _reader )
   336         setObj(&_reader,nil);
   337     else if( stream == _writer )
   338         setObj(&_writer,nil);
   339     if( !_reader.isOpen && !_writer.isOpen )
   340         [self _closed];
   341 }
   342 
   343 
   344 @end
   345 
   346 
   347 /*
   348  Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   349  
   350  Redistribution and use in source and binary forms, with or without modification, are permitted
   351  provided that the following conditions are met:
   352  
   353  * Redistributions of source code must retain the above copyright notice, this list of conditions
   354  and the following disclaimer.
   355  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   356  and the following disclaimer in the documentation and/or other materials provided with the
   357  distribution.
   358  
   359  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   360  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   361  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   362  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   363  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   364   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   365  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   366  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   367  */