Enhance architecture documentation and update project dependencies. Introduce new features for browser recording, page callables, and transform capabilities. Improve build scripts and permissions for better functionality.

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent 0371a8b802
commit af5a4db694
125 changed files with 25018 additions and 1675 deletions
+224
View File
@@ -0,0 +1,224 @@
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { chromium } from 'playwright-core'
import { resolveChromiumPath } from './resolve-chromium.mjs'
const root = resolve(import.meta.dirname, '..')
const extensionPath = resolve(root, process.env.EXTENSION_PATH || '.output/chrome-mv3-enterprise')
const targetURL = process.env.AESRSA_TARGET || 'http://127.0.0.1:82/'
const manifest = JSON.parse(await readFile(resolve(extensionPath, 'manifest.json'), 'utf8'))
const executablePath = await resolveChromiumPath()
const userDataDir = await mkdtemp(join(tmpdir(), 'yakit-aesrsa-'))
let context
async function extensionRequest(page, action, payload = {}) {
return page.evaluate(async ({ requestAction, requestPayload }) => {
const response = await chrome.runtime.sendMessage({ action: requestAction, payload: requestPayload })
if (!response?.ok) throw new Error(response?.error?.message || response?.error || requestAction)
return response.data
}, { requestAction: action, requestPayload: payload })
}
async function waitFor(page, action, payload, predicate, timeoutMs = 15_000) {
const deadline = Date.now() + timeoutMs
let value
while (Date.now() < deadline) {
value = await extensionRequest(page, action, payload)
if (predicate(value)) return value
await page.waitForTimeout(150)
}
throw new Error(`Timed out waiting for ${action}: ${JSON.stringify(value)}`)
}
try {
context = await chromium.launchPersistentContext(userDataDir, {
executablePath,
headless: true,
viewport: { width: 1280, height: 760 },
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
'--no-first-run',
'--no-default-browser-check',
],
})
let serviceWorker = context.serviceWorkers()[0]
if (!serviceWorker) serviceWorker = await context.waitForEvent('serviceworker', { timeout: 15_000 })
const extensionId = new URL(serviceWorker.url()).host
if (manifest.permissions?.includes('userScripts')) {
const extensionsPage = await context.newPage()
await extensionsPage.goto(`chrome://extensions/?id=${extensionId}`)
const toggle = extensionsPage.locator('#allow-user-scripts cr-toggle')
await toggle.waitFor({ state: 'visible', timeout: 10_000 })
if (!await toggle.evaluate((element) => Boolean(element.checked))) await toggle.click()
await extensionsPage.close()
}
const targetPage = await context.newPage()
targetPage.on('dialog', (dialog) => void dialog.dismiss())
let browserRequestCount = 0
targetPage.on('request', (request) => {
if (new URL(request.url()).pathname === '/encrypt/aesrsa.php') browserRequestCount += 1
})
await targetPage.goto(targetURL)
const controlPage = await context.newPage()
await controlPage.goto(`chrome-extension://${extensionId}/options.html`)
const tabId = await controlPage.evaluate(async (url) => {
const tabs = await chrome.tabs.query({})
return tabs.find((tab) => tab.url === url)?.id
}, targetPage.url())
if (!tabId) throw new Error('Could not resolve the AES+RSA target tab')
await extensionRequest(controlPage, 'recording.start', {
tabId, frameId: 0, captureValues: true, maxEntries: 120, maxValueBytes: 8_192,
})
await targetPage.locator('#username').fill('admin')
await targetPage.locator('#password').fill('wrong-password')
await targetPage.getByRole('button', { name: '登录', exact: true }).click()
await targetPage.getByRole('button', { name: 'AES+Rsa加密', exact: true }).click()
await targetPage.waitForTimeout(500)
if (browserRequestCount !== 1) throw new Error(`Initial recording expected one real request, received ${browserRequestCount}`)
const snapshot = await extensionRequest(controlPage, 'recording.get', { tabId, frameId: 0, limit: 120 })
const candidate = snapshot.profileCandidates?.find((item) => (
item.status === 'capture-required'
&& item.sources?.length === 3
&& new URL(item.request?.url, targetURL).pathname === '/encrypt/aesrsa.php'
))
if (!candidate) throw new Error(`AES+RSA request-level candidate was not inferred: ${JSON.stringify(snapshot)}`)
const matcherEvent = snapshot.events?.find((event) => event.id === candidate.capturePlan?.matcherEventId)
if (!matcherEvent?.crypto?.adapterId || !matcherEvent.wrapperHandleId) {
throw new Error(`AES+RSA candidate has no deep-capture matcher: ${JSON.stringify(candidate)}`)
}
await extensionRequest(controlPage, 'deep.capture.start', {
tabId,
frameId: 0,
matcher: {
kind: 'crypto',
adapterId: matcherEvent.crypto.adapterId,
operation: matcherEvent.crypto.operation,
wrapperHandleId: matcherEvent.wrapperHandleId,
scriptUrl: matcherEvent.scriptUrl,
frameHints: candidate.capturePlan.frameHints,
},
})
await targetPage.locator('#username').fill('admin')
await targetPage.locator('#password').fill('wrong-password')
await targetPage.getByRole('button', { name: '登录', exact: true }).click()
let replayClickFailure
const replayClick = targetPage.getByRole('button', { name: 'AES+Rsa加密', exact: true })
.click({ noWaitAfter: true, timeout: 20_000 })
.catch((reason) => { replayClickFailure = reason })
const paused = await waitFor(controlPage, 'deep.capture.status', { tabId, frameId: 0 }, (value) => (
value?.state === 'paused' && value.pause?.collecting !== true
), 20_000)
const automatic = paused.pause?.automaticCapture
const frame = paused.pause?.frames?.find((item) => item.id === automatic?.frameId)
if (automatic?.state !== 'ready' || automatic.strategy !== 'request-transaction'
|| frame?.functionName !== 'sendDataAesRsa') {
throw new Error(`Deep capture did not select sendDataAesRsa as a request transaction: ${JSON.stringify(paused)}`)
}
const callable = await extensionRequest(controlPage, 'callable.create', {
tabId,
frameId: 0,
source: 'deep-capture',
strategy: 'request-transaction',
callFrameId: frame.id,
name: 'sendDataAesRsa 请求事务',
transaction: {
request: {
method: candidate.request.method,
url: candidate.request.url,
expectedDestinations: candidate.sources.map((source) => source.destination).filter(Boolean),
},
inputMode: 'auto',
boundaries: ['fetch', 'xhr', 'beacon', 'form'],
},
})
if (callable.kind !== 'request-transaction' || callable.inputSlots?.[0]?.name !== 'body') {
throw new Error(`Captured callable is not a request transaction: ${JSON.stringify(callable)}`)
}
await replayClick
if (replayClickFailure) throw replayClickFailure
await targetPage.waitForTimeout(300)
if (browserRequestCount !== 1) {
throw new Error(`Deep-capture replay leaked a real request; observed ${browserRequestCount}`)
}
let execution
try {
execution = await extensionRequest(controlPage, 'callable.execute', {
tabId,
frameId: 0,
callableId: callable.id,
args: [{ username: 'admin', password: '123456' }],
})
} catch (reason) {
const diagnostics = await controlPage.evaluate(async ({ targetTabId, callableId }) => {
const tab = await chrome.tabs.get(targetTabId).catch(() => undefined)
const [injection] = await chrome.scripting.executeScript({
target: { tabId: targetTabId, frameIds: [0] },
world: 'MAIN',
func: async (registryKey, protocolVersion, requestedCallableId) => {
const controller = globalThis[registryKey]
let directExecution
try {
directExecution = {
ok: true,
value: await controller?.command('callable.execute', {
callableId: requestedCallableId,
args: [{ username: 'admin', password: '123456' }],
}),
}
} catch (error) {
directExecution = {
ok: false,
error: error instanceof Error ? `${error.name}: ${error.message}\n${error.stack || ''}` : String(error),
}
}
return {
href: location.href,
controllerVersion: controller?.version,
expectedVersion: protocolVersion,
callables: typeof controller?.command === 'function' ? controller.command('callable.list', {}) : [],
directExecution,
}
},
args: ['__YAKIT_PAGE_RECORDER_V8__', 8, callableId],
}).catch(() => [])
return { tabUrl: tab?.url, page: injection?.result }
}, { targetTabId: tabId, callableId: callable.id })
const selectedFrame = {
functionName: frame.functionName,
functionInspection: frame.functionInspection,
scopes: frame.scopes,
}
throw new Error(`Callable execution failed: ${reason instanceof Error ? reason.message : String(reason)}; browserRequests=${browserRequestCount}; frame=${JSON.stringify(selectedFrame)}; diagnostics=${JSON.stringify(diagnostics)}`)
}
const envelope = execution.value
for (const field of ['encryptedData', 'encryptedKey', 'encryptedIv']) {
if (typeof envelope?.[field] !== 'string' || !envelope[field]) {
throw new Error(`Transaction output is missing ${field}: ${JSON.stringify(execution)}`)
}
}
if (browserRequestCount !== 1) {
throw new Error(`Transaction execution leaked a real browser request; observed ${browserRequestCount}`)
}
const response = await fetch(new URL('/encrypt/aesrsa.php', targetURL), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(envelope),
})
const result = await response.json()
if (!result.success) throw new Error(`Target server rejected the transaction envelope: ${JSON.stringify(result)}`)
process.stdout.write('AES+RSA request transaction verified: no browser request leaked and the target server accepted the captured envelope.\n')
} finally {
await context?.close().catch(() => undefined)
await rm(userDataDir, { recursive: true, force: true })
}