From a161ae484501c90fa8e5e379ce59e327a1316cce Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Tue, 18 Aug 2026 14:40:25 +0300 Subject: [PATCH] =?UTF-8?q?feat(subscription):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=B8=D1=85=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=20HWID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../methods/custom/getDashboardSections.ts | 7 +++ fe-app-tachyon/src/tachyon/types.ts | 7 +++ .../resources/view/tachyon/main.js | 7 +++ tachyon/files/etc/config/tachyon | 7 +++ tests/luci_subscription_headers.sh | 50 +++++++++++++++++++ tests/subscription_cache_state.sh | 43 ++++++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 tests/luci_subscription_headers.sh diff --git a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts index 636408b7..a22cd812 100644 --- a/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts +++ b/fe-app-tachyon/src/tachyon/methods/custom/getDashboardSections.ts @@ -258,6 +258,13 @@ function hydrateConfigSections(configSections: Tachyon.ConfigSection[]) { user_agent: item.user_agent, auto_hwid: item.auto_hwid, hwid: item.hwid, + custom_device_headers: item.custom_device_headers, + device_os: item.device_os, + ver_os: item.ver_os, + device_model: item.device_model, + device_locale: item.device_locale, + app_version: item.app_version, + accept_language: item.accept_language, show_dashboard_metadata: item.show_dashboard_metadata, prefix_nodes: item.prefix_nodes, node_prefix: item.node_prefix, diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index 8832eb8e..bc9ab1b5 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -381,6 +381,13 @@ export namespace Tachyon { user_agent?: string; auto_hwid?: '0' | '1'; hwid?: string; + custom_device_headers?: '0' | '1'; + device_os?: string; + ver_os?: string; + device_model?: string; + device_locale?: string; + app_version?: string; + accept_language?: string; show_dashboard_metadata?: '0' | '1'; prefix_nodes?: '0' | '1'; node_prefix?: string; diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index f7b791ef..232b92c0 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -3968,6 +3968,13 @@ function hydrateConfigSections(configSections) { user_agent: item.user_agent, auto_hwid: item.auto_hwid, hwid: item.hwid, + custom_device_headers: item.custom_device_headers, + device_os: item.device_os, + ver_os: item.ver_os, + device_model: item.device_model, + device_locale: item.device_locale, + app_version: item.app_version, + accept_language: item.accept_language, show_dashboard_metadata: item.show_dashboard_metadata, prefix_nodes: item.prefix_nodes, node_prefix: item.node_prefix, diff --git a/tachyon/files/etc/config/tachyon b/tachyon/files/etc/config/tachyon index 78155a79..fe3e9764 100644 --- a/tachyon/files/etc/config/tachyon +++ b/tachyon/files/etc/config/tachyon @@ -102,6 +102,13 @@ config settings 'settings' # option auto_user_agent '0' # option user_agent 'CustomAgent/1.0' # option auto_hwid '1' +# option custom_device_headers '0' +# #option device_os 'iOS' +# #option ver_os '26.4' +# #option device_model 'iPhone 17 Pro Max' +# #option device_locale 'ru' +# #option app_version '4.6.0' +# #option accept_language 'ru-RU,ru;q=0.9' # option subscription_update_enabled '1' # option subscription_update_interval '1h' # option download_via_proxy_enabled '0' diff --git a/tests/luci_subscription_headers.sh b/tests/luci_subscription_headers.sh new file mode 100644 index 00000000..8ef669e8 --- /dev/null +++ b/tests/luci_subscription_headers.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SECTION_JS="$ROOT_DIR/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js" +CACHE_UC="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc" + +for key in custom_device_headers device_os ver_os device_model device_locale app_version accept_language; do + grep -Fq "\"$key\"" "$SECTION_JS" || { + printf 'FAIL: missing subscription header field %s\n' "$key" >&2 + exit 1 + } +done + +for header in X-Device-OS X-Ver-OS X-Device-Model X-Device-Locale X-App-Version Accept-Language; do + grep -Fq "$header" "$CACHE_UC" || { + printf 'FAIL: missing request header %s\n' "$header" >&2 + exit 1 + } +done + +kernel_version_line="$(grep -n '^function get_kernel_version' "$CACHE_UC" | cut -d: -f1)" +device_headers_line="$(grep -n '^function device_request_headers' "$CACHE_UC" | cut -d: -f1)" +if [ "$kernel_version_line" -ge "$device_headers_line" ]; then + printf 'FAIL: get_kernel_version must be declared before device_request_headers\n' >&2 + exit 1 +fi + +source="$(sed -n '/^function generateHwid16(/,/^}/p' "$SECTION_JS")" +HWID_SOURCE="$source" node <<'NODE' +global.window = { + crypto: { + getRandomValues(bytes) { + for (let i = 0; i < bytes.length; i += 1) bytes[i] = i + 1; + return bytes; + }, + }, +}; + +const generate = Function(`${process.env.HWID_SOURCE}; return generateHwid16`)(); +const hwid = generate(); +if (hwid !== '0102030405060708') { + throw new Error(`unexpected generated HWID: ${hwid}`); +} +if (!/^[0-9a-f]{16}$/.test(hwid)) { + throw new Error(`generated HWID is not 16 lowercase hex characters: ${hwid}`); +} +NODE + +printf 'LuCI subscription header checks passed\n' diff --git a/tests/subscription_cache_state.sh b/tests/subscription_cache_state.sh index 76ddec54..fe92e750 100644 --- a/tests/subscription_cache_state.sh +++ b/tests/subscription_cache_state.sh @@ -365,6 +365,49 @@ if grep -Fq "left-hand side is not a function" "$WORK_DIR/hwid.err"; then fi rm -f "$WORK_DIR/runtime-cache/proxy-subscription-1.hwid" +mkdir -p "$WORK_DIR/custom-header-cache" +cat >"$WORK_DIR/cache-custom-headers.json" <<'JSON' +{ + "section": [ + { + ".name": "custom", + "subscription_urls": [ + "https://example.com/custom.txt" + ], + "subscription_url_settings": "{\"https://example.com/custom.txt\":{\"user_agent\":\"v2rayN\",\"custom_device_headers\":\"1\",\"device_os\":\"iOS\",\"ver_os\":\"26.4\",\"device_model\":\"iPhone 17 Pro Max\",\"device_locale\":\"ru\",\"app_version\":\"4.6.0\",\"accept_language\":\"ru-RU,ru;q=0.9\"}}" + } + ] +} +JSON +cat >"$WORK_DIR/custom-header-cache/custom-subscription-1.json" <<'JSON' +{"outbounds":[{"type":"direct","tag":"custom"}]} +JSON +printf '%s' 'https://example.com/custom.txt' >"$WORK_DIR/custom-header-cache/custom-subscription-1.url" +printf '%s' 'v2rayN' >"$WORK_DIR/custom-header-cache/custom-subscription-1.user_agent" + +if cache_ucode section-current-usable-cache-fixture \ + "$WORK_DIR/cache-custom-headers.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then + fail "custom device headers should invalidate a cache without their signature" +fi + +cat >"$WORK_DIR/custom-header-cache/custom-subscription-1.json" <<'JSON' +{"outbounds":[{"type":"direct","tag":"custom"}]} +JSON +printf '%s' 'https://example.com/custom.txt' >"$WORK_DIR/custom-header-cache/custom-subscription-1.url" +printf '%s' 'v2rayN' >"$WORK_DIR/custom-header-cache/custom-subscription-1.user_agent" +printf '%s' '{ "enabled": "1", "device_os": "iOS", "ver_os": "26.4", "device_model": "iPhone 17 Pro Max", "device_locale": "ru", "app_version": "4.6.0", "accept_language": "ru-RU,ru;q=0.9" }' >"$WORK_DIR/custom-header-cache/custom-subscription-1.device_headers" + +cache_ucode section-current-usable-cache-fixture \ + "$WORK_DIR/cache-custom-headers.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null || + fail "matching custom device headers signature should keep the cache current" + +sed 's/\\"device_os\\":\\"iOS\\"/\\"device_os\\":\\"Android\\"/' \ + "$WORK_DIR/cache-custom-headers.json" >"$WORK_DIR/cache-custom-headers-changed.json" +if cache_ucode section-current-usable-cache-fixture \ + "$WORK_DIR/cache-custom-headers-changed.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then + fail "changed custom device headers should invalidate cache" +fi + printf '%s' 'https://example.com/stale.txt' >"$WORK_DIR/runtime-cache/proxy-subscription-1.url" if cache_ucode section-current-usable-cache-fixture \ "$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then