From 9aacc00631b02f5e34793f0472af19187fa7b430 Mon Sep 17 00:00:00 2001 From: scayac Date: Sat, 31 Jan 2026 07:58:00 +0100 Subject: [PATCH] Initial commit --- .github/prompts/structure.prompt.md | 109 +++++++ .github/prompts/update-nom-app.prompt.md | 6 + .github/prompts/update_doc.prompt.md | 6 + .gitignore | 26 ++ README.md | 258 +++++++++++++++ compress_assets.py | 58 ++++ data/css/styles.css | 327 +++++++++++++++++++ data/css/styles.css.gz | Bin 0 -> 1631 bytes data/index.html | 59 ++++ data/index.html.gz | Bin 0 -> 674 bytes data/js/api.js | 101 ++++++ data/js/api.js.gz | Bin 0 -> 736 bytes data/js/app.js | 75 +++++ data/js/app.js.gz | Bin 0 -> 982 bytes data/js/crypto-js.min.js | 1 + data/js/crypto-js.min.js.gz | Bin 0 -> 16587 bytes data/login.html | 70 ++++ data/login.html.gz | Bin 0 -> 929 bytes data/settings.html | 199 ++++++++++++ data/settings.html.gz | Bin 0 -> 1790 bytes data/update.html | 302 +++++++++++++++++ data/update.html.gz | Bin 0 -> 2649 bytes platformio.ini | 18 ++ platformio_extra.py | 37 +++ src/auth.cpp | 89 +++++ src/auth.h | 37 +++ src/main.cpp | 205 ++++++++++++ src/server.cpp | 394 +++++++++++++++++++++++ src/server.h | 37 +++ src/utils.cpp | 53 +++ src/utils.h | 26 ++ 31 files changed, 2493 insertions(+) create mode 100644 .github/prompts/structure.prompt.md create mode 100644 .github/prompts/update-nom-app.prompt.md create mode 100644 .github/prompts/update_doc.prompt.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 compress_assets.py create mode 100644 data/css/styles.css create mode 100644 data/css/styles.css.gz create mode 100644 data/index.html create mode 100644 data/index.html.gz create mode 100644 data/js/api.js create mode 100644 data/js/api.js.gz create mode 100644 data/js/app.js create mode 100644 data/js/app.js.gz create mode 100644 data/js/crypto-js.min.js create mode 100644 data/js/crypto-js.min.js.gz create mode 100644 data/login.html create mode 100644 data/login.html.gz create mode 100644 data/settings.html create mode 100644 data/settings.html.gz create mode 100644 data/update.html create mode 100644 data/update.html.gz create mode 100644 platformio.ini create mode 100644 platformio_extra.py create mode 100644 src/auth.cpp create mode 100644 src/auth.h create mode 100644 src/main.cpp create mode 100644 src/server.cpp create mode 100644 src/server.h create mode 100644 src/utils.cpp create mode 100644 src/utils.h diff --git a/.github/prompts/structure.prompt.md b/.github/prompts/structure.prompt.md new file mode 100644 index 0000000..fd5e700 --- /dev/null +++ b/.github/prompts/structure.prompt.md @@ -0,0 +1,109 @@ +# Spécifications générales +- Contexte: Application ESP32 +- Stack: Arduino C++, ESPAsyncWebServer, LittleFS +- Carte: ESP32 +- Interface: Dashboard web responsive +- Fonctionnalités clés: Gestion GPIO, OTA, WiFi/AP, Authentification, API REST + +# Arborescence des fichiers +``` +src/ +├── main.cpp [Point d'entrée] +├── auth.h/cpp [Authentification] +├── server.h/cpp [API endpoints] +└── utils.h/cpp [Helpers] + +data/ +├── config.json [Config par défaut] +├── index.html [Dashboard] +├── login.html [Page de login] +├── settings.html [Paramètres] +├── update.html [Mise à jour OTA] +└── js/ + ├── api.js [Client API] + └── app.js [UI logic] +└── css/ + └── styles.css [Styles CSS] +``` + +# Spécificités ESP32 +- Framework: Arduino +- Système de fichiers: LittleFS +- Utilisation de platformio pour la gestion des dépendances et le build +``` +[env:esp32] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +board_build.filesystem = littlefs +board_build.partitions = default.csv + +lib_deps = + ESP32Async/ESPAsyncWebServer + ESP32Async/AsyncTCP + bblanchon/ArduinoJson@^7.0.0 + +build_flags = + -DBOARD_HAS_PSRAM +``` + +# Spécificités dashboard web +- Pages: Login, Dashboard, Paramètres, Mise à jour OTA +- Technologies: HTML5, CSS3, JavaScript (Fetch API) +- Sécurité: Sessions avec cookies, redirection vers login si non authentifié +- Gestion des erreurs: Messages utilisateur clairs et en français +- Responsive: Adapté aux mobiles et tablettes + +# Spécificités stockage fichiers +- Système de fichiers: LittleFS +- Fichiers: HTML, CSS, JS, config.json +- Accès: Lecture seule pour fichiers web, lecture/écriture pour config.json +- Création d'une config.json par défaut si absente au démarrage +- Pas de fichiers compressés pour compatibilité avec LittleFS + +# Fonctionnalités OTA +- Mise à jour via interface web ou via espota +- Mise à jour firmware et système de fichiers depuis interface web + +# Spécificités API REST +- Endpoints: /api/config, /api/stats +- Sécurité: Authentification requise pour endpoints protégés +- Réponses: Statuts HTTP et messages d'erreur clairs + +# Spécificités Authentification +- Stockage: Nom d'utilisateur et hash SHA-256 du mot de passe dans config.json +- Sécurité: Sessions avec timeout configurable +- Gestion des sesssions avec cookies +- Redirection vers page de login si non authentifié +- Appel GET vers /logout pour déconnexion et /login pour connexion + +# Spécificités wifi +- Modes: Station (client) et Point d'accès (AP) si absence de réseau et/ou paramètres non configurés +- Sécurité: WPA2 pour Station et pour AP +- Configuration: Via interface web ou fichier config.json + +# Spécificités logs +- Sortie série pour debug en français +- Niveau de log configurable (DEBUG, INFO, WARN, ERROR) + +# Format de configuration (config.json) +```json +{ + "auth": { + "username": "admin", + "password_hash": "SHA-256" + }, + "wifi": { + "ssid": "Réseau WiFi", + "password": "***", + "ap_password": "Mode AP pwd" + }, + "system": { + "hostname": "WEBAPP", + "session_timeout": 60, // minutes + }, + "params": { + } +} +``` \ No newline at end of file diff --git a/.github/prompts/update-nom-app.prompt.md b/.github/prompts/update-nom-app.prompt.md new file mode 100644 index 0000000..7bdc245 --- /dev/null +++ b/.github/prompts/update-nom-app.prompt.md @@ -0,0 +1,6 @@ +--- +agent: agent +--- + +# Met à jour l'application nommée "ESP32 Sonnerie" pour qu'elle s'appelle comme indiqué dans le prompt dans tous les fichiers du projet. +- Les commentaires doivent aussi être mis à jour pour refléter le nouveau nom de l'application. \ No newline at end of file diff --git a/.github/prompts/update_doc.prompt.md b/.github/prompts/update_doc.prompt.md new file mode 100644 index 0000000..f9472cd --- /dev/null +++ b/.github/prompts/update_doc.prompt.md @@ -0,0 +1,6 @@ +--- +agent: agent +--- + +# Mise à jour des fichiers md +Mets à jour les fichiers markdown suivants pour refléter les modifications récentes apportées au code et aux fichiers de l'application \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f92e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# PlatformIO +.pio +.pioenvs +.piolibdeps + +# Git +.git + +# Configuration locale (peut contenir des mots de passe) +data/config.json + +# VS Code +.vscode/ + +# Backups +*.bak +*~ + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +*.bin +*.elf +*.map diff --git a/README.md b/README.md new file mode 100644 index 0000000..7240321 --- /dev/null +++ b/README.md @@ -0,0 +1,258 @@ +# ESP32 Webapp + +Squelette d'application web pour ESP32 avec interface de gestion via dashboard web responsive. + +## 🔔 Fonctionnalités + +- **Dashboard web responsive** - Interface utilisateur moderne et adaptative +- **Authentification sécurisée** - Système de login avec sessions et hash SHA-256 +- **Configuration WiFi** - Mode Station et Point d'Accès automatique +- **API REST** - Endpoints pour configuration, statistiques et changement de mot de passe +- **Gestion des mots de passe** - Changement sécurisé avec validation de l'ancien mot de passe +- **Mise à jour OTA** - Mise à jour du firmware et système de fichiers via web avec modales de confirmation +- **Compression gzip** - Optimisation automatique des assets (~70% de réduction) +- **Système de fichiers LittleFS** - Stockage de configuration et fichiers web + +## 📋 Prérequis + +- ESP32 +- PlatformIO +- Câble USB pour le téléversement initial + +## 🚀 Installation + +### 1. Compiler le projet + +```bash +platformio run --environment esp32 +``` + +### 2. Téléverser le firmware + +```bash +platformio run --environment esp32 --target upload +``` + +### 3. Téléverser le système de fichiers (data/) + +```bash +platformio run --environment esp32 --target uploadfs +``` + +### 4. Moniteur série (optionnel) + +```bash +platformio device monitor --environment esp32 +``` + +## 🔐 Connexion initiale + +### Première connexion + +1. Au premier démarrage, l'ESP32 crée un point d'accès WiFi : + - **SSID** : `ESP32-Webapp` + - **Mot de passe** : `webapp123` + +2. Connectez-vous au point d'accès depuis votre appareil + +3. Ouvrez un navigateur web et accédez à : `http://192.168.4.1` + +4. Identifiants par défaut : + - **Utilisateur** : `admin` + - **Mot de passe** : `admin` + +⚠️ **Important** : Changez le mot de passe par défaut après la première connexion! + +## ⚙️ Configuration WiFi + +### Configurer la connexion WiFi Station + +1. Accédez à la page **Paramètres** +2. Section **Configuration WiFi** +3. Entrez le SSID et le mot de passe de votre réseau +4. Cliquez sur **Enregistrer WiFi** +5. L'ESP32 redémarre et se connecte à votre réseau + +### Changer le mot de passe + +1. Accédez à la page **Paramètres** +2. Section **Changer le mot de passe** +3. Entrez votre mot de passe actuel +4. Entrez le nouveau mot de passe et confirmez +5. Cliquez sur **Changer le mot de passe** +6. Vous serez automatiquement redirigé vers la connexion + +### Mode Point d'Accès + +Si la connexion WiFi échoue ou n'est pas configurée, l'ESP32 démarre automatiquement en mode Point d'Accès (AP). + +## 📱 Interface Web + +### Pages disponibles + +- **Tableau de bord** (`/index.html`) - Statistiques et contrôles +- **Paramètres** (`/settings.html`) - Configuration WiFi, système, mot de passe +- **Mise à jour** (`/update.html`) - Mise à jour OTA du firmware +- **Connexion** (`/login.html`) - Page d'authentification + +### Endpoints API + +#### Configuration +- `GET /api/config` - Récupérer la configuration +- `POST /api/config` - Enregistrer la configuration + +#### Authentification +- `POST /login` - Se connecter (username + password) +- `GET /logout` - Se déconnecter (redirection 302 vers `/`) + +#### Gestion des mots de passe +- `POST /api/password` - Changer le mot de passe (validation de l'ancien mot de passe requise) + +#### Statistiques +- `GET /api/stats` - Récupérer les statistiques système (uptime, heap, WiFi, IP) + +#### Mise à jour +- `POST /api/update` - Mise à jour OTA (firmware ou filesystem) + +## 🔧 Structure du projet + +``` +esp32Webapp/ +├── src/ +│ ├── main.cpp # Point d'entrée +│ ├── auth.h/cpp # Gestion authentification +│ ├── server.h/cpp # Serveur web et API +│ └── utils.h/cpp # Fonctions utilitaires +├── data/ +│ ├── config.json # Configuration par défaut +│ ├── index.html # Dashboard +│ ├── login.html # Page de connexion +│ ├── settings.html # Page paramètres +│ ├── update.html # Page mise à jour OTA +│ ├── js/ +│ │ ├── api.js # Client API +│ │ ├── app.js # Logique UI +│ │ └── crypto-js.min.js # Hachage SHA-256 côté client +│ └── css/ +│ └── styles.css # Styles CSS +├── platformio.ini # Configuration PlatformIO +├── platformio_extra.py # Script d'optimisation (compression gzip) +├── compress_assets.py # Compression assets CSS/JS/HTML +└── README.md # Cette documentation +``` + +## 🔄 Mise à jour OTA + +### Via l'interface web + +1. Compilez votre nouveau firmware : `platformio run --environment esp32` +2. Accédez à la page **Mise à jour** +3. Sélectionnez le fichier `.bin` dans `.pio/build/esp32/firmware.bin` +4. Confirmez via le modal de confirmation +5. Attendez la fin du téléversement et le redémarrage automatique + +### Mise à jour du syst\u00e8me de fichiers + +1. Compilez l'image filesystem : `platformio run --environment esp32 --target buildfs` +2. Accédez à la page **Mise à jour** +3. Sélectionnez le fichier `.bin` de filesystem +4. Confirmez via le modal (⚠️ toutes les données seront effacées) +5. Le système redémarrera automatiquement + +### Compression automatique des assets + +Tous les assets CSS, JS et HTML sont compressés automatiquement en gzip avant la construction du filesystem : +- **Script** : `compress_assets.py` compresse les fichiers +- **Hook** : `platformio_extra.py` l'exécute automatiquement avant `buildfs` +- **Avantage** : ~70% de réduction de taille des fichiers statiques + +## 📊 Configuration (config.json) + +```json +{ + "auth": { + "username": "admin", + "password_hash": "hash_sha256_du_mot_de_passe" + }, + "wifi": { + "ssid": "Nom_du_réseau", + "password": "mot_de_passe_wifi", + "ap_password": "webapp123" + }, + "system": { + "hostname": "ESP32-Webapp", + "session_timeout": 60 + }, + "params": { + "app_name": "ESP32 Webapp" + } +} +``` + +## 🛠️ Dépendances + +- **ESPAsyncWebServer** - Serveur web asynchrone +- **AsyncTCP** - TCP asynchrone pour ESP32 +- **ArduinoJson** - Gestion JSON +- **LittleFS** - Système de fichiers +- **mbedtls** - Cryptographie (SHA-256) +- **Python 3** - Pour scripts d'optimisation (compression gzip) +- **CryptoJS** - Hachage SHA-256 côté client (inclus dans les assets statiques) + +## 🐛 Dépannage + +### L'ESP32 ne se connecte pas au WiFi + +1. Vérifiez le SSID et le mot de passe dans la configuration +2. Assurez-vous que le réseau WiFi est en 2.4 GHz (l'ESP32 ne supporte pas le 5 GHz) +3. L'ESP32 passera automatiquement en mode AP si la connexion échoue + +### Impossible de se connecter à l'interface web + +1. Vérifiez que vous êtes connecté au même réseau que l'ESP32 +2. En mode AP, l'adresse est toujours `192.168.4.1` +3. En mode Station, trouvez l'IP dans les logs du moniteur série +4. Désactivez le cache du navigateur (Ctrl+F5) + +### Erreur lors de la mise à jour OTA + +1. Vérifiez que le fichier .bin est correct +2. Assurez-vous d'avoir une connexion stable +3. Ne débranchez jamais l'ESP32 pendant la mise à jour +4. Après une mise à jour réussie, vous serez automatiquement redirigé vers la page de connexion + +### Message de succès mais page reste en erreur + +C'est normal ! La connexion se ferme intentionnellement après la mise à jour (l'ESP redémarre). Vous serez redirigé automatiquement vers la connexion après 3 secondes. + +## 📝 Logs + +Les logs sont disponibles via le moniteur série : + +```bash +platformio device monitor --environment esp32 --baud 115200 +``` + +## 🔒 Sécurité + +- Les mots de passe sont hashés en SHA-256 (CryptoJS côté client, mbedtls côté serveur) +- Changement de mot de passe sécurisé avec validation de l'ancien mot de passe +- Les sessions ont un timeout configurable (60 minutes par défaut) +- Les cookies de session sont HttpOnly +- Authentification requise pour tous les endpoints API +- Compression gzip des assets pour réduire la surface d'attaque +- Configuration sécurisée : les vrais mots de passe ne sont jamais exposés sur le frontend (masqués en "***") +- Redirection automatique vers login après déconnexion ou mise à jour OTA +- Pas de dépendance externe obligatoire (crypto-js inclus dans les assets statiques) + +## 📄 Licence + +Ce projet est sous licence libre. + +## 👨‍💻 Auteur + +Développé pour ESP32 + +--- + +**Note** : Cette application est un template de base. Adaptez-la selon vos besoins spécifiques! diff --git a/compress_assets.py b/compress_assets.py new file mode 100644 index 0000000..86924d1 --- /dev/null +++ b/compress_assets.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import os +import gzip +import shutil +from pathlib import Path + +def compress_file(src_path, dst_path): + """Compress a file using gzip.""" + try: + with open(src_path, 'rb') as f_in: + with gzip.open(dst_path, 'wb', compresslevel=9) as f_out: + shutil.copyfileobj(f_in, f_out) + print(f"✓ Compressed: {src_path} -> {dst_path}") + return True + except Exception as e: + print(f"✗ Error compressing {src_path}: {e}") + return False + +def compress_data_directory(data_dir): + """Compress CSS, JS, and HTML files in data directory.""" + data_path = Path(data_dir) + + if not data_path.exists(): + print(f"Data directory not found: {data_dir}") + return + + files_to_compress = [] + + # Find all CSS files + files_to_compress.extend(data_path.glob("**/*.css")) + + # Find all JS files + files_to_compress.extend(data_path.glob("**/*.js")) + + # Find all HTML files (excluding .gz files) + files_to_compress.extend(data_path.glob("**/*.html")) + + # Filter out files that are already .gz + files_to_compress = [f for f in files_to_compress if not str(f).endswith('.gz')] + + if not files_to_compress: + print("No files to compress found.") + return + + print(f"Compressing {len(files_to_compress)} files...") + + compressed_count = 0 + for src_file in files_to_compress: + dst_file = Path(str(src_file) + ".gz") + if compress_file(src_file, dst_file): + compressed_count += 1 + + print(f"\n✓ Successfully compressed {compressed_count}/{len(files_to_compress)} files") + +if __name__ == "__main__": + import sys + data_dir = sys.argv[1] if len(sys.argv) > 1 else "data" + compress_data_directory(data_dir) diff --git a/data/css/styles.css b/data/css/styles.css new file mode 100644 index 0000000..d2db6f8 --- /dev/null +++ b/data/css/styles.css @@ -0,0 +1,327 @@ +/* Variables CSS */ +:root { + --primary-color: #007bff; + --primary-dark: #0056b3; + --secondary-color: #6c757d; + --success-color: #28a745; + --danger-color: #dc3545; + --warning-color: #ffc107; + --info-color: #17a2b8; + --light-color: #f8f9fa; + --dark-color: #343a40; + --white: #ffffff; + --border-radius: 8px; + --box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +/* Reset et base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--dark-color); + background-color: var(--light-color); +} + +/* Navigation */ +.navbar { + background-color: var(--primary-color); + color: var(--white); + padding: 1rem 2rem; + box-shadow: var(--box-shadow); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; +} + +.nav-brand { + font-size: 1.5rem; + font-weight: bold; +} + +.nav-menu { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.nav-menu a { + color: var(--white); + text-decoration: none; + padding: 0.5rem 1rem; + border-radius: var(--border-radius); + transition: var(--transition); +} + +.nav-menu a:hover, +.nav-menu a.active { + background-color: var(--primary-dark); +} + +/* Container */ +.container { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +/* Cards */ +.card { + background-color: var(--white); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.card h2 { + margin-bottom: 1rem; + color: var(--primary-color); +} + +.card.warning { + border-left: 4px solid var(--warning-color); + background-color: #fff3cd; +} + +.card.warning h3 { + color: var(--warning-color); + margin-bottom: 0.5rem; +} + +.card.warning ul { + margin-left: 1.5rem; +} + +/* Grille de statistiques */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.stat-card { + background-color: var(--white); + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + padding: 1.5rem; + text-align: center; +} + +.stat-label { + font-size: 0.9rem; + color: var(--secondary-color); + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 1.5rem; + font-weight: bold; + color: var(--primary-color); +} + +/* Grille de contrôles */ +.controls-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +/* Formulaires */ +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--dark-color); +} + +.form-group input[type="text"], +.form-group input[type="password"], +.form-group input[type="number"], +.form-group input[type="file"] { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: var(--border-radius); + font-size: 1rem; + transition: var(--transition); +} + +.form-group input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +/* Boutons */ +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 500; + text-align: center; + text-decoration: none; + border: none; + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + width: 100%; +} + +.btn-primary { + background-color: var(--primary-color); + color: var(--white); +} + +.btn-primary:hover { + background-color: var(--primary-dark); +} + +.btn-secondary { + background-color: var(--secondary-color); + color: var(--white); +} + +.btn-secondary:hover { + background-color: #5a6268; +} + +/* Messages */ +.message { + padding: 1rem; + border-radius: var(--border-radius); + margin-top: 1rem; + margin-bottom: 1rem; + display: none; +} + +.message.success { + display: block; + background-color: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} + +.message.error { + display: block; + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} + +.message.info { + display: block; + background-color: #d1ecf1; + border: 1px solid #bee5eb; + color: #0c5460; +} + +/* Barre de progression */ +.progress-container { + margin-top: 1rem; +} + +.progress-bar { + width: 100%; + height: 30px; + background-color: #e9ecef; + border-radius: var(--border-radius); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: var(--primary-color); + transition: width 0.3s ease; + width: 0%; +} + +.progress-text { + text-align: center; + margin-top: 0.5rem; + font-weight: bold; +} + +/* Page de login */ +.login-page { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); +} + +.login-container { + width: 100%; + max-width: 400px; + padding: 1rem; +} + +.login-card { + background-color: var(--white); + border-radius: var(--border-radius); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + padding: 2rem; +} + +.login-card h1 { + text-align: center; + color: var(--primary-color); + margin-bottom: 0.5rem; + font-size: 2rem; +} + +.login-card h2 { + text-align: center; + color: var(--secondary-color); + margin-bottom: 2rem; + font-size: 1.5rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .navbar { + flex-direction: column; + gap: 1rem; + } + + .nav-menu { + justify-content: center; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .controls-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .container { + margin: 1rem auto; + } + + .card { + padding: 1rem; + } + + .nav-menu a { + padding: 0.5rem; + font-size: 0.9rem; + } +} diff --git a/data/css/styles.css.gz b/data/css/styles.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..abef4f78cbf4ec68a75e7d763d795bc831bc5e71 GIT binary patch literal 1631 zcmV-l2B7&LiwFo}B7JEB|8sPCY-MvUV{>x=%~;!x+cpq=&sPvx6j?7UN0KeuaZsR5 z(6oI>5M_+4f&NyPGi03?d`1|TuC7m`Rf_MAI~c!-%|PL zB_b1YMXr2*5vS?v-Ylc?uAYqM@wA;RArU!qH_wwrv|x6=N)jkbI~y+PVivWtjOJ^Q zc9JF2sGGc}GUxf)&ZcP+_=|Ry=c%w$!GebIvYpEKdZW5+%k))BJBjknPEBW1I`gf( z4OdXh(Le2IEF?n}C1t!SS7cc{bTbcLxuHzluZT~=;(^TYU#??%?T?B1n*^hFy^=I9 zx#A*U5t?Nmj2q z4%Mf|f<2l;Qjse!rCXjou82pAA_K2{EER0WXz$?gWZsM# zXY8oFcHLuOi)nJVmZHj;x3|#o@u41K>1jyA?Ms|c->=p$i3Fe0dA zWpg}!JENov`jhc{0;juhaLCozfs2(A+iptZU8}kY_mWz-(4KHIKHGC9=5}aJZ!E|l zMM~>5T#Agd)_(UQIht2=-k&CHK*VM`NacZ3dp7w!K$hy zxjTva0h*ETpDQqWl6hWK>NoXRz_-seeV%_FADu5~S>7XEPp!$TZ4C0%ij-&Y{IfB( zy%c=^n}b_$5%q3BTxYY~6=UqesB-Dph0OUYvv0>L6-iYZ14N~CiRWJAM)ui~YrmG( zzf-4{4#Me}gi+Mi(+%I=pgfT`T#HrSrwq^QdedP}AJ4-`>?1NvsG-zyrl4t^c7V)z zS0$A!HF$;Kj;PbFI-ryG9b(jC|Fw$Z-my&?b#cj5A7NAMneD@=_@s7r%NhNyB03M} z%a&j7v2UeolgzfwgW0>+2wdn-lfRTWdA-^R%jbqV+9spR#PrrDa^--HYzBX_OpDUW7r?m#iEA3r!9nNrR(;V}J;Hg8d|# z&3&6`sFd|pTS&3S>#b~C%=XzkEb{wK)+X5HU0-^T^xOV;YCD4gi{KR`kX}eU&HXgf z8Fs@N(;eoXPIwo!*RJi+tyY(03O6^^=DBY=9Gly`WHr$F(`axk#k#oqXIoly-A5g? z=($YbzS?K?H?Qb|`6;crtHaTr97`q7fo`|yW4Eebfl!GzC^`mrUI){N!FoK1=6EP! znzYs!(P!NOnVep8*0q|w?B$&IAJp)E-JZ<{-js#?ay}eVZxfuM$XR}#DZU4QOeNV%%E>D`EY>NHY zcD~o?eW^d~S?)Wbv$5gsJ?K(Tv>n)293C*#$vo{0#BBhk+0u7l>IaYe5+>Mx3A3kE d7b8b1fkFJ}&>ok1Ct6W$&%e@*Hqqx7005+%MeG0o literal 0 HcmV?d00001 diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..1836d83 --- /dev/null +++ b/data/index.html @@ -0,0 +1,59 @@ + + + + + + Tableau de bord - ESP32 Webapp + + + + + +
+

Tableau de bord

+ +
+
+
Temps de fonctionnement
+
--
+
+ +
+
Mémoire libre
+
--
+
+ +
+
Signal WiFi
+
--
+
+ +
+
Adresse IP
+
--
+
+
+ +
+

Contrôles

+
+ + +
+
+ +
+
+ + + + + diff --git a/data/index.html.gz b/data/index.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..33b346faac224f0a3ba06c017b599424000f6681 GIT binary patch literal 674 zcmV;T0$u$diwFo}B7JEB|7mVyWq2-VbZu+^#Z|p-+b|H`^As$Xv_NGy>C!YqW&nu*J1Liu9?ls3Ph7g+iAj^8LBRX(WfKbHjFfz{@(PVVhK#nC#*v zf`++B6*w13UM65~=ro5JtsVx61)a(G&$r9VJFFPG*|9)ft;B60u%t;k!}CF{zg+r2 z>S>O$D@HhkO+mOap7hMMapDnJC>GznMR$)=Rc7Gv#^snCip7;UF_pc?N12(ayo?VP z*J+*nIT^hMUy&jla`NqRt1Cy1(7pemGtvPf2?r-JJl1U=3@TmLnUee}WBI0`)Hy^ju+tDLz0pf};C@mGhZ;jyIQIwu?EpW<;qBd#`PgRB%V0! zDHjzMPy-T`M2V;I56bVzAL|1C0NWYINe-P(b+JLa+NbxPSouDTQ-RXL>GTY8RcM&Z zrXN4ROi77apyLEe8VfuZFoNF>(C~fu_HeI7Bv3=jcwrM-fgS^Hg_3m)H**<4kYI3g zO7m`SnOhC3=dGt|G&MAuocz|dBmk;+r zkyD{cOkQD$L|o`?DPy$bda5Jk6NGCPY%6z!YR4;P6Rk>+Si+=X^PHI5(DrcOMzZK& zdef3_8_3SG88Y9gMeSAUCu z3~i#q6JXPe*%j+|<`U!~YICyZ$T5OqiTc6SJV!?9JkQgmdHvf#S(bxH>1W;hYv}Fpe0!g(mCk<=I$ev?(g(ewkgIldrUpv54j?7J3^AT5|l=rhK_O zuNkgQlU1MjC%g0cHXSEi#+5}8GF4a1xiQ#j3suCF-j+&-&$DU1%>HlG$BlZ>cA$!m zMj!|zStqbBG-M-S-I>&I!ZynQ)~ zq2nC5LX#|&%XQrTXy?^Qa1AdaFvRbsM$f4=+D4Oen@FCaM~6BshIDY`li?Pra3;+PPnrmtR literal 0 HcmV?d00001 diff --git a/data/js/app.js b/data/js/app.js new file mode 100644 index 0000000..4762fb3 --- /dev/null +++ b/data/js/app.js @@ -0,0 +1,75 @@ +// Application principale pour l'application web ESP32 + +// Formater le temps de fonctionnement +function formatUptime(seconds) { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + let result = ''; + if (days > 0) result += `${days}j `; + if (hours > 0) result += `${hours}h `; + if (minutes > 0) result += `${minutes}m `; + result += `${secs}s`; + + return result; +} + +// Formater la mémoire +function formatMemory(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; +} + +// Formater le signal WiFi +function formatRSSI(rssi) { + if (rssi >= -50) return `${rssi} dBm (Excellent)`; + if (rssi >= -60) return `${rssi} dBm (Bon)`; + if (rssi >= -70) return `${rssi} dBm (Moyen)`; + if (rssi >= -80) return `${rssi} dBm (Faible)`; + return `${rssi} dBm (Très faible)`; +} + +// Rafraîchir les statistiques +async function refreshStats() { + try { + const stats = await api.getStats(); + + // Mettre à jour l'affichage + document.getElementById('uptime').textContent = formatUptime(stats.uptime); + document.getElementById('memory').textContent = formatMemory(stats.heap_free) + ' / ' + formatMemory(stats.heap_total); + document.getElementById('rssi').textContent = formatRSSI(stats.wifi_rssi); + document.getElementById('ip').textContent = stats.ip; + + } catch (error) { + console.error('Erreur de récupération des statistiques:', error); + showMessage('Erreur de récupération des statistiques', 'error'); + } +} + +// Afficher un message +function showMessage(text, type = 'info') { + const messageDiv = document.getElementById('message'); + if (messageDiv) { + messageDiv.className = `message ${type}`; + messageDiv.textContent = text; + + setTimeout(() => { + messageDiv.className = 'message'; + messageDiv.textContent = ''; + }, 5000); + } +} + +// Initialisation au chargement de la page +document.addEventListener('DOMContentLoaded', () => { + // Rafraîchir les stats si on est sur la page d'accueil + if (window.location.pathname.includes('index.html') || window.location.pathname === '/') { + refreshStats(); + + // Rafraîchir automatiquement toutes les 5 secondes + setInterval(refreshStats, 5000); + } +}); diff --git a/data/js/app.js.gz b/data/js/app.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..c841169b06e7a5b13697276e2ae59b71f966815b GIT binary patch literal 982 zcmV;{11bC;iwFo}B7JEB|6y=&E^2cCl~&u1+cpq=_gBnA3d!BA>|E9j>Ux1};{_Ip z(;`mM$C!~uD+Us&lGJ*QjsB&N*_Zx=f2>2@EXh`o8n7Ud=gg3EW`>+h;8mK&+-HVM z0jc7`=P8R3Qkf}OuK#%c4AxQzR2)a6$po&YN|-?fgfW<;8Uln+3f~$Kn4mDDFe^W4 z+=Bm14NuV3=t~jkF+7Zbe)ORY1Z<~a0V`%AFN~#BH4G;3a`s|69nV|nh!)j@H~1ea zJQ1JIdc1^-%-}GyAP{lZr4hh#4!GI0oGujS^Y#|)H(V~@r;BN`UJ9-q&LOx; zz+K+^7{`?StsdiaVRhF z-uxzop&4@C?Mv3VS0Yge?6?altYfC-grX#2IN={5}4yZUf$n z{7HXor_R9-Q%iazvg!Y_(p~lsCpSi|89uwm>otvKW19LV~O>=sm9^SgI!C$nV({G z^|D_l>PMsbKzS`Q<5F2JE?dsOv4Ca=^C5KCxzhsX=WsD?&fi*s8^H}{G1sMZECZ@w zbz20@N=xTXYGr9+%7S2dM=x(lU8L)W>%9JbRe5|XS%3k_-QLnb!=fh#@`$$gWU6)q zfx~=1LmoF0ZMg{K){A9%8S+xPDhP^)M|Yw)qhzBx5dq$Nkx638i!WbbhysfR1!dy2 zweEJv)2*((0hSp_C2cdi2$GS-1;Fm>qPmh$8*b@*L$OhJEOy(T`eM-Ae@NV3=ky5x E0GQh88UO$Q literal 0 HcmV?d00001 diff --git a/data/js/crypto-js.min.js b/data/js/crypto-js.min.js new file mode 100644 index 0000000..20b3099 --- /dev/null +++ b/data/js/crypto-js.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports?module.exports=exports=e():"function"==typeof define&&define.amd?define([],e):t.CryptoJS=e()}(this,function(){var n,o,s,a,h,t,e,l,r,i,c,f,d,u,p,S,x,b,A,H,z,_,v,g,y,B,w,k,m,C,D,E,R,M,F,P,W,O,I,U=U||function(h){var i;if("undefined"!=typeof window&&window.crypto&&(i=window.crypto),"undefined"!=typeof self&&self.crypto&&(i=self.crypto),!(i=!(i=!(i="undefined"!=typeof globalThis&&globalThis.crypto?globalThis.crypto:i)&&"undefined"!=typeof window&&window.msCrypto?window.msCrypto:i)&&"undefined"!=typeof global&&global.crypto?global.crypto:i)&&"function"==typeof require)try{i=require("crypto")}catch(t){}var r=Object.create||function(t){return e.prototype=t,t=new e,e.prototype=null,t};function e(){}var t={},n=t.lib={},o=n.Base={extend:function(t){var e=r(this);return t&&e.mixIn(t),e.hasOwnProperty("init")&&this.init!==e.init||(e.init=function(){e.$super.init.apply(this,arguments)}),(e.init.prototype=e).$super=this,e},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},l=n.WordArray=o.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:4*t.length},toString:function(t){return(t||c).stringify(this)},concat:function(t){var e=this.words,r=t.words,i=this.sigBytes,n=t.sigBytes;if(this.clamp(),i%4)for(var o=0;o>>2]>>>24-o%4*8&255;e[i+o>>>2]|=s<<24-(i+o)%4*8}else for(var c=0;c>>2]=r[c>>>2];return this.sigBytes+=n,this},clamp:function(){var t=this.words,e=this.sigBytes;t[e>>>2]&=4294967295<<32-e%4*8,t.length=h.ceil(e/4)},clone:function(){var t=o.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var e=[],r=0;r>>2]>>>24-n%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new l.init(r,e/2)}},a=s.Latin1={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],n=0;n>>2]>>>24-n%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new l.init(r,e)}},f=s.Utf8={stringify:function(t){try{return decodeURIComponent(escape(a.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return a.parse(unescape(encodeURIComponent(t)))}},d=n.BufferedBlockAlgorithm=o.extend({reset:function(){this._data=new l.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=f.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var e,r=this._data,i=r.words,n=r.sigBytes,o=this.blockSize,s=n/(4*o),c=(s=t?h.ceil(s):h.max((0|s)-this._minBufferSize,0))*o,n=h.min(4*c,n);if(c){for(var a=0;a>>32-e}function j(t,e,r,i){var n,o=this._iv;o?(n=o.slice(0),this._iv=void 0):n=this._prevBlock,i.encryptBlock(n,0);for(var s=0;s>24&255)?(r=t>>8&255,i=255&t,255===(e=t>>16&255)?(e=0,255===r?(r=0,255===i?i=0:++i):++r):++e,t=0,t+=e<<16,t+=r<<8,t+=i):t+=1<<24,t}function N(){for(var t=this._X,e=this._C,r=0;r<8;r++)E[r]=e[r];e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0;for(r=0;r<8;r++){var i=t[r]+e[r],n=65535&i,o=i>>>16;R[r]=((n*n>>>17)+n*o>>>15)+o*o^((4294901760&i)*i|0)+((65535&i)*i|0)}t[0]=R[0]+(R[7]<<16|R[7]>>>16)+(R[6]<<16|R[6]>>>16)|0,t[1]=R[1]+(R[0]<<8|R[0]>>>24)+R[7]|0,t[2]=R[2]+(R[1]<<16|R[1]>>>16)+(R[0]<<16|R[0]>>>16)|0,t[3]=R[3]+(R[2]<<8|R[2]>>>24)+R[1]|0,t[4]=R[4]+(R[3]<<16|R[3]>>>16)+(R[2]<<16|R[2]>>>16)|0,t[5]=R[5]+(R[4]<<8|R[4]>>>24)+R[3]|0,t[6]=R[6]+(R[5]<<16|R[5]>>>16)+(R[4]<<16|R[4]>>>16)|0,t[7]=R[7]+(R[6]<<8|R[6]>>>24)+R[5]|0}function q(){for(var t=this._X,e=this._C,r=0;r<8;r++)O[r]=e[r];e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0>>0?1:0)|0,this._b=e[7]>>>0>>0?1:0;for(r=0;r<8;r++){var i=t[r]+e[r],n=65535&i,o=i>>>16;I[r]=((n*n>>>17)+n*o>>>15)+o*o^((4294901760&i)*i|0)+((65535&i)*i|0)}t[0]=I[0]+(I[7]<<16|I[7]>>>16)+(I[6]<<16|I[6]>>>16)|0,t[1]=I[1]+(I[0]<<8|I[0]>>>24)+I[7]|0,t[2]=I[2]+(I[1]<<16|I[1]>>>16)+(I[0]<<16|I[0]>>>16)|0,t[3]=I[3]+(I[2]<<8|I[2]>>>24)+I[1]|0,t[4]=I[4]+(I[3]<<16|I[3]>>>16)+(I[2]<<16|I[2]>>>16)|0,t[5]=I[5]+(I[4]<<8|I[4]>>>24)+I[3]|0,t[6]=I[6]+(I[5]<<16|I[5]>>>16)+(I[4]<<16|I[4]>>>16)|0,t[7]=I[7]+(I[6]<<8|I[6]>>>24)+I[5]|0}return F=(M=U).lib,n=F.Base,o=F.WordArray,(M=M.x64={}).Word=n.extend({init:function(t,e){this.high=t,this.low=e}}),M.WordArray=n.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:8*t.length},toX32:function(){for(var t=this.words,e=t.length,r=[],i=0;i>>2]|=t[i]<<24-i%4*8;s.call(this,r,e)}else s.apply(this,arguments)}).prototype=P),function(){var t=U,n=t.lib.WordArray,t=t.enc;t.Utf16=t.Utf16BE={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],n=0;n>>2]>>>16-n%4*8&65535;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>1]|=t.charCodeAt(i)<<16-i%2*16;return n.create(r,2*e)}};function s(t){return t<<8&4278255360|t>>>8&16711935}t.Utf16LE={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],n=0;n>>2]>>>16-n%4*8&65535);i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i>>1]|=s(t.charCodeAt(i)<<16-i%2*16);return n.create(r,2*e)}}}(),a=(w=U).lib.WordArray,w.enc.Base64={stringify:function(t){var e=t.words,r=t.sigBytes,i=this._map;t.clamp();for(var n=[],o=0;o>>2]>>>24-o%4*8&255)<<16|(e[o+1>>>2]>>>24-(o+1)%4*8&255)<<8|e[o+2>>>2]>>>24-(o+2)%4*8&255,c=0;c<4&&o+.75*c>>6*(3-c)&63));var a=i.charAt(64);if(a)for(;n.length%4;)n.push(a);return n.join("")},parse:function(t){var e=t.length,r=this._map;if(!(i=this._reverseMap))for(var i=this._reverseMap=[],n=0;n>>6-o%4*2,c=s|c,i[n>>>2]|=c<<24-n%4*8,n++)}return a.create(i,n)}(t,e,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},h=(F=U).lib.WordArray,F.enc.Base64url={stringify:function(t,e=!0){var r=t.words,i=t.sigBytes,n=e?this._safe_map:this._map;t.clamp();for(var o=[],s=0;s>>2]>>>24-s%4*8&255)<<16|(r[s+1>>>2]>>>24-(s+1)%4*8&255)<<8|r[s+2>>>2]>>>24-(s+2)%4*8&255,a=0;a<4&&s+.75*a>>6*(3-a)&63));var h=n.charAt(64);if(h)for(;o.length%4;)o.push(h);return o.join("")},parse:function(t,e=!0){var r=t.length,i=e?this._safe_map:this._map;if(!(n=this._reverseMap))for(var n=this._reverseMap=[],o=0;o>>6-o%4*2,c=s|c,i[n>>>2]|=c<<24-n%4*8,n++)}return h.create(i,n)}(t,r,n)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",_safe_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"},function(a){var t=U,e=t.lib,r=e.WordArray,i=e.Hasher,e=t.algo,A=[];!function(){for(var t=0;t<64;t++)A[t]=4294967296*a.abs(a.sin(t+1))|0}();e=e.MD5=i.extend({_doReset:function(){this._hash=new r.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,e){for(var r=0;r<16;r++){var i=e+r,n=t[i];t[i]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8)}var o=this._hash.words,s=t[e+0],c=t[e+1],a=t[e+2],h=t[e+3],l=t[e+4],f=t[e+5],d=t[e+6],u=t[e+7],p=t[e+8],_=t[e+9],y=t[e+10],v=t[e+11],g=t[e+12],B=t[e+13],w=t[e+14],k=t[e+15],m=H(m=o[0],b=o[1],x=o[2],S=o[3],s,7,A[0]),S=H(S,m,b,x,c,12,A[1]),x=H(x,S,m,b,a,17,A[2]),b=H(b,x,S,m,h,22,A[3]);m=H(m,b,x,S,l,7,A[4]),S=H(S,m,b,x,f,12,A[5]),x=H(x,S,m,b,d,17,A[6]),b=H(b,x,S,m,u,22,A[7]),m=H(m,b,x,S,p,7,A[8]),S=H(S,m,b,x,_,12,A[9]),x=H(x,S,m,b,y,17,A[10]),b=H(b,x,S,m,v,22,A[11]),m=H(m,b,x,S,g,7,A[12]),S=H(S,m,b,x,B,12,A[13]),x=H(x,S,m,b,w,17,A[14]),m=z(m,b=H(b,x,S,m,k,22,A[15]),x,S,c,5,A[16]),S=z(S,m,b,x,d,9,A[17]),x=z(x,S,m,b,v,14,A[18]),b=z(b,x,S,m,s,20,A[19]),m=z(m,b,x,S,f,5,A[20]),S=z(S,m,b,x,y,9,A[21]),x=z(x,S,m,b,k,14,A[22]),b=z(b,x,S,m,l,20,A[23]),m=z(m,b,x,S,_,5,A[24]),S=z(S,m,b,x,w,9,A[25]),x=z(x,S,m,b,h,14,A[26]),b=z(b,x,S,m,p,20,A[27]),m=z(m,b,x,S,B,5,A[28]),S=z(S,m,b,x,a,9,A[29]),x=z(x,S,m,b,u,14,A[30]),m=C(m,b=z(b,x,S,m,g,20,A[31]),x,S,f,4,A[32]),S=C(S,m,b,x,p,11,A[33]),x=C(x,S,m,b,v,16,A[34]),b=C(b,x,S,m,w,23,A[35]),m=C(m,b,x,S,c,4,A[36]),S=C(S,m,b,x,l,11,A[37]),x=C(x,S,m,b,u,16,A[38]),b=C(b,x,S,m,y,23,A[39]),m=C(m,b,x,S,B,4,A[40]),S=C(S,m,b,x,s,11,A[41]),x=C(x,S,m,b,h,16,A[42]),b=C(b,x,S,m,d,23,A[43]),m=C(m,b,x,S,_,4,A[44]),S=C(S,m,b,x,g,11,A[45]),x=C(x,S,m,b,k,16,A[46]),m=D(m,b=C(b,x,S,m,a,23,A[47]),x,S,s,6,A[48]),S=D(S,m,b,x,u,10,A[49]),x=D(x,S,m,b,w,15,A[50]),b=D(b,x,S,m,f,21,A[51]),m=D(m,b,x,S,g,6,A[52]),S=D(S,m,b,x,h,10,A[53]),x=D(x,S,m,b,y,15,A[54]),b=D(b,x,S,m,c,21,A[55]),m=D(m,b,x,S,p,6,A[56]),S=D(S,m,b,x,k,10,A[57]),x=D(x,S,m,b,d,15,A[58]),b=D(b,x,S,m,B,21,A[59]),m=D(m,b,x,S,l,6,A[60]),S=D(S,m,b,x,v,10,A[61]),x=D(x,S,m,b,a,15,A[62]),b=D(b,x,S,m,_,21,A[63]),o[0]=o[0]+m|0,o[1]=o[1]+b|0,o[2]=o[2]+x|0,o[3]=o[3]+S|0},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;e[i>>>5]|=128<<24-i%32;var n=a.floor(r/4294967296),r=r;e[15+(64+i>>>9<<4)]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8),e[14+(64+i>>>9<<4)]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8),t.sigBytes=4*(e.length+1),this._process();for(var e=this._hash,o=e.words,s=0;s<4;s++){var c=o[s];o[s]=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8)}return e},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});function H(t,e,r,i,n,o,s){s=t+(e&r|~e&i)+n+s;return(s<>>32-o)+e}function z(t,e,r,i,n,o,s){s=t+(e&i|r&~i)+n+s;return(s<>>32-o)+e}function C(t,e,r,i,n,o,s){s=t+(e^r^i)+n+s;return(s<>>32-o)+e}function D(t,e,r,i,n,o,s){s=t+(r^(e|~i))+n+s;return(s<>>32-o)+e}t.MD5=i._createHelper(e),t.HmacMD5=i._createHmacHelper(e)}(Math),P=(M=U).lib,t=P.WordArray,e=P.Hasher,P=M.algo,l=[],P=P.SHA1=e.extend({_doReset:function(){this._hash=new t.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,e){for(var r=this._hash.words,i=r[0],n=r[1],o=r[2],s=r[3],c=r[4],a=0;a<80;a++){a<16?l[a]=0|t[e+a]:(h=l[a-3]^l[a-8]^l[a-14]^l[a-16],l[a]=h<<1|h>>>31);var h=(i<<5|i>>>27)+c+l[a];h+=a<20?1518500249+(n&o|~n&s):a<40?1859775393+(n^o^s):a<60?(n&o|n&s|o&s)-1894007588:(n^o^s)-899497514,c=s,s=o,o=n<<30|n>>>2,n=i,i=h}r[0]=r[0]+i|0,r[1]=r[1]+n|0,r[2]=r[2]+o|0,r[3]=r[3]+s|0,r[4]=r[4]+c|0},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=Math.floor(r/4294967296),e[15+(64+i>>>9<<4)]=r,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=e.clone.call(this);return t._hash=this._hash.clone(),t}}),M.SHA1=e._createHelper(P),M.HmacSHA1=e._createHmacHelper(P),function(n){var t=U,e=t.lib,r=e.WordArray,i=e.Hasher,e=t.algo,o=[],p=[];!function(){function t(t){return 4294967296*(t-(0|t))|0}for(var e=2,r=0;r<64;)!function(t){for(var e=n.sqrt(t),r=2;r<=e;r++)if(!(t%r))return;return 1}(e)||(r<8&&(o[r]=t(n.pow(e,.5))),p[r]=t(n.pow(e,1/3)),r++),e++}();var _=[],e=e.SHA256=i.extend({_doReset:function(){this._hash=new r.init(o.slice(0))},_doProcessBlock:function(t,e){for(var r=this._hash.words,i=r[0],n=r[1],o=r[2],s=r[3],c=r[4],a=r[5],h=r[6],l=r[7],f=0;f<64;f++){f<16?_[f]=0|t[e+f]:(d=_[f-15],u=_[f-2],_[f]=((d<<25|d>>>7)^(d<<14|d>>>18)^d>>>3)+_[f-7]+((u<<15|u>>>17)^(u<<13|u>>>19)^u>>>10)+_[f-16]);var d=i&n^i&o^n&o,u=l+((c<<26|c>>>6)^(c<<21|c>>>11)^(c<<7|c>>>25))+(c&a^~c&h)+p[f]+_[f],l=h,h=a,a=c,c=s+u|0,s=o,o=n,n=i,i=u+(((i<<30|i>>>2)^(i<<19|i>>>13)^(i<<10|i>>>22))+d)|0}r[0]=r[0]+i|0,r[1]=r[1]+n|0,r[2]=r[2]+o|0,r[3]=r[3]+s|0,r[4]=r[4]+c|0,r[5]=r[5]+a|0,r[6]=r[6]+h|0,r[7]=r[7]+l|0},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=n.floor(r/4294967296),e[15+(64+i>>>9<<4)]=r,t.sigBytes=4*e.length,this._process(),this._hash},clone:function(){var t=i.clone.call(this);return t._hash=this._hash.clone(),t}});t.SHA256=i._createHelper(e),t.HmacSHA256=i._createHmacHelper(e)}(Math),r=(w=U).lib.WordArray,F=w.algo,i=F.SHA256,F=F.SHA224=i.extend({_doReset:function(){this._hash=new r.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var t=i._doFinalize.call(this);return t.sigBytes-=4,t}}),w.SHA224=i._createHelper(F),w.HmacSHA224=i._createHmacHelper(F),function(){var t=U,e=t.lib.Hasher,r=t.x64,i=r.Word,n=r.WordArray,r=t.algo;function o(){return i.create.apply(i,arguments)}var t1=[o(1116352408,3609767458),o(1899447441,602891725),o(3049323471,3964484399),o(3921009573,2173295548),o(961987163,4081628472),o(1508970993,3053834265),o(2453635748,2937671579),o(2870763221,3664609560),o(3624381080,2734883394),o(310598401,1164996542),o(607225278,1323610764),o(1426881987,3590304994),o(1925078388,4068182383),o(2162078206,991336113),o(2614888103,633803317),o(3248222580,3479774868),o(3835390401,2666613458),o(4022224774,944711139),o(264347078,2341262773),o(604807628,2007800933),o(770255983,1495990901),o(1249150122,1856431235),o(1555081692,3175218132),o(1996064986,2198950837),o(2554220882,3999719339),o(2821834349,766784016),o(2952996808,2566594879),o(3210313671,3203337956),o(3336571891,1034457026),o(3584528711,2466948901),o(113926993,3758326383),o(338241895,168717936),o(666307205,1188179964),o(773529912,1546045734),o(1294757372,1522805485),o(1396182291,2643833823),o(1695183700,2343527390),o(1986661051,1014477480),o(2177026350,1206759142),o(2456956037,344077627),o(2730485921,1290863460),o(2820302411,3158454273),o(3259730800,3505952657),o(3345764771,106217008),o(3516065817,3606008344),o(3600352804,1432725776),o(4094571909,1467031594),o(275423344,851169720),o(430227734,3100823752),o(506948616,1363258195),o(659060556,3750685593),o(883997877,3785050280),o(958139571,3318307427),o(1322822218,3812723403),o(1537002063,2003034995),o(1747873779,3602036899),o(1955562222,1575990012),o(2024104815,1125592928),o(2227730452,2716904306),o(2361852424,442776044),o(2428436474,593698344),o(2756734187,3733110249),o(3204031479,2999351573),o(3329325298,3815920427),o(3391569614,3928383900),o(3515267271,566280711),o(3940187606,3454069534),o(4118630271,4000239992),o(116418474,1914138554),o(174292421,2731055270),o(289380356,3203993006),o(460393269,320620315),o(685471733,587496836),o(852142971,1086792851),o(1017036298,365543100),o(1126000580,2618297676),o(1288033470,3409855158),o(1501505948,4234509866),o(1607167915,987167468),o(1816402316,1246189591)],e1=[];!function(){for(var t=0;t<80;t++)e1[t]=o()}();r=r.SHA512=e.extend({_doReset:function(){this._hash=new n.init([new i.init(1779033703,4089235720),new i.init(3144134277,2227873595),new i.init(1013904242,4271175723),new i.init(2773480762,1595750129),new i.init(1359893119,2917565137),new i.init(2600822924,725511199),new i.init(528734635,4215389547),new i.init(1541459225,327033209)])},_doProcessBlock:function(t,e){for(var r=this._hash.words,i=r[0],n=r[1],o=r[2],s=r[3],c=r[4],a=r[5],h=r[6],l=r[7],f=i.high,d=i.low,u=n.high,p=n.low,_=o.high,y=o.low,v=s.high,g=s.low,B=c.high,w=c.low,k=a.high,m=a.low,S=h.high,x=h.low,b=l.high,r=l.low,A=f,H=d,z=u,C=p,D=_,E=y,R=v,M=g,F=B,P=w,W=k,O=m,I=S,U=x,K=b,X=r,L=0;L<80;L++){var j,T,N=e1[L];L<16?(T=N.high=0|t[e+2*L],j=N.low=0|t[e+2*L+1]):($=(q=e1[L-15]).high,J=q.low,G=(Q=e1[L-2]).high,V=Q.low,Z=(Y=e1[L-7]).high,q=Y.low,Y=(Q=e1[L-16]).high,T=(T=(($>>>1|J<<31)^($>>>8|J<<24)^$>>>7)+Z+((j=(Z=(J>>>1|$<<31)^(J>>>8|$<<24)^(J>>>7|$<<25))+q)>>>0>>0?1:0))+((G>>>19|V<<13)^(G<<3|V>>>29)^G>>>6)+((j+=J=(V>>>19|G<<13)^(V<<3|G>>>29)^(V>>>6|G<<26))>>>0>>0?1:0),j+=$=Q.low,N.high=T=T+Y+(j>>>0<$>>>0?1:0),N.low=j);var q=F&W^~F&I,Z=P&O^~P&U,V=A&z^A&D^z&D,G=(H>>>28|A<<4)^(H<<30|A>>>2)^(H<<25|A>>>7),J=t1[L],Q=J.high,Y=J.low,$=X+((P>>>14|F<<18)^(P>>>18|F<<14)^(P<<23|F>>>9)),N=K+((F>>>14|P<<18)^(F>>>18|P<<14)^(F<<23|P>>>9))+($>>>0>>0?1:0),J=G+(H&C^H&E^C&E),K=I,X=U,I=W,U=O,W=F,O=P,F=R+(N=(N=(N=N+q+(($=$+Z)>>>0>>0?1:0))+Q+(($=$+Y)>>>0>>0?1:0))+T+(($=$+j)>>>0>>0?1:0))+((P=M+$|0)>>>0>>0?1:0)|0,R=D,M=E,D=z,E=C,z=A,C=H,A=N+(((A>>>28|H<<4)^(A<<30|H>>>2)^(A<<25|H>>>7))+V+(J>>>0>>0?1:0))+((H=$+J|0)>>>0<$>>>0?1:0)|0}d=i.low=d+H,i.high=f+A+(d>>>0>>0?1:0),p=n.low=p+C,n.high=u+z+(p>>>0>>0?1:0),y=o.low=y+E,o.high=_+D+(y>>>0>>0?1:0),g=s.low=g+M,s.high=v+R+(g>>>0>>0?1:0),w=c.low=w+P,c.high=B+F+(w>>>0

>>0?1:0),m=a.low=m+O,a.high=k+W+(m>>>0>>0?1:0),x=h.low=x+U,h.high=S+I+(x>>>0>>0?1:0),r=l.low=r+X,l.high=b+K+(r>>>0>>0?1:0)},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;return e[i>>>5]|=128<<24-i%32,e[30+(128+i>>>10<<5)]=Math.floor(r/4294967296),e[31+(128+i>>>10<<5)]=r,t.sigBytes=4*e.length,this._process(),this._hash.toX32()},clone:function(){var t=e.clone.call(this);return t._hash=this._hash.clone(),t},blockSize:32});t.SHA512=e._createHelper(r),t.HmacSHA512=e._createHmacHelper(r)}(),P=(M=U).x64,c=P.Word,f=P.WordArray,P=M.algo,d=P.SHA512,P=P.SHA384=d.extend({_doReset:function(){this._hash=new f.init([new c.init(3418070365,3238371032),new c.init(1654270250,914150663),new c.init(2438529370,812702999),new c.init(355462360,4144912697),new c.init(1731405415,4290775857),new c.init(2394180231,1750603025),new c.init(3675008525,1694076839),new c.init(1203062813,3204075428)])},_doFinalize:function(){var t=d._doFinalize.call(this);return t.sigBytes-=16,t}}),M.SHA384=d._createHelper(P),M.HmacSHA384=d._createHmacHelper(P),function(l){var t=U,e=t.lib,f=e.WordArray,i=e.Hasher,d=t.x64.Word,e=t.algo,A=[],H=[],z=[];!function(){for(var t=1,e=0,r=0;r<24;r++){A[t+5*e]=(r+1)*(r+2)/2%64;var i=(2*t+3*e)%5;t=e%5,e=i}for(t=0;t<5;t++)for(e=0;e<5;e++)H[t+5*e]=e+(2*t+3*e)%5*5;for(var n=1,o=0;o<24;o++){for(var s,c=0,a=0,h=0;h<7;h++)1&n&&((s=(1<>>24)|4278255360&(o<<24|o>>>8);(m=r[n]).high^=s=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),m.low^=o}for(var c=0;c<24;c++){for(var a=0;a<5;a++){for(var h=0,l=0,f=0;f<5;f++)h^=(m=r[a+5*f]).high,l^=m.low;var d=C[a];d.high=h,d.low=l}for(a=0;a<5;a++)for(var u=C[(a+4)%5],p=C[(a+1)%5],_=p.high,p=p.low,h=u.high^(_<<1|p>>>31),l=u.low^(p<<1|_>>>31),f=0;f<5;f++)(m=r[a+5*f]).high^=h,m.low^=l;for(var y=1;y<25;y++){var v=(m=r[y]).high,g=m.low,B=A[y];l=B<32?(h=v<>>32-B,g<>>32-B):(h=g<>>64-B,v<>>64-B);B=C[H[y]];B.high=h,B.low=l}var w=C[0],k=r[0];w.high=k.high,w.low=k.low;for(a=0;a<5;a++)for(f=0;f<5;f++){var m=r[y=a+5*f],S=C[y],x=C[(a+1)%5+5*f],b=C[(a+2)%5+5*f];m.high=S.high^~x.high&b.high,m.low=S.low^~x.low&b.low}m=r[0],k=z[c];m.high^=k.high,m.low^=k.low}},_doFinalize:function(){var t=this._data,e=t.words,r=(this._nDataBytes,8*t.sigBytes),i=32*this.blockSize;e[r>>>5]|=1<<24-r%32,e[(l.ceil((1+r)/i)*i>>>5)-1]|=128,t.sigBytes=4*e.length,this._process();for(var n=this._state,e=this.cfg.outputLength/8,o=e/8,s=[],c=0;c>>24)|4278255360&(h<<24|h>>>8);s.push(a=16711935&(a<<8|a>>>24)|4278255360&(a<<24|a>>>8)),s.push(h)}return new f.init(s,e)},clone:function(){for(var t=i.clone.call(this),e=t._state=this._state.slice(0),r=0;r<25;r++)e[r]=e[r].clone();return t}});t.SHA3=i._createHelper(e),t.HmacSHA3=i._createHmacHelper(e)}(Math),Math,F=(w=U).lib,u=F.WordArray,p=F.Hasher,F=w.algo,S=u.create([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]),x=u.create([5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]),b=u.create([11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]),A=u.create([8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]),H=u.create([0,1518500249,1859775393,2400959708,2840853838]),z=u.create([1352829926,1548603684,1836072691,2053994217,0]),F=F.RIPEMD160=p.extend({_doReset:function(){this._hash=u.create([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(t,e){for(var r=0;r<16;r++){var i=e+r,n=t[i];t[i]=16711935&(n<<8|n>>>24)|4278255360&(n<<24|n>>>8)}for(var o,s,c,a,h,l,f=this._hash.words,d=H.words,u=z.words,p=S.words,_=x.words,y=b.words,v=A.words,g=o=f[0],B=s=f[1],w=c=f[2],k=a=f[3],m=h=f[4],r=0;r<80;r+=1)l=o+t[e+p[r]]|0,l+=r<16?(s^c^a)+d[0]:r<32?K(s,c,a)+d[1]:r<48?((s|~c)^a)+d[2]:r<64?X(s,c,a)+d[3]:(s^(c|~a))+d[4],l=(l=L(l|=0,y[r]))+h|0,o=h,h=a,a=L(c,10),c=s,s=l,l=g+t[e+_[r]]|0,l+=r<16?(B^(w|~k))+u[0]:r<32?X(B,w,k)+u[1]:r<48?((B|~w)^k)+u[2]:r<64?K(B,w,k)+u[3]:(B^w^k)+u[4],l=(l=L(l|=0,v[r]))+m|0,g=m,m=k,k=L(w,10),w=B,B=l;l=f[1]+c+k|0,f[1]=f[2]+a+m|0,f[2]=f[3]+h+g|0,f[3]=f[4]+o+B|0,f[4]=f[0]+s+w|0,f[0]=l},_doFinalize:function(){var t=this._data,e=t.words,r=8*this._nDataBytes,i=8*t.sigBytes;e[i>>>5]|=128<<24-i%32,e[14+(64+i>>>9<<4)]=16711935&(r<<8|r>>>24)|4278255360&(r<<24|r>>>8),t.sigBytes=4*(e.length+1),this._process();for(var e=this._hash,n=e.words,o=0;o<5;o++){var s=n[o];n[o]=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8)}return e},clone:function(){var t=p.clone.call(this);return t._hash=this._hash.clone(),t}}),w.RIPEMD160=p._createHelper(F),w.HmacRIPEMD160=p._createHmacHelper(F),P=(M=U).lib.Base,_=M.enc.Utf8,M.algo.HMAC=P.extend({init:function(t,e){t=this._hasher=new t.init,"string"==typeof e&&(e=_.parse(e));var r=t.blockSize,i=4*r;(e=e.sigBytes>i?t.finalize(e):e).clamp();for(var t=this._oKey=e.clone(),e=this._iKey=e.clone(),n=t.words,o=e.words,s=0;s>>2];t.sigBytes-=e}},d=(e.BlockCipher=a.extend({cfg:a.cfg.extend({mode:n,padding:l}),reset:function(){var t;a.reset.call(this);var e=this.cfg,r=e.iv,e=e.mode;this._xformMode==this._ENC_XFORM_MODE?t=e.createEncryptor:(t=e.createDecryptor,this._minBufferSize=1),this._mode&&this._mode.__creator==t?this._mode.init(this,r&&r.words):(this._mode=t.call(e,this,r&&r.words),this._mode.__creator=t)},_doProcessBlock:function(t,e){this._mode.processBlock(t,e)},_doFinalize:function(){var t,e=this.cfg.padding;return this._xformMode==this._ENC_XFORM_MODE?(e.pad(this._data,this.blockSize),t=this._process(!0)):(t=this._process(!0),e.unpad(t)),t},blockSize:4}),e.CipherParams=r.extend({init:function(t){this.mixIn(t)},toString:function(t){return(t||this.formatter).stringify(this)}})),l=(t.format={}).OpenSSL={stringify:function(t){var e=t.ciphertext,t=t.salt,e=t?s.create([1398893684,1701076831]).concat(t).concat(e):e;return e.toString(o)},parse:function(t){var e,r=o.parse(t),t=r.words;return 1398893684==t[0]&&1701076831==t[1]&&(e=s.create(t.slice(2,4)),t.splice(0,4),r.sigBytes-=16),d.create({ciphertext:r,salt:e})}},u=e.SerializableCipher=r.extend({cfg:r.extend({format:l}),encrypt:function(t,e,r,i){i=this.cfg.extend(i);var n=t.createEncryptor(r,i),e=n.finalize(e),n=n.cfg;return d.create({ciphertext:e,key:r,iv:n.iv,algorithm:t,mode:n.mode,padding:n.padding,blockSize:t.blockSize,formatter:i.format})},decrypt:function(t,e,r,i){return i=this.cfg.extend(i),e=this._parse(e,i.format),t.createDecryptor(r,i).finalize(e.ciphertext)},_parse:function(t,e){return"string"==typeof t?e.parse(t,this):t}}),t=(t.kdf={}).OpenSSL={execute:function(t,e,r,i){i=i||s.random(8);t=c.create({keySize:e+r}).compute(t,i),r=s.create(t.words.slice(e),4*r);return t.sigBytes=4*e,d.create({key:t,iv:r,salt:i})}},p=e.PasswordBasedCipher=u.extend({cfg:u.cfg.extend({kdf:t}),encrypt:function(t,e,r,i){r=(i=this.cfg.extend(i)).kdf.execute(r,t.keySize,t.ivSize);i.iv=r.iv;i=u.encrypt.call(this,t,e,r.key,i);return i.mixIn(r),i},decrypt:function(t,e,r,i){i=this.cfg.extend(i),e=this._parse(e,i.format);r=i.kdf.execute(r,t.keySize,t.ivSize,e.salt);return i.iv=r.iv,u.decrypt.call(this,t,e,r.key,i)}})}(),U.mode.CFB=((F=U.lib.BlockCipherMode.extend()).Encryptor=F.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize;j.call(this,t,e,i,r),this._prevBlock=t.slice(e,e+i)}}),F.Decryptor=F.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize,n=t.slice(e,e+i);j.call(this,t,e,i,r),this._prevBlock=n}}),F),U.mode.CTR=(M=U.lib.BlockCipherMode.extend(),P=M.Encryptor=M.extend({processBlock:function(t,e){var r=this._cipher,i=r.blockSize,n=this._iv,o=this._counter;n&&(o=this._counter=n.slice(0),this._iv=void 0);var s=o.slice(0);r.encryptBlock(s,0),o[i-1]=o[i-1]+1|0;for(var c=0;c>>2]|=e<<24-r%4*8,t.sigBytes+=e},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},U.pad.Iso10126={pad:function(t,e){e*=4,e-=t.sigBytes%e;t.concat(U.lib.WordArray.random(e-1)).concat(U.lib.WordArray.create([e<<24],1))},unpad:function(t){var e=255&t.words[t.sigBytes-1>>>2];t.sigBytes-=e}},U.pad.Iso97971={pad:function(t,e){t.concat(U.lib.WordArray.create([2147483648],1)),U.pad.ZeroPadding.pad(t,e)},unpad:function(t){U.pad.ZeroPadding.unpad(t),t.sigBytes--}},U.pad.ZeroPadding={pad:function(t,e){e*=4;t.clamp(),t.sigBytes+=e-(t.sigBytes%e||e)},unpad:function(t){for(var e=t.words,r=t.sigBytes-1,r=t.sigBytes-1;0<=r;r--)if(e[r>>>2]>>>24-r%4*8&255){t.sigBytes=r+1;break}}},U.pad.NoPadding={pad:function(){},unpad:function(){}},m=(P=U).lib.CipherParams,C=P.enc.Hex,P.format.Hex={stringify:function(t){return t.ciphertext.toString(C)},parse:function(t){t=C.parse(t);return m.create({ciphertext:t})}},function(){var t=U,e=t.lib.BlockCipher,r=t.algo,h=[],l=[],f=[],d=[],u=[],p=[],_=[],y=[],v=[],g=[];!function(){for(var t=[],e=0;e<256;e++)t[e]=e<128?e<<1:e<<1^283;for(var r=0,i=0,e=0;e<256;e++){var n=i^i<<1^i<<2^i<<3^i<<4;h[r]=n=n>>>8^255&n^99;var o=t[l[n]=r],s=t[o],c=t[s],a=257*t[n]^16843008*n;f[r]=a<<24|a>>>8,d[r]=a<<16|a>>>16,u[r]=a<<8|a>>>24,p[r]=a,_[n]=(a=16843009*c^65537*s^257*o^16843008*r)<<24|a>>>8,y[n]=a<<16|a>>>16,v[n]=a<<8|a>>>24,g[n]=a,r?(r=o^t[t[t[c^o]]],i^=t[t[i]]):r=i=1}}();var B=[0,1,2,4,8,16,32,64,128,27,54],r=r.AES=e.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var t=this._keyPriorReset=this._key,e=t.words,r=t.sigBytes/4,i=4*(1+(this._nRounds=6+r)),n=this._keySchedule=[],o=0;o>>24]<<24|h[a>>>16&255]<<16|h[a>>>8&255]<<8|h[255&a]):(a=h[(a=a<<8|a>>>24)>>>24]<<24|h[a>>>16&255]<<16|h[a>>>8&255]<<8|h[255&a],a^=B[o/r|0]<<24),n[o]=n[o-r]^a);for(var s=this._invKeySchedule=[],c=0;c>>24]]^y[h[a>>>16&255]]^v[h[a>>>8&255]]^g[h[255&a]]}}},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._keySchedule,f,d,u,p,h)},decryptBlock:function(t,e){var r=t[e+1];t[e+1]=t[e+3],t[e+3]=r,this._doCryptBlock(t,e,this._invKeySchedule,_,y,v,g,l);r=t[e+1];t[e+1]=t[e+3],t[e+3]=r},_doCryptBlock:function(t,e,r,i,n,o,s,c){for(var a=this._nRounds,h=t[e]^r[0],l=t[e+1]^r[1],f=t[e+2]^r[2],d=t[e+3]^r[3],u=4,p=1;p>>24]^n[l>>>16&255]^o[f>>>8&255]^s[255&d]^r[u++],y=i[l>>>24]^n[f>>>16&255]^o[d>>>8&255]^s[255&h]^r[u++],v=i[f>>>24]^n[d>>>16&255]^o[h>>>8&255]^s[255&l]^r[u++],g=i[d>>>24]^n[h>>>16&255]^o[l>>>8&255]^s[255&f]^r[u++],h=_,l=y,f=v,d=g;_=(c[h>>>24]<<24|c[l>>>16&255]<<16|c[f>>>8&255]<<8|c[255&d])^r[u++],y=(c[l>>>24]<<24|c[f>>>16&255]<<16|c[d>>>8&255]<<8|c[255&h])^r[u++],v=(c[f>>>24]<<24|c[d>>>16&255]<<16|c[h>>>8&255]<<8|c[255&l])^r[u++],g=(c[d>>>24]<<24|c[h>>>16&255]<<16|c[l>>>8&255]<<8|c[255&f])^r[u++];t[e]=_,t[e+1]=y,t[e+2]=v,t[e+3]=g},keySize:8});t.AES=e._createHelper(r)}(),function(){var t=U,e=t.lib,i=e.WordArray,r=e.BlockCipher,e=t.algo,h=[57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4],l=[14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32],f=[1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28],d=[{0:8421888,268435456:32768,536870912:8421378,805306368:2,1073741824:512,1342177280:8421890,1610612736:8389122,1879048192:8388608,2147483648:514,2415919104:8389120,2684354560:33280,2952790016:8421376,3221225472:32770,3489660928:8388610,3758096384:0,4026531840:33282,134217728:0,402653184:8421890,671088640:33282,939524096:32768,1207959552:8421888,1476395008:512,1744830464:8421378,2013265920:2,2281701376:8389120,2550136832:33280,2818572288:8421376,3087007744:8389122,3355443200:8388610,3623878656:32770,3892314112:514,4160749568:8388608,1:32768,268435457:2,536870913:8421888,805306369:8388608,1073741825:8421378,1342177281:33280,1610612737:512,1879048193:8389122,2147483649:8421890,2415919105:8421376,2684354561:8388610,2952790017:33282,3221225473:514,3489660929:8389120,3758096385:32770,4026531841:0,134217729:8421890,402653185:8421376,671088641:8388608,939524097:512,1207959553:32768,1476395009:8388610,1744830465:2,2013265921:33282,2281701377:32770,2550136833:8389122,2818572289:514,3087007745:8421888,3355443201:8389120,3623878657:0,3892314113:33280,4160749569:8421378},{0:1074282512,16777216:16384,33554432:524288,50331648:1074266128,67108864:1073741840,83886080:1074282496,100663296:1073758208,117440512:16,134217728:540672,150994944:1073758224,167772160:1073741824,184549376:540688,201326592:524304,218103808:0,234881024:16400,251658240:1074266112,8388608:1073758208,25165824:540688,41943040:16,58720256:1073758224,75497472:1074282512,92274688:1073741824,109051904:524288,125829120:1074266128,142606336:524304,159383552:0,176160768:16384,192937984:1074266112,209715200:1073741840,226492416:540672,243269632:1074282496,260046848:16400,268435456:0,285212672:1074266128,301989888:1073758224,318767104:1074282496,335544320:1074266112,352321536:16,369098752:540688,385875968:16384,402653184:16400,419430400:524288,436207616:524304,452984832:1073741840,469762048:540672,486539264:1073758208,503316480:1073741824,520093696:1074282512,276824064:540688,293601280:524288,310378496:1074266112,327155712:16384,343932928:1073758208,360710144:1074282512,377487360:16,394264576:1073741824,411041792:1074282496,427819008:1073741840,444596224:1073758224,461373440:524304,478150656:0,494927872:16400,511705088:1074266128,528482304:540672},{0:260,1048576:0,2097152:67109120,3145728:65796,4194304:65540,5242880:67108868,6291456:67174660,7340032:67174400,8388608:67108864,9437184:67174656,10485760:65792,11534336:67174404,12582912:67109124,13631488:65536,14680064:4,15728640:256,524288:67174656,1572864:67174404,2621440:0,3670016:67109120,4718592:67108868,5767168:65536,6815744:65540,7864320:260,8912896:4,9961472:256,11010048:67174400,12058624:65796,13107200:65792,14155776:67109124,15204352:67174660,16252928:67108864,16777216:67174656,17825792:65540,18874368:65536,19922944:67109120,20971520:256,22020096:67174660,23068672:67108868,24117248:0,25165824:67109124,26214400:67108864,27262976:4,28311552:65792,29360128:67174400,30408704:260,31457280:65796,32505856:67174404,17301504:67108864,18350080:260,19398656:67174656,20447232:0,21495808:65540,22544384:67109120,23592960:256,24641536:67174404,25690112:65536,26738688:67174660,27787264:65796,28835840:67108868,29884416:67109124,30932992:67174400,31981568:4,33030144:65792},{0:2151682048,65536:2147487808,131072:4198464,196608:2151677952,262144:0,327680:4198400,393216:2147483712,458752:4194368,524288:2147483648,589824:4194304,655360:64,720896:2147487744,786432:2151678016,851968:4160,917504:4096,983040:2151682112,32768:2147487808,98304:64,163840:2151678016,229376:2147487744,294912:4198400,360448:2151682112,425984:0,491520:2151677952,557056:4096,622592:2151682048,688128:4194304,753664:4160,819200:2147483648,884736:4194368,950272:4198464,1015808:2147483712,1048576:4194368,1114112:4198400,1179648:2147483712,1245184:0,1310720:4160,1376256:2151678016,1441792:2151682048,1507328:2147487808,1572864:2151682112,1638400:2147483648,1703936:2151677952,1769472:4198464,1835008:2147487744,1900544:4194304,1966080:64,2031616:4096,1081344:2151677952,1146880:2151682112,1212416:0,1277952:4198400,1343488:4194368,1409024:2147483648,1474560:2147487808,1540096:64,1605632:2147483712,1671168:4096,1736704:2147487744,1802240:2151678016,1867776:4160,1933312:2151682048,1998848:4194304,2064384:4198464},{0:128,4096:17039360,8192:262144,12288:536870912,16384:537133184,20480:16777344,24576:553648256,28672:262272,32768:16777216,36864:537133056,40960:536871040,45056:553910400,49152:553910272,53248:0,57344:17039488,61440:553648128,2048:17039488,6144:553648256,10240:128,14336:17039360,18432:262144,22528:537133184,26624:553910272,30720:536870912,34816:537133056,38912:0,43008:553910400,47104:16777344,51200:536871040,55296:553648128,59392:16777216,63488:262272,65536:262144,69632:128,73728:536870912,77824:553648256,81920:16777344,86016:553910272,90112:537133184,94208:16777216,98304:553910400,102400:553648128,106496:17039360,110592:537133056,114688:262272,118784:536871040,122880:0,126976:17039488,67584:553648256,71680:16777216,75776:17039360,79872:537133184,83968:536870912,88064:17039488,92160:128,96256:553910272,100352:262272,104448:553910400,108544:0,112640:553648128,116736:16777344,120832:262144,124928:537133056,129024:536871040},{0:268435464,256:8192,512:270532608,768:270540808,1024:268443648,1280:2097152,1536:2097160,1792:268435456,2048:0,2304:268443656,2560:2105344,2816:8,3072:270532616,3328:2105352,3584:8200,3840:270540800,128:270532608,384:270540808,640:8,896:2097152,1152:2105352,1408:268435464,1664:268443648,1920:8200,2176:2097160,2432:8192,2688:268443656,2944:270532616,3200:0,3456:270540800,3712:2105344,3968:268435456,4096:268443648,4352:270532616,4608:270540808,4864:8200,5120:2097152,5376:268435456,5632:268435464,5888:2105344,6144:2105352,6400:0,6656:8,6912:270532608,7168:8192,7424:268443656,7680:270540800,7936:2097160,4224:8,4480:2105344,4736:2097152,4992:268435464,5248:268443648,5504:8200,5760:270540808,6016:270532608,6272:270540800,6528:270532616,6784:8192,7040:2105352,7296:2097160,7552:0,7808:268435456,8064:268443656},{0:1048576,16:33555457,32:1024,48:1049601,64:34604033,80:0,96:1,112:34603009,128:33555456,144:1048577,160:33554433,176:34604032,192:34603008,208:1025,224:1049600,240:33554432,8:34603009,24:0,40:33555457,56:34604032,72:1048576,88:33554433,104:33554432,120:1025,136:1049601,152:33555456,168:34603008,184:1048577,200:1024,216:34604033,232:1,248:1049600,256:33554432,272:1048576,288:33555457,304:34603009,320:1048577,336:33555456,352:34604032,368:1049601,384:1025,400:34604033,416:1049600,432:1,448:0,464:34603008,480:33554433,496:1024,264:1049600,280:33555457,296:34603009,312:1,328:33554432,344:1048576,360:1025,376:34604032,392:33554433,408:34603008,424:0,440:34604033,456:1049601,472:1024,488:33555456,504:1048577},{0:134219808,1:131072,2:134217728,3:32,4:131104,5:134350880,6:134350848,7:2048,8:134348800,9:134219776,10:133120,11:134348832,12:2080,13:0,14:134217760,15:133152,2147483648:2048,2147483649:134350880,2147483650:134219808,2147483651:134217728,2147483652:134348800,2147483653:133120,2147483654:133152,2147483655:32,2147483656:134217760,2147483657:2080,2147483658:131104,2147483659:134350848,2147483660:0,2147483661:134348832,2147483662:134219776,2147483663:131072,16:133152,17:134350848,18:32,19:2048,20:134219776,21:134217760,22:134348832,23:131072,24:0,25:131104,26:134348800,27:134219808,28:134350880,29:133120,30:2080,31:134217728,2147483664:131072,2147483665:2048,2147483666:134348832,2147483667:133152,2147483668:32,2147483669:134348800,2147483670:134217728,2147483671:134219808,2147483672:134350880,2147483673:134217760,2147483674:134219776,2147483675:0,2147483676:133120,2147483677:2080,2147483678:131104,2147483679:134350848}],u=[4160749569,528482304,33030144,2064384,129024,8064,504,2147483679],n=e.DES=r.extend({_doReset:function(){for(var t=this._key.words,e=[],r=0;r<56;r++){var i=h[r]-1;e[r]=t[i>>>5]>>>31-i%32&1}for(var n=this._subKeys=[],o=0;o<16;o++){for(var s=n[o]=[],c=f[o],r=0;r<24;r++)s[r/6|0]|=e[(l[r]-1+c)%28]<<31-r%6,s[4+(r/6|0)]|=e[28+(l[r+24]-1+c)%28]<<31-r%6;s[0]=s[0]<<1|s[0]>>>31;for(r=1;r<7;r++)s[r]=s[r]>>>4*(r-1)+3;s[7]=s[7]<<5|s[7]>>>27}for(var a=this._invSubKeys=[],r=0;r<16;r++)a[r]=n[15-r]},encryptBlock:function(t,e){this._doCryptBlock(t,e,this._subKeys)},decryptBlock:function(t,e){this._doCryptBlock(t,e,this._invSubKeys)},_doCryptBlock:function(t,e,r){this._lBlock=t[e],this._rBlock=t[e+1],p.call(this,4,252645135),p.call(this,16,65535),_.call(this,2,858993459),_.call(this,8,16711935),p.call(this,1,1431655765);for(var i=0;i<16;i++){for(var n=r[i],o=this._lBlock,s=this._rBlock,c=0,a=0;a<8;a++)c|=d[a][((s^n[a])&u[a])>>>0];this._lBlock=s,this._rBlock=o^c}var h=this._lBlock;this._lBlock=this._rBlock,this._rBlock=h,p.call(this,1,1431655765),_.call(this,8,16711935),_.call(this,2,858993459),p.call(this,16,65535),p.call(this,4,252645135),t[e]=this._lBlock,t[e+1]=this._rBlock},keySize:2,ivSize:2,blockSize:2});function p(t,e){e=(this._lBlock>>>t^this._rBlock)&e;this._rBlock^=e,this._lBlock^=e<>>t^this._lBlock)&e;this._lBlock^=e,this._rBlock^=e<192.");var e=t.slice(0,2),r=t.length<4?t.slice(0,2):t.slice(2,4),t=t.length<6?t.slice(0,2):t.slice(4,6);this._des1=n.createEncryptor(i.create(e)),this._des2=n.createEncryptor(i.create(r)),this._des3=n.createEncryptor(i.create(t))},encryptBlock:function(t,e){this._des1.encryptBlock(t,e),this._des2.decryptBlock(t,e),this._des3.encryptBlock(t,e)},decryptBlock:function(t,e){this._des3.decryptBlock(t,e),this._des2.encryptBlock(t,e),this._des1.decryptBlock(t,e)},keySize:6,ivSize:2,blockSize:2});t.TripleDES=r._createHelper(e)}(),function(){var t=U,e=t.lib.StreamCipher,r=t.algo,i=r.RC4=e.extend({_doReset:function(){for(var t=this._key,e=t.words,r=t.sigBytes,i=this._S=[],n=0;n<256;n++)i[n]=n;for(var n=0,o=0;n<256;n++){var s=n%r,s=e[s>>>2]>>>24-s%4*8&255,o=(o+i[n]+s)%256,s=i[n];i[n]=i[o],i[o]=s}this._i=this._j=0},_doProcessBlock:function(t,e){t[e]^=n.call(this)},keySize:8,ivSize:0});function n(){for(var t=this._S,e=this._i,r=this._j,i=0,n=0;n<4;n++){var r=(r+t[e=(e+1)%256])%256,o=t[e];t[e]=t[r],t[r]=o,i|=t[(t[e]+t[r])%256]<<24-8*n}return this._i=e,this._j=r,i}t.RC4=e._createHelper(i);r=r.RC4Drop=i.extend({cfg:i.cfg.extend({drop:192}),_doReset:function(){i._doReset.call(this);for(var t=this.cfg.drop;0>>24)|4278255360&(t[r]<<24|t[r]>>>8);for(var i=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],n=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]],r=this._b=0;r<4;r++)N.call(this);for(r=0;r<8;r++)n[r]^=i[r+4&7];if(e){var o=e.words,s=o[0],c=o[1],e=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),o=16711935&(c<<8|c>>>24)|4278255360&(c<<24|c>>>8),s=e>>>16|4294901760&o,c=o<<16|65535&e;n[0]^=e,n[1]^=s,n[2]^=o,n[3]^=c,n[4]^=e,n[5]^=s,n[6]^=o,n[7]^=c;for(r=0;r<4;r++)N.call(this)}},_doProcessBlock:function(t,e){var r=this._X;N.call(this),D[0]=r[0]^r[5]>>>16^r[3]<<16,D[1]=r[2]^r[7]>>>16^r[5]<<16,D[2]=r[4]^r[1]>>>16^r[7]<<16,D[3]=r[6]^r[3]>>>16^r[1]<<16;for(var i=0;i<4;i++)D[i]=16711935&(D[i]<<8|D[i]>>>24)|4278255360&(D[i]<<24|D[i]>>>8),t[e+i]^=D[i]},blockSize:4,ivSize:2}),M.Rabbit=F._createHelper(P),F=(M=U).lib.StreamCipher,P=M.algo,W=[],O=[],I=[],P=P.RabbitLegacy=F.extend({_doReset:function(){for(var t=this._key.words,e=this.cfg.iv,r=this._X=[t[0],t[3]<<16|t[2]>>>16,t[1],t[0]<<16|t[3]>>>16,t[2],t[1]<<16|t[0]>>>16,t[3],t[2]<<16|t[1]>>>16],i=this._C=[t[2]<<16|t[2]>>>16,4294901760&t[0]|65535&t[1],t[3]<<16|t[3]>>>16,4294901760&t[1]|65535&t[2],t[0]<<16|t[0]>>>16,4294901760&t[2]|65535&t[3],t[1]<<16|t[1]>>>16,4294901760&t[3]|65535&t[0]],n=this._b=0;n<4;n++)q.call(this);for(n=0;n<8;n++)i[n]^=r[n+4&7];if(e){var o=e.words,s=o[0],t=o[1],e=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),o=16711935&(t<<8|t>>>24)|4278255360&(t<<24|t>>>8),s=e>>>16|4294901760&o,t=o<<16|65535&e;i[0]^=e,i[1]^=s,i[2]^=o,i[3]^=t,i[4]^=e,i[5]^=s,i[6]^=o,i[7]^=t;for(n=0;n<4;n++)q.call(this)}},_doProcessBlock:function(t,e){var r=this._X;q.call(this),W[0]=r[0]^r[5]>>>16^r[3]<<16,W[1]=r[2]^r[7]>>>16^r[5]<<16,W[2]=r[4]^r[1]>>>16^r[7]<<16,W[3]=r[6]^r[3]>>>16^r[1]<<16;for(var i=0;i<4;i++)W[i]=16711935&(W[i]<<8|W[i]>>>24)|4278255360&(W[i]<<24|W[i]>>>8),t[e+i]^=W[i]},blockSize:4,ivSize:2}),M.RabbitLegacy=F._createHelper(P),U}); \ No newline at end of file diff --git a/data/js/crypto-js.min.js.gz b/data/js/crypto-js.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..022247489e06a845cc4561cf137fff77213e4333 GIT binary patch literal 16587 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 G7#IMXL5Jf2 literal 0 HcmV?d00001 diff --git a/data/login.html b/data/login.html new file mode 100644 index 0000000..96fb546 --- /dev/null +++ b/data/login.html @@ -0,0 +1,70 @@ + + + + + + Connexion - ESP32 Webapp + + + +

+ + + + + diff --git a/data/login.html.gz b/data/login.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..884711bcf805fa257af459be94fbc86665b0487f GIT binary patch literal 929 zcmV;S177?eiwFo}B7JEB|7>q(X>KlPbZu+^tyax$+c*%u=P9_tqLP6~O0y`iNGu&T zae)HcHfY?#-b#~Wi(N`oNy>^5_$_+xEA|m~C|Qy1NZBB$0K+8boBtUOr@vi)yqf=W zGb5SJWqLAoe?-z^m3T|-xdWg)Jt6o_bFh@KjA{cm@jlPrhJSkdq=M#W^%s0{}K%{@r>O_Fc|=9&NM6&FJ{KX?Qw)l=S|$YGhHaYAxu(Z5>Kj?D8iDi zpfiDs^&!!DLItYdAx2PC^F2TV%3h~Ge|-P`$Ok+>0-pCiBl-N0e285tofE-#df(#i zNbRwGjv-uWRh0w4HjZ=wk{~YeDg)Z3(n~+6oN&LgLJC7IRC*dWxDgqmC@V|sro<|+ zaEEztHa>{k)knYA@U0RWIH8qQOy#8nyAK%fDMqIrAOAYvOJu2)OXGWG2?yd1;Q4Mp z@HejayVmu7KjJ#8w8oAJdox}hf<aShZd==SM=xzpICWUrBW$lkKu8>U5RiO~$(`KyU(DWb2&iESzA zKCBnX6{~U#WV8Z1li-$rZQk*~e`pwg5>d`)Yoz=w3|>>91HX-ue@dv?6pREgAxTQ^ z53Gl%)Zo~zVM!}#gURK1r(#TvC0#+2WZbY0gLbW{tl)A$PFEFA$>}gp$=N068guuA zq#eqC%0H_&N8#zUXfP!%oY%Cb!jdIemIZ#?y=&5sN6rOR6K z`+Y0r^QU)L*v+ay)j>y{Y+rS083uOL$@a@5ZQG;x5B6ajaZYgvchT;`kL{w0;5kCX zk_1jfWGcom`IFHMb>$N^-Tt91?XFsu1n2Q`#WrJX# zGxxt*6kOGjRBgs1_aJaK#&`~Qk$dHg*tb0;Z_dsJ;qUKH3JU#ydS|)+eiwXb$ D1un?A literal 0 HcmV?d00001 diff --git a/data/settings.html b/data/settings.html new file mode 100644 index 0000000..28c0277 --- /dev/null +++ b/data/settings.html @@ -0,0 +1,199 @@ + + + + + + Paramètres - ESP32 Webapp + + + + + +
+

Paramètres

+ +
+

Configuration WiFi

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+

Configuration système

+
+
+ + +
+ +
+ + +
+ + +
+
+ +
+

Changer le mot de passe

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+
+ + + + + + diff --git a/data/settings.html.gz b/data/settings.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..17967234024a87199031c013925dd90e2f6a2b92 GIT binary patch literal 1790 zcmV2iD&HGz4fQ|bQ9iYdi7@m6P-nD7@Hg~9QDVDPNKlsX%lV7`_eOq; zQy(m2#$X->LK(ykxB`x#bLJ@5a$v@6=*KVHgmI=A_4?AL9$35PPDV;G<-zEb3hFPf zl>kZl@kfi2}ASvK)e>oZ6c z8jb^Jky7AvMr@BtX&Lx9rXm$3-gH=gvp*J;JI3hWzdn7cNQI!yh(7QrM>htcnV5__ zaA|7W8>4eN_JBsj0WuDRGaS$+EIbCq_*5o{88d1tHUlGX!3{K`(4h)a)^C^uvizG| z1d-T);|0?oQpV_bd1GtkFIm6~;5kza2dI4VxK%cb_7BR?czJufyG67~ zY*=@1bQJIjn?`~vl#cw$UNEG!SAv@a!Y9np9y(z!@TEkk%uB=fry_{L8okM za0$(KQZkh8?CjMsaUvp?HxkcwIEcX-NH88oil{}1!KC04ojH>*p_ay3j(k<-qAynL z_b|GJh&KS4n)Yfe^ToGAoxn9a?x?T)DYJ(ei4xk{fDMOxu%$VkneoJ^P zht5-cT5zpLGY$=#OoJ|P8ZeG4o#(c_yq5R1Q=Ua?rNYvq*=QUoh4GchD5J5@)G8B= z6({)D4+Zn7SQw)dE?~+e2DV7OBtb7lFyJuHY5z8eX1aSTp3D8zFHlZZFRpz>wQZeR zg`ZRXC|RlV%X=PwnMHQ|mGuX(!=0kOF#tb1@U`APrZ+l;75nUcROqW$kIB2V1q&6C z!p08$LJnxi%nPY;lATdKm@O8e3i=n)#2ng;uol+?MdAaP5{rDeI;`qJ<$~K}5^+1O zx?Vt?qz-p`aVXQ<~g~2m0}c z#Ve=VS+&xgo;jml1jsy*!qi))-Xb4Al1`@`_p`JRw3i^O<5dBYhQ!POn#5?NT&k6g z3`%gC&$FE(pDSWhWxJ#cLIk2$b9B-T=5G>j?RHKuNn@{x!J+RArm;-=&7&#nv@h#y zlTJ*rQ##zMt?JY2VdYo7hEI%P4DwNxYxsbh>dtJ!ubR9UVyvKdF{ zTYWATd7rM6Z$4-VuHH9dX)%wI%MrxWZ$(fX?9*PVi!o!3YAoF#!%F1CO&CS~Wp}#T zCQo;Ec5+JBn15p|=_UgmjXKWB3^!h5e*`$@qcg^N6%Rbg5nZU!4$M%1MmvTHjXYHj z`K@ZM$VI7jhbZ4n7rjJdoTf$QR?1m5s;AB-q?;$#YfOBJGU1wS9<3=r8Wf;m8}W0K zW3%vUQn!hoBd3#icxTz`%fh-E$W=8+*w)m9LL#bO1rf;F_T>$j3%8j_s)u8Z1*-PuXlKds9TS%j~aL7;$*e2aEBY{m1~iO?Uj)5imUEj z>)a+KzO0{R)8PNk*Ycy}qj?)9vvlfflhMipVx!X`S)8cakjxKWo5-cZNh>K!b4-i6 zq%(0|;3t+<=6I{4yauVK7P+3%Iksp4T1c_Jh7tuss!X>&!24o^hq?k8S&s4#g5{yq){T2L4VfbC9W20WDhg>NApYa#x)R)h%j#TV^iYNyU_wH_0*S z+`GJ%cgf2wJ9PbX#pb(RDbOJnZ<=YJZ!&Is zkpr^Z+27gi^m+{w9^$m%Q5XyPdMwO`&;jX?Z%IL)jm+;- g*Yfufhv{8PcF8duB%g(cgXDAgf7_gWiyt2V0L?UOqW}N^ literal 0 HcmV?d00001 diff --git a/data/update.html b/data/update.html new file mode 100644 index 0000000..fb9d56d --- /dev/null +++ b/data/update.html @@ -0,0 +1,302 @@ + + + + + + Mise à jour - ESP32 Webapp + + + + + + +
+

Mise à jour OTA

+ +
+

⚠️ Attention

+
    +
  • Ne débranchez pas l'ESP32 pendant la mise à jour
  • +
  • La mise à jour du système de fichiers effacera toutes les données
  • +
  • L'ESP32 redémarrera automatiquement après la mise à jour
  • +
  • Vous devrez vous reconnecter après le redémarrage
  • +
+
+ +
+ +
+

Mise à jour du firmware

+

Sélectionnez un fichier .bin pour mettre à jour le firmware de l'ESP32.

+ +
+
+ + +
+ + +
+ + +
+ +
+

Mise à jour du système de fichiers

+

Sélectionnez un fichier .bin contenant le système de fichiers LittleFS.

+ +
+
+ + +
+ + +
+ + +
+
+ + + + + + + + diff --git a/data/update.html.gz b/data/update.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..ec683db0eb534463b9a17d1a25e43c638f692284 GIT binary patch literal 2649 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 HI4uAGamgGZ literal 0 HcmV?d00001 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..69f41ae --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +[env:esp32] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +board_build.filesystem = littlefs +board_build.partitions = default.csv +extra_scripts = platformio_extra.py + +lib_deps = + ESP32Async/ESPAsyncWebServer + ESP32Async/AsyncTCP + bblanchon/ArduinoJson@^7.0.0 + +build_flags = + -DBOARD_HAS_PSRAM + +upload_speed = 921600 diff --git a/platformio_extra.py b/platformio_extra.py new file mode 100644 index 0000000..089ad1b --- /dev/null +++ b/platformio_extra.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +PlatformIO extra script to compress assets before building filesystem +""" +import subprocess +import sys +from pathlib import Path + +def before_buildfs(source, target, env): + """Hook called before building filesystem image.""" + project_dir = Path(env.get("PROJECT_DIR")) + data_dir = project_dir / "data" + + # Get the script path + script_path = project_dir / "compress_assets.py" + + if not script_path.exists(): + print(f"Warning: {script_path} not found") + return + + print("\n" + "="*60) + print("Compressing web assets (CSS, JS, HTML)...") + print("="*60 + "\n") + + try: + result = subprocess.run( + [sys.executable, str(script_path), str(data_dir)], + check=True, + cwd=str(project_dir) + ) + except subprocess.CalledProcessError as e: + print(f"Error compressing assets: {e}") + sys.exit(1) + +# Register the hook +Import("env") +env.AddPreAction("buildfs", before_buildfs) diff --git a/src/auth.cpp b/src/auth.cpp new file mode 100644 index 0000000..bb7b5d1 --- /dev/null +++ b/src/auth.cpp @@ -0,0 +1,89 @@ +#include "auth.h" +#include "utils.h" + +AuthManager authManager; + +AuthManager::AuthManager() { + sessionTimeout = 60; // 60 minutes par défaut +} + +void AuthManager::setCredentials(const String& user, const String& passHash) { + username = user; + passwordHash = passHash; + logMessage(LOG_INFO, "Identifiants configurés pour l'utilisateur: " + user); +} + +void AuthManager::setSessionTimeout(unsigned long timeout) { + sessionTimeout = timeout; + logMessage(LOG_INFO, "Timeout de session défini à " + String(timeout) + " minutes"); +} + +bool AuthManager::authenticate(const String& user, const String& password) { + String inputHash = sha256(password); + + if (user == username && inputHash == passwordHash) { + logMessage(LOG_INFO, "Authentification réussie pour: " + user); + return true; + } + + logMessage(LOG_WARN, "Échec d'authentification pour: " + user); + return false; +} + +String AuthManager::createSession(const String& user) { + String sessionId = generateSessionId(); + Session session; + session.username = user; + session.lastActivity = getCurrentTime(); + session.timeout = sessionTimeout * 60 * 1000; // conversion en millisecondes + + sessions[sessionId] = session; + logMessage(LOG_INFO, "Session créée: " + sessionId + " pour " + user); + + return sessionId; +} + +bool AuthManager::validateSession(const String& sessionId) { + auto it = sessions.find(sessionId); + if (it == sessions.end()) { + return false; + } + + unsigned long now = getCurrentTime(); + unsigned long elapsed = now - it->second.lastActivity; + + if (elapsed > it->second.timeout) { + logMessage(LOG_INFO, "Session expirée: " + sessionId); + sessions.erase(it); + return false; + } + + return true; +} + +void AuthManager::updateSessionActivity(const String& sessionId) { + auto it = sessions.find(sessionId); + if (it != sessions.end()) { + it->second.lastActivity = getCurrentTime(); + } +} + +void AuthManager::removeSession(const String& sessionId) { + sessions.erase(sessionId); + logMessage(LOG_INFO, "Session supprimée: " + sessionId); +} + +void AuthManager::cleanExpiredSessions() { + unsigned long now = getCurrentTime(); + auto it = sessions.begin(); + + while (it != sessions.end()) { + unsigned long elapsed = now - it->second.lastActivity; + if (elapsed > it->second.timeout) { + logMessage(LOG_INFO, "Nettoyage session expirée: " + it->first); + it = sessions.erase(it); + } else { + ++it; + } + } +} diff --git a/src/auth.h b/src/auth.h new file mode 100644 index 0000000..36d0985 --- /dev/null +++ b/src/auth.h @@ -0,0 +1,37 @@ +#ifndef AUTH_H +#define AUTH_H + +#include +#include + +struct Session { + String username; + unsigned long lastActivity; + unsigned long timeout; // en millisecondes +}; + +class AuthManager { +private: + String username; + String passwordHash; + std::map sessions; + unsigned long sessionTimeout; // en minutes + +public: + AuthManager(); + void setCredentials(const String& user, const String& passHash); + void setSessionTimeout(unsigned long timeout); + + bool authenticate(const String& user, const String& password); + String createSession(const String& user); + bool validateSession(const String& sessionId); + void updateSessionActivity(const String& sessionId); + void removeSession(const String& sessionId); + void cleanExpiredSessions(); + + String getUsername() { return username; } +}; + +extern AuthManager authManager; + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6a651a3 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include "auth.h" +#include "server.h" +#include "utils.h" + +// Configuration par défaut +String hostname = "ESP32-Webapp"; +String wifiSSID = ""; +String wifiPassword = ""; +String apPassword = "webapp123"; + +void createDefaultConfig() { + logMessage(LOG_INFO, "Création de la configuration par défaut"); + + JsonDocument doc; + + // Configuration authentification + doc["auth"]["username"] = "admin"; + doc["auth"]["password_hash"] = sha256("admin"); // Mot de passe par défaut: admin + + // Configuration WiFi + doc["wifi"]["ssid"] = ""; + doc["wifi"]["password"] = ""; + doc["wifi"]["ap_password"] = apPassword; + + // Configuration système + doc["system"]["hostname"] = hostname; + doc["system"]["session_timeout"] = 60; + + // Paramètres application + doc["params"]["app_name"] = "ESP32 Webapp"; + + File file = LittleFS.open("/config.json", "w"); + if (file) { + serializeJson(doc, file); + file.close(); + logMessage(LOG_INFO, "Configuration par défaut créée"); + } else { + logMessage(LOG_ERROR, "Impossible de créer config.json"); + } +} + +void loadConfig() { + File file = LittleFS.open("/config.json", "r"); + if (!file) { + logMessage(LOG_WARN, "Fichier config.json introuvable, création..."); + createDefaultConfig(); + file = LittleFS.open("/config.json", "r"); + if (!file) { + logMessage(LOG_ERROR, "Échec de la création de config.json"); + return; + } + } + + String content = file.readString(); + file.close(); + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, content); + + if (error) { + logMessage(LOG_ERROR, "Erreur de lecture de config.json"); + return; + } + + // Charger les paramètres + if (doc["auth"]["username"]) { + String username = doc["auth"]["username"].as(); + String passHash = doc["auth"]["password_hash"].as(); + authManager.setCredentials(username, passHash); + } + + if (doc["system"]["hostname"]) { + hostname = doc["system"]["hostname"].as(); + } + + if (doc["system"]["session_timeout"]) { + authManager.setSessionTimeout(doc["system"]["session_timeout"]); + } + + if (doc["wifi"]["ssid"]) { + wifiSSID = doc["wifi"]["ssid"].as(); + } + + if (doc["wifi"]["password"]) { + wifiPassword = doc["wifi"]["password"].as(); + } + + if (doc["wifi"]["ap_password"]) { + apPassword = doc["wifi"]["ap_password"].as(); + } + + logMessage(LOG_INFO, "Configuration chargée"); +} + +void setupWiFi() { + if (wifiSSID.length() > 0) { + logMessage(LOG_INFO, "Connexion au WiFi: " + wifiSSID); + WiFi.mode(WIFI_STA); + WiFi.setHostname(hostname.c_str()); + WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str()); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 20) { + delay(500); + Serial.print("."); + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + logMessage(LOG_INFO, "WiFi connecté"); + logMessage(LOG_INFO, "Adresse IP: " + WiFi.localIP().toString()); + return; + } + + logMessage(LOG_WARN, "Échec de connexion WiFi"); + } + + // Mode Point d'accès + logMessage(LOG_INFO, "Démarrage en mode Point d'Accès"); + WiFi.mode(WIFI_AP); + WiFi.softAP(hostname.c_str(), apPassword.c_str()); + logMessage(LOG_INFO, "AP démarré: " + hostname); + logMessage(LOG_INFO, "Adresse IP: " + WiFi.softAPIP().toString()); +} + +void setupOTA() { + ArduinoOTA.setHostname(hostname.c_str()); + + ArduinoOTA.onStart([]() { + String type = (ArduinoOTA.getCommand() == U_FLASH) ? "firmware" : "filesystem"; + logMessage(LOG_INFO, "Début de la mise à jour OTA: " + type); + }); + + ArduinoOTA.onEnd([]() { + logMessage(LOG_INFO, "Mise à jour OTA terminée"); + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progression: %u%%\r", (progress / (total / 100))); + }); + + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Erreur OTA [%u]: ", error); + if (error == OTA_AUTH_ERROR) logMessage(LOG_ERROR, "Échec d'authentification"); + else if (error == OTA_BEGIN_ERROR) logMessage(LOG_ERROR, "Échec de démarrage"); + else if (error == OTA_CONNECT_ERROR) logMessage(LOG_ERROR, "Échec de connexion"); + else if (error == OTA_RECEIVE_ERROR) logMessage(LOG_ERROR, "Échec de réception"); + else if (error == OTA_END_ERROR) logMessage(LOG_ERROR, "Échec de fin"); + }); + + ArduinoOTA.begin(); + logMessage(LOG_INFO, "OTA prêt"); +} + +void setup() { + Serial.begin(115200); + delay(100); + + logMessage(LOG_INFO, "========================================"); + logMessage(LOG_INFO, "ESP32 Webapp - Démarrage"); + logMessage(LOG_INFO, "========================================"); + + // Initialiser LittleFS + if (!LittleFS.begin(true)) { + logMessage(LOG_ERROR, "Échec du montage de LittleFS"); + return; + } + logMessage(LOG_INFO, "LittleFS monté"); + + // Charger la configuration + loadConfig(); + + // Initialiser le WiFi + setupWiFi(); + + // Initialiser OTA + setupOTA(); + + // Démarrer le serveur web + webServer.begin(); + + logMessage(LOG_INFO, "========================================"); + logMessage(LOG_INFO, "Système prêt!"); + logMessage(LOG_INFO, "========================================"); +} + +unsigned long lastCleanup = 0; + +void loop() { + ArduinoOTA.handle(); + + // Nettoyer les sessions expirées toutes les minutes + unsigned long now = millis(); + if (now - lastCleanup > 60000) { + authManager.cleanExpiredSessions(); + lastCleanup = now; + } + + delay(10); +} diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..860d2ec --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,394 @@ +#include "server.h" +#include "utils.h" +#include + +WebServerManager webServer; + +WebServerManager::WebServerManager() { + server = new AsyncWebServer(80); +} + +String WebServerManager::getSessionIdFromRequest(AsyncWebServerRequest* request) { + if (request->hasHeader("Cookie")) { + String cookie = request->header("Cookie"); + int sessionStart = cookie.indexOf("session="); + if (sessionStart != -1) { + sessionStart += 8; // longueur de "session=" + int sessionEnd = cookie.indexOf(";", sessionStart); + if (sessionEnd == -1) sessionEnd = cookie.length(); + return cookie.substring(sessionStart, sessionEnd); + } + } + return ""; +} + +bool WebServerManager::isAuthenticated(AsyncWebServerRequest* request) { + String sessionId = getSessionIdFromRequest(request); + if (sessionId.length() > 0 && authManager.validateSession(sessionId)) { + authManager.updateSessionActivity(sessionId); + return true; + } + return false; +} + +void WebServerManager::sendJsonError(AsyncWebServerRequest* request, int code, const String& message) { + JsonDocument doc; + doc["success"] = false; + doc["message"] = message; + + String response; + serializeJson(doc, response); + request->send(code, "application/json", response); +} + +void WebServerManager::sendJsonSuccess(AsyncWebServerRequest* request, const String& message) { + JsonDocument doc; + doc["success"] = true; + doc["message"] = message; + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +} + +void WebServerManager::setupRoutes() { + // Route de login + server->on("/login", HTTP_POST, [this](AsyncWebServerRequest* request) { + this->handleLogin(request); + }); + + // Route de logout + server->on("/logout", HTTP_GET, [this](AsyncWebServerRequest* request) { + this->handleLogout(request); + }); + + // API - Configuration + server->on("/api/config", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!isAuthenticated(request)) { + sendJsonError(request, 401, "Non authentifié"); + return; + } + this->handleGetConfig(request); + }); + + server->on("/api/config", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!isAuthenticated(request)) { + sendJsonError(request, 401, "Non authentifié"); + return; + } + this->handleSetConfig(request); + }); + + // API - Changement de mot de passe + server->on("/api/password", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (!isAuthenticated(request)) { + sendJsonError(request, 401, "Non authentifié"); + return; + } + this->handleChangePassword(request); + }); + + // API - Statistiques + server->on("/api/stats", HTTP_GET, [this](AsyncWebServerRequest* request) { + if (!isAuthenticated(request)) { + sendJsonError(request, 401, "Non authentifié"); + return; + } + this->handleGetStats(request); + }); + + // OTA Update + server->on("/api/update", HTTP_POST, + [this](AsyncWebServerRequest* request) { + if (!isAuthenticated(request)) { + sendJsonError(request, 401, "Non authentifié"); + return; + } + AsyncWebServerResponse* response = request->beginResponse(200, "application/json", + Update.hasError() ? "{\"success\":false,\"message\":\"Échec de la mise à jour\"}" : + "{\"success\":true,\"message\":\"Mise à jour réussie, redémarrage...\"}"); + response->addHeader("Connection", "close"); + request->send(response); + if (!Update.hasError()) { + delay(1000); + ESP.restart(); + } + }, + [this](AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { + this->handleOTAUpdate(request, filename, index, data, len, final); + } + ); + + // 404 + server->onNotFound([](AsyncWebServerRequest* request) { + request->send(404, "text/plain", "Page non trouvée"); + }); + + // Servir les fichiers statiques (à la fin pour ne pas interférer avec les routes API) + server->serveStatic("/", LittleFS, "/").setDefaultFile("login.html"); +} + +void WebServerManager::handleLogin(AsyncWebServerRequest* request) { + if (!request->hasParam("username", true) || !request->hasParam("password", true)) { + sendJsonError(request, 400, "Paramètres manquants"); + return; + } + + String username = request->getParam("username", true)->value(); + String password = request->getParam("password", true)->value(); + + if (authManager.authenticate(username, password)) { + String sessionId = authManager.createSession(username); + + AsyncWebServerResponse* response = request->beginResponse(200, "application/json", + "{\"success\":true,\"message\":\"Connexion réussie\"}"); + response->addHeader("Set-Cookie", "session=" + sessionId + "; Path=/; HttpOnly; Max-Age=86400"); + request->send(response); + } else { + sendJsonError(request, 401, "Identifiants incorrects"); + } +} + +void WebServerManager::handleLogout(AsyncWebServerRequest* request) { + String sessionId = getSessionIdFromRequest(request); + if (sessionId.length() > 0) { + authManager.removeSession(sessionId); + } + + AsyncWebServerResponse* response = request->beginResponse(302); // Redirection temporaire + response->addHeader("Location", "/"); + response->addHeader("Set-Cookie", "session=; Path=/; HttpOnly; Max-Age=0"); + request->send(response); +} + +void WebServerManager::handleGetConfig(AsyncWebServerRequest* request) { + File file = LittleFS.open("/config.json", "r"); + if (!file) { + sendJsonError(request, 500, "Impossible de lire la configuration"); + return; + } + + String content = file.readString(); + file.close(); + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, content); + + if (error) { + sendJsonError(request, 500, "Erreur de lecture de la configuration"); + return; + } + + // Masquer le mot de passe + if (doc["wifi"]["password"]) { + doc["wifi"]["password"] = "***"; + } + if (doc["wifi"]["ap_password"]) { + doc["wifi"]["ap_password"] = "***"; + } + if (doc["auth"]["password_hash"]) { + doc["auth"]["password_hash"] = "***"; + } + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +} + +void WebServerManager::handleSetConfig(AsyncWebServerRequest* request) { + if (!request->hasParam("config", true)) { + sendJsonError(request, 400, "Configuration manquante"); + return; + } + + String configStr = request->getParam("config", true)->value(); + JsonDocument newDoc; + DeserializationError error = deserializeJson(newDoc, configStr); + + if (error) { + sendJsonError(request, 400, "Format JSON invalide"); + return; + } + + // Charger la configuration existante + File file = LittleFS.open("/config.json", "r"); + if (!file) { + sendJsonError(request, 500, "Impossible de lire la configuration existante"); + return; + } + + String existingContent = file.readString(); + file.close(); + + JsonDocument existingDoc; + DeserializationError existingError = deserializeJson(existingDoc, existingContent); + + if (existingError) { + sendJsonError(request, 500, "Erreur de lecture de la configuration existante"); + return; + } + + // Fusionner : ne remplacer les mots de passe que s'ils ne sont pas "***" + if (newDoc["wifi"]["password"] && newDoc["wifi"]["password"] != "***") { + existingDoc["wifi"]["password"] = newDoc["wifi"]["password"]; + } + if (newDoc["wifi"]["ap_password"] && newDoc["wifi"]["ap_password"] != "***") { + existingDoc["wifi"]["ap_password"] = newDoc["wifi"]["ap_password"]; + } + if (newDoc["auth"]["password_hash"] && newDoc["auth"]["password_hash"] != "***") { + existingDoc["auth"]["password_hash"] = newDoc["auth"]["password_hash"]; + } + + // Copier les autres champs + if (newDoc["auth"]["username"]) { + existingDoc["auth"]["username"] = newDoc["auth"]["username"]; + } + if (newDoc["wifi"]["ssid"]) { + existingDoc["wifi"]["ssid"] = newDoc["wifi"]["ssid"]; + } + if (newDoc["system"]["hostname"]) { + existingDoc["system"]["hostname"] = newDoc["system"]["hostname"]; + } + if (newDoc["system"]["session_timeout"]) { + existingDoc["system"]["session_timeout"] = newDoc["system"]["session_timeout"]; + } + if (newDoc["params"]["app_name"]) { + existingDoc["params"]["app_name"] = newDoc["params"]["app_name"]; + } + + // Sauvegarder la configuration fusionnée + file = LittleFS.open("/config.json", "w"); + if (!file) { + sendJsonError(request, 500, "Impossible d'écrire la configuration"); + return; + } + + serializeJson(existingDoc, file); + file.close(); + + // Attendre que LittleFS finisse l'écriture + delay(500); + + sendJsonSuccess(request, "Configuration mise à jour avec succès"); + + // Redémarrer après un délai + delay(1000); + ESP.restart(); +} + +void WebServerManager::handleChangePassword(AsyncWebServerRequest* request) { + if (!request->hasParam("config", true)) { + sendJsonError(request, 400, "Configuration manquante"); + return; + } + + String configStr = request->getParam("config", true)->value(); + JsonDocument newDoc; + DeserializationError error = deserializeJson(newDoc, configStr); + + if (error) { + sendJsonError(request, 400, "Format JSON invalide"); + return; + } + + // Vérifier que les champs requis sont présents + if (!newDoc["auth"]["password_hash"] || !newDoc["auth"]["old_password"]) { + sendJsonError(request, 400, "Données manquantes"); + return; + } + + String oldPassword = newDoc["auth"]["old_password"].as(); + String newPasswordHash = newDoc["auth"]["password_hash"].as(); + + // Vérifier que l'ancien mot de passe est correct + if (!authManager.authenticate(authManager.getUsername(), oldPassword)) { + sendJsonError(request, 401, "Mot de passe actuel incorrect"); + return; + } + + // Charger la configuration existante + File file = LittleFS.open("/config.json", "r"); + if (!file) { + sendJsonError(request, 500, "Impossible de lire la configuration existante"); + return; + } + + String existingContent = file.readString(); + file.close(); + + JsonDocument existingDoc; + DeserializationError existingError = deserializeJson(existingDoc, existingContent); + + if (existingError) { + sendJsonError(request, 500, "Erreur de lecture de la configuration existante"); + return; + } + + // Mettre à jour uniquement le hash du mot de passe + existingDoc["auth"]["password_hash"] = newPasswordHash; + + // Sauvegarder la configuration mise à jour + file = LittleFS.open("/config.json", "w"); + if (!file) { + sendJsonError(request, 500, "Impossible d'écrire la configuration"); + return; + } + + serializeJson(existingDoc, file); + file.close(); + + // Attendre que LittleFS finisse l'écriture + delay(500); + + // Mettre à jour les credentials en mémoire + authManager.setCredentials(authManager.getUsername(), newPasswordHash); + + sendJsonSuccess(request, "Mot de passe changé avec succès"); +} + +void WebServerManager::handleGetStats(AsyncWebServerRequest* request) { + JsonDocument doc; + doc["uptime"] = millis() / 1000; + doc["heap_free"] = ESP.getFreeHeap(); + doc["heap_total"] = ESP.getHeapSize(); + doc["wifi_rssi"] = WiFi.RSSI(); + doc["wifi_ssid"] = WiFi.SSID(); + doc["ip"] = WiFi.localIP().toString(); + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); +} + +void WebServerManager::handleOTAUpdate(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { + if (!index) { + logMessage(LOG_INFO, "Début de la mise à jour OTA: " + filename); + + int cmd = (filename.indexOf("spiffs") > -1 || filename.indexOf("littlefs") > -1) ? U_SPIFFS : U_FLASH; + + if (!Update.begin(UPDATE_SIZE_UNKNOWN, cmd)) { + Update.printError(Serial); + } + } + + if (len) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + } + } + + if (final) { + if (Update.end(true)) { + logMessage(LOG_INFO, "Mise à jour OTA terminée avec succès"); + } else { + Update.printError(Serial); + logMessage(LOG_ERROR, "Échec de la mise à jour OTA"); + } + } +} + +void WebServerManager::begin() { + setupRoutes(); + server->begin(); + logMessage(LOG_INFO, "Serveur web démarré sur le port 80"); +} diff --git a/src/server.h b/src/server.h new file mode 100644 index 0000000..f665a92 --- /dev/null +++ b/src/server.h @@ -0,0 +1,37 @@ +#ifndef SERVER_H +#define SERVER_H + +#include +#include +#include +#include +#include +#include "auth.h" + +class WebServerManager { +private: + AsyncWebServer* server; + + String getSessionIdFromRequest(AsyncWebServerRequest* request); + bool isAuthenticated(AsyncWebServerRequest* request); + void sendJsonError(AsyncWebServerRequest* request, int code, const String& message); + void sendJsonSuccess(AsyncWebServerRequest* request, const String& message); + +public: + WebServerManager(); + void begin(); + void setupRoutes(); + + // Handlers + void handleLogin(AsyncWebServerRequest* request); + void handleLogout(AsyncWebServerRequest* request); + void handleGetConfig(AsyncWebServerRequest* request); + void handleSetConfig(AsyncWebServerRequest* request); + void handleChangePassword(AsyncWebServerRequest* request); + void handleGetStats(AsyncWebServerRequest* request); + void handleOTAUpdate(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); +}; + +extern WebServerManager webServer; + +#endif \ No newline at end of file diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..de52584 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,53 @@ +#include "utils.h" + +// Niveau de log par défaut +LogLevel currentLogLevel = LOG_INFO; + +String sha256(const String& data) { + byte hash[32]; + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0); + mbedtls_md_starts(&ctx); + mbedtls_md_update(&ctx, (const unsigned char*)data.c_str(), data.length()); + mbedtls_md_finish(&ctx, hash); + mbedtls_md_free(&ctx); + + String hashString = ""; + for (int i = 0; i < 32; i++) { + char hex[3]; + sprintf(hex, "%02x", hash[i]); + hashString += hex; + } + return hashString; +} + +String generateSessionId() { + String sessionId = ""; + for (int i = 0; i < 32; i++) { + sessionId += String(random(0, 16), HEX); + } + return sessionId; +} + +unsigned long getCurrentTime() { + return millis(); +} + +void logMessage(LogLevel level, const String& message) { + if (level < currentLogLevel) return; + + String levelStr; + switch (level) { + case LOG_DEBUG: levelStr = "[DEBUG]"; break; + case LOG_INFO: levelStr = "[INFO]"; break; + case LOG_WARN: levelStr = "[WARN]"; break; + case LOG_ERROR: levelStr = "[ERROR]"; break; + } + + Serial.print(levelStr); + Serial.print(" "); + Serial.println(message); +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..8a8a614 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,26 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +// Fonction pour calculer le hash SHA-256 +String sha256(const String& data); + +// Fonction pour générer un ID de session aléatoire +String generateSessionId(); + +// Fonction pour obtenir le temps actuel en millisecondes +unsigned long getCurrentTime(); + +// Fonction pour logger des messages +enum LogLevel { + LOG_DEBUG, + LOG_INFO, + LOG_WARN, + LOG_ERROR +}; + +void logMessage(LogLevel level, const String& message); + +#endif