URI参照の基本

この記事は、[富士通クラウドテクノロジーズ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fjct)の22日目の記事です。21日目は[@kzmake](https://qiita.com/kzmake)さんの[GCRA(Generic cell rate algorithm)でAPIリクエストをレート制限する話](https://qiita.com/kzmake/items/29d8745173bb5c3de8d9)でした。

## はじめに

二フクラPaaSチームの@hunter9xです。今回はURI参照の一部に対する自分の理解をまとめてみようと思います。基本知識ですが、ネットで調べてみた際にあまり情報がなかったので書いてみようと思いました。

## URIの基本

[rfc3986#section-3](https://tools.ietf.org/html/rfc3986#section-3)ではURIは大体５つのパーツから構成されます。

```
     foo://example.com:8042/over/there?name=ferret#nose
     \_/   \______________/\_________/ \_________/ \__/
      |           |            |            |        |
   scheme     authority       path        query   fragment
      |   _____________________|__
     / \ /                        \
     urn:example:animal:ferret:nose
```

更に`//` から最初の`/`までの`authority`部は下記のように構成されます。

```
  authority   = [ userinfo "@" ] host [ ":" port ]
```

様々なプロトコルやソフトウェアで上記で定義されたURI参照を用いてリソースのアクセスを実現しています。URI参照では、渡されたURIのすべてのパーツに沿って参照しなくても、現在の場所(base URI)からターゲットのリソース(target URI)にアクセスすることができます。例えば、`https://pfs.nifcloud.com/service/dns.htm`にアクセスした後、`https://pfs.nifcloud.com/service/ess.htm`が渡されたとすると、`https://pfs.nifcloud.com/service/`までが同じなので`ess.htm`に参照すれば良いです。他には `#` などを用いた相対パスの参照があります。効率化のため、多くのプロトコルやソフトウェアではこのように柔軟にURIをパースしています。

## 相対URIの参照

さて、５つのパーツが揃っていないURIが渡された時にuri parserはどう動くべきかみて行きます。
詳細なアルゴリズムは[rfc3986#section-5.2](https://tools.ietf.org/html/rfc3986#section-5.2)で示されており、かつパターンが非常に多いのでここでは、筆者自身が興味あるものだけを対象とします。

| base uri                      | target uri         | result                            |
| ---                           | ---                | ---                               |
| `http://localhost:8080/hoge/` | `//localhost:8081` | `http://localhost:8081`             |
| `http://localhost:8080/hoge/` | `/fuga`            | `http://localhost:8080/fuga`      |
| `http://localhost:8080/hoge/` | `fuga`             | `http://localhost:8080/hoge/fuga` |
| `http://localhost:8080/hoge`  | `fuga`             | `http://localhost:8080/fuga`      |

## 検証

検証内容は、HTTPクライアントがHTTP サーバーに base uri でアクセスする時に、HTTPサーバー側で targe uri にリダイレクトする状況を作り、HTTPクライアント側の動きを確認してみたいと思います。検証で使うものは下記の通りです。

| 項目               | spec                              |
|--------------------|-----------------------------------|
| HTTP サーバー      | python 3.7.4 `http.server` module |
| HTTP クライアント1 | php 7.2.24 `file_get_contents()`  |
| HTTP クライアント2 | curl 7.58.0 (x86_64-pc-linux-gnu) |

### 準備

* 参照ディレクトリの構成

```
.
├── fuga
│   └── index.htm
└── hoge
    └── index.htm
```

#### HTTPサーバ側 (python)

```python
import http.server
import socketserver

PORT = 8080
BIND = "127.0.0.1"
target_uri = ""

class Redirect(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(301)
        self.send_header("Location", target_uri)
        self.end_headers()

with socketserver.TCPServer((BIND, PORT), Redirect) as httpd:
    print("serving at port", PORT)
    httpd.serve_forever()
```

すごく簡単なもので、無限にリダイレクトするようになっています。 `target_uri`を変更しながら４パターンをテストします。目的はクライアント側の動きをみるので、そちらでリダイレクト回数を制限し、サーバーの標準出力でHTTPヘッダーで結果を確認します。

### HTTP クライアント 1 (php)

```php
<?php
$url = ""; // base uri
$option = [
    "http" => [
        'method' => 'GET',
        'timeout' => 5,
        'follow_location' => 1,
        'max_redirects' => 2
    ]
];

file_get_contents($url, false, stream_context_create($option));
```

#### HTTP クライアント 2 (curl)

```shell
curl -v -L --max-redirs 2 {base uri}
```

### 結果

HTTP クライアントの base uri を変更し確認した結果になります。

| sample num | base uri                      | target uri         | curl                              | file_get_contents()                |
| ---        | ---                           | ---                | ---                               | ---                                |
| 1          | `http://localhost:8080/hoge/` | `//localhost:8081` | `http://localhost:8081`           | `http://localhost:8081`            |
| 2          | `http://localhost:8080/hoge/` | `/fuga`            | `http://localhost:8080/fuga`      | `http://localhost:8080/fuga`       |
| 3          | `http://localhost:8080/hoge/` | `fuga`             | `http://localhost:8080/hoge/fuga` | `http://localhost:8080/hoge//fuga` |
| 4          | `http://localhost:8080/hoge`  | `fuga`             | `http://localhost:8080/fuga`      | `http://localhost:8080/fuga`       |

4パターンの中で、3番目だけ `file_get_contents()` の動きが期待結果と違うことがわかります。一方 curl では期待どおりに動作していました。

## 終わりに

URI参照の基本について一部だけまとめました。実際２種類のHTTPクライアントの動作を確認し、参照アルゴリズムをみました。検証結果は正しいとは限らないのですが、URI参照の雰囲気を感じられたらなと思います。

明日は[@imaisato](https://qiita.com/imaisato)さんの[災害は忘れたころにやって来る](https://qiita.com/imaisato/items/6e56e57295593ecf3dfe)です。お楽しみに！

## 参考文献

- https://tools.ietf.org/html/rfc3986