Web Application Frameworkにおけるパスに含まれるPercent-Encodingの処理方法について

URIはUS-ASCII以外の文字を含めたい時、パスやクエリの要素にURIで区切り文字に使われる文字(`/`,`?`, `#`, `&`など)を含めたいとき、Percent-Encodingという方法を使ってエンコーディングします。

例えば、`http://www.example.com/event/吉祥寺pm?運営は?=Magnolia`というURLは、以下のようにエンコーディングする必要が有ります。

`http://www.example.com/event/%E5%90%89%E7%A5%A5%E5%AF%BApm?%E9%81%8B%E5%96%B6%E3%81%AF%3F=Magnolia`

仕組みとしてはUTF-8の文字コードをバイトごとに16進数表記し、先頭に`%`を付加しています。

(規格上は、UTF-8以外でもエンコーディングは可能ですが、現代で使う必要性は無いと思います)

エンコーディング方法について、詳しくは[RFC3986](https://tools.ietf.org/html/rfc3986)や、[URL Standard](https://url.spec.whatwg.org)を参照して下さい。

## Web Application Frameworkにおける処理

Web Application Frameworkのルーティング定義では、ハンドラをパスごとに定義します。例えば、PerlのKossyというSinatraライクなWAFでは`/hello`にアクセスした時の処理を以下のように定義します。

```perl
use Kossy;

get '/hello' => sub {
    my ( $self, $c )  = @_;
    $c->render('index.tx', { greeting => "Hello!" });
};
```

では、先ほどのPercent-Encodingされたパスを処理したい時はどのように定義するか？という話になります。

先ほどのKossyの例で言えば、以下のようになります(実際のアプリケーションでは、`get /event/:event_name`という形式でパラメータ化すると思いますが…)。

```perl
use Kossy;

get '/event/吉祥寺pm' => sub {
...
};
```

デコードされた後のパスで定義できますね。

### いつデコードされたのか？

さて、これはどんな仕組みになっているか？

Kossy.pmの中でルーティング定義と、パスをマッチさせる箇所は以下のようなっています。

```perl
my $path_info = Encode::decode_utf8( $env->{PATH_INFO},  Encode::FB_CROAK | Encode::LEAVE_SRC );
my @match = $router->match($path_info);
```

`PATH_INFO`という環境変数を参照していることが分かります。

`PATH_INFO`は、KossyのバックエンドであるPlack(Web Application Frameworkと、Application Serverとの間のインタフェースを標準化するためのミドルウェア)で定義されたURIのパスを取得するための環境変数ですが、これは既にPercent-Encodingがデコードされた状態で格納されています。その経緯は、Plack::FAQにずばり書かれています。

[PSGI::FAQ - Frequently Asked Questions and answers - metacpan.org](https://metacpan.org/pod/release/MIYAGAWA/PSGI-1.02/PSGI/FAQ.pod#Why-is-PATH_INFO-URI-decoded?)

CGIの仕様(RFC 3875)を踏襲したため、と明確に書かれています。

> Why is PATH_INFO URI decoded?
> To be compatible with CGI spec (RFC 3875) and most web servers' implementations (like Apache and lighttpd).

PlackベースのWAFでは、この`PATH_INFO`をベースにルーティングのディスパッチをすると定められています。

[Plack::Request](https://metacpan.org/pod/Plack::Request)のPODより

> DISPATCHING
> If your application or framework wants to dispatch (or route) actions based on request paths, be sure to use $req->path_info not $req->uri->path.

つまり、PlackをベースとしたWAFでは、Plack::FAQに書かれている通り、ルーティング定義においては `foo%2fbar`と、`foo/bar`を区別することができません(区別したい時は、別途`REQUEST_URI`という環境変数に格納された元のURIを使います)。

ただ、ルーティング定義でPercent-Encodingされた状態で定義するのも分かりづらいので、一律デコード済みの`PATH_INFO`とルーティング定義をマッチさせるのは一つの選択だと思います。

## 他の言語では？

では他のPerl以外の言語では、どうなっているか？

### RubyのRack

[https://www.rubydoc.info/github/rack/rack/file/SPEC](https://www.rubydoc.info/github/rack/rack/file/SPEC)

> PATH_INFO
> The remainder of the request URL's “path”, designating the virtual “location” of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.

と書かれているので、Percent-Encodingされていると、そのまま渡されます。あとは、WAF側の対応次第ですね。

RackベースのWAFの一つであるSinatraでは、これをスラッシュ(`/`)で分割して、パスとマッチさせるパターンを生成していました（Perlとは逆に、定義したルーティングをpercent-encodingする）。

ちょっとバージョンは古いですが、以下のコードが該当します。
[https://github.com/sinatra/sinatra/blob/1f4444df234a81df582d3171131f8c5e8dd3e6ea/lib/sinatra/base.rb#L1617-L1704](https://github.com/sinatra/sinatra/blob/1f4444df234a81df582d3171131f8c5e8dd3e6ea/lib/sinatra/base.rb#L1617-L1704)

このやり方なら`foo%2fbar`と、`foo/bar`を区別することは可能ですね。

ただし、正規表現を使ったルーティング定義では、これが適用されていません。つまり、Percent-Encodingに対して、何も処理されません。アプリケーション側で対応する必要が有ります。おそらく複雑な正規表現の文法を壊さず、エンコードするのが困難だったからでしょう。

### JavaのServlet

JavaのServletもCGIの仕様にならっていて、`getPathInfo`で得られる値はデコードされています。Servletの仕様には、CGIと同じ、としか書かれていませんが、以下のstack overflowの回答にまとまっています。

[https://stackoverflow.com/questions/966077/java-reading-undecoded-url-from-servlet](https://stackoverflow.com/questions/966077/java-reading-undecoded-url-from-servlet)

## 結論

他にも、node.js + Expressを少し触ってみましたが、こちらはフレームワーク側では一切なにも処理していませんでした。アプリケーション側で全部対応するしかないです。

ライブラリやフレームワークによってアプリケーション側での処理方法は全然違うので、ちゃんと調べましょう。