193 lines
6.2 KiB
Python
193 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
||
"""91Video PWA 登录脚本
|
||
|
||
通过逆向 pwa4.jl8v0p5t.cc 的加密协议,调用登录接口获取 token。
|
||
|
||
依赖: pip install requests pycryptodome
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import random
|
||
import string
|
||
import time
|
||
|
||
import requests
|
||
from Crypto.Cipher import AES
|
||
|
||
# ===== 协议常量 (从前端 JS 逆向得到) =====
|
||
API_URL = "https://api4.zqpnd5a7.cc/pwa.php"
|
||
|
||
# AES-256 密钥与 IV (请求加密 / 响应解密共用)
|
||
AES_KEY = b"cc88ddc9357ff461e08f047aedee692b" # 32 bytes
|
||
AES_IV = b"e89225cfbbimgkcu" # 16 bytes
|
||
|
||
# 签名用的密钥 (即 AES 密钥)
|
||
SIGN_KEY = "cc88ddc9357ff461e08f047aedee692b"
|
||
|
||
# oauth_id 随机字符集
|
||
OAUTH_CHARSET = (
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||
)
|
||
|
||
HEADERS = {
|
||
"User-Agent": (
|
||
"Mozilla/5.0 (Linux; Android 10; SM-G973F) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||
"Chrome/120.0.0.0 Mobile Safari/537.36"
|
||
),
|
||
"Origin": "https://pwa4.jl8v0p5t.cc",
|
||
"Referer": "https://pwa4.jl8v0p5t.cc/",
|
||
"Accept": "application/json, text/plain, */*",
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
}
|
||
|
||
|
||
def _aes_encrypt(plaintext: str) -> str:
|
||
"""AES-256-CFB (segment_size=128) 加密, 返回大写十六进制字符串。"""
|
||
cipher = AES.new(AES_KEY, AES.MODE_CFB, AES_IV, segment_size=128)
|
||
return cipher.encrypt(plaintext.encode("utf-8")).hex().upper()
|
||
|
||
|
||
def _aes_decrypt(hex_data: str) -> str:
|
||
"""AES-256-CFB (segment_size=128) 解密。"""
|
||
cipher = AES.new(AES_KEY, AES.MODE_CFB, AES_IV, segment_size=128)
|
||
return cipher.decrypt(bytes.fromhex(hex_data)).decode("utf-8", "replace")
|
||
|
||
|
||
def _make_sign(data_hex: str, timestamp_sec: int) -> str:
|
||
"""生成 sign: MD5(SHA256('client=pwa&data=<data>×tamp=<ts><sign_key>'))"""
|
||
base = f"client=pwa&data={data_hex}×tamp={timestamp_sec}{SIGN_KEY}"
|
||
sha = hashlib.sha256(base.encode("utf-8")).hexdigest()
|
||
return hashlib.md5(sha.encode("utf-8")).hexdigest()
|
||
|
||
|
||
def _gen_oauth_id() -> str:
|
||
"""生成 oauth_id: 16 位随机字符 + 毫秒时间戳。"""
|
||
rand_part = "".join(random.choices(OAUTH_CHARSET, k=16))
|
||
return rand_part + str(int(time.time() * 1000))
|
||
|
||
|
||
def _post(session: requests.Session, payload: dict) -> dict:
|
||
"""加密 payload 并发送请求,返回解密后的 JSON 响应。"""
|
||
ts_ms = int(time.time() * 1000)
|
||
ts_sec = ts_ms // 1000
|
||
payload["timestamp"] = ts_ms
|
||
|
||
plaintext = json.dumps(payload, separators=(",", ":"))
|
||
data_hex = _aes_encrypt(plaintext)
|
||
sign = _make_sign(data_hex, ts_sec)
|
||
|
||
body = f"client=pwa×tamp={ts_sec}&data={data_hex}&sign={sign}"
|
||
|
||
resp = session.post(API_URL, data=body, headers=HEADERS, timeout=30)
|
||
if resp.status_code == 503:
|
||
raise RuntimeError(
|
||
"服务端返回 503。若 oauth_id 未经 system/index 注册会触发此错误;"
|
||
"也可能是 CloudFront 限流,请稍后重试。"
|
||
)
|
||
resp.raise_for_status()
|
||
|
||
result = resp.json()
|
||
if result.get("errcode") != 0:
|
||
raise RuntimeError(f"接口返回错误: {result}")
|
||
|
||
encrypted_data = result.get("data", "")
|
||
if not encrypted_data:
|
||
return result
|
||
|
||
decrypted = _aes_decrypt(encrypted_data)
|
||
return json.loads(decrypted)
|
||
|
||
|
||
def login(account: str, password: str):
|
||
"""登录并返回 (session, oauth_id, 解析后的登录响应)。
|
||
|
||
登录前必须先调用 system/index 注册 oauth_id,
|
||
否则服务端会对未注册的 oauth_id 返回 503。
|
||
"""
|
||
session = requests.Session()
|
||
oauth_id = _gen_oauth_id()
|
||
|
||
base = {
|
||
"oauth_type": "pwa",
|
||
"oauth_id": oauth_id,
|
||
"trace_id": "",
|
||
"aff_x_code": "",
|
||
"version": "7.5.0",
|
||
}
|
||
|
||
# 1) 先调用 system/index 注册 oauth_id
|
||
_post(session, dict(base, mod="system", code="index", data={}, token=""))
|
||
time.sleep(0.3)
|
||
|
||
# 2) 再调用登录接口
|
||
payload = dict(
|
||
base,
|
||
mod="user",
|
||
code="regOrLogin",
|
||
account=account,
|
||
password=password,
|
||
type="login",
|
||
)
|
||
result = _post(session, payload)
|
||
return session, oauth_id, result
|
||
|
||
|
||
def fetch_home(session: requests.Session, oauth_id: str, token: str, page: int) -> dict:
|
||
"""请求首页列表 (mod=index, code=home) 并返回解密后的数据。"""
|
||
payload = {
|
||
"oauth_type": "pwa",
|
||
"oauth_id": oauth_id,
|
||
"trace_id": "",
|
||
"aff_x_code": "",
|
||
"version": "7.5.0",
|
||
"mod": "index",
|
||
"code": "home",
|
||
"page": page,
|
||
"token": token,
|
||
}
|
||
return _post(session, payload)
|
||
|
||
|
||
def main():
|
||
account = "16774351679"
|
||
password = "admin123456"
|
||
|
||
print(f"正在登录账号: {account}")
|
||
session, oauth_id, result = login(account, password)
|
||
|
||
inner = result.get("data", result)
|
||
if not inner.get("success"):
|
||
print("登录失败:", inner.get("msg", result))
|
||
return
|
||
|
||
token = inner.get("token")
|
||
print(f"登录成功! token: {token}")
|
||
|
||
# 请求首页第 1~10 页并解密保存
|
||
for page in range(1, 2):
|
||
try:
|
||
data = fetch_home(session, oauth_id, token, page)
|
||
except Exception as e:
|
||
print(f"第 {page} 页请求失败: {e}")
|
||
continue
|
||
|
||
out_file = f"home_page_{page}.json"
|
||
with open(out_file, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
# 打印摘要:data 直接就是视频列表
|
||
items = data.get("data") if isinstance(data.get("data"), list) else []
|
||
print(f"第 {page} 页: 已解密并保存到 {out_file} (共 {len(items)} 条)")
|
||
time.sleep(0.5)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# data_str = '46CF9369809931B4D43024E4A689EADDF078D83A100348DB8CB1B386522B6F5811D57BAD96801571597E550A83F20B865F34F7CEA7BCA0ED2CE780257CE0D818F2E36A04941BC58B0767C3263377995656FC9C251C58BB8C3C5BF36DBFD0B65348FF1031D9E381377593BB8B32F39C8F0AF2DFF7579BA2B260E6073B9B6A6EB1FC309116F38380C7EA555684C9BF4464D172B1DA3513461AB5E1A1865FF4E7AE146D980129EFFE464773B03212543462D1CBB31A79EDE0B9402A17677CCF1526213B4E787BAF7F6252A0C445412C519DCF663CA477D6CC9F1E7C8586F2D486EADE8A12A16FB215BCBCB9D4128DB568FEC5D572863A61AE0D55B36D00'
|
||
# decrypted = json.loads(_aes_decrypt(data_str))
|
||
|
||
# print("-----")
|
||
# print(json.dumps(decrypted, ensure_ascii=False, indent=2))
|
||
main()
|