In Chapter 9 of
More iPhone 3 Development, we showed how to use Bonjour and
NSInputStream /
NSOutputStream to do network communications. In that case, it was for making a simple networkable game, but the technique is essentially the same for any kind of low-level network communications.
On Mac OS X,
NSStream, the superclass of both
NSInputStream and
NSOutputStream, has a class convenience method called
getStreamsToHostNamed:port:inputStream:outputStream which creates a stream pair to a specified remote host. For some reason unbeknownst to me¹, Apple chose not to include this convenience method in the iPhone SDK. The underlying functionality is still there in the iPhone SDK, they just removed the method that make it easy to establish the streams. Apple rectified that situation shortly thereafter by issuing Technote
TN QA1652, which gives a category on
NSStream that restores the missing functionality.
Using this method really couldn't be easier. In our
TicTacToe application, we could have established an
OnlineSession object to another game specified by IP address and port rather than through Bounjour, like so:
NSInputStream *is;
NSOutputStream *os;
[NSStream getStreamsToHostNamed:address
port:port
inputStream:&is
outputStream:&os];
OnlineSession *session = [OnlineSession initWithInputStream:is outputStream:os];
Of course, there would still need to be an instance of
TicTacToe running on the remote client listening for connections. With internet play, you're generally going to need some way of finding the address and port of the machine to connect to, but if a game is running and isn't firewalled, that will connect you to it. In fact, you can use this same technique for pretty much any kind of low-level network communications.
One thing that's not obvious, however, is how you can encrypt your network communications using SSL. It's pretty easy, but there is a significant gotcha that's worth mentioning. Let's look at
OnlineSession's
initWithInputStream:outputStream: and then add SSL support for it. Here is the original:
- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream
{
if (self = [super init]) {
inStream = [theInStream retain];
outStream = [theOutStream retain];
[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
inStream.delegate = self;
outStream.delegate = self;
if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];
if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];
packetQueue = [[NSMutableArray alloc] init];
}
return self;
}
That version creates a plaintext, unencrypted connection to the remote server. The way that we enable SSL encryption is to simply use
setProperty:forKey: on both streams, setting the key
NSStreamSocketSecurityLevelKey to a value that specifies the version of SSL to use. If you want to tell
NSStream to use the highest version supported in common with the remote connection, specify
NSStreamSocketSecurityLevelKey. That's what you'll usually want. Here's a new version of our
init method that lets us specify whether to use SSL or not:
- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream useSSL:(BOOL)useSSL
{
if (self = [super init]) {
inStream = [theInStream retain];
outStream = [theOutStream retain];
[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
inStream.delegate = self;
outStream.delegate = self;
if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];
if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];
packetQueue = [[NSMutableArray alloc] init];
if (useSSL)
{
[inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey];
[outStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey];
}
}
return self;
}
And, we're done! Well, at least insofar as the
NSStream documentation would have you believe. This code will work if everything is perfect. However, by default, SSL support in
NSStream is a little paranoid. It won't, for example, use a self-signed certificate or an expired certificate to establish a secure connection.
NSStream does a number of validity checks when establishing the secure connection, and if they don't all pass,
the streams appear to be valid, but no data gets sent or received. This is somewhat frustrating, and it could be there's a way to find out when the secure connection failed, but I haven't been able to find it in the documentation, or using Google. There is an error domain declared for these errors (
NSStreamSocketSSLErrorDomain), but in my experimentation, no errors gets generated, the streams even accept bytes for transfer, but nothing happens².
When it comes to security, a little paranoia is good, but there are many situations where all you want is the encryption provided by SSL, and you don't really care about whether the root certificate is valid (the
root certificate identifies the certificate authority that issued the certificate). If you're doing an e-Commerce transaction, then you almost certainly want to make sure the certificate is valid and was issued by a valid authority. But if all you're trying to do is provide a little privacy from prying eyes, a self-signed certificate is often perfectly acceptable.
Unfortunately,
NSStream doesn't provide a way to say "don't check the root certificate". Fortunately,
CFStream does, and remember:
CFStream and
NSStream are
toll-free bridged.
The way we can turn off these validity checks is by creating an instance of
NSDictionary. In this dictionary, we'll specify Boolean values for a number of system-defined key values like
kCFStreamSSLAllowsAnyRoot,
kCFStreamSSLAllowsExpiredCertificates, and
kCFStreamSSLValidatesCertificateChain, specifying either
YES or
NO as appropriate to your situation. SSL also does verification on the name of the remote peer. We can turn that off by overriding the peer name using the key
kCFStreamSSLPeerName and setting it to null (well, technically,
kCFNull).
Once we have this dictionary, we can feed it to our stream pair by casting them to their
CF counterpart using
CFReadStreamSetProperty and
CFWriteStreamSetProperty. If we wanted to turn off all the validity checks and allow any certificate signed by any root certificate, we would do this:
- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream useSSL:(BOOL)useSSL
{
if (self = [super init]) {
inStream = [theInStream retain];
outStream = [theOutStream retain];
[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
inStream.delegate = self;
outStream.delegate = self;
if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];
if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];
packetQueue = [[NSMutableArray alloc] init];
if (useSSL)
{
[inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey];
[outStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates,
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot,
[NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain,
kCFNull,kCFStreamSSLPeerName,
nil];
CFReadStreamSetProperty((CFReadStreamRef)inStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
CFWriteStreamSetProperty((CFWriteStreamRef)outStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
}
}
return self;
}
If you're attempting to use SSL, but your network connection doesn't seem to do anything, try turning off the validity checks one at a time to see if that changes your result, or use the code above to turn them all off, which should at least tell you if the problem you're having is a failure to establish a secure connection. Be careful shipping applications with these checks turned
1 The reason is now knownst to me. The original method used NSHost which isn't available on the iPhone - thanks Chris!2 If anyone knows how to determine when this is happening, please let me know and I'll post the information. I'm sure there has to be a way to detect the failure to establish an encrypted connection so you can "fallback" to a less secure option if you want to, but I haven't found it yet