588 lines
24 KiB
JavaScript
588 lines
24 KiB
JavaScript
'use strict';
|
||
|
||
const http = require('http');
|
||
const https = require('https');
|
||
const crypto = require('crypto');
|
||
const Core = require('@alicloud/pop-core');
|
||
const TableStore = require('tablestore');
|
||
|
||
const MAX_BODY_SIZE = 1024;
|
||
const PORT = 9000;
|
||
const TOKEN_CACHE_DURATION_MS = 5 * 60 * 1000; // 5分钟
|
||
const STEAM_AUTH_CACHE_DURATION_MS = 10 * 60 * 1000; // 10分钟
|
||
const MAX_STANDARD_UPLOAD_SIZE = 3 * 1024 * 1024; // 3MB
|
||
const MAX_BUG_REPORT_UPLOAD_SIZE = 10 * 1024 * 1024; // 10MB
|
||
const MAX_MULTILINGUAL_REPORT_UPLOAD_SIZE = 1 * 1024 * 1024; // 1MB
|
||
const STS_DURATION_SECONDS = 900; // 15分钟
|
||
|
||
const UPLOAD_TYPE_CONFIG = {
|
||
ossdata: {
|
||
cache: true,
|
||
extension: 'dat',
|
||
maxUploadSize: MAX_STANDARD_UPLOAD_SIZE,
|
||
buildPathPrefix: versionSegment => versionSegment
|
||
},
|
||
collectdata: {
|
||
cache: true,
|
||
extension: 'dat',
|
||
maxUploadSize: MAX_STANDARD_UPLOAD_SIZE,
|
||
buildPathPrefix: versionSegment => `collect/${versionSegment}`
|
||
},
|
||
bugreport: {
|
||
cache: false,
|
||
extension: 'zip',
|
||
maxUploadSize: MAX_BUG_REPORT_UPLOAD_SIZE,
|
||
uniqueObjectKey: true,
|
||
buildPathPrefix: versionSegment => `bugreport/${versionSegment}`
|
||
},
|
||
multilingualreport: {
|
||
cache: false,
|
||
extension: 'zip',
|
||
maxUploadSize: MAX_MULTILINGUAL_REPORT_UPLOAD_SIZE,
|
||
uniqueObjectKey: true,
|
||
buildPathPrefix: versionSegment => `multilingualreport/${versionSegment}`
|
||
}
|
||
};
|
||
|
||
const ACTION_STEAM_AUTH = 'steamauth';
|
||
|
||
/**
|
||
* 获取 Tablestore 客户端
|
||
*/
|
||
function getOtsClient() {
|
||
return new TableStore.Client({
|
||
accessKeyId: process.env.ACCESS_KEY_ID,
|
||
accessKeySecret: process.env.ACCESS_KEY_SECRET,
|
||
endpoint: process.env.OTS_ENDPOINT,
|
||
instancename: process.env.OTS_INSTANCE
|
||
});
|
||
}
|
||
|
||
function normalizeOtsAttributes(row) {
|
||
const attrs = {};
|
||
if (!row || !row.attributes) return attrs;
|
||
|
||
for (const attr of row.attributes) {
|
||
const value = attr.columnValue;
|
||
attrs[attr.columnName] = (value && typeof value.toNumber === 'function')
|
||
? value.toNumber()
|
||
: value;
|
||
}
|
||
|
||
return attrs;
|
||
}
|
||
|
||
/**
|
||
* 从 Tablestore 获取缓存的令牌
|
||
* @param {string} steamId
|
||
* @param {string|null} version 客户端版本号,null 表示老版本未传
|
||
* @param {string} type 上传类型:'ossdata' | 'collectdata' | 'bugreport' | 'multilingualreport'
|
||
*/
|
||
async function getCachedToken(steamId, version, type) {
|
||
const otsClient = getOtsClient();
|
||
const cacheKey = `${steamId}#${type}`;
|
||
|
||
const params = {
|
||
tableName: 'Players',
|
||
primaryKey: [{ PlayerId: cacheKey }],
|
||
maxVersions: 1
|
||
};
|
||
|
||
try {
|
||
console.log(`[缓存查询] 正在查询 SteamID: ${steamId} 的缓存令牌...`);
|
||
const result = await otsClient.getRow(params);
|
||
const row = result.row;
|
||
|
||
if (!row || !row.attributes || row.attributes.length === 0) {
|
||
console.log(`[缓存查询] SteamID: ${steamId} 无缓存记录`);
|
||
return null;
|
||
}
|
||
|
||
const attrs = normalizeOtsAttributes(row);
|
||
|
||
// 版本号比对:缓存中存储的版本号(无版本号时为空字符串或不存在)
|
||
const cachedVersion = attrs.version || null;
|
||
const requestVersion = version || null;
|
||
if (cachedVersion !== requestVersion) {
|
||
console.log(`[缓存查询] SteamID: ${steamId} 版本号不匹配,缓存版本: ${cachedVersion ?? '(无)'}, 请求版本: ${requestVersion ?? '(无)'},忽略缓存`);
|
||
return null;
|
||
}
|
||
|
||
const issuedAt = attrs.issuedAt;
|
||
if (!issuedAt || Date.now() - issuedAt > TOKEN_CACHE_DURATION_MS) {
|
||
console.log(`[缓存查询] SteamID: ${steamId} 缓存已过期(签发时间: ${new Date(issuedAt).toISOString()})`);
|
||
return null;
|
||
}
|
||
|
||
const stsExpireAt = attrs.stsExpireAt;
|
||
if (!stsExpireAt || stsExpireAt - Date.now() < 2 * 60 * 1000) {
|
||
const remaining = stsExpireAt ? Math.floor((stsExpireAt - Date.now()) / 1000) : 0;
|
||
console.log(`[缓存查询] SteamID: ${steamId} STS令牌剩余有效期不足(剩余 ${remaining} 秒),需重新签发`);
|
||
return null;
|
||
}
|
||
|
||
const expiresIn = Math.floor((stsExpireAt - Date.now()) / 1000);
|
||
console.log(`[缓存命中] SteamID: ${steamId} 命中缓存,版本: ${cachedVersion ?? '(无)'},剩余有效期 ${expiresIn} 秒`);
|
||
|
||
return {
|
||
accessKeyId: attrs.accessKeyId,
|
||
accessKeySecret: attrs.accessKeySecret,
|
||
securityToken: attrs.securityToken,
|
||
endpoint: attrs.endpoint,
|
||
bucket: attrs.bucket,
|
||
objectKey: attrs.objectKey,
|
||
policy: attrs.policy,
|
||
signature: attrs.signature,
|
||
expiresIn: expiresIn
|
||
};
|
||
} catch (e) {
|
||
console.error(`[缓存查询] 读取缓存失败,SteamID: ${steamId},错误: ${e.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将令牌存入 Tablestore
|
||
* @param {string} steamId
|
||
* @param {object} tokenData
|
||
* @param {string|null} version 客户端版本号,null 表示老版本未传
|
||
* @param {string} type 上传类型:'ossdata' | 'collectdata' | 'bugreport' | 'multilingualreport'
|
||
*/
|
||
async function saveTokenToCache(steamId, tokenData, version, type) {
|
||
const otsClient = getOtsClient();
|
||
const cacheKey = `${steamId}#${type}`;
|
||
|
||
const now = Date.now();
|
||
const stsExpireAt = now + tokenData.expiresIn * 1000;
|
||
|
||
const attributeColumns = [
|
||
{ accessKeyId: tokenData.accessKeyId },
|
||
{ accessKeySecret: tokenData.accessKeySecret },
|
||
{ securityToken: tokenData.securityToken },
|
||
{ endpoint: tokenData.endpoint },
|
||
{ bucket: tokenData.bucket },
|
||
{ objectKey: tokenData.objectKey },
|
||
{ policy: tokenData.policy },
|
||
{ signature: tokenData.signature },
|
||
{ issuedAt: TableStore.Long.fromNumber(now) },
|
||
{ stsExpireAt: TableStore.Long.fromNumber(stsExpireAt) },
|
||
// version 为 null 时存空字符串,与查询时保持一致
|
||
{ version: version !== null && version !== undefined ? version : '' }
|
||
];
|
||
|
||
const params = {
|
||
tableName: 'Players',
|
||
condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
|
||
primaryKey: [{ PlayerId: cacheKey }],
|
||
attributeColumns,
|
||
returnContent: { returnType: TableStore.ReturnType.NONE }
|
||
};
|
||
|
||
try {
|
||
console.log(`[缓存写入] 正在写入 SteamID: ${steamId} [${type}] 的令牌缓存,版本: ${version ?? '(无)'}...`);
|
||
await otsClient.putRow(params);
|
||
console.log(`[缓存写入] SteamID: ${steamId} [${type}] 缓存写入成功,过期时间: ${new Date(stsExpireAt).toISOString()}`);
|
||
} catch (e) {
|
||
console.error(`[缓存写入] SteamID: ${steamId} [${type}] 缓存写入失败,错误: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询 Steam 身份缓存。该缓存只证明“此 SteamID 近期通过 Steam Web API 校验”,
|
||
* 不绑定上传类型,也不直接授权 OSS 写入。
|
||
*/
|
||
async function getCachedSteamAuth(steamId) {
|
||
const otsClient = getOtsClient();
|
||
const cacheKey = `${steamId}#steamauth`;
|
||
|
||
const params = {
|
||
tableName: 'Players',
|
||
primaryKey: [{ PlayerId: cacheKey }],
|
||
maxVersions: 1
|
||
};
|
||
|
||
try {
|
||
const result = await otsClient.getRow(params);
|
||
const row = result.row;
|
||
|
||
if (!row || !row.attributes || row.attributes.length === 0) {
|
||
console.log(`[Steam缓存] SteamID: ${steamId} 无身份缓存`);
|
||
return null;
|
||
}
|
||
|
||
const attrs = normalizeOtsAttributes(row);
|
||
const authExpireAt = attrs.authExpireAt;
|
||
if (!authExpireAt || authExpireAt <= Date.now()) {
|
||
console.log(`[Steam缓存] SteamID: ${steamId} 身份缓存已过期`);
|
||
return null;
|
||
}
|
||
|
||
const expiresIn = Math.floor((authExpireAt - Date.now()) / 1000);
|
||
console.log(`[Steam缓存] SteamID: ${steamId} 命中身份缓存,剩余 ${expiresIn} 秒`);
|
||
return {
|
||
steamId,
|
||
version: attrs.version || null,
|
||
verifiedAt: attrs.verifiedAt || null,
|
||
expiresIn
|
||
};
|
||
} catch (e) {
|
||
console.error(`[Steam缓存] 读取身份缓存失败,SteamID: ${steamId},错误: ${e.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 写入 Steam 身份缓存,供后续上传跳过实时 Steam Web API 校验。
|
||
*/
|
||
async function saveSteamAuthCache(steamId, version) {
|
||
const otsClient = getOtsClient();
|
||
const cacheKey = `${steamId}#steamauth`;
|
||
const now = Date.now();
|
||
const authExpireAt = now + STEAM_AUTH_CACHE_DURATION_MS;
|
||
|
||
const params = {
|
||
tableName: 'Players',
|
||
condition: new TableStore.Condition(TableStore.RowExistenceExpectation.IGNORE, null),
|
||
primaryKey: [{ PlayerId: cacheKey }],
|
||
attributeColumns: [
|
||
{ verifiedAt: TableStore.Long.fromNumber(now) },
|
||
{ authExpireAt: TableStore.Long.fromNumber(authExpireAt) },
|
||
{ version: version !== null && version !== undefined ? version : '' }
|
||
],
|
||
returnContent: { returnType: TableStore.ReturnType.NONE }
|
||
};
|
||
|
||
await otsClient.putRow(params);
|
||
const expiresIn = Math.floor(STEAM_AUTH_CACHE_DURATION_MS / 1000);
|
||
console.log(`[Steam缓存] SteamID: ${steamId} 身份缓存写入成功,过期时间: ${new Date(authExpireAt).toISOString()}`);
|
||
return { expiresIn, authExpireAt };
|
||
}
|
||
|
||
/**
|
||
* 生成 Post Policy 和签名
|
||
*/
|
||
function generatePostPolicy(accessKeySecret, objectKey, bucket, expiration, maxUploadSize) {
|
||
const policy = {
|
||
expiration: expiration,
|
||
conditions: [
|
||
{ bucket: bucket },
|
||
['eq', '$key', objectKey],
|
||
['content-length-range', 1, maxUploadSize]
|
||
]
|
||
};
|
||
|
||
const policyBase64 = Buffer.from(JSON.stringify(policy)).toString('base64');
|
||
const signature = crypto
|
||
.createHmac('sha1', accessKeySecret)
|
||
.update(policyBase64)
|
||
.digest('base64');
|
||
|
||
console.log(`[PostPolicy] 生成策略,objectKey: ${objectKey},最大文件: ${maxUploadSize} 字节,过期: ${expiration}`);
|
||
|
||
return { policy: policyBase64, signature };
|
||
}
|
||
|
||
function normalizeUploadType(type) {
|
||
return Object.prototype.hasOwnProperty.call(UPLOAD_TYPE_CONFIG, type) ? type : 'ossdata';
|
||
}
|
||
|
||
function normalizeAction(action) {
|
||
return action === ACTION_STEAM_AUTH ? ACTION_STEAM_AUTH : 'upload';
|
||
}
|
||
|
||
async function ensureSteamVerified(steamId, authTicket, version, requestId, reason) {
|
||
const cachedAuth = await getCachedSteamAuth(steamId);
|
||
if (cachedAuth) {
|
||
console.log(`[Steam验证] RequestId: ${requestId},SteamID: ${steamId} ${reason} 命中身份缓存,跳过实时校验`);
|
||
return { success: true, cached: true, expiresIn: cachedAuth.expiresIn };
|
||
}
|
||
|
||
if (!authTicket) {
|
||
return { success: false, statusCode: 400, error: 'authTicket is required' };
|
||
}
|
||
|
||
console.log(`[Steam验证] RequestId: ${requestId},SteamID: ${steamId} ${reason} 未命中身份缓存,开始实时校验...`);
|
||
const verification = await verifySteamTicket(authTicket, steamId);
|
||
if (!verification.success) {
|
||
return { success: false, statusCode: 403, error: `Steam verification failed: ${verification.error}` };
|
||
}
|
||
|
||
try {
|
||
const cache = await saveSteamAuthCache(steamId, version);
|
||
return { success: true, cached: false, expiresIn: cache.expiresIn };
|
||
} catch (e) {
|
||
console.error(`[Steam缓存] SteamID: ${steamId} 身份缓存写入失败,错误: ${e.message}`);
|
||
return { success: true, cached: false, expiresIn: 0 };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调用 Steam Web API 验证 Auth Ticket(带重试)
|
||
*/
|
||
async function verifySteamTicket(ticket, expectedSteamId) {
|
||
const maxRetries = 3;
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
try {
|
||
console.log(`[Steam验证] 第 ${attempt}/${maxRetries} 次尝试验证 SteamID: ${expectedSteamId}`);
|
||
const result = await verifySteamTicketOnce(ticket, expectedSteamId);
|
||
return result;
|
||
} catch (e) {
|
||
console.error(`[Steam验证] 第 ${attempt} 次请求异常: ${e.message}`);
|
||
if (attempt === maxRetries) {
|
||
return { success: false, error: `Steam API 请求失败(重试 ${maxRetries} 次): ${e.message}` };
|
||
}
|
||
await new Promise(r => setTimeout(r, 1000));
|
||
}
|
||
}
|
||
}
|
||
|
||
function verifySteamTicketOnce(ticket, expectedSteamId) {
|
||
return new Promise((resolve, reject) => {
|
||
const steamApiKey = process.env.STEAM_API_KEY;
|
||
const appId = process.env.STEAM_APP_ID;
|
||
|
||
console.log(`[Steam验证] 开始验证 SteamID: ${expectedSteamId},AppID: ${appId},Ticket长度: ${ticket.length}`);
|
||
|
||
const url = `https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1/?key=${steamApiKey}&appid=${appId}&ticket=${ticket}`;
|
||
|
||
const req = https.get(url, (res) => {
|
||
let data = '';
|
||
res.on('data', chunk => { data += chunk; });
|
||
res.on('end', () => {
|
||
try {
|
||
const json = JSON.parse(data);
|
||
const params = json?.response?.params;
|
||
|
||
if (!params) {
|
||
const error = json?.response?.error?.errordesc || 'Steam 验证失败';
|
||
console.error(`[Steam验证] SteamID: ${expectedSteamId} 验证失败: ${error}`);
|
||
resolve({ success: false, error });
|
||
return;
|
||
}
|
||
|
||
if (params.result !== 'OK') {
|
||
console.error(`[Steam验证] SteamID: ${expectedSteamId} 验证结果异常: ${params.result}`);
|
||
resolve({ success: false, error: `Steam auth result: ${params.result}` });
|
||
return;
|
||
}
|
||
|
||
if (params.steamid !== expectedSteamId) {
|
||
console.error(`[Steam验证] SteamID 不匹配,期望: ${expectedSteamId},实际: ${params.steamid}`);
|
||
resolve({ success: false, error: 'SteamID mismatch' });
|
||
return;
|
||
}
|
||
|
||
console.log(`[Steam验证] SteamID: ${expectedSteamId} 验证通过 ✅`);
|
||
resolve({ success: true });
|
||
} catch (e) {
|
||
console.error(`[Steam验证] 解析响应失败: ${e.message},原始数据: ${data.substring(0, 200)}`);
|
||
resolve({ success: false, error: '解析 Steam API 响应失败' });
|
||
}
|
||
});
|
||
res.on('error', (e) => {
|
||
console.error(`[Steam验证] 响应流错误: ${e.message}`);
|
||
reject(e);
|
||
});
|
||
});
|
||
|
||
req.setTimeout(15000, () => {
|
||
console.error(`[Steam验证] 请求超时(15秒),主动销毁连接`);
|
||
req.destroy(new Error('Steam API 请求超时(15秒)'));
|
||
});
|
||
|
||
req.on('error', (e) => {
|
||
console.error(`[Steam验证] HTTPS 请求失败: ${e.message}`);
|
||
reject(e);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function handleRequest(req, res) {
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||
|
||
if (req.method === 'OPTIONS') {
|
||
res.writeHead(200);
|
||
res.end('');
|
||
return;
|
||
}
|
||
|
||
if (req.method !== 'POST') {
|
||
console.warn(`[请求拒绝] 不支持的请求方法: ${req.method}`);
|
||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||
return;
|
||
}
|
||
|
||
const requiredEnv = ['ACCESS_KEY_ID', 'ACCESS_KEY_SECRET', 'ROLE_ARN', 'BUCKET_NAME', 'STEAM_API_KEY', 'OTS_ENDPOINT', 'OTS_INSTANCE', 'STEAM_APP_ID'];
|
||
const missing = requiredEnv.filter(k => !process.env[k]);
|
||
if (missing.length) {
|
||
console.error(`[环境变量] 缺少必要的环境变量: ${missing.join(', ')}`);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: `Missing environment variables: ${missing.join(', ')}` }));
|
||
return;
|
||
}
|
||
|
||
let body = '';
|
||
for await (const chunk of req) {
|
||
body += chunk;
|
||
if (Buffer.byteLength(body, 'utf8') > MAX_BODY_SIZE) {
|
||
console.warn(`[请求拒绝] 请求体超过最大限制 ${MAX_BODY_SIZE} 字节`);
|
||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: 'Payload Too Large' }));
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const requestData = JSON.parse(body || '{}');
|
||
const { steamId, authTicket } = requestData;
|
||
// version 为可选字段,老版本不传则为 null
|
||
const version = (typeof requestData.version === 'string' && requestData.version.trim() !== '')
|
||
? requestData.version.trim()
|
||
: null;
|
||
const action = normalizeAction(requestData.action);
|
||
|
||
if (!steamId) {
|
||
console.warn('[参数校验] 缺少 steamId 参数');
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: 'steamId is required' }));
|
||
return;
|
||
}
|
||
|
||
const requestId = req.headers['x-fc-request-id'] || '-';
|
||
console.log(`[请求开始] RequestId: ${requestId},SteamID: ${steamId},Action: ${action},AuthTicket长度: ${authTicket ? authTicket.length : 0},版本: ${version ?? '(无)'}`);
|
||
|
||
if (action === ACTION_STEAM_AUTH) {
|
||
const verification = await ensureSteamVerified(steamId, authTicket, version, requestId, '[预校验]');
|
||
if (!verification.success) {
|
||
console.error(`[请求失败] RequestId: ${requestId},SteamID: ${steamId} Steam预校验失败: ${verification.error}`);
|
||
res.writeHead(verification.statusCode || 403, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: verification.error }));
|
||
return;
|
||
}
|
||
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
cached: verification.cached,
|
||
steamId,
|
||
version: version ?? '',
|
||
expiresIn: verification.expiresIn
|
||
}));
|
||
return;
|
||
}
|
||
|
||
// type 为可选字段,默认 'ossdata',兼容老客户端
|
||
const type = normalizeUploadType(requestData.type);
|
||
const typeConfig = UPLOAD_TYPE_CONFIG[type];
|
||
console.log(`[上传请求] RequestId: ${requestId},SteamID: ${steamId},版本: ${version ?? '(无)'},类型: ${type}`);
|
||
|
||
// 1. 先检查 Tablestore 缓存(版本号必须一致才命中)
|
||
if (typeConfig.cache) {
|
||
const cachedToken = await getCachedToken(steamId, version, type);
|
||
if (cachedToken) {
|
||
console.log(`[请求完成] RequestId: ${requestId},SteamID: ${steamId} [${type}] — 返回缓存令牌,剩余有效期 ${cachedToken.expiresIn} 秒`);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(cachedToken));
|
||
return;
|
||
}
|
||
} else {
|
||
console.log(`[缓存跳过] SteamID: ${steamId} [${type}] 需要每次生成新的上传对象,跳过缓存`);
|
||
}
|
||
|
||
// 2. Steam Ticket 验证(优先复用预校验身份缓存)
|
||
const verification = await ensureSteamVerified(steamId, authTicket, version, requestId, `[${type}]`);
|
||
if (!verification.success) {
|
||
console.error(`[请求失败] RequestId: ${requestId},SteamID: ${steamId} Steam验证失败: ${verification.error}`);
|
||
res.writeHead(verification.statusCode || 403, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: verification.error }));
|
||
return;
|
||
}
|
||
|
||
// 3. 申请 STS 令牌
|
||
console.log(`[STS签发] SteamID: ${steamId} [${type}] Steam身份有效,正在申请 STS 令牌...`);
|
||
const client = new Core({
|
||
accessKeyId: process.env.ACCESS_KEY_ID,
|
||
accessKeySecret: process.env.ACCESS_KEY_SECRET,
|
||
endpoint: 'https://sts.cn-shanghai.aliyuncs.com',
|
||
apiVersion: '2015-04-01'
|
||
});
|
||
|
||
// 根据 type 和版本号构建文件路径
|
||
// ossdata: {version}/{steamId}/{timestamp}.dat 或 common/{steamId}/{timestamp}.dat
|
||
// collectdata: collect/{version}/{steamId}/{timestamp}.dat 或 collect/common/{steamId}/{timestamp}.dat
|
||
// bugreport: bugreport/{version}/{steamId}/{timestamp}-{random}.zip 或 bugreport/common/{steamId}/{timestamp}-{random}.zip
|
||
// multilingualreport: multilingualreport/{version}/{steamId}/{timestamp}-{random}.zip 或 multilingualreport/common/{steamId}/{timestamp}-{random}.zip
|
||
const versionSegment = version !== null ? version : 'common';
|
||
const pathPrefix = typeConfig.buildPathPrefix(versionSegment);
|
||
const uniqueSuffix = typeConfig.uniqueObjectKey
|
||
? `-${crypto.randomUUID().replace(/-/g, '').substring(0, 12)}`
|
||
: '';
|
||
const objectKey = `${pathPrefix}/${steamId}/${Date.now()}${uniqueSuffix}.${typeConfig.extension}`;
|
||
console.log(`[路径构建] SteamID: ${steamId},类型: ${type},版本: ${version ?? '(无)'},ObjectKey: ${objectKey}`);
|
||
|
||
const stsParams = {
|
||
RoleArn: process.env.ROLE_ARN,
|
||
RoleSessionName: `p-${steamId}`.substring(0, 32),
|
||
DurationSeconds: STS_DURATION_SECONDS,
|
||
Policy: JSON.stringify({
|
||
Version: '1',
|
||
Statement: [{
|
||
Effect: 'Allow',
|
||
Action: ['oss:PutObject'],
|
||
Resource: [`acs:oss:*:*:${process.env.BUCKET_NAME}/${objectKey}`]
|
||
}]
|
||
})
|
||
};
|
||
|
||
const result = await client.request('AssumeRole', stsParams);
|
||
console.log(`[STS签发] SteamID: ${steamId} STS令牌签发成功,ObjectKey: ${objectKey}`);
|
||
|
||
// 4. 生成 Post Policy 和签名
|
||
const expiration = new Date(Date.now() + STS_DURATION_SECONDS * 1000).toISOString();
|
||
const { policy, signature } = generatePostPolicy(
|
||
result.Credentials.AccessKeySecret,
|
||
objectKey,
|
||
process.env.BUCKET_NAME,
|
||
expiration,
|
||
typeConfig.maxUploadSize
|
||
);
|
||
|
||
const tokenData = {
|
||
accessKeyId: result.Credentials.AccessKeyId,
|
||
accessKeySecret: result.Credentials.AccessKeySecret,
|
||
securityToken: result.Credentials.SecurityToken,
|
||
endpoint: 'oss-cn-shanghai.aliyuncs.com',
|
||
bucket: process.env.BUCKET_NAME,
|
||
objectKey: objectKey,
|
||
policy: policy,
|
||
signature: signature,
|
||
expiresIn: STS_DURATION_SECONDS
|
||
};
|
||
|
||
// 5. 存入 Tablestore 缓存(携带版本号和类型)
|
||
if (typeConfig.cache) {
|
||
await saveTokenToCache(steamId, tokenData, version, type);
|
||
}
|
||
|
||
console.log(`[请求完成] RequestId: ${requestId},SteamID: ${steamId} [${type}] 全流程完成 ✅(验证 → 签发 → Policy${typeConfig.cache ? ' → 缓存' : ''})`);
|
||
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(tokenData));
|
||
|
||
} catch (error) {
|
||
const msg = error && error.message ? error.message : 'Internal Server Error';
|
||
console.error(`[请求异常] 未捕获的错误: ${msg}`);
|
||
console.error(`[请求异常] 错误堆栈: ${error?.stack || '无堆栈信息'}`);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({ error: msg }));
|
||
}
|
||
}
|
||
|
||
const server = http.createServer(handleRequest);
|
||
server.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`[服务启动] 服务器已启动,监听端口: ${PORT},标准最大上传: ${MAX_STANDARD_UPLOAD_SIZE} 字节,Bug汇报最大上传: ${MAX_BUG_REPORT_UPLOAD_SIZE} 字节,多语言汇报最大上传: ${MAX_MULTILINGUAL_REPORT_UPLOAD_SIZE} 字节`);
|
||
})
|