cangku/backend/src/api/tmallProxy.js

138 lines
4.2 KiB
JavaScript

const express = require('express');
const axios = require('axios');
const authMiddleware = require('../middleware/auth');
const router = express.Router();
const DEFAULT_PROXY_BASE_URL = 'http://localhost:5252';
const DEFAULT_PROXY_TIMEOUT_MS = 10 * 60 * 1000;
const PROXY_USER_HEADER = 'x-commerceinsight-proxy-user';
const PROXY_SECRET_HEADER = 'x-commerceinsight-proxy-secret';
function getProxyBaseUrl() {
const baseUrl = String(process.env.TMALL_DOTNET_PROXY_BASE_URL || DEFAULT_PROXY_BASE_URL).trim();
return baseUrl.replace(/\/+$/, '');
}
function getProxyTimeoutMs() {
const timeoutMs = Number(process.env.TMALL_DOTNET_PROXY_TIMEOUT_MS);
if (Number.isFinite(timeoutMs) && timeoutMs >= 1000) {
return timeoutMs;
}
return DEFAULT_PROXY_TIMEOUT_MS;
}
function buildTargetUrl(req) {
const upstreamBaseUrl = new URL(`${getProxyBaseUrl()}/`);
const queryStringIndex = req.originalUrl.indexOf('?');
const queryString = queryStringIndex >= 0 ? req.originalUrl.slice(queryStringIndex) : '';
const upstreamBasePath = upstreamBaseUrl.pathname === '/' ? '' : upstreamBaseUrl.pathname.replace(/\/$/, '');
upstreamBaseUrl.pathname = `${upstreamBasePath}${req.baseUrl}${req.path}`;
upstreamBaseUrl.search = queryString.startsWith('?') ? queryString.slice(1) : queryString;
return upstreamBaseUrl.toString();
}
function buildProxyHeaders(req) {
const proxySharedSecret = String(process.env.TMALL_DOTNET_PROXY_SHARED_SECRET || process.env.CUBEJS_SECRET || '').trim();
const headers = {
accept: req.headers.accept,
'accept-language': req.headers['accept-language'],
authorization: req.headers.authorization,
cookie: req.headers.cookie,
'content-type': req.headers['content-type'],
'user-agent': req.headers['user-agent'],
'x-forwarded-for': req.ip,
'x-forwarded-host': req.get('host'),
'x-forwarded-prefix': req.baseUrl || '',
'x-forwarded-proto': req.protocol,
};
if (proxySharedSecret && req.user) {
headers[PROXY_SECRET_HEADER] = proxySharedSecret;
headers[PROXY_USER_HEADER] = Buffer.from(JSON.stringify(req.user), 'utf8').toString('base64url');
}
return Object.fromEntries(
Object.entries(headers).filter(([, value]) => value !== undefined && value !== null && value !== '')
);
}
function hasRequestBody(method) {
return !['GET', 'HEAD'].includes(String(method || '').toUpperCase());
}
function isJsonContentType(contentType) {
const normalized = String(contentType || '').toLowerCase();
return normalized.includes('application/json') || normalized.includes('+json');
}
function tryParseJsonPayload(buffer) {
if (!buffer || buffer.length === 0) {
return null;
}
try {
return JSON.parse(buffer.toString('utf8'));
} catch (_error) {
return null;
}
}
function normalizeJsonPayload(payload) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return payload;
}
if (payload.success === false && !payload.error && typeof payload.message === 'string' && payload.message) {
return {
...payload,
error: payload.message,
};
}
return payload;
}
router.use(authMiddleware);
router.use(async (req, res) => {
try {
const response = await axios({
method: req.method,
url: buildTargetUrl(req),
headers: buildProxyHeaders(req),
data: hasRequestBody(req.method) ? req.body : undefined,
timeout: getProxyTimeoutMs(),
responseType: 'arraybuffer',
transformResponse: [(data) => data],
validateStatus: () => true,
maxBodyLength: Infinity,
maxContentLength: Infinity,
});
const contentType = response.headers['content-type'] || '';
if (isJsonContentType(contentType)) {
const payload = tryParseJsonPayload(response.data);
if (payload !== null) {
return res.status(response.status).json(normalizeJsonPayload(payload));
}
}
if (contentType) {
res.set('Content-Type', contentType);
}
return res.status(response.status).send(Buffer.from(response.data || []));
} catch (error) {
return res.status(502).json({
success: false,
error: 'Tmall .NET service unavailable',
message: error.message || 'Proxy request failed',
});
}
});
router.prewarmHotBoards = async () => null;
module.exports = router;