Files
LWLT-AI/tools/browser_order_add_submit_intercept.mjs
2026-07-13 19:57:46 +08:00

308 lines
10 KiB
JavaScript

#!/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 <number> Chrome DevTools port. Default: ${DEFAULT_PORT}`,
' --call-submit Required. Calls SubmitInfoForm() after installing the ajax intercept.',
' --out <path> Write redacted intercept report.',
' --payload-out <path> 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);
});