Files
openclaw/scripts/check-raw-socket-callsite-classification.mjs
2026-05-08 01:18:04 +10:00

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);