Work around crash-on-close by temporarily retaining the connection object.
5 // Created by Jens Alfke on 5/18/08.
6 // Copyright 2008 Jens Alfke. All rights reserved.
9 #import "TCP_Internal.h"
14 #import "ExceptionUtils.h"
18 // SecureTransport.h is missing on iPhone, with its SSL constants:
20 errSSLClosedAbort = -9806, /* connection closed via error */
26 NSString* const TCPErrorDomain = @"TCP";
29 @interface TCPConnection ()
30 @property TCPConnectionStatus status;
31 @property (retain) IPAddress *address;
32 - (BOOL) _checkIfClosed;
37 @implementation TCPConnection
40 static NSMutableArray *sAllConnections;
43 - (Class) readerClass {return [TCPReader class];}
44 - (Class) writerClass {return [TCPWriter class];}
47 - (id) _initWithAddress: (IPAddress*)address
48 inputStream: (NSInputStream*)input
49 outputStream: (NSOutputStream*)output
53 if( !input || !output ) {
54 LogTo(TCP,@"Failed to create %@: addr=%@, in=%@, out=%@",
55 self.class,address,input,output);
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);
69 - (id) initToAddress: (IPAddress*)address
71 NSInputStream *input = nil;
72 NSOutputStream *output = nil;
74 // +getStreamsToHost: is missing for some stupid reason on iPhone. Grrrrrrrrrr.
75 CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)address.hostname, address.port,
76 (CFReadStreamRef*)&input, (CFWriteStreamRef*)&output);
78 [(id)CFMakeCollectable(input) autorelease];
80 [(id)CFMakeCollectable(output) autorelease];
82 [NSStream getStreamsToHost: [NSHost hostWithAddress: address.ipv4name]
85 outputStream: &output];
87 return [self _initWithAddress: address inputStream: input outputStream: output];
90 - (id) initToNetService: (NSNetService*)service
92 IPAddress *address = nil;
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];
103 return [self _initWithAddress: address inputStream: input outputStream: output];
107 - (id) initIncomingFromSocket: (CFSocketNativeHandle)socket
108 listener: (TCPListener*)listener
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];
118 _server = [listener retain];
119 CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
120 CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
128 LogTo(TCP,@"DEALLOC %@",self);
136 - (NSString*) description
138 return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address);
142 @synthesize address=_address, isIncoming=_isIncoming, status=_status, delegate=_delegate,
143 reader=_reader, writer=_writer, server=_server, openTimeout=_openTimeout;
152 - (NSString*) actualSecurityLevel
154 return _reader.securityLevel;
158 - (NSArray*) peerSSLCerts
160 return _reader.peerSSLCerts ?: _writer.peerSSLCerts;
164 - (void) _setStreamProperty: (id)value forKey: (NSString*)key
166 [_reader setProperty: value forKey: (CFStringRef)key];
167 [_writer setProperty: value forKey: (CFStringRef)key];
172 #pragma mark OPENING / CLOSING:
177 if( _status<=kTCP_Closed && _reader ) {
178 _reader.SSLProperties = _sslProperties;
179 _writer.SSLProperties = _sslProperties;
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];
190 - (void) _stopOpenTimer
192 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_openTimeoutExpired) object: nil];
195 - (void) _openTimeoutExpired
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]];
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;
215 [self _stopOpenTimer];
221 [self closeWithTimeout: 60.0];
224 - (void) closeWithTimeout: (NSTimeInterval)timeout
226 [self _stopOpenTimer];
227 if( _status == kTCP_Opening ) {
228 LogTo(TCP,@"%@ canceling open",self);
230 } else if( _status == kTCP_Open ) {
231 LogTo(TCP,@"%@ closing",self);
232 self.status = kTCP_Closing;
236 if( ! [self _checkIfClosed] ) {
239 else if( timeout != INFINITY )
240 [self performSelector: @selector(_closeTimeoutExpired)
241 withObject: nil afterDelay: timeout];
247 - (void) _closeTimeoutExpired
249 if( _status==kTCP_Closing )
254 - (BOOL) _checkIfClosed
256 if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
264 // called by my streams when they close (after my -close is called)
267 [[self retain] autorelease];
268 if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
269 LogTo(TCP,@"%@ is now closed",self);
270 TCPConnectionStatus prevStatus = _status;
271 self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
272 if( prevStatus==kTCP_Opening )
273 [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error];
275 [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
277 [NSObject cancelPreviousPerformRequestsWithTarget: self
278 selector: @selector(_closeTimeoutExpired)
280 [self _stopOpenTimer];
281 [sAllConnections removeObjectIdenticalTo: self];
285 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
287 NSArray *connections = [sAllConnections copy];
288 for( TCPConnection *conn in connections )
289 [conn closeWithTimeout: timeout];
290 [connections release];
293 + (void) waitTillAllClosed
295 while( sAllConnections.count ) {
296 if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
297 beforeDate: [NSDate distantFuture]] )
304 #pragma mark STREAM CALLBACKS:
307 - (void) _streamOpened: (TCPStream*)stream
310 self.address = stream.peerAddress;
311 if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
312 LogTo(TCP,@"%@ opened; address=%@",self,_address);
313 [self _stopOpenTimer];
314 self.status = kTCP_Open;
315 [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
320 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
323 if( ! _checkedPeerCert ) {
325 _checkedPeerCert = YES;
326 if( stream.securityLevel != nil ) {
327 NSArray *certs = stream.peerSSLCerts;
328 if( ! certs && ! _isIncoming )
329 allow = NO; // Server MUST have a cert!
331 SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
332 LogTo(TCP,@"%@: Peer cert = %@",self,cert);
333 if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
334 allow = [_delegate connection: self authorizeSSLPeer: cert];
337 }@catch( NSException *x ) {
338 MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
339 _checkedPeerCert = NO;
343 [self _stream: stream
344 gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
345 code: errSSLClosedAbort
352 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
354 LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
356 [[self retain] autorelease];
357 setObj(&_error,error);
358 [_reader disconnect];
359 setObj(&_reader,nil);
360 [_writer disconnect];
361 setObj(&_writer,nil);
365 - (void) _streamGotEOF: (TCPStream*)stream
367 LogTo(TCP,@"%@ got EOF on %@",self,stream);
368 if( stream == _reader ) {
369 setObj(&_reader,nil);
370 // This is the expected way for he peer to initiate closing the connection.
371 if( _status==kTCP_Open ) {
372 [self closeWithTimeout: INFINITY];
375 } else if( stream == _writer ) {
376 setObj(&_writer,nil);
379 if( _status == kTCP_Closing ) {
380 [self _checkIfClosed];
382 [self _stream: stream
383 gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
388 // Called after I called -close on a stream and it finished closing:
389 - (void) _streamClosed: (TCPStream*)stream
391 LogTo(TCP,@"%@ finished closing %@",self,stream);
392 if( stream == _reader )
393 setObj(&_reader,nil);
394 else if( stream == _writer )
395 setObj(&_writer,nil);
396 if( !_reader.isOpen && !_writer.isOpen )
405 Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
407 Redistribution and use in source and binary forms, with or without modification, are permitted
408 provided that the following conditions are met:
410 * Redistributions of source code must retain the above copyright notice, this list of conditions
411 and the following disclaimer.
412 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
413 and the following disclaimer in the documentation and/or other materials provided with the
416 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
417 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
418 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
419 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
420 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
421 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
422 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
423 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.