mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-24 04:01:53 +08:00
143 lines
6.4 KiB
TypeScript
143 lines
6.4 KiB
TypeScript
import type {
|
|
ActiveTabInfo,
|
|
BrowserPageCallable,
|
|
BrowserProfileInferenceCandidate,
|
|
BrowserRecordingEvent,
|
|
BrowserTransformDirection,
|
|
BrowserTransformProfileInput,
|
|
BrowserTransformPacket,
|
|
} from '@/types/models';
|
|
import { ExtensionError } from '@/shared/errors';
|
|
import { compileGuidedTransform, defaultGuidedTransform, type GuidedTransformOutputKind } from './guided';
|
|
|
|
interface RequestRouteSource {
|
|
url?: string;
|
|
method?: string;
|
|
}
|
|
|
|
export interface BrowserTransformProfileBinding {
|
|
candidate: BrowserProfileInferenceCandidate;
|
|
callable: BrowserPageCallable;
|
|
packet?: BrowserTransformPacket;
|
|
}
|
|
|
|
function originOf(url?: string): string {
|
|
try { return url ? new URL(url).origin : ''; } catch { return ''; }
|
|
}
|
|
|
|
function routeOf(event?: RequestRouteSource, tab?: ActiveTabInfo): string {
|
|
const value = event?.url || tab?.url;
|
|
try { return value ? `*${new URL(value, tab?.url).pathname}` : '*'; } catch { return '*'; }
|
|
}
|
|
|
|
function emptyDirection(enabled = false): BrowserTransformDirection {
|
|
return { enabled, nodes: [] };
|
|
}
|
|
|
|
function sameTarget(left: BrowserProfileInferenceCandidate, right: BrowserProfileInferenceCandidate): boolean {
|
|
return left.target.tabId === right.target.tabId
|
|
&& left.target.frameId === right.target.frameId
|
|
&& (!left.target.documentId || !right.target.documentId || left.target.documentId === right.target.documentId);
|
|
}
|
|
|
|
export function pairedBrowserTransformCandidate(
|
|
candidates: BrowserProfileInferenceCandidate[],
|
|
candidate: BrowserProfileInferenceCandidate,
|
|
requireUnambiguous = false,
|
|
): BrowserProfileInferenceCandidate | undefined {
|
|
const opposite = candidates.filter((item) => item.id !== candidate.id
|
|
&& item.recordingId === candidate.recordingId
|
|
&& item.direction !== candidate.direction && sameTarget(item, candidate));
|
|
const matches = candidate.transactionId
|
|
? opposite.filter((item) => item.transactionId === candidate.transactionId)
|
|
: opposite.filter((item) => !item.transactionId
|
|
&& item.traceId === candidate.traceId
|
|
&& item.request.method.toUpperCase() === candidate.request.method.toUpperCase()
|
|
&& item.request.url === candidate.request.url);
|
|
if (requireUnambiguous && matches.length > 1) {
|
|
throw new ExtensionError('profile_evidence_ambiguous', `同一事务存在 ${matches.length} 个反方向候选,无法完整合并;尚未保存单向网关`);
|
|
}
|
|
return matches.length === 1 ? matches[0] : undefined;
|
|
}
|
|
|
|
function candidateGuidance(candidate?: BrowserProfileInferenceCandidate, callable?: BrowserPageCallable, packet?: BrowserTransformPacket): {
|
|
inputPaths?: string[];
|
|
outputKind?: GuidedTransformOutputKind;
|
|
outputField?: string;
|
|
} {
|
|
const destination = candidate?.request.destination;
|
|
const serialization = candidate?.request.serialization;
|
|
if (!destination) return {};
|
|
if (candidate?.direction === 'response') {
|
|
return {
|
|
inputPaths: candidate.request.mappings
|
|
.map((mapping) => mapping.destination)
|
|
.filter((path): path is string => Boolean(path)),
|
|
outputKind: 'body',
|
|
};
|
|
}
|
|
if (serialization === 'form-field') {
|
|
let inputPaths: string[] | undefined;
|
|
const slots = callable?.inputSlots.filter((slot) => !slot.retained);
|
|
if (packet && slots?.length === 1 && slots[0].dataType === 'string') {
|
|
const contentType = packet.headers.find((header) => header.name.toLowerCase() === 'content-type')?.value.split(';')[0].trim().toLowerCase();
|
|
if (contentType === 'application/x-www-form-urlencoded') {
|
|
const body = new TextDecoder().decode(Uint8Array.from(atob(packet.bodyBase64), (char) => char.charCodeAt(0)));
|
|
const fields = new URLSearchParams(body);
|
|
const destinations = new Set(candidate?.request.mappings.map((mapping) => mapping.destination));
|
|
if (destinations.size > 1 || fields.getAll(destination.slice(5)).length !== 1) {
|
|
throw new ExtensionError('profile_input_mismatch', '无法唯一确定表单明文输入,请显式指定 input_paths;尚未发送请求');
|
|
}
|
|
inputPaths = [destination];
|
|
}
|
|
}
|
|
return { inputPaths, outputKind: 'form-field', outputField: destination.slice(5) };
|
|
}
|
|
if (serialization === 'json-field') return { outputKind: 'json-field', outputField: destination.slice(5) };
|
|
if (serialization === 'header') return { outputKind: 'header', outputField: destination.slice(7) };
|
|
if (serialization === 'query') return { outputKind: 'query', outputField: destination.slice(6) };
|
|
return { outputKind: 'body' };
|
|
}
|
|
|
|
export function createBrowserTransformProfileInput(
|
|
tab: ActiveTabInfo,
|
|
event?: BrowserRecordingEvent,
|
|
callable?: BrowserPageCallable,
|
|
candidate?: BrowserProfileInferenceCandidate,
|
|
packet?: BrowserTransformPacket,
|
|
paired?: BrowserTransformProfileBinding,
|
|
): BrowserTransformProfileInput {
|
|
const guide = defaultGuidedTransform(callable, candidateGuidance(candidate, callable, packet));
|
|
const compiled = callable ? compileGuidedTransform(guide, callable) : emptyDirection(true);
|
|
const responseDirection = candidate?.direction === 'response';
|
|
const routeEvent = candidate ? {
|
|
url: candidate.request.url,
|
|
method: candidate.request.method,
|
|
} : event;
|
|
const profile: BrowserTransformProfileInput = {
|
|
name: routeEvent?.url
|
|
? `${routeEvent.method || 'HTTP'} ${routeOf(routeEvent, tab)} 浏览器协议网关`
|
|
: `${tab.title || '当前页面'} 明文网关`,
|
|
enabled: true,
|
|
target: { tabId: tab.id, frameId: 0 },
|
|
origin: originOf(tab.url),
|
|
match: { methods: routeEvent?.method ? [routeEvent.method.toUpperCase()] : ['POST'], urlPattern: routeOf(routeEvent, tab) },
|
|
request: responseDirection ? emptyDirection(false) : compiled,
|
|
response: responseDirection ? compiled : emptyDirection(false),
|
|
failMode: 'closed',
|
|
maxConcurrency: callable?.kind === 'request-transaction' ? 1 : 2,
|
|
};
|
|
if (!paired) return profile;
|
|
if (!candidate
|
|
|| pairedBrowserTransformCandidate([candidate, paired.candidate], candidate)?.id !== paired.candidate.id) {
|
|
throw new ExtensionError('profile_evidence_mismatch', '请求与响应候选不属于同一个浏览器协议网关');
|
|
}
|
|
const pairedGuide = defaultGuidedTransform(
|
|
paired.callable,
|
|
candidateGuidance(paired.candidate, paired.callable, paired.packet),
|
|
);
|
|
profile[paired.candidate.direction] = compileGuidedTransform(pairedGuide, paired.callable);
|
|
if (paired.callable.kind === 'request-transaction') profile.maxConcurrency = 1;
|
|
return profile;
|
|
}
|