mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 08:39:09 -05:00
feat: Add configurable permissions for Workers (#8215)
This commit adds new option to "Worker" Web API that allows to configure permissions. New "Worker.deno.permissions" option can be used to define limited permissions to the worker thread by either: - inherit set of parent thread permissions - use limited subset of parent thread permissions - revoke all permissions (full sandbox) In order to achieve this functionality "CliModuleLoader" was modified to accept "initial permissions", which are used for top module loading (ie. uses parent thread permission set to load top level module of a worker).
This commit is contained in:
parent
2e18fcebcc
commit
adc2f08c17
33 changed files with 1062 additions and 73 deletions
56
cli/dts/lib.deno.shared_globals.d.ts
vendored
56
cli/dts/lib.deno.shared_globals.d.ts
vendored
|
@ -662,24 +662,33 @@ declare class Worker extends EventTarget {
|
||||||
options?: {
|
options?: {
|
||||||
type?: "classic" | "module";
|
type?: "classic" | "module";
|
||||||
name?: string;
|
name?: string;
|
||||||
/** UNSTABLE: New API. Expect many changes; most likely this
|
/** UNSTABLE: New API.
|
||||||
* field will be made into an object for more granular
|
|
||||||
* configuration of worker thread (permissions, import map, etc.).
|
|
||||||
*
|
*
|
||||||
* Set to `true` to make `Deno` namespace and all of its methods
|
* Set deno.namespace to `true` to make `Deno` namespace and all of its methods
|
||||||
* available to worker thread.
|
* available to worker thread. The namespace is disabled by default.
|
||||||
*
|
*
|
||||||
* Currently worker inherits permissions from main thread (permissions
|
* Configure deno.permissions options to change the level of access the worker will
|
||||||
* given using `--allow-*` flags).
|
* have. By default it will inherit the permissions of its parent thread. The permissions
|
||||||
* Configurable permissions are on the roadmap to be implemented.
|
* of a worker can't be extended beyond its parent's permissions reach.
|
||||||
|
* - "inherit" will take the permissions of the thread the worker is created in
|
||||||
|
* - You can disable/enable permissions all together by passing a boolean
|
||||||
|
* - You can provide a list of routes relative to the file the worker
|
||||||
|
* is created in to limit the access of the worker (read/write permissions only)
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* // mod.ts
|
* // mod.ts
|
||||||
* const worker = new Worker(
|
* const worker = new Worker(
|
||||||
* new URL("deno_worker.ts", import.meta.url).href,
|
* new URL("deno_worker.ts", import.meta.url).href, {
|
||||||
* { type: "module", deno: true }
|
* type: "module",
|
||||||
|
* deno: {
|
||||||
|
* namespace: true,
|
||||||
|
* permissions: {
|
||||||
|
* read: true,
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* }
|
||||||
* );
|
* );
|
||||||
* worker.postMessage({ cmd: "readFile", fileName: "./log.txt" });
|
* worker.postMessage({ cmd: "readFile", fileName: "./log.txt" });
|
||||||
*
|
*
|
||||||
|
@ -707,7 +716,30 @@ declare class Worker extends EventTarget {
|
||||||
* hello world2
|
* hello world2
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
deno?: boolean;
|
// TODO(Soremwar)
|
||||||
|
// `deno: true` is kept for backwards compatibility with the previous worker
|
||||||
|
// options implementation. Remove for 2.0
|
||||||
|
deno?: true | {
|
||||||
|
namespace?: boolean;
|
||||||
|
/** Set to false to disable all the permissions in the worker */
|
||||||
|
permissions?: "inherit" | false | {
|
||||||
|
env?: "inherit" | boolean;
|
||||||
|
hrtime?: "inherit" | boolean;
|
||||||
|
/**
|
||||||
|
* The format of the net access list must be `hostname[:port]`
|
||||||
|
* in order to be resolved
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* net: ["https://deno.land", "localhost:8080"],
|
||||||
|
* ```
|
||||||
|
* */
|
||||||
|
net?: "inherit" | boolean | string[];
|
||||||
|
plugin?: "inherit" | boolean;
|
||||||
|
read?: "inherit" | boolean | Array<string | URL>;
|
||||||
|
run?: "inherit" | boolean;
|
||||||
|
write?: "inherit" | boolean | Array<string | URL>;
|
||||||
|
};
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
postMessage(message: any, transfer: ArrayBuffer[]): void;
|
postMessage(message: any, transfer: ArrayBuffer[]): void;
|
||||||
|
|
|
@ -100,7 +100,10 @@ fn create_web_worker_callback(
|
||||||
|| program_state.coverage_dir.is_some();
|
|| program_state.coverage_dir.is_some();
|
||||||
let maybe_inspector_server = program_state.maybe_inspector_server.clone();
|
let maybe_inspector_server = program_state.maybe_inspector_server.clone();
|
||||||
|
|
||||||
let module_loader = CliModuleLoader::new_for_worker(program_state.clone());
|
let module_loader = CliModuleLoader::new_for_worker(
|
||||||
|
program_state.clone(),
|
||||||
|
args.parent_permissions.clone(),
|
||||||
|
);
|
||||||
let create_web_worker_cb =
|
let create_web_worker_cb =
|
||||||
create_web_worker_callback(program_state.clone());
|
create_web_worker_callback(program_state.clone());
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,10 @@ pub struct CliModuleLoader {
|
||||||
/// import map file will be resolved and set.
|
/// import map file will be resolved and set.
|
||||||
pub import_map: Option<ImportMap>,
|
pub import_map: Option<ImportMap>,
|
||||||
pub lib: TypeLib,
|
pub lib: TypeLib,
|
||||||
|
/// The initial set of permissions used to resolve the imports in the worker.
|
||||||
|
/// They are decoupled from the worker permissions since read access errors
|
||||||
|
/// must be raised based on the parent thread permissions
|
||||||
|
pub initial_permissions: Rc<RefCell<Option<Permissions>>>,
|
||||||
pub program_state: Arc<ProgramState>,
|
pub program_state: Arc<ProgramState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,11 +42,15 @@ impl CliModuleLoader {
|
||||||
Rc::new(CliModuleLoader {
|
Rc::new(CliModuleLoader {
|
||||||
import_map,
|
import_map,
|
||||||
lib,
|
lib,
|
||||||
|
initial_permissions: Rc::new(RefCell::new(None)),
|
||||||
program_state,
|
program_state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_for_worker(program_state: Arc<ProgramState>) -> Rc<Self> {
|
pub fn new_for_worker(
|
||||||
|
program_state: Arc<ProgramState>,
|
||||||
|
permissions: Permissions,
|
||||||
|
) -> Rc<Self> {
|
||||||
let lib = if program_state.flags.unstable {
|
let lib = if program_state.flags.unstable {
|
||||||
TypeLib::UnstableDenoWorker
|
TypeLib::UnstableDenoWorker
|
||||||
} else {
|
} else {
|
||||||
|
@ -52,6 +60,7 @@ impl CliModuleLoader {
|
||||||
Rc::new(CliModuleLoader {
|
Rc::new(CliModuleLoader {
|
||||||
import_map: None,
|
import_map: None,
|
||||||
lib,
|
lib,
|
||||||
|
initial_permissions: Rc::new(RefCell::new(Some(permissions))),
|
||||||
program_state,
|
program_state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -118,7 +127,16 @@ impl ModuleLoader for CliModuleLoader {
|
||||||
let state = op_state.borrow();
|
let state = op_state.borrow();
|
||||||
|
|
||||||
// The permissions that should be applied to any dynamically imported module
|
// The permissions that should be applied to any dynamically imported module
|
||||||
let dynamic_permissions = state.borrow::<Permissions>().clone();
|
let dynamic_permissions =
|
||||||
|
// If there are initial permissions assigned to the loader take them
|
||||||
|
// and use only once for top level module load.
|
||||||
|
// Otherwise use permissions assigned to the current worker.
|
||||||
|
if let Some(permissions) = self.initial_permissions.borrow_mut().take() {
|
||||||
|
permissions
|
||||||
|
} else {
|
||||||
|
state.borrow::<Permissions>().clone()
|
||||||
|
};
|
||||||
|
|
||||||
let lib = self.lib.clone();
|
let lib = self.lib.clone();
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
const w = new Worker(
|
const w = new Worker(
|
||||||
new URL("subdir/worker_unstable.ts", import.meta.url).href,
|
new URL("workers/worker_unstable.ts", import.meta.url).href,
|
||||||
{
|
{
|
||||||
type: "module",
|
type: "module",
|
||||||
deno: true,
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
},
|
||||||
name: "Unstable Worker",
|
name: "Unstable Worker",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const r = await fetch(
|
const r = await fetch(
|
||||||
"http://localhost:4545/cli/tests/subdir/fetching_worker.js",
|
"http://localhost:4545/cli/tests/workers/fetching_worker.js",
|
||||||
);
|
);
|
||||||
await r.text();
|
await r.text();
|
||||||
postMessage("Done!");
|
postMessage("Done!");
|
17
cli/tests/workers/no_permissions_worker.js
Normal file
17
cli/tests/workers/no_permissions_worker.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
self.onmessage = async () => {
|
||||||
|
const hrtime = await Deno.permissions.query({ name: "hrtime" });
|
||||||
|
const net = await Deno.permissions.query({ name: "net" });
|
||||||
|
const plugin = await Deno.permissions.query({ name: "plugin" });
|
||||||
|
const read = await Deno.permissions.query({ name: "read" });
|
||||||
|
const run = await Deno.permissions.query({ name: "run" });
|
||||||
|
const write = await Deno.permissions.query({ name: "write" });
|
||||||
|
self.postMessage(
|
||||||
|
hrtime.state === "denied" &&
|
||||||
|
net.state === "denied" &&
|
||||||
|
plugin.state === "denied" &&
|
||||||
|
read.state === "denied" &&
|
||||||
|
run.state === "denied" &&
|
||||||
|
write.state === "denied",
|
||||||
|
);
|
||||||
|
self.close();
|
||||||
|
};
|
43
cli/tests/workers/parent_read_check_granular_worker.js
Normal file
43
cli/tests/workers/parent_read_check_granular_worker.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { fromFileUrl } from "../../../std/path/mod.ts";
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./read_check_granular_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
read: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let received = 0;
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
worker.onmessage = ({ data: childResponse }) => {
|
||||||
|
received++;
|
||||||
|
postMessage({
|
||||||
|
childHasPermission: childResponse.hasPermission,
|
||||||
|
index: childResponse.index,
|
||||||
|
parentHasPermission: messages[childResponse.index],
|
||||||
|
});
|
||||||
|
if (received === messages.length) {
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onmessage = async ({ data }) => {
|
||||||
|
const { state } = await Deno.permissions.query({
|
||||||
|
name: "read",
|
||||||
|
path: fromFileUrl(new URL(data.route, import.meta.url)),
|
||||||
|
});
|
||||||
|
|
||||||
|
messages[data.index] = state === "granted";
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
index: data.index,
|
||||||
|
route: data.route,
|
||||||
|
});
|
||||||
|
};
|
27
cli/tests/workers/parent_read_check_worker.js
Normal file
27
cli/tests/workers/parent_read_check_worker.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
onmessage = async () => {
|
||||||
|
const { state } = await Deno.permissions.query({
|
||||||
|
name: "read",
|
||||||
|
});
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./read_check_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.onmessage = ({ data: childHasPermission }) => {
|
||||||
|
postMessage({
|
||||||
|
parentHasPermission: state === "granted",
|
||||||
|
childHasPermission,
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
worker.postMessage(null);
|
||||||
|
};
|
13
cli/tests/workers/read_check_granular_worker.js
Normal file
13
cli/tests/workers/read_check_granular_worker.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { fromFileUrl } from "../../../std/path/mod.ts";
|
||||||
|
|
||||||
|
onmessage = async ({ data }) => {
|
||||||
|
const { state } = await Deno.permissions.query({
|
||||||
|
name: "read",
|
||||||
|
path: fromFileUrl(new URL(data.route, import.meta.url)),
|
||||||
|
});
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
hasPermission: state === "granted",
|
||||||
|
index: data.index,
|
||||||
|
});
|
||||||
|
};
|
7
cli/tests/workers/read_check_worker.js
Normal file
7
cli/tests/workers/read_check_worker.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
onmessage = async () => {
|
||||||
|
const { state } = await Deno.permissions.query({
|
||||||
|
name: "read",
|
||||||
|
});
|
||||||
|
postMessage(state === "granted");
|
||||||
|
close();
|
||||||
|
};
|
|
@ -22,7 +22,7 @@ async function main(): Promise<void> {
|
||||||
const workers: Array<[Map<number, Deferred<string>>, Worker]> = [];
|
const workers: Array<[Map<number, Deferred<string>>, Worker]> = [];
|
||||||
for (let i = 1; i <= workerCount; ++i) {
|
for (let i = 1; i <= workerCount; ++i) {
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
new URL("subdir/bench_worker.ts", import.meta.url).href,
|
new URL("workers/bench_worker.ts", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
|
@ -5,7 +5,7 @@ async function bench(): Promise<void> {
|
||||||
const workers: Worker[] = [];
|
const workers: Worker[] = [];
|
||||||
for (let i = 1; i <= workerCount; ++i) {
|
for (let i = 1; i <= workerCount; ++i) {
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
new URL("subdir/bench_worker.ts", import.meta.url).href,
|
new URL("workers/bench_worker.ts", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
const promise = new Promise<void>((resolve): void => {
|
const promise = new Promise<void>((resolve): void => {
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
// Requires to be run with `--allow-net` flag
|
// Requires to be run with `--allow-net` flag
|
||||||
|
|
||||||
// FIXME(bartlomieju): this file is an integration test only because
|
import {
|
||||||
// workers are leaking ops at the moment - `worker.terminate()` is not
|
assert,
|
||||||
// yet implemented. Once it gets implemented this file should be
|
assertEquals,
|
||||||
// again moved to `cli/js/` as an unit test file.
|
assertThrows,
|
||||||
|
fail,
|
||||||
import { assert, assertEquals } from "../../std/testing/asserts.ts";
|
} from "../../std/testing/asserts.ts";
|
||||||
import { deferred } from "../../std/async/deferred.ts";
|
import { deferred } from "../../std/async/deferred.ts";
|
||||||
|
|
||||||
Deno.test({
|
Deno.test({
|
||||||
|
@ -16,11 +16,11 @@ Deno.test({
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
||||||
const jsWorker = new Worker(
|
const jsWorker = new Worker(
|
||||||
new URL("subdir/test_worker.js", import.meta.url).href,
|
new URL("workers/test_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
const tsWorker = new Worker(
|
const tsWorker = new Worker(
|
||||||
new URL("subdir/test_worker.ts", import.meta.url).href,
|
new URL("workers/test_worker.ts", import.meta.url).href,
|
||||||
{ type: "module", name: "tsWorker" },
|
{ type: "module", name: "tsWorker" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ Deno.test({
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
||||||
const nestedWorker = new Worker(
|
const nestedWorker = new Worker(
|
||||||
new URL("subdir/nested_worker.js", import.meta.url).href,
|
new URL("workers/nested_worker.js", import.meta.url).href,
|
||||||
{ type: "module", name: "nested" },
|
{ type: "module", name: "nested" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ Deno.test({
|
||||||
fn: async function (): Promise<void> {
|
fn: async function (): Promise<void> {
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
const throwingWorker = new Worker(
|
const throwingWorker = new Worker(
|
||||||
new URL("subdir/throwing_worker.js", import.meta.url).href,
|
new URL("workers/throwing_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ Deno.test({
|
||||||
fn: async function (): Promise<void> {
|
fn: async function (): Promise<void> {
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
const w = new Worker(
|
const w = new Worker(
|
||||||
new URL("subdir/worker_globals.ts", import.meta.url).href,
|
new URL("workers/worker_globals.ts", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
w.onmessage = (e): void => {
|
w.onmessage = (e): void => {
|
||||||
|
@ -133,7 +133,7 @@ Deno.test({
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
||||||
const fetchingWorker = new Worker(
|
const fetchingWorker = new Worker(
|
||||||
new URL("subdir/fetching_worker.js", import.meta.url).href,
|
new URL("workers/fetching_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ Deno.test({
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
||||||
const busyWorker = new Worker(
|
const busyWorker = new Worker(
|
||||||
new URL("subdir/busy_worker.js", import.meta.url).href,
|
new URL("workers/busy_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ Deno.test({
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
|
|
||||||
const racyWorker = new Worker(
|
const racyWorker = new Worker(
|
||||||
new URL("subdir/racy_worker.js", import.meta.url).href,
|
new URL("workers/racy_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@ Deno.test({
|
||||||
const promise2 = deferred();
|
const promise2 = deferred();
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
new URL("subdir/event_worker.js", import.meta.url).href,
|
new URL("workers/event_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -265,7 +265,7 @@ Deno.test({
|
||||||
const promise1 = deferred();
|
const promise1 = deferred();
|
||||||
|
|
||||||
const worker = new Worker(
|
const worker = new Worker(
|
||||||
new URL("subdir/event_worker_scope.js", import.meta.url).href,
|
new URL("workers/event_worker_scope.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -294,12 +294,18 @@ Deno.test({
|
||||||
const promise2 = deferred();
|
const promise2 = deferred();
|
||||||
|
|
||||||
const regularWorker = new Worker(
|
const regularWorker = new Worker(
|
||||||
new URL("subdir/non_deno_worker.js", import.meta.url).href,
|
new URL("workers/non_deno_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
const denoWorker = new Worker(
|
const denoWorker = new Worker(
|
||||||
new URL("subdir/deno_worker.ts", import.meta.url).href,
|
new URL("workers/deno_worker.ts", import.meta.url).href,
|
||||||
{ type: "module", deno: true },
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: "inherit",
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
regularWorker.onmessage = (e): void => {
|
regularWorker.onmessage = (e): void => {
|
||||||
|
@ -326,7 +332,7 @@ Deno.test({
|
||||||
fn: async function (): Promise<void> {
|
fn: async function (): Promise<void> {
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
const w = new Worker(
|
const w = new Worker(
|
||||||
new URL("subdir/worker_crypto.js", import.meta.url).href,
|
new URL("workers/worker_crypto.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
w.onmessage = (e): void => {
|
w.onmessage = (e): void => {
|
||||||
|
@ -344,7 +350,7 @@ Deno.test({
|
||||||
fn: async function (): Promise<void> {
|
fn: async function (): Promise<void> {
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
const w = new Worker(
|
const w = new Worker(
|
||||||
new URL("subdir/test_worker.ts", import.meta.url).href,
|
new URL("workers/test_worker.ts", import.meta.url).href,
|
||||||
{ type: "module", name: "tsWorker" },
|
{ type: "module", name: "tsWorker" },
|
||||||
);
|
);
|
||||||
const arr: number[] = [];
|
const arr: number[] = [];
|
||||||
|
@ -368,7 +374,7 @@ Deno.test({
|
||||||
fn: async function (): Promise<void> {
|
fn: async function (): Promise<void> {
|
||||||
const promise = deferred();
|
const promise = deferred();
|
||||||
const w = new Worker(
|
const w = new Worker(
|
||||||
new URL("./immediately_close_worker.js", import.meta.url).href,
|
new URL("./workers/immediately_close_worker.js", import.meta.url).href,
|
||||||
{ type: "module" },
|
{ type: "module" },
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -378,3 +384,233 @@ Deno.test({
|
||||||
w.terminate();
|
w.terminate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Worker inherits permissions", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/read_check_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: "inherit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.onmessage = ({ data: hasPermission }) => {
|
||||||
|
assert(hasPermission);
|
||||||
|
promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage(null);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Worker limit children permissions", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/read_check_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.onmessage = ({ data: hasPermission }) => {
|
||||||
|
assert(!hasPermission);
|
||||||
|
promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage(null);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Worker limit children permissions granularly", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/read_check_granular_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
read: [
|
||||||
|
new URL("./workers/read_check_worker.js", import.meta.url),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
//Routes are relative to the spawned worker location
|
||||||
|
const routes = [
|
||||||
|
{ permission: false, route: "read_check_granular_worker.js" },
|
||||||
|
{ permission: true, route: "read_check_worker.js" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
checked++;
|
||||||
|
assertEquals(data.hasPermission, routes[data.index].permission);
|
||||||
|
routes.shift();
|
||||||
|
if (checked === routes.length) {
|
||||||
|
promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
routes.forEach(({ route }, index) =>
|
||||||
|
worker.postMessage({
|
||||||
|
index,
|
||||||
|
route,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Nested worker limit children permissions", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
|
||||||
|
/** This worker has read permissions but doesn't grant them to its children */
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/parent_read_check_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: "inherit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
assert(data.parentHasPermission);
|
||||||
|
assert(!data.childHasPermission);
|
||||||
|
promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage(null);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Nested worker limit children permissions granularly", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
|
||||||
|
/** This worker has read permissions but doesn't grant them to its children */
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/parent_read_check_granular_worker.js", import.meta.url)
|
||||||
|
.href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
read: [
|
||||||
|
new URL("./workers/read_check_granular_worker.js", import.meta.url),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
//Routes are relative to the spawned worker location
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
childHasPermission: false,
|
||||||
|
parentHasPermission: true,
|
||||||
|
route: "read_check_granular_worker.js",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
childHasPermission: false,
|
||||||
|
parentHasPermission: false,
|
||||||
|
route: "read_check_worker.js",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
checked++;
|
||||||
|
assertEquals(
|
||||||
|
data.childHasPermission,
|
||||||
|
routes[data.index].childHasPermission,
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
data.parentHasPermission,
|
||||||
|
routes[data.index].parentHasPermission,
|
||||||
|
);
|
||||||
|
if (checked === routes.length) {
|
||||||
|
promise.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Index needed cause requests will be handled asynchronously
|
||||||
|
routes.forEach(({ route }, index) =>
|
||||||
|
worker.postMessage({
|
||||||
|
index,
|
||||||
|
route,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
||||||
|
// This test relies on env permissions not being granted on main thread
|
||||||
|
Deno.test("Worker initialization throws on worker permissions greater than parent thread permissions", function () {
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/deno_worker.ts", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
env: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
worker.terminate();
|
||||||
|
},
|
||||||
|
Deno.errors.PermissionDenied,
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Worker with disabled permissions", async function () {
|
||||||
|
const promise = deferred();
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./workers/no_permissions_worker.js", import.meta.url).href,
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
worker.onmessage = ({ data: sandboxed }) => {
|
||||||
|
assert(sandboxed);
|
||||||
|
promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage(null);
|
||||||
|
await promise;
|
||||||
|
worker.terminate();
|
||||||
|
});
|
||||||
|
|
|
@ -16,15 +16,15 @@ specifier for some nearby script.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Good
|
// Good
|
||||||
new Worker(new URL("worker.js", import.meta.url).href, { type: "module" });
|
new Worker(new URL("./worker.js", import.meta.url).href, { type: "module" });
|
||||||
|
|
||||||
// Bad
|
// Bad
|
||||||
new Worker(new URL("worker.js", import.meta.url).href);
|
new Worker(new URL("./worker.js", import.meta.url).href);
|
||||||
new Worker(new URL("worker.js", import.meta.url).href, { type: "classic" });
|
new Worker(new URL("./worker.js", import.meta.url).href, { type: "classic" });
|
||||||
new Worker("./worker.js", { type: "module" });
|
new Worker("./worker.js", { type: "module" });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Permissions
|
### Instantiation permissions
|
||||||
|
|
||||||
Creating a new `Worker` instance is similar to a dynamic import; therefore Deno
|
Creating a new `Worker` instance is similar to a dynamic import; therefore Deno
|
||||||
requires appropriate permission for this action.
|
requires appropriate permission for this action.
|
||||||
|
@ -34,7 +34,7 @@ For workers using local modules; `--allow-read` permission is required:
|
||||||
**main.ts**
|
**main.ts**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
new Worker(new URL("worker.ts", import.meta.url).href, { type: "module" });
|
new Worker(new URL("./worker.ts", import.meta.url).href, { type: "module" });
|
||||||
```
|
```
|
||||||
|
|
||||||
**worker.ts**
|
**worker.ts**
|
||||||
|
@ -82,14 +82,17 @@ hello world
|
||||||
|
|
||||||
By default the `Deno` namespace is not available in worker scope.
|
By default the `Deno` namespace is not available in worker scope.
|
||||||
|
|
||||||
To add the `Deno` namespace pass `deno: true` option when creating new worker:
|
To enable the `Deno` namespace pass `deno.namespace = true` option when creating
|
||||||
|
new worker:
|
||||||
|
|
||||||
**main.js**
|
**main.js**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const worker = new Worker(new URL("worker.js", import.meta.url).href, {
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
type: "module",
|
type: "module",
|
||||||
deno: true,
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
worker.postMessage({ filename: "./log.txt" });
|
worker.postMessage({ filename: "./log.txt" });
|
||||||
```
|
```
|
||||||
|
@ -116,7 +119,129 @@ $ deno run --allow-read --unstable main.js
|
||||||
hello world
|
hello world
|
||||||
```
|
```
|
||||||
|
|
||||||
When the `Deno` namespace is available in worker scope, the worker inherits its
|
### Specifying worker permissions
|
||||||
parent process' permissions (the ones specified using `--allow-*` flags).
|
|
||||||
|
|
||||||
We intend to make permissions configurable for workers.
|
> This is an unstable Deno feature. Learn more about
|
||||||
|
> [unstable features](./stability.md).
|
||||||
|
|
||||||
|
The permissions available for the worker are analogous to the CLI permission
|
||||||
|
flags, meaning every permission enabled there can be disabled at the level of
|
||||||
|
the Worker API. You can find a more detailed description of each of the
|
||||||
|
permission options [here](../getting_started/permissions.md).
|
||||||
|
|
||||||
|
By default a worker will inherit permissions from the thread it was created in,
|
||||||
|
however in order to allow users to limit the access of this worker we provide
|
||||||
|
the `deno.permissions` option in the worker API.
|
||||||
|
|
||||||
|
- For permissions that support granular access you can pass in a list of the
|
||||||
|
desired resources the worker will have access to, and for those who only have
|
||||||
|
the on/off option you can pass true/false respectively.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: [
|
||||||
|
net: [
|
||||||
|
"https://deno.land/",
|
||||||
|
],
|
||||||
|
read: [
|
||||||
|
new URL("./file_1.txt", import.meta.url),
|
||||||
|
new URL("./file_2.txt", import.meta.url),
|
||||||
|
],
|
||||||
|
write: false,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Granular access permissions receive both absolute and relative routes as
|
||||||
|
arguments, however take into account that relative routes will be resolved
|
||||||
|
relative to the file the worker is instantiated in, not the path the worker
|
||||||
|
file is currently in
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const worker = new Worker(new URL("./worker/worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: [
|
||||||
|
read: [
|
||||||
|
"/home/user/Documents/deno/worker/file_1.txt",
|
||||||
|
"./worker/file_2.txt",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Both `deno.permissions` and its children support the option `"inherit"`, which
|
||||||
|
implies it will borrow its parent permissions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// This worker will inherit its parent permissions
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: "inherit",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// This worker will inherit only the net permissions of its parent
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
env: false,
|
||||||
|
hrtime: false,
|
||||||
|
net: "inherit",
|
||||||
|
plugin: false,
|
||||||
|
read: false,
|
||||||
|
run: false,
|
||||||
|
write: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Not specifying the `deno.permissions` option or one of its children will cause
|
||||||
|
the worker to inherit by default.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// This worker will inherit its parent permissions
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// This worker will inherit all the permissions of its parent BUT net
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: {
|
||||||
|
net: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can disable the permissions of the worker all together by passing false to
|
||||||
|
the `deno.permissions` option.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// This worker will not have any permissions enabled
|
||||||
|
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
|
||||||
|
type: "module",
|
||||||
|
deno: {
|
||||||
|
namespace: true,
|
||||||
|
permissions: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
|
@ -3,21 +3,24 @@
|
||||||
((window) => {
|
((window) => {
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
const { Window } = window.__bootstrap.globalInterfaces;
|
const { Window } = window.__bootstrap.globalInterfaces;
|
||||||
const { log } = window.__bootstrap.util;
|
const { log, pathFromURL } = window.__bootstrap.util;
|
||||||
const { defineEventHandler } = window.__bootstrap.webUtil;
|
const { defineEventHandler } = window.__bootstrap.webUtil;
|
||||||
|
const build = window.__bootstrap.build.build;
|
||||||
|
|
||||||
function createWorker(
|
function createWorker(
|
||||||
specifier,
|
specifier,
|
||||||
hasSourceCode,
|
hasSourceCode,
|
||||||
sourceCode,
|
sourceCode,
|
||||||
useDenoNamespace,
|
useDenoNamespace,
|
||||||
|
permissions,
|
||||||
name,
|
name,
|
||||||
) {
|
) {
|
||||||
return core.jsonOpSync("op_create_worker", {
|
return core.jsonOpSync("op_create_worker", {
|
||||||
specifier,
|
|
||||||
hasSourceCode,
|
hasSourceCode,
|
||||||
sourceCode,
|
|
||||||
name,
|
name,
|
||||||
|
permissions,
|
||||||
|
sourceCode,
|
||||||
|
specifier,
|
||||||
useDenoNamespace,
|
useDenoNamespace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -47,14 +50,122 @@
|
||||||
return JSON.parse(dataJson);
|
return JSON.parse(dataJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} permission
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function parseBooleanPermission(
|
||||||
|
value,
|
||||||
|
permission,
|
||||||
|
) {
|
||||||
|
if (value !== "inherit" && typeof value !== "boolean") {
|
||||||
|
throw new Error(
|
||||||
|
`Expected 'boolean' for ${permission} permission, ${typeof value} received`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value === "inherit" ? undefined : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} permission
|
||||||
|
* @return {(boolean | string[])}
|
||||||
|
* */
|
||||||
|
function parseArrayPermission(
|
||||||
|
value,
|
||||||
|
permission,
|
||||||
|
) {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value !== "inherit") {
|
||||||
|
throw new Error(
|
||||||
|
`Expected 'array' or 'boolean' for ${permission} permission, "${value}" received`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (!Array.isArray(value) && typeof value !== "boolean") {
|
||||||
|
throw new Error(
|
||||||
|
`Expected 'array' or 'boolean' for ${permission} permission, ${typeof value} received`,
|
||||||
|
);
|
||||||
|
//Casts URLs to absolute routes
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
value = value.map((route) => {
|
||||||
|
if (route instanceof URL) {
|
||||||
|
route = pathFromURL(route);
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return value === "inherit" ? undefined : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes data, runs checks on parameters and deletes inherited permissions
|
||||||
|
*/
|
||||||
|
function parsePermissions({
|
||||||
|
env = "inherit",
|
||||||
|
hrtime = "inherit",
|
||||||
|
net = "inherit",
|
||||||
|
plugin = "inherit",
|
||||||
|
read = "inherit",
|
||||||
|
run = "inherit",
|
||||||
|
write = "inherit",
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
env: parseBooleanPermission(env, "env"),
|
||||||
|
hrtime: parseBooleanPermission(hrtime, "hrtime"),
|
||||||
|
net: parseArrayPermission(net, "net"),
|
||||||
|
plugin: parseBooleanPermission(plugin, "plugin"),
|
||||||
|
read: parseArrayPermission(read, "read"),
|
||||||
|
run: parseBooleanPermission(run, "run"),
|
||||||
|
write: parseArrayPermission(write, "write"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class Worker extends EventTarget {
|
class Worker extends EventTarget {
|
||||||
#id = 0;
|
#id = 0;
|
||||||
#name = "";
|
#name = "";
|
||||||
#terminated = false;
|
#terminated = false;
|
||||||
|
|
||||||
constructor(specifier, options) {
|
constructor(specifier, options = {}) {
|
||||||
super();
|
super();
|
||||||
const { type = "classic", name = "unknown" } = options ?? {};
|
const {
|
||||||
|
deno = {},
|
||||||
|
name = "unknown",
|
||||||
|
type = "classic",
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// TODO(Soremwar)
|
||||||
|
// `deno: true` is kept for backwards compatibility with the previous worker
|
||||||
|
// options implementation. Remove for 2.0
|
||||||
|
let workerDenoAttributes;
|
||||||
|
if (deno === true) {
|
||||||
|
workerDenoAttributes = {
|
||||||
|
// Change this to enable the Deno namespace by default
|
||||||
|
namespace: deno,
|
||||||
|
permissions: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
workerDenoAttributes = {
|
||||||
|
// Change this to enable the Deno namespace by default
|
||||||
|
namespace: !!(deno?.namespace ?? false),
|
||||||
|
permissions: (deno?.permissions ?? "inherit") === "inherit"
|
||||||
|
? null
|
||||||
|
: deno?.permissions,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the permission option is set to false, all permissions
|
||||||
|
// must be removed from the worker
|
||||||
|
if (workerDenoAttributes.permissions === false) {
|
||||||
|
workerDenoAttributes.permissions = {
|
||||||
|
env: false,
|
||||||
|
hrtime: false,
|
||||||
|
net: false,
|
||||||
|
plugin: false,
|
||||||
|
read: false,
|
||||||
|
run: false,
|
||||||
|
write: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type !== "module") {
|
if (type !== "module") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -66,13 +177,14 @@
|
||||||
const hasSourceCode = false;
|
const hasSourceCode = false;
|
||||||
const sourceCode = decoder.decode(new Uint8Array());
|
const sourceCode = decoder.decode(new Uint8Array());
|
||||||
|
|
||||||
const useDenoNamespace = options ? !!options.deno : false;
|
|
||||||
|
|
||||||
const { id } = createWorker(
|
const { id } = createWorker(
|
||||||
specifier,
|
specifier,
|
||||||
hasSourceCode,
|
hasSourceCode,
|
||||||
sourceCode,
|
sourceCode,
|
||||||
useDenoNamespace,
|
workerDenoAttributes.namespace,
|
||||||
|
workerDenoAttributes.permissions === null
|
||||||
|
? null
|
||||||
|
: parsePermissions(workerDenoAttributes.permissions),
|
||||||
options?.name,
|
options?.name,
|
||||||
);
|
);
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use crate::permissions::resolve_fs_allowlist;
|
||||||
|
use crate::permissions::PermissionState;
|
||||||
use crate::permissions::Permissions;
|
use crate::permissions::Permissions;
|
||||||
|
use crate::permissions::UnaryPermission;
|
||||||
use crate::web_worker::run_web_worker;
|
use crate::web_worker::run_web_worker;
|
||||||
use crate::web_worker::WebWorker;
|
use crate::web_worker::WebWorker;
|
||||||
use crate::web_worker::WebWorkerHandle;
|
use crate::web_worker::WebWorkerHandle;
|
||||||
use crate::web_worker::WorkerEvent;
|
use crate::web_worker::WorkerEvent;
|
||||||
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::generic_error;
|
use deno_core::error::generic_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::error::JsError;
|
use deno_core::error::JsError;
|
||||||
use deno_core::futures::channel::mpsc;
|
use deno_core::futures::channel::mpsc;
|
||||||
|
use deno_core::serde::de;
|
||||||
|
use deno_core::serde::de::SeqAccess;
|
||||||
|
use deno_core::serde::Deserialize;
|
||||||
|
use deno_core::serde::Deserializer;
|
||||||
use deno_core::serde_json;
|
use deno_core::serde_json;
|
||||||
use deno_core::serde_json::json;
|
use deno_core::serde_json::json;
|
||||||
use deno_core::serde_json::Value;
|
use deno_core::serde_json::Value;
|
||||||
|
@ -16,10 +24,12 @@ use deno_core::BufVec;
|
||||||
use deno_core::ModuleSpecifier;
|
use deno_core::ModuleSpecifier;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use deno_core::ZeroCopyBuf;
|
use deno_core::ZeroCopyBuf;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
|
@ -27,6 +37,7 @@ use std::thread::JoinHandle;
|
||||||
pub struct CreateWebWorkerArgs {
|
pub struct CreateWebWorkerArgs {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub worker_id: u32,
|
pub worker_id: u32,
|
||||||
|
pub parent_permissions: Permissions,
|
||||||
pub permissions: Permissions,
|
pub permissions: Permissions,
|
||||||
pub main_module: ModuleSpecifier,
|
pub main_module: ModuleSpecifier,
|
||||||
pub use_deno_namespace: bool,
|
pub use_deno_namespace: bool,
|
||||||
|
@ -47,6 +58,14 @@ struct HostUnhandledErrorArgs {
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WorkerThread {
|
||||||
|
join_handle: JoinHandle<Result<(), AnyError>>,
|
||||||
|
worker_handle: WebWorkerHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type WorkersTable = HashMap<u32, WorkerThread>;
|
||||||
|
pub type WorkerId = u32;
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
rt: &mut deno_core::JsRuntime,
|
rt: &mut deno_core::JsRuntime,
|
||||||
sender: Option<mpsc::Sender<WorkerEvent>>,
|
sender: Option<mpsc::Sender<WorkerEvent>>,
|
||||||
|
@ -86,21 +105,348 @@ pub fn init(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WorkerThread {
|
fn merge_permission_state(
|
||||||
join_handle: JoinHandle<Result<(), AnyError>>,
|
target: &PermissionState,
|
||||||
worker_handle: WebWorkerHandle,
|
incoming: Option<PermissionState>,
|
||||||
|
) -> Result<PermissionState, AnyError> {
|
||||||
|
match target {
|
||||||
|
PermissionState::Granted => match incoming {
|
||||||
|
Some(x) => Ok(x),
|
||||||
|
None => Ok(*target),
|
||||||
|
},
|
||||||
|
_ => match incoming {
|
||||||
|
Some(x) => match x {
|
||||||
|
PermissionState::Denied => Ok(x),
|
||||||
|
_ => Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
None => Ok(*target),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type WorkersTable = HashMap<u32, WorkerThread>;
|
fn check_net_permission_contains(
|
||||||
pub type WorkerId = u32;
|
a: &HashSet<String>,
|
||||||
|
b: &HashSet<String>,
|
||||||
|
) -> bool {
|
||||||
|
b.iter().all(|x| a.contains(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_net_permissions(
|
||||||
|
target: &UnaryPermission<String>,
|
||||||
|
incoming: Option<UnaryPermission<String>>,
|
||||||
|
) -> Result<UnaryPermission<String>, AnyError> {
|
||||||
|
if incoming.is_none() {
|
||||||
|
return Ok(target.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_permissions = incoming.unwrap();
|
||||||
|
match &target.global_state {
|
||||||
|
PermissionState::Granted => Ok(UnaryPermission::<String> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
PermissionState::Prompt => match new_permissions.global_state {
|
||||||
|
//Throw
|
||||||
|
PermissionState::Granted => Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
)),
|
||||||
|
//Merge
|
||||||
|
PermissionState::Prompt => {
|
||||||
|
if check_net_permission_contains(
|
||||||
|
&target.granted_list,
|
||||||
|
&new_permissions.granted_list,
|
||||||
|
) {
|
||||||
|
Ok(UnaryPermission::<String> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: target.denied_list.clone(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Copy
|
||||||
|
PermissionState::Denied => Ok(UnaryPermission::<String> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
PermissionState::Denied => match new_permissions.global_state {
|
||||||
|
PermissionState::Denied => Ok(UnaryPermission::<String> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
_ => Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WorkerPermissionType {
|
||||||
|
READ,
|
||||||
|
WRITE,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_read_permissions(
|
||||||
|
allow_list: &HashSet<PathBuf>,
|
||||||
|
current_permissions: &Permissions,
|
||||||
|
) -> bool {
|
||||||
|
allow_list
|
||||||
|
.iter()
|
||||||
|
.all(|x| current_permissions.check_read(&x).is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_write_permissions(
|
||||||
|
allow_list: &HashSet<PathBuf>,
|
||||||
|
current_permissions: &Permissions,
|
||||||
|
) -> bool {
|
||||||
|
allow_list
|
||||||
|
.iter()
|
||||||
|
.all(|x| current_permissions.check_write(&x).is_ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_read_write_permissions(
|
||||||
|
permission_type: WorkerPermissionType,
|
||||||
|
target: &UnaryPermission<PathBuf>,
|
||||||
|
incoming: Option<UnaryPermission<PathBuf>>,
|
||||||
|
current_permissions: &Permissions,
|
||||||
|
) -> Result<UnaryPermission<PathBuf>, AnyError> {
|
||||||
|
if incoming.is_none() {
|
||||||
|
return Ok(target.clone());
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_permissions = incoming.unwrap();
|
||||||
|
match &target.global_state {
|
||||||
|
PermissionState::Granted => Ok(UnaryPermission::<PathBuf> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
PermissionState::Prompt => match new_permissions.global_state {
|
||||||
|
//Throw
|
||||||
|
PermissionState::Granted => Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
)),
|
||||||
|
//Merge
|
||||||
|
PermissionState::Prompt => {
|
||||||
|
if match permission_type {
|
||||||
|
WorkerPermissionType::READ => check_read_permissions(
|
||||||
|
&new_permissions.granted_list,
|
||||||
|
current_permissions,
|
||||||
|
),
|
||||||
|
WorkerPermissionType::WRITE => check_write_permissions(
|
||||||
|
&new_permissions.granted_list,
|
||||||
|
current_permissions,
|
||||||
|
),
|
||||||
|
} {
|
||||||
|
Ok(UnaryPermission::<PathBuf> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: target.denied_list.clone(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Copy
|
||||||
|
PermissionState::Denied => Ok(UnaryPermission::<PathBuf> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
PermissionState::Denied => match new_permissions.global_state {
|
||||||
|
PermissionState::Denied => Ok(UnaryPermission::<PathBuf> {
|
||||||
|
global_state: new_permissions.global_state,
|
||||||
|
granted_list: new_permissions.granted_list,
|
||||||
|
denied_list: new_permissions.denied_list,
|
||||||
|
}),
|
||||||
|
_ => Err(custom_error(
|
||||||
|
"PermissionDenied",
|
||||||
|
"Can't escalate parent thread permissions",
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_worker_permissions(
|
||||||
|
main_thread_permissions: &Permissions,
|
||||||
|
permission_args: PermissionsArg,
|
||||||
|
) -> Result<Permissions, AnyError> {
|
||||||
|
Ok(Permissions {
|
||||||
|
env: merge_permission_state(
|
||||||
|
&main_thread_permissions.env,
|
||||||
|
permission_args.env,
|
||||||
|
)?,
|
||||||
|
hrtime: merge_permission_state(
|
||||||
|
&main_thread_permissions.hrtime,
|
||||||
|
permission_args.hrtime,
|
||||||
|
)?,
|
||||||
|
net: merge_net_permissions(
|
||||||
|
&main_thread_permissions.net,
|
||||||
|
permission_args.net,
|
||||||
|
)?,
|
||||||
|
plugin: merge_permission_state(
|
||||||
|
&main_thread_permissions.plugin,
|
||||||
|
permission_args.plugin,
|
||||||
|
)?,
|
||||||
|
read: merge_read_write_permissions(
|
||||||
|
WorkerPermissionType::READ,
|
||||||
|
&main_thread_permissions.read,
|
||||||
|
permission_args.read,
|
||||||
|
&main_thread_permissions,
|
||||||
|
)?,
|
||||||
|
run: merge_permission_state(
|
||||||
|
&main_thread_permissions.run,
|
||||||
|
permission_args.run,
|
||||||
|
)?,
|
||||||
|
write: merge_read_write_permissions(
|
||||||
|
WorkerPermissionType::WRITE,
|
||||||
|
&main_thread_permissions.write,
|
||||||
|
permission_args.write,
|
||||||
|
&main_thread_permissions,
|
||||||
|
)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct PermissionsArg {
|
||||||
|
#[serde(default, deserialize_with = "as_permission_state")]
|
||||||
|
env: Option<PermissionState>,
|
||||||
|
#[serde(default, deserialize_with = "as_permission_state")]
|
||||||
|
hrtime: Option<PermissionState>,
|
||||||
|
#[serde(default, deserialize_with = "as_unary_string_permission")]
|
||||||
|
net: Option<UnaryPermission<String>>,
|
||||||
|
#[serde(default, deserialize_with = "as_permission_state")]
|
||||||
|
plugin: Option<PermissionState>,
|
||||||
|
#[serde(default, deserialize_with = "as_unary_path_permission")]
|
||||||
|
read: Option<UnaryPermission<PathBuf>>,
|
||||||
|
#[serde(default, deserialize_with = "as_permission_state")]
|
||||||
|
run: Option<PermissionState>,
|
||||||
|
#[serde(default, deserialize_with = "as_unary_path_permission")]
|
||||||
|
write: Option<UnaryPermission<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_permission_state<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<PermissionState>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value: bool = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
match value {
|
||||||
|
true => Ok(Some(PermissionState::Granted)),
|
||||||
|
false => Ok(Some(PermissionState::Denied)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnaryPermissionBase {
|
||||||
|
global_state: PermissionState,
|
||||||
|
paths: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParseBooleanOrStringVec;
|
||||||
|
|
||||||
|
impl<'de> de::Visitor<'de> for ParseBooleanOrStringVec {
|
||||||
|
type Value = UnaryPermissionBase;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a vector of strings or a boolean")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bool<E>(self, v: bool) -> Result<UnaryPermissionBase, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(UnaryPermissionBase {
|
||||||
|
global_state: match v {
|
||||||
|
true => PermissionState::Granted,
|
||||||
|
false => PermissionState::Denied,
|
||||||
|
},
|
||||||
|
paths: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<V>(self, mut visitor: V) -> Result<UnaryPermissionBase, V::Error>
|
||||||
|
where
|
||||||
|
V: SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut vec: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
let mut value = visitor.next_element::<String>()?;
|
||||||
|
while value.is_some() {
|
||||||
|
vec.push(value.unwrap());
|
||||||
|
value = visitor.next_element()?;
|
||||||
|
}
|
||||||
|
Ok(UnaryPermissionBase {
|
||||||
|
global_state: PermissionState::Prompt,
|
||||||
|
paths: vec,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_unary_string_permission<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<UnaryPermission<String>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value: UnaryPermissionBase =
|
||||||
|
deserializer.deserialize_any(ParseBooleanOrStringVec)?;
|
||||||
|
|
||||||
|
let allowed: HashSet<String> = value.paths.into_iter().collect();
|
||||||
|
|
||||||
|
Ok(Some(UnaryPermission::<String> {
|
||||||
|
global_state: value.global_state,
|
||||||
|
granted_list: allowed,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_unary_path_permission<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<Option<UnaryPermission<PathBuf>>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value: UnaryPermissionBase =
|
||||||
|
deserializer.deserialize_any(ParseBooleanOrStringVec)?;
|
||||||
|
|
||||||
|
let paths: Vec<PathBuf> =
|
||||||
|
value.paths.into_iter().map(PathBuf::from).collect();
|
||||||
|
|
||||||
|
Ok(Some(UnaryPermission::<PathBuf> {
|
||||||
|
global_state: value.global_state,
|
||||||
|
granted_list: resolve_fs_allowlist(&Some(paths)),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct CreateWorkerArgs {
|
struct CreateWorkerArgs {
|
||||||
name: Option<String>,
|
|
||||||
specifier: String,
|
|
||||||
has_source_code: bool,
|
has_source_code: bool,
|
||||||
|
name: Option<String>,
|
||||||
|
permissions: Option<PermissionsArg>,
|
||||||
source_code: String,
|
source_code: String,
|
||||||
|
specifier: String,
|
||||||
use_deno_namespace: bool,
|
use_deno_namespace: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +467,16 @@ fn op_create_worker(
|
||||||
let args_name = args.name;
|
let args_name = args.name;
|
||||||
let use_deno_namespace = args.use_deno_namespace;
|
let use_deno_namespace = args.use_deno_namespace;
|
||||||
if use_deno_namespace {
|
if use_deno_namespace {
|
||||||
super::check_unstable(state, "Worker.deno");
|
super::check_unstable(state, "Worker.deno.namespace");
|
||||||
}
|
}
|
||||||
let permissions = state.borrow::<Permissions>().clone();
|
let parent_permissions = state.borrow::<Permissions>().clone();
|
||||||
|
let worker_permissions = if let Some(permissions) = args.permissions {
|
||||||
|
super::check_unstable(state, "Worker.deno.permissions");
|
||||||
|
create_worker_permissions(&parent_permissions, permissions)?
|
||||||
|
} else {
|
||||||
|
parent_permissions.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let worker_id = state.take::<WorkerId>();
|
let worker_id = state.take::<WorkerId>();
|
||||||
let create_module_loader = state.take::<CreateWebWorkerCbHolder>();
|
let create_module_loader = state.take::<CreateWebWorkerCbHolder>();
|
||||||
state.put::<CreateWebWorkerCbHolder>(create_module_loader.clone());
|
state.put::<CreateWebWorkerCbHolder>(create_module_loader.clone());
|
||||||
|
@ -149,7 +502,8 @@ fn op_create_worker(
|
||||||
let worker = (create_module_loader.0)(CreateWebWorkerArgs {
|
let worker = (create_module_loader.0)(CreateWebWorkerArgs {
|
||||||
name: worker_name,
|
name: worker_name,
|
||||||
worker_id,
|
worker_id,
|
||||||
permissions,
|
parent_permissions,
|
||||||
|
permissions: worker_permissions,
|
||||||
main_module: module_specifier.clone(),
|
main_module: module_specifier.clone(),
|
||||||
use_deno_namespace,
|
use_deno_namespace,
|
||||||
});
|
});
|
||||||
|
|
|
@ -78,7 +78,7 @@ pub struct Permissions {
|
||||||
pub hrtime: PermissionState,
|
pub hrtime: PermissionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_fs_allowlist(allow: &Option<Vec<PathBuf>>) -> HashSet<PathBuf> {
|
pub fn resolve_fs_allowlist(allow: &Option<Vec<PathBuf>>) -> HashSet<PathBuf> {
|
||||||
if let Some(v) = allow {
|
if let Some(v) = allow {
|
||||||
v.iter()
|
v.iter()
|
||||||
.map(|raw_path| resolve_from_cwd(Path::new(&raw_path)).unwrap())
|
.map(|raw_path| resolve_from_cwd(Path::new(&raw_path)).unwrap())
|
||||||
|
|
Loading…
Reference in a new issue