User-Agentと私

この記事は、[mediba Advent Calendar 2020](https://qiita.com/advent-calendar/2020/mediba) 2日目です。
[auスマートパス](https://pass.auone.jp/home)のバックエンドエンジニア(BE)、アーキテクト担当をしている北田です。
みんな大好き[User-Agent(以下UA)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent)を通して今年を振り返り、来年を展望してみようと思います。

## User-Agentは便利

auスマートパスでは、auブランドで提供している多彩な端末(以下デバイス)をサポートしています。

- 参考: [Android\(TM\) 技術情報 \| 開発者向け技術情報 \| au](https://www.au.com/developer/android/)

そのため、開発プロジェクトではデバイス要件を必ず確認するようにし、デバイス種別も定義していす。

これまでは、WebAppサーバにて、UAをキーにデバイス種別を判定し、HTMLテンプレートを変更する機能(デバイス種別判定機能)を実装していました。
社内ではよくあるパターンです。

![デバイス種別判定機能をWebAppサーバに実装.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27655/46c690f2-902e-b432-d894-0ad73e79e207.png)

auスマートパスは、2020年2月にUX向上を目的としてシステムの刷新を行い、[BFF](https://samnewman.io/patterns/architectural/bff/)とREST APIサーバで構成されるようになりました。
これを機に、機能配置も見直し、デバイス種別判定機能を独立させました。
Lambda@Edgeでは、よくあるパターンかと思いますが、デバイス種別毎にオリジンを変更する機能を実装しました。

![デバイス種別判定機能を独立させた.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27655/1015cea7-ac86-aec0-21a4-fda8f6d3c2cb.png)

図を見て、富豪過ぎワロタと思われた方もいらっしゃるとは思いますが、その狙いや背景はまた後日...
デバイス種別判定機能を独立して開発、提供出来るようになったので、BFFと開発チームはそのリソースをUX向上に注力できるようなりました。
UAは、多彩な端末をサポートするauスマートパスには、便利で頼もしい存在です。

## User-Agentはリスク

UAを頼りにデバイス毎に最適なUXの提供を図ってきたauスマートパスですが、リスクも明らかになってきました。

### わがままボディ

各所で指摘されている通り、UAはわがままボディです。
以下は、2020年冬モデルの最新端末 [Google Pixel 5](https://www.au.com/mobile/product/smartphone/pixel5/)に搭載されているChromeのUAです。

```
Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36 
```

何度見ても何を伝えたいのかわからない、わがままボディぷりです。
iOS端末に搭載されているauスマートパスアプリは、もっとわがままです。

```
Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148/smps-app/9.1.1
```

最近ですと、我らがCloudFront様が素敵なカスタムヘッダ(`CloudFront-Is-Android-Viewer`や`CloudFront-Is-IOS-Viewer`)を用意してくれており、涙ぐましい正規表現もパースの結果で分岐するというアレな実装も必要がなくなりました。

- [CloudFront HTTP ヘッダーの使用 \- Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-cloudfront-headers.html#cloudfront-headers-device-type)

とはいえ、OSバージョンの判定処理は、自前で組む必要があり、以前アレな実装を続けています。メンテナンス性は、高いとは言えません。

### ゆるふわ

各所で指摘されている通り、UAはゆるふわです。
リクエストヘッダに付与されますので、クライアントからすると容易に偽装でき、サーバからすると容易に取得出来てしまいます。
クライアントサイドにおいても、フリーハンドで取得出来てしまいます。

```javascript
console.log(navigator.userAgent)
# Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148/smps-app/9.1.10
```

デバイス種別という要件が存在し、その種別に応じたUIを提供しているauスマートパスでは、dev tool等での検証が容易であり、サーバサイドでの機能にUAを活かす事もできるメリットはあります。
しかし、リクエストヘッダの取得に何かしらの理由で失敗した場合、そのデバイス種別に対してUIを提供出来ないデメリットとリスクが存在します(2020年5月には、同様の理由で障害が発生してしまいました)。
また、以前から指摘されている通り、[フィンガープリンティングに利用](https://www.saitolab.org/fp_site/)出来てしまい、プライバシー保護にも課題があります。
ゆるふわ故にウェブの理想から大きく逸脱した目的に用いられ、拡張され続けてきたとも言えそうです。

>ウェブは使用しているブラウザーや機器に関係なく、誰からでもアクセスできるものです。
[ユーザーエージェント文字列を用いたブラウザーの判定 \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Browser_detection_using_the_user_agent)

## User-Agentと未来の私

UAに依存するリスクは、各所の動向をフォローしても明らかです。
Googleは、UA文字列を凍結し、[User Agent Client Hints(以下UA-CH)](https://wicg.github.io/ua-client-hints/)を提唱しています。

>We want to freeze and unify (but not remove) the User Agent string in HTTP requests as well as in `navigator.userAgent`
[Intent to Deprecate and Freeze: The User\-Agent string](https://groups.google.com/a/chromium.org/g/blink-dev/c/-2JIRNMWJ7s/m/yHe4tQNLCgAJ?pli=1)
__コロナ禍で、[2021年に延期のステータス](https://groups.google.com/a/chromium.org/g/blink-dev/c/-2JIRNMWJ7s/m/u-YzXjZ8BAAJ)の様です__

Mozillaは、様子見のポジションです。

>The proposal is to build on top of the Client Hints infrastructure and solve these problems over time.
[User Agent Client Hints · Issue \#202 · mozilla/standards\-positions](https://github.com/mozilla/standards-positions/issues/202)

auスマートパスは、UAの課題に対して、どう取り組むべきでしょうか。
UA-CHの[動作確認サイト](https://user-agent-client-hints.glitch.me/)とその[実装](https://glitch.com/edit/#!/user-agent-client-hints?path=public%2Findex.html%3A1%3A0)から、サーバサイドの観点で考察してみます。

尚、UA-CHの動作確認は、Chrome v85 stable以上のバージョンと関連するフラグを有効にする必要があります。

- [User\-Agent Client\-Hints \- The Chromium Projects](https://www.chromium.org/updates/ua-ch)

### サーバサイドUA-CH

まずは、動作確認サイトへのリクエストとレスポンスを確認してみます。

初回のリクエストでは、以下のようなヘッダが付与されています。
![初回リクエスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27655/6d8f726f-493f-de67-ec81-aae09fa6c462.png)

リクエストヘッダは、`sec-ch-ua`及び`sec-ch-ua-mobile`のみ設定されており、制限されていることが伺えます。
また、[server.js](https://glitch.com/edit/#!/user-agent-client-hints?path=server.js%3A52%3A0)でもレスポンスヘッダ:`accept-ch`を付与する実装を確認出来ます。

```javascript
	res.set('Accept-CH', acceptCH.join(', '));
	header = 'Accept-CH: ' + acceptCH.join(', ');
```

2回目のリクエストでは、以下のようなヘッダが付与されています。

![2回目リクエスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27655/0067535e-7a0d-e7a5-6cd6-526096a2040a.png)

レスポンスヘッダ:`accept-ch`は、付与されておりません。
1回目のレスポンスにヘッダとして付与されていた、`accept-ch`の値に基づいたリクエストヘッダが付与されています。

[2回目のリクエスト処理の実装](https://glitch.com/edit/#!/user-agent-client-hints?path=public%2Findex.html%3A137%3A7)は以下の通り、リクエストヘッダを指定しているわけではなく、ブラウザがUA-CHの仕様に基づき付与していることが伺えます。

```javascript
  fetch('/show-headers.json' + document.location.search)
  .then((res) => {
      if (res.status === 200) {
        res.json().then((data) => {
          let secChText = '';
          for (const header in data['Sec-CH']) {
            secChText += `Sec-CH-${header}: ${data['Sec-CH'][header]}\n`;
          }
          document.querySelector('.request').textContent = secChText;
        });
      }
  });
```

上記の挙動は、2020年12月現在のものであり、今後もアップデートが予想されます。
引き続き、フォローする必要がありそうです。

### デバイス種別判定機能をUA-CHする

考察を踏まえ、デバイス種別判定機能は、どうあるべきでしょうか。
要件それ自体を見直す必要もありそうですが、UA-CHに適合していくのであれば、BFFは再びスマートになる必要があるのでしょうか。

![デバイス種別判定機能をUA-CHする.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27655/413f730c-7aae-a11a-72dd-a8c7f1debd6c.png)

JavaScript APIを用いたクライアントサイドでのUA-CHもあるので、時間をつくって、そちらも考察してみようと思います。


## おわりに

UAは便利で息の長いウェブのレガシーですが、転換期にあります。
技術の境界線が変わろうとしていることを強く感じられるものでもあるので、引き続きフォローしていきたいと思います。
また、私はサーバサイドでの機能開発がメインのバックエンドエンジニアのポジションですが、auスマートパスでは、BFFの構築、運用にも携わっています。

技術の境界線を跨ぎ、~~ヒィヒィ言いながら~~貪欲に開発に取り組みたい仲間を募集していますので、興味が湧いた方、話だけでも聞いてみたい方は、是非エントリーをお願いします。

- [バックエンドエンジニア（auスマートパス・５Gサービス） \| 株式会社mediba](https://hrmos.co/pages/mediba/jobs/900000039)