上传文件至「/」
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