/** * 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;