lundi 12 mars 2012

NSStream: TCP et SSL

Dans le chapitre 9 du Plus l'iPhone 3 de développement, Nous avons montré comment utiliser Bonjour et NSInputStream / NSOutputStream à faire des communications réseau. Dans ce cas, c'était pour faire un jeu simple en réseau, mais la technique est essentiellement la même pour tout type de réseau de communication de bas niveau.

Sur Mac OS X, NSStream, la superclasse des deux NSInputStream et NSOutputStream, a une méthode pratique de classe appelé getStreamsToHostNamed: port: fluxEntrée: fluxSortie qui crée une paire flux à un hôte distant spécifié. Pour une raison à mon insu ^ 1, Apple a choisi de ne pas inclure cette méthode de convenance dans le SDK de l'iPhone. La fonctionnalité sous-jacente est toujours là dans le SDK de l'iPhone, ils ont juste enlevé la méthode qui font qu'il est facile d'établir les ruisseaux. Pomme rectifié cette situation peu après par l'émission Technote TN QA1652, Ce qui donne une catégorie sur NSStream qui restaure la fonctionnalité manquante.

En utilisant cette méthode ne pouvait vraiment pas être plus facile. Dans notre TicTacToe demande, nous pourrions avoir créé un objet à un autre jeu OnlineSession spécifié par l'adresse IP et le port plutôt que par Bounjour, comme ceci:

    NSInputStream *is;
NSOutputStream *os;
[NSStream getStreamsToHostNamed:address
port:port
inputStream:&is
outputStream:&os
]
;
OnlineSession *session = [OnlineSession initWithInputStream:is outputStream:os];

Bien sûr, il y aurait encore besoin d'être une instance de TicTacToe s'exécutant sur le client distant en attente de connexions. Avec le jeu sur Internet, vous êtes généralement allez avoir besoin de quelque façon de trouver l'adresse et le port de la machine de se connecter, mais si un jeu est lancé et n'est pas un pare-feu, qui va vous relier à elle. En fait, vous pouvez utiliser cette même technique pour à peu près n'importe quel type de réseau de communication de bas niveau.

Une chose qui n'est pas évident, cependant, est de savoir comment vous pouvez crypter vos communications réseau utilisant le protocole SSL. Il est assez facile, mais il ya un piège significatif que le mentionner. Regardons initWithInputStream OnlineSession de: fluxSortie: et puis ajouter le support de SSL pour elle. Voici l'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;
}

Cette version crée un texte en clair, non cryptée de connexion au serveur distant. La façon dont nous activer le cryptage SSL est d'utiliser simplement setProperty: forKey: sur les deux ruisseaux, fixant les NSStreamSocketSecurityLevelKey clé d'une valeur qui spécifie la version du protocole SSL à utiliser. Si vous voulez dire NSStream d'utiliser la plus haute version prise en charge en commun avec la connexion à distance, spécifiez NSStreamSocketSecurityLevelKey. C'est ce que vous souhaitez généralement. Voici une nouvelle version de notre méthode d'initialisation qui nous permet de spécifier si vous souhaitez utiliser SSL ou non:
- (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;
}

Et, nous voilà! Eh bien, au moins dans la mesure où la documentation NSStream voudraient vous faire croire. Ce code ne fonctionnera que si tout est parfait. Cependant, par défaut, le support SSL dans NSStream est un peu paranoïaque. Il ne sera pas, par exemple, utiliser un certificat auto-signé ou un certificat expiré pour établir une connexion sécurisée. NSStream fait un certain nombre de contrôles de validité au moment d'établir la connexion sécurisée, et si elles ne sont pas tous passer, les flux semblent être valables, mais aucune donnée est envoyé ou reçu. C'est un peu frustrant, et il pourrait être là un moyen de savoir quand la connexion sécurisée a échoué, mais je n'ai pas été capable de le trouver dans la documentation, ou en utilisant Google. Il ya une erreur de domaine déclarés pour ces erreurs (NSStreamSocketSSLErrorDomain), mais dans mon expérimentation, pas d'erreurs est généré, les flux de même accepter octets pour le transfert, mais rien ne se passe ^ 2.

Quand il s'agit de sécurité, une paranoïa peu, c'est bien, mais il ya beaucoup de situations où tous que vous voulez est le chiffrement fourni par le protocole SSL, et vous n'avez pas vraiment à savoir si le certificat racine est valide (le certificat racine identifie l'autorité de certification qui a émis le certificat). Si vous faites une transaction e-commerce, alors vous voudrez certainement vous assurer que le certificat est valide et a été délivré par une autorité valide. Mais si tout ce que vous essayez de faire est de fournir un peu d'intimité des regards indiscrets, un certificat auto-signé est souvent parfaitement acceptable.

Malheureusement, NSStream ne fournit pas un moyen de dire «ne pas vérifier le certificat de racine». Heureusement, CFStream fait, et souvenez-vous: CFStream et NSStream sont sans frais comblé.

La façon dont nous pouvons désactiver ces contrôles de validité est de créer une instance de NSDictionary. Dans ce dictionnaire, nous allons spécifier les valeurs booléennes pour un certain nombre de système de valeurs définies clés comme kCFStreamSSLAllowsAnyRoot, kCFStreamSSLAllowsExpiredCertificates et kCFStreamSSLValidatesCertificateChain, précisant OUI ou NON en fonction de votre situation. SSL fait aussi la vérification sur le nom de l'hôte distant. Nous pouvons désactiver cette en remplaçant le nom de pairs en utilisant les clés et kCFStreamSSLPeerName lui la valeur NULL (bien que, techniquement, kCFNull).

Une fois que nous avons ce dictionnaire, nous pouvons nourrir nos deux flux en les jetant à leurs homologues des FC utilisant CFReadStreamSetProperty et CFWriteStreamSetProperty. Si nous voulions pour désactiver tous les contrôles de validité et de permettre à tout certificat signé par un certificat racine, nous le ferions:

- (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;
}

Si vous tentez d'utiliser SSL, mais votre connexion réseau ne semble pas faire quelque chose, essayez de désactiver la validité vérifie une à une pour voir si cela change votre résultat, ou utiliser le code ci-dessus pour les éteindre tous, qui devrait au moins vous dire si le problème que vous rencontrez est un échec pour établir une connexion sécurisée. Soyez prudent avec les applications d'expédition de ces contrôles s'est



1 La raison en est désormais knownst pour moi. La méthode originale utilisée NSHost qui n'est pas disponible sur l'iPhone - grâce Chris!
2 Si quelqu'un sait comment déterminer à quel moment cela se passe, s'il vous plaît faites le moi savoir et je vais poster l'information. Je suis sûr qu'il doit y avoir un moyen de détecter le défaut d'établir une connexion cryptée afin que vous puissiez «repli» à une option de moins en sécurité si vous voulez, mais je n'ai pas encore trouvé

Aucun commentaire: