Service Web et proxys

Beaucoup d'applications mobiles reposent sur des services web. L'intelligence se trouve sur un serveur tandis que l'iPhone sert de simple terminal, pour afficher des informations à l'utilisateur ou lui permettre d'en saisir. Il peut y avoir tout un tas de bonnes raisons à choisir cette architecture :

  • Lorsque la partie métier réclame une puissance de calcul élevée. C'est l'architecture retenue par Siri : la reconnaissance et la synthèse vocales sont faites localement mais la partie intelligence artificielle, qui demande de grosses bases de connaissances et un moteur d'inférence complexe, est réalisée sur les serveurs d'Apple.
  • Lorsque l'application nécessite un énorme volume de donnée. C'est le cas des applications comme Google Maps ou Plans, qui ne pourraient pas se permettre de stocker sur le téléphone toute la cartographie du monde entier.
  • Pour des raisons de sécurité. Une application iPhone peut être copiée, désassemblée, analysée, etc. Aussi les banques préfèrent-elles produire des applications ne contenant aucune information sensible. Dans ce cas, l'application n'est qu'une simple UIWebView dans laquelle s'affiche un site web mobile servi par des serveurs sécurisés.

Le piège dans cette architecture est que souvent, l'iPhone n'est pas connecté directement au réseau internet. En 3G, la connexion passe par les proxys de l'opérateur qui peuvent bloquer des ports, mettre des données en cache, voire modifier des pages à la volée dans le but de minimiser le trafic sur les antennes relais. Sur un WiFi public, la borne d'accès peut rediriger à la première connexion vers une page demandant d'entrer des identifiants. C'est typiquement le cas des connexions offertes par les hôtels et les restaurants.

Lorsque ces blocages et redirections intempestives se produisent pendant la consultation d'une page web dans un navigateur, l'utilisateur s'en aperçoit immédiatement. En revanche, lorsque c'est du code qui appelle un service web, cela peut passer complètement inaperçu. Votre application recevra un status HTTP 200 et en déduira que votre serveur a bien répondu à l'appel alors qu'en réalité, ce code indique juste vous avez reçu avec succès la page d'accueil du proxy ou la réponse mise en cache d'une précédente requête.

C'est la situation illustrée ici. À la première requête ci-dessus, le proxy transmet correctement la demande au serveur. Au passage, il stocke la réponse en cache.

Lors des requêtes suivantes, considérant que la ressource n'a pas changé, le proxy retourne directement la réponse en cache sans consulter le serveur. En théorie, ce genre de chose ne devrait jamais arriver si votre serveur retourne les valeurs appropriées dans les champs Cache-Control: et Expires: de ses réponses ; en pratique, certains proxys n'en font qu'à leur tête et ignorent ces directives.

Ajouter un token

Pour me prémunir contre ce problème, j'ai pris l'habitude d'ajouter un token à mes transactions. Il s'agit d'une simple valeur numérique aléatoire générée par le mobile à chaque appel. Le serveur, lorsqu'il reçoit ce token, effectue un calcul simple dessus puis retourne le résultat avec la réponse. Si l'application reçoit la bonne valeur, elle sait avec une quasi certitude que sa requête a bien été traitée par le bon serveur. Sinon, elle est en droit de supposer que la réponse a été générée par un proxy et que le serveur n'a jamais vu passer la requête.

Bien sûr, cette solution implique que vous ayez la main sur le code côté serveur pour y implémenter les fonctions nécessaires. On supposera ici que c'est le cas.

Mais passons à la pratique ! Voici une fonction basique servant à envoyer un bloc de données à une API REST en HTTP. Pour des raisons de lisibilité, le code de gestion d'erreur est omis, ainsi que le code servant à fixer les paramètres accessoires de la requête (type MIME du contenu, support de la compression, délai avant timeout, etc.).

- (void)callWebService:(NSURL *)theURL withData:(NSData *)theData
{
  NSMutableURLRequest * req;

  // Construction de la requête.

  req = [NSMutableURLRequest requestWithURL:theURL];
  [req setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
  [req setHTTPBody:theData];
  [req setHTTPMethod:@"POST"];

  // Ajout du token. Le token attendu sera le résultat
  // d'une opération simple.

  sentToken = random() % 1000000;
  expectedToken = (sentToken * 17) + 3;
  [req setValue:[NSString stringWithFormat:@"%d", sentToken] forHTTPHeaderField:@"MyToken"];

  // Envoi de la requête au serveur.

  serverConnection = [[NSURLConnection alloc] initWithRequest:req delegate:self];
}

Le cœur de l'algorithme est bien sûr constitué des deux lignes qui choisissent un token aléatoire, l'écrivent dans un champ d'entête HTTP, puis calculent le token qui sera attendu en réponse.

N'importe quelle fonction peut ici faire l'affaire. Nul besoin de choisir une formule obscure et compliquée, il suffit juste d'être sûr que le token attendu sera toujours différent de celui envoyé. On prendra juste garde aux débordements de capacité. En effet, si le mobile effectue le calcul sur 32 bits et le serveur sur 64, une opération provoquant un overflow ne se comportera pas de façon identique sur le client et sur le serveur. Ici, on évite ce cas en ramenant le token entre 0 et 999999 grâce à l'opérateur modulo.

Voici maintenant la fonction qui traite la réponse (reportez-vous à la documentation du protocole NSURLConnectionDelegate pour plus d'informations sur ce que doit faire cette fonction). Ici encore, la majeure partie de l'implémentation est omise pour des raisons de simplicité et seule la partie qui nous intéresse est présentée.

- (void)connection:(NSURLConnection *)theConnection didReceiveResponse:(NSURLResponse *)theResponse
{
  NSString  * value;

  value = [[((NSHTTPURLResponse *) theResponse) allHeaderFields] objectForKey:@"MyToken"];
  if ([value intValue] == expectedToken)
  {
    // La requête a bien été traitée par notre serveur.
  }
  else
  {
    // La réponse a probablement été générée par un proxy.
  }
}

Le fonctionnement est trivial : on récupère la valeur écrite par le serveur dans les entêtes HTTP et on vérifie qu'elle est conforme à celle attendu.

Si tel n'est pas le cas, l'action à entreprendre dépend de votre application. Elle peut par exemple afficher un message d'erreur à l'utilisateur, ou bien tenter de répéter la requête immédiatement, ou encore ignorer silencieusement le problème dans l'espoir que la requête suivante se fera alors que l'utilisateur sera connecté au réseau via un proxy plus coopératif.