mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
fix: tighten apns-id validation in proxy reachability check
This commit is contained in:
@@ -423,7 +423,9 @@ describe("proxy validation", () => {
|
||||
|
||||
it("adds an APNs reachability check when requested", async () => {
|
||||
const fetchCheck = vi.fn().mockResolvedValue({ ok: true, status: 200 });
|
||||
const apnsCheck = vi.fn().mockResolvedValue({ status: 403 });
|
||||
const apnsCheck = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ status: 403, apnsId: "00000000-0000-0000-0000-000000000000" });
|
||||
|
||||
const result = await runProxyValidation({
|
||||
config: {
|
||||
@@ -465,6 +467,30 @@ describe("proxy validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fails APNs reachability when response has no apns-id (proxy intercept)", async () => {
|
||||
const result = await runProxyValidation({
|
||||
config: {
|
||||
enabled: true,
|
||||
proxyUrl: "http://127.0.0.1:3128",
|
||||
},
|
||||
env: {},
|
||||
allowedUrls: [],
|
||||
deniedUrls: [],
|
||||
apnsReachability: true,
|
||||
apnsCheck: vi.fn().mockResolvedValue({ status: 200 }),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.checks).toEqual([
|
||||
{
|
||||
kind: "apns",
|
||||
url: "https://api.sandbox.push.apple.com",
|
||||
ok: false,
|
||||
error: expect.stringContaining("apns-id"),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails APNs reachability when the proxy blocks CONNECT", async () => {
|
||||
const result = await runProxyValidation({
|
||||
config: {
|
||||
|
||||
@@ -60,6 +60,8 @@ export type ProxyValidationApnsCheckParams = {
|
||||
|
||||
export type ProxyValidationApnsCheckResult = {
|
||||
status: number;
|
||||
/** Present when the response originated from a real APNs server (Apple always returns this UUID). */
|
||||
apnsId?: string;
|
||||
};
|
||||
|
||||
export type ProxyValidationApnsCheck = (
|
||||
@@ -201,7 +203,7 @@ async function defaultProxyValidationApnsCheck({
|
||||
timeoutMs,
|
||||
}: ProxyValidationApnsCheckParams): Promise<ProxyValidationApnsCheckResult> {
|
||||
const result = await probeApnsHttp2ReachabilityViaProxy({ proxyUrl, authority, timeoutMs });
|
||||
return { status: result.status };
|
||||
return { status: result.status, apnsId: result.responseHeaders?.["apns-id"] };
|
||||
}
|
||||
|
||||
function normalizeTimeoutMs(value: number | undefined): number {
|
||||
@@ -420,6 +422,16 @@ async function runApnsReachabilityCheck(params: {
|
||||
authority: params.authority,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
if (!result.apnsId) {
|
||||
return {
|
||||
kind: "apns",
|
||||
url: params.authority,
|
||||
ok: false,
|
||||
error:
|
||||
"APNs reachability check failed: response did not include an apns-id header. " +
|
||||
"The proxy may be intercepting the connection instead of tunneling it.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "apns",
|
||||
url: params.authority,
|
||||
|
||||
@@ -185,7 +185,11 @@ describe("connectApnsHttp2Session", () => {
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ status: 403, body: '{"reason":"InvalidProviderToken"}' });
|
||||
expect(result).toEqual({
|
||||
status: 403,
|
||||
body: '{"reason":"InvalidProviderToken"}',
|
||||
responseHeaders: {},
|
||||
});
|
||||
const tunnelCall = tunnelSpy.mock.calls.at(-1)?.[0];
|
||||
const proxyUrl = tunnelCall?.proxyUrl;
|
||||
expect(proxyUrl).toBeInstanceOf(URL);
|
||||
|
||||
@@ -30,6 +30,8 @@ export type ProbeApnsHttp2ReachabilityViaProxyParams = {
|
||||
export type ProbeApnsHttp2ReachabilityViaProxyResult = {
|
||||
status: number;
|
||||
body: string;
|
||||
/** Raw response headers from APNs. Includes apns-id when the connection was truly tunneled to Apple. */
|
||||
responseHeaders: Record<string, string>;
|
||||
};
|
||||
|
||||
function assertApnsAuthority(authority: string): ApnsAuthority {
|
||||
@@ -96,6 +98,7 @@ export async function probeApnsHttp2ReachabilityViaProxy(
|
||||
let settled = false;
|
||||
let body = "";
|
||||
let status: number | undefined;
|
||||
let responseHeaders: Record<string, string> = {};
|
||||
const timeout = setTimeout(() => {
|
||||
fail(
|
||||
new Error(`APNs reachability probe timed out after ${Math.trunc(params.timeoutMs)}ms`),
|
||||
@@ -132,6 +135,11 @@ export async function probeApnsHttp2ReachabilityViaProxy(
|
||||
request.on("response", (headers) => {
|
||||
const rawStatus = headers[":status"];
|
||||
status = typeof rawStatus === "number" ? rawStatus : Number(rawStatus);
|
||||
responseHeaders = Object.fromEntries(
|
||||
Object.entries(headers)
|
||||
.filter(([k]) => !k.startsWith(":"))
|
||||
.map(([k, v]) => [k, String(v)]),
|
||||
);
|
||||
});
|
||||
request.on("data", (chunk) => {
|
||||
body += String(chunk);
|
||||
@@ -147,7 +155,7 @@ export async function probeApnsHttp2ReachabilityViaProxy(
|
||||
reject(new Error("APNs reachability probe ended without an HTTP/2 status"));
|
||||
return;
|
||||
}
|
||||
resolve({ status, body });
|
||||
resolve({ status, body, responseHeaders });
|
||||
});
|
||||
request.end(JSON.stringify({ aps: { alert: "OpenClaw APNs proxy validation" } }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user