#!/usr/bin/env python3 """Détecte les centrales d'alarme Somfy Protexiom sur le réseau local. Étapes : 1. ping de tout le sous-réseau, on garde les hôtes avec le TTL voulu (100 par défaut) 2. pour chacun, test de http://IP:80/fr/login.htm puis :592 3. si aucun des deux ne répond, scan des ports 1-1000 puis test HTTP sur les ports ouverts 4. verdict clair pour chaque hôte + résumé final """ import argparse import ipaddress import platform import re import socket import subprocess import sys import urllib.error import urllib.request from concurrent.futures import ThreadPoolExecutor IS_WINDOWS = platform.system().lower() == "windows" PRIORITY_PORTS = (80, 592) # Niveaux de verdict CONFIRMED, PROBABLE, SOMFY_OTHER, UNKNOWN = 3, 2, 1, 0 VERDICT_TEXT = { CONFIRMED: "CENTRALE D'ALARME SOMFY PROTEXIOM TROUVEE", PROBABLE: "PROBABLEMENT UNE CENTRALE SOMFY PROTEXIOM (a verifier)", SOMFY_OTHER: "APPAREIL SOMFY DETECTE (modele a confirmer)", UNKNOWN: "Reponse HTTP non reconnue (pas une centrale Somfy)", } def get_local_ip() -> str: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("8.8.8.8", 80)) return s.getsockname()[0] finally: s.close() def ping(ip: str): if IS_WINDOWS: cmd = ["ping", "-n", "1", "-w", "1000", ip] else: cmd = ["ping", "-c", "1", "-W", "1", ip] try: out = subprocess.run( cmd, capture_output=True, text=True, errors="ignore", timeout=5 ).stdout except subprocess.TimeoutExpired: return ip, None m = re.search(r"ttl[=:](\d+)", out, re.IGNORECASE) return ip, int(m.group(1)) if m else None def classify(status, title, text): """Renvoie (niveau, raisons) à partir de la page récupérée.""" low = text.lower() title_centrale = bool(title) and "centrale" in title.lower() has_somfy = "somfy" in low has_protexiom = "protexiom" in low has_password = bool(re.search(r"type\s*=\s*[\"']?password", low)) ok = status == 200 reasons = [] if ok: reasons.append("page /fr/login.htm accessible (HTTP 200)") if title_centrale: reasons.append(f"titre de la page = '{title}'") if has_protexiom: reasons.append("mot 'Protexiom' present dans la page") if has_somfy: reasons.append("mot 'Somfy' present dans la page") if has_password: reasons.append("formulaire de connexion avec mot de passe") if ok and title_centrale and (has_somfy or has_protexiom): level = CONFIRMED elif ok and (has_protexiom or (title_centrale and has_password)): level = PROBABLE elif has_somfy: level = SOMFY_OTHER else: level = UNKNOWN return level, reasons def check_somfy(ip: str, path: str, port: int, timeout: float = 3.0) -> dict: url = f"http://{ip}:{port}{path}" result = {"ip": ip, "port": port, "url": url, "status": None, "title": None, "server": None, "level": UNKNOWN, "reasons": [], "error": None} req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: result["status"] = resp.status headers = str(resp.headers) result["server"] = resp.headers.get("Server") body = resp.read(200_000).decode("utf-8", errors="ignore") except urllib.error.HTTPError as e: result["status"] = e.code headers = str(e.headers) result["server"] = e.headers.get("Server") try: body = e.read(200_000).decode("utf-8", errors="ignore") except Exception: body = "" except Exception as e: result["error"] = f"{type(e).__name__}: {e}" return result m = re.search(r"]*>(.*?)", body, re.IGNORECASE | re.DOTALL) if m: result["title"] = " ".join(m.group(1).split()) result["level"], result["reasons"] = classify( result["status"], result["title"], headers + "\n" + body ) return result def port_open(ip: str, port: int, timeout: float = 0.5) -> bool: try: with socket.create_connection((ip, port), timeout=timeout): return True except OSError: return False def scan_ports(ip: str, ports, workers: int = 200): ports = list(ports) with ThreadPoolExecutor(max_workers=workers) as pool: flags = list(pool.map(lambda p: port_open(ip, p), ports)) return [p for p, ok in zip(ports, flags) if ok] def find_http(ip: str, path: str, max_port: int): """Renvoie (réponses HTTP, ports ouverts scannés ou None).""" responses = [] for port in PRIORITY_PORTS: r = check_somfy(ip, path, port) if not r["error"]: responses.append(r) if responses: return responses, None print(f" {ip} : rien sur les ports {PRIORITY_PORTS}, scan des ports 1-{max_port}...") others = [p for p in range(1, max_port + 1) if p not in PRIORITY_PORTS] open_ports = scan_ports(ip, others) for port in open_ports: r = check_somfy(ip, path, port) if not r["error"]: responses.append(r) return responses, open_ports def print_result(r: dict): bar = "=" * 64 if r["level"] >= PROBABLE else "-" * 64 print(bar) print(f" {'>>> ' if r['level'] >= PROBABLE else ''}{VERDICT_TEXT[r['level']]}" f"{' <<<' if r['level'] >= PROBABLE else ''}") print(bar) print(f" Adresse IP : {r['ip']}") print(f" Port : {r['port']}") print(f" Lien : {r['url']}") if r["reasons"]: print(" Indices :") for reason in r["reasons"]: print(f" - {reason}") print() def main(): parser = argparse.ArgumentParser(description="Recherche de centrales Somfy Protexiom") parser.add_argument("-n", "--network", help="Réseau CIDR (ex: 192.168.1.0/24)") parser.add_argument("-t", "--ttl", type=int, default=100, help="TTL recherché (défaut : 100)") parser.add_argument("-w", "--workers", type=int, default=100, help="Pings en parallèle") parser.add_argument("-p", "--path", default="/fr/login.htm", help="Page à tester (défaut : /fr/login.htm)") parser.add_argument("--max-port", type=int, default=1000, help="Dernier port scanné en repli (défaut : 1000)") args = parser.parse_args() if args.network: net = ipaddress.ip_network(args.network, strict=False) else: local = get_local_ip() net = ipaddress.ip_network(f"{local}/24", strict=False) print(f"IP locale : {local}") hosts = [str(h) for h in net.hosts()] print(f"Scan de {net} ({len(hosts)} adresses), recherche TTL={args.ttl}...\n") alive, matches = [], [] with ThreadPoolExecutor(max_workers=args.workers) as pool: for ip, ttl in pool.map(ping, hosts): if ttl is not None: alive.append((ip, ttl)) if ttl == args.ttl: matches.append(ip) print(f"[TTL OK] {ip} TTL={ttl}") print("\nHotes ayant repondu :") for ip, ttl in sorted(alive, key=lambda x: ipaddress.ip_address(x[0])): print(f" {ip:<15} TTL={ttl}{' <--' if ttl == args.ttl else ''}") if not matches: print(f"\nAucun hote avec TTL={args.ttl}.") return print(f"\nVerification HTTP de {args.path} sur {len(matches)} hote(s)...\n") all_results, silent = [], [] for ip in matches: responses, open_ports = find_http(ip, args.path, args.max_port) if not responses: silent.append((ip, open_ports)) continue for r in responses: all_results.append(r) print_result(r) for ip, open_ports in silent: print("-" * 64) print(f" {ip} : aucune reponse HTTP (pas identifiable comme centrale Somfy)") if open_ports: print(f" Ports ouverts : {open_ports}") print() # Résumé final found = [r for r in all_results if r["level"] >= PROBABLE] print("#" * 64) print(" RESUME") print("#" * 64) if found: print(f" {len(found)} centrale(s) Somfy Protexiom trouvee(s) :") for r in sorted(found, key=lambda x: (-x["level"], ipaddress.ip_address(x["ip"]))): label = "confirmee" if r["level"] == CONFIRMED else "probable" print(f" * {r['ip']:<15} port {r['port']:<5} ({label}) {r['url']}") else: print(" Aucune centrale Somfy Protexiom identifiee.") others = [r for r in all_results if r["level"] < PROBABLE] if others: print(f" Autres appareils avec TTL={args.ttl} (non identifies comme centrale) :") for r in others: print(f" * {r['ip']:<15} port {r['port']:<5} {VERDICT_TEXT[r['level']]}") print() if __name__ == "__main__": try: main() finally: # Quand le script est compilé en .exe (double-clic), garde la fenêtre ouverte if getattr(sys, "frozen", False): input("\nAppuie sur Entree pour quitter...")