Traduction française complète + courriel de divulgation responsable
- README, PROTOCOL, FINDINGS, endpoints, rapport et outils traduits en français - docs/DISCLOSURE.md : courriel prêt à envoyer à hello@boti.education (compte démo codé en dur, transmission en clair, logging de production) - extract_endpoints.py vérifié après traduction (100 points d'accès toujours extraits)
This commit is contained in:
+16
-14
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Log in to a Boti Education school API (Green Wood School variant) and cache the session token.
|
||||
# Credentials are read from boti.ini in $XDG_CONFIG_HOME (default ~/.config), never hard-coded.
|
||||
# Connexion à l'API Boti Education d'une école (variante Green Wood School)
|
||||
# et mise en cache de la clé de session.
|
||||
# Les identifiants sont lus dans boti.ini de $XDG_CONFIG_HOME (~/.config par
|
||||
# défaut), jamais codés en dur.
|
||||
#
|
||||
# boti.ini format:
|
||||
# Format de boti.ini :
|
||||
# [boti]
|
||||
# login = 0600000000
|
||||
# password = whatever
|
||||
# password = motdepasse
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -14,31 +16,31 @@ CONF="${XDG_CONFIG_HOME:-$HOME/.config}/boti.ini"
|
||||
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/boti-key"
|
||||
|
||||
if [[ ! -f "$CONF" ]]; then
|
||||
echo "missing $CONF (see header of this script for format)" >&2
|
||||
echo "fichier $CONF manquant (format : voir l'en-tête de ce script)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
login=$(awk -F= '/^login/{gsub(/[[:space:]]/,"",$2); print $2}' "$CONF")
|
||||
password=$(awk -F= '/^password/{gsub(/[[:space:]]/,"",$2); print $2}' "$CONF")
|
||||
|
||||
resp=$(curl -sS -m 20 "$BASE/login" \
|
||||
reponse=$(curl -sS -m 20 "$BASE/login" \
|
||||
-F "login=$login" \
|
||||
-F "password=$password" \
|
||||
-F "privacy=true" \
|
||||
-F "remembreMe=false")
|
||||
|
||||
if ! grep -q '"error":false' <<<"$resp"; then
|
||||
echo "login failed: $(head -c 200 <<<"$resp")" >&2
|
||||
if ! grep -q '"error":false' <<<"$reponse"; then
|
||||
echo "échec de connexion : $(head -c 200 <<<"$reponse")" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
key=$(sed -n 's/.*"keyToken":"\([0-9a-f]*\)".*/\1/p' <<<"$resp")
|
||||
if [[ -z "$key" ]]; then
|
||||
echo "no keyToken in response" >&2
|
||||
cle=$(sed -n 's/.*"keyToken":"\([0-9a-f]*\)".*/\1/p' <<<"$reponse")
|
||||
if [[ -z "$cle" ]]; then
|
||||
echo "keyToken absent de la réponse" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
printf '%s' "$key" > "$CACHE"
|
||||
echo "session key cached at $CACHE (40 hex chars)"
|
||||
echo "usage: curl \"$BASE/<endpoint>?user_id=<userId>&key=$(cat "$CACHE")\""
|
||||
printf '%s' "$cle" > "$CACHE"
|
||||
echo "clé de session enregistrée dans $CACHE (40 caractères hex)"
|
||||
echo "usage : curl \"$BASE/<endpoint>?user_id=<userId>&key=$(cat "$CACHE")\""
|
||||
|
||||
+31
-31
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the Boti Education app API endpoints from decompiled Capacitor JS chunks.
|
||||
"""Extrait les points d'accès de l'API des apps Boti Education depuis les chunks JS décompilés.
|
||||
|
||||
Usage: python3 extract_endpoints.py <dir-with-chunks> [--markdown]
|
||||
Usage : python3 extract_endpoints.py <dossier-avec-chunks> [--markdown]
|
||||
|
||||
Scans *.js for the Angular ApiService call signature:
|
||||
Cherche dans les *.js la signature d'appel de l'ApiService Angular :
|
||||
<http>.post(<payload>, "endpoint") / <http>.get(<payload>, "endpoint")
|
||||
and emits a deduplicated endpoint -> methods table.
|
||||
et produit une table endpoint -> méthodes dédupliquée.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
@@ -13,51 +13,51 @@ import glob
|
||||
import os
|
||||
import argparse
|
||||
|
||||
CALL = re.compile(
|
||||
APPEL = re.compile(
|
||||
r'\.(post|get|getBlob)\(\s*'
|
||||
r'(?:"[^"]*"|`[^`]*`|[A-Za-z_$][\w$]*(?:\s*\.\s*[\w$]+)*(?:\([^()]*\))?|\{[^{}]*\})'
|
||||
r'\s*,\s*"([^"]*)"'
|
||||
)
|
||||
|
||||
|
||||
def scan(directory):
|
||||
found = {} # endpoint -> {method: [chunks]}
|
||||
for path in sorted(glob.glob(os.path.join(directory, '*.js'))):
|
||||
chunk = os.path.basename(path)
|
||||
src = open(path, encoding='utf8', errors='replace').read()
|
||||
for m in CALL.finditer(src):
|
||||
method, ep = m.group(1), m.group(2)
|
||||
d = found.setdefault(ep, {})
|
||||
d.setdefault(method, []).append(chunk)
|
||||
return found
|
||||
def scanner(dossier):
|
||||
trouves = {} # endpoint -> {méthode: [chunks]}
|
||||
for chemin in sorted(glob.glob(os.path.join(dossier, '*.js'))):
|
||||
chunk = os.path.basename(chemin)
|
||||
src = open(chemin, encoding='utf8', errors='replace').read()
|
||||
for m in APPEL.finditer(src):
|
||||
methode, ep = m.group(1), m.group(2)
|
||||
d = trouves.setdefault(ep, {})
|
||||
d.setdefault(methode, []).append(chunk)
|
||||
return trouves
|
||||
|
||||
|
||||
def main():
|
||||
def principal():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('directory')
|
||||
ap.add_argument('dossier')
|
||||
ap.add_argument('--markdown', action='store_true')
|
||||
args = ap.parse_args()
|
||||
|
||||
found = scan(args.directory)
|
||||
if not found:
|
||||
sys.exit('no endpoints found, wrong directory?')
|
||||
trouves = scanner(args.dossier)
|
||||
if not trouves:
|
||||
sys.exit('aucun point d\'accès trouvé, mauvais dossier ?')
|
||||
|
||||
if args.markdown:
|
||||
print(f'# API endpoints extracted from {os.path.basename(args.directory)}')
|
||||
print(f'# Points d\'accès extraits de {os.path.basename(args.dossier)}')
|
||||
print()
|
||||
print(f'{len(found)} unique endpoints.')
|
||||
print(f'{len(trouves)} points d\'accès uniques (non filtrés, voir docs/endpoints.md pour la liste nettoyée).')
|
||||
print()
|
||||
print('| endpoint | methods | seen in chunk(s) |')
|
||||
print('| endpoint | méthodes | chunk(s) |')
|
||||
print('|---|---|---|')
|
||||
for ep in sorted(found):
|
||||
methods = ', '.join(sorted(found[ep]))
|
||||
chunks = ', '.join(sorted({c for ms in found[ep].values() for c in ms})[:3])
|
||||
print(f'| {ep} | {methods} | {chunks} |')
|
||||
for ep in sorted(trouves):
|
||||
methodes = ', '.join(sorted(trouves[ep]))
|
||||
chunks = ', '.join(sorted({c for ms in trouves[ep].values() for c in ms})[:3])
|
||||
print(f'| {ep} | {methodes} | {chunks} |')
|
||||
else:
|
||||
for ep in sorted(found):
|
||||
print(ep, sorted(found[ep]))
|
||||
print(f'TOTAL endpoints: {len(found)}', file=sys.stderr)
|
||||
for ep in sorted(trouves):
|
||||
print(ep, sorted(trouves[ep]))
|
||||
print(f'TOTAL points d\'accès : {len(trouves)}', file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
principal()
|
||||
|
||||
Reference in New Issue
Block a user