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;