mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
- 在 netlify/functions/_shared/server-logger.js 中重构日志函数,统一日志输出格式,支持 context 字段覆盖基础字段(如 message) - 在 netlify/functions/_shared/middleware.js 中增强 withAuth 中间件,注入结构化 logger 到上下文,增加 request_start / request_end / request_error 日志追踪,并记录请求耗时 - 为所有 Netlify Function(giffgaff-sms-activate、giffgaff-graphql、auto-activate-esim、verify-cookie、giffgaff-token-exchange、giffgaff-mfa-challenge、giffgaff-mfa-validation、health)添加结构化日志,替换原有 console 输出,包含请求入参、状态和耗时 - 在 netlify/edge-functions/bff-proxy.js 和 markdown-negotiation.js 中实现 Deno 内联结构化日志,生成 requestId 并统一日志输出格式,支持 INFO/WARN/ERROR/DEBUG 级别控制 - 更新测试用例 tests/modules/server-logger.test.js 和 tests/functions/middleware-logging.test.js,新增日志级别过滤、context 覆盖、多实例隔离、OPTIONS 预检处理等测试场景
192 lines
5.5 KiB
JavaScript
192 lines
5.5 KiB
JavaScript
/**
|
||
* Netlify Function: Cookie验证服务
|
||
* 将Giffgaff Cookie转换为Access Token(尽可能接近真实登录状态)
|
||
*/
|
||
|
||
const axios = require('axios');
|
||
const cheerio = require('cheerio');
|
||
const { withAuth, validateInput, AuthError } = require('./_shared/middleware');
|
||
|
||
const { isRateLimited } = require('./_shared/rate-limiter');
|
||
|
||
// 输入验证schema
|
||
const verifyCookieSchema = {
|
||
cookie: {
|
||
required: true,
|
||
type: 'string',
|
||
minLength: 10,
|
||
maxLength: 8192
|
||
}
|
||
};
|
||
|
||
exports.handler = withAuth(async (event, ctx, { auth, body }) => {
|
||
const logger = ctx.logger;
|
||
const startTime = Date.now();
|
||
|
||
logger.info('invoked', { hasCookie: !!body.cookie });
|
||
|
||
// 输入验证
|
||
validateInput(verifyCookieSchema, body);
|
||
|
||
// 限流检查(优先使用 Netlify Blobs,降级为内存限流)
|
||
const ip = (event.headers['x-forwarded-for'] || '').split(',')[0] ||
|
||
event.headers['client-ip'] ||
|
||
event.headers['x-real-ip'] ||
|
||
'unknown';
|
||
|
||
if (await isRateLimited(ip)) {
|
||
throw new AuthError('请求过于频繁,请稍后再试', 429);
|
||
}
|
||
|
||
const { cookie } = body;
|
||
|
||
// 验证Cookie并获取Access Token
|
||
const result = await validateCookieAndGetToken(cookie);
|
||
|
||
// success: Cookie 验证请求本身是否成功(HTTP 层面)
|
||
// valid: Cookie 是否可用于后续 API 调用(需同时拿到可用的 JWT 令牌)
|
||
// 调用方(前端 & 内部函数)应以 valid 字段作为能否继续的唯一判断依据。
|
||
logger.info('cookie_verify_result', {
|
||
success: result.success,
|
||
hasAccessToken: !!result.accessToken,
|
||
duration: Date.now() - startTime,
|
||
});
|
||
|
||
if (result.success) {
|
||
const looksLikeJwt = typeof result.accessToken === 'string' &&
|
||
result.accessToken.includes('.') &&
|
||
result.accessToken.length > 200;
|
||
|
||
// Cookie 验证通过但未拿到可用 JWT → success=true, valid=false
|
||
// 表示服务可达、Cookie 有效,但缺少可用于 API 的令牌(需走 OAuth 或补充会话)
|
||
if (!looksLikeJwt) {
|
||
return {
|
||
statusCode: 200,
|
||
body: JSON.stringify({
|
||
success: true,
|
||
valid: false,
|
||
accessToken: null,
|
||
memberId: result.memberId || null,
|
||
emailSignature: null,
|
||
message: 'Cookie验证通过但未获取可用于API的访问令牌,请使用OAuth或确保包含 id.giffgaff.com 域的完整会话后重试'
|
||
})
|
||
};
|
||
}
|
||
|
||
// Cookie 验证通过且拿到可用 JWT → success=true, valid=true
|
||
return {
|
||
statusCode: 200,
|
||
body: JSON.stringify({
|
||
success: true,
|
||
valid: true,
|
||
accessToken: result.accessToken,
|
||
memberId: result.memberId || null,
|
||
emailSignature: null,
|
||
message: 'Cookie验证成功'
|
||
})
|
||
};
|
||
}
|
||
|
||
// Cookie 验证失败 → success=false, valid=false
|
||
return {
|
||
statusCode: 200,
|
||
body: JSON.stringify({
|
||
success: false,
|
||
valid: false,
|
||
accessToken: null,
|
||
emailSignature: null,
|
||
message: result.error || '无法通过Cookie获取访问令牌'
|
||
})
|
||
};
|
||
}, {
|
||
validateSchema: verifyCookieSchema
|
||
});
|
||
|
||
/**
|
||
* 验证Cookie并尝试获取Access Token
|
||
*/
|
||
async function validateCookieAndGetToken(cookie) {
|
||
try {
|
||
// 访问主页,尝试从HTML中提取登录状态或令牌
|
||
const response = await axios.get('https://www.giffgaff.com/', {
|
||
headers: {
|
||
'Cookie': cookie,
|
||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
|
||
},
|
||
timeout: 15000
|
||
});
|
||
|
||
const $ = cheerio.load(response.data);
|
||
|
||
// 尝试从页面中提取member ID
|
||
const memberId = extractMemberId($, response.data);
|
||
|
||
// 尝试从Cookie中提取或通过后续请求获取token
|
||
const accessToken = await extractAccessToken(cookie);
|
||
|
||
return {
|
||
success: true,
|
||
valid: !!memberId,
|
||
memberId,
|
||
accessToken
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从页面中提取Member ID
|
||
*/
|
||
function extractMemberId($, html) {
|
||
// 尝试多种方法提取member ID
|
||
const metaMemberId = $('meta[name="member-id"]').attr('content');
|
||
if (metaMemberId) return metaMemberId;
|
||
|
||
const dataGgMember = $('[data-gg-member]').attr('data-gg-member');
|
||
if (dataGgMember) return dataGgMember;
|
||
|
||
const matchMemberId = html.match(/memberId["\s:]+(\d+)/i);
|
||
if (matchMemberId && matchMemberId[1]) return matchMemberId[1];
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 尝试提取或获取Access Token
|
||
*/
|
||
async function extractAccessToken(cookie) {
|
||
try {
|
||
// 尝试访问需要认证的API端点来触发token生成
|
||
const apiResponse = await axios.get('https://www.giffgaff.com/auth/user-status', {
|
||
headers: {
|
||
'Cookie': cookie,
|
||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
||
'Accept': 'application/json'
|
||
},
|
||
timeout: 10000
|
||
});
|
||
|
||
// 检查响应中是否包含token
|
||
const token = apiResponse.data?.accessToken ||
|
||
apiResponse.data?.token ||
|
||
apiResponse.headers['x-access-token'];
|
||
|
||
return token || null;
|
||
} catch (error) {
|
||
// 尝试从Set-Cookie中提取
|
||
const setCookie = error.response?.headers['set-cookie'];
|
||
if (setCookie && Array.isArray(setCookie)) {
|
||
const tokenCookie = setCookie.find(c => c.includes('access_token='));
|
||
if (tokenCookie) {
|
||
const match = tokenCookie.match(/access_token=([^;]+)/);
|
||
return match ? match[1] : null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
}
|