#!/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=×tamp='))""" 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()