#!/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...")