mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-06 07:47:27 +08:00
- 新增共享模块 netlify/functions/_shared/internal-headers.js,集中构建内部函数互调请求头,统一 ACCESS_KEY 未配置时的 500 抛错逻辑 - 更新 giffgaff-graphql.js / giffgaff-mfa-challenge.js / giffgaff-mfa-validation.js,移除各自重复的 getInternalHeaders 实现,改为复用 getInternalHeaders - 调整 verify-cookie.js 返回语义:以 success 表示 HTTP 层请求是否成功、以 valid 表示是否具备可用于后续 API 的 JWT 令牌 - 修正 verify-cookie.js 错误提示文案中的乱码问题,确保提示“无法通过Cookie获取访问令牌”表述正确
181 lines
5.2 KiB
JavaScript
181 lines
5.2 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, context, { auth, body }) => {
|
||
// 输入验证
|
||
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 字段作为能否继续的唯一判断依据。
|
||
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;
|
||
}
|
||
}
|