mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
♻️ refactor(simyo): 统一客户端版本与 User-Agent 配置
抽取 client-identity 作为版本、设备型号与 User-Agent 的单一事实来源; 本地与 Netlify 代理强制使用配置的 User-Agent,不再透传浏览器 UA。
This commit is contained in:
@@ -41,9 +41,12 @@ ACCESS_KEY=
|
||||
SIMYO_CLIENT_TOKEN=
|
||||
# 可选:固定 X-Device-ID(UUID,官方 App 每个请求必带;未设则进程内自动生成)
|
||||
# SIMYO_DEVICE_ID=
|
||||
# 可选:覆盖默认客户端版本 / UA(默认对齐 iOS 4.28.0 抓包)
|
||||
# 可选:覆盖默认客户端身份(默认对齐 iOS 4.28.0 / iOS 27.0 / iPhone16,1 抓包)
|
||||
# 与 src/simyo/js/modules/client-identity.js 保持同步
|
||||
# SIMYO_CLIENT_VERSION=4.28.0
|
||||
# SIMYO_CLIENT_PLATFORM=ios
|
||||
# SIMYO_IOS_VERSION=27.0
|
||||
# SIMYO_DEVICE_MODEL=iPhone16,1
|
||||
# SIMYO_USER_AGENT=MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)
|
||||
|
||||
# Giffgaff OAuth token exchange 回调 URI
|
||||
|
||||
@@ -35,12 +35,14 @@
|
||||
|
||||
# API代理重定向(用于解决CORS问题)
|
||||
# 通用 Simyo API 代理 (webapi - 新版API)
|
||||
# User-Agent 覆盖为官方 iOS 4.28.0 抓包值(浏览器无法可靠设置 UA)
|
||||
# 与 src/simyo/js/modules/client-identity.js 保持同步
|
||||
[[redirects]]
|
||||
from = "/api/simyo/*"
|
||||
to = "https://appapi.simyo.nl/webapi/api/v1/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl", User-Agent = "MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)"}
|
||||
|
||||
# v2 API 代理(用于特定端点如可用验证方式查询)
|
||||
[[redirects]]
|
||||
@@ -48,7 +50,7 @@
|
||||
to = "https://appapi.simyo.nl/webapi/api/v2/esim.availableValidationMethods"
|
||||
status = 200
|
||||
force = true
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl", User-Agent = "MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)"}
|
||||
|
||||
# 保留旧版 simyoapi 路径的兼容性(如果需要)
|
||||
[[redirects]]
|
||||
@@ -56,7 +58,7 @@
|
||||
to = "https://appapi.simyo.nl/simyoapi/api/v1/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl"}
|
||||
headers = {X-Forwarded-Host = "appapi.simyo.nl", User-Agent = "MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)"}
|
||||
|
||||
[[headers]]
|
||||
# 限制 Simyo 代理仅允许指定站点跨域访问
|
||||
|
||||
14
server.js
14
server.js
@@ -29,10 +29,14 @@ const { parseOrigins, isAllowedOrigin: _isAllowedOrigin, resolveCorsOrigin: _res
|
||||
const origins = parseOrigins(process.env.ALLOWED_ORIGIN);
|
||||
const isAllowedOrigin = (origin) => _isAllowedOrigin(origin, origins);
|
||||
const getCorsOrigin = (origin) => _resolveCorsOrigin(origin, origins);
|
||||
// 与 src/simyo/js/modules/client-identity.js 保持同步(官方 iOS 4.28.0 抓包)
|
||||
const DEFAULT_SIMYO_CLIENT_PLATFORM = 'ios';
|
||||
const DEFAULT_SIMYO_CLIENT_VERSION = '4.28.0';
|
||||
// 与官方 iOS 4.28.0 抓包一致(版本号后两个空格)
|
||||
const DEFAULT_SIMYO_USER_AGENT = 'MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)';
|
||||
const DEFAULT_SIMYO_IOS_VERSION = '27.0';
|
||||
const DEFAULT_SIMYO_DEVICE_MODEL = 'iPhone16,1';
|
||||
// 版本号与括号之间为两个空格
|
||||
const DEFAULT_SIMYO_USER_AGENT =
|
||||
`MijnSimyoFT/${DEFAULT_SIMYO_CLIENT_VERSION} (iOS ${DEFAULT_SIMYO_IOS_VERSION}; ${DEFAULT_SIMYO_DEVICE_MODEL})`;
|
||||
const crypto = require('crypto');
|
||||
function getDefaultSimyoDeviceId() {
|
||||
if (process.env.SIMYO_DEVICE_ID) return process.env.SIMYO_DEVICE_ID;
|
||||
@@ -208,7 +212,8 @@ app.use('/api/simyo/*', (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 代理请求(X-Device-ID 为 Simyo 4.28+ 必填)
|
||||
// 代理请求:强制官方 App 身份头(浏览器 UA 不可靠,禁止透传)
|
||||
// X-Device-ID 为 Simyo 4.28+ 必填,优先使用前端持久化 ID
|
||||
const axios = require('axios');
|
||||
const config = {
|
||||
method: req.method.toLowerCase(),
|
||||
@@ -216,7 +221,8 @@ app.use('/api/simyo/*', (req, res) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'User-Agent': req.headers['user-agent'] || process.env.SIMYO_USER_AGENT || DEFAULT_SIMYO_USER_AGENT,
|
||||
'Accept-Encoding': 'gzip',
|
||||
'User-Agent': process.env.SIMYO_USER_AGENT || DEFAULT_SIMYO_USER_AGENT,
|
||||
'X-Client-Token': simyoClientToken,
|
||||
'X-Client-Platform': process.env.SIMYO_CLIENT_PLATFORM || DEFAULT_SIMYO_CLIENT_PLATFORM,
|
||||
'X-Client-Version': process.env.SIMYO_CLIENT_VERSION || DEFAULT_SIMYO_CLIENT_VERSION,
|
||||
|
||||
@@ -2,25 +2,28 @@
|
||||
* Simyo API配置模块
|
||||
* 定义所有API端点和请求配置
|
||||
*
|
||||
* 请求头字段对齐官方 Mijn Simyo iOS 4.28.0 抓包:
|
||||
* 请求头字段对齐官方 Mijn Simyo iOS 抓包:
|
||||
* X-Client-Token / X-Client-Platform / X-Client-Version / X-Device-ID / User-Agent
|
||||
* 版本与 UA 见 client-identity.js(单一事实来源)
|
||||
*/
|
||||
|
||||
import { isNetlifyEnvironment } from './utils.js';
|
||||
import { t } from '../../../js/modules/i18n.js';
|
||||
import { simyoClientIdentity } from './client-identity.js';
|
||||
|
||||
/** localStorage 键:持久化设备 ID,模拟 iOS identifierForVendor */
|
||||
const DEVICE_ID_STORAGE_KEY = 'simyo_device_id';
|
||||
|
||||
/**
|
||||
* Simyo客户端配置(与官方 App 抓包一致)
|
||||
* Simyo客户端配置(与官方 App 抓包一致,源自 client-identity)
|
||||
*/
|
||||
export const simyoConfig = {
|
||||
clientToken: 'e77b7e2f43db41bb95b17a2a11581a38',
|
||||
clientPlatform: 'ios',
|
||||
clientVersion: '4.28.0',
|
||||
// 官方 UA 在版本号后有两个空格
|
||||
userAgent: 'MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)'
|
||||
clientToken: simyoClientIdentity.clientToken,
|
||||
clientPlatform: simyoClientIdentity.clientPlatform,
|
||||
clientVersion: simyoClientIdentity.clientVersion,
|
||||
iosVersion: simyoClientIdentity.iosVersion,
|
||||
deviceModel: simyoClientIdentity.deviceModel,
|
||||
userAgent: simyoClientIdentity.userAgent
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
51
src/simyo/js/modules/client-identity.js
Normal file
51
src/simyo/js/modules/client-identity.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Simyo 官方 App 客户端身份(单一事实来源)
|
||||
*
|
||||
* 对齐 Mijn Simyo iOS 抓包(2026-07-22 HAR):
|
||||
* - App 版本: 4.28.0
|
||||
* - API User-Agent: MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)
|
||||
* (版本号与括号之间为两个空格)
|
||||
* - 平台: ios
|
||||
* - 设备型号: iPhone16,1
|
||||
* - 系统版本: 27.0
|
||||
*
|
||||
* 修改此处后请同步:
|
||||
* - server.js 中的 DEFAULT_SIMYO_* 常量
|
||||
* - src/simyo/simyo_proxy_server.js 中的 SIMYO_CONFIG.headers
|
||||
* - netlify.toml 中 /api/simyo 代理的 User-Agent 覆盖
|
||||
* - env.example 注释示例
|
||||
*/
|
||||
|
||||
/** 官方 App 版本号(X-Client-Version) */
|
||||
export const SIMYO_CLIENT_VERSION = '4.28.0';
|
||||
|
||||
/** 官方平台(X-Client-Platform) */
|
||||
export const SIMYO_CLIENT_PLATFORM = 'ios';
|
||||
|
||||
/** 官方客户端 Token(X-Client-Token) */
|
||||
export const SIMYO_CLIENT_TOKEN = 'e77b7e2f43db41bb95b17a2a11581a38';
|
||||
|
||||
/** 抓包中的 iOS 系统版本 */
|
||||
export const SIMYO_IOS_VERSION = '27.0';
|
||||
|
||||
/** 抓包中的设备型号(identifier) */
|
||||
export const SIMYO_DEVICE_MODEL = 'iPhone16,1';
|
||||
|
||||
/**
|
||||
* appapi 请求使用的 User-Agent
|
||||
* 注意:版本号后必须保留两个空格,与官方 Flutter 客户端一致
|
||||
*/
|
||||
export const SIMYO_USER_AGENT =
|
||||
`MijnSimyoFT/${SIMYO_CLIENT_VERSION} (iOS ${SIMYO_IOS_VERSION}; ${SIMYO_DEVICE_MODEL})`;
|
||||
|
||||
/**
|
||||
* 聚合配置,便于 createHeaders / 测试一次性读取
|
||||
*/
|
||||
export const simyoClientIdentity = Object.freeze({
|
||||
clientToken: SIMYO_CLIENT_TOKEN,
|
||||
clientPlatform: SIMYO_CLIENT_PLATFORM,
|
||||
clientVersion: SIMYO_CLIENT_VERSION,
|
||||
iosVersion: SIMYO_IOS_VERSION,
|
||||
deviceModel: SIMYO_DEVICE_MODEL,
|
||||
userAgent: SIMYO_USER_AGENT
|
||||
});
|
||||
@@ -33,21 +33,27 @@ function isAllowedTarget(urlStr) {
|
||||
}
|
||||
}
|
||||
|
||||
// Simyo API配置(对齐官方 iOS 4.28.0 抓包)
|
||||
// Simyo API配置(与 client-identity.js 同步:iOS 4.28.0 / iOS 27.0 / iPhone16,1)
|
||||
const crypto = require('crypto');
|
||||
const SIMYO_CLIENT_VERSION = process.env.SIMYO_CLIENT_VERSION || '4.28.0';
|
||||
const SIMYO_IOS_VERSION = process.env.SIMYO_IOS_VERSION || '27.0';
|
||||
const SIMYO_DEVICE_MODEL = process.env.SIMYO_DEVICE_MODEL || 'iPhone16,1';
|
||||
const SIMYO_USER_AGENT =
|
||||
process.env.SIMYO_USER_AGENT ||
|
||||
`MijnSimyoFT/${SIMYO_CLIENT_VERSION} (iOS ${SIMYO_IOS_VERSION}; ${SIMYO_DEVICE_MODEL})`;
|
||||
const SIMYO_DEVICE_ID = process.env.SIMYO_DEVICE_ID || crypto.randomUUID().toUpperCase();
|
||||
const SIMYO_CONFIG = {
|
||||
baseUrl: 'https://appapi.simyo.nl/webapi/api/v1',
|
||||
headers: {
|
||||
'X-Client-Token': 'e77b7e2f43db41bb95b17a2a11581a38',
|
||||
'X-Client-Platform': 'ios',
|
||||
'X-Client-Version': '4.28.0',
|
||||
'X-Client-Token': process.env.SIMYO_CLIENT_TOKEN || 'e77b7e2f43db41bb95b17a2a11581a38',
|
||||
'X-Client-Platform': process.env.SIMYO_CLIENT_PLATFORM || 'ios',
|
||||
'X-Client-Version': SIMYO_CLIENT_VERSION,
|
||||
'X-Device-ID': SIMYO_DEVICE_ID,
|
||||
'User-Agent': 'MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)',
|
||||
'User-Agent': SIMYO_USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Connection: 'keep-alive',
|
||||
'Accept-Encoding': 'gzip, deflate, br'
|
||||
'Accept-Encoding': 'gzip'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Simyo api-config:请求头与设备 ID(对齐官方 App 抓包)
|
||||
* Simyo api-config / client-identity:请求头与设备身份(对齐官方 App 抓包)
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -9,8 +9,31 @@ import {
|
||||
getOrCreateDeviceId,
|
||||
simyoConfig
|
||||
} from '../../src/simyo/js/modules/api-config.js';
|
||||
import {
|
||||
SIMYO_CLIENT_PLATFORM,
|
||||
SIMYO_CLIENT_VERSION,
|
||||
SIMYO_DEVICE_MODEL,
|
||||
SIMYO_IOS_VERSION,
|
||||
SIMYO_USER_AGENT,
|
||||
simyoClientIdentity
|
||||
} from '../../src/simyo/js/modules/client-identity.js';
|
||||
|
||||
const UUID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/;
|
||||
/** 官方抓包 UA:版本号后恰好两个空格 */
|
||||
const OFFICIAL_UA = 'MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)';
|
||||
|
||||
describe('Simyo client-identity', () => {
|
||||
it('版本 / 设备 / UA 应与 4.28.0 抓包完全一致', () => {
|
||||
expect(SIMYO_CLIENT_VERSION).toBe('4.28.0');
|
||||
expect(SIMYO_CLIENT_PLATFORM).toBe('ios');
|
||||
expect(SIMYO_IOS_VERSION).toBe('27.0');
|
||||
expect(SIMYO_DEVICE_MODEL).toBe('iPhone16,1');
|
||||
expect(SIMYO_USER_AGENT).toBe(OFFICIAL_UA);
|
||||
// 版本号与括号之间必须是两个空格(不是一个)
|
||||
expect(SIMYO_USER_AGENT).toMatch(/^MijnSimyoFT\/4\.28\.0 {2}\(iOS 27\.0; iPhone16,1\)$/);
|
||||
expect(simyoClientIdentity.userAgent).toBe(OFFICIAL_UA);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Simyo api-config headers', () => {
|
||||
beforeEach(() => {
|
||||
@@ -29,9 +52,10 @@ describe('Simyo api-config headers', () => {
|
||||
it('createHeaders 必须包含官方抓包中的核心头', () => {
|
||||
const headers = createHeaders(false);
|
||||
expect(headers['X-Client-Token']).toBe(simyoConfig.clientToken);
|
||||
expect(headers['X-Client-Platform']).toBe('ios');
|
||||
expect(headers['X-Client-Version']).toBe('4.28.0');
|
||||
expect(headers['X-Client-Platform']).toBe(SIMYO_CLIENT_PLATFORM);
|
||||
expect(headers['X-Client-Version']).toBe(SIMYO_CLIENT_VERSION);
|
||||
expect(headers['X-Device-ID']).toMatch(UUID_RE);
|
||||
expect(headers['User-Agent']).toBe(OFFICIAL_UA);
|
||||
expect(headers['User-Agent']).toBe(simyoConfig.userAgent);
|
||||
expect(headers['Content-Type']).toBe('application/json');
|
||||
expect(headers.Accept).toBe('application/json');
|
||||
@@ -42,5 +66,6 @@ describe('Simyo api-config headers', () => {
|
||||
const headers = createHeaders(true, 'sess-token-1');
|
||||
expect(headers['X-Session-Token']).toBe('sess-token-1');
|
||||
expect(headers['X-Device-ID']).toMatch(UUID_RE);
|
||||
expect(headers['User-Agent']).toBe(OFFICIAL_UA);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -540,6 +540,7 @@
|
||||
'X-Client-Platform': 'ios',
|
||||
'X-Client-Version': '4.28.0',
|
||||
'X-Device-ID': 'E766D17B-BDF5-43B8-8807-35E43BA129E2',
|
||||
// 官方抓包:版本号后两个空格 + iOS 27.0 + iPhone16,1
|
||||
'User-Agent': 'MijnSimyoFT/4.28.0 (iOS 27.0; iPhone16,1)',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user