Files

74 lines
5.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { PIN, refuse, safePath, sha256, regularBytes } from './common.mjs';
import { installPaths } from './settings.mjs';
const RELEASE_HOSTS = new Set(['github.com', 'release-assets.githubusercontent.com', 'objects.githubusercontent.com']);
const releaseUrl = u => u.protocol === 'https:' && !u.username && !u.password && !u.port && !u.hash
&& u.hostname === 'github.com' && /^\/router-for-me\/(CLIProxyAPI|Cli-Proxy-API-Management-Center)\/releases\/download\/[^/]+\/[^/]+$/.test(u.pathname);
// testOrigin is an in-process local fake-server seam, never read from argv/config/env.
export async function downloadRelease(url, { testOrigin } = {}) {
let current; try { current = new URL(url); } catch { refuse('proxy-fetch-url'); }
const local = testOrigin && /^http:\/\/127\.0\.0\.1:[0-9]+$/.test(testOrigin) ? testOrigin : null;
if (!(local ? current.origin === local : releaseUrl(current))) refuse('proxy-fetch-url');
for (let redirects = 0; redirects <= 5; redirects++) {
let response;
try { response = await fetch(current, { redirect: 'manual', signal: AbortSignal.timeout(120000) }); }
catch { refuse('proxy-fetch-failed'); }
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('location'); await response.body?.cancel();
let next; try { if (!location) throw Error(); next = new URL(location, current); } catch { refuse('proxy-fetch-redirect'); }
if (next.username || next.password || next.hash || (local ? next.origin !== local : next.protocol !== 'https:' || next.port || !RELEASE_HOSTS.has(next.hostname))) refuse('proxy-fetch-redirect');
current = next; continue;
}
if (!response.ok) { await response.body?.cancel(); refuse('proxy-fetch-failed'); }
const reader = response.body.getReader(), chunks = []; let size = 0;
try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > 256 * 1024 * 1024) { await reader.cancel(); refuse('proxy-fetch-failed'); } chunks.push(Buffer.from(value)); } }
catch { refuse('proxy-fetch-failed'); }
return Buffer.concat(chunks);
}
refuse('proxy-fetch-redirect');
}
export async function fetchPinned({ dataRoot, pin = PIN, download = downloadRelease }) {
safePath(dataRoot, 'proxy-fetch-existing');
const paths = installPaths(dataRoot, pin);
safePath(paths.dir, 'proxy-fetch-existing');
// Fetch and verify both assets before any install filesystem mutation.
const archive = await download(pin.archive.url);
if (sha256(archive) !== pin.archive.sha256) refuse('proxy-fetch-hash');
const panel = await download(pin.panel.url);
if (sha256(panel) !== pin.panel.sha256) refuse('proxy-fetch-hash');
let binary;
try {
const listing = execFileSync('tar', ['-tzf', '-'], { input: archive, maxBuffer: 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }).toString().split('\n').filter(Boolean);
if (listing.filter(x => x === 'cli-proxy-api').length !== 1) refuse('proxy-fetch-archive');
// Only the root executable is read to stdout. No archive path is extracted to disk.
binary = execFileSync('tar', ['-xOzf', '-', 'cli-proxy-api'], { input: archive, maxBuffer: 256 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
} catch { refuse('proxy-fetch-archive'); }
if (sha256(binary) !== pin.binary.sha256) refuse('proxy-fetch-hash');
try {
fs.lstatSync(paths.dir);
if (fs.readdirSync(paths.dir).sort().join(',') !== 'cli-proxy-api,static'
|| fs.readdirSync(path.join(paths.dir, 'static')).join(',') !== 'management.html'
|| sha256(regularBytes(paths.binary, 'proxy-fetch-existing')) !== pin.binary.sha256
|| sha256(regularBytes(paths.panel, 'proxy-fetch-existing')) !== pin.panel.sha256) refuse('proxy-fetch-existing');
return paths;
} catch (e) { if (e.code !== 'ENOENT') throw e; }
const parent = path.dirname(paths.dir);
fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
const stage = fs.mkdtempSync(path.join(parent, '.fetch-'));
// Lock the version name with exclusive mkdir; a competing installer cannot overwrite it.
let ownsTarget = false;
try {
fs.writeFileSync(path.join(stage, 'cli-proxy-api'), binary, { flag: 'wx', mode: 0o755 });
fs.mkdirSync(path.join(stage, 'static'), { mode: 0o700 });
fs.writeFileSync(path.join(stage, 'static', 'management.html'), panel, { flag: 'wx', mode: 0o644 });
try { fs.mkdirSync(paths.dir, { mode: 0o700 }); ownsTarget = true; } catch { refuse('proxy-fetch-existing'); }
// Same-filesystem rename of each already-verified file; any failure removes only our target.
fs.renameSync(path.join(stage, 'cli-proxy-api'), paths.binary);
fs.renameSync(path.join(stage, 'static'), path.join(paths.dir, 'static'));
return paths;
} catch (e) { if (ownsTarget) fs.rmSync(paths.dir, { recursive: true }); if (e.code?.startsWith('proxy-')) throw e; refuse('proxy-fetch-failed'); }
finally { fs.rmSync(stage, { recursive: true, force: true }); }
}