Pourquoi ce document ? Il sâagit dâune reprise Ă©crite du cours que jâai pu donner pendant le stream de 25 heures organisĂ© par h25 Ă lâoccasion du confinement provoquĂ© par lâĂ©pidĂ©mie de Coronavirus qui frappe actuellement lâEurope. Une rediffusion est disponible ici.
Objectif
Ce cours a pour objectif de prĂ©senter les aspects essentiels du langage JavaScript, en particulier ceux utiles au joueur de Capture The Flag (CTF). En rĂ©alitĂ©, jâavais besoin dâun prĂ©texte pour parler de JavaScriptâŠ
Aucune connaissance du langage JavaScript nâest requise.
Attention ! On ne parlera pas ici de XSS ou autres vulnérabilités client.
Introduction
JavaScript est un langage qui fait débat, toujours trÚs controversé. Plusieurs
enquĂȘtes, notamment celles de GitHub (qui
vient dâailleurs dâacquĂ©rir npm), le placent en tĂȘte des langages les plus
utilisés au monde (on peut débattre sur ce que plus utilisé signifie, certes)
et il est aujourdâhui absolument partout : dans nos navigateurs, dans nos
serveurs web, dans nos appareils mobiles, dans nos applications de bureauâŠ
Mais il est aussi dans nos CTFs ! Et on le retrouve dans plusieurs catĂ©gories : web, bien sur, mais aussi crypto, forensique, malware, mobile, etc. Le JavaScript peut bien sur ĂȘtre le sujet de lâĂ©preuve en elle-mĂȘme, mais il peut Ă©galement ĂȘtre simplement le moyen dâintĂ©ragir avec lâĂ©preuve (comme on lâa vu lors du dernier stream H25).
Le problĂšme, câest que beaucoup de joueurs de CTFs ne connaissent pas vraiment le JavaScript. Câest Ă dire : beaucoup de joueurs sont capables dâinfĂ©rer ce que fait un bout de code en se ratachant Ă des mots clĂ©s, Ă des noms de mĂ©thode, ou Ă des constructions de programmation orientĂ©e objet qui leurs sont familiers parce que similaires Ă ce qui se fait dans dâautres langages. Le bat blesse quand il sâagit dâĂ©crire du code, et plus seulement en lire. Ou bien, lorsque le code Ă©tudiĂ© prĂ©sente des fonctionnalitĂ©s avancĂ©es de JavaScript, en particulier celles relatives Ă la programmation asynchrone.
Fondamentaux
Commençons par lâĂ©tude dâun bout de code qui va nous permettre de clarifier certains aspects simples, mais pourtant souvent sources de confusion, du langageâŠ
// Quelques notions de JavaScript
// D'autres constructions utiles sont présentées plus tard
// Il n'y a pas de fonction main
// Chaque fichier est un script indépendant, évalué comme tel
// Des fonctionnalités d'importation permettent de diviser les sources
// en plusieurs fichiers. Voir l'instruction "import" et la fonction
// "require" pour plus d'informations.
// Le mot clé var permet d'associer un identifiant (le "nom" de la variable)
// a une valeur
var name = 'some_value';
// Il est possible de réaffecter cet identifiant
// On peut utiliser des guillemets simples, ou doubles
// Attention, var est trĂšs souvent banni, au profit de let et const
name = "some 'other' value";
name = 'some "other" value';
name = 'some \'other\' value';
// On peut aussi utiliser l'accent grave pour profiter des "template literals"
// (en français : littéraux de gabarits, quel plaisir), et donc de
// fonctionnalités d'interpolation
// Les accents graves permettent également de faire du multi-lignes
name = `I am ${Math.floor(Math.random() * 10)} years old!`;
// (Optionnel)
// Pour plus d'informations, voir le "hoisting"
// https://www.w3schools.com/js/js_hoisting.asp
// Les noms déclarés à l'aide de let et const ne SONT PAS hoistés
// Le mot clé let est une autre maniÚre d'associer un identifiant à une valeur
// Tout comme var, on peut réaffecter l'identifiant
// En revanche, un identifiant déclaré avec let a une durée de vie
// limitée : le bloc courant
let other_name = name;
other_name = "my own name";
// Finalement, le mot clé const permet également d'associer un idenfiant
// Ă une valeur
// Grosse diffĂ©rence : un idenfiant dĂ©clarĂ© avec const ne peut ĂȘtre rĂ©affectĂ©
const constant_name = "this is so constant";
// !! Ne fonctionne pas
// constant_name = "nop, sorry";
// Attention, seul l'identifiant est garanti constant
// La valeur ne n'est pas
const some_object = {
prop_one: 'value one',
prop_two: 'value two'
};
// Aucun problĂšme : on met Ă jour la valeur et non l'identifiant
some_object.prop_two = 'value twotwo';
some_object.prop_three = 'value three';
// Les ; en fin d'instruction sont obligatoires (d'aprĂšs le standard)
// Mais, dans de nombreux cas, un code sans ; pourra ĂȘtre correctement exĂ©cutĂ©
// parce que l'interpréteur peut les ajouter à la volée
// Le mot clé function permet de déclarer une fonction
function some_function(some_argument) {
// Si un indentifiant a été déclaré mais n'a pas été initialisé,
// sa valeur est undefined
if (some_argument !== undefined) {
console.log(`I got an argument: ${some_argument}!`);
}
}
// Ne génÚre aucune erreur, some_argument aura simplement une valeur de
// undefined au sein de la fonction some_function
some_function();
// JavaScript dispose d'un systĂšme de typage dynamique
some_function("some string");
some_function(1337);
// Les fonctions sont des objets de premiĂšre classe
// (en anglais : "first class citizen")
// On peut donc les passer comme argument, les utiliser comme
// valeur de retour, etc
some_function(some_function);
// Il existe une autre syntaxe permettant de déclarer une
// fonction : la fat arrow
// On parle aussi de fonctions fléchées
// Ces fonctions introduisent bien un nouveau bloc (scope) mais pas
// de this ou de super
a => console.log(a);
(a, b) => a + b;
(a, b, c) => {
let d = a + b;
return d * c;
}
// Toutes les fonctions déclarées ci-dessus sont anonymes : ce sont des valeurs
// qui n'ont pas été associées à un identifiant
// A l'instar de var, function est parfois banni au profit de
// la construction suivante
const my_function = (arg1, arg2) => {
console.log(arg1, arg2);
return arg1 * arg2;
};
// Les tableaux
const some_array = [1, 2, 3, 4];
some_array.push('hello'); // Le tableau n'est pas fortement typé
// Ou bien, via constructeur
const another_array = new Array(); // Renvoie []
// ContrĂŽler la longueur initiale
const more_array = new Array(5); // Renvoie un tableau de 5 éléments vides
// Attention, si plus de deux arguments, les arguments deviennent des valeurs
const awesome_array = new Array(5, 6, 7); // [5, 6, 7]
// Les objets
// JSON : JavaScript Object Notation
const my_object = {
property_one: 42,
property_two: 'hello',
method_one: () => 'hello from method_one',
method_two: function () {
return this.property_one;
}
};
// AccĂšs par .
console.log(my_object.property_one);
// AccĂšs par [] (genre dictionnaire)
console.log(my_object['method_one']);
const attr = 'method_one'; console.log(my_object[attr]);
// Boucle while
let i = 0;
while (i < 10) {
console.log(i++); // on a aussi --i, ++i et i--
}
// Boucle for
const arr = ['hello', 'les', 'amis'];
// Simple
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// AccĂšs aux valeurs
// On remarque le const : un nouveau scope est introduit Ă chaque tour
for (const i of arr) {
console.log(i);
}
// AccĂšs aux indices
// MĂȘme principe pour le const
for (const i in arr) {
console.log(i);
}
On peut exĂ©cuter du JavaScript dans diffĂ©rents environnements. Le plus Ă©vident est un navigateur WEB : on navigue sur un site web qui utilise du JavaScript. Mais on peut aussi exĂ©cuter du JavaScript en dehors du navigateur, par exemple Ă lâaide de Node.js.
Si on part du principe que deux environnements dâexĂ©cution implĂ©mentent la mĂȘme version du standard, tous les Ă©lĂ©ments de syntaxe que lâon vient dâĂ©tudier seront valables et produiront des rĂ©sultats identiques dans les deux environnements.
En revanche, le contexte sera différent. Un bout de code JavaScript
exĂ©cutĂ© dans le navigateur peut accĂ©der Ă lâobjet document, qui
représente la page web. Node.js ne peut accéder à cet objet car il ne
fait pas sens dans son contexte. En revanche, Node.js fourni le module
fs qui est une API pour intéragir avec le systÚme de fichier. Le
navigateur ne propose pas cette API, car il nâa pas accĂšs au systĂšme de
fichier (câest en train
dâĂ©voluer
ceci dit).
En fonction de lâenvironnement, les connaissances requises pour gĂ©rer une Ă©preuve peuvent ĂȘtre trĂšs diffĂ©rentes. Une Ă©preuve qui se passe dans le navigateur pourra demander dâĂȘtre Ă lâaise avec les manipulations du DOM tandis quâune Ă©preuve serveur demandera de connaitre le fonctionnement de la librairie Express.
Constructions utiles
Maintenant que nous avons les bases, voyons quelques constructions qui reviennent frĂ©quemment en CTFâŠ
Manipulations ASCII
Bout de code adaptĂ© de lâĂ©tape 1 des Hexpresso FIC Quals 2019. Write-up complet disponible ici.
for (i = 0; i < some_string.length; i++) {
if (some_string.charCodeAt(i) + flag.charCodeAt(i) + i * 42 != some_array[i]) {
alert("NOPE");
return;
}
}
On remarque:
- La méthode
charCodeAt(i)qui, lorsque appliquée sur une chaßne de caractÚre, nous renvoie la valeur numérique du iÚme caractÚre de la chaßne. - La fonction
alert, uniquement disponible dans le navigateur, qui permet de rapidement afficher un message. La fonction est utile pour du dĂ©bogage car elle bloque entiĂšrement le flux dâexĂ©cution. - Quelques opĂ©rateurs comme
+et*, ici appliquĂ©s Ă des nombres. Dâautres existent, y compris les opĂ©rateurs bits Ă bits.
Voyons un code solution :
let r = "";
for (const i in flag) {
r += String.fromCharCode(some_array[i] - some_string.charCodeAt(i) - i * 42);
}
On remarque alors que String.fromCharCode(i) (méthode statique) permet
lâopĂ©ration inverse de charCodeAt(i). On obtient alors le caractĂšre
(attention, il sâagit en fait dâune chaĂźne de caractĂšres de longueur
- correspondant au nombre
i.
Utilisateurs de Python, attention, JavaScript permet lâopĂ©rateur +
sur des chaĂźnes de caractĂšre mais il ne permet pas la multiplication. On
utilise pour ça la méthode statique String.repeat().
On commence Ă comprendre que JavaScript fourni de trĂšs nombreux utilitaires. Le langage dispose Ă©galement dâun Ă©cosystĂšme imposant. Il est trĂšs rare de ne pas trouver une dĂ©pendance permettant de rĂ©pondre Ă son problĂšme.
Minification
Beaucoup dâĂ©preuves prĂ©sentent de la minification. Le premier cas dâusage en JavaScript est certainement la rĂ©duction de la taille du code. Parce que le code JavaScript client est tĂ©lĂ©chargĂ© depuis un serveur WEB, on cherche Ă rĂ©duire la taille de ce code afin dâaccĂ©lĂ©rer sa rĂ©cupĂ©ration.
Note : dans la grande majoritĂ© des cas, un code minifiĂ© ne sâexĂ©cute pas plus rapidement quâun code non-minifiĂ©. Il est simplement plus rapide Ă tĂ©lĂ©charger.
Dans le contexte dâun CTF, la minification devient une forme dâobfuscation. Le code suivant:
const obj = {
firstname: 'Geo',
lastname: 'Le Berlingot'
};
function getFullName(person) {
return person.firstname + ' ' + person.lastname
}
console.log(getFullName(obj));
Devient, une fois minifié avec JavaScript Minifier :
function getFullName(e){return e.firstname+" "+e.lastname}const obj={firstname:"Geo",lastname:"Le Berlingot"}
console.log(getFullName(obj))
On utilise alors le minimum de caractĂšres possibles. Les espaces sont retirĂ©s, les noms sont manglĂ©s⊠Mais ce code est tout Ă fait lisible et on peut trĂšs facilement retomber proche du code initial en utilisant un outil tel que lâoutil de formatage de Chrome.
Mais il existe aussi des outils proposant une obfuscation beaucoup plus franche. Par exemple, avec JavaScript Obfucator, notre code initial devient:
const _0x1616=['lastname','firstname','Geo','Le\x20Berlingot'];(function(_0x177eb0,_0x1616a6){const _0x1e9abe=function(_0x204014){while(--_0x204014){_0x177eb0['push'](_0x177eb0['shift']());}};_0x1e9abe(++_0x1616a6);}(_0x1616,0xc6));const _0x1e9a=function(_0x177eb0,_0x1616a6){_0x177eb0=_0x177eb0-0x0;let _0x1e9abe=_0x1616[_0x177eb0];return _0x1e9abe;};const obj={'firstname':_0x1e9a('0x0'),'lastname':_0x1e9a('0x1')};function getFullName(_0xb6014e){return _0xb6014e[_0x1e9a('0x3')]+'\x20'+_0xb6014e[_0x1e9a('0x2')];}console['log'](getFullName(obj));
Et on a quelque chose qui est dĂ©jĂ beaucoup moins lisible. La mĂ©thodologie varie en fonction du temps que lâon peut investir et le type dâobfuscation mais dans le cas gĂ©nĂ©ral, on peut commencer par faire passer le code dans un outil de dĂ©obfuscation tel que de4js et ensuite terminer Ă la main. On va chercher principalement Ă :
- Renommer les noms de variables et functions
- Extraire les différents éléments, notamment les chaßnes de caractÚres, que les obfuscateurs ont tendance à regrouper dans des tableaux
- Retirer le code inutile
Jâavais dĂ©jĂ Ă©voquĂ© lâobfuscation en JavaScript dans ce writeup dâune Ă©preuve des qualifications de la Nuit Du Hack 2018.
Encodages
JavaScript donne accĂšs, mĂȘme sans dĂ©pendances, Ă plusieurs encodages dont les challenges ont tendance Ă abuser. Si on ne les connait pas, ils peuvent donner lâimpression quâon doit gĂ©rer un chiffrement complexe ou une bizarrerie JavaScript alors quâun simple appel de fonction suffit. On a :
unescape()pour gĂ©rer les sĂ©quences hexadĂ©cimalesdecodeURI(), semblable Ăunescape()- base64
- API Buffer, qui expose plusieurs fonctions intéressantes, notamment
toStringetfrom
Attention également à la fonction eval.
Quelques writeups
Les writeups suivant couvrent bien le spectre dâĂ©preuves quâon peut rencontrer.
- JS SAFE 2.0 - Google CTF 2018, par LiveOverflow
- JS Kiddie - picoCTF 2019, par radekk
- Javascript Obfusqué - Qualification SIGSEGV1, par Jean MARSAULT
- Web 2.0 - Flare-On 5
Concurrence
JavaScript inplĂ©mente une boucle dâĂ©vĂšnement (en anglais: event loop), qui
lâammĂšne Ă ne pas ĂȘtre bloquant. En Python, lorsquâon fait une requĂȘte HTTP
avec le module request, il est impossible (du moins, sans efforts) dâexĂ©cuter
plus de code pendant que cette requĂȘte a lieu. On dit alors que lâappel Python
est bloquant, car il monopolise le runtime et ne ârend pas la mainâ.
Attention ! Si vous dĂ©couvrez la concurrence, il est trĂšs important que vous preniez le temps dâexĂ©cuter les bouts de code fournis. Essayez de deviner la sortie puis voyez si votre prĂ©diction est correcte.
Callbacks
Lâapproche la plus simpliste est celle du callback. La fonction permettant de faire une requĂȘte HTTP prend alors un autre paramĂštre : une fonction, qui sera appelĂ©e lorsque le rĂ©sultat sera disponible. Ce callback reçoit en paramĂštre le (ou les, y compris les erreurs) rĂ©sultat(s) de lâopĂ©ration asynchrone.
const request = require('request');
request.get('https://geographer.fr/', (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result.body.slice(0, 500));
});
console.log('Where am I?');
On remarque que la méthode request.get retourne immédiatement. Le flux
dâexĂ©cution nâest donc pas bloquĂ© pendant la requĂȘte HTTP, on peut continuer
dâexĂ©cuter le code qui se trouve plus bas.
Le callback, qui est ici une fonction anonyme, sera appelĂ© une fois que le rĂ©sultat de lâopĂ©ration asynchrone sera disponible. Le runtime fera alors une pause dans lâexĂ©cution de son fil âprincipalâ afin dâexĂ©cuter le callback.
const request = require('request');
console.log('BEFORE request call!');
request.get('https://geographer.fr/', (err, result) => {
console.log('START of callback!');
if (err) {
console.error(err);
return;
}
console.log(`Body has a length of ${result.body.length}.`);
console.log('END of callback!');
});
console.log('AFTER request call!');
Il est plus rare de voir des callbacks aujourdâhui en raison du callback hell. Câest cette situation oĂč on se retrouve Ă imbriquer plusieurs callbacks, ce qui rend le code trĂšs peu lisible. Imaginons un code qui effectue les actions suivantes : requĂȘte HTTP, sauvegarde du rĂ©sultat dans un fichier texte, puis duplication de ce fichier. Le code aurait cette allure :
const request = require('request');
const fs = require('fs');
request.get('https://geographer.fr/', (e, r) => {
if (e) {
console.error(e);
return;
}
fs.writeFile('/tmp/output.txt', r.body, (e, r) => {
if (e) {
console.error(e);
return;
}
fs.copyFile('/tmp/output.txt', '/tmp/output_copy.txt', (e, r) => {
if (e) {
console.error(e);
return;
}
console.log('Work is done!');
});
});
});
Quel plaisir⊠Il est trop difficile de suivre le fil de ce code, sans compter quâil vire dangereusement Ă droite. Voyons comment les promesses peuvent nous aider !
Promise
LâAPI Promise est un systĂšme qui vient se substituer au callback quand il sâagit de gĂ©rer des appels asynchrones. Voyons Ă quoi ressemble le code prĂ©cĂ©dent avec des promesses :
const request = require('request-promise-native');
const fs = require('fs').promises;
request.get('https://geographer.fr/')
.then(r => fs.writeFile('/tmp/result_promise.txt', r))
.then(r => fs.copyFile('/tmp/result_promise.txt', '/tmp/copy_promise.txt'))
.then(() => console.log('Work is done!'))
.catch(e => console.error(e));
Beaucoup mieux ! On remarque deux méthodes : then et catch, et
lâobjet sur lequel elles sont appliquĂ©es nâapparait pas clairement. Il
sâagit en fait dâun objet Promise qui vient enrober les valeurs de
retour des appels asynchrones. Pour mieux comprendre, étudions ce code
qui implémente une fonction retournant une promesse :
const fs = require('fs');
const writeFilePromise = (path, data) => new Promise((resolve, reject) => {
fs.writeFile(path, data, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
writeFilePromise('/tmp/hello.txt', 'Hello!')
.then(() => console.log('Work is done!'))
.catch(e => console.error(e));
On obtient donc une fonction writeFilePromise qui se substitue Ă
fs.writeFile. Cette nouvelle fonction ne prend pas de callback en paramĂštre
mais retourne immédiatement une variable Promise. La fonction à exécuter est
passée en paramÚtre au constructeur Promise. Ce constructeur reçoit deux
autres fonctions en paramĂštre : resolve, et reject. La premiĂšre sera
appelĂ©e lorsquâil faut transfĂ©rer une valeur de retour qui nâest pas une
erreur. La seconde sera appelée afin de transférer une erreur.
Toujours au sein de la fonction anonyme passée en paramÚtre à Promise,
on retrouve finalement lâappel Ă fs.writeFile. Ce dernier prend
toujours un callback en paramĂštre mais maintenant que nous avons une
promesse à disposition, nous pouvons utiliser sa fonction de résolution
(resolve) et de rejet (reject) afin dâexfiltrer les valeurs de
retour afin de ne pas créer de callback hell.
Revenons au dernier bloc de code, lâappel Ă writeFilePromise. Nous savons
maintenant que cette fonction retourne un objet Promise. Cet objet admet
différentes méthodes, dont then et catch. La méthode then est appelée
lorsque câest resolve qui a Ă©tĂ© appelĂ© dans le callback de promesse (donc,
lorsquâil nây a pas eu dâerreur). La mĂ©thode catch est appelĂ©e lorsque câest
reject qui a été appelé dans le callback de promesse. Les méthodes then et
catch admettent des fonctions en paramÚtre. Ces fonctions reçoivent
elles-mĂȘmes en paramĂštres les valeurs qui ont Ă©tĂ© passĂ©s Ă resolve et
reject.
Enfin, on peut noter quâil est possible dâenchainer les promesses,
comme on a pu le voir plus haut. On se retrouve alors avec une
construction trĂšs lisible quâon appelle une chaĂźne de promesse. De ce
fait dĂ©coule la possibilitĂ© de ne placer quâun seul catch, Ă la fin
de la chaĂźne. Celui-ci couvre les erreurs de lâintĂ©gralitĂ© de la
chaĂźne, ce qui allĂšge considĂ©rablement le code (mais ammĂšne dâautres
problĂšmes, malheureusement câest hors-sujet).
Async/Await
Les mots clés async et await sont des sucres syntaxiques autour des
promesses. Ils permettent dâĂ©crire du code trĂšs lisible, plus proche du
schéma de pensée séquentielle auquel nous sommes généralement habitués.
Voyons tout de suite un exemple :
const request = require('request-promise-native');
const fs = require('fs').promises;
const main = async () => {
const body = await request.get('https://geographer.fr/');
await fs.writeFile('/tmp/result_async.txt', body);
await fs.copyFile('/tmp/result_async.txt', '/tmp/copy_async.txt');
console.log('Work is done!');
};
main().catch(e => console.error(e));
La fonction main est marquée async : on pourra donc utiliser le mot
clé await dans son scope. On remarque ensuite que le mot clé await
semble automatiser la rĂ©solution de lâobjet Promise qui est retournĂ©
par les diffĂ©rents appels asynchrones. Dâune part, le mot clĂ© await
fait office de barriĂšre : on ne passe pas Ă lâinstruction suivante tant
que la promesse nâest pas rĂ©solue. Dâautre part, il permet dâextraire
le rĂ©sultat de lâobjet Promise (ce que lâon recevait auparavant dans
le callback cĂŽtĂ© then) lorsquâil nây a pas dâerreur.
Il semble il y avoir un problĂšme avec ce code : le cas dâerreur nâest
pas gĂȘrĂ©. Câest vrai : on aurait pu utiliser un try/catch afin de
rĂ©cupĂ©rer une eventuelle erreur. Mais on peut aussi profiter dâune
autre propriĂ©tĂ© intĂ©ressante des promesses. Si une promesse nâest pas
gĂȘrĂ©e dans son scope courant, elle est remontĂ©e au scope parent.
Lâerreur sera donc gĂȘrĂ©e au niveau du main().catch(). On comprend
alors quâune fonction marquĂ©e async retourne une Promise.
Un crawler concurrent
Pour conclure notre petite balade dans le monde féérique de la concurrence, nous allons nous faire un petit crawler qui visite des pages web et nous liste toutes celles contenant le mot âJavaScriptâ.
const request = require('request-promise-native');
const PAGES = [...]; // Truncated
const work = async page => {
const url = `https://blog.geographer.fr/${page}`;
const body = await request.get(url);
return { result: body.toLowerCase().includes('javascript'), url };
}
console.log(`Crawling ${PAGES.length} pages...`);
const tasks = PAGES.map(work);
Promise.all(tasks)
.then(r => r.filter(e => e.result).forEach(e => console.log(e.url)))
.catch(console.error);
On remarque la méthode statique Promise.all qui prend un tableau de
Promise et nous renvoie un tableau de résultats.
Voici un équivalent Python :
import requests
PAGES = [] # Truncated
results = []
for page in PAGES:
url = f'https://blog.geographer.fr/{page}'
response = None
try:
response = requests.get(url)
except requests.exceptions.RequestException as e:
print(e)
sys.exit(1)
results.append({
'result': 'javascript' in response.text.lower(),
'url': url
})
for e in list(filter(lambda r: r['result'], results)):
print(e['url'])
Bien entendu, il est absurde de comparer une implémentation concurrente JavaScript avec une implémentation non-concurrente Python. Nous allons donc les comparer:
$ time node crawler.js > /dev/null
Executed in 712,13 millis fish external
usr time 295,40 millis 111,00 micros 295,29 millis
sys time 74,41 millis 557,00 micros 73,85 millis
$ time python crawler.py > /dev/null
Executed in 1,48 secs fish external
usr time 651,95 millis 92,00 micros 651,86 millis
sys time 61,17 millis 487,00 micros 60,68 millis
La version JavaScript concurrente est environ deux fois plus rapide. Le nombre de lignes écrites est trÚs similaire.