Attaque2 / app.py
Docfile's picture
Update app.py
56cfb68 verified
raw
history blame
13.8 kB
import Flask
from flask import request, render_template_string, jsonify, redirect, url_for
import requests
import threading
import uuid # Pour générer des identifiants uniques pour chaque tâche
import time
import copy # Pour copier le payload pour chaque requête
app = Flask(__name__)
# --- Configuration ---
TARGET_URL = "https://hook.us1.make.com/zal5qn0ggbewmvtsbo2uenfno8tz3n56"
BASE_PAYLOAD = {
"name": "Testeur Auto ",
"email": "[email protected]", # Ajout de '+auto' pour distinguer
"company": "aragon Inc.",
"message": "Ceci est un test automatisé via Flask.",
"date": "2023-10-27T10:30:00Z", # Tu pourrais rendre cette date dynamique si besoin
"source": "http://simulateur-bsbs-flask.com"
}
# Structure pour stocker l'état des tâches (jobs) en mémoire
# Format: { 'job_id': {'status': 'running'/'completed'/'failed', 'total': N, 'completed_count': M, 'error_count': E, 'errors': [...] } }
jobs = {}
jobs_lock = threading.Lock() # Pour éviter les problèmes d'accès concurrents au dict jobs
# --- Templates HTML ---
# Page d'accueil pour démarrer les requêtes
HTML_INDEX = """
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lanceur de Requêtes</title>
<style>
body { font-family: sans-serif; margin: 20px; }
label { display: block; margin-bottom: 5px; }
input[type=number] { width: 100px; padding: 8px; margin-bottom: 15px; }
button { padding: 10px 15px; cursor: pointer; }
.error { color: red; margin-top: 10px; }
</style>
</head>
<body>
<h1>Envoyer des Requêtes POST en Masse</h1>
<form method="POST" action="/start">
<label for="num_requests">Nombre de requêtes à envoyer :</label>
<input type="number" id="num_requests" name="num_requests" min="1" required>
<button type="submit">Lancer les requêtes</button>
</form>
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<h2>Tâches en cours / terminées :</h2>
<ul>
{% for job_id, job_info in jobs_list.items() %}
<li>
<a href="{{ url_for('job_status', job_id=job_id) }}">Tâche {{ job_id }}</a>
({{ job_info.status }}, {{ job_info.completed_count }}/{{ job_info.total }} complétées, {{ job_info.error_count }} erreurs)
</li>
{% else %}
<li>Aucune tâche récente.</li>
{% endfor %}
</ul>
</body>
</html>
"""
# Page pour suivre la progression d'une tâche spécifique
HTML_STATUS = """
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statut Tâche {{ job_id }}</title>
<style>
body { font-family: sans-serif; margin: 20px; }
#progress-bar-container { width: 100%; background-color: #f0f0f0; border-radius: 5px; margin-bottom: 10px; }
#progress-bar { width: 0%; height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; border-radius: 5px; transition: width 0.5s ease-in-out; }
.error-log { margin-top: 15px; max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; background-color: #f9f9f9;}
.error-log p { margin: 5px 0; font-size: 0.9em; }
.status-message { font-weight: bold; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Statut de la Tâche : {{ job_id }}</h1>
<div id="status-message" class="status-message">Chargement...</div>
<div id="progress-bar-container">
<div id="progress-bar">0%</div>
</div>
<p>Requêtes complétées : <span id="completed">0</span> / <span id="total">?</span></p>
<p>Erreurs : <span id="errors">0</span></p>
<div id="error-details" class="error-log" style="display: none;">
<h3>Détails des erreurs :</h3>
<div id="error-list"></div>
</div>
<p><a href="/">Retour à l'accueil</a></p>
<script>
const jobId = "{{ job_id }}";
const statusMessageEl = document.getElementById('status-message');
const progressBarEl = document.getElementById('progress-bar');
const completedEl = document.getElementById('completed');
const totalEl = document.getElementById('total');
const errorsEl = document.getElementById('errors');
const errorDetailsEl = document.getElementById('error-details');
const errorListEl = document.getElementById('error-list');
let intervalId = null;
function updateStatus() {
fetch(`/api/status/${jobId}`)
.then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!data) { // Gère le cas où la tâche n'est pas encore prête
statusMessageEl.textContent = "En attente de démarrage...";
return;
}
completedEl.textContent = data.completed_count;
totalEl.textContent = data.total;
errorsEl.textContent = data.error_count;
statusMessageEl.textContent = `Statut : ${data.status}`;
let percentage = 0;
if (data.total > 0) {
percentage = Math.round((data.completed_count / data.total) * 100);
}
progressBarEl.style.width = percentage + '%';
progressBarEl.textContent = percentage + '%';
// Afficher les erreurs
if (data.error_count > 0 && data.errors && data.errors.length > 0) {
errorListEl.innerHTML = ''; // Clear previous errors
data.errors.forEach(err => {
const p = document.createElement('p');
p.textContent = `Req ${err.index}: ${err.error}`;
errorListEl.appendChild(p);
});
errorDetailsEl.style.display = 'block';
} else {
errorDetailsEl.style.display = 'none';
}
if (data.status === 'completed' || data.status === 'failed') {
if (intervalId) {
clearInterval(intervalId);
intervalId = null; // Arrête les mises à jour
console.log("Mises à jour arrêtées car la tâche est terminée.");
if (data.status === 'completed' && data.error_count == 0) {
progressBarEl.style.backgroundColor = '#4CAF50'; // Vert
} else if (data.status === 'completed' && data.error_count > 0) {
progressBarEl.style.backgroundColor = '#ff9800'; // Orange
} else { // failed
progressBarEl.style.backgroundColor = '#f44336'; // Rouge
}
}
}
})
.catch(error => {
console.error("Erreur lors de la récupération du statut:", error);
statusMessageEl.textContent = "Erreur lors de la récupération du statut.";
if (intervalId) {
clearInterval(intervalId); // Arrête en cas d'erreur persistante
intervalId = null;
}
});
}
// Mettre à jour immédiatement puis toutes les 2 secondes
updateStatus();
intervalId = setInterval(updateStatus, 2000);
</script>
</body>
</html>
"""
# --- Fonctions Logiques ---
def send_single_request(target_url, payload, job_id, request_index):
"""Fonction pour envoyer UNE requête POST."""
# Crée une copie pour éviter de modifier l'original et pour ajouter un identifiant
current_payload = copy.deepcopy(payload)
current_payload['message'] += f" (Requête {request_index + 1})" # Ajoute un numéro à chaque message
current_payload['request_uuid'] = str(uuid.uuid4()) # Ajoute un id unique par requête
try:
response = requests.post(target_url, json=current_payload, timeout=30) # Timeout de 30s
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP (4xx, 5xx)
return True, None # Succès
except requests.exceptions.RequestException as e:
print(f"Erreur requête {request_index + 1} pour job {job_id}: {e}")
return False, f"Req {request_index + 1}: {str(e)}" # Échec avec message d'erreur
def background_task(job_id, num_requests, target_url, base_payload):
"""Fonction exécutée dans un thread séparé pour envoyer les requêtes."""
print(f"Tâche {job_id}: Démarrage de {num_requests} requêtes vers {target_url}")
completed_count = 0
error_count = 0
error_messages = []
# Initialiser le statut (on le fait déjà dans /start mais re-vérifier est ok)
with jobs_lock:
if job_id not in jobs:
jobs[job_id] = {
'status': 'running',
'total': num_requests,
'completed_count': 0,
'error_count': 0,
'errors': []
}
for i in range(num_requests):
success, error_msg = send_single_request(target_url, base_payload, job_id, i)
completed_count += 1
if not success:
error_count += 1
error_messages.append({'index': i + 1, 'error': error_msg})
# Mettre à jour la progression dans le dictionnaire partagé (avec verrou)
with jobs_lock:
jobs[job_id]['completed_count'] = completed_count
jobs[job_id]['error_count'] = error_count
# Gardons seulement les X dernières erreurs pour éviter de saturer la mémoire
jobs[job_id]['errors'] = error_messages[-50:] # Garde les 50 dernières erreurs
# Petite pause optionnelle pour ne pas submerger la cible (ex: 0.1 seconde)
# time.sleep(0.1)
# Marquer la tâche comme terminée
final_status = 'failed' if error_count == num_requests else ('completed' if error_count == 0 else 'completed_with_errors')
with jobs_lock:
jobs[job_id]['status'] = final_status
print(f"Tâche {job_id}: Terminé. {completed_count - error_count} succès, {error_count} erreurs.")
# --- Routes Flask ---
@app.route('/', methods=['GET'])
def index():
"""Affiche la page d'accueil avec le formulaire."""
# On passe une copie triée des jobs récents au template
with jobs_lock:
# Trie par exemple par clé (qui approxime l'ordre de création ici)
# ou ajoute un timestamp à la création du job pour trier par date.
sorted_jobs = dict(sorted(jobs.items(), reverse=True))
return render_template_string(HTML_INDEX, jobs_list=sorted_jobs)
@app.route('/start', methods=['POST'])
def start_requests():
"""Reçoit le nombre de requêtes et lance la tâche en arrière-plan."""
try:
num_requests = int(request.form.get('num_requests'))
if num_requests <= 0:
raise ValueError("Le nombre de requêtes doit être positif.")
except (TypeError, ValueError) as e:
with jobs_lock:
sorted_jobs = dict(sorted(jobs.items(), reverse=True))
return render_template_string(HTML_INDEX, error=f"Nombre invalide: {e}", jobs_list=sorted_jobs), 400
job_id = str(uuid.uuid4())[:8] # ID de tâche court et unique
# Initialiser l'état de la tâche avant de démarrer le thread
with jobs_lock:
jobs[job_id] = {
'status': 'starting', # ou 'queued'
'total': num_requests,
'completed_count': 0,
'error_count': 0,
'errors': []
}
# Créer et démarrer le thread
thread = threading.Thread(target=background_task, args=(job_id, num_requests, TARGET_URL, BASE_PAYLOAD))
thread.daemon = True # Permet au programme principal de quitter même si des threads tournent
thread.start()
print(f"Nouvelle tâche démarrée avec ID: {job_id}")
# Rediriger vers la page de statut de cette tâche
return redirect(url_for('job_status', job_id=job_id))
@app.route('/status/<job_id>', methods=['GET'])
def job_status(job_id):
"""Affiche la page HTML de suivi pour une tâche spécifique."""
with jobs_lock:
if job_id not in jobs:
return "Tâche non trouvée", 404
# La page HTML utilisera l'API /api/status pour les mises à jour dynamiques
return render_template_string(HTML_STATUS, job_id=job_id)
@app.route('/api/status/<job_id>', methods=['GET'])
def api_job_status(job_id):
"""Fournit l'état actuel d'une tâche au format JSON (pour le JavaScript)."""
with jobs_lock:
job_info = jobs.get(job_id)
if job_info:
# Renvoyer une copie pour éviter les modifs concurrentes pendant la sérialisation JSON
return jsonify(copy.deepcopy(job_info))
else:
# Renvoyer une réponse JSON même pour une erreur 404
return jsonify({"error": "Tâche non trouvée"}), 404
# --- Démarrage de l'application ---
if __name__ == '__main__':
# Utilise host='0.0.0.0' pour rendre accessible depuis d'autres machines sur le réseau
# Attention: debug=True ne doit PAS être utilisé en production
app.run(host='0.0.0.0', port=5000, debug=True)