📚

Chrome 拡張 manifest v3 で サイトブロッカーを作ってみた

5 min read 1

意志の力は弱い、ということで Web サイトの閲覧を禁止する Chrome 拡張を作ってみました。

どうせなら、最新のマニフェスト v3 対応の拡張機能を作ることにします。

マニフェスト v3 について

  • 本家ドキュメント

https://developer.chrome.com/docs/extensions/mv3/intro/
  • 日本語記事

https://forest.watch.impress.co.jp/docs/news/1294367.html

マニフェスト v2 との違い

サイトブロッカーの作成にあたり、次の点でマニフェスト v2 との相違がありました。

  • サイトのブロックに chrome.webRequest が使えない

    Web Request を使うと、Chrome はネットワーク リクエストに含まれるすべてのデータを、監視している拡張機能に送ります。(中略)すべてのリクエスト データが拡張機能に渡るので、悪意のあるデベロッパーは、いとも簡単にユーザーの認証情報やアカウント、個人情報へのアクセスを悪用できます。

    https://developers-jp.googleblog.com/2019/07/web-request-declarative-net-request.html

    There is a new declarativeNetRequest for network request modification, which provides an alternative for much of the webRequest AP's functionality.

    ネットワークリクエストを修正するための新しい宣言型 NetRequest があり、これは webRequest AP の機能の多くを代替するものです。

    https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#when-use-blocking-webrequest

  • 繰り返し処理に setTimeout が使えない

    It's common for web developers to perform delayed or periodic operations using the setTimeout or setInterval methods. These APIs can fail in service workers, though, because the scheduler will cancel the timers when the service worker is terminated.

    Web 開発者が setTimeout や setInterval メソッドを使って遅延や周期的な操作を行うのはよくあることです。しかし、これらの API はサービス・ワーカーでは失敗することがあります。なぜなら、サービス・ワーカーが終了したときにスケジューラーがタイマーをキャンセルするからです。

    https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/

https://zenn.dev/junkawa/scraps/baada1ec8f5227

ソースコード

  • マニフェスト manifest.json
  • サービスワーカー background.js
  • ルールセット rules.json

マニフェスト

manifest.json
{
  "name": "Site Blocker",
  "description": "Site Blocker",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "declarative_net_request": {
    "rule_resources": [
      {
        "id": "ruleset_1",
        "enabled": true,
        "path": "rules.json"
      }
    ]
  },
  "permissions": ["declarativeNetRequest", "alarms"]
}
  • background > service_worker でサービスワーカーに background.js を指定
  • declarative_net_request > rule_resources でルールセットに rules.json を指定
  • パーミッション permissionsdeclarativeNetRequestalarms を指定

サービスワーカー

background.js
function updateRuleset() {
  const dateObj = new Date();
  const isAm = dateObj.getHours() < 12 ? true : false;
  if (isAm) {
    // 午前
    chrome.declarativeNetRequest.updateEnabledRulesets({
      enableRulesetIds: ["ruleset_1"],
    });
  } else {
    // 午後
    chrome.declarativeNetRequest.updateEnabledRulesets({
      disableRulesetIds: ["ruleset_1"],
    });
  }
}

chrome.alarms.create({ delayInMinutes: 1, periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener(() => {
  updateRuleset();
});
  • 午前はルールセットruleset_1 を有効(仕事に集中)
  • 午後はルールセットruleset_1 を無効(怠けても良いよ)
  • 拡張機能の読み込み 1 分後(delayInMinutes: 1)から、1 分ごとに(periodInMinutes: 1)、アラームを起動

備考

  • background.js が存在しなくても、マニフェストとルールセットだけでサイトの閲覧禁止は実現できる
  • 今回 background.js を使用している理由は、午前と午後で閲覧禁止の可否を変更したかったため

ルールセット

rules.json
[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "block"
    },
    "condition": {
      "urlFilter": "youtube.com",
      "resourceTypes": [
        "main_frame",
        "sub_frame",
        "stylesheet",
        "script",
        "image",
        "font",
        "object",
        "xmlhttprequest",
        "ping",
        "csp_report",
        "media",
        "websocket"
      ]
    }
  },
  {
    "id": 2,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "twitter.com",
      "resourceTypes": ["xmlhttprequest"]
    }
  },
  {
    "id": 3,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "mail.google.com",
      "resourceTypes": [
        "main_frame",
        "sub_frame",
        "stylesheet",
        "script",
        "image",
        "font",
        "object",
        "xmlhttprequest",
        "ping",
        "csp_report",
        "media",
        "websocket"
      ]
    }
  },
  {
    "id": 4,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "feedly.com",
      "resourceTypes": ["main_frame"]
    }
  }
]
  • YouTube、Gmail を完全にブロックするのは、難しい。resourceTypes を増やしてごまかしている
  • Twitter は xmlhttprequest をブロックする必要があった
  • Feedly は main_frame でブロックできた

https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#rules

Discussion

勉強になりました。試行錯誤して気づいたことお伝えします。長文すいません。

1. キャッシュ
declarativeNetRequest は一部のキャッシュアクセスをブロックしない。

  • 「www.youtube.com はブロックされています」の画面が表示されてもリロードするとキャッシュデータが表示されることがある。
  • Chromeの設定「閲覧履歴データの削除」で「Cookie と他のサイトデータ」を消すと、ルールが正しければ、きれいにブロックできる。
  • DevTools の Disable cache を有効にしても、ブラウザの「キャッシュの消去とハード再読み込み」を行ってもキャッシュアクセスは完全に止められない。


2. ルールの切り替え
ルールの有効/無効の切り替えを行うと、有効に切り替わる際、既に対象ページを開いている場合が問題となる。例えばYouTube視聴中に、ルールが有効になってもダウンロード済みの動画データは再生され続ける。

3. デバッグ
実際に何がブロックされているかは DevTools -> Network で確認できる。

  • Filterにis:from-cache と入力するとキャッシュアクセスのみ、 -is:from-cache と入力すると、キャッシュアクセス以外を表示できる。


4. ブログ記事の不具合
(1) YouTube条件
動画の視聴中、YouTubeのページは次のようなリクエストを送信する。

initiator: "https://www.youtube.com"
type     : "xmlhttprequest"
method   : "GET"
url      : "https://r1---sn-ogul7nll.googlevideo.com/videoplayback?expire=163792...

このリクエストはURLが "urlFilter": "youtube.com" に該当しないため、ブロックされない。

解決法
ルールを下記のように2つ作成する。
"domains" の内容はURLではなくリクエストのinitiatorと比較される。
2番目のルールは initiator = youtube.com の通信をブロックする。

rules.json
[
{"id": 1, "priority": 1, "action": {"type": "block"},
"condition": {
    "urlFilter": "||youtube.com",
    "resourceTypes": ["main_frame"]
}},
{"id": 2, "priority": 1, "action": {"type": "block"},
"condition": {
    "domains": ["youtube.com"]
}}
]


(2) Twitter条件
"condition": {"urlFilter": "twitter.com", "resourceTypes": ["xmlhttprequest"]} ではブロックできなかった。"main_frame" が抜けている。
なお、ルールが常に有効なら、Twitterも main_frame だけでブロックできた。そうでないなら、(1) YouTube条件 のような修正が必要。

ログインするとコメントできます