213 lines
7.5 KiB
Python
213 lines
7.5 KiB
Python
import hashlib
|
||
import re
|
||
import time
|
||
import json
|
||
import random
|
||
import base64
|
||
import urllib.parse
|
||
from os import path
|
||
import subprocess
|
||
original_popen = subprocess.Popen
|
||
def patched_popen(*args, **kwargs):
|
||
if kwargs.get('universal_newlines') or kwargs.get('text'):
|
||
if 'encoding' not in kwargs:
|
||
kwargs['encoding'] = 'utf-8'
|
||
return original_popen(*args, **kwargs)
|
||
subprocess.Popen = patched_popen
|
||
|
||
import execjs
|
||
import requests
|
||
|
||
basedir = path.dirname(__file__)
|
||
static_dir = path.join(basedir, 'static')
|
||
node_modules = path.join(static_dir, 'node_modules')
|
||
|
||
# 全局唯一 User-Agent:a_bogus 签名、HTTP 请求头、protobuf body、webid 采集等
|
||
# 必须全部使用同一个 UA,否则抖音服务端重算 a_bogus 时与请求头 UA 不一致 -> 7911。
|
||
# 该值需与浏览器登录上下文(playwright new_context user_agent)保持一致。
|
||
DEFAULT_USER_AGENT = (
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||
)
|
||
|
||
# 动态编译 JS
|
||
dy_path = path.join(static_dir, 'dy_ab.js')
|
||
dy_js = execjs.compile(open(dy_path, 'r', encoding='utf-8').read(), cwd=node_modules)
|
||
|
||
sign_path = path.join(static_dir, 'dy_live_sign.js')
|
||
sign_js = execjs.compile(open(sign_path, 'r', encoding='utf-8').read(), cwd=node_modules)
|
||
|
||
login_path = path.join(static_dir, 'login.js')
|
||
login_js = execjs.compile(open(login_path, 'r', encoding='utf-8').read(), cwd=node_modules)
|
||
|
||
|
||
def generateSecretPhoneNum(phone):
|
||
return login_js.call('generateSecretPhoneNum', phone)
|
||
|
||
|
||
def generateSecretCode(phone, code):
|
||
return login_js.call('generateSecretCode', phone, code)
|
||
|
||
|
||
def trans_cookies(cookies_str):
|
||
cookies = {}
|
||
for i in cookies_str.split("; "):
|
||
try:
|
||
parts = i.split('=')
|
||
key = parts[0].strip()
|
||
val = '='.join(parts[1:]).strip()
|
||
# 防御性清洗:过滤掉因为误粘贴 fetch 等包含非法字符或换行的 Cookie 键
|
||
if not key or any(c in key for c in "()[]{}'\"\n \t\\"):
|
||
continue
|
||
cookies[key] = val
|
||
except:
|
||
continue
|
||
return cookies
|
||
|
||
|
||
def generate_req_sign(e, priK):
|
||
"""私信传 obj,其他的拼接"""
|
||
return dy_js.call('get_req_sign', e, priK)
|
||
|
||
|
||
def generate_a_bogus(query, data="", user_agent=None):
|
||
"""query, data 都是拼接字符串。
|
||
|
||
user_agent 必须与实际发出请求所用的 User-Agent 完全一致(见 DEFAULT_USER_AGENT),
|
||
否则抖音服务端用请求头 UA 重算 a_bogus 会对不上,导致 7911 安全校验失败。
|
||
"""
|
||
return dy_js.call('get_ab', query, data, user_agent or DEFAULT_USER_AGENT)
|
||
|
||
|
||
def generate_signature(room_id, user_unique_id):
|
||
raw_string = f"live_id=1,aid=6383,version_code=180800,webcast_sdk_version=1.0.15,room_id={room_id},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id={user_unique_id},device_platform=web,device_type=,ac=,identity=audience"
|
||
x_ms_stub = hashlib.md5(raw_string.encode("utf-8")).hexdigest()
|
||
result = sign_js.call("get_signature", x_ms_stub)
|
||
return result.get("X-Bogus")
|
||
|
||
|
||
def generate_ree_key(prik):
|
||
"""传递私钥"""
|
||
return dy_js.call('get_ree_key', prik)
|
||
|
||
|
||
def generate_bd_ticket_client_data(api, ticket, ts_sign, priK):
|
||
"""传递 query, ticket, ts_sign, priK"""
|
||
timestamp = int(time.time())
|
||
res_sign = f"ticket={ticket}&path={api}×tamp={timestamp}"
|
||
p = {
|
||
'ts_sign': ts_sign,
|
||
'req_content': 'ticket,path,timestamp',
|
||
'req_sign': generate_req_sign(res_sign, priK),
|
||
'timestamp': timestamp,
|
||
}
|
||
p = json.dumps(p, ensure_ascii=False, separators=(',', ':'))
|
||
return base64.urlsafe_b64encode(p.encode('utf-8')).decode('utf-8')
|
||
|
||
|
||
def generate_msToken(randomlength=107):
|
||
random_str = ''
|
||
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789='
|
||
length = len(base_str) - 1
|
||
for _ in range(randomlength):
|
||
random_str += base_str[random.randint(0, length)]
|
||
return random_str
|
||
|
||
|
||
def generate_fake_webid(random_length=19):
|
||
random_str = ''
|
||
base_str = '0123456789'
|
||
length = len(base_str) - 1
|
||
for _ in range(random_length):
|
||
random_str += base_str[random.randint(0, length)]
|
||
return random_str
|
||
|
||
|
||
def generate_webid(auth=None, url="", user_agent=""):
|
||
# 优先用已采集到的 web_id(避免每次发送都发起一次阻塞的 HTTP 请求,导致事件循环卡顿)
|
||
cached = getattr(auth, "web_id", None) if auth is not None else None
|
||
if cached:
|
||
return str(cached)
|
||
if url == "":
|
||
url = "https://www.douyin.com/discover?modal_id=7376449060384935209"
|
||
# UA 优先级:显式参数 > auth.user_agent(from_im_session / perepare_auth 已带)> 全局默认。
|
||
# 必须与 a_bogus 签名、其余请求头使用同一个 UA,否则服务端重算失配 -> 7911。
|
||
ua = (
|
||
user_agent
|
||
or (getattr(auth, "user_agent", "") if auth is not None else "")
|
||
or DEFAULT_USER_AGENT
|
||
)
|
||
try:
|
||
from .auth import DouyinAuth
|
||
headers = {
|
||
"User-Agent": ua,
|
||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||
"upgrade-insecure-requests": "1"
|
||
}
|
||
if auth and auth.cookie_str:
|
||
headers['cookie'] = auth.cookie_str
|
||
try:
|
||
from rpa_engine.runtime_config import requests_proxies
|
||
proxies = requests_proxies()
|
||
except Exception:
|
||
proxies = None
|
||
response = requests.get(
|
||
url, headers=headers, verify=False, timeout=10, proxies=proxies
|
||
)
|
||
res_text = response.text
|
||
user_unique_id = re.findall(r'\\"user_unique_id\\":\\"(.*?)\\"', res_text)[0]
|
||
# 把发现的 web_id 回写到 auth 上,避免同一次同步里反复发起阻塞的 HTTP 请求
|
||
if auth is not None and user_unique_id:
|
||
try:
|
||
auth.web_id = user_unique_id
|
||
except Exception:
|
||
pass
|
||
return user_unique_id
|
||
except Exception:
|
||
# 失败时同样缓存一个伪 web_id,避免后续调用重复走 10s 超时的网络请求
|
||
fake = generate_fake_webid()
|
||
if auth is not None:
|
||
try:
|
||
if not getattr(auth, "web_id", None):
|
||
auth.web_id = fake
|
||
except Exception:
|
||
pass
|
||
return fake
|
||
|
||
|
||
def generate_millisecond():
|
||
return int(round(time.time() * 1000))
|
||
|
||
|
||
def normalize_client_cert(cert: str) -> str:
|
||
"""统一 client_cert / sdk_cert 格式为「base64 证书体」。
|
||
|
||
web_protect.client_cert 与 frontier WS 的 sdk_cert 通常已是 base64(PEM);
|
||
若误把 PEM 原文或 frontier 证书二次 base64,会导致 7911。
|
||
"""
|
||
cert = (cert or "").strip()
|
||
if not cert:
|
||
return ""
|
||
if cert.startswith("-----BEGIN"):
|
||
return base64.b64encode(cert.encode("utf-8")).decode("utf-8")
|
||
return cert
|
||
|
||
|
||
def resolve_proto_device_id(device_id: str = "", web_id: str = "", my_uid: int = 0) -> str:
|
||
"""protobuf / frontier 更倾向使用数字 device_id(通常等于 my_uid)。"""
|
||
for candidate in (device_id, web_id, str(my_uid or "")):
|
||
c = str(candidate or "").strip()
|
||
if c.isdigit():
|
||
return c
|
||
return str(device_id or web_id or "0")
|
||
|
||
|
||
def splice_url(params):
|
||
splice_url_str = ''
|
||
for key, value in params.items():
|
||
if value is None:
|
||
value = ''
|
||
splice_url_str += key + '=' + urllib.parse.quote(str(value)) + '&'
|
||
return splice_url_str[:-1]
|