プロキシをON/OFFするChrome拡張を作ってみた

プロキシを設定できるChrome拡張はすでに高機能なものがいくつかあるが、Chromeウェブストアでは「ベスト プラクティスに沿わないため、まもなくサポートされなくなる可能性があります。」などと警告が表示されているものもある。

使用できなくなると困るので念のため自前で作成することにした。

* Proxy SwitchyOmega https://chromewebstore.google.com/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=ja
* Proxy SwitchySharp https://chromewebstore.google.com/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm?hl=ja

:::note
Chrome拡張のManifest V2は廃止が決定しており、警告が表示されているのはManifest V2を使用したままのためと思われる。
なお、この記事のコードはManifest V3の仕様で作成している。

Google Developers Japan: Manifest V2 の段階的廃止を開始 - https://developers-jp.googleblog.com/2024/06/manifest-v2-phase-out-begins.html
:::

# Chrome拡張でプロキシを設定するには

そもそもなぜChrome拡張でプロキシを設定できるのか謎だが、[chrome.proxy](https://developer.chrome.com/docs/extensions/reference/api/proxy)のリファレンスを見るとたしかに設定できると書いてある。

設定を変更するには`chrome.proxy.settings.set`メソッドを呼び出す。

```background.js
async function setProxySettingsFixedServer() {
    // 省略

    let config = {
        mode: "fixed_servers",
        rules: {
            singleProxy: {
                host: "proxy.example.com",
                port: 8080
            },
        }
    };

    await chrome.proxy.settings.set({ value: config });
}
```

`value`には`mode`と`rules`を指定する必要がある。
プロキシサーバーのホスト名を自前で設定するなら`mode`には`fixed_servers`を指定する。
`rules`にはプロキシサーバーのホスト名などを指定する。
単に設定するなら`singleProxy`と`host`を設定する。
必要に応じて`scheme`と`port`も指定する。

プロキシサーバーを使用したくない場合は`mode`に`direct`を指定することもできる。

プロキシサーバーがhttpとhttpsで分かれている場合はそれぞれ指定することもできるらしい。
（今時サーバーをわけるメリットはあまりないと思われるが）

```javascript
// プロキシサーバーがhttpとhttpsで分かれている場合はたぶんこんな感じ（動作未確認）

let config = {
  mode: "fixed_servers",
  rules: {
    proxyForHttp: {
      scheme: "http",
      host: "proxy.example.com"
    },
    proxyForHttps: {
      scheme: "https",
      host: "proxy2.example.com"
    }
  }
};
```

設定したプロキシの設定を元に戻すには`chrome.proxy.settings.clear`を呼び出す。

```background.js
async function clearProxySettings() {
    await chrome.proxy.settings.clear({ scope: 'regular' });
}
```

なお、`chrome.proxy`を使用するには`manifest.json`で`permissions`に`proxy`の指定が必要である。

```manifest.json
    "permissions": [
        "proxy",
        "storage",
        "tabs"
    ]
```

:::note
ここでは`chrome.storage`と`chrome.tabs`も使用するので`storage`と`tabs`も指定している。
:::

## Chrome拡張のアイコンをクリックしたときの処理

アイコンをクリックしたときの処理を受け取るには`manifest.json`に`action`の指定が必要になる。

```manifest.json
    "action": {}
```

:::note warn
`action`の指定は**空でよいが必要**。
消してはいけない。
:::

ようやくメインのコードだが、アイコンがクリックされると`chrome.action.onClicked`のイベントが呼ばれるので`addListener`で処理を追加しておく。
プロキシ設定を有効にしたかのフラグは`chrome.storage.session`から取得・保存しておく。
proxyEnableの値を反転して、値によりプロキシの設定をするか、設定をクリアするか切り替える。

```background.js
chrome.action.onClicked.addListener(async () => {
    chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' });

    let data = await chrome.storage.session.get('proxyEnable');
    let proxyEnable = !data.proxyEnable;

    if (proxyEnable) {
        await setProxySettingsFixedServer();
    }
    else {
        await clearProxySettings();
    }

    await chrome.storage.session.set({ "proxyEnable": proxyEnable });
});
```

:::note
`chrome.storage.session`を使用するには先に`setAccessLevel`を呼び出す必要がある。
:::

## ブラウザを閉じたときの設定クリア

実は`chrome.proxy.settings.set`メソッドで設定した内容はブラウザを閉じても残っている。（終了時に自動で破棄されるような設定はみあたらなかった）　
このため、ブラウザを再度開いた際に「Chrome拡張をクリックしてないのにプロキシがONになってるぞ？？」という事態になってしまう。
これを避けるため、ブラウザを閉じたときに設定をクリアしておく。

ブラウザを閉じたかの判定は`chrome.tabs.onRemoved`で`removeInfo.isWindowClosing`をチェックすればよい。
ここでは`removeInfo.isWindowClosing`がセットされていたらプロキシ設定をクリアする。

```background.js
chrome.tabs.onRemoved.addListener((_tabId, removeInfo) => {
    if (removeInfo.isWindowClosing) {
        clearProxySettings();
    }
});
```

## ソースコード

https://github.com/betarium/proxy-on-off-button-chrome-extension