LoginSignup
3
4

More than 1 year has passed since last update.

Denoで安全にevalしたい (Web Worker)

Last updated at Posted at 2021-07-19

なんでそんなことしたいの?

某サイトをスクレイピングするとき必要だった。

evalがアレなのはさんざん言われてるから隔離したい。
eval() を使わないでください! - JavaScript | MDN

Web Workerを使えばメイン側とワーカー側でグローバルが違うから隔離できるはず。
Denoのデフォルトではワーカー側がメイン側のパーミッションを引き継ぐ。
メイン側で--allow-netしてるとワーカー側でネット通信とかできちゃう。
ので、ワーカー側のパーミッションを無効化して使います。

Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら
普通にdeno run sample.tsecho "1 + 1" | deno --でいいと思います。
Denoに権限を与えてないから何もできないはずなので。

とりあえずWorkerを使ってみる

メイン側。
new Workerのオプションには必ず{type: "module"}をつける。

main.ts
const workerPath = new URL("./worker.ts", import.meta.url).href;
const worker = new Worker(workerPath, { type: "module" });

worker.postMessage("あなたが青く見えるなら、");
console.log("main: posted.");

worker.onmessage = ({ data }) => {
  console.log("main: received: ", data!);
};

ワーカー側。
イベントハンドラの型を認識させるため/// <reference lib="deno.worker" />を一行目に書く。

worker.ts
/// <reference lib="deno.worker" />
self.onmessage = ({ data }) => {
  console.log("  worker: received: ", data);

  self.postMessage("りんごもうさぎの体も青くていいんだよ");
  console.log("  worker: posted.");

  self.close();
};

実行結果。
worker.tsがローカルにあるので--allow-readを指定してる。
外部にあるなら--allow-netが必要。

実行結果
% deno run -q --allow-read main.ts
main: posted.
  worker: received:  あなたが青く見えるなら、
  worker: posted.
main: received:  りんごもうさぎの体も青くていいんだよ

new Workerしたらメイン側からworker.terminate()するか、ワーカー側で自らself.close()しないと動き続ける。

受け渡しするデータは送信側で文字列(JSON)にされて受信側でオブジェクトにされるので同一じゃない。
どこでもドアみたい。怖い。
【閲覧注意】 どこでもドアの秘密

Worker側でDenoの機能を使ってみる

オプションに{deno: {namespace: true}}をつけるとワーカー側でDenoが使えるようになる。
実行時に--unstableが必要になる。

ワーカー側からDeno.readTextFileで同じフォルダにあるファイルを読んでみる。

main.ts
const workerPath = new URL("./worker.ts", import.meta.url).href;
const worker = new Worker(workerPath, {
  type: "module",
  deno: {
    namespace: true
  },
});

// テキストファイルのパスを絶対パスにする。
// カレントフォルダにあるファイルを読みたいなら相対パスをそのまま渡す。
const textPath = new URL("./msg.txt", import.meta.url).href.replace("file://", "");
worker.postMessage(textPath);

worker.onmessage = ({ data }) => {
  console.log(data)
};
worker.ts
/// <reference lib="deno.worker" />
self.onmessage = async ({ data }) => {
  const text = await Deno.readTextFile(data);
  self.postMessage(text);
  self.close();
};
結果
% deno run --allow-read -q --unstable main.ts
「失敗」という概念は一度捨ててみて
そしたら今まで得た技術知識...そしてこの絵が
あなたの味方をしてくれますよ

パーミッションの指定

パーミッションはデフォルトではmain側のものが引き継がれる。("inherit")
deno run -Aで全ての権限を与えてnew Workerの第二引数でdenoを書かなかった場合、
Denoが使えないからDeno.readTextFileは使えないけど、fetchは使えるので通信はできちゃう。

全部拒否する場合はこう。
denoプロパティを指定するために--unstableが必要。

全部許可
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
  type: "module",
  deno: {
    namespace: false,
    permissions: "none",
  },
});

それかdeno: falseでnamespaceとpermissionsを全て無効にできる。

一部許可する場合はこんな感じ。
ここで許可してもmain.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,
    },
  },
});

Denoで安全にevalしたいコード

while(true) ;とかも怖いから、処理が長いときは†自戒†する。

save_eval.ts
const WORKER_PATH = "./safe_eval_worker.ts";
const TIMEOUT_SEC = 3;

/** workerにpostMessageして返事を受け取るPromiseを返す */
function waitForWorkerResponse(
  worker: Worker,
  message: string,
  timeoutSeconds = TIMEOUT_SEC,
) {
  return new Promise((resolve, reject) => {
    worker.postMessage(message);

    // sec秒経過したら失敗
    const timerId = setTimeout(() => {
      worker.terminate();
      reject(new Error(`worker operation timeout (${timeoutSeconds} seconds)`));
    }, timeoutSeconds * 1000);

    // 返事がきたら成功
    worker.onmessage = ({ data }) => {
      clearTimeout(timerId); //タイマー解除
      worker.terminate();
      resolve(data);
    };
  });
}

/** Workerを使って安全にevalできるはずの関数 */
export async function safeEval(source: string, timeoutSeconds = TIMEOUT_SEC) {
  const workerURL = new URL(WORKER_PATH, import.meta.url).href;
  const worker = new Worker(workerURL, { type: "module", deno: false });
  const result = await waitForWorkerResponse(worker, source, timeoutSeconds);
  return result;
}
safe_eval_worker.ts
/// <reference lib="deno.worker" />

/** evalの代替 */
function evaluator(source: string) {
  return Function(`'use strict'; return ${source}`)();
}

self.onmessage = ({ data: source }) => {
  const evaluated = evaluator(source);
  self.postMessage(evaluated);
  self.close();
};

sample.ts
import {safeEval} from "./safe_eval.ts"

const source = `1 + 1`;
const result = await safeEval(source);

console.log(result); // 2

こんな感じにした。
何回も実行する必要があるなら毎回new Workerせず使いまわしたほうがいいと思います。

なんか間違ってたり改善点あれば教えて欲しいです。
あと「こんなん全然ダメだよ」って思われた方もぜひ。
絶対ですよ。

参考

3
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4