fix: proxy direct APNs HTTP2 sessions (#74905)

Summary:
- This PR routes direct APNs HTTP/2 sends through an APNs allowlisted managed-proxy CONNECT wrapper, adds APNs proxy validation/docs/guardrails, and expands regression and live-test coverage.
- Reproducibility: yes. source-reproducible: current main `sendApnsRequest()` still uses raw `http2.connect(au ... nly covers HTTP/global-agent/Undici hooks. I did not run a live APNs reproduction in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test: guard raw HTTP2 APNs connections
- PR branch already contained follow-up commit before automerge: test: guard raw HTTP2 with OpenGrep
- PR branch already contained follow-up commit before automerge: lint: ban raw HTTP2 imports
- PR branch already contained follow-up commit before automerge: fix: use managed proxy state for APNs
- PR branch already contained follow-up commit before automerge: test: exercise APNs active proxy state
- PR branch already contained follow-up commit before automerge: fix: reject conflicting managed proxy activation

Validation:
- ClawSweeper review passed for head dab7c86a75.
- Required merge gates passed before the squash merge.

Prepared head SHA: dab7c86a75
Review: https://github.com/openclaw/openclaw/pull/74905#issuecomment-4350181159

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
Jesse Merhi
2026-05-04 21:04:17 +10:00
committed by GitHub
parent 5efbb3078a
commit d5b0083300
30 changed files with 2159 additions and 89 deletions

View File

@@ -19,6 +19,7 @@ import {
getDebugProxyCaptureStore,
} from "../proxy-capture/store.sqlite.js";
import type { CaptureQueryPreset } from "../proxy-capture/types.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
export async function runDebugProxyStartCommand(opts: { host?: string; port?: number }) {
const settings = resolveDebugProxySettings();
@@ -148,11 +149,41 @@ function redactProxyValidationResult(result: ProxyValidationResult): ProxyValida
};
}
function formatProxyCheckLine(check: ProxyValidationResult["checks"][number]): string {
const icon = check.ok ? "✓" : "✗";
const paddedKind = check.kind.padEnd(7, " ");
const status = check.status === undefined ? "" : ` HTTP ${check.status}`;
return ` ${icon} ${paddedKind} ${check.url}${status}`;
type ProxyValidationTextColors = {
heading: (value: string) => string;
success: (value: string) => string;
error: (value: string) => string;
muted: (value: string) => string;
warn: (value: string) => string;
};
function getProxyValidationTextColors(): ProxyValidationTextColors {
const rich = isRich();
const apply = (color: (value: string) => string) => (value: string) =>
colorize(rich, color, value);
return {
heading: apply(theme.heading),
success: apply(theme.success),
error: apply(theme.error),
muted: apply(theme.muted),
warn: apply(theme.warn),
};
}
function formatProxyCheckLine(
check: ProxyValidationResult["checks"][number],
colors: ProxyValidationTextColors,
): string {
const icon = check.ok ? colors.success("✓") : colors.error("✗");
const paddedKind = colors.muted(check.kind.padEnd(7, " "));
const status =
check.status === undefined
? ""
: ` ${check.ok ? colors.success(`HTTP ${check.status}`) : colors.error(`HTTP ${check.status}`)}`;
const detail = check.error
? `${check.ok ? colors.muted(check.error) : colors.error(check.error)}`
: "";
return ` ${icon} ${paddedKind} ${check.url}${status}${detail}`;
}
function formatProxyValidationNextSteps(result: ProxyValidationResult): string[] {
@@ -185,37 +216,35 @@ function formatProxyValidationNextSteps(result: ProxyValidationResult): string[]
}
function formatProxyValidationText(result: ProxyValidationResult): string {
const colors = getProxyValidationTextColors();
const redactedProxyUrl = redactProxyUrl(result.config.proxyUrl);
const lines = [
`Proxy validation ${result.ok ? "passed" : "failed"}`,
result.ok ? colors.success("Proxy validation passed") : colors.error("Proxy validation failed"),
"",
"Proxy",
` Source: ${result.config.source}`,
` URL: ${redactedProxyUrl ?? "not configured"}`,
colors.heading("Proxy"),
` Source: ${colors.muted(result.config.source)}`,
` URL: ${redactedProxyUrl ?? colors.muted("not configured")}`,
];
if (result.config.errors.length > 0) {
lines.push("", "Problems");
lines.push("", colors.heading("Problems"));
for (const error of result.config.errors) {
lines.push(` - ${error}`);
lines.push(` - ${colors.error(error)}`);
}
}
if (result.checks.length > 0) {
lines.push("", "Checks");
lines.push("", colors.heading("Checks"));
for (const check of result.checks) {
lines.push(formatProxyCheckLine(check));
if (check.error) {
lines.push(` ${check.error}`);
}
lines.push(formatProxyCheckLine(check, colors));
}
}
const nextSteps = formatProxyValidationNextSteps(result);
if (nextSteps.length > 0) {
lines.push("", "Next steps");
lines.push("", colors.heading("Next steps"));
for (const nextStep of nextSteps) {
lines.push(` ${nextStep}`);
lines.push(` ${colors.warn(nextStep)}`);
}
}
@@ -227,6 +256,8 @@ export async function runProxyValidateCommand(opts: {
proxyUrl?: string;
allowedUrls?: string[];
deniedUrls?: string[];
apnsReachability?: boolean;
apnsAuthority?: string;
timeoutMs?: number;
}) {
const config = getRuntimeConfig();
@@ -236,6 +267,8 @@ export async function runProxyValidateCommand(opts: {
proxyUrlOverride: opts.proxyUrl,
allowedUrls: opts.allowedUrls,
deniedUrls: opts.deniedUrls,
apnsReachability: opts.apnsReachability,
apnsAuthority: opts.apnsAuthority,
timeoutMs: opts.timeoutMs,
});
const outputResult = redactProxyValidationResult(result);