""" LuCI HTTP Firmware Flasher for OpenWrt devices. Tự động hoá 3 bước flash qua web interface LuCI: 1. Login → lấy sysauth cookie + stok token 2. Upload firmware.bin (multipart) 3. Confirm (Proceed) → thiết bị flash và reboot Tương thích: - Barrier Breaker 14.07 (field: username/password) - OpenWrt mới hơn (field: luci_username/luci_password) """ import re import time import os from typing import Optional import requests def _extract_stok(text: str) -> Optional[str]: """Tách stok token từ URL hoặc body HTML. Trả về None nếu không tìm thấy.""" m = re.search(r";stok=([a-f0-9]+)", text) return m.group(1) if m else None def flash_device_api(ip, firmware_path, username="root", password="", keep_settings=False, status_cb=None): """ Flash firmware lên thiết bị OpenWrt qua LuCI HTTP. Args: ip : địa chỉ IP thiết bị firmware_path : đường dẫn file .bin trên máy tính username : LuCI username (mặc định "root") password : LuCI password (mặc định rỗng — Barrier Breaker không có pass) keep_settings : True = giữ cấu hình cũ sau flash status_cb : callback(str) cập nhật tiến độ lên UI Returns: "DONE" — flash thành công "FAIL: …" — thất bại, kèm lý do """ base_url = f"http://{ip}" login_url = f"{base_url}/cgi-bin/luci" session = requests.Session() try: # ── STEP 1: Login ──────────────────────────────────────────── if status_cb: status_cb("Logging in...") # Phát hiện field name tương thích Barrier Breaker vs OpenWrt mới try: page_html = session.get(login_url, timeout=10).text except Exception: page_html = "" if 'name="luci_username"' in page_html: login_data = {"luci_username": username, "luci_password": password} else: login_data = {"username": username, "password": password} resp = session.post(login_url, data=login_data, timeout=10, allow_redirects=True) if resp.status_code == 403: return "FAIL: Login denied (403)" # Lấy stok từ URL → body → redirect history stok = (_extract_stok(resp.url) or _extract_stok(resp.text) or next( (_extract_stok(h.headers.get("Location", "")) for h in resp.history if _extract_stok(h.headers.get("Location", ""))), None )) if not stok and "sysauth" not in str(session.cookies): return "FAIL: Login failed — no session" flash_url = ( f"{base_url}/cgi-bin/luci/;stok={stok}/admin/system/flashops" if stok else f"{base_url}/cgi-bin/luci/admin/system/flashops" ) # ── STEP 2: Upload Firmware ────────────────────────────────── if status_cb: status_cb("Uploading firmware...") filename = os.path.basename(firmware_path) extra = {"keep": "on"} if keep_settings else {} with open(firmware_path, "rb") as f: resp = session.post( flash_url, data=extra, files={"image": (filename, f, "application/octet-stream")}, timeout=300, ) if resp.status_code != 200: return f"FAIL: Upload HTTP {resp.status_code}" body = resp.text.lower() if "invalid image" in body or "bad image" in body: return "FAIL: Invalid firmware image" if "unsupported" in body or "not compatible" in body: return "FAIL: Firmware not compatible" if "verify" not in body or "proceed" not in body: return ("FAIL: Upload ignored by server" if 'name="image"' in resp.text else "FAIL: Unexpected response after upload") # ── STEP 3: Confirm (Proceed) ──────────────────────────────── if status_cb: status_cb("Confirming (Proceed)...") confirm = {"step": "2", "keep": "on" if keep_settings else ""} try: session.post(flash_url, data=confirm, timeout=300) except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): pass # Đứt kết nối khi reboot = bình thường if status_cb: status_cb("Rebooting...") time.sleep(3) return "DONE" except requests.ConnectionError: return "FAIL: Cannot connect" except requests.Timeout: return "DONE (rebooting)" except Exception as e: return f"FAIL: {e}" finally: session.close()