475 lines
17 KiB
JavaScript
475 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const PORT = process.env.LWLT_CDP_PORT || '9223';
|
|
const TARGET_ID = process.env.LWLT_TARGET_ID || '';
|
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
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() {
|
|
const targets = await getJson(`http://127.0.0.1:${PORT}/json`);
|
|
const pages = targets.filter((target) => target.type === 'page');
|
|
if (TARGET_ID) {
|
|
const found = pages.find((target) => target.id.startsWith(TARGET_ID));
|
|
if (!found) throw new Error(`No page target matching ${TARGET_ID}`);
|
|
return found;
|
|
}
|
|
const ltjt = pages.find((target) => target.url.includes('ltjt.yunzhi.run'));
|
|
if (!ltjt) throw new Error(`No ltjt.yunzhi.run page target on port ${PORT}`);
|
|
return ltjt;
|
|
}
|
|
|
|
class CDP {
|
|
constructor(wsUrl) {
|
|
this.wsUrl = wsUrl;
|
|
this.id = 0;
|
|
this.pending = new Map();
|
|
this.handlers = 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)) {
|
|
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);
|
|
return;
|
|
}
|
|
if (msg.method && this.handlers.has(msg.method)) {
|
|
for (const handler of this.handlers.get(msg.method)) handler(msg.params || {}, msg);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
send(method, params = {}, timeoutMs = 15000) {
|
|
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 });
|
|
});
|
|
}
|
|
|
|
on(method, handler) {
|
|
if (!this.handlers.has(method)) this.handlers.set(method, new Set());
|
|
this.handlers.get(method).add(handler);
|
|
}
|
|
|
|
close() {
|
|
this.ws.close();
|
|
}
|
|
}
|
|
|
|
function expressionForMap() {
|
|
return `(() => {
|
|
const text = (value, max = 240) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, max);
|
|
const attr = (el, name) => el.getAttribute(name) || '';
|
|
const compactEl = (el) => ({
|
|
tag: el.tagName.toLowerCase(),
|
|
text: text(el.innerText || el.textContent || el.value || el.title || el.alt || '', 160),
|
|
id: el.id || '',
|
|
name: attr(el, 'name'),
|
|
className: attr(el, 'class'),
|
|
href: el.href || attr(el, 'href'),
|
|
src: el.src || attr(el, 'src'),
|
|
action: el.action || attr(el, 'action'),
|
|
method: attr(el, 'method'),
|
|
target: attr(el, 'target'),
|
|
onclick: attr(el, 'onclick'),
|
|
title: attr(el, 'title')
|
|
});
|
|
const endpointMatches = (source) => {
|
|
const found = new Set();
|
|
const re = /(?:['"])([^'"]+\\.(?:asp|aspx)(?:\\?[^'"]*)?)(?:['"])/ig;
|
|
let match;
|
|
while ((match = re.exec(source || ''))) found.add(match[1]);
|
|
return Array.from(found).slice(0, 120);
|
|
};
|
|
const collectDoc = (doc, label) => {
|
|
const pick = (selector, limit = 300) => Array.from(doc.querySelectorAll(selector)).slice(0, limit).map(compactEl);
|
|
const inlineScripts = Array.from(doc.querySelectorAll('script:not([src])')).map((script) => script.textContent || '').join('\\n');
|
|
const tables = Array.from(doc.querySelectorAll('table')).slice(0, 80).map((table) => {
|
|
const headers = Array.from(table.querySelectorAll('th')).map((th) => text(th.innerText || th.textContent, 80)).filter(Boolean);
|
|
const firstRowHeaders = headers.length ? headers : Array.from(table.querySelectorAll('tr')).slice(0, 1).flatMap((tr) => Array.from(tr.children).map((cell) => text(cell.innerText || cell.textContent, 80))).filter(Boolean);
|
|
return {
|
|
id: table.id || '',
|
|
className: attr(table, 'class'),
|
|
rows: table.querySelectorAll('tr').length,
|
|
headers: headers.length || table.querySelectorAll('tr').length <= 2 ? firstRowHeaders.slice(0, 40) : []
|
|
};
|
|
});
|
|
return {
|
|
label,
|
|
title: doc.title,
|
|
url: doc.location.href,
|
|
readyState: doc.readyState,
|
|
scripts: pick('script[src]', 120).map((el) => el.src),
|
|
inlineEndpointRefs: endpointMatches(inlineScripts),
|
|
stylesheets: pick('link[href]', 120).map((el) => el.href),
|
|
frames: pick('frame,iframe', 80),
|
|
links: pick('a[href],a[onclick],area[href],area[onclick]', 500),
|
|
forms: pick('form', 80),
|
|
buttons: pick('input[type=button],input[type=submit],button', 200),
|
|
tables,
|
|
inputs: pick('input,select,textarea', 260).map((el) => ({
|
|
tag: el.tag,
|
|
type: el.tag === 'input' ? (doc.getElementById(el.id)?.type || '') : el.tag,
|
|
id: el.id,
|
|
name: el.name,
|
|
text: el.text,
|
|
title: el.title,
|
|
valueRedacted: el.name || el.id ? '[redacted]' : ''
|
|
}))
|
|
};
|
|
};
|
|
const docs = [collectDoc(document, 'top')];
|
|
for (const frame of Array.from(window.frames)) {
|
|
try {
|
|
if (frame.document) docs.push(collectDoc(frame.document, frame.name || frame.frameElement?.id || 'frame'));
|
|
} catch (err) {
|
|
docs.push({ label: frame.name || 'cross-origin-frame', error: err.message });
|
|
}
|
|
}
|
|
return docs;
|
|
})()`;
|
|
}
|
|
|
|
async function evaluate(cdp, expression) {
|
|
const result = await cdp.send('Runtime.evaluate', {
|
|
expression,
|
|
returnByValue: true,
|
|
awaitPromise: true
|
|
}, 20000);
|
|
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Evaluation failed');
|
|
return result.result.value;
|
|
}
|
|
|
|
function sanitizePostData(postData = '') {
|
|
return String(postData)
|
|
.replace(/(UserName|UserPwd|Code|session_id|password|pwd|PassWord)=([^&]*)/ig, '$1=[redacted]');
|
|
}
|
|
|
|
async function commandMap(cdp) {
|
|
await cdp.send('Runtime.enable');
|
|
return evaluate(cdp, expressionForMap());
|
|
}
|
|
|
|
async function commandCookies(cdp) {
|
|
await cdp.send('Network.enable');
|
|
const { cookies } = await cdp.send('Network.getCookies', { urls: ['https://ltjt.yunzhi.run/'] });
|
|
return cookies.map((cookie) => ({
|
|
name: cookie.name,
|
|
domain: cookie.domain,
|
|
path: cookie.path,
|
|
expires: cookie.expires,
|
|
size: cookie.size,
|
|
httpOnly: cookie.httpOnly,
|
|
secure: cookie.secure,
|
|
sameSite: cookie.sameSite || ''
|
|
}));
|
|
}
|
|
|
|
async function commandCapture(cdp, durationMs = 8000, reload = false) {
|
|
await cdp.send('Network.enable');
|
|
const requests = new Map();
|
|
cdp.on('Network.requestWillBeSent', (params) => {
|
|
const req = params.request || {};
|
|
requests.set(params.requestId, {
|
|
requestId: params.requestId,
|
|
type: params.type,
|
|
frameId: params.frameId,
|
|
documentURL: params.documentURL,
|
|
method: req.method,
|
|
url: req.url,
|
|
postData: req.postData || '',
|
|
initiatorType: params.initiator?.type || '',
|
|
timestamp: params.timestamp,
|
|
status: null,
|
|
mimeType: '',
|
|
responseUrl: ''
|
|
});
|
|
});
|
|
cdp.on('Network.responseReceived', (params) => {
|
|
const item = requests.get(params.requestId);
|
|
if (!item) return;
|
|
item.status = params.response?.status ?? null;
|
|
item.mimeType = params.response?.mimeType || '';
|
|
item.responseUrl = params.response?.url || '';
|
|
});
|
|
if (reload) await cdp.send('Page.reload');
|
|
await sleep(durationMs);
|
|
return Array.from(requests.values())
|
|
.filter((item) => item.url.includes('ltjt.yunzhi.run'))
|
|
.map((item) => ({
|
|
...item,
|
|
postData: item.postData ? sanitizePostData(item.postData) : ''
|
|
}));
|
|
}
|
|
|
|
async function waitForFrameUrl(cdp, urlNeedle, timeoutMs = 15000) {
|
|
const started = Date.now();
|
|
while (Date.now() - started < timeoutMs) {
|
|
const state = await evaluate(cdp, `(() => {
|
|
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
|
try {
|
|
const doc = frame && frame.contentWindow && frame.contentWindow.document;
|
|
return {
|
|
url: doc ? doc.location.href : '',
|
|
readyState: doc ? doc.readyState : '',
|
|
title: doc ? doc.title : ''
|
|
};
|
|
} catch (err) {
|
|
return { error: err.message };
|
|
}
|
|
})()`).catch((err) => ({ error: err.message }));
|
|
if (state.url && state.url.includes(urlNeedle) && state.readyState === 'complete') return state;
|
|
await sleep(500);
|
|
}
|
|
return evaluate(cdp, `(() => {
|
|
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
|
try {
|
|
const doc = frame && frame.contentWindow && frame.contentWindow.document;
|
|
return {
|
|
url: doc ? doc.location.href : '',
|
|
readyState: doc ? doc.readyState : '',
|
|
title: doc ? doc.title : ''
|
|
};
|
|
} catch (err) {
|
|
return { error: err.message };
|
|
}
|
|
})()`);
|
|
}
|
|
|
|
async function commandNavFrame(cdp, path, durationMs = 5000) {
|
|
await cdp.send('Runtime.enable');
|
|
await cdp.send('Network.enable');
|
|
const requests = [];
|
|
cdp.on('Network.requestWillBeSent', (params) => {
|
|
const req = params.request || {};
|
|
if (!req.url || !req.url.includes('ltjt.yunzhi.run')) return;
|
|
requests.push({
|
|
requestId: params.requestId,
|
|
type: params.type,
|
|
method: req.method,
|
|
url: req.url,
|
|
postData: req.postData || '',
|
|
initiatorType: params.initiator?.type || '',
|
|
status: null,
|
|
mimeType: ''
|
|
});
|
|
});
|
|
cdp.on('Network.responseReceived', (params) => {
|
|
const item = requests.find((row) => row.requestId === params.requestId);
|
|
if (!item) return;
|
|
item.status = params.response?.status ?? null;
|
|
item.mimeType = params.response?.mimeType || '';
|
|
});
|
|
|
|
const absolute = new URL(path, 'https://ltjt.yunzhi.run').href;
|
|
await evaluate(cdp, `(async () => {
|
|
const frame = document.getElementById('Iframe_Home') || document.querySelector('iframe[name="MainIframe"], iframe');
|
|
if (!frame) throw new Error('Main iframe not found');
|
|
frame.src = ${JSON.stringify(absolute)};
|
|
return true;
|
|
})()`);
|
|
const finalFrame = await waitForFrameUrl(cdp, new URL(absolute).pathname, Math.max(12000, durationMs));
|
|
await sleep(durationMs);
|
|
const docs = await commandMap(cdp);
|
|
return {
|
|
navigatedTo: absolute,
|
|
finalFrame,
|
|
requests: requests.map((item) => ({
|
|
...item,
|
|
postData: item.postData ? sanitizePostData(item.postData) : ''
|
|
})),
|
|
docs
|
|
};
|
|
}
|
|
|
|
async function commandClickMenu(cdp, label, durationMs = 5000) {
|
|
await cdp.send('Runtime.enable');
|
|
await cdp.send('Network.enable');
|
|
await cdp.send('Page.enable');
|
|
const requests = [];
|
|
const dialogs = [];
|
|
cdp.on('Page.javascriptDialogOpening', (params) => {
|
|
dialogs.push({
|
|
type: params.type,
|
|
message: params.message,
|
|
url: params.url
|
|
});
|
|
cdp.send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
|
|
});
|
|
cdp.on('Network.requestWillBeSent', (params) => {
|
|
const req = params.request || {};
|
|
if (!req.url || !req.url.includes('ltjt.yunzhi.run')) return;
|
|
requests.push({
|
|
requestId: params.requestId,
|
|
type: params.type,
|
|
method: req.method,
|
|
url: req.url,
|
|
postData: req.postData || '',
|
|
initiatorType: params.initiator?.type || '',
|
|
status: null,
|
|
mimeType: ''
|
|
});
|
|
});
|
|
cdp.on('Network.responseReceived', (params) => {
|
|
const item = requests.find((row) => row.requestId === params.requestId);
|
|
if (!item) return;
|
|
item.status = params.response?.status ?? null;
|
|
item.mimeType = params.response?.mimeType || '';
|
|
});
|
|
|
|
const currentPage = await evaluate(cdp, `(() => ({ title: document.title, url: location.href }))()`);
|
|
if (/login/i.test(currentPage.url) || /云智办公i-5.1/.test(currentPage.title || '')) {
|
|
return {
|
|
label,
|
|
clickResult: { ok: false, reason: 'currently on login page', currentPage },
|
|
dialogs,
|
|
requests: [],
|
|
docs: await commandMap(cdp).catch((error) => ([{ error: error.message }]))
|
|
};
|
|
}
|
|
|
|
const clickResult = await evaluate(cdp, `(() => {
|
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
const label = ${JSON.stringify(label)};
|
|
const links = Array.from(document.querySelectorAll('a'));
|
|
const found = links.find((link) => normalize(link.innerText || link.textContent) === label);
|
|
if (!found) return { ok: false, reason: 'link not found', available: links.map((link) => normalize(link.innerText || link.textContent)).filter(Boolean).slice(0, 120) };
|
|
found.click();
|
|
return {
|
|
ok: true,
|
|
text: normalize(found.innerText || found.textContent),
|
|
href: found.href || found.getAttribute('href') || '',
|
|
onclick: found.getAttribute('onclick') || ''
|
|
};
|
|
})()`);
|
|
await sleep(durationMs);
|
|
const docs = await commandMap(cdp).catch((error) => ([{ error: error.message }]));
|
|
return {
|
|
label,
|
|
clickResult,
|
|
dialogs,
|
|
requests: requests.map((item) => ({
|
|
...item,
|
|
postData: item.postData ? sanitizePostData(item.postData) : ''
|
|
})),
|
|
docs
|
|
};
|
|
}
|
|
|
|
function summarizeDocs(docs) {
|
|
return docs.map((doc) => ({
|
|
label: doc.label,
|
|
title: doc.title,
|
|
url: doc.url,
|
|
scripts: doc.scripts || [],
|
|
inlineEndpointRefs: doc.inlineEndpointRefs || [],
|
|
forms: (doc.forms || []).map((form) => ({
|
|
id: form.id,
|
|
name: form.name,
|
|
action: form.action,
|
|
method: form.method,
|
|
text: form.text
|
|
})),
|
|
inputs: (doc.inputs || []).map((input) => ({
|
|
tag: input.tag,
|
|
type: input.type,
|
|
id: input.id,
|
|
name: input.name,
|
|
text: input.text,
|
|
title: input.title
|
|
})),
|
|
buttons: (doc.buttons || []).map((button) => ({
|
|
text: button.text,
|
|
id: button.id,
|
|
name: button.name,
|
|
onclick: button.onclick,
|
|
title: button.title
|
|
})),
|
|
links: (doc.links || []).map((link) => ({
|
|
text: link.text,
|
|
href: link.href,
|
|
onclick: link.onclick,
|
|
title: link.title
|
|
})),
|
|
tables: doc.tables || []
|
|
}));
|
|
}
|
|
|
|
async function commandBatchMenus(cdp, labels, durationMs = 4500) {
|
|
const results = [];
|
|
for (const label of labels) {
|
|
const before = await commandMap(cdp).catch(() => []);
|
|
const result = await commandClickMenu(cdp, label, durationMs);
|
|
const afterDocs = summarizeDocs(result.docs || []);
|
|
const newOrMatchingFrames = afterDocs.filter((doc) => {
|
|
if (doc.label === 'top') return false;
|
|
const existed = before.some((oldDoc) => oldDoc.url === doc.url && oldDoc.label === doc.label);
|
|
return !existed || doc.title === label || doc.url.includes('/System/Business/');
|
|
});
|
|
results.push({
|
|
label,
|
|
clickResult: result.clickResult,
|
|
dialogs: result.dialogs,
|
|
requests: result.requests
|
|
.filter((request) => request.type === 'Document' || request.type === 'XHR')
|
|
.map((request) => ({
|
|
method: request.method,
|
|
status: request.status,
|
|
type: request.type,
|
|
mimeType: request.mimeType,
|
|
url: request.url,
|
|
postData: request.postData
|
|
})),
|
|
frames: newOrMatchingFrames
|
|
});
|
|
const latestTop = (result.docs || []).find((doc) => doc.label === 'top');
|
|
if (latestTop && (/login/i.test(latestTop.url || '') || /云智办公i-5.1/.test(latestTop.title || ''))) break;
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async function main() {
|
|
const command = process.argv[2] || 'map';
|
|
const target = await getTarget();
|
|
const cdp = new CDP(target.webSocketDebuggerUrl);
|
|
await cdp.connect();
|
|
try {
|
|
let output;
|
|
if (command === 'target') output = target;
|
|
else if (command === 'map') output = await commandMap(cdp);
|
|
else if (command === 'cookies') output = await commandCookies(cdp);
|
|
else if (command === 'capture') output = await commandCapture(cdp, Number(process.argv[3] || 8000), process.argv.includes('--reload'));
|
|
else if (command === 'navframe') output = await commandNavFrame(cdp, process.argv[3], Number(process.argv[4] || 5000));
|
|
else if (command === 'clickmenu') output = await commandClickMenu(cdp, process.argv[3], Number(process.argv[4] || 5000));
|
|
else if (command === 'batchmenus') output = await commandBatchMenus(cdp, JSON.parse(process.argv[3]), Number(process.argv[4] || 4500));
|
|
else throw new Error(`Unknown command: ${command}`);
|
|
console.log(JSON.stringify(output, null, 2));
|
|
} finally {
|
|
cdp.close();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error.stack || error.message);
|
|
process.exit(1);
|
|
});
|