TCP/TCPConnection.m
author Jens Alfke <jens@mooseyard.com>
Fri Jul 24 14:06:28 2009 -0700 (2009-07-24)
changeset 63 5e4855a592ee
parent 48 2b4ad2067074
permissions -rw-r--r--
* The BLIPConnection receivedRequest: delegate method now returns BOOL. If the method returns NO (or if the method isn't implemented in the delegate), that means it didn't handle the message at all; an error will be returned to the sender.
* If the connection closes unexpectedly due to an error, then the auto-generated responses to pending requests will contain that error. This makes it easier to display a meaningful error message in the handler for the request.
jens@0
     1
//
jens@0
     2
//  TCPConnection.m
jens@0
     3
//  MYNetwork
jens@0
     4
//
jens@0
     5
//  Created by Jens Alfke on 5/18/08.
jens@0
     6
//  Copyright 2008 Jens Alfke. All rights reserved.
jens@0
     7
//
jens@0
     8
jens@0
     9
#import "TCP_Internal.h"
jens@0
    10
#import "IPAddress.h"
jens@33
    11
#import "MYBonjourService.h"
jens@0
    12
jens@1
    13
#import "Logging.h"
jens@1
    14
#import "Test.h"
jens@0
    15
#import "ExceptionUtils.h"
jens@0
    16
jens@0
    17
jens@26
    18
#if TARGET_OS_IPHONE && !defined(__SEC_TYPES__)
jens@8
    19
// SecureTransport.h is missing on iPhone, with its SSL constants:
jens@8
    20
enum{
jens@8
    21
    errSSLClosedAbort 			= -9806,	/* connection closed via error */
jens@8
    22
};
jens@8
    23
#endif
jens@8
    24
jens@8
    25
jens@8
    26
jens@0
    27
NSString* const TCPErrorDomain = @"TCP";
jens@0
    28
jens@0
    29
jens@0
    30
@interface TCPConnection ()
jens@0
    31
@property TCPConnectionStatus status;
jens@7
    32
@property (retain) IPAddress *address;
jens@0
    33
- (BOOL) _checkIfClosed;
jens@0
    34
- (void) _closed;
jens@0
    35
@end
jens@0
    36
jens@0
    37
jens@0
    38
@implementation TCPConnection
jens@0
    39
jens@0
    40
jens@0
    41
static NSMutableArray *sAllConnections;
jens@0
    42
jens@0
    43
jens@0
    44
- (Class) readerClass   {return [TCPReader class];}
jens@0
    45
- (Class) writerClass   {return [TCPWriter class];}
jens@0
    46
jens@0
    47
jens@0
    48
- (id) _initWithAddress: (IPAddress*)address
jens@0
    49
            inputStream: (NSInputStream*)input
jens@0
    50
           outputStream: (NSOutputStream*)output
jens@0
    51
{
jens@0
    52
    self = [super init];
jens@0
    53
    if (self != nil) {
jens@7
    54
        if( !input || !output ) {
jens@0
    55
            LogTo(TCP,@"Failed to create %@: addr=%@, in=%@, out=%@",
jens@0
    56
                  self.class,address,input,output);
jens@0
    57
            [self release];
jens@0
    58
            return nil;
jens@0
    59
        }
jens@0
    60
        _address = [address copy];
jens@0
    61
        _reader = [[[self readerClass] alloc] initWithConnection: self stream: input];
jens@0
    62
        _writer = [[[self writerClass] alloc] initWithConnection: self stream: output];
jens@7
    63
        LogTo(TCP,@"%@ initialized, address=%@",self,address);
jens@0
    64
    }
jens@0
    65
    return self;
jens@0
    66
}
jens@0
    67
jens@0
    68
jens@0
    69
jens@0
    70
- (id) initToAddress: (IPAddress*)address
jens@0
    71
{
jens@0
    72
    NSInputStream *input = nil;
jens@0
    73
    NSOutputStream *output = nil;
jens@8
    74
#if TARGET_OS_IPHONE
jens@8
    75
    // +getStreamsToHost: is missing for some stupid reason on iPhone. Grrrrrrrrrr.
jens@8
    76
    CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)address.hostname, address.port,
jens@8
    77
                                       (CFReadStreamRef*)&input, (CFWriteStreamRef*)&output);
danpreston@36
    78
    if( input )  [NSMakeCollectable(input) autorelease];
danpreston@36
    79
    if( output ) [NSMakeCollectable(output) autorelease];
jens@8
    80
#else
jens@0
    81
    [NSStream getStreamsToHost: [NSHost hostWithAddress: address.ipv4name]
jens@0
    82
                          port: address.port 
jens@0
    83
                   inputStream: &input 
jens@0
    84
                  outputStream: &output];
jens@8
    85
#endif
jens@0
    86
    return [self _initWithAddress: address inputStream: input outputStream: output];
jens@0
    87
}
jens@0
    88
jens@7
    89
- (id) initToNetService: (NSNetService*)service
jens@7
    90
{
jens@7
    91
    IPAddress *address = nil;
jens@7
    92
    NSInputStream *input;
jens@7
    93
    NSOutputStream *output;
jens@7
    94
    if( [service getInputStream: &input outputStream: &output] ) {
jens@7
    95
        NSArray *addresses = service.addresses;
jens@7
    96
        if( addresses.count > 0 )
jens@7
    97
            address = [[[IPAddress alloc] initWithSockAddr: [[addresses objectAtIndex: 0] bytes]] autorelease];
jens@7
    98
    } else {
jens@7
    99
        input = nil;
jens@7
   100
        output = nil;
jens@7
   101
    }
jens@7
   102
    return [self _initWithAddress: address inputStream: input outputStream: output];
jens@7
   103
}
jens@7
   104
jens@33
   105
- (id) initToBonjourService: (MYBonjourService*)service;
jens@33
   106
{
jens@33
   107
    NSNetService *netService = [[NSNetService alloc] initWithDomain: service.domain
jens@33
   108
                                                               type: service.type name: service.name];
jens@33
   109
    self = [self initToNetService: netService];
jens@48
   110
    [netService release];
jens@33
   111
    return self;
jens@33
   112
}
jens@33
   113
jens@0
   114
jens@0
   115
- (id) initIncomingFromSocket: (CFSocketNativeHandle)socket
jens@0
   116
                     listener: (TCPListener*)listener
jens@0
   117
{
jens@0
   118
    CFReadStreamRef readStream = NULL;
jens@0
   119
    CFWriteStreamRef writeStream = NULL;
jens@0
   120
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, socket, &readStream, &writeStream);
danpreston@36
   121
	if( readStream )  [NSMakeCollectable(readStream) autorelease];
danpreston@36
   122
    if( writeStream ) [NSMakeCollectable(writeStream) autorelease];
danpreston@36
   123
	
jens@0
   124
    self = [self _initWithAddress: [IPAddress addressOfSocket: socket] 
danpreston@36
   125
                      inputStream: (NSInputStream*)readStream
danpreston@36
   126
                     outputStream: (NSOutputStream*)writeStream];
jens@0
   127
    if( self ) {
jens@0
   128
        _isIncoming = YES;
jens@0
   129
        _server = [listener retain];
jens@0
   130
        CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
jens@0
   131
        CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
jens@0
   132
    }
jens@0
   133
    return self;
jens@0
   134
}    
jens@0
   135
jens@0
   136
jens@0
   137
- (void) dealloc
jens@0
   138
{
jens@0
   139
    LogTo(TCP,@"DEALLOC %@",self);
jens@0
   140
    [_reader release];
jens@0
   141
    [_writer release];
jens@0
   142
    [_address release];
jens@0
   143
    [super dealloc];
jens@0
   144
}
jens@0
   145
jens@0
   146
jens@0
   147
- (NSString*) description
jens@0
   148
{
jens@0
   149
    return $sprintf(@"%@[%@ %@]",self.class,(_isIncoming ?@"from" :@"to"),_address);
jens@0
   150
}
jens@0
   151
jens@0
   152
jens@47
   153
@synthesize address=_address, isIncoming=_isIncoming, status=_status,
jens@16
   154
            reader=_reader, writer=_writer, server=_server, openTimeout=_openTimeout;
jens@0
   155
jens@47
   156
- (id<TCPConnectionDelegate>) delegate                      {return _delegate;}
jens@47
   157
- (void) setDelegate: (id<TCPConnectionDelegate>) delegate  {_delegate = delegate;}
jens@0
   158
jens@0
   159
- (NSError*) error
jens@0
   160
{
jens@0
   161
    return _error;
jens@0
   162
}
jens@0
   163
jens@0
   164
jens@0
   165
- (NSString*) actualSecurityLevel
jens@0
   166
{
jens@0
   167
    return _reader.securityLevel;
jens@0
   168
jens@0
   169
}
jens@0
   170
jens@0
   171
- (NSArray*) peerSSLCerts
jens@0
   172
{
jens@0
   173
    return _reader.peerSSLCerts ?: _writer.peerSSLCerts;
jens@0
   174
}
jens@0
   175
jens@0
   176
jens@0
   177
- (void) _setStreamProperty: (id)value forKey: (NSString*)key
jens@0
   178
{
jens@0
   179
    [_reader setProperty: value forKey: (CFStringRef)key];
jens@0
   180
    [_writer setProperty: value forKey: (CFStringRef)key];
jens@0
   181
}
jens@0
   182
jens@0
   183
jens@0
   184
#pragma mark -
jens@0
   185
#pragma mark OPENING / CLOSING:
jens@0
   186
jens@0
   187
jens@0
   188
- (void) open
jens@0
   189
{
jens@0
   190
    if( _status<=kTCP_Closed && _reader ) {
jens@0
   191
        _reader.SSLProperties = _sslProperties;
jens@0
   192
        _writer.SSLProperties = _sslProperties;
jens@0
   193
        [_reader open];
jens@0
   194
        [_writer open];
jens@0
   195
        if( ! [sAllConnections my_containsObjectIdenticalTo: self] )
jens@0
   196
            [sAllConnections addObject: self];
jens@0
   197
        self.status = kTCP_Opening;
jens@16
   198
        if( _openTimeout > 0 )
jens@16
   199
            [self performSelector: @selector(_openTimeoutExpired) withObject: nil afterDelay: _openTimeout];
jens@16
   200
    }
jens@16
   201
}
jens@16
   202
jens@16
   203
- (void) _stopOpenTimer
jens@16
   204
{
jens@16
   205
    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_openTimeoutExpired) object: nil];
jens@16
   206
}
jens@16
   207
jens@16
   208
- (void) _openTimeoutExpired
jens@16
   209
{
jens@16
   210
    if( _status == kTCP_Opening ) {
jens@16
   211
        LogTo(TCP,@"%@: timed out waiting to open",self);
jens@16
   212
        [self _stream: _reader gotError: [NSError errorWithDomain: NSPOSIXErrorDomain
jens@16
   213
                                                             code: ETIMEDOUT userInfo: nil]];
jens@0
   214
    }
jens@0
   215
}
jens@0
   216
jens@0
   217
jens@0
   218
- (void) disconnect
jens@0
   219
{
jens@0
   220
    if( _status > kTCP_Closed ) {
jens@0
   221
        LogTo(TCP,@"%@ disconnecting",self);
jens@0
   222
        [_writer disconnect];
jens@0
   223
        [_reader disconnect];
jens@0
   224
        self.status = kTCP_Disconnected;
jens@0
   225
    }
jens@16
   226
    [self _stopOpenTimer];
jens@0
   227
}
jens@0
   228
jens@0
   229
jens@0
   230
- (void) close
jens@0
   231
{
jens@0
   232
    [self closeWithTimeout: 60.0];
jens@0
   233
}
jens@0
   234
jens@0
   235
- (void) closeWithTimeout: (NSTimeInterval)timeout
jens@0
   236
{
jens@16
   237
    [self _stopOpenTimer];
jens@0
   238
    if( _status == kTCP_Opening ) {
jens@0
   239
        LogTo(TCP,@"%@ canceling open",self);
jens@0
   240
        [self _closed];
jens@0
   241
    } else if( _status == kTCP_Open ) {
jens@0
   242
        LogTo(TCP,@"%@ closing",self);
jens@0
   243
        self.status = kTCP_Closing;
jens@0
   244
        [self retain];
jens@18
   245
        [self _beginClose];
jens@0
   246
        if( ! [self _checkIfClosed] ) {
jens@0
   247
            if( timeout <= 0.0 )
jens@0
   248
                [self disconnect];
jens@0
   249
            else if( timeout != INFINITY )
jens@0
   250
                [self performSelector: @selector(_closeTimeoutExpired)
jens@0
   251
                           withObject: nil afterDelay: timeout];
jens@0
   252
        }
jens@0
   253
        [self release];
jens@0
   254
    }
jens@0
   255
}
jens@0
   256
jens@0
   257
- (void) _closeTimeoutExpired
jens@0
   258
{
jens@0
   259
    if( _status==kTCP_Closing )
jens@0
   260
        [self disconnect];
jens@0
   261
}
jens@0
   262
jens@19
   263
- (void) _stopCloseTimer
jens@19
   264
{
jens@19
   265
    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(_closeTimeoutExpired) object: nil];
jens@19
   266
}
jens@0
   267
jens@19
   268
- (void) _unclose
jens@19
   269
{
jens@19
   270
    if( _status == kTCP_Closing ) {
jens@19
   271
        LogTo(TCP,@"%@: _unclose!",self);
jens@19
   272
        [_reader _unclose];
jens@19
   273
        [_writer _unclose];
jens@19
   274
        [self _stopCloseTimer];
jens@19
   275
        self.status = kTCP_Open;
jens@19
   276
    }
jens@19
   277
}
jens@19
   278
jens@19
   279
jens@19
   280
/** Subclasses can override this to customize what happens when -close is called. */
jens@18
   281
- (void) _beginClose
jens@18
   282
{
jens@18
   283
    [_reader close];
jens@18
   284
    [_writer close];
jens@18
   285
}
jens@18
   286
jens@18
   287
jens@0
   288
- (BOOL) _checkIfClosed
jens@0
   289
{
jens@0
   290
    if( _status == kTCP_Closing && _writer==nil && _reader==nil ) {
jens@0
   291
        [self _closed];
jens@0
   292
        return YES;
jens@0
   293
    } else
jens@0
   294
        return NO;
jens@0
   295
}
jens@0
   296
jens@0
   297
jens@0
   298
// called by my streams when they close (after my -close is called)
jens@0
   299
- (void) _closed
jens@0
   300
{
jens@17
   301
    [[self retain] autorelease];
jens@0
   302
    if( _status != kTCP_Closed && _status != kTCP_Disconnected ) {
jens@0
   303
        LogTo(TCP,@"%@ is now closed",self);
jens@15
   304
        TCPConnectionStatus prevStatus = _status;
jens@15
   305
        self.status = (prevStatus==kTCP_Closing ?kTCP_Closed :kTCP_Disconnected);
jens@15
   306
        if( prevStatus==kTCP_Opening )
jens@15
   307
            [self tellDelegate: @selector(connection:failedToOpen:) withObject: self.error];
jens@15
   308
        else
jens@15
   309
            [self tellDelegate: @selector(connectionDidClose:) withObject: nil];
jens@0
   310
    }
jens@19
   311
    [self _stopCloseTimer];
jens@16
   312
    [self _stopOpenTimer];
jens@0
   313
    [sAllConnections removeObjectIdenticalTo: self];
jens@0
   314
}
jens@0
   315
jens@0
   316
jens@0
   317
+ (void) closeAllWithTimeout: (NSTimeInterval)timeout
jens@0
   318
{
jens@0
   319
    NSArray *connections = [sAllConnections copy];
jens@0
   320
    for( TCPConnection *conn in connections )
jens@0
   321
        [conn closeWithTimeout: timeout];
jens@0
   322
    [connections release];
jens@0
   323
}
jens@0
   324
jens@0
   325
+ (void) waitTillAllClosed
jens@0
   326
{
jens@0
   327
    while( sAllConnections.count ) {
jens@0
   328
        if( ! [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode
jens@0
   329
                                       beforeDate: [NSDate distantFuture]] )
jens@0
   330
            break;
jens@0
   331
    }
jens@0
   332
}
jens@0
   333
jens@0
   334
jens@0
   335
#pragma mark -
jens@0
   336
#pragma mark STREAM CALLBACKS:
jens@0
   337
jens@0
   338
jens@0
   339
- (void) _streamOpened: (TCPStream*)stream
jens@0
   340
{
jens@7
   341
    if( ! _address )
jens@7
   342
        self.address = stream.peerAddress;
jens@0
   343
    if( _status==kTCP_Opening && _reader.isOpen && _writer.isOpen ) {
jens@7
   344
        LogTo(TCP,@"%@ opened; address=%@",self,_address);
jens@16
   345
        [self _stopOpenTimer];
jens@0
   346
        self.status = kTCP_Open;
jens@0
   347
        [self tellDelegate: @selector(connectionDidOpen:) withObject: nil];
jens@0
   348
    }
jens@0
   349
}
jens@0
   350
jens@0
   351
jens@0
   352
- (BOOL) _streamPeerCertAvailable: (TCPStream*)stream
jens@0
   353
{
jens@0
   354
    BOOL allow = YES;
jens@0
   355
    if( ! _checkedPeerCert ) {
jens@0
   356
        @try{
jens@0
   357
            _checkedPeerCert = YES;
jens@0
   358
            if( stream.securityLevel != nil ) {
jens@0
   359
                NSArray *certs = stream.peerSSLCerts;
jens@0
   360
                if( ! certs && ! _isIncoming )
jens@0
   361
                    allow = NO; // Server MUST have a cert!
jens@0
   362
                else {
jens@0
   363
                    SecCertificateRef cert = certs.count ?(SecCertificateRef)[certs objectAtIndex:0] :NULL;
jens@49
   364
                    if ([TCPEndpoint respondsToSelector: @selector(describeCert:)])
jens@49
   365
                        LogTo(TCP,@"%@: Peer cert = %@",self,[TCPEndpoint describeCert: cert]);
jens@0
   366
                    if( [_delegate respondsToSelector: @selector(connection:authorizeSSLPeer:)] )
jens@0
   367
                        allow = [_delegate connection: self authorizeSSLPeer: cert];
jens@0
   368
                }
jens@0
   369
            }
jens@0
   370
        }@catch( NSException *x ) {
jens@0
   371
            MYReportException(x,@"TCPConnection _streamPeerCertAvailable");
jens@0
   372
            _checkedPeerCert = NO;
jens@0
   373
            allow = NO;
jens@0
   374
        }
jens@0
   375
        if( ! allow )
jens@0
   376
            [self _stream: stream 
jens@0
   377
                 gotError: [NSError errorWithDomain: NSStreamSocketSSLErrorDomain
jens@0
   378
                                               code: errSSLClosedAbort
jens@0
   379
                                           userInfo: nil]];
jens@0
   380
    }
jens@0
   381
    return allow;
jens@0
   382
}
jens@0
   383
jens@0
   384
jens@0
   385
- (void) _stream: (TCPStream*)stream gotError: (NSError*)error
jens@0
   386
{
jens@0
   387
    LogTo(TCP,@"%@ got %@ on %@",self,error,stream.class);
jens@0
   388
    Assert(error);
jens@17
   389
    [[self retain] autorelease];
jens@0
   390
    setObj(&_error,error);
jens@0
   391
    [_reader disconnect];
jens@0
   392
    [_writer disconnect];
jens@0
   393
    [self _closed];
jens@0
   394
}
jens@0
   395
jens@0
   396
- (void) _streamGotEOF: (TCPStream*)stream
jens@0
   397
{
jens@0
   398
    LogTo(TCP,@"%@ got EOF on %@",self,stream);
jens@18
   399
    [stream disconnect];
jens@0
   400
    if( _status == kTCP_Closing ) {
jens@18
   401
        [self _streamCanClose: stream];
jens@0
   402
        [self _checkIfClosed];
jens@0
   403
    } else {
jens@0
   404
        [self _stream: stream 
jens@0
   405
             gotError: [NSError errorWithDomain: NSPOSIXErrorDomain code: ECONNRESET userInfo: nil]];
jens@0
   406
    }
jens@0
   407
}
jens@0
   408
jens@0
   409
jens@18
   410
// Called as soon as a stream is ready to close, after its -close method has been called.
jens@18
   411
- (void) _streamCanClose: (TCPStream*)stream
jens@18
   412
{
jens@18
   413
    if( ! _reader.isActive && !_writer.isActive ) {
jens@18
   414
        LogTo(TCPVerbose,@"Both streams are ready to close now!");
jens@18
   415
        [_reader disconnect];
jens@18
   416
        [_writer disconnect];
jens@18
   417
    }
jens@18
   418
}
jens@18
   419
jens@18
   420
jens@0
   421
// Called after I called -close on a stream and it finished closing:
jens@18
   422
- (void) _streamDisconnected: (TCPStream*)stream
jens@0
   423
{
jens@18
   424
    LogTo(TCP,@"%@: disconnected %@",self,stream);
jens@0
   425
    if( stream == _reader )
jens@0
   426
        setObj(&_reader,nil);
jens@0
   427
    else if( stream == _writer )
jens@0
   428
        setObj(&_writer,nil);
jens@18
   429
    else
jens@18
   430
        return;
jens@0
   431
    if( !_reader.isOpen && !_writer.isOpen )
jens@0
   432
        [self _closed];
jens@0
   433
}
jens@0
   434
jens@0
   435
jens@0
   436
@end
jens@0
   437
jens@0
   438
jens@0
   439
/*
jens@0
   440
 Copyright (c) 2008, Jens Alfke <jens@mooseyard.com>. All rights reserved.
jens@0
   441
 
jens@0
   442
 Redistribution and use in source and binary forms, with or without modification, are permitted
jens@0
   443
 provided that the following conditions are met:
jens@0
   444
 
jens@0
   445
 * Redistributions of source code must retain the above copyright notice, this list of conditions
jens@0
   446
 and the following disclaimer.
jens@0
   447
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
jens@0
   448
 and the following disclaimer in the documentation and/or other materials provided with the
jens@0
   449
 distribution.
jens@0
   450
 
jens@0
   451
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
jens@0
   452
 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
jens@0
   453
 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
jens@0
   454
 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
jens@0
   455
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
jens@0
   456
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
jens@0
   457
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
jens@0
   458
 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
jens@0
   459
 */