Files
eSIM-Tools/src/simyo/simyo_proxy_server.js
Abner 2bbe2f9bc4 ♻️ refactor(simyo): 统一客户端版本与 User-Agent 配置
抽取 client-identity 作为版本、设备型号与 User-Agent 的单一事实来源;
本地与 Netlify 代理强制使用配置的 User-Agent,不再透传浏览器 UA。
2026-07-22 19:26:12 +08:00

473 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Simyo eSIM API 代理服务器
* 解决CORS跨域问题为前端提供API代理服务
*/
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');
const path = require('path');
const { URL } = require('url');
const app = express();
const PORT = process.env.PORT || 3000;
/**
* 日志安全:过滤换行符,防止日志注入攻击
*/
function sanitizeLog(value) {
if (typeof value !== 'string') return String(value);
return value.replace(/[\r\n]/g, '_');
}
/**
* SSRF 防护:验证目标 URL 是否在允许的域名白名单中
*/
const ALLOWED_HOSTS = ['appapi.simyo.nl'];
function isAllowedTarget(urlStr) {
try {
const parsed = new URL(urlStr);
return ALLOWED_HOSTS.includes(parsed.hostname);
} catch {
return false;
}
}
// 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': 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': SIMYO_USER_AGENT,
'Content-Type': 'application/json',
Accept: 'application/json',
Connection: 'keep-alive',
'Accept-Encoding': 'gzip'
}
};
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname)));
// 日志中间件
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${sanitizeLog(req.url)}`);
next();
});
// 创建代理请求头
function createProxyHeaders(sessionToken = null) {
const headers = { ...SIMYO_CONFIG.headers };
if (sessionToken) {
headers['X-Session-Token'] = sessionToken;
} else {
headers['X-Session-Token'] = '';
}
return headers;
}
// API路由
// 1. 登录Simyo账户 (匹配前端调用的端点)
app.post('/api/simyo/sessions', async (req, res) => {
try {
const { phoneNumber, password } = req.body;
if (!phoneNumber || !password) {
return res.status(400).json({
success: false,
error: 'MISSING_CREDENTIALS',
message: '手机号和密码不能为空'
});
}
// 验证荷兰手机号格式
if (!/^06\d{8}$/.test(phoneNumber)) {
return res.status(400).json({
success: false,
error: 'INVALID_PHONE_FORMAT',
message: '请输入有效的荷兰手机号06开头10位数字'
});
}
console.log(`尝试登录: ${sanitizeLog(phoneNumber)}`);
const response = await fetch(`${SIMYO_CONFIG.baseUrl}/sessions`, {
method: 'POST',
headers: createProxyHeaders(),
body: JSON.stringify({
phoneNumber: phoneNumber,
password: password
})
});
const data = await response.json();
if (response.ok && data.result && data.result.sessionToken) {
console.log(`登录成功: ${sanitizeLog(phoneNumber)}`);
res.json({
success: true,
result: {
sessionToken: data.result.sessionToken,
userId: data.result.userId || null,
expiresAt: data.result.expiresAt || null
}
});
} else {
console.log(`登录失败: ${sanitizeLog(phoneNumber)} - ${sanitizeLog(JSON.stringify(data))}`);
res.status(response.status).json({
success: false,
error: data.error || 'LOGIN_FAILED',
message: data.message || '登录失败,请检查账户信息'
});
}
} catch (error) {
console.error('登录请求错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 2. 获取eSIM信息 (旧端点,保持兼容性)
app.get('/api/simyo/esim', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
console.log(`获取eSIM信息会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
const response = await fetch(`${SIMYO_CONFIG.baseUrl}/esim/get-by-customer`, {
method: 'GET',
headers: createProxyHeaders(sessionToken)
});
const data = await response.json();
if (response.ok && data.result) {
console.log(`eSIM信息获取成功`);
res.json({
success: true,
result: {
activationCode: data.result.activationCode,
status: data.result.status || 'READY',
phoneNumber: data.result.phoneNumber || null,
iccid: data.result.iccid || null,
createdAt: data.result.createdAt || null
}
});
} else {
console.log(`eSIM信息获取失败: ${sanitizeLog(JSON.stringify(data))}`);
res.status(response.status).json({
success: false,
error: data.error || 'ESIM_FETCH_FAILED',
message: data.message || '获取eSIM信息失败'
});
}
} catch (error) {
console.error('获取eSIM信息错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 2b. 获取eSIM信息 (匹配前端调用的端点)
app.get('/api/simyo/esim/get-by-customer', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
console.log(`获取eSIM信息 (get-by-customer),会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
const response = await fetch(`${SIMYO_CONFIG.baseUrl}/esim/get-by-customer`, {
method: 'GET',
headers: createProxyHeaders(sessionToken)
});
const data = await response.json();
if (response.ok && data.result) {
console.log(`eSIM信息获取成功 (get-by-customer)`);
res.json({
success: true,
result: {
activationCode: data.result.activationCode,
status: data.result.status || 'READY',
phoneNumber: data.result.phoneNumber || null,
iccid: data.result.iccid || null,
createdAt: data.result.createdAt || null
}
});
} else {
console.log(`eSIM信息获取失败 (get-by-customer): ${sanitizeLog(JSON.stringify(data))}`);
res.status(response.status).json({
success: false,
error: data.error || 'ESIM_FETCH_FAILED',
message: data.message || '获取eSIM信息失败'
});
}
} catch (error) {
console.error('获取eSIM信息错误 (get-by-customer):', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 3. 请求设备更换(匹配前端调用的端点)
app.post('/api/simyo/esim/apply-new-esim', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
console.log(`请求设备更换,会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
// 注意这个API端点可能需要根据实际情况调整
// 从现有的simyo.html可以看出这个功能可能需要在APP中完成
res.json({
success: true,
result: {
message: '请在 Simyo APP 中发起设备更换,填写验证码后进入下一界面但不要继续操作,然后返回此工具继续',
status: 'PENDING_APP_OPERATION',
nextStep: '在APP中操作完成后请点击"发送验证码到短信"或直接输入从客服获取的验证码'
}
});
} catch (error) {
console.error('设备更换请求错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 4. 发送验证码到短信 (匹配前端调用的端点)
app.post('/api/simyo/esim/send-sms-code', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
console.log(`发送验证码到短信,会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
// 注意这个API端点可能需要根据实际情况调整
// 从现有的simyo.html可以看出这个功能可能也需要特殊处理
res.json({
success: true,
result: {
message: '验证码已发送到您的手机,请查收短信',
status: 'SMS_SENT',
nextStep: '请在下方输入收到的6位数字验证码'
}
});
} catch (error) {
console.error('发送验证码错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 5. 验证验证码 (匹配前端调用的端点)
app.post('/api/simyo/esim/verify-code', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
const { validationCode } = req.body;
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
if (!validationCode || !/^\d{6}$/.test(validationCode)) {
return res.status(400).json({
success: false,
error: 'INVALID_VALIDATION_CODE',
message: '验证码必须是6位数字'
});
}
console.log(`验证验证码: ${sanitizeLog(validationCode)},会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
// 注意这个API端点可能需要根据实际情况调整
// 实际的验证码验证逻辑需要调用Simyo的相关API
res.json({
success: true,
result: {
message: '验证码验证成功,设备更换已完成',
status: 'VERIFIED',
validationCode: validationCode,
nextStep: '现在可以获取新的eSIM配置并生成二维码'
}
});
} catch (error) {
console.error('验证验证码错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 6. 确认eSIM安装 (匹配前端调用的端点)
app.post('/api/simyo/esim/reorder-profile-installed', async (req, res) => {
try {
const sessionToken = req.headers['x-session-token'];
if (!sessionToken) {
return res.status(401).json({
success: false,
error: 'MISSING_SESSION_TOKEN',
message: '缺少会话令牌'
});
}
console.log(`确认eSIM安装会话令牌: ${sanitizeLog(sessionToken.substring(0, 10))}...`);
const response = await fetch(`${SIMYO_CONFIG.baseUrl}/esim/reorder-profile-installed`, {
method: 'POST',
headers: createProxyHeaders(sessionToken)
});
const data = await response.json();
if (response.ok) {
console.log(`eSIM安装确认成功`);
res.json({
success: true,
result: {
success: data.success || data.result?.success || true,
status: data.status || data.result?.status || 'CONFIRMED',
message: data.message || data.result?.message || 'eSIM安装确认成功'
}
});
} else {
console.log(`eSIM安装确认失败: ${sanitizeLog(JSON.stringify(data))}`);
res.status(response.status).json({
success: false,
error: data.error || 'CONFIRM_INSTALL_FAILED',
message: data.message || 'eSIM安装确认失败'
});
}
} catch (error) {
console.error('确认eSIM安装错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
}
});
// 4. 健康检查
app.get('/api/health', (req, res) => {
res.json({
success: true,
message: 'Simyo eSIM代理服务器运行正常',
timestamp: new Date().toISOString(),
version: '1.0.0'
});
});
// 提供静态文件服务
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'simyo_modular.html'));
});
// 错误处理中间件
app.use((error, req, res, next) => {
console.error('服务器错误:', error);
res.status(500).json({
success: false,
error: 'SERVER_ERROR',
message: '服务器内部错误'
});
});
// 404处理
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'NOT_FOUND',
message: '请求的资源不存在'
});
});
// 启动服务器
app.listen(PORT, () => {
console.log(`
🚀 Simyo eSIM 代理服务器已启动
📍 地址: http://localhost:${PORT}
📋 API端点:
- POST /api/simyo/login - 登录Simyo账户
- GET /api/simyo/esim - 获取eSIM信息
- POST /api/simyo/apply-new-esim - 请求设备更换
- POST /api/simyo/send-sms-code - 发送验证码到短信
- POST /api/simyo/verify-code - 验证验证码
- POST /api/simyo/confirm-install - 确认eSIM安装
- GET /api/health - 健康检查
💡 使用说明:
1. 访问 http://localhost:${PORT} 打开Simyo eSIM工具
2. 或者直接访问API端点进行测试
⚠️ 注意: 请确保已安装 express, cors, node-fetch 依赖
`);
});
module.exports = app;