Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Miniflare 3] Add additional traps to magic proxy stubs #710

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions packages/miniflare/src/plugins/core/proxy/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,12 @@ class ProxyClientBridge {
class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
readonly #version: number;
readonly #stringifiedTarget: string;
readonly #known = new Map<string, unknown>();
readonly #knownValues = new Map<string, unknown>();
readonly #knownDescriptors = new Map<
string,
PropertyDescriptor | undefined
>();
#knownOwnKeys?: string[];

revivers: ReducersRevivers = {
...revivers,
Expand Down Expand Up @@ -327,7 +332,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
if (typeof key === "symbol" || key === "then") return undefined;

// See optimisation comments below for cases where this will be set
const maybeKnown = this.#known.get(key);
const maybeKnown = this.#knownValues.get(key);
if (maybeKnown !== undefined) return maybeKnown;

// Always perform a synchronous GET, if this returns a `Promise`, we'll
Expand Down Expand Up @@ -361,7 +366,7 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
// (e.g. accessing `R2ObjectBody#body` multiple times)
result instanceof ReadableStream
) {
this.#known.set(key, result);
this.#knownValues.set(key, result);
}
return result;
}
Expand All @@ -371,6 +376,54 @@ class ProxyStubHandler<T extends object> implements ProxyHandler<T> {
return this.get(target, key, undefined) !== undefined;
}

getOwnPropertyDescriptor(target: T, key: string | symbol) {
if (typeof key === "symbol") return undefined;

// Optimisation: assume constant prototypes of proxied objects, descriptors
// should never change after we've fetched them
const maybeKnown = this.#knownDescriptors.get(key);
if (maybeKnown !== undefined) return maybeKnown;

const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP]: ProxyOps.GET_OWN_DESCRIPTOR,
[CoreHeaders.OP_KEY]: key,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
},
});
const result = this.#parseSyncResponse(
syncRes,
this.getOwnPropertyDescriptor
) as PropertyDescriptor | undefined;

this.#knownDescriptors.set(key, result);
return result;
}

ownKeys(_target: T) {
// Optimisation: assume constant prototypes of proxied objects, own keys
// should never change after we've fetched them
if (this.#knownOwnKeys !== undefined) return this.#knownOwnKeys;

const syncRes = this.bridge.sync.fetch(this.bridge.url, {
method: "POST",
headers: {
[CoreHeaders.OP]: ProxyOps.GET_OWN_KEYS,
[CoreHeaders.OP_TARGET]: this.#stringifiedTarget,
},
});
const result = this.#parseSyncResponse(syncRes, this.ownKeys) as string[];

this.#knownOwnKeys = result;
return result;
}

getPrototypeOf(_target: T) {
// Return a `null` prototype, so users know this isn't a plain object
return null;
}

#createFunction(key: string) {
// Optimisation: if the function returns a `Promise`, we know it must be
// async (assuming all async functions always return `Promise`s). When
Expand Down
4 changes: 4 additions & 0 deletions packages/miniflare/src/workers/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const CoreBindings = {
export const ProxyOps = {
// Get the target or a property of the target
GET: "GET",
// Get the descriptor for a property of the target
GET_OWN_DESCRIPTOR: "GET_OWN_DESCRIPTOR",
// Get the target's own property names
GET_OWN_KEYS: "GET_OWN_KEYS",
// Call a method on the target
CALL: "CALL",
// Remove the strong reference to the target on the "heap", allowing it to be
Expand Down
14 changes: 13 additions & 1 deletion packages/miniflare/src/workers/core/proxy.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class ProxyServer implements DurableObject {
const targetName = target.constructor.name;

let status = 200;
let result;
let result: unknown;
let unbufferedRest: ReadableStream | undefined;
if (opHeader === ProxyOps.GET) {
// If no key header is specified, just return the target
Expand All @@ -168,6 +168,18 @@ export class ProxyServer implements DurableObject {
headers: { [CoreHeaders.OP_RESULT_TYPE]: "Function" },
});
}
} else if (opHeader === ProxyOps.GET_OWN_DESCRIPTOR) {
if (keyHeader === null) return new Response(null, { status: 400 });
const descriptor = Object.getOwnPropertyDescriptor(target, keyHeader);
if (descriptor !== undefined) {
result = <PropertyDescriptor>{
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: descriptor.writable,
};
}
} else if (opHeader === ProxyOps.GET_OWN_KEYS) {
result = Object.getOwnPropertyNames(target);
} else if (opHeader === ProxyOps.CALL) {
// We don't allow callable targets yet (could be useful to implement if
// we ever need to proxy functions that return functions)
Expand Down
23 changes: 23 additions & 0 deletions packages/miniflare/test/plugins/core/proxy/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,26 @@ test("ProxyClient: returns empty ReadableStream synchronously", async (t) => {
assert(objectBody != null);
t.is(await text(objectBody.body), ""); // Synchronous empty stream access
});
test("ProxyClient: can `JSON.stringify()` proxies", async (t) => {
const mf = new Miniflare({ script: nullScript, r2Buckets: ["BUCKET"] });
t.teardown(() => mf.dispose());

const bucket = await mf.getR2Bucket("BUCKET");
const object = await bucket.put("key", "value");
assert(object !== null);
t.is(Object.getPrototypeOf(object), null);
const plainObject = JSON.parse(JSON.stringify(object));
t.deepEqual(plainObject, {
checksums: {
md5: "2063c1608d6e0baf80249c42e2be5804",
},
customMetadata: {},
etag: "2063c1608d6e0baf80249c42e2be5804",
httpEtag: '"2063c1608d6e0baf80249c42e2be5804"',
httpMetadata: {},
key: "key",
size: 5,
uploaded: object.uploaded.toISOString(),
version: object.version,
});
});