142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
"""
|
|
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()
|