mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
抽取 client-identity 作为版本、设备型号与 User-Agent 的单一事实来源; 本地与 Netlify 代理强制使用配置的 User-Agent,不再透传浏览器 UA。
473 lines
16 KiB
JavaScript
473 lines
16 KiB
JavaScript
/**
|
||
* 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;
|