Skip to content

Instantly share code, notes, and snippets.

@acrogenesis
Created September 18, 2025 23:39
Show Gist options
  • Save acrogenesis/c287f57f0c6e484413d1043ea0691085 to your computer and use it in GitHub Desktop.
Save acrogenesis/c287f57f0c6e484413d1043ea0691085 to your computer and use it in GitHub Desktop.
Cloudflare worker script to proxy nerves firmware updates
// Cloudflare Worker: firmware proxy
// Endpoint: https://example.com/download?firmware=[safe-encoded-url]
// Accepts either URL-encoded URL or base64url in the "firmware" param.
// Only proxies from https://files.nervescloud.com/firmware/...
const ALLOWED_HOST = "files.nervescloud.com";
const REQUIRED_PREFIX = "/firmware/";
export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
// Only one route: /download
if (url.pathname !== "/download") {
return jsonError(404, "Route not found");
}
// Only allow GET/HEAD
if (!["GET", "HEAD"].includes(request.method)) {
return jsonError(405, "Method not allowed", {
"Allow": "GET, HEAD",
});
}
// Read & decode the firmware URL
const raw = url.searchParams.get("firmware");
if (!raw) return jsonError(400, "Missing 'firmware' query parameter");
const targetStr = decodeFirmwareParam(raw);
let target;
try {
target = new URL(targetStr);
} catch {
return jsonError(400, "Invalid firmware URL after decoding");
}
// Security: enforce allowlist
if (target.protocol !== "https:") {
return jsonError(400, "Only https URLs are allowed");
}
if (target.hostname !== ALLOWED_HOST) {
return jsonError(403, "Origin host not allowed");
}
if (!target.pathname.startsWith(REQUIRED_PREFIX)) {
return jsonError(403, "Path not allowed (must start with /firmware/)");
}
// Build upstream request; pass through Range and If-* headers for resumable downloads & caching
const upstreamHeaders = new Headers();
copyHeaderIfPresent(request.headers, upstreamHeaders, "range");
copyHeaderIfPresent(request.headers, upstreamHeaders, "if-none-match");
copyHeaderIfPresent(request.headers, upstreamHeaders, "if-modified-since");
copyHeaderIfPresent(request.headers, upstreamHeaders, "if-range");
copyHeaderIfPresent(request.headers, upstreamHeaders, "accept-encoding"); // let CF handle it, but passing is fine
// Fetch from origin with Cloudflare cache hints
const upstream = await fetch(target.toString(), {
method: request.method,
headers: upstreamHeaders,
redirect: "follow",
cf: {
// Cache successful responses; avoid caching 5xx
cacheEverything: true,
cacheTtlByStatus: {
"200-299": 86400, // 1 day
"304": 86400,
"404": 60,
"500-599": 0,
},
},
});
// Pass-through most headers safely
const respHeaders = new Headers(upstream.headers);
// Force a download filename only if origin didn't set it
if (!respHeaders.has("content-disposition")) {
const filename = inferFilename(target.pathname) || "firmware.bin";
respHeaders.set(
"Content-Disposition",
`attachment; filename="${sanitizeFilename(filename)}"`
);
}
// Optional: allow cross-origin fetches if you serve from browsers
// (Devices typically don't need CORS, but this doesn't hurt.)
respHeaders.set("Access-Control-Allow-Origin", "*");
// Return streaming response as-is
return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: respHeaders,
});
} catch (err) {
// Avoid leaking internals
return jsonError(500, "Internal error");
}
},
};
/** Helpers **/
function copyHeaderIfPresent(from, to, name) {
const v = from.get(name);
if (v) to.set(name, v);
}
// Accept either:
// 1) Plain URL-encoded string, e.g. encodeURIComponent("https://files.nervescloud.com/firmware/x.bin")
// 2) Base64url-encoded string, e.g. base64url("https://files.nervescloud.com/firmware/x.bin")
function decodeFirmwareParam(input) {
// Try percent-decoding first (safe for already-decoded strings too)
let decoded = input;
try {
decoded = decodeURIComponent(input);
} catch (_) {
// ignore
}
// If it looks like a URL already, use it
if (looksLikeUrl(decoded)) return decoded;
// Otherwise try base64url
try {
const b64 = decoded.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(decoded.length / 4) * 4, "=");
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const asString = new TextDecoder().decode(bytes);
if (looksLikeUrl(asString)) return asString;
} catch (_) {
// fall through
}
return decoded; // let URL() validation throw if it's still not a URL
}
function looksLikeUrl(s) {
return typeof s === "string" && (s.startsWith("http://") || s.startsWith("https://"));
}
function inferFilename(pathname) {
const last = pathname.split("/").filter(Boolean).pop();
return last || null;
}
function sanitizeFilename(name) {
// Very light sanitization for header context
return name.replace(/[\r\n"]/g, "_");
}
function jsonError(status, message, extraHeaders = {}) {
return new Response(JSON.stringify({ error: message }), {
status,
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
...extraHeaders,
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment