commit 9aacc00631b02f5e34793f0472af19187fa7b430 Author: scayac Date: Sat Jan 31 07:58:00 2026 +0100 Initial commit 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 0000000..abef4f7 Binary files /dev/null and b/data/css/styles.css.gz differ 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 0000000..33b346f Binary files /dev/null and b/data/index.html.gz differ diff --git a/data/js/api.js b/data/js/api.js new file mode 100644 index 0000000..4f480d9 --- /dev/null +++ b/data/js/api.js @@ -0,0 +1,101 @@ +// Client API pour interagir avec le backend + +const api = { + // Vérifier l'authentification + async checkAuth() { + try { + const response = await fetch('/api/stats'); + if (response.status === 401) { + window.location.href = '/login.html'; + return false; + } + return response.ok; + } catch (error) { + console.error('Erreur de vérification d\'authentification:', error); + return false; + } + }, + + // Récupérer la configuration + async getConfig() { + const response = await fetch('/api/config'); + if (response.status === 401) { + window.location.href = '/login.html'; + throw new Error('Non authentifié'); + } + if (!response.ok) { + throw new Error('Erreur de récupération de la configuration'); + } + return await response.json(); + }, + + // Enregistrer la configuration + async setConfig(config) { + const response = await fetch('/api/config', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'config=' + encodeURIComponent(JSON.stringify(config)) + }); + + if (response.status === 401) { + window.location.href = '/login.html'; + throw new Error('Non authentifié'); + } + + if (!response.ok) { + throw new Error('Erreur d\'enregistrement de la configuration'); + } + + return await response.json(); + }, + + // Changer le mot de passe + async changePassword(config) { + const response = await fetch('/api/password', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'config=' + encodeURIComponent(JSON.stringify(config)) + }); + + if (response.status === 401) { + throw new Error('Non authentifié ou mot de passe incorrect'); + } + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Erreur de changement de mot de passe'); + } + + return await response.json(); + }, + + // Récupérer les statistiques + async getStats() { + const response = await fetch('/api/stats'); + if (response.status === 401) { + window.location.href = '/login.html'; + throw new Error('Non authentifié'); + } + if (!response.ok) { + throw new Error('Erreur de récupération des statistiques'); + } + return await response.json(); + }, + + // Déconnexion + async logout() { + const response = await fetch('/logout'); + if (response.ok) { + window.location.href = '/login.html'; + } + } +}; + +// Vérifier l'authentification au chargement de la page (sauf sur login.html) +if (!window.location.pathname.includes('login.html')) { + api.checkAuth(); +} diff --git a/data/js/api.js.gz b/data/js/api.js.gz new file mode 100644 index 0000000..d8dc0d8 Binary files /dev/null and b/data/js/api.js.gz differ 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 0000000..c841169 Binary files /dev/null and b/data/js/app.js.gz differ 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 0000000..0222474 Binary files /dev/null and b/data/js/crypto-js.min.js.gz differ 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 0000000..884711b Binary files /dev/null and b/data/login.html.gz differ 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 0000000..1796723 Binary files /dev/null and b/data/settings.html.gz differ 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 0000000..ec683db Binary files /dev/null and b/data/update.html.gz differ 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