CONNECT の後の空行を待つ
ステータス行が先に届いても、ソケットを TLS に渡す前に HTTP/1 CONNECT の応答全体を読み切ること。
クライアントは 200 Connection Established を見ると TLS を開始し、TLS レコードがあるべき場所でたちまち意味不明な内容を報告する。宛先の暗号設定を疑う前に、読み終えていない HTTP 応答がないか探そう。
HTTP/1 CONNECT のやり取りでは、ステータス行は応答の始まりにすぎない。その後ろにヘッダーフィールドが続くことがある。CONNECT 応答が成功した場合、ヘッダー部を終える空行がトンネルへの切り替えを示す。
HTTP/1.1 200 Connection Established\r\n
Proxy-Agent: example\r\n
\r\n
これらのバイトは 1 回の読み取りで来ることも、複数回に分かれて来ることも、後続のバイトまで含んだ読み取りで来ることもある。TCP はサーバーの書き込み境界を保たない。
受け渡しを明示的なパーサー状態にする
- ステータス行を集めて解析する。厳密な英語の理由句ではなく、数値のステータスを使う。
- ヘッダー部の終端が完全に届くまで読み進める。ステータス行が成功していても、残りのフィールドが消えるわけではない。
- CONNECT 応答が成功したら、その終端の直後のバイトをトンネルデータの先頭とみなす。それ以外の応答では、対応する HTTP 応答と認証の扱いに従う。
- 接続と未消費のバイトを、次のプロトコルが対応するバッファ付き I/O インターフェース経由で引き渡す。
区切り文字の検索がうまくいっていても、最後の手順は重要だ。バッファ付きリーダーはパーサーが要求した以上にソケットを消費していることがある。そのリーダーを捨てたまま生のソケットで TLS を始めると、バイトがリーダーの内部バッファに取り残される。
セットアップの期限とヘッダーサイズの上限を設ける。終端の前に来る EOF は不完全なプロキシ応答であり、永遠に待つ理由にも、とにかく TLS を試す理由にもならない。
いくつかの境界を考える
Proxy-Agent: の直後で読み取りが終わる場合を考えよう。パーサーにはまだ HTTP ヘッダーのバイトが足りない。最後の CRLF 列の途中で終わる場合も考えよう。それでもヘッダー部は完成していない。さらに、完全な成功ヘッダー部の直後に同じバッファーでトンネルバイトが続く場合を考えよう。その余分なバイトは受け渡しを生き延びなければならない。
407 は別の結末だ。認証の処理が必要であり、一度も開いていないトンネルに TLS ClientHello を送ることを許しはしない。
ハンドシェイクの実装自体が課題でない限り、保守されているコネクターを選ぼう。この境界はここで示した HTTP/1 のやり取りに固有のものだ。HTTP/2 プロキシは異なるフレーミングのストリームで CONNECT を運ぶ。