TLSTool/TLSToolServer.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Core of the s_server implementation. |
*/ |
#import "TLSToolServer.h" |
@interface TLSToolServer () <NSNetServiceDelegate> |
@property (atomic, strong, readwrite) NSNetService * server; ///< The net service that manages our listening socket. |
@property (atomic, assign, readwrite) BOOL hasPrintedServerDidStartMessage;///< See the discussion in -netServiceDidPublish:. |
@property (atomic, copy, readwrite) NSArray * autorespondContent; ///< An array of data with which to autorespond. |
@property (atomic, copy, readwrite) NSArray * autorespondURLs; ///< An array of URLs indicating where the autoresponse data was read from. |
@property (atomic, assign, readwrite) NSUInteger nextAutorespondContentIndex; ///< The next item in the autorespondContent array to use. |
@end |
@implementation TLSToolServer |
- (instancetype)initWithServerIdentify:(SecIdentityRef)serverIdentity port:(NSInteger)port { |
NSParameterAssert(serverIdentity != NULL); |
NSParameterAssert( (port > 0) && (port <= 65536) ); |
self = [super init]; |
if (self != nil) { |
self->_serverIdentity = serverIdentity; |
CFRetain(self->_serverIdentity); |
self->_port = port; |
self->_clientCertificateRoots = @[]; |
} |
return self; |
} |
- (void)dealloc { |
// This object is not set up to be deallocated. |
assert(NO); |
} |
/*! Reads a single autorespond file, adding the content to the content array and the URL to the URLs array. |
* \param url The location of the autorespond file. |
* \param content The array to which to add the file's content. |
* \param URLs The arary to which to add the file's URL. |
*/ |
- (void)addAutorespondFileAtURL:(NSURL *)url toContent:(NSMutableArray *)content URLs:(NSMutableArray *)URLs { |
NSData * d; |
NSError * error; |
d = [NSData dataWithContentsOfURL:url options:0 error:&error]; |
if (d == nil) { |
[self logWithFormat:@"server failed to read autorespond content: %@", url.path]; |
} else { |
[content addObject:d]; |
[URLs addObject:url]; |
} |
} |
/*! Reads autorespond content from the location on disk into an in-memory array. |
*/ |
- (void)readAutorespondContent { |
BOOL success; |
NSNumber * isDir; |
NSMutableArray * content; |
NSMutableArray * URLs; |
content = [[NSMutableArray alloc] init]; |
URLs = [[NSMutableArray alloc] init]; |
success = [self.autorespondContentURL getResourceValue:&isDir forKey:NSURLIsDirectoryKey error:NULL]; |
if (success && isDir.boolValue) { |
for (NSURL * childURL in [[NSFileManager defaultManager] enumeratorAtURL:self.autorespondContentURL includingPropertiesForKeys:@[ NSURLIsDirectoryKey ] options:NSDirectoryEnumerationSkipsSubdirectoryDescendants | NSDirectoryEnumerationSkipsHiddenFiles errorHandler:nil]) { |
success = [childURL getResourceValue:&isDir forKey:NSURLIsDirectoryKey error:NULL]; |
if (success && ! isDir.boolValue ) { |
[self addAutorespondFileAtURL:childURL toContent:content URLs:URLs]; |
} |
} |
} else { |
[self addAutorespondFileAtURL:self.autorespondContentURL toContent:content URLs:URLs]; |
} |
self.autorespondContent = content; |
self.autorespondURLs = URLs; |
assert([self.autorespondContent count] == [self.autorespondURLs count]); |
} |
- (void)run { |
[self logWithFormat:@"server identity: %@", [TLSUtilities subjectSummaryForIdentity:self.serverIdentity]]; |
// Read in our autoresponse content. |
if (self.autorespondContentURL != nil) { |
[self readAutorespondContent]; |
if (self.autorespondContent.count == 0) { |
[self logWithFormat:@"server could not read any autorespond content"]; |
exit(EXIT_FAILURE); |
} |
} |
// Create the NSNetService object that handles incoming connections. We don't care |
// about the domain or name (they both have reasonable defaults) but we have to supply |
// a type. |
self.server = [[NSNetService alloc] initWithDomain:@"" type:@"_x-TLSTool._tcp." name:@"" port:(int) self.port]; |
self.server.delegate = self; |
[self.server publishWithOptions:NSNetServiceListenForConnections]; |
// Run the server. We can't use dispatch_main because NSNetService really wants a |
// run loop <rdar://problem/17960834>. We have to have an exit() after the |
// -[NSRunLoop run] because it can return under oddball circumstances. |
[[NSRunLoop currentRunLoop] run]; |
exit(EXIT_FAILURE); |
} |
- (void)startConnectionWithInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream responseData:(NSData *)responseData { |
BOOL success; |
SSLContextRef context; |
assert(inputStream != nil); |
assert(outputStream != nil); |
// Apply TLS settings. Not that we always disable client trust evaluation because |
// the built-in support exhibits some very odd behaviour. Rather, we do it all |
// manually in -evaluateClientTrustManually. |
success = [inputStream setProperty:@{ |
(__bridge NSString *) kCFStreamSSLIsServer: @YES, |
(__bridge NSString *) kCFStreamSSLCertificates: @[ (__bridge id) self.serverIdentity ], |
(__bridge NSString *) kCFStreamSSLValidatesCertificateChain: @NO |
} forKey:(__bridge NSString *) kCFStreamPropertySSLSettings]; |
assert(success); |
// Requesting a client certificate can't be done directly using socket stream properties; |
// instead we get the Secure Transport context and set it up there. |
context = (__bridge SSLContextRef) [inputStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLContext]; |
assert(context != NULL); |
success = SSLSetClientSideAuthenticate(context, self.clientCertificateMode) == errSecSuccess; |
assert(success); |
// Previous versions of the code used to call SSLSetCertificateAuthorities here. Now we |
// completely disable client trust evaluation and do it all ourselves in -evaluateClientTrustManually. |
[super startConnectionWithInputStream:inputStream outputStream:outputStream responseData:responseData]; |
} |
/*! Called when a connection is established to do client trust evaluation. |
*/ |
- (void)evaluateClientTrustManually{ |
OSStatus err; |
NSString * errorMessage; |
SecTrustRef trust; |
SecTrustResultType trustResult; |
errorMessage = nil; |
trust = (__bridge SecTrustRef) [self.inputStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust]; |
if (trust == NULL) { |
// On some OS X versions prior to 10.11, if you set the client-side authentication mode |
// to kAlwaysAuthenticate the systems acts like you set it to mode kTryAuthenticate |
// <rdar://problem/18816667>. So, if we're in the mode kAlwaysAuthenticate and we didn't |
// get a trust object, we fail. |
if (self.clientCertificateMode == kAlwaysAuthenticate) { |
errorMessage = @"client certificate required but not presented"; |
} |
} else if ( ! self.disableClientTrustEvaluation ) { |
// In -startConnectionWithInputStream:outputStream:responseData: we always disable the |
// default client trust evaluation so here we have to do the trust evaluation manually. |
// If anything fails, the connection fails. |
errorMessage = @"manual client trust evaluation failed"; |
if (self.clientCertificateRoots.count == 0) { |
err = errSecSuccess; |
} else { |
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef) self.clientCertificateRoots); |
} |
if (err == errSecSuccess) { |
err = SecTrustEvaluate(trust, &trustResult); |
if (err == errSecSuccess) { |
if ( (trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified) ) { |
errorMessage = nil; |
} |
} |
} |
} |
if (errorMessage != nil) { |
[self logWithFormat:@"%@", errorMessage]; |
[self stopConnectionWithError:[NSError errorWithDomain:NSOSStatusErrorDomain code:errSSLXCertChainInvalid userInfo:nil]]; |
} |
} |
- (void)connectionDidOpen { |
[self evaluateClientTrustManually]; |
} |
- (void)netServiceDidPublish:(NSNetService *)sender { |
assert(sender == self.server); |
// If you have multiple Bonjour registration domains (you most commonly see this |
// when Back to My Mac is enabled), -netServiceDidPublish: is called multiple |
// times, once for each domain. We don't want to confused our users by printing |
// multiple "server did start" messages, so we only print that message on the |
// first call. |
// |
// Note that hasPrintedServerDidStartMessage is never cleared because the server |
// runs until someone terminates the entire process. |
if ( ! self.hasPrintedServerDidStartMessage ) { |
[self logWithFormat:@"server did start"]; |
self.hasPrintedServerDidStartMessage = YES; |
} |
} |
- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict { |
assert(sender == self.server); |
[self logWithFormat:@"server startup failed %@ / %@", errorDict[NSNetServicesErrorDomain], errorDict[NSNetServicesErrorCode]]; |
exit(EXIT_FAILURE); |
} |
- (void)netService:(NSNetService *)sender didAcceptConnectionWithInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream { |
dispatch_async(self.queue, ^{ |
assert(sender == self.server); |
if (self.isStarted) { |
// We already have a connection in place; reject this connection. |
[inputStream open]; |
[outputStream open]; |
[inputStream close]; |
[outputStream close]; |
} else { |
NSData * responseData; |
// If we have autoresponse content, work out which content to send next. |
responseData = nil; |
if (self.autorespondContent != nil) { |
NSString * responseFileNamePath; |
responseData = self.autorespondContent[self.nextAutorespondContentIndex]; |
responseFileNamePath = [self.autorespondURLs[self.nextAutorespondContentIndex] path].lastPathComponent; |
self.nextAutorespondContentIndex += 1; |
if (self.nextAutorespondContentIndex == self.autorespondContent.count) { |
self.nextAutorespondContentIndex = 0; |
} |
[self logWithFormat:@"server will respond with contents of '%@'", responseFileNamePath]; |
} |
// Start a connection based on these streams. |
[self startConnectionWithInputStream:inputStream outputStream:outputStream responseData:responseData]; |
} |
}); |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-05-23