// // MLStream.m // monalxmpp // // Created by Thilo Molitor on 11.04.21. // Copyright © 2021 Monal.im. All rights reserved. // #import #import "MLConstants.h" #import "MLStream.h" #import "HelperTools.h" #import @class MLCrypto; #define BUFFER_SIZE 4096 @interface MLSharedStreamState : NSObject @property (atomic, strong) id 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 _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) 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