上传文件至「/」
This commit is contained in:
@@ -0,0 +1,704 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
import uuid
|
||||||
|
import subprocess
|
||||||
|
import concurrent.futures
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
try:
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
BASE_DIR = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
||||||
|
STATE_FILE = os.path.join(BASE_DIR, ".eportal_state.json")
|
||||||
|
|
||||||
|
# 模块级默认值,main() 会从 config.json 覆盖
|
||||||
|
PORTAL_HOST = "http://192.168.200.100"
|
||||||
|
PORTAL_BASE_URL = PORTAL_HOST + "/eportal"
|
||||||
|
USERNAME = ""
|
||||||
|
PASSWORD = ""
|
||||||
|
MAX_ATTEMPTS = 60
|
||||||
|
SLEEP_SECONDS = 3
|
||||||
|
PING_HOST = "www.dufe.edu.cn"
|
||||||
|
PUBLIC_KEY_FALLBACK = "10001&94dd2a8675fb779e6b9f7103698634cd400f27a154afa67af6166a43fc26417222a79506d34cacc7641946abda1785b7acf9910ad6a0978c91ec84d40b71d2891379af19ffb333e7517e390bd26ac312fe940c340466b4a5d4af1d65c3b5944078f96a1a51a5a53e4bc302818b7c9f63c4a1b07bd7d874cef1c3d4b2f5eb7871"
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"portal_host": "http://192.168.200.100",
|
||||||
|
"max_attempts": 60,
|
||||||
|
"sleep_seconds": 3,
|
||||||
|
"ping_host": "www.dufe.edu.cn",
|
||||||
|
"public_key_fallback": "10001&94dd2a8675fb779e6b9f7103698634cd400f27a154afa67af6166a43fc26417222a79506d34cacc7641946abda1785b7acf9910ad6a0978c91ec84d40b71d2891379af19ffb333e7517e390bd26ac312fe940c340466b4a5d4af1d65c3b5944078f96a1a51a5a53e4bc302818b7c9f63c4a1b07bd7d874cef1c3d4b2f5eb7871",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
def _write_default():
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(DEFAULT_CONFIG, f, ensure_ascii=False, indent=4)
|
||||||
|
_print(f"已创建默认配置文件: {CONFIG_FILE}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not os.path.exists(CONFIG_FILE):
|
||||||
|
_write_default()
|
||||||
|
return dict(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
# 检测空文件或内容为空
|
||||||
|
try:
|
||||||
|
if os.path.getsize(CONFIG_FILE) == 0:
|
||||||
|
_print("config.json 为空,重新生成默认配置", "ERROR")
|
||||||
|
_write_default()
|
||||||
|
return dict(DEFAULT_CONFIG)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cfg = dict(DEFAULT_CONFIG)
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
_print("config.json 格式错误,使用默认配置", "ERROR")
|
||||||
|
return dict(DEFAULT_CONFIG)
|
||||||
|
cfg.update(data)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
_print(f"config.json 解析失败: {e},使用默认配置", "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"读取 config.json 失败: {e}", "ERROR")
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _print(msg, level="INFO"):
|
||||||
|
print(f"[{level}] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
_skip_wait = False # Ctrl+C 退出时跳过等待
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_exit():
|
||||||
|
global _skip_wait
|
||||||
|
if _skip_wait:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
input("按任意键退出...")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(mac, user_index):
|
||||||
|
try:
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump({"mac": mac, "userIndex": user_index}, f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_state():
|
||||||
|
try:
|
||||||
|
if os.path.exists(STATE_FILE):
|
||||||
|
with open(STATE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def detect_portal():
|
||||||
|
try:
|
||||||
|
requests.get(PORTAL_HOST + "/", timeout=(2, 2), allow_redirects=False)
|
||||||
|
return None, None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for url in [
|
||||||
|
"http://www.baidu.com",
|
||||||
|
"http://detectportal.firefox.com/canonical.html",
|
||||||
|
"http://www.msftconnecttest.com/redirect",
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=(2, 2), allow_redirects=False)
|
||||||
|
if resp.status_code in (301, 302, 303, 307, 308):
|
||||||
|
loc = resp.headers.get("Location", "")
|
||||||
|
m = re.match(r'(https?://[^/]+(?:/eportal)?)', loc)
|
||||||
|
if m:
|
||||||
|
host = re.match(r'https?://[^/]+', m.group(1)).group(0)
|
||||||
|
base = m.group(1)
|
||||||
|
return host, base
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
_page_info_cache = None
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_page_info():
|
||||||
|
global _page_info_cache
|
||||||
|
if _page_info_cache:
|
||||||
|
return _page_info_cache
|
||||||
|
|
||||||
|
info = {"public_key": None, "query_string": "", "portal_mac": None}
|
||||||
|
|
||||||
|
def parse_page(resp, extract_qs=True):
|
||||||
|
html = resp.text
|
||||||
|
if "?" in resp.url:
|
||||||
|
info["query_string"] = resp.url.split("?", 1)[1]
|
||||||
|
m = re.search(r'[?&]mac=([^&]+)', resp.url)
|
||||||
|
if m:
|
||||||
|
info["portal_mac"] = urllib.parse.unquote(m.group(1)).replace(':', '').replace('-', '').lower()
|
||||||
|
# 从 HTML 提取 publicKey
|
||||||
|
for pid in ['publicKey', 'publicKeyModulus', 'publicKeyExponent']:
|
||||||
|
m = re.search(rf'(?:id|name)=["\']{pid}["\'][^>]*value=["\']([^"\']+)', html)
|
||||||
|
if m:
|
||||||
|
if pid == 'publicKey' and '&' in m.group(1):
|
||||||
|
info["public_key"] = m.group(1)
|
||||||
|
break
|
||||||
|
if not info["public_key"]:
|
||||||
|
exp_match = re.search(r'(?:id|name)=["\']publicKeyExponent["\'][^>]*value=["\']([^"\']+)', html)
|
||||||
|
mod_match = re.search(r'(?:id|name)=["\']publicKeyModulus["\'][^>]*value=["\']([^"\']+)', html)
|
||||||
|
if exp_match and mod_match:
|
||||||
|
info["public_key"] = exp_match.group(1) + "&" + mod_match.group(1)
|
||||||
|
if not extract_qs:
|
||||||
|
return
|
||||||
|
# 从 HTML 提取 queryString(JS 重定向: location.href='...?wlanuserip=...')
|
||||||
|
if not info["query_string"]:
|
||||||
|
m = re.search(r"location\.href\s*=\s*['\"]([^'\"]*\?[^'\"]+)", html)
|
||||||
|
if m:
|
||||||
|
qs_url = m.group(1)
|
||||||
|
if "?" in qs_url:
|
||||||
|
info["query_string"] = qs_url.split("?", 1)[1]
|
||||||
|
if not info["query_string"]:
|
||||||
|
params = []
|
||||||
|
for key in ['wlanuserip', 'wlanacname', 'wlanacip', 'wlanusermac', 'wlanuserfirsturl',
|
||||||
|
'ssid', 'nasip', 'snmpagentip', 'mac', 't', 'url']:
|
||||||
|
m = re.search(rf'(?:id|name)=["\']{key}["\'][^>]*value=["\']([^"\']+)', html)
|
||||||
|
if m:
|
||||||
|
params.append(f"{key}={m.group(1)}")
|
||||||
|
if params:
|
||||||
|
info["query_string"] = "&".join(params)
|
||||||
|
|
||||||
|
# 方法1: 跟随 portal 根路径重定向链,提取 JS 重定向中的 queryString
|
||||||
|
try:
|
||||||
|
resp = requests.get(PORTAL_HOST + "/", timeout=10, allow_redirects=True)
|
||||||
|
parse_page(resp)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法2: 访问 eportal 页面获取 publicKey(同时尝试提取 queryString)
|
||||||
|
try:
|
||||||
|
resp = requests.get(PORTAL_BASE_URL + "/", timeout=10, allow_redirects=True)
|
||||||
|
parse_page(resp, extract_qs=not bool(info["query_string"]))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法3: 对 HTTP 外部 URL 发起请求触发 DNS 劫持,从返回的 HTML 提取 queryString
|
||||||
|
if not info["query_string"]:
|
||||||
|
for test_url in ["http://www.baidu.com", "http://detectportal.firefox.com/canonical.html"]:
|
||||||
|
try:
|
||||||
|
resp = requests.get(test_url, timeout=5, allow_redirects=False)
|
||||||
|
if resp.status_code in (301, 302, 303, 307, 308):
|
||||||
|
loc = resp.headers.get("Location", "")
|
||||||
|
if "?" in loc:
|
||||||
|
info["query_string"] = loc.split("?", 1)[1]
|
||||||
|
m = re.search(r'[?&]mac=([^&]+)', loc)
|
||||||
|
if m:
|
||||||
|
info["portal_mac"] = urllib.parse.unquote(m.group(1)).replace(':', '').replace('-', '').lower()
|
||||||
|
break
|
||||||
|
elif resp.status_code == 200:
|
||||||
|
parse_page(resp)
|
||||||
|
if info["query_string"]:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_page_info_cache = info
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_key():
|
||||||
|
info = _fetch_page_info()
|
||||||
|
if info["public_key"]:
|
||||||
|
return info["public_key"]
|
||||||
|
return PUBLIC_KEY_FALLBACK
|
||||||
|
|
||||||
|
|
||||||
|
def get_query_string():
|
||||||
|
return _fetch_page_info()["query_string"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_portal_mac():
|
||||||
|
return _fetch_page_info()["portal_mac"]
|
||||||
|
|
||||||
|
|
||||||
|
def js_rsa_encrypt(data_str, modulus_hex, exponent_hex):
|
||||||
|
# 模拟 JS RSAUtils.encryptedString:原始RSA,零填充,每2个charCode打包为1个16bit digit
|
||||||
|
modulus = int(modulus_hex, 16)
|
||||||
|
exponent = int(exponent_hex, 16)
|
||||||
|
|
||||||
|
modulus_bytes = (modulus.bit_length() + 7) // 8
|
||||||
|
js_digits = (modulus_bytes + 1) // 2
|
||||||
|
chunk_size = 2 * (js_digits - 1)
|
||||||
|
|
||||||
|
a = [ord(ch) for ch in data_str]
|
||||||
|
while len(a) % chunk_size != 0:
|
||||||
|
a.append(0)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for i in range(0, len(a), chunk_size):
|
||||||
|
val = 0
|
||||||
|
j = 0
|
||||||
|
for k in range(i, i + chunk_size, 2):
|
||||||
|
val += (a[k] + (a[k + 1] << 8)) << (16 * j)
|
||||||
|
j += 1
|
||||||
|
parts.append(hex(pow(val, exponent, modulus))[2:])
|
||||||
|
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def double_encode(s):
|
||||||
|
return urllib.parse.quote(urllib.parse.quote(s, safe=''), safe='')
|
||||||
|
|
||||||
|
|
||||||
|
def _ocr_single(img_bytes):
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
import pytesseract
|
||||||
|
img = Image.open(BytesIO(img_bytes))
|
||||||
|
gray = img.convert("L")
|
||||||
|
binary = gray.point(lambda x: 0 if x < 200 else 255)
|
||||||
|
return pytesseract.image_to_string(
|
||||||
|
binary,
|
||||||
|
config="--psm 7 -c tessedit_char_whitelist=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
).strip()
|
||||||
|
except (ImportError, Exception):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_captcha():
|
||||||
|
import random
|
||||||
|
url = f"{PORTAL_BASE_URL}/validcode?rnd={random.random()}"
|
||||||
|
resp = requests.get(url, timeout=10)
|
||||||
|
return resp.content, url
|
||||||
|
|
||||||
|
|
||||||
|
def recognize_captcha(first_url):
|
||||||
|
try:
|
||||||
|
if not first_url.startswith("http"):
|
||||||
|
first_url = urllib.parse.urljoin(PORTAL_BASE_URL, first_url)
|
||||||
|
r = requests.get(first_url, timeout=10)
|
||||||
|
text = _ocr_single(r.content)
|
||||||
|
if text:
|
||||||
|
_print(f"验证码 OCR 识别: {text}")
|
||||||
|
return text
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"验证码下载失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
for i in range(2, 4):
|
||||||
|
_print(f"OCR未识别,刷新验证码重试({i}/3)...")
|
||||||
|
try:
|
||||||
|
img_bytes, _ = _fetch_captcha()
|
||||||
|
text = _ocr_single(img_bytes)
|
||||||
|
if text:
|
||||||
|
_print(f"OCR第{i}次: {text}")
|
||||||
|
return text
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"验证码刷新失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
_print("OCR 3次均失败,切换为手动输入...", "ERROR")
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
img_bytes, _ = _fetch_captcha()
|
||||||
|
Image.open(BytesIO(img_bytes)).show()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return input(" 请手动输入验证码: ").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_macs():
|
||||||
|
macs = []
|
||||||
|
mac_raw = set()
|
||||||
|
|
||||||
|
def add_mac(m):
|
||||||
|
if not m:
|
||||||
|
return
|
||||||
|
m = m.replace(':', '').replace('-', '').replace('.', '').lower().strip()
|
||||||
|
if len(m) == 12 and all(c in '0123456789abcdef' for c in m):
|
||||||
|
if m not in mac_raw and m != '000000000000':
|
||||||
|
mac_raw.add(m)
|
||||||
|
macs.append(m)
|
||||||
|
|
||||||
|
# ipconfig /all(Windows 最可靠)
|
||||||
|
try:
|
||||||
|
output = subprocess.run(
|
||||||
|
['ipconfig', '/all'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
).stdout
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if '物理地址' in line or 'Physical' in line:
|
||||||
|
parts = line.split(':')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
add_mac(parts[-1])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# getmac 库
|
||||||
|
try:
|
||||||
|
from getmac import get_mac_address
|
||||||
|
m = get_mac_address()
|
||||||
|
if m:
|
||||||
|
add_mac(m)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# uuid(不可靠,放最后)
|
||||||
|
try:
|
||||||
|
m = format(uuid.getnode(), '012x')
|
||||||
|
if m != '000000000000':
|
||||||
|
add_mac(m)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# PowerShell 备用
|
||||||
|
if not macs:
|
||||||
|
try:
|
||||||
|
output = subprocess.run(
|
||||||
|
['powershell', '-Command',
|
||||||
|
'Get-NetAdapter | Select-Object -ExpandProperty MacAddress'],
|
||||||
|
capture_output=True, text=True, timeout=5
|
||||||
|
).stdout
|
||||||
|
for line in output.strip().split('\n'):
|
||||||
|
add_mac(line)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return macs
|
||||||
|
|
||||||
|
|
||||||
|
def is_authenticated():
|
||||||
|
test_urls = [
|
||||||
|
("https://detectportal.firefox.com/canonical.html", "success"),
|
||||||
|
("https://www.msftconnecttest.com/connecttest.txt", "Microsoft"),
|
||||||
|
("https://www.baidu.com", None),
|
||||||
|
]
|
||||||
|
passed = 0
|
||||||
|
for url, keyword in test_urls:
|
||||||
|
try:
|
||||||
|
r = requests.get(url, allow_redirects=False, timeout=3)
|
||||||
|
if r.status_code == 200:
|
||||||
|
if keyword:
|
||||||
|
if keyword in r.text:
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
passed += 1
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return passed >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def do_login(mac, query_string=""):
|
||||||
|
exponent_hex, modulus_hex = get_public_key().split('&')
|
||||||
|
plain = f"{PASSWORD}>{mac}"
|
||||||
|
encrypted = js_rsa_encrypt(plain[::-1], modulus_hex, exponent_hex)
|
||||||
|
|
||||||
|
def build_body(validcode=""):
|
||||||
|
return "&".join([
|
||||||
|
f"userId={double_encode(USERNAME)}",
|
||||||
|
f"password={double_encode(encrypted)}",
|
||||||
|
f"service={double_encode('')}",
|
||||||
|
f"queryString={double_encode(query_string)}",
|
||||||
|
"operatorPwd=",
|
||||||
|
"operatorUserId=",
|
||||||
|
f"validcode={double_encode(validcode)}",
|
||||||
|
f"passwordEncrypt={double_encode('true')}",
|
||||||
|
])
|
||||||
|
|
||||||
|
def parse_result(result):
|
||||||
|
ok = result.get("result") == "success"
|
||||||
|
ui = result.get("userIndex", "")
|
||||||
|
ki = result.get("keepaliveInterval", 0)
|
||||||
|
return ok, result.get("message", ""), ui, ki
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{PORTAL_BASE_URL}/InterFace.do?method=login",
|
||||||
|
data=build_body(),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.encoding = 'utf-8'
|
||||||
|
result = resp.json()
|
||||||
|
|
||||||
|
if result.get("result") == "fail" and result.get("validCodeUrl"):
|
||||||
|
captcha_url = result["validCodeUrl"]
|
||||||
|
if captcha_url and captcha_url.strip():
|
||||||
|
_print(f"需要验证码 ({captcha_url})")
|
||||||
|
code = recognize_captcha(captcha_url)
|
||||||
|
if code:
|
||||||
|
_print(f"输入验证码: {code},重试...")
|
||||||
|
resp2 = requests.post(
|
||||||
|
f"{PORTAL_BASE_URL}/InterFace.do?method=login",
|
||||||
|
data=build_body(code),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp2.encoding = 'utf-8'
|
||||||
|
result = resp2.json()
|
||||||
|
|
||||||
|
return parse_result(result)
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e), "", 0
|
||||||
|
|
||||||
|
|
||||||
|
def ping_test(host=None, count=3):
|
||||||
|
if host is None:
|
||||||
|
host = PING_HOST
|
||||||
|
try:
|
||||||
|
cmd = ["ping", "-n", str(count), host] if sys.platform == "win32" else ["ping", "-c", str(count), host]
|
||||||
|
output = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
if output.returncode == 0:
|
||||||
|
# 提取平均延迟
|
||||||
|
m = re.search(r'平均\s*=\s*(\d+)\s*ms|avg.*?(\d+\.?\d*).*?ms', output.stdout)
|
||||||
|
if m:
|
||||||
|
avg = m.group(1) or m.group(2)
|
||||||
|
_print(f"Ping {host} 平均延迟: {avg}ms")
|
||||||
|
else:
|
||||||
|
_print(f"Ping {host} 成功(延迟未解析)")
|
||||||
|
else:
|
||||||
|
_print(f"Ping {host} 失败: {output.stderr.strip()}", "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"Ping {host} 异常: {e}", "ERROR")
|
||||||
|
|
||||||
|
|
||||||
|
def register_mac(mac, user_index):
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{PORTAL_BASE_URL}/InterFace.do?method=registerMac",
|
||||||
|
data=f"mac={mac}&userIndex={user_index}",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.encoding = 'utf-8'
|
||||||
|
result = resp.json()
|
||||||
|
if result.get("result") == "success":
|
||||||
|
_print(f"MAC快速认证已注册: {mac}")
|
||||||
|
else:
|
||||||
|
_print(f"MAC注册: {result.get('message', result)}", "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"MAC注册异常: {e}", "ERROR")
|
||||||
|
|
||||||
|
|
||||||
|
def start_keepalive(user_index, interval_seconds):
|
||||||
|
interval = max(60, min(600, int(interval_seconds) if interval_seconds else 180))
|
||||||
|
|
||||||
|
def _loop():
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
time.sleep(interval)
|
||||||
|
count += 1
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{PORTAL_BASE_URL}/InterFace.do?method=keepalive",
|
||||||
|
data=f"userIndex={user_index}",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.encoding = 'utf-8'
|
||||||
|
result = resp.json()
|
||||||
|
_print(f"保活 #{count} → {result.get('result', '?')}")
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"保活 #{count} 异常: {e}", "ERROR")
|
||||||
|
|
||||||
|
t = threading.Thread(target=_loop, daemon=True)
|
||||||
|
t.start()
|
||||||
|
_print(f"保活线程已启动(间隔 {interval}s)")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global PORTAL_HOST, PORTAL_BASE_URL, _page_info_cache
|
||||||
|
global USERNAME, PASSWORD, MAX_ATTEMPTS, SLEEP_SECONDS
|
||||||
|
global PING_HOST, PUBLIC_KEY_FALLBACK
|
||||||
|
|
||||||
|
cfg = load_config()
|
||||||
|
if not cfg["username"] or not cfg["password"]:
|
||||||
|
_print("请编辑 config.json 填入账号密码后重新运行", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
PORTAL_HOST = cfg["portal_host"]
|
||||||
|
PORTAL_BASE_URL = PORTAL_HOST + "/eportal"
|
||||||
|
USERNAME = cfg["username"]
|
||||||
|
PASSWORD = cfg["password"]
|
||||||
|
MAX_ATTEMPTS = int(cfg["max_attempts"])
|
||||||
|
SLEEP_SECONDS = int(cfg["sleep_seconds"])
|
||||||
|
PING_HOST = cfg["ping_host"]
|
||||||
|
PUBLIC_KEY_FALLBACK = cfg["public_key_fallback"]
|
||||||
|
|
||||||
|
_print(f"校园网认证登录")
|
||||||
|
_print(f"目标: {PORTAL_HOST} 用户: {USERNAME}")
|
||||||
|
|
||||||
|
print("[INFO] 自动检测 Portal 地址...", end=" ", flush=True)
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
future = executor.submit(detect_portal)
|
||||||
|
try:
|
||||||
|
host, base = future.result(timeout=5)
|
||||||
|
except concurrent.futures.TimeoutError:
|
||||||
|
host, base = None, None
|
||||||
|
if host and base:
|
||||||
|
PORTAL_HOST = host
|
||||||
|
PORTAL_BASE_URL = base
|
||||||
|
_page_info_cache = None
|
||||||
|
print(f"→ {base}")
|
||||||
|
else:
|
||||||
|
print("→ 使用默认")
|
||||||
|
|
||||||
|
if is_authenticated():
|
||||||
|
_print("已认证,无需登录!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
saved = load_state()
|
||||||
|
if saved and saved.get("userIndex"):
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{PORTAL_BASE_URL}/InterFace.do?method=getOnlineUserInfo",
|
||||||
|
data=f"userIndex={saved['userIndex']}",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.encoding = 'utf-8'
|
||||||
|
info = resp.json()
|
||||||
|
if info.get("result") == "success":
|
||||||
|
_print(f"恢复上次会话 userIndex={saved['userIndex']}")
|
||||||
|
ping_test()
|
||||||
|
start_keepalive(saved["userIndex"], info.get("keepaliveInterval", 180))
|
||||||
|
_print("按 Ctrl+C 退出...")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(60)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
_print("退出保活")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_print("上次会话已失效,重新登录...")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
all_macs = []
|
||||||
|
if saved and saved.get("mac"):
|
||||||
|
all_macs.append(saved["mac"])
|
||||||
|
_print(f"[上次MAC] {saved['mac']}")
|
||||||
|
|
||||||
|
portal_mac = get_portal_mac()
|
||||||
|
if portal_mac:
|
||||||
|
all_macs.append(portal_mac)
|
||||||
|
_print(f"[Portal MAC] {portal_mac}")
|
||||||
|
|
||||||
|
local_macs = get_all_macs()
|
||||||
|
for m in local_macs:
|
||||||
|
if m not in all_macs:
|
||||||
|
all_macs.append(m)
|
||||||
|
|
||||||
|
_print(f"[扫描] 发现 {len(local_macs)} 个本地网卡MAC:")
|
||||||
|
for i, m in enumerate(local_macs):
|
||||||
|
print(f" {i+1}. {m}")
|
||||||
|
|
||||||
|
qs = get_query_string()
|
||||||
|
if qs:
|
||||||
|
_print(f"[queryString] {qs[:80]}...")
|
||||||
|
|
||||||
|
fallbacks = ["111111111111", "000000000000", "ffffffffffff",
|
||||||
|
"111111111", "123456789abc", "abcdef123456",
|
||||||
|
"000000000001", "deadbeef0000"]
|
||||||
|
for m in fallbacks:
|
||||||
|
if m not in all_macs:
|
||||||
|
all_macs.append(m)
|
||||||
|
|
||||||
|
_print(f"共 {len(all_macs)} 种MAC方案,最多尝试 {MAX_ATTEMPTS} 次\n")
|
||||||
|
|
||||||
|
last_msg = ""
|
||||||
|
mac_index = 0
|
||||||
|
delay = SLEEP_SECONDS
|
||||||
|
consecutive_fails = 0
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_ATTEMPTS + 1):
|
||||||
|
mac = all_macs[mac_index % len(all_macs)]
|
||||||
|
mac_index += 1
|
||||||
|
|
||||||
|
if attempt % (len(all_macs) + 1) == 0:
|
||||||
|
fresh = get_all_macs()
|
||||||
|
for m in fresh:
|
||||||
|
if m not in all_macs:
|
||||||
|
all_macs.append(m)
|
||||||
|
_print(f"[新增MAC] {m}")
|
||||||
|
|
||||||
|
print(f"[{attempt:02d}/{MAX_ATTEMPTS}] MAC={mac:14s}", end=" ")
|
||||||
|
|
||||||
|
ok, msg, user_idx, keep_interval = do_login(mac, query_string=qs)
|
||||||
|
print(f"→ {msg}")
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
_print(f"登录成功!MAC={mac}")
|
||||||
|
ping_test()
|
||||||
|
save_state(mac, user_idx)
|
||||||
|
if user_idx:
|
||||||
|
_print(f"userIndex: {user_idx}")
|
||||||
|
register_mac(mac, user_idx)
|
||||||
|
start_keepalive(user_idx, keep_interval)
|
||||||
|
_print("按 Ctrl+C 退出保活...")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(60)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
global _skip_wait
|
||||||
|
_skip_wait = True
|
||||||
|
_print("退出保活")
|
||||||
|
return True
|
||||||
|
|
||||||
|
consecutive_fails += 1
|
||||||
|
if msg != last_msg:
|
||||||
|
delay = SLEEP_SECONDS
|
||||||
|
consecutive_fails = 0
|
||||||
|
else:
|
||||||
|
delay = min(SLEEP_SECONDS * (1.5 ** (consecutive_fails // 3)), 60)
|
||||||
|
|
||||||
|
last_msg = msg
|
||||||
|
|
||||||
|
if attempt % 5 == 0 and is_authenticated():
|
||||||
|
_print("检测到已认证重定向!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
if is_authenticated():
|
||||||
|
_print("最终检查:已认证")
|
||||||
|
return True
|
||||||
|
|
||||||
|
_print(f"{MAX_ATTEMPTS} 次尝试失败。最后: {last_msg}", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
_print(f"异常退出: {e}", "ERROR")
|
||||||
|
_wait_exit()
|
||||||
Reference in New Issue
Block a user