From 380b426000aac3d612837ed72c99808347dc94c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 19 Sep 2026 13:25:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(dy):=20=E8=A1=A5=E4=B8=8A=20ArgusSecurityPl?= =?UTF-8?q?ugin=20=E8=A6=81=E6=B1=82=E7=9A=84=20x-tt-argus=20=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抖音在边缘网关新挂了 ArgusSecurityPlugin,对 aweme/detail、aweme/post 这批接口做 业务前置校验:缺少 x-tt-argus 时直接 403,响应体为 "Blocked by ArgusSecurityPlugin Uifid Not Found";只补 uifid 参数但仍没有这个头 则是 "... Signature Not Found"——后者很容易被误判成 a_bogus / verifyFp 的问题。 网关当前不校验该头取值,传固定字符串即可(实测 "1" 就够)。 参考 https://github.com/Johnserf-Seed/f2/issues/443 注意这是权宜之计:网关哪天升级到真校验该值,会重新出现 Signature Not Found, 届时需要改为页面内注入 JS 让抖音自带 SDK 补齐 Argus 头。 验证:真实 cookie 下 get_video_by_id 恢复可用。 - client.py: 默认请求头补 x-tt-argus,cookie 有 UIFID 时带 uifid 头 (两种都没有则不发,避免被当成「有但为空」) - 新增 tests/test_douyin_argus_header.py(不发网络请求) --- media_platform/douyin/client.py | 13 +++++ tests/test_douyin_argus_header.py | 84 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/test_douyin_argus_header.py diff --git a/media_platform/douyin/client.py b/media_platform/douyin/client.py index cfa095e..c361528 100644 --- a/media_platform/douyin/client.py +++ b/media_platform/douyin/client.py @@ -39,6 +39,10 @@ from .exception import * from .field import * from .help import * +# 抖音边缘网关 ArgusSecurityPlugin 要求的请求头。网关目前不校验取值, +# 传固定字符串即可;将来若开始真校验,会重新出现 "Signature Not Found"。 +DOUYIN_ARGUS_HEADER_VALUE = "1" + class DouYinClient(AbstractApiClient, ProxyRefreshMixin): @@ -55,6 +59,15 @@ class DouYinClient(AbstractApiClient, ProxyRefreshMixin): self.proxy = proxy self.timeout = timeout self.headers = headers + # 抖音边缘网关的 ArgusSecurityPlugin 会对这批接口做业务前置校验,缺少 + # x-tt-argus 头时直接 403,响应体为 + # "Blocked by ArgusSecurityPlugin Uifid Not Found"(补了 uifid 但没这个头则是 + # "... Signature Not Found")。当前网关尚未校验该头的值,可传任意字符串; + # 一旦升级到真校验,需要改为 WebView 内注入 JS 让页面自带 SDK 补齐。 + self.headers.setdefault("x-tt-argus", DOUYIN_ARGUS_HEADER_VALUE) + uifid = cookie_dict.get("UIFID") or cookie_dict.get("UIFID_TEMP", "") + if uifid: + self.headers.setdefault("uifid", uifid) self._host = "https://www.douyin.com" self.cookie_urls = [ "https://douyin.com", diff --git a/tests/test_douyin_argus_header.py b/tests/test_douyin_argus_header.py new file mode 100644 index 0000000..f14c5a5 --- /dev/null +++ b/tests/test_douyin_argus_header.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2025 relakkes@gmail.com +# +# This file is part of MediaCrawler project. +# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/tests/test_douyin_argus_header.py +# GitHub: https://github.com/NanmiCoder +# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1 +# + +"""抖音 ArgusSecurityPlugin 请求头的回归测试。 + +回归背景:抖音在边缘网关挂了 ArgusSecurityPlugin,对一批接口做业务前置校验。 +缺少 ``x-tt-argus`` 请求头时直接 403,响应体为 +``Blocked by ArgusSecurityPlugin Uifid Not Found``;补上 uifid 参数但仍没有这个头 +则是 ``... Signature Not Found``(容易误导成 a_bogus / verifyFp 的问题)。 +网关当前不校验该头的取值,传固定字符串即可。 + +这里不发起任何网络请求,只断言客户端默认请求头带上了这两个头。 +""" + +from __future__ import annotations + +import pytest + +from media_platform.douyin.client import DOUYIN_ARGUS_HEADER_VALUE, DouYinClient + +COOKIE_DICT = { + "sessionid": "fake-session", + "UIFID": "uifid-from-cookie", + "UIFID_TEMP": "uifid-temp-from-cookie", +} + + +class _StubPage: + async def evaluate(self, expression): # noqa: ANN001 + return {} + + +def _make_client(cookie_dict: dict) -> DouYinClient: + return DouYinClient( + headers={"User-Agent": "test-user-agent", "Cookie": "a=1"}, + playwright_page=_StubPage(), + cookie_dict=cookie_dict, + ) + + +def test_argus_header_is_present_by_default(): + """x-tt-argus 必须在默认请求头里""" + client = _make_client(COOKIE_DICT) + + assert client.headers.get("x-tt-argus") == DOUYIN_ARGUS_HEADER_VALUE + + +def test_uifid_header_comes_from_cookie(): + """uifid 头取自 cookie 里的 UIFID""" + client = _make_client(COOKIE_DICT) + + assert client.headers.get("uifid") == "uifid-from-cookie" + + +def test_uifid_header_falls_back_to_uifid_temp(): + """没有 UIFID 时退到 UIFID_TEMP""" + client = _make_client({"sessionid": "s", "UIFID_TEMP": "temp-only"}) + + assert client.headers.get("uifid") == "temp-only" + + +def test_missing_uifid_omits_header(): + """cookie 里两种都没有时不发这个头(发空值可能被当成「有但为空」)""" + client = _make_client({"sessionid": "s"}) + + assert client.headers.get("uifid") is None + assert client.headers.get("x-tt-argus") == DOUYIN_ARGUS_HEADER_VALUE + + +def test_caller_supplied_values_win(): + """调用方显式传了同名头时不覆盖""" + client = DouYinClient( + headers={"User-Agent": "ua", "Cookie": "a=1", "x-tt-argus": "custom"}, + playwright_page=_StubPage(), + cookie_dict=COOKIE_DICT, + ) + + assert client.headers.get("x-tt-argus") == "custom"