mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:30:42 +00:00
371 lines
12 KiB
JavaScript
371 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import ts from "typescript";
|
|
import { bundledPluginCallsite } from "./lib/bundled-plugin-paths.mjs";
|
|
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
|
|
import {
|
|
collectCallExpressionLines,
|
|
runAsScript,
|
|
unwrapExpression,
|
|
} from "./lib/ts-guard-utils.mjs";
|
|
|
|
const sourceRoots = ["src", "extensions"];
|
|
|
|
// Managed-proxy raw-socket classification allowlist.
|
|
// Each entry is intentionally a concrete callsite so new raw socket egress fails until reviewed.
|
|
const allowedRawSocketCallsites = new Set([
|
|
// Local Gateway run loop readiness probe.
|
|
"src/cli/gateway-cli/run-loop.ts:46",
|
|
|
|
// Local loopback readiness probe for SSH tunnels.
|
|
"src/infra/ssh-tunnel.ts:80",
|
|
|
|
// Local gateway lock probe.
|
|
"src/infra/gateway-lock.ts:147",
|
|
|
|
// Local Unix-domain socket IPC client.
|
|
"src/infra/jsonl-socket.ts:35",
|
|
|
|
// Managed HTTP CONNECT tunnel helper used by APNs and proxy validation.
|
|
"src/infra/net/http-connect-tunnel.ts:117",
|
|
"src/infra/net/http-connect-tunnel.ts:123",
|
|
"src/infra/net/http-connect-tunnel.ts:268",
|
|
|
|
// APNs HTTP/2 wrapper: direct only when managed proxy is inactive; tunneled when active.
|
|
"src/infra/push-apns-http2.ts:74",
|
|
"src/infra/push-apns-http2.ts:85",
|
|
|
|
// Debug proxy CONNECT internals. PR #77010 guards this path while managed proxy mode is active.
|
|
"src/proxy-capture/proxy-server.ts:266",
|
|
|
|
// QA-lab tunnel/capture helpers used for local lab diagnostics.
|
|
bundledPluginCallsite("qa-lab", "src/lab-server-capture.ts", 99),
|
|
bundledPluginCallsite("qa-lab", "src/lab-server-ui.ts", 207),
|
|
bundledPluginCallsite("qa-lab", "src/lab-server-ui.ts", 212),
|
|
|
|
// IRC is a raw TCP/TLS channel and is documented as outside managed HTTP proxy coverage.
|
|
bundledPluginCallsite("irc", "src/client.ts", 124),
|
|
bundledPluginCallsite("irc", "src/client.ts", 129),
|
|
]);
|
|
|
|
function stringLiteralText(node) {
|
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
return node.text;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const rawModuleSpecifiers = new Map([
|
|
["node:net", "net"],
|
|
["net", "net"],
|
|
["node:tls", "tls"],
|
|
["tls", "tls"],
|
|
["node:http2", "http2"],
|
|
["http2", "http2"],
|
|
]);
|
|
|
|
function unwrapInitializer(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isAwaitExpression(unwrapped)) {
|
|
return unwrapExpression(unwrapped.expression);
|
|
}
|
|
return unwrapped;
|
|
}
|
|
|
|
function rawMemberAliasKind(initializer, aliases) {
|
|
const unwrapped = unwrapExpression(initializer);
|
|
let receiverExpression;
|
|
let member;
|
|
if (ts.isPropertyAccessExpression(unwrapped)) {
|
|
receiverExpression = unwrapped.expression;
|
|
member = unwrapped.name.text;
|
|
} else if (ts.isElementAccessExpression(unwrapped)) {
|
|
receiverExpression = unwrapped.expression;
|
|
member = stringLiteralText(unwrapExpression(unwrapped.argumentExpression));
|
|
} else {
|
|
return undefined;
|
|
}
|
|
if (!member) {
|
|
return undefined;
|
|
}
|
|
const receiverKind = aliasKind(receiverExpression, aliases);
|
|
if (
|
|
(receiverKind === "net" || receiverKind === "tls") &&
|
|
(member === "connect" || member === "createConnection")
|
|
) {
|
|
return `${receiverKind}.${member}`;
|
|
}
|
|
if (receiverKind === "net" && member === "Socket") {
|
|
return "net.Socket";
|
|
}
|
|
if (receiverKind === "http2" && member === "connect") {
|
|
return "http2.connect";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function bindRawModuleDestructureAliases(bindingName, moduleKind, aliases) {
|
|
if (!ts.isObjectBindingPattern(bindingName)) {
|
|
return;
|
|
}
|
|
for (const element of bindingName.elements) {
|
|
if (!ts.isIdentifier(element.name)) {
|
|
continue;
|
|
}
|
|
const importedName =
|
|
element.propertyName && ts.isIdentifier(element.propertyName)
|
|
? element.propertyName.text
|
|
: element.name.text;
|
|
if (importedName === "default") {
|
|
aliases.set(element.name.text, moduleKind);
|
|
continue;
|
|
}
|
|
if (
|
|
importedName === "connect" ||
|
|
importedName === "createConnection" ||
|
|
importedName === "Socket"
|
|
) {
|
|
aliases.set(element.name.text, `${moduleKind}.${importedName}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function collectSocketInstanceAliases(sourceFile, rawAliases) {
|
|
const socketAliases = new Set();
|
|
const visit = (node) => {
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
if (isSocketConstructorExpression(node.initializer, rawAliases)) {
|
|
socketAliases.add(node.name.text);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return socketAliases;
|
|
}
|
|
|
|
function collectRawModuleAliases(sourceFile) {
|
|
const aliases = new Map();
|
|
const visit = (node) => {
|
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
const moduleKind = rawModuleSpecifiers.get(node.moduleSpecifier.text);
|
|
const clause = node.importClause;
|
|
if (moduleKind && clause) {
|
|
if (clause.name) {
|
|
aliases.set(clause.name.text, moduleKind);
|
|
}
|
|
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
aliases.set(clause.namedBindings.name.text, moduleKind);
|
|
}
|
|
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
for (const specifier of clause.namedBindings.elements) {
|
|
const importedName = (specifier.propertyName ?? specifier.name).text;
|
|
if (importedName === "default") {
|
|
aliases.set(specifier.name.text, moduleKind);
|
|
continue;
|
|
}
|
|
if (
|
|
importedName === "connect" ||
|
|
importedName === "createConnection" ||
|
|
importedName === "Socket"
|
|
) {
|
|
aliases.set(specifier.name.text, `${moduleKind}.${importedName}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
const initializer = unwrapInitializer(node.initializer);
|
|
if (ts.isIdentifier(node.name)) {
|
|
const moduleKind = aliasKind(initializer, aliases);
|
|
if (moduleKind === "net" || moduleKind === "tls" || moduleKind === "http2") {
|
|
aliases.set(node.name.text, moduleKind);
|
|
}
|
|
const memberKind = rawMemberAliasKind(initializer, aliases);
|
|
if (memberKind) {
|
|
aliases.set(node.name.text, memberKind);
|
|
}
|
|
}
|
|
if (
|
|
ts.isCallExpression(initializer) &&
|
|
ts.isIdentifier(unwrapExpression(initializer.expression)) &&
|
|
unwrapExpression(initializer.expression).text === "require" &&
|
|
initializer.arguments.length === 1 &&
|
|
ts.isStringLiteral(initializer.arguments[0])
|
|
) {
|
|
const moduleKind = rawModuleSpecifiers.get(initializer.arguments[0].text);
|
|
if (moduleKind) {
|
|
if (ts.isIdentifier(node.name)) {
|
|
aliases.set(node.name.text, moduleKind);
|
|
} else {
|
|
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
|
}
|
|
}
|
|
}
|
|
if (ts.isObjectBindingPattern(node.name) && ts.isIdentifier(initializer)) {
|
|
const moduleKind = aliases.get(initializer.text);
|
|
if (moduleKind === "net" || moduleKind === "tls" || moduleKind === "http2") {
|
|
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
|
}
|
|
}
|
|
if (
|
|
ts.isCallExpression(initializer) &&
|
|
initializer.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
initializer.arguments.length === 1 &&
|
|
ts.isStringLiteral(initializer.arguments[0])
|
|
) {
|
|
const moduleKind = rawModuleSpecifiers.get(initializer.arguments[0].text);
|
|
if (moduleKind) {
|
|
if (ts.isIdentifier(node.name)) {
|
|
aliases.set(node.name.text, moduleKind);
|
|
} else {
|
|
bindRawModuleDestructureAliases(node.name, moduleKind, aliases);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return aliases;
|
|
}
|
|
|
|
function rawModuleKindFromExpression(expression) {
|
|
const unwrappedExpression = unwrapExpression(expression);
|
|
const unwrapped = ts.isAwaitExpression(unwrappedExpression)
|
|
? unwrapExpression(unwrappedExpression.expression)
|
|
: unwrappedExpression;
|
|
if (
|
|
ts.isCallExpression(unwrapped) &&
|
|
ts.isIdentifier(unwrapped.expression) &&
|
|
unwrapped.expression.text === "require" &&
|
|
unwrapped.arguments.length === 1
|
|
) {
|
|
const moduleName = stringLiteralText(unwrapExpression(unwrapped.arguments[0]));
|
|
return moduleName ? rawModuleSpecifiers.get(moduleName) : undefined;
|
|
}
|
|
if (
|
|
ts.isCallExpression(unwrapped) &&
|
|
unwrapped.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
unwrapped.arguments.length === 1
|
|
) {
|
|
const moduleName = stringLiteralText(unwrapExpression(unwrapped.arguments[0]));
|
|
return moduleName ? rawModuleSpecifiers.get(moduleName) : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function aliasKind(expression, aliases) {
|
|
const receiver = unwrapExpression(expression);
|
|
if (ts.isIdentifier(receiver)) {
|
|
return aliases.get(receiver.text);
|
|
}
|
|
if (ts.isPropertyAccessExpression(receiver) && receiver.name.text === "default") {
|
|
return rawModuleKindFromExpression(receiver.expression);
|
|
}
|
|
return rawModuleKindFromExpression(receiver);
|
|
}
|
|
|
|
function isRawModuleAlias(expression, aliases, expectedKinds) {
|
|
const kind = aliasKind(expression, aliases);
|
|
return expectedKinds.has(kind);
|
|
}
|
|
|
|
function isSocketConstructorExpression(expression, aliases) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (!ts.isNewExpression(unwrapped)) {
|
|
return false;
|
|
}
|
|
const callee = unwrapExpression(unwrapped.expression);
|
|
if (aliasKind(callee, aliases) === "net.Socket") {
|
|
return true;
|
|
}
|
|
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "Socket") {
|
|
return false;
|
|
}
|
|
return isRawModuleAlias(callee.expression, aliases, new Set(["net"]));
|
|
}
|
|
|
|
function rawSocketCallee(expression, aliases, socketAliases = new Set()) {
|
|
const callee = unwrapExpression(expression);
|
|
if (ts.isIdentifier(callee)) {
|
|
const kind = aliasKind(callee, aliases);
|
|
return kind === "net.connect" ||
|
|
kind === "tls.connect" ||
|
|
kind === "http2.connect" ||
|
|
kind === "net.createConnection" ||
|
|
kind === "tls.createConnection"
|
|
? callee
|
|
: null;
|
|
}
|
|
let receiverExpression;
|
|
let member;
|
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
receiverExpression = callee.expression;
|
|
member = callee.name.text;
|
|
} else if (ts.isElementAccessExpression(callee)) {
|
|
receiverExpression = callee.expression;
|
|
member = stringLiteralText(unwrapExpression(callee.argumentExpression));
|
|
} else {
|
|
return null;
|
|
}
|
|
if (!member) {
|
|
return null;
|
|
}
|
|
if (
|
|
member === "connect" &&
|
|
isRawModuleAlias(receiverExpression, aliases, new Set(["net", "tls", "http2"]))
|
|
) {
|
|
return callee;
|
|
}
|
|
if (
|
|
member === "createConnection" &&
|
|
isRawModuleAlias(receiverExpression, aliases, new Set(["net", "tls"]))
|
|
) {
|
|
return callee;
|
|
}
|
|
if (member === "connect" && isSocketConstructorExpression(receiverExpression, aliases)) {
|
|
return callee;
|
|
}
|
|
if (
|
|
member === "connect" &&
|
|
ts.isIdentifier(unwrapExpression(receiverExpression)) &&
|
|
socketAliases.has(unwrapExpression(receiverExpression).text)
|
|
) {
|
|
return callee;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function findRawSocketClientCallLines(content, fileName = "source.ts") {
|
|
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
|
const aliases = collectRawModuleAliases(sourceFile);
|
|
const socketAliases = collectSocketInstanceAliases(sourceFile, aliases);
|
|
return collectCallExpressionLines(ts, sourceFile, (node) =>
|
|
rawSocketCallee(node.expression, aliases, socketAliases),
|
|
);
|
|
}
|
|
|
|
export async function main() {
|
|
await runCallsiteGuard({
|
|
importMetaUrl: import.meta.url,
|
|
sourceRoots,
|
|
extraTestSuffixes: [
|
|
".browser.test.ts",
|
|
".node.test.ts",
|
|
".live.test.ts",
|
|
".e2e.test.ts",
|
|
".integration.test.ts",
|
|
],
|
|
findCallLines: findRawSocketClientCallLines,
|
|
skipRelativePath: (relPath) => relPath.includes("/test-support/"),
|
|
allowCallsite: (callsite) => allowedRawSocketCallsites.has(callsite),
|
|
header: "Found unclassified raw socket client calls:",
|
|
footer:
|
|
"Classify raw net/tls/http2 egress as managed/proxied, local-only, diagnostic guarded, or documented unsupported before adding callsites.",
|
|
});
|
|
}
|
|
|
|
runAsScript(import.meta.url, main);
|