I'm using Network to establish a connection to various SMTP servers. For SMTP servers that use SSL on port 465, using the following code to establish the connection and communicate with the server works fine (read/write code omitted):
nw_endpoint_t endpoint = nw_endpoint_create_host("smtp.example.com", "465");
nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DEFAULT_CONFIGURATION, NW_PARAMETERS_DEFAULT_CONFIGURATION);
nw_connection_t connection = nw_connection_create(endpoint, parameters);
nw_connection_set_queue(connection, dispatch_get_main_queue());
nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
//...
}
nw_connection_start(connection);
For servers on port 587 that require an insecure connection at start then renegotiate a TLS handshake with the STARTTLS
command, I change the parameters like so:
nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
This does allow me to establish the connection to the SMTP server but I'm stumped on how to initiate TLS on the established connection after invoking the STARTTLS
command and receiving an OK response from the SMTP server. How do I add TLS options to an existing connected nw_connection_t
connection?
Solved! I open the insecure connection and communicate with the SMTP server directly using the framer until it's ready to start TLS:
connected to smtp.example.com:587
<- 220 Example SMTP...
-> EHLO localhost
<- 250-example.com
<- 250-SIZE 28319744
<- 250-STARTTLS
<- 250-SMTPUTF8
<- 250 CHUNKING
-> STARTTLS
<- 220 2.0.0 Ready to start TLS
//[_framer enablePassthrough];
-> EHLO localhost
<- 250-example.com
<- 250-SIZE 28319744
<- 250-AUTH LOGIN PLAIN ATOKEN ATOKEN2 WSTOKEN WETOKEN
<- 250-SMTPUTF8
<- 250 CHUNKING
-> AUTH PLAIN AHVzZXJAZXhhbXBsZS5jb20AcGFzc3dvcmQ=
<- 235 2.7.0 Authentication successful
-> QUIT
<- 221 2.0.0 Bye
disconnected from smtp.example.com:587
This required implementing an actual nw_framer_set_output_handler()
and allowing the delegate to send through the framer
directly (framerSendData:).
Once the "220 2.0.0" message is received, the delegate calls enablePassthrough
and subsequent read/writes from the delegate use the connection
object and not directly through the framer.
Doing it this way traps all data sent through the framer
until enablePassthrough
is called so no need to use parseResult.
Thanks for your help. Revised code below.
//STARTTLSFramer.h
#import <Cocoa/Cocoa.h>
#import <Network/Network.h>
@protocol STARTTLSFramerDelegate;
@interface STARTTLSFramer : NSObject
@property (assign) id <STARTTLSFramerDelegate> delegate;
@property (readonly) BOOL isPassthroughEnabled;
- (nw_connection_t)connectionWithSTARTTLSToEndpoint:(nw_endpoint_t)endpoint;
- (void)framerSendData:(NSData *)data;
- (void)enablePassthrough;
@end
@protocol STARTTLSFramerDelegate <NSObject>
@required
- (void)STARTTLSFramer:(STARTTLSFramer *)framer didReceiveData:(NSData *)data;
@end
//STARTTLSFramer.m
#import "STARTTLSFramer.h"
@interface STARTTLSFramer ()
@property (nonatomic, strong) NSMutableData *accumulated;
@property (nonatomic, strong) nw_framer_start_handler_t start;
@property (nonatomic, assign) BOOL passthroughEnabled;
@end
@implementation STARTTLSFramer{
nw_framer_t _framer;
}
- (instancetype)init {
if (self = [super init]) {
_accumulated = [NSMutableData new];
_passthroughEnabled = NO;
}
return self;
}
- (BOOL)isPassthroughEnabled { return self.passthroughEnabled; }
- (void)enablePassthrough {
// MARK: go go gadget pass through
self.passthroughEnabled = YES;
nw_protocol_options_t protocol_options = nw_tls_create_options();
nw_framer_prepend_application_protocol(_framer, protocol_options);
nw_release(protocol_options);
nw_framer_pass_through_input(_framer);
nw_framer_pass_through_output(_framer);
nw_framer_mark_ready(_framer);
}
- (void)framerSendData:(NSData *)data {
dispatch_data_t dispatch_data = dispatch_data_create(data.bytes, (size_t)data.length, dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_DEFAULT);
nw_framer_write_output_data(_framer, dispatch_data);
}
- (nw_framer_start_handler_t)startHandler {
if (!_start) {
nw_framer_start_result_t (^start)(nw_framer_t) = ^nw_framer_start_result_t(nw_framer_t framer) {
_framer = framer;
nw_framer_set_output_handler(framer, ^(nw_framer_t framer, nw_framer_message_t message, size_t message_length, bool is_complete) {
if (message) {
dispatch_data_t data = nw_framer_message_copy_object_value(message, "data");
nw_framer_write_output_data(framer, data);
nw_release(data);
} else {
nw_framer_write_output_no_copy(framer, message_length);
}
});
nw_framer_set_wakeup_handler(framer, ^(nw_framer_t framer) {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"nw_framer_set_wakeup_handler not implemented" userInfo:nil];
});
nw_framer_set_stop_handler(framer, ^bool(nw_framer_t framer) {
return true;
});
nw_framer_set_cleanup_handler(framer, ^(nw_framer_t framer) {
});
nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) {
// MARK: accumulate
BOOL complete = NO;
while (!complete) {
complete = nw_framer_parse_input(framer, 1, 2048, NULL, ^size_t(uint8_t *buffer, size_t buffer_length, bool is_complete) {
if (buffer) [self.accumulated appendBytes:(const void *)buffer length:(NSUInteger)buffer_length];
return buffer_length;
});
}
// MARK: parse
[self.delegate STARTTLSFramer:self didReceiveData:_accumulated];
self.accumulated.length = 0;
return 0;
});
return nw_framer_start_result_will_mark_ready;
};
self.start = start;
}
return _start;
}
- (nw_connection_t)connectionWithSTARTTLSToEndpoint:(nw_endpoint_t)endpoint {
nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION);
nw_protocol_stack_t protocol_stack = nw_parameters_copy_default_protocol_stack(parameters);
nw_protocol_definition_t protocol_definition = nw_framer_create_definition("STARTTLSFramer", NW_FRAMER_CREATE_FLAGS_DEFAULT, self.startHandler);
nw_protocol_options_t protocol_options = nw_framer_create_options(protocol_definition);
nw_protocol_stack_prepend_application_protocol(protocol_stack, protocol_options);
nw_connection_t connection = nw_connection_create(endpoint, parameters);
nw_release(protocol_options);
nw_release(protocol_definition);
nw_release(protocol_stack);
nw_release(parameters);
return connection;
}
@end
Calls from the delegate:
nw_endpoint_t endpoint = nw_endpoint_create_host("smtp.example.com", "587");
_framer = [STARTTLSFramer new];
_framer.delegate = self;
_connection = [_framer connectionWithSTARTTLSToEndpoint:endpoint];
- (void)STARTTLSFramer:(STARTTLSFramer *)framer didReceiveData:(NSData *)data {
//parse the data pseudocode:
if ([dataString hasPrefix:@"220 2.0.0"]) {
[framer enablePassthrough];
[self sendData:@"EHLO localhost\r\n".data];
} else if ([dataString hasPrefix:@"220 "]) {
[self sendData:@"EHLO localhost\r\n".data];
} else if ([dataString containsString:@"250-STARTTLS"]) {
[self sendData:@"STARTTLS\r\n".data];
}
}
- (void)sendData:(NSData *)data {
if (!_framer.isPassthroughEnabled) {
[_framer framerSendData:data];
return;
}
//...
nw_connection_send(_connection, dispatch_data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, is_complete, ^(nw_error_t error) { /*...*/ });
}
- (void)receive {
nw_connection_receive(_connection, 1, UINT32_MAX, ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { /*...*/ });
}