const { spawn } = require('child_process'); const STATUS_MARKER = '__CODEX_HTTP_STATUS__:'; const DEFAULT_TIMEOUT_MS = 30000; const MAX_CAPTURE_LENGTH = 1024 * 1024; const DATA_FLAGS = new Set(['--data', '--data-raw', '--data-binary', '-d']); const STRIP_VALUE_FLAGS = new Set(['--write-out', '-w', '--output', '-o', '--dump-header', '-D']); const STRIP_BOOLEAN_FLAGS = new Set([ '--silent', '-s', '--show-error', '-S', '--include', '-i', '--verbose', '-v', '--remote-name', '-O', '--remote-header-name', '-J', '--fail', '-f', '--fail-with-body', ]); const TOKEN_REGEX = /__(?:PROFILE|SMS)_[A-Z0-9_]+__/g; function createExecutionError(message, extra = {}) { const error = new Error(message); Object.assign(error, extra); return error; } function skipShellIndentation(input, index) { let cursor = index; while (cursor < input.length && /[\t \f\v\u00a0]/.test(input[cursor])) { cursor += 1; } return cursor; } function normalizeCommand(command) { const input = String(command || '') .replace(/\r\n/g, '\n') .replace(/\r/g, '\n'); let output = ''; let quote = null; for (let index = 0; index < input.length; index += 1) { const char = input[index]; if (quote === '\'') { output += char; if (char === '\'') { quote = null; } continue; } if (quote === '"') { output += char; if (char === '\\' && index + 1 < input.length) { output += input[index + 1]; index += 1; continue; } if (char === '"') { quote = null; } continue; } if (char === '\'' || char === '"') { quote = char; output += char; continue; } if (char === '\\') { const nextChar = input[index + 1]; if (nextChar === '\n') { output += ' '; index = skipShellIndentation(input, index + 2) - 1; continue; } if (nextChar === 'n') { output += ' '; index = skipShellIndentation(input, index + 2) - 1; continue; } if (nextChar === 'r' && input[index + 2] === 'n') { output += ' '; index = skipShellIndentation(input, index + 3) - 1; continue; } } output += char; } return output.trim(); } function tokenizeCurlCommand(command) { const input = normalizeCommand(command); const tokens = []; let current = ''; let quote = null; let escaping = false; for (let index = 0; index < input.length; index += 1) { const char = input[index]; if (escaping) { current += char; escaping = false; continue; } if (quote === '\'') { if (char === '\'') { quote = null; } else { current += char; } continue; } if (quote === '"') { if (char === '"') { quote = null; continue; } if (char === '\\') { const nextChar = input[index + 1]; if (nextChar) { current += nextChar; index += 1; continue; } } current += char; continue; } if (char === '\\') { escaping = true; continue; } if (char === '\'' || char === '"') { quote = char; continue; } if (/\s/.test(char)) { if (current) { tokens.push(current); current = ''; } continue; } current += char; } if (escaping) { current += '\\'; } if (quote) { throw createExecutionError('Stored cURL contains an unterminated quoted value.', { code: 'INVALID_CURL_TEMPLATE', }); } if (current) { tokens.push(current); } return tokens; } function parseCurlCommand(command) { const tokens = tokenizeCurlCommand(command); if (tokens.length === 0 || tokens[0] !== 'curl') { throw createExecutionError('Stored cURL template must start with "curl".', { code: 'INVALID_CURL_TEMPLATE', }); } return { command: 'curl', args: tokens.slice(1), }; } function replaceTokensInString(value, tokenValues = {}) { let output = String(value || ''); const entries = Object.entries(tokenValues).sort((left, right) => right[0].length - left[0].length); entries.forEach(([token, replacement]) => { if (!token) return; output = output.split(token).join(String(replacement ?? '')); }); return output; } function replaceTokensInJsonValue(value, tokenValues = {}) { if (Array.isArray(value)) { return value.map((entry) => replaceTokensInJsonValue(entry, tokenValues)); } if (value && typeof value === 'object') { return Object.entries(value).reduce((accumulator, [key, entry]) => { accumulator[key] = replaceTokensInJsonValue(entry, tokenValues); return accumulator; }, {}); } if (typeof value === 'string') { return replaceTokensInString(value, tokenValues); } return value; } function normalizeJsonFormattingEscapes(value) { const input = String(value || '') .replace(/\r\n/g, '\n') .replace(/\r/g, '\n'); let output = ''; let inString = false; let escaping = false; for (let index = 0; index < input.length; index += 1) { const char = input[index]; if (inString) { output += char; if (escaping) { escaping = false; continue; } if (char === '\\') { escaping = true; continue; } if (char === '"') { inString = false; } continue; } if (char === '"') { inString = true; output += char; continue; } if (char === '\\') { const nextChar = input[index + 1]; if (nextChar === 'n') { output += '\n'; index += 1; continue; } if (nextChar === 'r' && input[index + 2] === 'n') { output += '\n'; index += 2; continue; } if (nextChar === 'r') { output += '\n'; index += 1; continue; } if (nextChar === 't') { output += '\t'; index += 1; continue; } } output += char; } return output; } function parseJsonLikeArgument(value) { const trimmed = String(value || '').trim(); if ( !trimmed || !( (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')) ) ) { return null; } try { return JSON.parse(trimmed); } catch { const normalized = normalizeJsonFormattingEscapes(trimmed); if (normalized === trimmed) { return null; } try { return JSON.parse(normalized); } catch { return null; } } } function hydrateDataArgument(rawArgument, tokenValues = {}) { const trimmed = String(rawArgument || '').trim(); if (!trimmed) return ''; const parsedJson = parseJsonLikeArgument(trimmed); if (parsedJson !== null) { return JSON.stringify(replaceTokensInJsonValue(parsedJson, tokenValues)); } return replaceTokensInString(rawArgument, tokenValues); } function hydrateCurlArgs(args = [], tokenValues = {}) { const hydratedArgs = []; for (let index = 0; index < args.length; index += 1) { const argument = args[index]; if (DATA_FLAGS.has(argument) && index + 1 < args.length) { hydratedArgs.push(argument); hydratedArgs.push(hydrateDataArgument(args[index + 1], tokenValues)); index += 1; continue; } const dataFlagWithValue = Array.from(DATA_FLAGS).find((flag) => argument.startsWith(`${flag}=`)); if (dataFlagWithValue) { const rawValue = argument.slice(dataFlagWithValue.length + 1); hydratedArgs.push(`${dataFlagWithValue}=${hydrateDataArgument(rawValue, tokenValues)}`); continue; } hydratedArgs.push(replaceTokensInString(argument, tokenValues)); } return hydratedArgs; } function normalizeExecutionArgs(args = []) { const normalizedArgs = []; for (let index = 0; index < args.length; index += 1) { const argument = args[index]; if (STRIP_BOOLEAN_FLAGS.has(argument)) { continue; } const stripValueFlag = Array.from(STRIP_VALUE_FLAGS).find((flag) => argument === flag || argument.startsWith(`${flag}=`)); if (stripValueFlag) { if (argument === stripValueFlag) { index += 1; } continue; } normalizedArgs.push(argument); } normalizedArgs.push( '--silent', '--show-error', '--output', '-', '--write-out', `\n${STATUS_MARKER}%{http_code}`, ); return normalizedArgs; } function findUnresolvedTokens(args = []) { const unresolved = new Set(); args.forEach((argument) => { const matches = String(argument || '').match(TOKEN_REGEX) || []; matches.forEach((token) => unresolved.add(token)); }); return Array.from(unresolved); } function appendChunk(buffer, chunk) { const nextValue = `${buffer}${chunk}`; if (nextValue.length <= MAX_CAPTURE_LENGTH) return nextValue; return nextValue.slice(nextValue.length - MAX_CAPTURE_LENGTH); } function parseCurlStdout(stdout = '') { const marker = `\n${STATUS_MARKER}`; const markerIndex = stdout.lastIndexOf(marker); if (markerIndex < 0) { return { statusCode: 0, body: stdout, }; } const statusText = stdout.slice(markerIndex + marker.length).trim(); const parsedStatusCode = Number.parseInt(statusText, 10); return { statusCode: Number.isFinite(parsedStatusCode) ? parsedStatusCode : 0, body: stdout.slice(0, markerIndex), }; } function parseResponseBody(body = '') { const normalizedBody = String(body || '').trim(); if (!normalizedBody) return ''; try { return JSON.parse(normalizedBody); } catch { return body; } } function executeParsedCurl(command, args = [], options = {}) { const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS; return new Promise((resolve, reject) => { const child = spawn(command, normalizeExecutionArgs(args), { shell: false, env: process.env, }); let stdout = ''; let stderr = ''; let timedOut = false; let settled = false; const timeout = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => { if (!settled) child.kill('SIGKILL'); }, 1500).unref(); }, timeoutMs); child.stdout.on('data', (chunk) => { stdout = appendChunk(stdout, chunk.toString()); }); child.stderr.on('data', (chunk) => { stderr = appendChunk(stderr, chunk.toString()); }); child.on('error', (error) => { if (settled) return; settled = true; clearTimeout(timeout); reject(createExecutionError(`Failed to start curl: ${error.message}`, { code: 'CURL_EXECUTION_START_FAILED', })); }); child.on('close', (exitCode, signal) => { if (settled) return; settled = true; clearTimeout(timeout); const parsedStdout = parseCurlStdout(stdout); const response = parseResponseBody(parsedStdout.body); if (timedOut) { reject(createExecutionError(`curl execution timed out after ${timeoutMs}ms`, { code: 'CURL_EXECUTION_TIMEOUT', details: { timeoutMs, stderr: stderr.trim(), statusCode: parsedStdout.statusCode, }, })); return; } if (exitCode !== 0) { reject(createExecutionError('curl execution failed', { code: 'CURL_EXECUTION_FAILED', details: { exitCode, signal, stderr: stderr.trim(), statusCode: parsedStdout.statusCode, response, }, })); return; } resolve({ success: parsedStdout.statusCode >= 200 && parsedStdout.statusCode < 300, exitCode, signal, statusCode: parsedStdout.statusCode, response, stderr: stderr.trim(), }); }); }); } async function executeTemplatedCurl(curlTemplate, tokenValues = {}, options = {}) { const parsed = parseCurlCommand(curlTemplate); const hydratedArgs = hydrateCurlArgs(parsed.args, tokenValues); const unresolvedTokens = findUnresolvedTokens(hydratedArgs); if (unresolvedTokens.length > 0) { throw createExecutionError('Stored cURL still contains unresolved execution tokens.', { code: 'UNRESOLVED_CURL_TOKENS', details: { unresolvedTokens, }, }); } return executeParsedCurl(parsed.command, hydratedArgs, options); } module.exports = { executeTemplatedCurl, parseCurlCommand, };