HTTP/2: ちょっと詳細: プロトコルネゴシエーション編

# この記事について

今さら、個人的に HTTP/2 の少し詳細をまとめたもの。内容は HTTP/2 を開始するためのプロトコルネゴシエーションについて。

# HTTPS(h2) と HTTP(h2c)

HTTP/2 には HTTP/1.x と同様に、TLS を使う HTTPS と、使わない HTTP がある。

- `HTTPS (HTTP/2 over TLS)`
  - TLS を用いた暗号化通信を行う
  - 識別子は h2
- `HTTP (HTTP/2 over cleartext TCP)`
  - TLSを用いないで平文(Cleartext)で通信を行う
  - 識別子は h2c

一般的なブラウザは h2 のみに対応しているため、実質的に h2 が必須となっている。また、h2 で用いる TLS には以下の要件がある。

- TLS のバージョンは 1.2 以上
- SNI 拡張の対応が必要
  - クライアントは TLS のネゴシエーションの際に、対象のドメイン名を示す必要がある
- TLS 1.2 の場合は暗号化スイートに以下の要件あり
  - [RFC 7540 のブラックリスト](https://tools.ietf.org/html/rfc7540#appendix-A)に記載された暗号化スイートを使うべきではない
  - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 への対応が必須

# HTTP/2 での通信開始までの流れ

クライアントは最初に通信先のサーバーまたはプロキシが HTTP/2 をサポートしているか知る(もしくは事前に知っている)必要がある。そのための方法は h2 と h2c で異なり、次の3パターンに分類できる。

- h2c で、通信相手が HTTP/2 対応かどうかを事前に知らない場合
- h2c で、通信相手が HTTP/2 対応済みなのを事前に知っている場合
- h2 の場合

最初に HTTP/1.1 の機構を用いる h2c から説明する。

## h2c : HTTP/2 対応かどうか、事前に知らない場合

クライアントは、通信相手が HTTP/2 に対応しているかを事前に知らない場合、 HTTP/1.1 の Upgrade の機構を用いる。Upgrade とは接続済みのコネクション上で、HTTP/1.1 から別のプロトコルに変更をする仕組み(例えば、WebSocket を使う場合も利用される)。

HTTP/2 への Upgrade を行うために、クライアントは次のヘッダーを送る必要がある。

- Upgrade ヘッダーに `h2c` を指定
- HTTP2-Settings ヘッダーを含める
- Connection ヘッダーに `HTTP2-Settings` を含める  
  ※ これは、Upgrade は次のホップにのみ適用することを意図してるので、プロキシなどでヘッダーを転送させたくないため。

例えば、このようなリクエストになる。

```
GET / HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <SETTINGS フレームのペイロードを base64url エンコードした文字列>
```

Upgrade を受け取った HTTP/2 対応のサーバーは、101 (Switching Protocols) のレスポンスを返す。その後、HTTP/2 のフレーム送信が可能になる。

```
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
```

HTTP/2 に対応してないサーバーは、Upgrade ヘッダーが無いかのごとく HTTP/1.1 でレスポンスを返す。

```
HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html
```

参考までに Upgrade の実例を添付。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/1f37f4d4-7013-912e-0e11-de049859e7d4.png)


## h2c : HTTP/2 対応済みなのを、事前に知っている場合

クライアントが、接続先サーバーが HTTP/2 対応済みであることを知ってる場合は事前知識(Prior Knowledge)があると言う。

その場合、クライアントはサーバーへ TCP コネクションを確立するとコネクションプリフェイスと呼ばれるマジックワードを送信し、それから HTTP/2 のフレーム送受信が開始される。

コネクションプリフェイスは `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` という文字列である。以下、コネクション確立後にコネクションプリフェイスを送信している実例を添付。
![prior-knowledge.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/81f86065-bb1b-a629-6624-04ab8e87027a.png)

なお、curl コマンドには `--http2-prior-knowledge` というオプションがあるが、それはこのパターンのこと。


## h2

h2 の場合は TLS の拡張である ALPN(Application-Layer Protocol Negotiation)を用いて、HTTP のどのバージョンを使うかを決定する。

ALPN は TLS ハンドシェイク中にアプリケーションレベルで用いるプロトコルをネゴシエーションする TLS の拡張で、h2 でのプロトコルネゴシエーションのために追加された(ただし、任意のプロトコルで利用可能)。

具体的には、次のようにバージョンを決定する。

1. クライアントは、自身が対応している HTTP バージョンのリストを TLS の ClientHello に含めて送る
2. サーバーは、その中から1つのバージョンを選択して TLS の ServerHello に含めて返す

以下に、ClientHello と ServerHello の実例を添付する。

**[クライアントが ClientHello の ALPN 拡張で、 h2 と http/1.1 を提示しているところ]**
![alpn_clienthello.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/b3007374-c440-220f-9242-d27a1e02aaca.png)

**[サーバーが ServerHello の ALPN 拡張で、選択した h2 を返したところ]**
![alpn_serverhello.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/fa9ad6f6-45b6-f7b9-a305-a2d73408149f.png)

ちなみに、ALPN が TLS に追加された経緯や他のプロトコルでも使えることは、RFC 7301 に以下のように書かれている。

[Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension](https://tools.ietf.org/html/rfc7301)
> This document specifies a TLS extension that permits the application layer to negotiate protocol selection within the TLS handshake. This work was requested by the HTTPbis WG to address the negotiation of HTTP/2 ([HTTP2]) over TLS; however, ALPN facilitates negotiation of arbitrary application-layer protocols.