Created
September 18, 2025 23:39
-
-
Save acrogenesis/c287f57f0c6e484413d1043ea0691085 to your computer and use it in GitHub Desktop.
Cloudflare worker script to proxy nerves firmware updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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