mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-24 04:13:59 +02:00
Address response bodies by response id instead of a filesystem path
The body commands took a path from the client, so a token holder could read any file the process could. They now take a response id and resolve the location themselves, and the UI never sees a path at all: the desktop host asks the backend where the file is, and the bridge fetches /responses/:id/body. Ephemeral responses (GraphQL introspection) never reach the database, so resolution falls back to the path send writes them to.
This commit is contained in:
@@ -63,14 +63,13 @@ function detectOsType(): OsType {
|
||||
}
|
||||
|
||||
/**
|
||||
* A response body path is an opaque handle the backend minted, and the bridge
|
||||
* writes them as `<data dir>/responses/<response id>`. Taking the last segment
|
||||
* turns it back into the id the `/responses/{id}/body` route wants, which keeps
|
||||
* the server from ever being asked for a path chosen by the page.
|
||||
* The route that serves a response body, keyed by the response id.
|
||||
*
|
||||
* The bridge resolves the id against its own database, so this is the only
|
||||
* thing the page ever has to know about where a body lives.
|
||||
*/
|
||||
function responseIdFromBodyPath(path: string): string {
|
||||
const segments = path.split(/[/\\]/);
|
||||
return segments[segments.length - 1] ?? path;
|
||||
function responseBodyRoute(responseId: string): string {
|
||||
return `/responses/${encodeURIComponent(responseId)}/body`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,25 +220,33 @@ export function createBridgePlatform(baseUrl: string, token: string | null): Pla
|
||||
},
|
||||
|
||||
files: {
|
||||
async readFile(path) {
|
||||
const res = await connection.fetch(`/responses/${responseIdFromBodyPath(path)}/body`);
|
||||
readDir: async () => {
|
||||
throw unsupported("Browsing the filesystem");
|
||||
},
|
||||
|
||||
// App resources are served by the page's own origin, so the path the
|
||||
// caller resolved is already a URL a tab can load.
|
||||
url: (path) => path,
|
||||
|
||||
basename: async (path) => path.split(/[/\\]/).pop() ?? path,
|
||||
resolveResource: async (path) => path,
|
||||
|
||||
// The route takes the response id and looks the body up itself, so the
|
||||
// page never names a file — the same reason the desktop asks the backend
|
||||
// for the path instead of building one.
|
||||
async readResponseBody(responseId) {
|
||||
const res = await connection.fetch(responseBodyRoute(responseId));
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to read response body (${res.status})`);
|
||||
}
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
},
|
||||
|
||||
readDir: async () => {
|
||||
throw unsupported("Browsing the filesystem");
|
||||
},
|
||||
|
||||
// The `<img src>`/`<video src>` equivalent of Tauri's `convertFileSrc`.
|
||||
// The token rides in the query because the browser makes these requests
|
||||
// itself and the page cannot add a header to them.
|
||||
url: (path) => connection.url(`/responses/${responseIdFromBodyPath(path)}/body`),
|
||||
|
||||
basename: async (path) => path.split(/[/\\]/).pop() ?? path,
|
||||
resolveResource: async (path) => path,
|
||||
responseBodyUrl: async (responseId) => connection.url(responseBodyRoute(responseId)),
|
||||
},
|
||||
|
||||
rpc: <T,>(cmd: string, payload?: RpcPayload): Promise<T> => {
|
||||
|
||||
@@ -104,6 +104,17 @@ async function rpc<T>(cmd: string, payload?: RpcPayload): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the backend put a response's body, or null if it has none.
|
||||
*
|
||||
* The app holds response ids and nothing else; a path exists for exactly as
|
||||
* long as it takes this host to open the file, which is the one thing a desktop
|
||||
* host can do that a tab cannot.
|
||||
*/
|
||||
function responseBodyPath(responseId: string): Promise<string | null> {
|
||||
return rpc<string | null>("cmd_http_response_body_path", { responseId });
|
||||
}
|
||||
|
||||
export function createTauriPlatform(): Platform {
|
||||
const window = createWindow();
|
||||
|
||||
@@ -124,11 +135,20 @@ export function createTauriPlatform(): Platform {
|
||||
},
|
||||
|
||||
files: {
|
||||
readFile: (path) => readFile(path),
|
||||
readDir: (path) => readDir(path),
|
||||
url: (path) => convertFileSrc(path),
|
||||
basename: (path) => basename(path),
|
||||
resolveResource: (path) => resolveResource(path),
|
||||
|
||||
async readResponseBody(responseId) {
|
||||
const path = await responseBodyPath(responseId);
|
||||
return path == null ? null : readFile(path);
|
||||
},
|
||||
|
||||
async responseBodyUrl(responseId) {
|
||||
const path = await responseBodyPath(responseId);
|
||||
return path == null ? null : convertFileSrc(path);
|
||||
},
|
||||
},
|
||||
|
||||
rpc,
|
||||
|
||||
@@ -135,11 +135,15 @@ export interface PlatformDialog {
|
||||
* File-ish operations, keyed by paths the backend handed us.
|
||||
*
|
||||
* A path here is an opaque handle, not something to parse or construct: the UI
|
||||
* only ever passes one back to `readFile` or `url`. A host without a filesystem
|
||||
* only ever passes one back to `url` or `readDir`. A host without a filesystem
|
||||
* can mint handles of its own (a blob id, a URL) and stay compatible.
|
||||
*
|
||||
* Response bodies are addressed by response id instead, because they are the
|
||||
* one thing every host stores somewhere different. The desktop reads the file
|
||||
* the engine wrote; the bridge fetches it over HTTP. Neither is ever handed a
|
||||
* location the page chose.
|
||||
*/
|
||||
export interface PlatformFiles {
|
||||
readFile(path: string): Promise<Uint8Array<ArrayBuffer>>;
|
||||
readDir(path: string): Promise<DirEntry[]>;
|
||||
|
||||
/** A URL the page can load a file from, for `<img>`, `<video>`, and friends. */
|
||||
@@ -149,6 +153,19 @@ export interface PlatformFiles {
|
||||
|
||||
/** Resolve a path bundled with the app itself, rather than one from the backend. */
|
||||
resolveResource(path: string): Promise<string>;
|
||||
|
||||
/** The bytes of a stored response body, or null if the response has none. */
|
||||
readResponseBody(responseId: string): Promise<Uint8Array<ArrayBuffer> | null>;
|
||||
|
||||
/**
|
||||
* A URL the media viewers can point an element at, or null if the response
|
||||
* has no stored body.
|
||||
*
|
||||
* Asynchronous because the host may have to ask the backend where the body
|
||||
* is: only the host is allowed to know that, and only the moment before it
|
||||
* opens it.
|
||||
*/
|
||||
responseBodyUrl(responseId: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
|
||||
Reference in New Issue
Block a user