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

# なんでそんなことしたいの？

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

evalがアレなのはさんざん言われてるから隔離したい。
[eval() を使わないでください! - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!)

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

Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら
普通に`deno run sample.ts`か`echo "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`が必要。

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

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

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

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

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

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

```ts: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)
};
```

```ts:worker.ts
/// <reference lib="deno.worker" />
self.onmessage = async ({ data }) => {
  const text = await Deno.readTextFile(data);
  self.postMessage(text);
  self.close();
};
```

```sh:結果
% deno run --allow-read -q --unstable main.ts
「失敗」という概念は一度捨ててみて
そしたら今まで得た技術知識...そしてこの絵が
あなたの味方をしてくれますよ
```

# パーミッションの指定

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

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

```ts:全部許可
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`に与えられてる権限以上のことは出来ない。

```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) ;`とかも怖いから、処理が長いときは†自戒†する。

```ts: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;
}
```

```ts: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();
};

```

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

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

console.log(result); // 2
```

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

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


# 参考

- [eval() を使わないでください! - eval() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!)
- [Type Checking Web Workers | Deno Manual](https://deno.land/manual@v1.12.0/typescript/types#type-checking-web-workers)
- [Specifying worker permissions | Deno Manual](https://deno.land/manual/runtime/workers#specifying-worker-permissions)