You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

340 lines
9.9 KiB

///
// CWTCPConnection.m
// Pantomime
//
// Created by Dirk Zimmermann on 12/04/16.
// Copyright © 2016 p≡p Security S.A. All rights reserved.
//
#import <netinet/tcp.h>
#import <netinet/in.h>
#import "CWTCPConnection.h"
#import <pEpIOSToolboxForExtensions/PEPLogger.h>
#import "NSStream+TLS.h"
NS_ASSUME_NONNULL_BEGIN
@interface CWTCPConnection ()
@property (nonatomic, nullable) NSError *streamError;
@property (atomic) BOOL connected;
@property (atomic, strong) NSString *name;
@property (atomic) uint32_t port;
@property (atomic) ConnectionTransport transport;
@property (atomic, strong, nullable) NSInputStream *readStream;
@property (atomic, strong, nullable) NSOutputStream *writeStream;
@property (atomic, strong) NSMutableSet<NSStream *> *openConnections;
@property (nonatomic) NSMutableArray *fatalErrors;
@property (nullable, strong) NSThread *backgroundThread;
@property (atomic) BOOL isGettingClosed;
@property (nonatomic, nullable) SecIdentityRef clientCertificate;
@end
@implementation CWTCPConnection
- (instancetype)initWithName:(NSString *)theName
port:(unsigned int)thePort
transport:(ConnectionTransport)transport
background:(BOOL)theBOOL
clientCertificate: (SecIdentityRef _Nullable)clientCertificate;
{
if (self = [super init]) {
_openConnections = [[NSMutableSet alloc] init];
_connected = NO;
_name = [theName copy];
_port = thePort;
_transport = transport;
_clientCertificate = clientCertificate;
_fatalErrors = [NSMutableArray new];
LogInfo(@"init %@:%d (%@)", self.name, self.port, self);
NSAssert(theBOOL, @"TCPConnection only supports background mode");
}
return self;
}
- (void)dealloc
{
LogInfo(@"dealloc %@:%d (%@)", self.name, self.port, self);
[self close];
}
- (void)startTLS
{
// Setting a client certificate already enables TLS,
// so only enable it explicitly when there is none
if (self.clientCertificate) {
[self.readStream setClientCertificate:self.clientCertificate];
[self.writeStream setClientCertificate:self.clientCertificate];
} else {
[self.readStream enableTLS];
[self.writeStream enableTLS];
}
}
#pragma mark - Stream Handling
- (void)setupStream:(NSStream *)stream
{
stream.delegate = self;
switch (self.transport) {
case ConnectionTransportPlain:
case ConnectionTransportStartTLS:
[stream disableTLS];
break;
case ConnectionTransportTLS:
// Setting a client certificate already enables TLS,
// so only enable it explicitly when there is none
if (self.clientCertificate) {
[stream setClientCertificate:self.clientCertificate];
} else {
[stream enableTLS];
}
break;
}
[stream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[stream open];
}
- (void)closeAndRemoveStream:(NSStream *)stream
{
if (stream) {
[stream close];
[self.openConnections removeObject:stream];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
if (stream == self.readStream) {
self.readStream = nil;
} else if (stream == self.writeStream) {
self.writeStream = nil;
}
}
}
#pragma mark - Run Loop
- (void)connectInBackgroundAndStartRunLoop
{
NSInputStream *inputStream = nil;
NSOutputStream *outputStream = nil;
[NSStream getStreamsToHostWithName:self.name
port:self.port
inputStream:&inputStream
outputStream:&outputStream];
if (inputStream == nil || outputStream == nil) {
[self signalErrorAndClose];
}
self.readStream = inputStream;
self.writeStream = outputStream;
[self setupStream:self.readStream];
[self setupStream:self.writeStream];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
while (1) {
if ( [NSThread currentThread].isCancelled ) {
break;
}
@autoreleasepool {
[runLoop runMode:NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
}
}
self.backgroundThread = nil;
}
- (void)cancelNoop {}
- (void)cancelBackgroundThread
{
if (self.backgroundThread) {
[self.backgroundThread cancel];
[self performSelector:@selector(cancelNoop) onThread:self.backgroundThread withObject:nil
waitUntilDone:NO];
}
}
#pragma mark - CWConnection
- (BOOL)isConnected
{
return _connected;
}
- (void)close
{
@synchronized(self) {
[self closeAndRemoveStream:self.readStream];
[self closeAndRemoveStream:self.writeStream];
self.connected = NO;
self.isGettingClosed = YES;
[self cancelBackgroundThread];
}
}
- (NSInteger)read:(unsigned char *)buf length:(NSInteger)len
{
if (![self.readStream hasBytesAvailable]) {
return -1;
}
NSInteger count = [self.readStream read:buf maxLength:len];
/*LogInfo("< %@:%d %ld: \"%@\"",
self.name, self.port,
(long)count,
[self bufferToString:buf length:count]);*/
return count;
}
- (NSInteger) write:(unsigned char *)buf length:(NSInteger)len
{
if (![self.writeStream hasSpaceAvailable]) {
return -1;
}
NSInteger count = [self.writeStream write:buf maxLength:len];
/*LogInfo("> %@:%d %ld: \"%@\"",
self.name, self.port,
(long)count,
[self bufferToString:buf length:len]);*/
return count;
}
- (void)connect
{
self.backgroundThread = [[NSThread alloc]
initWithTarget:self
selector:@selector(connectInBackgroundAndStartRunLoop)
object:nil];
self.backgroundThread.name = [NSString
stringWithFormat:@"CWTCPConnection %@:%d 0x%lu",
self.name,
self.port,
(unsigned long) self.backgroundThread];
[self.backgroundThread start];
}
- (BOOL)canWrite
{
return [self.writeStream hasSpaceAvailable];
}
#pragma mark - Util
/// Makes sure there is still a non-nil delegate and returns it, if not,
/// warns about it, and shuts the connection down.
///
/// There's no point in going on without a live delegate.
///
/// @return The set CWConnectionDelegate, or nil if not set or if it went out of scope.
- (id<CWConnectionDelegate>)forceDelegate
{
if (self.delegate == nil) {
LogWarn(@"CWTCPConnection: No delegate. Will close");
if (!self.isGettingClosed) {
[self close];
}
}
return self.delegate;
}
- (NSString *)bufferToString:(unsigned char *)buf length:(NSInteger)length
{
static NSInteger maxLength = 200;
if (length) {
NSData *data = [NSData dataWithBytes:buf length:MIN(length, maxLength)];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (length >= maxLength) {
return [string stringByAppendingString:@"..."];
}
return string;
} else {
return @"";
}
}
- (void)signalErrorAndClose
{
// We abuse ET_EDESC for error indication.
NSError *errorToReturn = self.streamError;
for (NSError *error in self.fatalErrors) {
if (error.code == errSSLPeerCertUnknown) {
// errSSLPeerCertUnknown relatively clearly indicates problems with
// a client certificate, so give that error priority over others,
// in case they get mixed
errorToReturn = error;
break;
}
}
[self.forceDelegate receivedEvent:nil
type:ET_EDESC
extra:(__bridge void * _Nullable)(errorToReturn)
forMode:nil];
[self close];
}
@end
@implementation CWTCPConnection (NSStreamDelegate)
#pragma mark - NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventNone:
//LogInfo(@"NSStreamEventNone");
break;
case NSStreamEventOpenCompleted:
//LogInfo(@"NSStreamEventOpenCompleted");
[self.openConnections addObject:aStream];
if (self.openConnections.count == 2) {
LogInfo(@"connectionEstablished");
self.connected = YES;
[self.forceDelegate connectionEstablished];
}
break;
case NSStreamEventHasBytesAvailable:
//LogInfo(@"NSStreamEventHasBytesAvailable");
[self.forceDelegate receivedEvent:nil type:ET_RDESC extra:nil forMode:nil];
break;
case NSStreamEventHasSpaceAvailable:
//LogInfo(@"NSStreamEventHasSpaceAvailable");
[self.forceDelegate receivedEvent:nil type:ET_WDESC extra:nil forMode:nil];
break;
case NSStreamEventErrorOccurred:
LogError(@"NSStreamEventErrorOccurred: read: %@, write: %@",
[self.readStream.streamError localizedDescription],
[self.writeStream.streamError localizedDescription]);
if (self.readStream.streamError) {
[self.fatalErrors addObject:self.readStream.streamError];
} else if (self.writeStream.streamError) {
[self.fatalErrors addObject:self.writeStream.streamError];
}
self.streamError = [self.fatalErrors firstObject];
[self signalErrorAndClose];
break;
case NSStreamEventEndEncountered:
LogWarn(@"NSStreamEventEndEncountered");
[self.forceDelegate receivedEvent:nil type:ET_EDESC extra:nil forMode:nil];
[self close];
break;
}
}
@end
NS_ASSUME_NONNULL_END