初めに
テックタッチアドベントカレンダー 14 日目を担当する izzii です。最近個人 PC の SSD を増設したのですが、CPU やマザボも換装しようかなーなどと考えている今日この頃です。
さて、 Web エンジニアの方ですと、集計処理やセキュリティ運用のために、
- URL バリデーション
- URL エンコーディング
- URL パース
など URL の文字列を操作・評価したことがあるしょう。しかし、抽象化されたライブラリを利用することで、具体的な実装をあまり意識されたことはないのではないでしょうか?というのも自分達で設計した URL を弄る上で壁にぶつかる機会というのは、滅多に遭遇しないと思われるからです。私はたまたま多様な URL を複数のライブラリに適用する機会があったため、改めて URL の標準や現実的に使われている URL について考える機会がありました。
この記事ではライブラリごとに URL の仕様が異なる ことから得られた学びを共有します。 URL の標準や実際、具体的なライブラリの挙動を調べてみましたので興味が湧きましたらぜひお読みください!
URL の標準を知るためのポイント
一般的に、URL の標準とは RFC3986 に定義された URI 標準形式だと考えられていそうです。 ただし標準というよりは最も厳格な URL 形式と呼んだほうが良いかもしれません。URL に公式の標準が存在しないことは RFC 3305 のセクション 2.2 にも書かれています。
RFC3986 は URL ではなく URI について書かれた資料ではありますが、実際に自分が URL バリデーターやパーサーのソースコードやドキュメントを調べたところ RFC3986 を参照して実装されていることがうかがえました。URL と URI の共通見解については先ほども挙げた RFC 3305 のセクション 2.2 を参考にしてみてください。
さて、 RFC3986 から URL バリデーションのポイントを抑えていきましょう。 RFC3986 セクション 2 に着目します。
まずはこちら、URL として利用可能な文字についてです。
reserved = gen-delims / sub-delims gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" pct-encoded = "%" HEXDIG HEXDIG
^<> といった文字や日本語は除かれていることがうかがえます。
URI 標準形式は reserved, unreserved, pct-encoded で構成されます。 reserved 文字もデリミタとして利用されない場合はパーセントエンコーディングの対象です。 例えばクエリ文字列に & が含まれてしまうと URL が壊れてしまうことからも想像に難くないと思います。
ここまでで機械的に URL として利用可能な記号や文字種を知ることができました。 URI 標準形には構文があるので、それを知らなければ有効な URL について考えることはできないのですが、 URI 標準形の構文自体は直感や実際に利用されている URL とそこまで差異はありませんのでスキップさせてください。
あ!やせいの URL がとびだしてきた!
web の URL か否かで多様性は大きく異なりますが、以下のような URL で通信しているシステムに出会う可能性があります。
# TLD がない、パブリック DNS は使えないだろう。 https://foo/ # 日本語ドメイン(Punycode) https://ふうばあ.com/ # クエリパラメータがありそうでない https://foo.com/bar? # 日本語がエンコードされていないし空白文字も入っている https://foo.com/bar?time=Mon Oct 17 2022 16:48:37 GMT+0900(東京(標準時)) # クエリ中の {} は URI 標準形的にはパーセントエンコードの対象 https://foo.com/bar?ID={03abc184-9bca-490e-b1c2-a0ee3789d10f} # あまり見ない形式のクエリ構文 https://foo.com/bar;q1;q2;q3
日本語を暗にエンコードしてくれるブラウザのアドレスバーや Punycode といった話ではなく、日本語を含んだ URL で通信しているサイトも存在します。UTF8 などの形式で Unicode を直接埋め込んでいる、という意味です。
先に参照した、RFC 3305 のセクション 2.2 にもあるように現在 URL に公式の定義はないので、URI 標準形に従わないからダメな URL ということはありません。ただし、後述するようにツールによって URL の解釈が異なるのでなるべく URI 標準形で通信するように設計された方が環境依存のバグを起こしにくいとは思います。
ライブラリを比較
さて、こうした背景の中、URL のバリデーションは如何にして行うべきでしょうか?私は「公開された実装を参考にしながらも目的に応じてカスタマイズするべき。」だと考えていますが、その根拠となるようにいくつかライブラリを例に挙げて挙動を見ていきます。
比較として用いる URL? の一覧です。
# URI 標準形 https://example.com https://exam_ple.com https://exam--ple.com https://[ffff::ffff:ffff:ffff:ffff]/ https://172.16.24.58/example https://localhost/example https://localhost:3001/example https://foo/bar/ ftps://hoge.com/foo # ここから下は URI 標準形に沿わない https://日本語.com https://foo.com/bar?time=Mon Oct 17 2022 16:48:37 GMT+0900(東京(標準時, https://example.com.s https://exam+ple.com https://exam ple.com https://exam<>ple.com https://exam%20ple.com
そして比較のためのバリデーターツール validators(Python)、regex-weburl.js(Javascript)、url.Parse(Go)での URL validation の結果です。 それぞれの紹介と結果の振り返りは後述します。
URL | validators(Python) | regex-weburl.js(Javascript) | url.Parse(Go) |
---|---|---|---|
https://example.com | True | True | True |
https://exam_ple.com | False | True | True |
https://exam--ple.com | False | True | True |
https://[ffff::ffff:ffff:ffff:ffff]/ | True | False | True |
https://172.16.24.58/example | True | False | True |
https://localhost/example | True | False | True |
https://localhost:3001/example | True | False | True |
https://foo/bar/ | False | False | True |
ftps://hoge.com/foo | False | False | True |
https://日本語.com | True | True | True |
https://foo.com/bar?time=Mon Oct 17 2022 16:48:37 GMT+0900(東京(標準時)) | False | False | True |
https://example.com.s | False | False | True |
https://exam+ple.com | False | False | True |
https://exam ple.com | False | False | False |
https://exam<>ple.com | False | False | True |
https://exam%20ple.com | False | False | False |
validators (Python)
Python の比較的利用者の多い validators というバリデーションツールの結果を最初に見てみましょう。
validators ライブラリの url バリデーターは比較対象の regex-weburl.js の実装を参考にしているとドキュメントに記載があります。
実行バージョンは 0.20.0
です。
host 部分で _ や - を有効な文字列と見做しません。
また TLD のついていない host も有効と見做しません。しかし、クライアント端末やネットワーク設定によっては通信できる URL です。さらに path 以降の要素の判定も厳格です。
regex-weburl.js (Javascript)
regex-weburl.js は、先ほど紹介した validators も実装を参考にしている、比較的アクティブに更新、議論がなされているスニペットです。しかし、validators とは若干挙動が異なります。 weburl という名前の通り、特に private IP レンジや IPv6 を有効な URL と見做しません。
url.Parse (Go)
url.Parse は Go の URL パーサーです。正確にはバリデーターではないのですが、Go-SCP (テックタッチが公開する日本語版もあります。) では URL バリデーションの際の選択肢として挙げられていました。パースの可否に応じて有効か判定することによる利用が想定されます。しかし、今まで挙げた2つに比べると今度は緩すぎる印象です。
以上のように使うライブラリによって有効な URL だと判定される範囲は異なります。 評価したい URL の多様性や目的に応じてバリデーターはある程度ご自身で実装された方が良いと考える根拠です。
最後に
実はブラウザの URL 書き換えの挙動や、URI エンコーダーの挙動を見てみたりもしたのですが、ブログが収拾しなくなるので載せるのをやめました...笑 この記事が学びになったと感じてくれた方が少しでもいらっしゃれば幸いです!
テイクホームメッセージは以下です。
- URL には URI 標準形という標準らしきものは存在する。
- URL バリデーションツールはある程度 URI 標準形を参考にしつつ、それぞれが独自に URL を定義しているため、目的に沿わない可能性がある
- URL バリデーションについては公開された実装を参考にしながらも、目的に応じてカスタマイズするべき。
メリクリ&良いお年を!