Skip to content

Commit

Permalink
Allow arbitary CD-ROMs to be loaded via a URL parameter
Browse files Browse the repository at this point in the history
Does an on-demand fetch of the source CD-ROM URL (exposed as a
`PUT /CD-ROM/<url>` API endpoint) and generates a CD-ROM manifest for
it (main thing that's needed is the total file size).

Updates #166
Fixes #87
  • Loading branch information
mihaip committed Jun 20, 2023
1 parent 7c914d6 commit 656d682
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 21 deletions.
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function App() {
debugAudio,
ethernetProvider,
showCDROMs,
cdromURL,
] = useMemo(() => {
const searchParams = new URLSearchParams(location.search);
const url = searchParams.get("url") ?? location.href;
Expand All @@ -32,13 +33,15 @@ function App() {
? new BroadcastChannelEthernetProvider()
: undefined;
const showCDROMs = searchParams.get("cdroms") === "true";
const cdromURL = searchParams.get("cdrom_url") ?? undefined;
return [
url,
runDefFromUrl(url),
useSharedMemory,
debugAudio,
ethernetProvider,
showCDROMs,
cdromURL,
];
}, []);
const [runDef, setRunDef] = useState<BrowserRunDef | undefined>(
Expand Down Expand Up @@ -79,6 +82,7 @@ function App() {
? (cdromsManifest as any as EmulatorCDROMLibrary)
: undefined
}
cdromURL={cdromURL}
/>
);
footer = <Footer onLogoClick={handleDone} />;
Expand Down
23 changes: 23 additions & 0 deletions src/Mac.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type MacProps = {
debugAudio?: boolean;
onDone?: () => void;
cdroms?: EmulatorCDROMLibrary;
cdromURL?: string;
};

export default function Mac({
Expand All @@ -54,6 +55,7 @@ export default function Mac({
debugAudio,
onDone,
cdroms,
cdromURL,
}: MacProps) {
const screenRef = useRef<HTMLCanvasElement>(null);
const [emulatorLoaded, setEmulatorLoaded] = useState(false);
Expand Down Expand Up @@ -360,6 +362,16 @@ export default function Mac({
emulatorRef.current?.loadCDROM(cdrom);
}

useEffect(() => {
if (emulatorLoaded && cdromURL) {
fetchCDROMInfo(cdromURL)
.then(loadCDROM)
.catch((error: unknown) => {
setEmulatorErrorText(`Could not load the CD-ROM: ${error}`);
});
}
}, [cdromURL, emulatorLoaded]);

// Can't use media queries because they would need to depend on the
// emulator screen size, but we can't pass that in via CSS variables. We
// could in theory dynamically generate the media query via JS, but it's not
Expand Down Expand Up @@ -715,3 +727,14 @@ function MacCDROM({cdrom, onLoad}: {cdrom: EmulatorCDROM; onLoad: () => void}) {
</div>
);
}

async function fetchCDROMInfo(cdromURL: string): Promise<EmulatorCDROM> {
const response = await fetch(`/CD-ROM/${btoa(cdromURL)}`, {
method: "PUT",
});
if (!response.ok) {
throw new Error(await response.text());
}
const cdrom = await response.json();
return cdrom;
}
10 changes: 7 additions & 3 deletions src/vite-dev-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function viteDevMiddleware(
handleVarz(req, res);
return true;
} else if (url.pathname.startsWith("/CD-ROM/")) {
handleCDROM(url.pathname, res);
handleCDROM(url.pathname, req.method ?? "GET", res);
return true;
}

Expand All @@ -42,8 +42,12 @@ function handleVarz(req: http.IncomingMessage, res: http.ServerResponse) {
).end();
}

async function handleCDROM(path: string, res: http.ServerResponse) {
const response = await cdrom.handleRequest(path);
async function handleCDROM(
path: string,
method: string,
res: http.ServerResponse
) {
const response = await cdrom.handleRequest(path, method);
const {status, statusText, headers} = response;
res.writeHead(status, statusText, Object.fromEntries(headers.entries()));
const body = await response.arrayBuffer();
Expand Down
90 changes: 73 additions & 17 deletions workers-site/cd-rom.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
export async function handleRequest(path: string) {
import {type EmulatorCDROM} from "../src/emulator/emulator-common";

export async function handleRequest(path: string, method: string) {
const pathPieces = path.split("/");
let srcUrl;
try {
srcUrl = atob(pathPieces[2]);
} catch (e) {
return new Response("Malformed CD-ROM src URL: " + pathPieces[2], {
status: 400,
statusText: "Bad Request",
headers: {"Content-Type": "text/plain"},
});
return errorResponse("Malformed CD-ROM src URL: " + pathPieces[2]);
}

// Don't want to become a proxy for arbitrary URLs
Expand All @@ -17,18 +15,23 @@ export async function handleRequest(path: string) {
srcUrlParsed.protocol !== "https:" ||
!["macintoshgarden.org", "archive.org"].includes(srcUrlParsed.host)
) {
return new Response("Unexpected CD-ROM src URL: " + srcUrl, {
status: 400,
headers: {"Content-Type": "text/plain"},
});
return errorResponse("Unexpected CD-ROM src URL: " + srcUrl);
}

if (method === "GET") {
return await handleGET(pathPieces, srcUrl);
}
if (method === "PUT") {
return await handlePUT(srcUrl);
}

return errorResponse("Method not allowed", 405);
}

async function handleGET(pathPieces: string[], srcUrl: string) {
const chunkMatch = /(\d+)-(\d+).chunk$/.exec(pathPieces[3]);
if (!chunkMatch) {
return new Response("Malformed CD-ROM src chunk: " + pathPieces[3], {
status: 400,
headers: {"Content-Type": "text/plain"},
});
return errorResponse("Malformed CD-ROM src chunk: " + pathPieces[3]);
}

const chunkStart = parseInt(chunkMatch[1]);
Expand Down Expand Up @@ -64,9 +67,54 @@ export async function handleRequest(path: string) {
chunkEnd,
chunkFetchError,
});
return new Response("CD-ROM fetch failed: " + chunkFetchError, {
status: 500,
headers: {"Content-Type": "text/plain"},
return errorResponse("CD-ROM fetch failed: " + chunkFetchError, 500);
}

/**
* Generates a CD-ROM manifest from a source URL on the fly, equivalent to what
* get_output_manifest from import-cd-roms.py does.
*/
async function handlePUT(srcUrl: string) {
const response = await fetch(srcUrl, {
method: "HEAD",
});
if (!response.ok) {
return errorResponse(
`CD-ROM HEAD request failed: ${response.status} (${response.statusText})`
);
}
const contentLength = response.headers.get("Content-Length");
if (!contentLength) {
return errorResponse(`CD-ROM HEAD request failed: no Content-Length`);
}

const fileSize = parseInt(contentLength);
if (isNaN(fileSize)) {
return errorResponse(
`CD-ROM HEAD request failed: invalid Content-Length (${contentLength})`
);
}

// It would be nice to also check that the Accept-Ranges header contains
// `bytes`, but it seems to be stripped from the response when running in
// a Cloudflare Worker.

const cdrom: EmulatorCDROM = {
// The name is not that important, but try to use the filename from the
// URL if possible.
name: new URL(srcUrl).pathname.split("/").pop() ?? "Untitled",
srcUrl,
fileSize,
// Cover images are not shown for on-demand CD-ROMs, so leave them
// blank.
coverImageHash: "",
coverImageSize: [0, 0],
};
return new Response(JSON.stringify(cdrom), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}

Expand Down Expand Up @@ -94,3 +142,11 @@ async function fetchChunk(
contentLength: srcRes.headers.get("Content-Length")!,
};
}

function errorResponse(message: string, status: number = 400): Response {
return new Response(message, {
status,
statusText: message,
headers: {"Content-Type": "text/plain"},
});
}
2 changes: 1 addition & 1 deletion workers-site/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function handleRequest(
return varz.handleRequest(request, env.VARZ);
}
if (path[0] === "CD-ROM") {
return cdrom.handleRequest(url.pathname);
return cdrom.handleRequest(url.pathname, request.method);
}

const legacyDomainRedirect = getLegacyDomainRedirect(url);
Expand Down

0 comments on commit 656d682

Please sign in to comment.