mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 05:01:53 +08:00
fix(capture): automate safe bidirectional gateways for users and agents
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { resolve } from 'node:path'
|
||||
import { createDecipheriv } from 'node:crypto'
|
||||
import { extensionRequest, launchBrowserAgentContractHarness } from './browser-agent-contract-harness.mjs'
|
||||
|
||||
function body(packet) { return JSON.parse(packet.raw.slice(packet.raw.indexOf('\r\n\r\n') + 4)) }
|
||||
function decrypt(envelope) {
|
||||
const decipher = createDecipheriv('aes-128-cbc', Buffer.from(envelope.key, 'hex'), Buffer.from(envelope.iv, 'hex'))
|
||||
return JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.message, 'base64')), decipher.final()]).toString())
|
||||
}
|
||||
|
||||
const targetURL = process.env.LOGIN_TARGET || 'http://localhost:8080/crypto/sqli/aes-ecb/encrypt/login'
|
||||
const engine = spawn('go', ['test', './common/yakgrpc', '-run', '^TestBrowserAgentLiveGateway$', '-count=1', '-v'], {
|
||||
cwd: process.env.YAKLANG_ROOT || resolve(import.meta.dirname, '../../../go/yaklang'),
|
||||
env: { ...process.env, YAK_BROWSER_AGENT_E2E: '1' }, stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
let output = ''
|
||||
let harness, connection
|
||||
const exited = new Promise(resolve => engine.once('exit', code => resolve(code)))
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`Go Agent bridge startup timed out: ${output.slice(-4000)}`)), 120_000)
|
||||
engine.stdout.on('data', chunk => {
|
||||
output += chunk
|
||||
const match = output.match(/YAK_AGENT_E2E=(\{[^\n]+\})/)
|
||||
if (match) { clearTimeout(timer); resolve(JSON.parse(match[1])) }
|
||||
})
|
||||
engine.stderr.on('data', chunk => { output += chunk })
|
||||
engine.once('error', error => { clearTimeout(timer); reject(error) })
|
||||
engine.once('exit', code => { clearTimeout(timer); reject(new Error(`Go bridge exited ${code}: ${output}`)) })
|
||||
})
|
||||
try {
|
||||
connection = await ready
|
||||
const tool = async (name, params = {}) => {
|
||||
const response = await fetch(connection.endpoint, { method: 'POST', headers: { 'X-Test-Token': connection.token }, body: JSON.stringify({ tool: name, params }) })
|
||||
if (!response.ok) throw new Error(`${name}: ${await response.text()}`)
|
||||
return response.json()
|
||||
}
|
||||
harness = await launchBrowserAgentContractHarness({ profilePrefix: 'yakit-agent-gateway-', targetURL })
|
||||
const { controlPage, targetPage, tabId } = harness
|
||||
let completed = 0, blocked = 0
|
||||
targetPage.on('response', response => { if (response.url() === targetURL && response.request().method() === 'POST') completed++ })
|
||||
targetPage.on('requestfailed', request => { if (request.url() === targetURL && request.failure()?.errorText.includes('BLOCKED_BY_CLIENT')) blocked++ })
|
||||
const state = await controlPage.evaluate(async () => (await chrome.runtime.sendMessage({ action: 'state.get' })).data)
|
||||
await extensionRequest(controlPage, 'bridge.config.save', {
|
||||
transport: 'websocket', nativeHost: 'com.yaklang.browser_agent', endpoint: connection.bridge,
|
||||
autoConnect: false, installationId: state.bridge.installationId,
|
||||
})
|
||||
await controlPage.evaluate(async () => {
|
||||
const result = await chrome.runtime.sendMessage({ action: 'bridge.pair' })
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.error))
|
||||
})
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const status = await controlPage.evaluate(async () => (await chrome.runtime.sendMessage({ action: 'bridge.status' })).data)
|
||||
if (status.state === 'connected') break
|
||||
if (attempt === 100) throw new Error(`Bridge did not connect: ${JSON.stringify(status)}`)
|
||||
await controlPage.waitForTimeout(100)
|
||||
}
|
||||
// Close the extension UI. All workflow calls below go through Go Agent tools.
|
||||
// A previously released human debugging session must not lock out the Agent.
|
||||
await extensionRequest(controlPage, 'deep.capture.start', { tabId, frameId: 0, matcher: { kind: 'request', urlPattern: '/not-triggered' } })
|
||||
await extensionRequest(controlPage, 'deep.capture.detach', { tabId, frameId: 0 })
|
||||
await controlPage.close()
|
||||
const call = (method, params = {}) => tool('browser.capability.call', { method, params: { tabId, frameId: 0, ...params } })
|
||||
const catalog = await tool('browser.capability.catalog', { domain: 'debugger' })
|
||||
assert(catalog.capabilities.some(item => item.method === 'browser.deep_capture.start'))
|
||||
const context = await call('browser.context', { includeDom: true })
|
||||
const nodes = context.document.interactive
|
||||
const username = nodes.find(node => node.name === 'username' || node.id === 'username')
|
||||
const password = nodes.find(node => node.name === 'password' || node.id === 'password')
|
||||
const submit = nodes.find(node => node.tag === 'button' && node.type === 'submit')
|
||||
assert(username && password && submit, JSON.stringify(nodes))
|
||||
for (const [node, value] of [[username, 'agent-original'], [password, 'agent-original-wrong']]) {
|
||||
await call('browser.node.action', { captureId: context.captureId, nodeId: node.nodeId, action: 'setValue', value })
|
||||
}
|
||||
const inspection = await tool('browser.crypto.inspect', { tabId, frameId: 0, captureId: context.captureId, nodeId: submit.nodeId })
|
||||
assert.equal(inspection.gatewayPreparation.state, 'capture-required')
|
||||
const plaintext = { username: 'agent-new-user', password: 'agent-new-wrong' }
|
||||
const url = new URL(targetURL)
|
||||
const request = `POST ${url.pathname} HTTP/1.1\r\nHost: ${url.host}\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(plaintext)}`
|
||||
const prepareInput = { ...inspection.target, candidate_id: inspection.gatewayPreparation.candidateId, request, is_https: false }
|
||||
await assert.rejects(tool('browser.transform.prepare', { ...prepareInput, captureId: 'stale-context', nodeId: submit.nodeId }), /快照已经失效/)
|
||||
assert.equal((await call('browser.deep_capture.status')).state, 'detached', 'failed preparation must release its debugger')
|
||||
assert.equal(completed, 1)
|
||||
const prepared = await tool('browser.transform.prepare', prepareInput)
|
||||
assert(prepared.valid, JSON.stringify(prepared))
|
||||
assert.deepEqual(prepared.validationDraft.directions, { request: true, response: true })
|
||||
assert.equal(completed, 1, 'automatic preparation must not submit another real browser login')
|
||||
assert.equal(blocked, 1)
|
||||
const preparedAgain = await tool('browser.transform.prepare', prepareInput)
|
||||
assert(preparedAgain.valid)
|
||||
assert.equal(blocked, 1, 'repeated preparation must reuse existing captured functions')
|
||||
const tested = await tool('browser.http.test', { validation_id: preparedAgain.validationDraft.id, request, is_https: false })
|
||||
assert(tested.responseTransformEnabled, JSON.stringify(tested))
|
||||
assert(tested.requestTransformed, JSON.stringify(tested))
|
||||
assert(tested.responseTransformed)
|
||||
assert.deepEqual(decrypt(body(tested.wireRequest)), plaintext)
|
||||
assert.deepEqual(body(tested.plaintextResponse), decrypt(body(tested.wireResponse)))
|
||||
const draft = await call('browser.profile.validation.latest')
|
||||
const saved = await tool('browser.capability.call', { method: 'browser.transform.profile.save', params: draft.profile })
|
||||
assert(saved.request.enabled && saved.response.enabled)
|
||||
await tool('browser.capability.call', { method: 'browser.transform.profile.delete', params: { id: saved.id } })
|
||||
assert.equal(completed, 1)
|
||||
assert.equal(await targetPage.locator('#username').inputValue(), 'agent-original')
|
||||
assert.equal(await targetPage.locator('#password').inputValue(), 'agent-original-wrong')
|
||||
console.log('Agent gateway passed: signed Go bridge, advanced capability access, no plugin UI, failed-capture cleanup, automatic business capture, bidirectional validation, independently checked request/response crypto, default response decryption and Profile save/delete.')
|
||||
} finally {
|
||||
await harness?.close()
|
||||
if (connection) await fetch(`${connection.endpoint}/finish`, { method: 'POST', headers: { 'X-Test-Token': connection.token } }).catch(() => undefined)
|
||||
else engine.kill('SIGTERM')
|
||||
const code = await exited
|
||||
if (code !== 0 && connection) throw new Error(`Go integration exited ${code}: ${output.slice(-4000)}`)
|
||||
}
|
||||
Reference in New Issue
Block a user