#!/usr/bin/env node import { createHash } from 'node:crypto'; import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; const DEFAULT_PORT = process.env.LWLT_CDP_PORT || '9223'; function parseArgs(argv) { const args = { _: [] }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (!arg.startsWith('--')) { args._.push(arg); continue; } const key = arg.slice(2); const next = argv[i + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; i += 1; } } return args; } function usage() { return [ 'Usage:', ' node tools/browser_order_add_submit_intercept.mjs --call-submit --out reports/submit-intercept.json', '', 'Options:', ` --port Chrome DevTools port. Default: ${DEFAULT_PORT}`, ' --call-submit Required. Calls SubmitInfoForm() after installing the ajax intercept.', ' --out Write redacted intercept report.', ' --payload-out Write intercepted Act=DoInfoJH payload. Requires --unsafe-include-values.', ' --unsafe-include-values Keep real intercepted request values in output artifacts.', ' --help Show this help.', '', 'This tool monkey-patches jQuery.ajax before calling SubmitInfoForm(). It never lets DoInfoJH reach the network.' ].join('\n'); } function writeJson(path, data) { mkdirSync(dirname(resolve(path)), { recursive: true }); writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`); } function writeText(path, data) { mkdirSync(dirname(resolve(path)), { recursive: true }); writeFileSync(path, data); } async function getJson(url) { const res = await fetch(url); if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`); return res.json(); } async function getTarget(port) { const targets = await getJson(`http://127.0.0.1:${port}/json`); const target = targets.find((item) => item.type === 'page' && item.url.includes('ltjt.yunzhi.run')); if (!target) throw new Error(`No ltjt.yunzhi.run page target on port ${port}`); return target; } class CDP { constructor(wsUrl) { this.wsUrl = wsUrl; this.id = 0; this.pending = new Map(); } connect() { return new Promise((resolve, reject) => { this.ws = new WebSocket(this.wsUrl); this.ws.onopen = resolve; this.ws.onerror = (event) => reject(new Error(event.message || event.type || 'WebSocket error')); this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (!msg.id || !this.pending.has(msg.id)) return; const { resolve: ok, reject: fail, timer } = this.pending.get(msg.id); clearTimeout(timer); this.pending.delete(msg.id); msg.error ? fail(new Error(msg.error.message)) : ok(msg.result); }; }); } send(method, params = {}, timeoutMs = 30000) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Timeout: ${method}`)); }, timeoutMs); this.pending.set(id, { resolve, reject, timer }); }); } close() { this.ws?.close(); } } async function evaluate(cdp, expression, timeoutMs = 30000) { const result = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, timeoutMs); if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed'); return result.result.value; } function submitInterceptExpression() { return `(async () => { const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function findOrderWindow() { const queue = [window]; while (queue.length) { const win = queue.shift(); let doc; try { doc = win.document; } catch (err) { continue; } if (doc.querySelector('#ListForm') && doc.location.href.includes('/orders_add.asp')) return win; Array.from(win.frames).forEach((frame) => queue.push(frame)); } return null; } const orderWin = findOrderWindow(); if (!orderWin) return { ok: false, status: 'orders_add_form_not_found', blockers: ['orders_add_form_not_found'] }; const doc = orderWin.document; const form = doc.querySelector('#ListForm'); if (!form || form.elements.length < 800) { return { ok: false, status: 'orders_add_form_not_ready', blockers: ['orders_add_form_not_ready'], form_element_count: form?.elements?.length || 0 }; } const $ = orderWin.jQuery || orderWin.$; if (!$ || typeof $.ajax !== 'function') { return { ok: false, status: 'jquery_ajax_not_found', blockers: ['jquery_ajax_not_found'] }; } if (typeof orderWin.SubmitInfoForm !== 'function') { return { ok: false, status: 'SubmitInfoForm_not_found', blockers: ['SubmitInfoForm_not_found'] }; } const intercepted = []; const passthroughAjax = []; const alerts = []; const originalAjax = $.ajax; const originalAlert = orderWin.alert; const originalDialogAlert = $.dialog?.alert; function summarizePayload(data) { const text = String(data || ''); const body = text.startsWith('Act=DoInfoJH&') ? text : ''; const paramsText = body ? body.slice('Act=DoInfoJH&'.length) : text; const params = new URLSearchParams(paramsText); return { starts_with_DoInfoJH: body.startsWith('Act=DoInfoJH&'), byte_length: new Blob([text]).size, field_count: Array.from(params.keys()).length, field_names: Array.from(new Set(Array.from(params.keys()))).sort(), value_redacted: true }; } orderWin.alert = function interceptedAlert(message) { alerts.push({ type: 'alert', message_length: String(message || '').length, message_redacted: true }); return undefined; }; if ($.dialog && typeof $.dialog.alert === 'function') { $.dialog.alert = function interceptedDialogAlert(message) { alerts.push({ type: 'dialog.alert', message_length: String(message || '').length, message_redacted: true }); return undefined; }; } $.ajax = function ajaxIntercept(options, ...rest) { const config = typeof options === 'string' ? { url: options } : { ...(options || {}) }; const url = String(config.url || ''); const data = String(config.data || ''); const isSubmit = /\\/System\\/DAT\\/orders\\.asp/i.test(url) && data.startsWith('Act=DoInfoJH&'); if (!isSubmit) { passthroughAjax.push({ url_redacted: true, url_length: url.length, data_length: data.length }); return originalAjax.call(this, options, ...rest); } const summary = summarizePayload(data); intercepted.push({ url_redacted: '/System/DAT/orders.asp', method: config.type || config.method || 'GET', dataType: config.dataType || '', prevented_from_network: true, payload_summary: summary, payload: data }); return { readyState: 4, status: 0, statusText: 'intercepted_by_browser_order_add_submit_intercept', abort() {} }; }; let thrown = ''; try { orderWin.SubmitInfoForm(); } catch (err) { thrown = err.message || String(err); } await sleep(1000); $.ajax = originalAjax; orderWin.alert = originalAlert; if ($.dialog && typeof originalDialogAlert === 'function') $.dialog.alert = originalDialogAlert; const blockers = []; if (thrown) blockers.push('SubmitInfoForm threw before ajax: ' + thrown.slice(0, 160)); if (!intercepted.length) blockers.push('SubmitInfoForm did not attempt a DoInfoJH ajax submit while intercepted'); return { ok: true, status: blockers.length ? 'submit_intercept_blocked' : 'submit_intercept_captured', page: { url: doc.location.href, title: doc.title, form_element_count: form.elements.length }, intercepted_submit_count: intercepted.length, intercepted_submits: intercepted, passthrough_ajax_count: passthroughAjax.length, alerts, blockers, submit_safety: { live_submit_attempted: false, DoInfoJH_network_prevented: true, note: 'jQuery.ajax was patched before SubmitInfoForm() was called; intercepted DoInfoJH requests were not sent.' } }; })()`; } function redactReport(output, includeValues) { const report = { ...output, intercepted_submits: (output.intercepted_submits || []).map((item) => { const payload = item.payload || ''; const sha256 = payload ? createHash('sha256').update(payload).digest('hex') : ''; const redacted = { ...item, payload_sha256: sha256, payload_redacted: !includeValues }; if (!includeValues) delete redacted.payload; return redacted; }) }; return report; } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { console.log(usage()); return; } if (!args['call-submit']) { console.error(usage()); process.exit(1); } const target = await getTarget(args.port || DEFAULT_PORT); const cdp = new CDP(target.webSocketDebuggerUrl); await cdp.connect(); try { await cdp.send('Runtime.enable'); const output = await evaluate(cdp, submitInterceptExpression(), 30000); output.generated_at = new Date().toISOString(); const includeValues = Boolean(args['unsafe-include-values']); const report = redactReport(output, includeValues); if (args.out) writeJson(args.out, report); if (args['payload-out']) { if (!includeValues) { console.error('Refusing to write payload-out without --unsafe-include-values.'); } else if (output.blockers?.length) { console.error('Refusing to write payload-out because submit intercept is blocked.'); } else { writeText(args['payload-out'], output.intercepted_submits?.[0]?.payload || ''); } } console.log(JSON.stringify({ status: output.status, blockers: output.blockers?.length || 0, intercepted_submit_count: output.intercepted_submit_count || 0, alerts: output.alerts?.length || 0, out: args.out || '', payload_out: includeValues && !(output.blockers?.length) ? (args['payload-out'] || '') : '' }, null, 2)); if (output.blockers?.length) process.exit(2); } finally { cdp.close(); } } main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });