/** * Edge middleware for cran.rpkgs.com. * * Two jobs: * * 1. Resolve the bare `https://cran.rpkgs.com` form to a concrete * `/` slot from the User-Agent, or send the client to CRAN when * the distro cannot be identified. * 2. Route `PACKAGES*` requests to the per-R-minor slot * (`…/latest/src/contrib//`), so a stock `install.packages()` sees the * packages that only exist there. * * Only index files are routed. Tarballs are deliberately left alone: R keeps * the contrib URL it asked for, not the one it was redirected to, so every * tarball URL is resolved against the flat directory and the union index steers * the per-minor ones with a `Path: ` field. Rewriting a tarball request * here would send flat-slot packages into a directory that does not hold them. * * Routing is gated on UNION_SLOTS. The raw per-minor index holds only the * ABI-sensitive subset of a slot; it is safe to route to it only once bincraft * has republished it as a union of the per-minor and flat slots. * * Deployed by OpenTofu from this file (`bunnynet_compute_script.rpkgs_router`). * Test with `just edge-test`. */ import * as BunnySDK from 'https://esm.sh/@bunny.net/edgescript-sdk@0.12'; const PUBLIC_CDN_ORIGIN = 'https://cran.rpkgs.com'; const CRAN_ORIGIN = 'https://cran.r-project.org'; /** Slots ("/", comma separated) whose per-minor index is a union. */ const UNION_SLOTS = new Set( (Deno.env.get('UNION_SLOTS') ?? '') .split(',') .map((slot) => slot.trim()) .filter((slot) => slot.length > 0), ); /** `///latest/src/contrib[/]` */ const SLOT_PATH_REGEX = /^\/(amd64|arm64)\/([a-z0-9._-]+)\/latest\/src\/contrib\/?(.*)$/; /** A path that already sits in a per-minor slot, e.g. `4.5/PACKAGES.gz`. */ const MINOR_DIR_REGEX = /^\d+\.\d+\//; /** The only files this script routes. */ const INDEX_FILE_REGEX = /^PACKAGES(\.gz|\.rds)?$/; const SRC_CONTRIB_REGEX = /^\/src\/contrib\/(.+)$/; const MACOS_BIN_REGEX = /^\/bin\/macosx\/(big-sur-arm64|big-sur-x86_64|monterey-arm64|monterey-x86_64)\/contrib\/([0-9.]+)\/(.+)$/; const RHEL_REGEX = /(almalinux|rocky)[^\d]*(\d+)/i; const UBUNTU_REGEX = /Ubuntu ([\d.]+)/i; const UBUNTU_CODENAMES: Record = { '24.04': 'noble', '22.04': 'jammy', }; const ALPINE_REGEX = /(?:Alpine Linux(?:\s+VERSION_ID=)?|alpine-)\s*(\d+)\.(\d+)/i; /** * R's own User-Agent is `R (4.5.3 x86_64-pc-linux-musl …)`; the Posit-style one * some sites configure is `R/4.5.3 R (…)`. Both carry the minor, which is why * per-minor routing works without the distro being identifiable. */ const R_MINOR_REGEXES = [/\bR\/(\d+)\.(\d+)/, /\bR \((\d+)\.(\d+)/]; function normalizePathname(pathname: string): string { return pathname.replace(/\/{2,}/g, '/'); } function redirectTo(location: string, status = 302): Response { return new Response(null, { status, headers: { Location: location, // The target depends on the User-Agent, so the redirect itself must // never be cached; only its target is a cacheable, UA-independent URL. 'Cache-Control': 'no-store', 'X-Via': 'MyMiddleware', 'X-Rewritten-By': 'rpkgs-edge-middleware', }, }); } function extractRMinor(userAgent: string): string | null { for (const regex of R_MINOR_REGEXES) { const match = userAgent.match(regex); if (match) { return `${match[1]}.${match[2]}`; } } return null; } function mapArch(arch: string): string { if (arch === 'aarch64') return 'arm64'; if (arch === 'x86_64') return 'amd64'; return arch; } function extractArch(userAgent: string): string { const match = userAgent.match(/(x86_64|aarch64|arm64|i386|i686)/); return match ? mapArch(match[1]) : ''; } /** * Identify the `/` slot from the User-Agent, or null. * * A stock R User-Agent carries only `linux-gnu` / `linux-musl`, which are not * slot names: returning them produced redirects into slots that do not exist * (`/amd64/linux-musl/latest/…`, a guaranteed 404). An unidentifiable distro * is reported as such so the caller can fall back to CRAN. */ function parseSlot(userAgent: string): string | null { const arch = extractArch(userAgent); if (!arch) { return null; } const rhel = userAgent.match(RHEL_REGEX); if (rhel) { return `${arch}/rhel${rhel[2]}`; } const ubuntu = userAgent.match(UBUNTU_REGEX); if (ubuntu) { const codename = UBUNTU_CODENAMES[ubuntu[1]]; if (codename) { return `${arch}/${codename}`; } } const alpine = userAgent.match(ALPINE_REGEX); if (alpine) { return `${arch}/alpine${alpine[1]}${alpine[2]}`; } return null; } function parseMacUserAgent(userAgent: string): { os: string; arch: string; rver: string } | null { const rverMatch = userAgent.match(/R \((\d+)\.(\d+)/); const archMatch = userAgent.match(/(aarch64|arm64|x86_64)/); const osMatch = userAgent.match(/darwin(\d+)/); if (!rverMatch || !archMatch || !osMatch) { return null; } const arch = archMatch[1] === 'aarch64' ? 'arm64' : archMatch[1]; const darwinVer = parseInt(osMatch[1], 10); const os = darwinVer >= 21 && darwinVer < 22 ? `monterey-${arch}` : `big-sur-${arch}`; return { os, arch, rver: `${rverMatch[1]}.${rverMatch[2]}` }; } /** * The contrib path a request should be served from, relative to the slot. * * Returns the per-minor path for an index file when the slot is known to carry * a union index and the client's R minor is known; otherwise the flat path, * which is what every client sees today. */ function contribPath(slot: string, rest: string, userAgent: string): string { const flat = rest ? `/${slot}/latest/src/contrib/${rest}` : `/${slot}/latest/src/contrib`; if (!INDEX_FILE_REGEX.test(rest) || !UNION_SLOTS.has(slot)) { return flat; } const rMinor = extractRMinor(userAgent); return rMinor ? `/${slot}/latest/src/contrib/${rMinor}/${rest}` : flat; } BunnySDK.net.http .servePullZone({ url: 'https://cran.rpkgs.com/' }) .onOriginRequest((ctx) => { const url = new URL(ctx.request.url); const path = normalizePathname(url.pathname); const userAgent = ctx.request.headers.get('User-Agent') || ''; // macOS clients are served from CRAN's own binary tree. const srcContrib = path.match(SRC_CONTRIB_REGEX); if (srcContrib && /darwin/.test(userAgent)) { const mac = parseMacUserAgent(userAgent); if (mac) { return Promise.resolve( redirectTo(`${PUBLIC_CDN_ORIGIN}/bin/macosx/${mac.os}/contrib/${mac.rver}/${srcContrib[1]}`), ); } } if (MACOS_BIN_REGEX.test(path)) { return Promise.resolve(redirectTo(`${CRAN_ORIGIN}${path}`)); } // Already-qualified slot URLs: what the runtime images have baked in. const slotPath = path.match(SLOT_PATH_REGEX); if (slotPath) { const slot = `${slotPath[1]}/${slotPath[2]}`; const rest = slotPath[3]; // Never rewrite a request that is already in a per-minor slot, or the // redirect would chase its own tail. if (MINOR_DIR_REGEX.test(rest)) { return Promise.resolve(ctx.request); } const target = contribPath(slot, rest, userAgent); if (target === path) { return Promise.resolve(ctx.request); } return Promise.resolve(redirectTo(`${PUBLIC_CDN_ORIGIN}${target}`)); } // The bare `https://cran.rpkgs.com` form, resolved from the User-Agent. if (path === '/' || path === '/src/contrib' || path.startsWith('/src/contrib/')) { const slot = parseSlot(userAgent); if (!slot) { return Promise.resolve(redirectTo(`${CRAN_ORIGIN}${path}`)); } const rest = srcContrib ? srcContrib[1] : ''; return Promise.resolve(redirectTo(`${PUBLIC_CDN_ORIGIN}${contribPath(slot, rest, userAgent)}`)); } return Promise.resolve(ctx.request); }) .onOriginResponse((ctx) => { ctx.response.headers.append('X-Via', 'MyMiddleware'); return Promise.resolve(ctx.response); });