openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
219 lines (218 loc) • 8.85 kB
JavaScript
import { F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { t as decodeTextPrefix } from "./src-vebZIeLe.js";
import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js";
import "./errors-Db3Ymjlb.js";
import { n as getActiveManagedProxyTlsOptions, r as getActiveManagedProxyUrl } from "./active-proxy-state-DJLhrP_Z.js";
import { once } from "node:events";
import tls from "node:tls";
import { openProxyConnectTunnel } from "@openclaw/proxyline";
import http2 from "node:http2";
//#region src/infra/push-apns-http2.ts
const APNS_DEFAULT_PORT = "443";
const APNS_AUTHORITIES = /* @__PURE__ */ new Set(["https://api.push.apple.com", "https://api.sandbox.push.apple.com"]);
const APNS_HTTP2_CANCEL_CODE = http2.constants.NGHTTP2_CANCEL;
const APNS_RESPONSE_BODY_MAX_BYTES = 8192;
const APNS_HTTP2_MIN_TIMEOUT_MS = 1e3;
function apnsAbortError(signal) {
return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("APNs send invalidated");
}
function assertApnsAuthority(authority) {
let parsed;
try {
parsed = new URL(authority);
} catch {
throw new Error(`Unsupported APNs authority: ${authority}`);
}
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) throw new Error(`Unsupported APNs authority: ${authority}`);
const port = parsed.port && parsed.port !== APNS_DEFAULT_PORT ? `:${parsed.port}` : "";
const normalized = `${parsed.protocol}//${parsed.hostname}${port}`;
if (!APNS_AUTHORITIES.has(normalized)) throw new Error(`Unsupported APNs authority: ${authority}`);
return normalized;
}
function normalizeConnectProxyUrl(proxyUrl) {
const normalized = new URL(proxyUrl);
normalized.pathname = "/";
normalized.search = "";
normalized.hash = "";
try {
decodeURIComponent(normalized.username);
decodeURIComponent(normalized.password);
} catch (err) {
throw new Error(`Proxy CONNECT failed via ${normalized.origin}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
}
return normalized;
}
async function openApnsTlsTunnel(params) {
const proxyUrl = normalizeConnectProxyUrl(params.proxyUrl);
const deadline = Date.now() + params.timeoutMs;
const proxySocket = await openProxyConnectTunnel({
proxyUrl,
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
targetHost: params.targetHost,
targetPort: params.targetPort,
timeoutMs: params.timeoutMs,
...params.signal ? { signal: params.signal } : {}
});
const abortController = new AbortController();
const abortFromCaller = () => {
if (params.signal) abortController.abort(apnsAbortError(params.signal));
};
params.signal?.addEventListener("abort", abortFromCaller, { once: true });
if (params.signal?.aborted) abortFromCaller();
let targetTlsSocket;
let timeout;
try {
targetTlsSocket = tls.connect({
socket: proxySocket,
servername: params.targetHost,
ALPNProtocols: ["h2"]
});
timeout = setTimeout(() => abortController.abort(/* @__PURE__ */ new Error(`Proxy CONNECT timed out after ${params.timeoutMs}ms`)), Math.max(1, deadline - Date.now()));
timeout.unref?.();
await Promise.race([once(targetTlsSocket, "secureConnect", { signal: abortController.signal }), once(targetTlsSocket, "close", { signal: abortController.signal }).then(() => {
throw new Error("APNs TLS tunnel closed before secureConnect");
})]);
if (targetTlsSocket.alpnProtocol !== "h2") throw new Error(`APNs TLS tunnel negotiated ${targetTlsSocket.alpnProtocol || "no ALPN protocol"} instead of h2`);
return targetTlsSocket;
} catch (err) {
targetTlsSocket?.destroy();
proxySocket.destroy();
const failure = abortController.signal.aborted ? abortController.signal.reason : err;
throw new Error(`Proxy CONNECT failed via ${proxyUrl.origin}: ${failure instanceof Error ? failure.message : String(failure)}`, { cause: err });
} finally {
if (timeout) clearTimeout(timeout);
params.signal?.removeEventListener("abort", abortFromCaller);
abortController.abort();
}
}
async function openProxiedApnsHttp2Session(params) {
const apnsHost = new URL(params.authority).hostname;
const tlsSocket = await openApnsTlsTunnel({
proxyUrl: params.proxyUrl,
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
targetHost: apnsHost,
targetPort: 443,
timeoutMs: params.timeoutMs,
...params.signal ? { signal: params.signal } : {}
});
if (params.signal?.aborted) {
tlsSocket.destroy();
throw apnsAbortError(params.signal);
}
return http2.connect(params.authority, { createConnection: () => tlsSocket });
}
/** Connects to APNs directly, or through the active managed proxy when present. */
async function connectApnsHttp2Session(params) {
const authority = assertApnsAuthority(params.authority);
const timeoutMs = resolveApnsHttp2TimeoutMs(params.timeoutMs);
const proxyUrl = getActiveManagedProxyUrl();
if (!proxyUrl) {
if (params.signal?.aborted) throw apnsAbortError(params.signal);
return http2.connect(authority);
}
return await openProxiedApnsHttp2Session({
authority,
proxyUrl,
proxyTls: getActiveManagedProxyTlsOptions(),
timeoutMs,
...params.signal ? { signal: params.signal } : {}
});
}
function resolveApnsHttp2TimeoutMs(timeoutMs) {
return resolveTimerTimeoutMs(timeoutMs, APNS_HTTP2_MIN_TIMEOUT_MS, APNS_HTTP2_MIN_TIMEOUT_MS);
}
function createApnsResponseBodyCapture() {
return {
chunks: [],
capturedBytes: 0,
bytes: 0,
truncated: false
};
}
function appendApnsResponseBodyCapture(capture, chunk, maxBytes = APNS_RESPONSE_BODY_MAX_BYTES) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
capture.bytes += buffer.byteLength;
const remaining = maxBytes - capture.capturedBytes;
if (remaining <= 0) {
capture.truncated = capture.truncated || buffer.byteLength > 0;
return;
}
const slice = buffer.byteLength > remaining ? buffer.subarray(0, remaining) : buffer;
capture.chunks.push(Buffer.from(slice));
capture.capturedBytes += slice.byteLength;
if (slice.byteLength < buffer.byteLength) capture.truncated = true;
}
function getApnsResponseBodyCaptureText(capture) {
return decodeTextPrefix(Buffer.concat(capture.chunks, capture.capturedBytes), { truncated: capture.truncated });
}
/** Sends an intentionally invalid APNs push through a proxy to prove HTTP/2 reachability. */
async function probeApnsHttp2ReachabilityViaProxy(params) {
const authority = assertApnsAuthority(params.authority);
const timeoutMs = resolveApnsHttp2TimeoutMs(params.timeoutMs);
const session = await openProxiedApnsHttp2Session({
authority,
proxyUrl: new URL(params.proxyUrl),
...params.proxyTls ? { proxyTls: params.proxyTls } : {},
timeoutMs
});
try {
return await new Promise((resolve, reject) => {
let settled = false;
const body = createApnsResponseBodyCapture();
let status;
let responseHeaders = {};
const timeout = setTimeout(() => {
fail(/* @__PURE__ */ new Error(`APNs reachability probe timed out after ${timeoutMs}ms`));
}, timeoutMs);
timeout.unref?.();
const cleanup = () => {
clearTimeout(timeout);
session.off("error", fail);
};
const fail = (err) => {
if (settled) return;
settled = true;
cleanup();
session.destroy(err instanceof Error ? err : new Error(String(err)));
reject(toErrorObject(err, "Non-Error rejection"));
};
const request = session.request({
":method": "POST",
":path": `/3/device/${"0".repeat(64)}`,
authorization: "bearer intentionally.invalid.openclaw.proxy.validation",
"apns-topic": "ai.openclaw.ios",
"apns-push-type": "alert",
"apns-priority": "10"
});
session.once("error", fail);
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) => {
appendApnsResponseBodyCapture(body, chunk);
});
request.once("error", fail);
request.once("end", () => {
if (settled) return;
settled = true;
cleanup();
if (status === void 0 || !Number.isFinite(status)) {
reject(/* @__PURE__ */ new Error("APNs reachability probe ended without an HTTP/2 status"));
return;
}
resolve({
status,
body: getApnsResponseBodyCaptureText(body),
responseHeaders
});
});
request.end(JSON.stringify({ aps: { alert: "OpenClaw APNs proxy validation" } }));
});
} finally {
if (!session.closed && !session.destroyed) session.close();
}
}
//#endregion
export { getApnsResponseBodyCaptureText as a, createApnsResponseBodyCapture as i, appendApnsResponseBodyCapture as n, probeApnsHttp2ReachabilityViaProxy as o, connectApnsHttp2Session as r, APNS_HTTP2_CANCEL_CODE as t };