Ctrl K
ComunidadGuías…

scripts/prepare-cloudflared.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Stage a pinned Cloudflare Tunnel connector for every desktop architecture
// electron-builder packages on this host; --current stages only this host's
// platform and architecture. Verified before extraction and on every reuse.
// cloudflared's own updater stays disabled.
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
  chmodSync,
  copyFileSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  renameSync,
  rmSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

export const CLOUDFLARED_VERSION = "2026.10.3";

export const CLOUDFLARED_ASSETS = Object.freeze({
  "darwin-arm64": Object.freeze({
    name: "cloudflared-darwin-arm64.tgz",
    sha256: "1165d14b6f6360a3b8c283d7d1100024d6ec7cd9bc9e32519e96150645f3ec55",
    binarySha256: "f249139e0541fa589c987397d867cc16c83ab1236d31eb6782f2e4552ee9cf80",
    archive: true,
  }),
  "darwin-x64": Object.freeze({
    name: "cloudflared-darwin-amd64.tgz",
    sha256: "81b7b85f2376815c4f59b1aa323887b62e16ce8b67555c2e1577dc23f2a82000",
    binarySha256: "947e2942d6a59922d80190d23b12f3ade8da16e23e3bd937182a48993b7c00db",
    archive: true,
  }),
  "linux-x64": Object.freeze({
    name: "cloudflared-linux-amd64",
    sha256: "053b80b0a23031a038fdbf3c073f59f82b8f1f152aa67971d7383ffafc0dbab0",
    binarySha256: "053b80b0a23031a038fdbf3c073f59f82b8f1f152aa67971d7383ffafc0dbab0",
    archive: false,
  }),
  "linux-arm64": Object.freeze({
    name: "cloudflared-linux-arm64",
    sha256: "89297ebfb9e8c99c0dc013a1140601453cf3798d8306c5591651f4a3f5210be8",
    binarySha256: "89297ebfb9e8c99c0dc013a1140601453cf3798d8306c5591651f4a3f5210be8",
    archive: false,
  }),
  "win32-x64": Object.freeze({
    name: "cloudflared-windows-amd64.exe",
    sha256: "76b2805253d0d6ffacb092a7acfbee99e7af6917835c0c5410e23f3ef5052da2",
    binarySha256: "76b2805253d0d6ffacb092a7acfbee99e7af6917835c0c5410e23f3ef5052da2",
    archive: false,
  }),
});

export function targetsForHost(platform) {
  // Desktop package targets only. Self-hosted installs use --current instead.
  if (platform === "darwin") return ["darwin-arm64", "darwin-x64"];
  if (platform === "linux") return ["linux-x64"];
  if (platform === "win32") return ["win32-x64"];
  throw new Error(`Cloudflare Tunnel packaging is unsupported on ${platform}`);
}

export function targetForCurrentHost(platform = process.platform, arch = process.arch) {
  const target = `${platform}-${arch}`;
  if (!Object.hasOwn(CLOUDFLARED_ASSETS, target)) {
    throw new Error(`Cloudflare Tunnel development is unsupported on ${target}`);
  }
  return target;
}

export function targetsForPreparation({
  current = false,
  platform = process.platform,
  arch = process.arch,
} = {}) {
  return current ? [targetForCurrentHost(platform, arch)] : targetsForHost(platform);
}

export function parsePrepareCloudflaredArgs(args = []) {
  const options = { current: false };
  for (let index = 0; index < args.length; index += 1) {
    const argument = args[index];
    const next = args[index + 1];
    if (argument === "--current" && !options.current) {
      options.current = true;
    } else if (argument === "--root" && !options.root && typeof next === "string" && next !== "") {
      // `openmausbot serve --tunnel` stages into its data dir, not a checkout.
      options.root = next;
      index += 1;
    } else {
      throw new Error("Usage: node scripts/prepare-cloudflared.mjs [--current] [--root DIR]");
    }
  }
  return options;
}

export function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

export function verifySha256(value, expected, label = "cloudflared asset") {
  const actual = sha256(value);
  if (actual !== expected) {
    throw new Error(`${label} failed SHA-256 verification (expected ${expected}, received ${actual})`);
  }
  return actual;
}

const executableName = (target) => (target.startsWith("win32-") ? "cloudflared.exe" : "cloudflared");

/** Identify the target from the executable header without running it, so a
 * correctly checksummed asset is never staged into the wrong resource directory. */
export function executableTarget(value) {
  const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);

  // 64-bit little-endian Mach-O. CPU types include ABI64 (0x01000000).
  if (bytes.length >= 8 && bytes.readUInt32LE(0) === 0xfeedfacf) {
    const cpu = bytes.readUInt32LE(4);
    if (cpu === 0x0100000c) return "darwin-arm64";
    if (cpu === 0x01000007) return "darwin-x64";
    throw new Error(`unsupported cloudflared Mach-O CPU type 0x${cpu.toString(16)}`);
  }

  // ELF64, little-endian: AMD64 (EM_X86_64 = 62) or ARM64 (EM_AARCH64 = 183).
  if (
    bytes.length >= 20 &&
    bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) &&
    bytes[4] === 2 &&
    bytes[5] === 1
  ) {
    const machine = bytes.readUInt16LE(18);
    if (machine === 62) return "linux-x64";
    if (machine === 183) return "linux-arm64";
  }

  // PE32+ AMD64. e_lfanew points from the DOS header to PE\0\0.
  if (bytes.length >= 0x40 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
    const pe = bytes.readUInt32LE(0x3c);
    if (
      pe <= bytes.length - 6 &&
      bytes.subarray(pe, pe + 4).equals(Buffer.from([0x50, 0x45, 0, 0])) &&
      bytes.readUInt16LE(pe + 4) === 0x8664
    ) {
      return "win32-x64";
    }
  }

  throw new Error("cloudflared release has an unsupported executable format or architecture");
}

export function verifyPinnedBinary(value, target) {
  const asset = CLOUDFLARED_ASSETS[target];
  if (!asset) throw new Error(`No pinned cloudflared asset for ${target}`);
  const actualTarget = executableTarget(value);
  if (actualTarget !== target) {
    throw new Error(`cloudflared architecture mismatch (expected ${target}, received ${actualTarget})`);
  }
  return verifySha256(value, asset.binarySha256, `${target} cloudflared executable`);
}

function expectedManifest(target) {
  const asset = CLOUDFLARED_ASSETS[target];
  return {
    version: CLOUDFLARED_VERSION,
    target,
    releaseAsset: asset.name,
    releaseAssetSha256: asset.sha256,
    binarySha256: asset.binarySha256,
  };
}

function targetRunsOnHost(target, platform = process.platform, arch = process.arch) {
  return target === `${platform}-${arch}`;
}

function executableHasPinnedVersion(binary, target) {
  // A dual-architecture macOS package is prepared in one invocation. Do not
  // assume Rosetta is installed or attempt to execute the other architecture;
  // its executable bytes are still pinned and its Mach-O header is checked.
  if (!targetRunsOnHost(target)) return true;
  const result = spawnSync(binary, ["version"], {
    encoding: "utf8",
    windowsHide: true,
    timeout: 10_000,
  });
  return result.status === 0 && `${result.stdout}\n${result.stderr}`.includes(CLOUDFLARED_VERSION);
}

export function verifyCloudflaredExecutable(binary, target) {
  verifyPinnedBinary(readFileSync(binary), target);
  if (!executableHasPinnedVersion(binary, target)) {
    throw new Error(`${target} executable did not identify as cloudflared ${CLOUDFLARED_VERSION}`);
  }
}

function executableIsCurrent(binary, manifestFile, target) {
  if (!existsSync(binary) || !existsSync(manifestFile)) return false;
  try {
    const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
    if (JSON.stringify(manifest) !== JSON.stringify(expectedManifest(target))) return false;
    verifyCloudflaredExecutable(binary, target);
    return true;
  } catch {
    return false;
  }
}

function extractionFailure(result) {
  return result.error?.message ?? String(result.stderr || result.stdout || `exit status ${result.status}`).trim();
}

/** Retry transient download failures; callers still verify the pinned digest. */
export async function releaseBytes(asset) {
  const cacheDirectory = process.env.OMB_CLOUDFLARED_ARCHIVE_DIR;
  const cached = cacheDirectory ? join(cacheDirectory, asset.name) : "";
  if (cached && existsSync(cached)) return readFileSync(cached);

  const url = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset.name}`;
  const retryableStatuses = new Set([408, 429, 500, 502, 503, 504]);
  for (let attempt = 1; attempt <= 3; attempt++) {
    let failure;
    try {
      const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
      if (response.ok) return Buffer.from(await response.arrayBuffer());
      await response.body?.cancel().catch(() => {});
      failure = new Error(`could not download ${asset.name}: HTTP ${response.status}`);
      if (!retryableStatuses.has(response.status)) throw failure;
    } catch (error) {
      // Node fetch reports network/body failures as TypeError; each attempt
      // has its own timeout, including reading the complete response body.
      if (!(error instanceof TypeError) && !["TimeoutError", "AbortError"].includes(error?.name)) throw error;
      failure = error;
    }
    if (attempt === 3) throw failure;
    const delay = 1_000 * 2 ** (attempt - 1);
    console.warn(`cloudflared download attempt ${attempt}/3 failed: ${failure.message}; retrying in ${delay}ms`);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

async function stageTarget(root, target) {
  const asset = CLOUDFLARED_ASSETS[target];
  if (!asset) throw new Error(`No pinned cloudflared asset for ${target}`);

  const finalDirectory = join(root, "dist-native", "cloudflared", target);
  const binary = join(finalDirectory, executableName(target));
  const manifestFile = join(finalDirectory, "manifest.json");
  if (executableIsCurrent(binary, manifestFile, target)) {
    console.log(`cloudflared ${CLOUDFLARED_VERSION} already staged for ${target}`);
    return;
  }

  const scratch = mkdtempSync(join(tmpdir(), `openmaus-cloudflared-${target}-`));
  try {
    const payload = await releaseBytes(asset);
    verifySha256(payload, asset.sha256, asset.name);

    let candidate;
    if (asset.archive) {
      const archive = join(scratch, basename(asset.name));
      writeFileSync(archive, payload, { mode: 0o600 });
      const extracted = join(scratch, "extracted");
      mkdirSync(extracted, { mode: 0o700 });
      const result = spawnSync("tar", ["-xzf", archive, "-C", extracted], {
        encoding: "utf8",
        windowsHide: true,
        timeout: 60_000,
      });
      if (result.status !== 0) throw new Error(`could not extract ${asset.name}: ${extractionFailure(result)}`);
      candidate = join(extracted, "cloudflared");
    } else {
      candidate = join(scratch, executableName(target));
      writeFileSync(candidate, payload, { mode: 0o700 });
    }
    if (!existsSync(candidate)) throw new Error(`${asset.name} did not contain cloudflared`);
    if (!target.startsWith("win32-")) chmodSync(candidate, 0o700);

    verifyCloudflaredExecutable(candidate, target);

    const parent = dirname(finalDirectory);
    mkdirSync(parent, { recursive: true });
    const stagedDirectory = mkdtempSync(join(parent, `.${target}-`));
    const stagedBinary = join(stagedDirectory, executableName(target));
    copyFileSync(candidate, stagedBinary);
    if (!target.startsWith("win32-")) {
      // copyFile preserves the source mode on Unix in current Node, but make
      // the executable contract explicit instead of depending on that detail.
      chmodSync(stagedBinary, 0o755);
    }
    writeFileSync(
      join(stagedDirectory, "manifest.json"),
      `${JSON.stringify(expectedManifest(target), null, 2)}\n`,
      { mode: 0o600 },
    );
    rmSync(finalDirectory, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
    renameSync(stagedDirectory, finalDirectory);
    console.log(`staged cloudflared ${CLOUDFLARED_VERSION} for ${target}`);
  } finally {
    // Windows Defender can briefly retain the executable after the version
    // probe exits. Node retries EPERM for recursive removals when asked.
    rmSync(scratch, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
  }
}

export async function prepareCloudflared({
  root = join(dirname(fileURLToPath(import.meta.url)), ".."),
  platform = process.platform,
  arch = process.arch,
  current = false,
} = {}) {
  for (const target of targetsForPreparation({ current, platform, arch })) {
    await stageTarget(root, target);
  }
}

if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
  await prepareCloudflared(parsePrepareCloudflaredArgs(process.argv.slice(2)));
}