UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

219 lines (218 loc) 8.85 kB
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 };