Chiron (10/11) : gourmandises

8 views
Skip to first unread message

Laurent Caillette

unread,
Feb 23, 2018, 1:06:57 AM2/23/18
to tec...@googlegroups.com

Pour se remettre du précédent épisode sur le contrat réactif ça va faire du bien de regarder quelques petits utilitaires de Chiron, utilisables indépendemment des fonctions de connectivité.


=== Configuration

La bibliothèque **Chiron-configuration** fournit de quoi lire des fichiers de configuration ou des paramètres en ligne de commande. Cet outil a déjà été mentionné sur Techos alors qu'il portait le nom de "Wrench"
. L'idée c'est de définir les propriétés dans une interface dont les méthodes renvoient les types attendus, de rajouter des métadonnées ou du comportement dans une classe ``ConfigurationFactory``, puis de lire une source de données avec toujours le meilleur typage possible et les meilleurs retours d'erreur possibles. Il y a 3 ans aucune autre bibliothèque Java ne fournissait ce niveau de fonctionnalité et ça n'a pas du changer depuis.


=== Diagnostic

Dans **Chiron-toolbox**, l'interface ``Diagnostic`` définit le contrat pour représenter le paramétrage de l'application sous forme textuelle, avec des indentations. En général au démarrage on imprime ce rapport dans le journal d'exécution, ou on l'affiche dans la fenêtre "À propos".

La classe ``GeneralDiagnostic`` produit un rapport avec les propriétés système, les variables d'environnement et le classpath de Java. 

La classes``InternetProxyAccessDiagnostic`` montre les paramètres de connexion à un proxy, tels qu'utilisés par le Downend.

La classes ``ConfigurationDiagnostic`` développe le contenu d'un objet ``Configuration`` (celui cité plus haut, issu de ``ConfigurationFactory``).

On peut bien sûr ajouter ses propres diagnostics. Un ``Diagnostic`` est quelque chose qui possède un nom, des paires clé-valeur sous forme de ``String``, et une liste de sous-diagnostics.



=== Proxy HTTP

Dans **Chiron-fixture**, le ``ConnectProxy`` est un proxy HTTP supportant uniquement la commande CONNECT, c'est à dire qu'il se contente paresseusement de faire le passe-plat sans s'intéresser au contenu HTTP.

Quel intérêt par rapport à un proxy HTTP embarquable en pur Java comme "LittleProxy" 
? Le ``ConnectProxy`` permet d'ajouter du délai dans la transmission, chose que LittleProxy ne permet pas (les points d'extension de LittleProxy sont synchrones). Grâce à l'ajout de latence, on simule un réseau encombré, ou un proxy HTTP qui ne répercute pas correctement la perte de connexion au serveur-cible. 

Attention le ``ConnectProxy`` est conçu pour tester unitairement la connexion à travers un proxy HTTP. Il n'est pas fait pour encaisser de fortes charges (contrairement à LittleProxy).


=== Redirection de port

Toujours dans **Chiron-fixture** on a le ``PortForwarder``, petit frère du ``ConnectProxy`` qui redirige tout ce qu'il reçoit vers un autre port, éventuellement sur une autre machine. 



=== Client HTTP

Dans **Chiron-toolbox**, le ``NettyHttpClient`` est un client HTTP léger basé sur Netty. À l'origine il est conçu à l'origine pour écrire des tests, mais c'est aussi lui qui sert à envoyer des requêtes HTTP à Twilio.

<<<
 final Recorder recorder = new NettyHttpClient.Recorder() ;
 final NettyHttpClient httpClient = new NettyHttpClient() ;
 httpClient.start() ;
 httpClient.httpGet( "http://localhost:8080/test", recorder ) ;
 final NettyHttpClient.Outcome response = recorder.nextResponse() ;
 WatchedResponseAssert.assertThat( response )
     .isComplete()
     .hasStatusCode( HttpResponseStatus.OK )
 ;
>>>

Le ``NettyHttpClient`` peut utiliser une boucle d'événements externe. Dans sa version de base, il ne contient aucun état. Quand on exécute une requête, on passe une instance de ``NettyHttpClient.Watcher`` qui définit des méthodes pour traiter la réussite, l'échec, le délai de réponse expiré. Il n'y a que le code effectuant la connexion HTTP qui conserve une référence sur le  ``Watcher`` (à travers les retours d'appel et la tâche planifiée pour indiquer l'expiration du délai de réponse). Même si ce n'est pas une fonctionnalité en soi c'est marrant de voir que la boucle d'événements Netty permet ça.

Dans l'exemple ci-dessus on remarque que l'instance du ``Watcher`` est un enregistreur qui accumule toutes les réponses reçues (y compris les erreurs comme l'expiration du délai de réponse). Comme ça on transforme un comportement asynchrone en comportement synchrone : le code du test bloque jusqu'à obtenir une réponse, ce qui permet de bien montrer la séquence attendue.

Selon les goûts et les besoins on pourra préférer le "Netty HTTP Client"
de Tim Boudreau, qui offre beaucoup plus de fonctionnalités, y compris pour les tests.


=== Certificats TLS

Dans **Chiron-toolbox**, le ``KeystoreAccess`` encapsule l'accès à un Keystore Java protégé par mot de passe. L'idée, c'est que l'application soit livrée avec un Keystore par défaut, versionné en même temps que les sources. Dans les paramètres de démarrage de l'application, il suffit de préciser le mot de passe pour le certificat correspondant à l'alias par défaut dans le Keystore par défaut. De cette façon la clé privée n'est dévoilée que sur les serveurs où ont lieu le déploiement. Mais souvent il faut tester un certificat alternatif en production, ou tester du code avec un certificat autosigné. La solution c'est que l'alias et l'URL de la ressource du Keystore soient des valeurs par défaut. Concrètement, la représentation textuelle du ``KeystoreAccess`` est la suivante :

<<<
[alias:]password[@URL-with-no-authority]
>>>

Le ``KeystoreAccess`` a un bon ami, l'``Autosigner``. L'``Autosigner`` sait générer une paire Keystore-Truststore, en mémoire ou dans un fichier. L'interface programmatique basée sur le modèle de conception "Bâtisseur" ("Builder") permet plein de variations sympas. Par exemple on peut charger un Keystore existant, ou exporter la clé privée dans un fichier. Comme ça on n'a pas besoin de redémarrer Wireshark à chaque fois qu'on démarre un test utilisant l'``Autosigner`` (Wireshark sait décoder le TLS si on lui file un fichier avec la clé privée). Et une fois que tout est chargé ou généré, l'``Autosigner`` produit l'objet ``KeystoreAccess`` approprié.

On a vu que le ``KeystoreAccess`` fournissait le Keystore sous forme d'un flux binaire tout droit sorti d'une URL. Ça peut être un fichier (``file:///``) mais si on veut tout faire en mémoire il y a moyen (pour accélérer les tests notamment). L'``Autosigner`` crée alors une URL spéciale avec un protocole ``keystore://`` et un ``URLStreamHandler`` renvoyant la copie d'un tableau d'octets en mémoire. Je sais que tu ne savais pas qu'on pouvait faire ça.

Voici un exemple d'utilisation de l'``Autosigner`` :

<<<
    final CertificateHolder certificateHolder = Autosigner.builder()
        .commonNameAndDefaults( "some.name.com" )
        .createCertificate( Feature.SUBJECT_KEY_IDENTIFIER )
    ;
    final KeyStore keystore = certificateHolder.createKeystore() ;
    final KeyStore trustStore = certificateHolder.createTruststore() ;
    final String keypass = certificateHolder.certificateDescription().keypass ;
    // etc.

    final FileGenerator fileGenerator1 = certificateHolder
        .withKeystoreFile( new File( "my-own-keystore.jks" ) )
        .withTruststoreFile( new File( "my-own-truststore.jks" ) )
        .withPrivateKeyFile( new File( "my-own-private-key.jks" ) )
    ;
    fileGenerator1.generateFiles() ;
    
    final FileGenerator fileGenerator2 = certificateHolder
        .withDirectory( new File( "securitystores" ) ) ;
    fileGenerator2.generateFiles() ; 
    
    final InterningGenerator interningGenerator = certificateHolder.interning() ;
    final KeystoreAccess keystoreAccess = interningGenerator.keystoreAccess() ;
    final URL truststoreAccess = interningGenerator.truststoreUrl() ;
>>>

Plus moyen de prétendre c'est trop compliqué d'écrire des tests pour le HTTPS !


=== Temps

Dans **Chiron-Toolbox** on trouve quelques trucs pour modéliser l'écoulement du temps.

``Clock`` est une interface pour obtenir une mesure du temps universel.

Le ``Pulse`` planifie des tâches à intervalles réguliers (chaque heure, minute, ou seconde) au moment où on passe à l'unité suivante. C'est beaucoup moins souple qu'un ``ScheduledExecutorService``, mais on a la garantie que le moment de l'exécution ne dérive pas car le ``Pulse`` replanifie chaque exécution au plus juste. Le ``PulseModulator`` est une implantation du``Pulse`` basée sur une horloge instrumentée pour les tests. Le ``Pulse`` est une alternative ultra-légère à "Quartz"
.

Un ``Stamp`` est un identifiant unique basé sur un horodatage à la milliseconde près, et un compteur pour différencier des instances créées durant la même milliseconde. Un ``Stamp.Generator`` génère des instances de ``Stamp`` avec des garanties d'unicité. Le code pour supporter des accès concurrents aux compteurs sans verrouillage est sympa.

Le ``TimeKit`` fournit une instance de ``Clock``, une instance de ``Stamp.Factory``, un générateur de ``Designator`` et une instance de ``Pulse``. Le ``TimeKit`` garantit qu'on utilise toujours la même ``Clock``. On peut créer un ``TimeKit`` avec une ``UpdateableClock`` pour modifier l'écoulement du temps à la main (dans les tests) et donc déclencher des événements de ``Pulse``.

Ces classes souffrent maintenant de la concurrence de `Java 8` qui offre notamment ``java.util.Clock``. De plus, Chiron utilise toujours les classes de Joda Time, et il faudrait migrer vers leur équivalent `Java 8`.




=== Collections

Guava fournit à peu près tout ce dont on peut rêver pour triturer les collections de Java sans se faire mal. Toujours dans **Chiron-toolbox** on trouve quelques classes pour boucher les derniers trous.

``KeyHolder`` est une interface pour les objets identifiables par une clé.

``KeyHolderMap`` est une ``java.util.Map`` pour référencer des instances de ``KeyHolder``. Chaque élément est référencé par une clé qui correspond à celle fournie par la valeur. Autrement dit la ``KeyHolderMap`` est une collection qui garantit que chaque élément est unique en regard de sa clé.

``ImmutableKeyHolderMap`` est une variante immuable qui respecte les conventions établies par Guava (y compris le ``Builder``). Une ``ImmutableKeyHolderMap`` garantit automatiquement la cohérence clé-valeur, la non-nullité, ainsi que le respect de l'ordre d'ajout lors d'une itération.


=== Enumération extensible

``Autoconstant`` est une classe de base de **Chiron-toolbox** pour une énumération typée qui supporte l'extension (et qui ne repose donc pas sur ``java.lang.Enum``). Une instance d'``Autoconstant`` garantit que son nom renvoyé par ``name()`` est identique à celui de la déclaration dans du code Java. On écrit donc ``static final MyConstant FOO = new MyConstant() ;`` Il y a aussi quelques trucs pour passer un paramètre de type par instance, et obtenir la liste de toutes les instances. Une classe dérivée d'``Autoconstant`` garantit que chaque instance (y compris parmi les sous-classes) possède  un nom unique.



=== Retours d'erreur

Dans **Chiron-middle**, la classe ``TypedNotice`` fournit la base pour des retours d'erreurs avec un code normalisé (basé sur une ``enum``) et un message personnalisable (par défaut défini par l'``enum``). On peut la dériver de façon à utiliser n'importe quelle énumération qui implante l'interface ``EnumeratedMessageKind`` qui reprend les méthodes d'une ``enum``. (Oui ça serait bien de migrer vers ``Autoconstant`` qui est un peu là pour ça, plus le temps passe plus c'est dur de garder un code homogène.)

Une ``TypedNotice`` est destinée à être envoyée au Downend, c'est à dire qu'elle ne doit pas contenir d'information sensible ; notamment elle ne contient pas d'exception Java.



=== SSH 

Dans **Chiron-ssh** il y a la merveille des merveilles : une coquille pour exécuter des programmes sur une machine distante, sans autre prérequis qu'un accès par SSH. Le ``SshDriver`` exécute des programmes tous nus, tandis que le ``SshJavaDriver`` exécute des programmes Java en recopiant tous les fichiers accessibles dans le classpath courant. Autrement dit, tu écris une classe exécutable qui communique par les entrées-sorties standard, tu appuies sur le bouton, et magiquement elle s'exécute sur une ou plusieurs machines distantes pourvu que tu aies préalablement déployé une JVM et ta clé publique. Pour des tests d'intégration c'est l'arme absolue : à part le petit délai pour la synchronisation des fichiers ``.class`` tu as l'impression de sauter la phase construction-déploiement. 

Le ``SshDriver`` supporte plein d'options sympathiques : 
- Une condition arbitraire sur la sortie standard pour indiquer que le démarrage a eu lieu.
- Une ``CompletableFuture`` pour savoir quand le démarrage a eu lieu.
- Une ``CompletableFuture`` pour savoir quand l'arrêt a eu lieu.
- Un arrêt immédiat avec ``#stop()``.
- Des fils d'exécution convenablement nommé pour des journaux d'exécution plus lisibles.
- Une option pour ne pas allouer un pseudo-terminal ("pty"), ce qui conserve la distinction entre la sortie d'erreur et la sortie standard, et empêche que le démon SSH tue le processus à la fin de la session (un peu le même effet que le ``nohup``).

Le ``SshDriver`` utilise l'excellentissime bibliothèque SshJ, ainsi que Ssh-Agent pour éviter d'avoir à saisir son mot de passe à chaque exécution. Il faut donc déployer préalablement sa clé publique.

Le ``SshJavaDriver`` étend le ``SshDriver``. Tu lui files juste :
- Le répertoire d'exécution.
- Le chemin vers l'exécutable de la JVM.
- Des propriétés système.
- Un énumérateur des ``.jar`` ou ``.class`` à copier sur la machine distante (par défaut le classpath courant).
- La classe exécutable et les paramètres.
- Un port JMX si on veut faire du JMX non-authentifié.

Oui on peut passer des options pour profiler ou déboguer à distance.

Le ``SshJavaDriver`` devrait à lui tout seul apporter des millions d'étoiles au projet Chiron sur GitHub. Ça serait bien qu'un contributeur fasse évoluer le code pour ne pas lancer une commande SCP pour chaque fichier.


=== Autres

Allez encore quelques petits trucs de **Chiron-toolbox**.

``ToStringTools`` pour faire des ``toString()`` dans la joie, notamment extraire le nom de classe le plus lisible possible.

``TcpPortBooker`` pour trouver un port TCP de libre. C'est utile pour écrire des tests qui ne se marchent pas dessus. Le principe est d'affecter des ports "sentinelle" à intervalle régulier pour marquer les plages de port utilisées par un ``TcpPortBooker``. Comme l'ouverture d'une Socket est une opération atomique, un autre ``TcpPortBooker`` dans une autre machine virtuelle ira chercher une autre plage de ports. 

``SafeSystemProperty`` pour enrober des propriétés système dans de jolies constantes typées. Comme ça on sait si la propriété est définie, si elle bien formée, quelle est sa valeur, et comme une ``SafeSystemProperty`` est immuable on peut recharger sa valeur en créant une nouvelle instance. En standard on a des ``SafeSystemProperty`` pour ``String``, ``List< String >``, ``Boolean``, ``Integer``, ``File``, ``Hostname``. On peut ajouter ses propres types.

``AbstractService`` pour un une unité d'exécution qui se démarre et s'arrête. C'est la classe de base de ``SshDriver`` mais c'est drôlement pratique pour écrire son propre conteneur d'application embarquable. Voir la Javadoc de "``Service``"
pour une comparaison avec son homologue dans Guava.

On a aussi les classiques : ``Holder``, une référence à un objet qu'on ne peut affecter qu'une seule fois, et ``Lazy``, une référence à un objet à travers une fonction évaluée une seule fois et le plus tard possible.


Henri Tremblay

unread,
Feb 23, 2018, 9:16:48 AM2/23/18
to tec...@googlegroups.com
Plein de choses utiles là-dedans. Beaucoup de choses sont déjà dans Spring. Le scheduler par exemple. Mais si veux avoir une stack autonome, tu n'as pas trop le choix de refaire.

Le SshJavaDrive me semble très utile pour pas mal de trucs que je fais. J'y jetterais un coup d'oeil à l'occasion.

Au sujet des proxy, c'est rigolo, je joues avec ça c'est temps-ci. Il y a toxiproxy qui fait ça mais il faut le lancer à l'extérieur de la JVM donc c'est pas pratique. Par contre il y a NetCrusher qui est prometteur. Dans mon cas, je veux aussi pouvoir avoir des coupures de connexions, du jittering et patata. Nous avons aussi un truc en interne mais ça fait pas assez.

--
Vous recevez ce message, car vous êtes abonné au groupe Google Groupes "techos".
Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse techos+unsubscribe@googlegroups.com.
Pour envoyer un message à ce groupe, envoyez un e-mail à l'adresse tec...@googlegroups.com.
Visitez ce groupe à l'adresse https://groups.google.com/group/techos.
Pour obtenir davantage d'options, consultez la page https://groups.google.com/d/optout.

Laurent Caillette

unread,
Feb 23, 2018, 9:47:58 AM2/23/18
to tec...@googlegroups.com
Salut Henri,

Ouais se traîner Spring c'était pas une option. 

NetCrusher a l'air vraiment bien (pur Java, embarquable, NIO etc.) mais c'est un proxy TCP/UDP qui n'a pas connaissance du protocole HTTP, donc pas vraiment la même bestiole que LittleProxy. Pour comparer à ce qu'il y a dans Chiron j'ai l'impression que NetCrusher est plus proche du ``PortForwarder`` que du ``ConnectProxy``. Même si 95 % du code est le même ce sont deux utilisations différentes. 

Henri Tremblay

unread,
Feb 23, 2018, 11:30:06 AM2/23/18
to tec...@googlegroups.com
En fait dans mon cas, je n'ai pas besoin de Layer 7 comme ils disent. Je veux juste faire déconner la connexion.

Tu l'utilises pour quoi?

Laurent Caillette

unread,
Feb 23, 2018, 12:28:28 PM2/23/18
to tec...@googlegroups.com
Pour ceux qui découvrent le monde merveilleux des proxies je précise d'abord quelques trucs.

Un proxy TCP/UDP comme NetCrusher (ou le ``PortForwarder`` de Chiron) se connecte à une adresse donnée (par exemple ``meteofrance.fr:80``) et écoute sur une adresse locale (par exemple ``localhost:9999``). Le proxy fait alors le passe-plat entre les deux pour les requêtes et les réponses. On obtient le même comportement avec le remote port forwarding de SSH. C'est une façon d'intercepter le trafic réseau sans bricoler les règles de routage au niveau de l'OS. Un inconvénient c'est que si on utilise ce type de proxy avec une requête HTTP le navigateur enverra l'adresse locale dans les en-têtes et il se peut que le serveur n'aime pas, surtout s'il y a du HTTPS qui implique une vérification du nom de domaine (c'est pour ça que je prenais ``meteofrance.fr`` comme exemple, avec du pur HTTP).

Un proxy HTTP c'est un serveur auquel on se connecte avec une requête HTTP pour indiquer à quel serveur on veut se connecter. Un proxy HTTP voit passer le contenu HTTP et peut modifier (cas typique : cache et filtrage de publicité). 

Un proxy HTTP supporte une connexion spéciale avec le verbe CONNECT qui dit de passer le contenu tel quel. Techniquement c'est comme le proxy TCP cité plus haut, sauf que le serveur cible est indiqué dans la requête CONNECT qui fait partie de la spec du proxy HTTP. Un proxy HTTP utilisé avec CONNECT permet de faire passer du HTTPS car il ne touche pas au contenu.

Le ``ConnectProxy`` de Chiron est un proxy HTTP qui supporte uniquement CONNECT. C'est utile pour les tests. J'ai eu quelques misères en faisant passer des WebSockets avec un proxy HTTP. En parlant de misères je n'insiste pas sur Jetty et CometD qui ne supportaient pas le proxy HTTP imposé par l'infrastructure réseau de notre prospect. Je parle du ``ProxyHandler`` de Netty qu'il a fallu bricoler sérieusement parce qu'il broutait les WebSockets. (J'ai créé une issue, je crois que c'est corrigé maintenant.) Donc il a fallu des tests unitaires avec un proxy HTTP embarquable et j'ai écrit le ``ConnectProxy`` pour ça. Il sert également à mettre du délai dans une démo interactive qui fait tourner clients et serveur (et proxy) dans la même JVM. 

Le ``PortForwarder`` de Chiron sert également pour les tests unitaires. Le serveur-qui-fait-tout-dans-un-seul-processus possède une console d'administration Web. Cette console est accessible uniquement par port atttaché à l'interface ``loopback``, c'est à dire qu'on ne peut s'y connecter que si l'initiateur de la connexion est sur la même machine. Mais alors comment administrer à distance ? Grâce à un tunnel SSH. Donc il faut que la console supporte un port qui n'est pas celui indiqué dans le champ ``Host`` de la requête HTTP. Bon ça c'est une chose mais il y a également l'histoire d'Unix qui refuse qu'on ouvre des ports avec un numéro inférieur à 1000 si on n'est pas ``root``. Ça se résoud avec une règle DNAT dans iptables, mais après la requête HTTPS arrive avec un port différent du port réel. Il y a aussi le HTTP qui doit être redirigé vers du HTTPS. Mais attention, alors que le processus écoute sur les ports 8080 et 8443, il doit rediriger vers 443 à cause de la règle iptables. Donc il faut prévoir des redirection et des erreurs. Et en plus on peut débrayer le HTTPS. Arrivé là j'espère que tout le monde a l'impression que c'est compliqué. Donc il faut des tests unitaires. Donc il faut quelque chose pour effectuer la redirection de port dans les tests unitaires et le ``PortForwarder`` est là pour ça.

Tout cela peut donner envie de critiquer l'hallucinante complexité du protocole HTTP (surtout quand on empile HTTPS, WebSocket et les proxies) mais vis-à-vis de tout ça je préfère rester humble parce que ça résoud un paquet de problèmes plus ou moins contradictoires. L'élément le plus criticable, c'est le déploiement de proxies HTTP dans les infrastructures d'entreprise, alors qu'une majorité de sites utilise HTTPS. Mais ça ne risque pas de changer de sitôt.

J'imagine que toutes les misères que je décris, c'est ce qu'affrontent quotidiennement les administrateurs système. Là j'ai juste une architecture qui fait rentrer tous les problèmes dans une JVM, avec la capacité de tester les solutions unitairement.








Reply all
Reply to author
Forward
0 new messages