const https = require('https'); const { requireAuth, jsonError, getClientIp } = require('./auth-middleware'); const ONE2EDIT_API = 'https://oliver.one2edit.com/v3/Api.php'; const SERVICE_USERNAME = () => process.env.SERVICE_USERNAME; const SERVICE_PASSWORD = () => process.env.SERVICE_PASSWORD; const UPSTREAM_MS = parseInt(process.env.UPSTREAM_TIMEOUT_MS, 10) || 30000; const IS_PROD = process.env.NODE_ENV === 'production'; const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; function injectCredentials(params, userExternSessionId) { params.delete('authUsername'); params.delete('authPassword'); if (params.has('sessionId')) { // Anti-hijack: the sessionId must be the one we issued to this user if (userExternSessionId && params.get('sessionId') !== userExternSessionId) { return false; } // sessionId present — One2Edit accepts it directly, no service-account creds needed return true; } if (!params.has('authDomain')) params.set('authDomain', 'local'); params.set('authUsername', SERVICE_USERNAME()); params.set('authPassword', SERVICE_PASSWORD()); return true; } function logParams(method, params) { const p = new URLSearchParams(params); p.set('authPassword', '***'); if (IS_PROD) { if (p.has('authUsername')) p.set('authUsername', '***'); if (p.has('username')) p.set('username', '***'); } console.log(`Proxy ${method}:`, p.toString().substring(0, 200)); } function handleApiResponse(res) { return (apiRes) => { if (apiRes.statusCode === 301 || apiRes.statusCode === 302) { res.writeHead(401, { 'Content-Type': 'text/xml;charset=UTF-8', ...CORS_HEADERS }); res.end('Authentication failed'); return; } const chunks = []; apiRes.on('data', c => chunks.push(c)); apiRes.on('end', () => { const ct = apiRes.headers['content-type'] || 'text/xml;charset=UTF-8'; res.writeHead(apiRes.statusCode, { 'Content-Type': ct, ...CORS_HEADERS }); res.end(Buffer.concat(chunks)); }); }; } function handleProxyError(res) { return (err) => { console.error('Proxy error:', err.message); if (res.headersSent) return; res.writeHead(502, { 'Content-Type': 'text/plain', ...CORS_HEADERS }); res.end('Error connecting to API'); }; } function attachTimeout(apiReq, res) { apiReq.setTimeout(UPSTREAM_MS, () => { apiReq.destroy(new Error('Upstream timeout')); if (res.headersSent) return; res.writeHead(504, { 'Content-Type': 'text/plain', ...CORS_HEADERS }); res.end('Upstream request timed out'); }); } function proxyGet(queryString, res, userExternSessionId) { const params = new URLSearchParams(queryString); if (!injectCredentials(params, userExternSessionId)) { res.writeHead(403, { 'Content-Type': 'text/xml;charset=UTF-8', ...CORS_HEADERS }); res.end('Session mismatch'); return; } logParams('GET', params); const apiReq = https.get(`${ONE2EDIT_API}?${params}`, handleApiResponse(res)); apiReq.on('error', handleProxyError(res)); attachTimeout(apiReq, res); } function proxyPost(body, res, userExternSessionId) { const params = new URLSearchParams(body); if (!injectCredentials(params, userExternSessionId)) { res.writeHead(403, { 'Content-Type': 'text/xml;charset=UTF-8', ...CORS_HEADERS }); res.end('Session mismatch'); return; } logParams('POST', params); const newBody = params.toString(); const apiUrl = new URL(ONE2EDIT_API); const options = { hostname: apiUrl.hostname, path: apiUrl.pathname, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(newBody) }, }; const apiReq = https.request(options, handleApiResponse(res)); apiReq.on('error', handleProxyError(res)); attachTimeout(apiReq, res); apiReq.write(newBody); apiReq.end(); } function handleProxy(req, res, parsedUrl) { // Require authenticated portal session if (!requireAuth(req, res)) return; // Reject limited sessions (must change password) if (req.user.must_change_password) { const xmlErr = 'Password change required'; res.writeHead(403, { 'Content-Type': 'text/xml;charset=UTF-8', ...CORS_HEADERS }); res.end(xmlErr); return; } const userExternSessionId = req.session.one2edit_extern_session_id; if (req.method === 'POST') { const chunks = []; req.on('data', c => chunks.push(c)); req.on('end', () => proxyPost(Buffer.concat(chunks).toString(), res, userExternSessionId)); } else { proxyGet(parsedUrl.search.slice(1), res, userExternSessionId); } } module.exports = { handleProxy, CORS_HEADERS };