* 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.
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 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];
274 [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
276 [NSObject cancelPreviousPerformRequestsWithTarget: self
277 selector: @selector(_closeTimeoutExpired)
279 [self _stopOpenTimer];
280 [sAllConnections removeObjectIdenticalTo: self];
284 + (void) closeAllWithTimeout: (NSTimeInterval)timeout
286 NSArray *connections = [sAllConnections copy];
287 for( TCPConnection *conn in connections )
288 [conn closeWithTimeout: timeout];
289 [connections release];
292 + (void) waitTillAllClosed
294 while( sAllConnections.count ) {
295 if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
296 beforeDate: [NSDate distantFuture]] )
303 #pragma mark STREAM CALLBACKS:
306 - (void) _streamOpened: (TCPStream*)stream
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];
319 - (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
322 if( ! _checkedPeerCert ) {
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!
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];
336 }@catch( NSException *x ) {
337 MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
338 _checkedPeerCert = NO;
342 [self _stream: stream
343 gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
344 code: errSSLClosedAbort
351 - (void) _stream: (TCPStream*)stream gotError: (NSError*)error
353 LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
355 setObj(&_error,error);
356 [_reader disconnect];
357 setObj(&_reader,nil);
358 [_writer disconnect];
359 setObj(&_writer,nil);
363 - (void) _streamGotEOF: (TCPStream*)stream
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];
373 } else if( stream == _writer ) {
374 setObj(&_writer,nil);
377 if( _status == kTCP_Closing ) {
378 [self _checkIfClosed];
380 [self _stream: stream
381 gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
386 // Called after I called -close on a stream and it finished closing:
387 - (void) _streamClosed: (TCPStream*)stream
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 )
403 Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
405 Redistribution and use in source and binary forms, with or without modification, are permitted
406 provided that the following conditions are met:
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
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.