953 lines
41 KiB
Objective-C
953 lines
41 KiB
Objective-C
//
|
|
// MLStream.m
|
|
// monalxmpp
|
|
//
|
|
// Created by Thilo Molitor on 11.04.21.
|
|
// Copyright © 2021 Monal.im. All rights reserved.
|
|
//
|
|
|
|
#import <Network/Network.h>
|
|
#import "MLConstants.h"
|
|
#import "MLStream.h"
|
|
#import "HelperTools.h"
|
|
#import <monalxmpp/monalxmpp-Swift.h>
|
|
|
|
@class MLCrypto;
|
|
|
|
#define BUFFER_SIZE 4096
|
|
|
|
@interface MLSharedStreamState : NSObject
|
|
@property (atomic, strong) id<NSStreamDelegate> delegate;
|
|
@property (atomic, strong) NSRunLoop* runLoop;
|
|
@property (atomic) NSRunLoopMode runLoopMode;
|
|
@property (atomic, strong) NSError* error;
|
|
@property (atomic) nw_connection_t connection;
|
|
@property (atomic) BOOL opening;
|
|
@property (atomic) BOOL open;
|
|
@property (atomic) BOOL hasTLS;
|
|
@property (atomic) nw_parameters_configure_protocol_block_t configure_tls_block;
|
|
@property (atomic) nw_framer_t _Nullable framer;
|
|
@property (atomic) NSCondition* tlsHandshakeCompleteCondition;
|
|
@end
|
|
|
|
@interface MLStream()
|
|
{
|
|
id<NSStreamDelegate> _delegate;
|
|
}
|
|
@property (atomic, strong) MLSharedStreamState* shared_state;
|
|
@property (atomic) BOOL open_called;
|
|
@property (atomic) BOOL closed;
|
|
-(instancetype) initWithSharedState:(MLSharedStreamState*) shared;
|
|
-(void) generateEvent:(NSStreamEvent) event;
|
|
@end
|
|
|
|
@interface MLInputStream()
|
|
{
|
|
NSMutableData* _buf;
|
|
volatile __block BOOL _reading;
|
|
//this semaphore will make sure that at most only one call to nw_connection_receive() or nw_framer_parse_input() is in flight
|
|
//we use it as mutex: be careful to never increase it beyond 1!!
|
|
//(mutexes can not be unlocked in a thread different from the one it got locked in and NSLock internally uses mutext --> both can not be used)
|
|
dispatch_semaphore_t _read_sem;
|
|
}
|
|
@property (atomic, readonly) void (^incoming_data_handler)(NSData* _Nullable, BOOL, NSError* _Nullable, BOOL);
|
|
@end
|
|
|
|
@interface MLOutputStream()
|
|
{
|
|
volatile __block unsigned long _writing;
|
|
}
|
|
@end
|
|
|
|
@implementation MLSharedStreamState
|
|
|
|
-(instancetype) init
|
|
{
|
|
self = [super init];
|
|
self.error = nil;
|
|
self.opening = NO;
|
|
self.open = NO;
|
|
self.hasTLS = NO;
|
|
self.framer = nil;
|
|
self.tlsHandshakeCompleteCondition = [NSCondition new];
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation MLInputStream
|
|
|
|
-(instancetype) initWithSharedState:(MLSharedStreamState*) shared
|
|
{
|
|
self = [super initWithSharedState:shared];
|
|
_buf = [NSMutableData new];
|
|
_reading = NO;
|
|
//(see the comments added to the declaration of this member var)
|
|
_read_sem = dispatch_semaphore_create(1); //the first schedule_read call is always allowed
|
|
|
|
//this handler will be called by the schedule_read method
|
|
//since the framer swallows all data, nw_connection_receive() and the framer cannot race against each other and deliver reordered data
|
|
weakify(self);
|
|
_incoming_data_handler = ^(NSData* _Nullable content, BOOL is_complete, NSError* _Nullable st_error, BOOL polling_active) {
|
|
strongify(self);
|
|
if(self == nil)
|
|
return;
|
|
|
|
DDLogVerbose(@"Incoming data handler called with is_complete=%@, st_error=%@, content=%@", bool2str(is_complete), st_error, content);
|
|
@synchronized(self.shared_state) {
|
|
self->_reading = NO;
|
|
}
|
|
BOOL generate_bytes_available_event = NO;
|
|
BOOL generate_error_event = NO;
|
|
|
|
//handle content received
|
|
if(content != NULL)
|
|
{
|
|
if([content length] > 0)
|
|
{
|
|
@synchronized(self->_buf) {
|
|
[self->_buf appendData:content];
|
|
}
|
|
generate_bytes_available_event = YES;
|
|
}
|
|
}
|
|
|
|
//handle errors
|
|
if(st_error)
|
|
{
|
|
//ignore enodata and eagain errors
|
|
if([st_error.domain isEqualToString:(__bridge NSString *)kNWErrorDomainPOSIX] && (st_error.code == ENODATA || st_error.code == EAGAIN))
|
|
DDLogWarn(@"Ignoring transient receive error: %@", st_error);
|
|
else
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
self.shared_state.error = st_error;
|
|
}
|
|
generate_error_event = YES;
|
|
}
|
|
}
|
|
|
|
//allow new call to schedule_read
|
|
//(see the comments added to the declaration of this member var)
|
|
dispatch_semaphore_signal(self->_read_sem);
|
|
|
|
//emit events
|
|
if(generate_bytes_available_event)
|
|
[self generateEvent:NSStreamEventHasBytesAvailable];
|
|
if(generate_error_event)
|
|
[self generateEvent:NSStreamEventErrorOccurred];
|
|
//check if we're read-closed and stop our loop if true (this has to be done *after* processing content)
|
|
if(is_complete)
|
|
[self generateEvent:NSStreamEventEndEncountered];
|
|
|
|
//try to read again
|
|
if(!is_complete && !generate_error_event && !generate_bytes_available_event && polling_active)
|
|
[self schedule_read];
|
|
};
|
|
return self;
|
|
}
|
|
|
|
-(NSInteger) read:(uint8_t*) buffer maxLength:(NSUInteger) len
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
if(self.closed || !self.open_called || !self.shared_state.open)
|
|
return -1;
|
|
}
|
|
BOOL was_smaller = NO;
|
|
@synchronized(self->_buf) {
|
|
if(len > [_buf length])
|
|
len = [_buf length];
|
|
[_buf getBytes:buffer length:len];
|
|
if(len < [_buf length])
|
|
{
|
|
NSData* to_append = [_buf subdataWithRange:NSMakeRange(len, [_buf length]-len)];
|
|
[_buf setLength:0];
|
|
[_buf appendData:to_append];
|
|
was_smaller = YES;
|
|
}
|
|
else
|
|
{
|
|
[_buf setLength:0];
|
|
was_smaller = NO;
|
|
}
|
|
}
|
|
//this has to be done outside of our @synchronized block
|
|
if(was_smaller)
|
|
[self generateEvent:NSStreamEventHasBytesAvailable];
|
|
else if(len > 0) //only do this if we really provided some data to the reader
|
|
{
|
|
//buffered data got retrieved completely --> schedule new read
|
|
[self schedule_read];
|
|
}
|
|
return len;
|
|
}
|
|
|
|
-(BOOL) getBuffer:(uint8_t* _Nullable *) buffer length:(NSUInteger*) len
|
|
{
|
|
return NO; //this method is not available in this implementation
|
|
/*
|
|
@synchronized(_buf) {
|
|
*len = [_buf length];
|
|
*buffer = (uint8_t* _Nullable)[_buf bytes];
|
|
return YES;
|
|
}*/
|
|
}
|
|
|
|
-(BOOL) hasBytesAvailable
|
|
{
|
|
@synchronized(_buf) {
|
|
return _buf && [_buf length];
|
|
}
|
|
}
|
|
|
|
-(NSStreamStatus) streamStatus
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
if(self.open_called && self.shared_state.open && _reading)
|
|
return NSStreamStatusReading;
|
|
}
|
|
return [super streamStatus];
|
|
}
|
|
|
|
-(void) schedule_read
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
if(self.closed || !self.open_called || !self.shared_state.open)
|
|
{
|
|
DDLogVerbose(@"ignoring schedule_read call because connection is closed: %@", self);
|
|
return;
|
|
}
|
|
|
|
//don't call nw_connection_receive() or nw_framer_parse_input() multiple times in parallel: this will introduce race conditions
|
|
//(see the comments added to the declaration of this member var)
|
|
if(dispatch_semaphore_wait(_read_sem, DISPATCH_TIME_NOW) != 0)
|
|
{
|
|
DDLogWarn(@"Ignoring call to schedule_read, reading already in progress...");
|
|
return;
|
|
}
|
|
_reading = YES;
|
|
|
|
if(self.shared_state.framer != nil)
|
|
{
|
|
DDLogDebug(@"dispatching async call to nw_framer_parse_input into framer queue");
|
|
nw_framer_async(self.shared_state.framer, ^{
|
|
DDLogDebug(@"now calling nw_framer_parse_input inside framer queue");
|
|
nw_framer_parse_input(self.shared_state.framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) {
|
|
DDLogDebug(@"nw_framer_parse_input got callback with is_complete:%@, length=%zu", bool2str(is_complete), (unsigned long)buffer_length);
|
|
//we don't want to do "polling" here, our next nw_framer_parse_input will be triggered by the nw_framer_set_input_handler block
|
|
self.incoming_data_handler([NSData dataWithBytes:buffer length:buffer_length], is_complete, nil, NO);
|
|
return buffer_length;
|
|
});
|
|
});
|
|
}
|
|
else
|
|
{
|
|
DDLogVerbose(@"calling nw_connection_receive");
|
|
nw_connection_receive(self.shared_state.connection, 1, BUFFER_SIZE, ^(dispatch_data_t content, nw_content_context_t context __unused, bool is_complete, nw_error_t receive_error) {
|
|
DDLogVerbose(@"nw_connection_receive got callback with is_complete:%@, receive_error=%@, length=%zu", bool2str(is_complete), receive_error, (unsigned long)((NSData*)content).length);
|
|
NSError* st_error = nil;
|
|
if(receive_error)
|
|
st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(receive_error));
|
|
//we want to do "polling" here (e.g. start our next blocking nw_connection_receive call if we did not receive new data nor any error)
|
|
self.incoming_data_handler((NSData*)content, is_complete, st_error, YES);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) generateEvent:(NSStreamEvent) event
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
[super generateEvent:event];
|
|
//in contrast to the normal nw_receive, the framer receive will not block until we receive any data
|
|
//--> don't call schedule_read if a framer is active, the framer will call it itself once it gets signalled that data is available
|
|
if(event == NSStreamEventOpenCompleted && self.open_called && self.shared_state.open && self.shared_state.framer == nil)
|
|
{
|
|
//we are open now --> allow reading (this will block until we receive any data)
|
|
[self schedule_read];
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MLOutputStream
|
|
|
|
-(instancetype) initWithSharedState:(MLSharedStreamState*) shared
|
|
{
|
|
self = [super initWithSharedState:shared];
|
|
_writing = 0;
|
|
return self;
|
|
}
|
|
|
|
-(NSInteger) write:(const uint8_t*) buffer maxLength:(NSUInteger) len
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
if(self.closed)
|
|
return -1;
|
|
}
|
|
|
|
NSCondition* condition = [NSCondition new];
|
|
void (^write_completion)(nw_error_t) = ^(nw_error_t _Nullable error) {
|
|
DDLogVerbose(@"Write completed...");
|
|
|
|
@synchronized(self) {
|
|
self->_writing--;
|
|
}
|
|
|
|
[condition lock];
|
|
[condition signal];
|
|
[condition unlock];
|
|
|
|
if(error)
|
|
{
|
|
NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error));
|
|
@synchronized(self.shared_state) {
|
|
self.shared_state.error = st_error;
|
|
}
|
|
[self generateEvent:NSStreamEventErrorOccurred];
|
|
}
|
|
else
|
|
{
|
|
@synchronized(self) {
|
|
if([self hasSpaceAvailable])
|
|
[self generateEvent:NSStreamEventHasSpaceAvailable];
|
|
}
|
|
}
|
|
};
|
|
|
|
//the call to dispatch_get_main_queue() is a dummy because we are using DISPATCH_DATA_DESTRUCTOR_DEFAULT which is performed inline
|
|
dispatch_data_t data = dispatch_data_create(buffer, len, dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_DEFAULT);
|
|
|
|
//support tcp fast open for all data sent before the connection got opened, but only usable for connections NOT using a framer
|
|
/*if(!self.open_called)
|
|
{
|
|
DDLogInfo(@"Sending TCP fast open early data: %@", data);
|
|
nw_connection_send(self.shared_state.connection, data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, NO, NW_CONNECTION_SEND_IDEMPOTENT_CONTENT);
|
|
return len;
|
|
}*/
|
|
|
|
@synchronized(self.shared_state) {
|
|
if(!self.open_called || !self.shared_state.open)
|
|
return -1;
|
|
}
|
|
@synchronized(self) {
|
|
_writing++;
|
|
}
|
|
|
|
//decide if we should use our framer or normal nw_connection_send()
|
|
//framer being nil is the hot path --> make it fast (we'll check if it's still != nil in an @synchronized block below --> still threadsafe
|
|
//for the record: wrapping this into an @synchronized block would create a deadlock with our condition wait inside this
|
|
//block and the second @synchronized block inside nw_framer_async()
|
|
[condition lock];
|
|
if(self.shared_state.framer != nil)
|
|
{
|
|
DDLogDebug(@"Switching async to framer thread in COLD path...");
|
|
//framer methods must be called inside the framer thread
|
|
nw_framer_async(self.shared_state.framer, ^{
|
|
//make sure that self.shared_state.framer still isn't nil, if it is, we fall back to nw_connection_send()
|
|
@synchronized(self.shared_state) {
|
|
if(self.shared_state.framer != nil)
|
|
{
|
|
DDLogDebug(@"Calling nw_framer_write_output_data() in COLD path...");
|
|
nw_framer_write_output_data(self.shared_state.framer, data);
|
|
//make sure to not call the write_completion inside this @synchronized block
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
write_completion(nil); //TODO: can we detect write errors like in nw_connection_send() somehow?
|
|
});
|
|
}
|
|
else
|
|
{
|
|
//make sure to not call nw_connection_send() and the following write_completion inside this @synchronized block
|
|
//we don't know if calling nw_connection_send() from the framer thread is safe --> just don't do this to be on the safe side
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
DDLogDebug(@"Calling nw_connection_send() in COLD path...");
|
|
nw_connection_send(self.shared_state.connection, data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, NO, write_completion);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
//wait for write complete signal
|
|
[condition wait];
|
|
[condition unlock];
|
|
DDLogDebug(@"Returning from write in COLD path: %zu", (unsigned long)len);
|
|
return len; //return instead of else to leave @synchronized block early
|
|
}
|
|
DDLogVerbose(@"Calling nw_connection_send() in hot path...");
|
|
nw_connection_send(self.shared_state.connection, data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, NO, write_completion);
|
|
//wait for write complete signal
|
|
[condition wait];
|
|
[condition unlock];
|
|
DDLogVerbose(@"Returning from write in hot path: %zu", (unsigned long)len);
|
|
return len;
|
|
}
|
|
|
|
-(BOOL) hasSpaceAvailable
|
|
{
|
|
@synchronized(self) {
|
|
return self.open_called && self.shared_state.open && !self.closed && _writing == 0;
|
|
}
|
|
}
|
|
|
|
-(NSStreamStatus) streamStatus
|
|
{
|
|
@synchronized(self) {
|
|
if(self.open_called && self.shared_state.open && !self.closed && _writing > 0)
|
|
return NSStreamStatusWriting;
|
|
}
|
|
return [super streamStatus];
|
|
}
|
|
|
|
-(void) generateEvent:(NSStreamEvent) event
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
[super generateEvent:event];
|
|
//generate the first NSStreamEventHasSpaceAvailable event directly after our NSStreamEventOpenCompleted event
|
|
//(the network framework buffers outgoing data itself, e.g. it is always writable)
|
|
if(event == NSStreamEventOpenCompleted && [self hasSpaceAvailable])
|
|
[super generateEvent:NSStreamEventHasSpaceAvailable];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation MLStream
|
|
|
|
+(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host connectPort:(NSNumber*) port tls:(BOOL) tls inputStream:(NSInputStream* _Nullable * _Nonnull) inputStream outputStream:(NSOutputStream* _Nullable * _Nonnull) outputStream logtag:(id _Nullable) logtag
|
|
{
|
|
//create state
|
|
volatile __block BOOL wasOpenOnce = NO;
|
|
MLSharedStreamState* shared_state = [[MLSharedStreamState alloc] init];
|
|
|
|
//create and configure public stream instances returned later
|
|
MLInputStream* input = [[MLInputStream alloc] initWithSharedState:shared_state];
|
|
MLOutputStream* output = [[MLOutputStream alloc] initWithSharedState:shared_state];
|
|
|
|
nw_parameters_configure_protocol_block_t tcp_options = ^(nw_protocol_options_t tcp_options) {
|
|
nw_tcp_options_set_enable_fast_open(tcp_options, YES); //enable tcp fast open
|
|
//nw_tcp_options_set_no_delay(tcp_options, YES); //disable nagle's algorithm
|
|
//nw_tcp_options_set_connection_timeout(tcp_options, 4);
|
|
};
|
|
nw_parameters_configure_protocol_block_t configure_tls_block = ^(nw_protocol_options_t tls_options) {
|
|
sec_protocol_options_t options = nw_tls_copy_sec_protocol_options(tls_options);
|
|
sec_protocol_options_set_tls_server_name(options, [SNIDomain cStringUsingEncoding:NSUTF8StringEncoding]);
|
|
sec_protocol_options_add_tls_application_protocol(options, "xmpp-client");
|
|
sec_protocol_options_set_tls_ocsp_enabled(options, 1);
|
|
sec_protocol_options_set_tls_false_start_enabled(options, 1);
|
|
sec_protocol_options_set_min_tls_protocol_version(options, tls_protocol_version_TLSv12);
|
|
//sec_protocol_options_set_max_tls_protocol_version(options, tls_protocol_version_TLSv12);
|
|
sec_protocol_options_set_tls_resumption_enabled(options, 1);
|
|
sec_protocol_options_set_tls_tickets_enabled(options, 1);
|
|
sec_protocol_options_set_tls_renegotiation_enabled(options, 0);
|
|
//tls-exporter channel-binding is only usable for TLSv1.2 if ECDHE is used instead of RSA key exchange
|
|
//(see https://mitls.org/pages/attacks/3SHAKE)
|
|
//see also https://developer.apple.com/documentation/security/preventing_insecure_network_connections?language=objc
|
|
sec_protocol_options_append_tls_ciphersuite_group(options, tls_ciphersuite_group_ats);
|
|
};
|
|
|
|
//configure tcp connection parameters
|
|
nw_parameters_t parameters;
|
|
if(tls)
|
|
{
|
|
parameters = nw_parameters_create_secure_tcp(configure_tls_block, tcp_options);
|
|
shared_state.hasTLS = YES;
|
|
}
|
|
else
|
|
{
|
|
parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, tcp_options);
|
|
shared_state.hasTLS = NO;
|
|
|
|
//create simple framer and append it to our stack
|
|
//first framer initialization is allowed to send tcp early data
|
|
volatile __block int startupCounter = 0; //workaround for some weird apple stuff, see below
|
|
nw_protocol_definition_t starttls_framer_definition = nw_framer_create_definition([[[NSUUID UUID] UUIDString] UTF8String], NW_FRAMER_CREATE_FLAGS_DEFAULT, ^(nw_framer_t framer) {
|
|
//we don't need any locking for our counter because all framers will be started in the same internal network queue
|
|
int framerId = startupCounter++;
|
|
DDLogInfo(@"%@: Framer(%d) %@ start called with wasOpenOnce=%@...", logtag, framerId, framer, bool2str(wasOpenOnce));
|
|
nw_framer_set_stop_handler(framer, (nw_framer_stop_handler_t)^(nw_framer_t _Nullable framer) {
|
|
DDLogInfo(@"%@, Framer(%d) stop called: %@", logtag, framerId, framer);
|
|
return YES;
|
|
});
|
|
|
|
/*
|
|
//some weird apple stuff creates the framer twice: once directly when starting the tcp handshake
|
|
//and again later after the tcp connection was established successfully --> ignore the first one
|
|
if(framerId < 1)
|
|
{
|
|
nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
|
|
nw_framer_parse_input(framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) {
|
|
MLAssert(NO, @"Unexpected incoming bytes in first framer!", (@{
|
|
@"logtag": nilWrapper(logtag),
|
|
@"framer": framer,
|
|
@"buffer": [NSData dataWithBytes:buffer length:buffer_length],
|
|
@"buffer_length": @(buffer_length),
|
|
@"is_complete": bool2str(is_complete),
|
|
}));
|
|
return buffer_length;
|
|
});
|
|
return 0; //why that?
|
|
});
|
|
nw_framer_set_output_handler(framer, ^(nw_framer_t framer, nw_framer_message_t message, size_t message_length, bool is_complete) {
|
|
MLAssert(NO, @"Unexpected outgoing bytes in first framer!", (@{
|
|
@"logtag": nilWrapper(logtag),
|
|
@"framer": framer,
|
|
@"message": message,
|
|
@"message_length": @(message_length),
|
|
@"is_complete": bool2str(is_complete),
|
|
}));
|
|
});
|
|
return nw_framer_start_result_will_mark_ready;
|
|
}
|
|
*/
|
|
|
|
//we have to simulate nw_connection_state_ready because the connection state will not reflect that while our framer is active
|
|
//--> use framer start as "connection active" signal
|
|
//first framer start is allowed to directly send data which will be used as tcp early data
|
|
if(!wasOpenOnce)
|
|
{
|
|
wasOpenOnce = YES;
|
|
@synchronized(shared_state) {
|
|
shared_state.open = YES;
|
|
}
|
|
//make sure to not do this inside the framer thread to not cause any deadlocks
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[input generateEvent:NSStreamEventOpenCompleted];
|
|
[output generateEvent:NSStreamEventOpenCompleted];
|
|
});
|
|
}
|
|
|
|
nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
|
|
[input schedule_read];
|
|
return 0; //why that??
|
|
});
|
|
|
|
shared_state.framer = framer;
|
|
return nw_framer_start_result_will_mark_ready;
|
|
});
|
|
DDLogInfo(@"%@: Not doing direct TLS: appending framer to protocol stack...", logtag);
|
|
nw_protocol_stack_prepend_application_protocol(nw_parameters_copy_default_protocol_stack(parameters), nw_framer_create_options(starttls_framer_definition));
|
|
}
|
|
//needed to activate tcp fast open with apple's internal tls framer
|
|
nw_parameters_set_fast_open_enabled(parameters, YES);
|
|
//use dnssec if configured
|
|
if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"])
|
|
nw_parameters_set_requires_dnssec_validation(parameters, YES);
|
|
|
|
//create and configure connection object
|
|
nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]);
|
|
nw_connection_t connection = nw_connection_create(endpoint, parameters);
|
|
nw_connection_set_queue(connection, dispatch_queue_create_with_target([NSString stringWithFormat:@"im.monal.networking:%@", logtag].UTF8String, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)));
|
|
|
|
//configure shared state
|
|
shared_state.connection = connection;
|
|
shared_state.configure_tls_block = configure_tls_block;
|
|
|
|
//configure state change handler proxying state changes to our public stream instances
|
|
nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
|
|
@synchronized(shared_state) {
|
|
//connection was opened once (e.g. opening=YES) and closed later on (e.g. open=NO)
|
|
if(wasOpenOnce && !shared_state.open)
|
|
{
|
|
DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler, connection already closed: %@ --> %du, %@", logtag, self, state, error);
|
|
return;
|
|
}
|
|
}
|
|
//always handle errors regardless of current state (cert errors etc.)
|
|
if(error != nil)
|
|
{
|
|
DDLogVerbose(@"%@: %@ got error in state %du and reporting: %@", logtag, self, state, error);
|
|
NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error));
|
|
@synchronized(shared_state) {
|
|
shared_state.error = st_error;
|
|
}
|
|
[input generateEvent:NSStreamEventErrorOccurred];
|
|
[output generateEvent:NSStreamEventErrorOccurred];
|
|
}
|
|
|
|
if(state == nw_connection_state_waiting)
|
|
{
|
|
//do nothing here, documentation says the connection will be automatically retried "when conditions are favourable"
|
|
//which seems to mean: if the network path changed (for example connectivity regained)
|
|
//if this happens inside the connection timeout all is ok
|
|
//if not, the connection will be cancelled already and everything will be ok, too
|
|
DDLogVerbose(@"%@: got nw_connection_state_waiting and ignoring it, see comments in code: %@ (%@)", logtag, self, error);
|
|
}
|
|
else if(state == nw_connection_state_failed)
|
|
{
|
|
//errors already reported by generic handling above
|
|
DDLogError(@"%@: Connection failed (error already reported): %@", logtag, error);
|
|
}
|
|
else if(state == nw_connection_state_ready)
|
|
{
|
|
DDLogInfo(@"%@: Connection established, wasOpenOnce: %@", logtag, bool2str(wasOpenOnce));
|
|
if(!wasOpenOnce)
|
|
{
|
|
wasOpenOnce = YES;
|
|
@synchronized(shared_state) {
|
|
shared_state.open = YES;
|
|
}
|
|
[input generateEvent:NSStreamEventOpenCompleted];
|
|
[output generateEvent:NSStreamEventOpenCompleted];
|
|
}
|
|
else
|
|
{
|
|
//the nw_connection_state_ready state while already wasOpenOnce comes from our framer set to ready
|
|
//this informs the upper layer that the connection is in ready state now, but we already treat the framer start
|
|
//as connection ready event
|
|
|
|
@synchronized(shared_state) {
|
|
//tls handshake completed now
|
|
shared_state.hasTLS = YES;
|
|
|
|
//unlock thread waiting on tls handshake completion (starttls)
|
|
[shared_state.tlsHandshakeCompleteCondition lock];
|
|
[shared_state.tlsHandshakeCompleteCondition signal];
|
|
[shared_state.tlsHandshakeCompleteCondition unlock];
|
|
}
|
|
|
|
//we still want to inform our stream users that they can write data now and schedule a read operation
|
|
[output generateEvent:NSStreamEventHasSpaceAvailable];
|
|
[input schedule_read];
|
|
}
|
|
}
|
|
else if(state == nw_connection_state_cancelled)
|
|
{
|
|
//ignore this (we use reference counting)
|
|
DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_cancelled: %@ (%@)", logtag, self, error);
|
|
}
|
|
else if(state == nw_connection_state_invalid)
|
|
{
|
|
//ignore all other states (preparing, invalid)
|
|
DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_invalid: %@ (%@)", logtag, self, error);
|
|
}
|
|
else if(state == nw_connection_state_preparing)
|
|
{
|
|
//ignore all other states (preparing, invalid)
|
|
DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_preparing: %@ (%@)", logtag, self, error);
|
|
}
|
|
else
|
|
unreachable();
|
|
});
|
|
|
|
*inputStream = (NSInputStream*)input;
|
|
*outputStream = (NSOutputStream*)output;
|
|
}
|
|
|
|
-(void) startTLS
|
|
{
|
|
[self.shared_state.tlsHandshakeCompleteCondition lock];
|
|
@synchronized(self.shared_state) {
|
|
MLAssert(!self.shared_state.hasTLS, @"We already have TLS on this connection!");
|
|
MLAssert(self.shared_state.framer != nil, @"Trying to start tls handshake without having a running framer!");
|
|
DDLogInfo(@"Starting TLS handshake on framer: %@", self.shared_state.framer);
|
|
nw_framer_async(self.shared_state.framer, ^{
|
|
@synchronized(self.shared_state) {
|
|
DDLogVerbose(@"Prepending tls to framer: %@", self.shared_state.framer);
|
|
nw_framer_t framer = self.shared_state.framer;
|
|
self.shared_state.framer = nil;
|
|
nw_protocol_options_t tls_options = nw_tls_create_options();
|
|
self.shared_state.configure_tls_block(tls_options);
|
|
nw_framer_prepend_application_protocol(framer, tls_options);
|
|
nw_framer_pass_through_input(framer);
|
|
nw_framer_pass_through_output(framer);
|
|
nw_framer_mark_ready(framer);
|
|
DDLogVerbose(@"Framer deactivated and TLS prepended now...");
|
|
}
|
|
});
|
|
}
|
|
[self.shared_state.tlsHandshakeCompleteCondition wait];
|
|
[self.shared_state.tlsHandshakeCompleteCondition unlock];
|
|
DDLogInfo(@"TLS handshake completed: %@...", bool2str(self.shared_state.hasTLS));
|
|
}
|
|
|
|
-(BOOL) hasTLS
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
return self.shared_state.hasTLS;
|
|
}
|
|
}
|
|
|
|
-(instancetype) initWithSharedState:(MLSharedStreamState*) shared
|
|
{
|
|
self = [super init];
|
|
self.shared_state = shared;
|
|
@synchronized(self.shared_state) {
|
|
self.open_called = NO;
|
|
self.closed = NO;
|
|
self.delegate = self;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
-(void) generateEvent:(NSStreamEvent) event
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
//don't schedule delegate calls if no runloop was specified
|
|
if(self.shared_state.runLoop == nil)
|
|
return;
|
|
//make sure to NOT hold the @synchronized lock when calling the delegate to not introduce deadlocks
|
|
BOOL handleEvent = NO;
|
|
if(event == NSStreamEventOpenCompleted && self.open_called && self.shared_state.open)
|
|
handleEvent = YES;
|
|
else if(event == NSStreamEventHasBytesAvailable && self.open_called && self.shared_state.open)
|
|
handleEvent = YES;
|
|
else if(event == NSStreamEventHasSpaceAvailable && self.open_called && self.shared_state.open)
|
|
handleEvent = YES;
|
|
else if(event == NSStreamEventErrorOccurred)
|
|
handleEvent = YES;
|
|
else if(event == NSStreamEventEndEncountered && self.open_called && self.shared_state.open)
|
|
handleEvent = YES;
|
|
//check if the event should be handled
|
|
if(!handleEvent)
|
|
DDLogVerbose(@"Ignoring event %ld", (long)event);
|
|
else
|
|
{
|
|
//schedule the delegate calls in the runloop that was registered
|
|
CFRunLoopPerformBlock([self.shared_state.runLoop getCFRunLoop], (__bridge CFStringRef)self.shared_state.runLoopMode, ^{
|
|
[self->_delegate stream:self handleEvent:event];
|
|
});
|
|
//trigger wakeup of runloop to execute the block as soon as possible
|
|
CFRunLoopWakeUp([self.shared_state.runLoop getCFRunLoop]);
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) open
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
MLAssert(!self.closed, @"streams can not be reopened!");
|
|
self.open_called = YES;
|
|
if(!self.shared_state.opening)
|
|
{
|
|
DDLogVerbose(@"Calling nw_connection_start()...");
|
|
nw_connection_start(self.shared_state.connection);
|
|
}
|
|
self.shared_state.opening = YES;
|
|
//already opened by stream for other direction? --> directly trigger open event
|
|
if(self.shared_state.open)
|
|
[self generateEvent:NSStreamEventOpenCompleted];
|
|
}
|
|
}
|
|
|
|
-(void) close
|
|
{
|
|
nw_connection_t connection;
|
|
@synchronized(self.shared_state) {
|
|
connection = self.shared_state.connection;
|
|
}
|
|
DDLogVerbose(@"Closing connection via nw_connection_send()...");
|
|
nw_connection_send(connection, NULL, NW_CONNECTION_FINAL_MESSAGE_CONTEXT, YES, ^(nw_error_t _Nullable error) {
|
|
if(error)
|
|
{
|
|
NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error));
|
|
@synchronized(self.shared_state) {
|
|
self.shared_state.error = st_error;
|
|
}
|
|
[self generateEvent:NSStreamEventErrorOccurred];
|
|
}
|
|
});
|
|
@synchronized(self.shared_state) {
|
|
self.closed = YES;
|
|
self.shared_state.open = NO;
|
|
|
|
//unlock thread waiting on tls handshake
|
|
[self.shared_state.tlsHandshakeCompleteCondition lock];
|
|
[self.shared_state.tlsHandshakeCompleteCondition signal];
|
|
[self.shared_state.tlsHandshakeCompleteCondition unlock];
|
|
}
|
|
}
|
|
|
|
-(void) setDelegate:(id<NSStreamDelegate>) delegate
|
|
{
|
|
_delegate = delegate;
|
|
if(_delegate == nil)
|
|
_delegate = self;
|
|
}
|
|
|
|
-(void) scheduleInRunLoop:(NSRunLoop*) loop forMode:(NSRunLoopMode) mode
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
self.shared_state.runLoop = loop;
|
|
self.shared_state.runLoopMode = mode;
|
|
}
|
|
}
|
|
|
|
-(void) removeFromRunLoop:(NSRunLoop*) loop forMode:(NSRunLoopMode) mode
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
self.shared_state.runLoop = nil;
|
|
self.shared_state.runLoopMode = mode;
|
|
}
|
|
}
|
|
|
|
-(id) propertyForKey:(NSStreamPropertyKey) key
|
|
{
|
|
return [super propertyForKey:key];
|
|
}
|
|
|
|
-(BOOL) setProperty:(id) property forKey:(NSStreamPropertyKey) key
|
|
{
|
|
return [super setProperty:property forKey:key];
|
|
}
|
|
|
|
-(NSStreamStatus) streamStatus
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
if(self.shared_state.error)
|
|
return NSStreamStatusError;
|
|
else if(!self.open_called && self.closed)
|
|
return NSStreamStatusNotOpen;
|
|
else if(self.open_called && self.shared_state.open)
|
|
return NSStreamStatusOpen;
|
|
else if(self.open_called)
|
|
return NSStreamStatusOpening;
|
|
else if(self.closed)
|
|
return NSStreamStatusClosed;
|
|
}
|
|
unreachable();
|
|
return 0;
|
|
}
|
|
|
|
-(NSError*) streamError
|
|
{
|
|
NSError* error = nil;
|
|
@synchronized(self.shared_state) {
|
|
error = self.shared_state.error;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
//list supported channel-binding types (highest security first!)
|
|
-(NSArray*) supportedChannelBindingTypes
|
|
{
|
|
//we made sure we only use PFS based ciphers for which tls-exporter can safely be used even with TLS1.2
|
|
//(see https://mitls.org/pages/attacks/3SHAKE)
|
|
return @[@"tls-exporter", @"tls-server-end-point"];
|
|
|
|
/*
|
|
//BUT: other implementations simply don't support tls-exporter on non-tls1.3 connections --> do the same for compatibility
|
|
if(self.isTLS13)
|
|
return @[@"tls-exporter", @"tls-server-end-point"];
|
|
return @[@"tls-server-end-point"];
|
|
*/
|
|
}
|
|
|
|
-(NSData* _Nullable) channelBindingDataForType:(NSString* _Nullable) type
|
|
{
|
|
//don't log a warning in this special case
|
|
if(type == nil)
|
|
return nil;
|
|
|
|
if([@"tls-exporter" isEqualToString:type])
|
|
return [self channelBindingData_TLSExporter];
|
|
else if([@"tls-server-end-point" isEqualToString:type])
|
|
return [self channelBindingData_TLSServerEndPoint];
|
|
else if([kServerDoesNotFollowXep0440Error isEqualToString:type])
|
|
return [kServerDoesNotFollowXep0440Error dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
unreachable(@"Trying to use unknown channel-binding type!", (@{@"type":type}));
|
|
}
|
|
|
|
-(BOOL) isTLS13
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
MLAssert([self streamStatus] >= NSStreamStatusOpen && [self streamStatus] < NSStreamStatusClosed, @"Stream must be open to call this method!", (@{@"streamStatus": @([self streamStatus])}));
|
|
MLAssert(self.shared_state.hasTLS, @"Stream must have TLS negotiated to call this method!");
|
|
nw_protocol_metadata_t p_metadata = nw_connection_copy_protocol_metadata(self.shared_state.connection, nw_protocol_copy_tls_definition());
|
|
MLAssert(nw_protocol_metadata_is_tls(p_metadata), @"Protocol metadata is not TLS!");
|
|
sec_protocol_metadata_t s_metadata = nw_tls_copy_sec_protocol_metadata(p_metadata);
|
|
return sec_protocol_metadata_get_negotiated_tls_protocol_version(s_metadata) == tls_protocol_version_TLSv13;
|
|
}
|
|
}
|
|
|
|
-(NSData*) channelBindingData_TLSExporter
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
MLAssert([self streamStatus] >= NSStreamStatusOpen && [self streamStatus] < NSStreamStatusClosed, @"Stream must be open to call this method!", (@{@"streamStatus": @([self streamStatus])}));
|
|
MLAssert(self.shared_state.hasTLS, @"Stream must have TLS negotiated to call this method!");
|
|
nw_protocol_metadata_t p_metadata = nw_connection_copy_protocol_metadata(self.shared_state.connection, nw_protocol_copy_tls_definition());
|
|
MLAssert(nw_protocol_metadata_is_tls(p_metadata), @"Protocol metadata is not TLS!");
|
|
sec_protocol_metadata_t s_metadata = nw_tls_copy_sec_protocol_metadata(p_metadata);
|
|
//see https://www.rfc-editor.org/rfc/rfc9266.html
|
|
return (NSData*)sec_protocol_metadata_create_secret(s_metadata, 24, "EXPORTER-Channel-Binding", 32);
|
|
}
|
|
}
|
|
|
|
-(NSData*) channelBindingData_TLSServerEndPoint
|
|
{
|
|
@synchronized(self.shared_state) {
|
|
MLAssert([self streamStatus] >= NSStreamStatusOpen && [self streamStatus] < NSStreamStatusClosed, @"Stream must be open to call this method!", (@{@"streamStatus": @([self streamStatus])}));
|
|
MLAssert(self.shared_state.hasTLS, @"Stream must have TLS negotiated to call this method!");
|
|
nw_protocol_metadata_t p_metadata = nw_connection_copy_protocol_metadata(self.shared_state.connection, nw_protocol_copy_tls_definition());
|
|
MLAssert(nw_protocol_metadata_is_tls(p_metadata), @"Protocol metadata is not TLS!");
|
|
sec_protocol_metadata_t s_metadata = nw_tls_copy_sec_protocol_metadata(p_metadata);
|
|
__block NSData* cert = nil;
|
|
sec_protocol_metadata_access_peer_certificate_chain(s_metadata, ^(sec_certificate_t certificate) {
|
|
if(cert == nil)
|
|
cert = (__bridge_transfer NSData*)SecCertificateCopyData(sec_certificate_copy_ref(certificate));
|
|
});
|
|
MLCrypto* crypto = [MLCrypto new];
|
|
NSString* signatureAlgo = [crypto getSignatureAlgoOfCert:cert];
|
|
DDLogDebug(@"Signature algo OID: %@", signatureAlgo);
|
|
//OIDs taken from https://www.rfc-editor.org/rfc/rfc3279#section-2.2.3 and "Updated by" RFCs
|
|
if([@"1.2.840.113549.2.5" isEqualToString:signatureAlgo]) //md5WithRSAEncryption
|
|
return [HelperTools sha256:cert]; //use sha256 as per RFC 5929
|
|
else if([@"1.3.14.3.2.26" isEqualToString:signatureAlgo]) //sha1WithRSAEncryption
|
|
return [HelperTools sha256:cert]; //use sha256 as per RFC 5929
|
|
else if([@"1.2.840.113549.1.1.11" isEqualToString:signatureAlgo]) //sha256WithRSAEncryption
|
|
return [HelperTools sha256:cert];
|
|
else if([@"1.2.840.113549.1.1.12" isEqualToString:signatureAlgo]) //sha384WithRSAEncryption (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (sha384WithRSAEncryption)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else if([@"1.2.840.113549.1.1.13" isEqualToString:signatureAlgo]) //sha512WithRSAEncryption
|
|
return [HelperTools sha512:cert];
|
|
else if([@"1.2.840.113549.1.1.14" isEqualToString:signatureAlgo]) //sha224WithRSAEncryption (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (sha224WithRSAEncryption)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else if([@"1.2.840.10045.4.1" isEqualToString:signatureAlgo]) //ecdsa-with-SHA1
|
|
return [HelperTools sha256:cert];
|
|
else if([@"1.2.840.10045.4.3.1" isEqualToString:signatureAlgo]) //ecdsa-with-SHA224 (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (ecdsa-with-SHA224)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else if([@"1.2.840.10045.4.3.2" isEqualToString:signatureAlgo]) //ecdsa-with-SHA256
|
|
return [HelperTools sha256:cert];
|
|
else if([@"1.2.840.10045.4.3.3" isEqualToString:signatureAlgo]) //ecdsa-with-SHA384 (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (ecdsa-with-SHA384)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else if([@"1.2.840.10045.4.3.4" isEqualToString:signatureAlgo]) //ecdsa-with-SHA512
|
|
return [HelperTools sha256:cert];
|
|
else if([@"1.3.6.1.5.5.7.6.32" isEqualToString:signatureAlgo]) //id-ecdsa-with-shake128 (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (id-ecdsa-with-shake128)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else if([@"1.3.6.1.5.5.7.6.33" isEqualToString:signatureAlgo]) //id-ecdsa-with-shake256 (not supported, return sha256, will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unsupported OID %@ (id-ecdsa-with-shake256)", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
else //all other algos use sha256 (that most probably will fail cb)
|
|
{
|
|
DDLogError(@"Using sha256 for unknown/unsupported OID: %@", signatureAlgo);
|
|
return [HelperTools sha256:cert];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void) stream:(NSStream*) stream handleEvent:(NSStreamEvent) event
|
|
{
|
|
//ignore event in this dummy delegate
|
|
DDLogVerbose(@"ignoring event in dummy delegate: %@ --> %ld", stream, (long)event);
|
|
}
|
|
|
|
@end
|