阅读 87

如何在 Deno 中方便且安全地使用第三方库?

如何在 Deno 中方便且安全地使用第三方库?

Node 的安全问题是一直被诟病的,社区大、很多没有被维护好的库,而 Node 没有提供访问权限的控制,所以引入一个依赖包时它有可能做任何事。

Deno 被提出时要解决的问题之一就是安全。所以 Deno 提供了进程上或 Worker 上的访问控制,每次运行 Deno 程序时都要指明程序的权限:

  • 是否可读
  • 是否可写
  • 是否可以访问网络
  • 是否可以运行子命令
  • ...

但目前 Deno 还不支持在 import 时指定一个包的访问权限,因为相关实现相当麻烦。而这个需求又是普遍存在的:我们有一个庞大的社区,社区仓库相当多,其中也龙蛇混杂,在使用一个库的时候很少有人会有精力去检查/跟踪安全问题,而如果不能解决信任问题我们可能更倾向于不使用或是 fork 一个库。

Deno issues 上的相关讨论:Sandbox mode for external scripts。 因为 Deno 是遵照 ECMA 的标准走的,所以等到沙盒相关的提案被加入 ECMAScript 标准后 Deno 就大概率会有官方的沙盒支持。其中包括了到 Stage 3 的 realms 和还处于 Stage 1 的ses

方案

那目前我们有什么方式可以 100% 放心地引入一个包呢?

Workers 是目前最佳方案。Deno 中的 Worker 除了加载 js 也可以直接加载 deno 格式的 TypeScript 的代码。每个 Worker 在创建时都可以细粒度地指定权限,例如对哪些文件可读,对哪些可写,只允许访问哪些域名。 Worker 中可以再开启子 Worker,子 Worker 能继承父亲的权限或是被限制得更严格。

const worker = new Worker(new URL("./worker.ts", 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,
    },
  },
});
复制代码

但通过 Worker 就会有大量重复性的代码,有没有什么库可以帮我们做这个事呢?

Comlink

Comlink 是让用户能够更自然使用 Worker 的一个库,只要在正常 worker 操作前加上 await 就可以使用 worker 导出的对象。

并且 2021 年 7 月发布的 Deno 1.12 支持了 MessageChannel 和 Message Port,这让 Comlink 的特性能够在 Deno 上被完全发挥(例如下例中向 Worker 传递一个 callback,其实依赖了 Message Port 的接口)。想了解 Comlink 的具体实现原理可见这篇文章

通过 Comlink 实现在 Worker 上插入和查询:

main.js

import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
const DataProcessor = Comlink.wrap(new Worker("./worker.js"));
let processor = await new DataProcessor();

export async function insert(s) {
  await processor.insert(s);
}

export async function search(s) {
  return await processor.search(s);
}

export async function map(callback) {
  return processor.map(Comlink.proxy(callback));
}
复制代码

worker.js

importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");

class DataProcessor {
  arr = [];
  constructor() {}

  insert(...s) {
    this.arr.push(...s);
  }

  search(target) {
    const ans = [];
    for (const value of this.arr) {
      if (value.str.match(target)) {
        ans.push(value);
      }
    }

    return ans;
  }

  map(callback) {
    return Promise.all(this.arr.map(callback));
  }
}

Comlink.expose(DataProcessor);
复制代码

Comlink + Deno Worker

从而我们可以提供一个 Comlink + Workers 的封装,暴露一个新的 import 方法,从而我们就可以安全地 import 任何脚本啦。

但刚想动手做的时候发现了一样想法的仓库 importw

使用方式为

import {
  importw,
  release,
  worker,
} from "https://deno.land/x/importw@1.0.0/mod.ts";

// 在 worker 内 import 一个包
// log, add 是包所提供的函数
// release 和 worker 是内置地 symbol,用于获取 worker 的操作对象
const { log, add, [release]: terminate, [worker]: workerRef } = await importw(
  "https://deno.land/x/importw@1.0.0/examples/basic/exampleMod.ts",
  {
    name: "exampleWorker",
    deno: {
      namespace: true,
      permissions: {
        net: [
          "deno.land",
        ],
        write: false,
      },
    },
  },
);

// 访问内部的 Worker 的方式
console.log(workerRef.constructor.name); // Worker

// 在 Worker 内执行代码
await log(`add(40, 2) in a worker:`, await add(40, 2));

// Gracefully release Worker resources
await terminate();
复制代码

添加类型目前会略显啰嗦

import type * as M from "https://deno.land/x/importw@1.0.0/examples/basic/exampleMod.ts";

...

const ... = await importw<{log: typeof M.log, add: typeof M.add}>(
  ...
);
复制代码

但要注意通过这种方式也达不到 100% 的安全。如果是长时间运行的脚本还有可能受到 side channel timing attacks。

通过这种方式引入包之后除了沙盒也并行化了 挺好 🌝

文章分类
前端
文章标签