Expose dev subscription headers without HWID
This commit is contained in:
@@ -210,6 +210,8 @@ pub struct LocalSingBoxStatusResponse {
|
|||||||
pub component: ComponentStatusDto,
|
pub component: ComponentStatusDto,
|
||||||
pub generated_config_path: String,
|
pub generated_config_path: String,
|
||||||
pub lan_listen_host: Option<String>,
|
pub lan_listen_host: Option<String>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub subscription_identity: SubscriptionRequestIdentityDto,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -243,6 +245,21 @@ pub struct SubscriptionServerDto {
|
|||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SubscriptionRequestIdentityDto {
|
||||||
|
pub headers: Vec<SubscriptionRequestHeaderDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SubscriptionRequestHeaderDto {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SaveSingBoxSubscriptionInputDto {
|
pub struct SaveSingBoxSubscriptionInputDto {
|
||||||
@@ -492,6 +509,21 @@ impl SubscriptionFetcher for SystemSubscriptionFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn subscription_request_identity_for_display() -> SubscriptionRequestIdentityDto {
|
||||||
|
let identity = subscription::SubscriptionFetchIdentity::default();
|
||||||
|
let headers = identity
|
||||||
|
.request_headers_without_device_hwid()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| SubscriptionRequestHeaderDto {
|
||||||
|
name: name.to_string(),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SubscriptionRequestIdentityDto { headers }
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Clock {
|
pub trait Clock {
|
||||||
fn now(&self) -> String;
|
fn now(&self) -> String;
|
||||||
}
|
}
|
||||||
@@ -1404,6 +1436,8 @@ fn read_singbox_status_with_detection(
|
|||||||
.display()
|
.display()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
lan_listen_host: local_lan_ipv4(),
|
lan_listen_host: local_lan_ipv4(),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
subscription_identity: subscription_request_identity_for_display(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,30 @@ impl SubscriptionFetchIdentity {
|
|||||||
..Self::default()
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_headers_without_device_hwid(&self) -> Vec<(&'static str, String)> {
|
||||||
|
let mut headers = vec![
|
||||||
|
("User-Agent", self.user_agent.clone()),
|
||||||
|
("X-App-Name", self.app_name.clone()),
|
||||||
|
("X-Device-OS", self.device_os.clone()),
|
||||||
|
("X-Device-Model", self.device_model.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(device_os_version) = self
|
||||||
|
.device_os_version
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
let header_value = sanitize_header_value(device_os_version);
|
||||||
|
if !header_value.is_empty() {
|
||||||
|
headers.push(("X-Device-OS-Version", header_value.clone()));
|
||||||
|
headers.push(("X-Ver-OS", header_value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SubscriptionFetchIdentity {
|
impl Default for SubscriptionFetchIdentity {
|
||||||
@@ -119,23 +143,10 @@ pub fn fetch_subscription_with_identity(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut request = reqwest::blocking::Client::new()
|
let mut request = reqwest::blocking::Client::new().get(parsed_url);
|
||||||
.get(parsed_url)
|
|
||||||
.header("user-agent", identity.user_agent.as_str())
|
|
||||||
.header("x-app-name", identity.app_name.as_str())
|
|
||||||
.header("x-device-os", identity.device_os.as_str())
|
|
||||||
.header("x-device-model", identity.device_model.as_str());
|
|
||||||
|
|
||||||
if let Some(device_os_version) = identity
|
for (name, value) in identity.request_headers_without_device_hwid() {
|
||||||
.device_os_version
|
request = request.header(name, value);
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
{
|
|
||||||
let header_value = sanitize_header_value(device_os_version);
|
|
||||||
if !header_value.is_empty() {
|
|
||||||
request = request.header("x-device-os-version", header_value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(device_hwid) = identity
|
if let Some(device_hwid) = identity
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use proxywarden_lib::adapters::singbox::{
|
|||||||
};
|
};
|
||||||
use proxywarden_lib::commands::{
|
use proxywarden_lib::commands::{
|
||||||
fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
|
fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage,
|
||||||
generate_singbox_config_with_services, save_singbox_subscription_to_storage,
|
generate_singbox_config_with_services, read_singbox_status,
|
||||||
select_singbox_server_in_storage, Clock, SaveSingBoxSubscriptionInputDto,
|
save_singbox_subscription_to_storage, select_singbox_server_in_storage, Clock,
|
||||||
SelectSingBoxServerInputDto, SubscriptionFetcher,
|
SaveSingBoxSubscriptionInputDto, SelectSingBoxServerInputDto, SubscriptionFetcher,
|
||||||
};
|
};
|
||||||
use proxywarden_lib::models::{
|
use proxywarden_lib::models::{
|
||||||
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
|
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
|
||||||
@@ -49,6 +49,39 @@ fn saves_subscription_url_without_exposing_secret_query() {
|
|||||||
cleanup(&root);
|
cleanup(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[test]
|
||||||
|
fn singbox_status_exposes_dev_subscription_headers_without_hwid() {
|
||||||
|
let root = test_root("dev-subscription-identity");
|
||||||
|
let storage = JsonStorage::new(root.clone());
|
||||||
|
|
||||||
|
let status = read_singbox_status(&storage).expect("read sing-box status");
|
||||||
|
let headers = status
|
||||||
|
.subscription_identity
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.map(|header| (header.name.clone(), header.value.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let expected_headers = subscription::SubscriptionFetchIdentity::default()
|
||||||
|
.request_headers_without_device_hwid()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| (name.to_string(), value))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(headers, expected_headers);
|
||||||
|
assert!(headers
|
||||||
|
.iter()
|
||||||
|
.all(|(name, _)| !name.eq_ignore_ascii_case("x-hwid")));
|
||||||
|
let serialized = serde_json::to_value(&status).expect("serialize status");
|
||||||
|
assert!(serialized.get("subscriptionIdentity").is_some());
|
||||||
|
assert!(!serialized
|
||||||
|
.to_string()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("x-hwid"));
|
||||||
|
|
||||||
|
cleanup(&root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_non_http_subscription_url() {
|
fn rejects_non_http_subscription_url() {
|
||||||
let root = test_root("invalid-subscription");
|
let root = test_root("invalid-subscription");
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ fn fetch_subscription_sends_device_hwid_header_when_identity_is_set() {
|
|||||||
assert!(request.contains("x-app-name: proxywarden"));
|
assert!(request.contains("x-app-name: proxywarden"));
|
||||||
assert!(request.contains("x-device-os:"));
|
assert!(request.contains("x-device-os:"));
|
||||||
assert!(request.contains("x-device-os-version: windows 11 pro | 25h2 | build 26200.8655"));
|
assert!(request.contains("x-device-os-version: windows 11 pro | 25h2 | build 26200.8655"));
|
||||||
|
assert!(request.contains("x-ver-os: windows 11 pro | 25h2 | build 26200.8655"));
|
||||||
assert!(request.contains("x-device-model: proxywarden"));
|
assert!(request.contains("x-device-model: proxywarden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,16 @@ export interface LocalSingBoxStatusResponse {
|
|||||||
component: ComponentStatus;
|
component: ComponentStatus;
|
||||||
generatedConfigPath: string;
|
generatedConfigPath: string;
|
||||||
lanListenHost?: string;
|
lanListenHost?: string;
|
||||||
|
subscriptionIdentity?: SubscriptionRequestIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionRequestIdentity {
|
||||||
|
headers: SubscriptionRequestHeader[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionRequestHeader {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PingServerResponse {
|
export interface PingServerResponse {
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ const MAIN_PROFILE_ID = 'main-profile';
|
|||||||
const LOCAL_SINGBOX_TARGET_ID = 'local-singbox';
|
const LOCAL_SINGBOX_TARGET_ID = 'local-singbox';
|
||||||
const LOG_VISIBLE_MS = 6500;
|
const LOG_VISIBLE_MS = 6500;
|
||||||
const PANEL_ORDER: PanelId[] = ['proxifyre', 'summary', 'proxy'];
|
const PANEL_ORDER: PanelId[] = ['proxifyre', 'summary', 'proxy'];
|
||||||
|
const SHOW_DEV_SUBSCRIPTION_IDENTITY = import.meta.env.DEV;
|
||||||
const proxyWardenToggleOnImage = new URL('../assets/proxywarden-toggle-on.png', import.meta.url).href;
|
const proxyWardenToggleOnImage = new URL('../assets/proxywarden-toggle-on.png', import.meta.url).href;
|
||||||
const proxyWardenToggleOffImage = new URL('../assets/proxywarden-toggle-off.png', import.meta.url).href;
|
const proxyWardenToggleOffImage = new URL('../assets/proxywarden-toggle-off.png', import.meta.url).href;
|
||||||
|
|
||||||
@@ -1381,7 +1382,13 @@ export function App() {
|
|||||||
inlineActions={(
|
inlineActions={(
|
||||||
<DetailsPopover
|
<DetailsPopover
|
||||||
className="setup-summary"
|
className="setup-summary"
|
||||||
details={singBoxDetailLines(singbox, singBoxStatus, singBoxSetupStatus, selectedServerTag)}
|
details={singBoxDetailLines(
|
||||||
|
singbox,
|
||||||
|
singBoxStatus,
|
||||||
|
singBoxSetupStatus,
|
||||||
|
selectedServerTag,
|
||||||
|
SHOW_DEV_SUBSCRIPTION_IDENTITY,
|
||||||
|
)}
|
||||||
popoverLabel="Состав Local sing-box"
|
popoverLabel="Состав Local sing-box"
|
||||||
aria-label={`Подробности Local sing-box: ${setupSummary}`}
|
aria-label={`Подробности Local sing-box: ${setupSummary}`}
|
||||||
>
|
>
|
||||||
@@ -2552,6 +2559,7 @@ function singBoxDetailLines(
|
|||||||
status: LocalSingBoxStatusResponse | null,
|
status: LocalSingBoxStatusResponse | null,
|
||||||
setupStatus: SingBoxSetupStatus | null,
|
setupStatus: SingBoxSetupStatus | null,
|
||||||
selectedServerTag: string | undefined,
|
selectedServerTag: string | undefined,
|
||||||
|
showDevSubscriptionIdentity = false,
|
||||||
) {
|
) {
|
||||||
const setupDetails = setupStatus
|
const setupDetails = setupStatus
|
||||||
? setupStatus.items
|
? setupStatus.items
|
||||||
@@ -2559,7 +2567,7 @@ function singBoxDetailLines(
|
|||||||
.join('; ')
|
.join('; ')
|
||||||
: 'состав не проверен';
|
: 'состав не проверен';
|
||||||
|
|
||||||
return [
|
const details = [
|
||||||
`Локально: ${localSingBoxAddress(status)}`,
|
`Локально: ${localSingBoxAddress(status)}`,
|
||||||
`LAN: ${lanSingBoxAddress(status) ?? 'недоступен'}`,
|
`LAN: ${lanSingBoxAddress(status) ?? 'недоступен'}`,
|
||||||
`Сервер: ${selectedServerTag ? displayServerTag(selectedServerTag) : 'не выбран'}`,
|
`Сервер: ${selectedServerTag ? displayServerTag(selectedServerTag) : 'не выбран'}`,
|
||||||
@@ -2568,6 +2576,30 @@ function singBoxDetailLines(
|
|||||||
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
|
`Подписка: ${status?.config.subscriptionDisplayUrl ?? (status?.config.hasSubscription ? 'сохранена' : 'не загружена')}`,
|
||||||
`Состав: ${setupDetails}`,
|
`Состав: ${setupDetails}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (showDevSubscriptionIdentity) {
|
||||||
|
details.push(...subscriptionIdentityDetailLines(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscriptionIdentityDetailLines(status: LocalSingBoxStatusResponse | null) {
|
||||||
|
const headers = status?.subscriptionIdentity?.headers
|
||||||
|
?.filter((header) => header.name.trim().toLowerCase() !== 'x-hwid') ?? [];
|
||||||
|
|
||||||
|
if (!headers.length) return ['Dev headers подписки: недоступны'];
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
'Dev headers подписки (без HWID):',
|
||||||
|
...headers.map((header) => `${header.name}: ${header.value || 'пусто'}`),
|
||||||
|
];
|
||||||
|
const sendsLegacyOsVersion = headers.some((header) => header.name.trim().toLowerCase() === 'x-ver-os');
|
||||||
|
if (!sendsLegacyOsVersion) {
|
||||||
|
details.push('X-Ver-OS: не отправится, версия ОС не определена');
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
function componentDetails(component: ComponentStatus | undefined, checking: boolean) {
|
function componentDetails(component: ComponentStatus | undefined, checking: boolean) {
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Reference in New Issue
Block a user