💭
【PHP】HTTP/2 フレームを解析する
nghttpd のレスポンスの HTTP/2 フレームを解析してみた。
解析をする上でそれぞれの HTTP/2 フレームの冒頭の位置を求めるイテレーターを定義する必要がある。この記事では h2frame_pos_iter
である。
ここで HTTP/2 フレームのデータ構造の復習。固定長9バイトのヘッダーと任意のサイズのペイロードから構成される。ヘッダーの最初の3バイトはペイロードのサイズをあらわす。get_payload_size
という関数を用意した。フレーム全体のサイズはヘッダーの9バイトとペイロードのサイズを足した値になる。ここでは get_h2frame_size
という関数を用意した。
サーバーからのレスポンスデータの取得を終わらせるかどうかの判断は HEADERS
(0x01
) もしくは DATA
(0x00
) フレームに END_STREAM
(0x01) フラグが含まれているかどうかである。この記事では get_end_stream_flag
という関数を用意した。
parser.php
<?php
$chunk = get_server_response();
$iter = h2frame_iter($chunk);
foreach ($iter as $index => $value) {
$h2frame = array_merge(
array_filter($value, fn($key) => $key !== 'payload', ARRAY_FILTER_USE_KEY),
['payload_hex' => bin2hex($value['payload'])]
);
var_dump(['index' => $index, 'h2frame' => $h2frame]);
}
function h2frame_iter($chunk) {
$iter = h2frame_pos_iter($chunk);
foreach ($iter as $key => $current) {
$size = get_h2frame_size($chunk, $current);
$name = get_h2frame_name($chunk, $current);
$payload_size = get_payload_size($chunk, $current);
$type = get_type($chunk, $current);
$flag = get_flag($chunk, $current);
$end_stream_flag = get_end_stream_flag($chunk, $current);
$stream_id = get_stream_id($chunk, $current);
$payload = get_payload($chunk, $current);
yield $key => [
'name' => $name, 'payload_size' => $payload_size,
'type' => $type, 'flag' => $flag,
'end_stream_flag' => $end_stream_flag,
'stream_id' => $stream_id, 'payload' => $payload
];
}
}
function h2frame_pos_iter($chunk) {
$chunk_size = strlen($chunk);
$index = 0;
$current = 0;
while (true) {
$size = get_h2frame_size($chunk, $current);
yield $index => $current;
++$index;
$current += $size;
if ($current >= $chunk_size) {
break;
}
}
}
function get_h2frame_name($str, $current) {
$type = get_type($str, $current);
$flag = get_flag($str, $current);
$names = [
0x00 => 'DATA',
0x01 => 'HEADERS',
0x04 => $flag === 0x01 ? 'ACK' : 'SETTINGS'
];
return $names[$type];
}
function get_h2frame_size($str, $current) {
if (strlen($str) <= $current) {
return 0;
}
return get_payload_size($str, $current) + 9;
}
function get_payload_size($str, $current) {
if (strlen($str) <= $current) {
return 0;
}
return hexdec(bin2hex(substr($str, $current, 3)));
}
function get_type($str, $current) {
return ord(substr($str, $current + 3, 1));
}
function get_flag($str, $current) {
return ord(substr($str, $current + 4, 1));
}
function get_end_stream_flag($str, $current) {
$type = get_type($str, $current);
$flag = get_flag($str, $current);
return $type === 0x00 && $flag === 0x01 ?: false;
}
function get_stream_id($str, $current) {
return hexdec(bin2hex(substr($str, $current +5, 4)));
}
function get_payload($str, $current) {
$size = get_payload_size($str, $current);
if ($size === 0) {
return "";
}
return substr($str, $current + 9, $size);
}
function get_server_response() {
$frames['settings'] = "\x00\x00\x06".
"\x04".
"\x00".
"\x00\x00\x00\x00".
"\x00\x03\x00\x00\x00\x64";
$frames['ack'] = "\x00\x00\x00".
"\x04".
"\x01".
"\x00\x00\x00\x00";
$frames['headers'] = "\x00\x00\x5c".
"\x01".
"\x04".
"\x00\x00\x00\x01".
"\x88\x76\x90\xaa\x69\xd2\x9a\xe4\x52\xa9\xa7".
"\x4a\x6b\x13\x01\x5d\xb1\x2e\x0f\x58\x89\xa4".
"\x7e\x56\x1c\xc5\x81\x97\x00\x0f\x61\x97\xdf".
"\x3d\xbf\x4a\x05\xe5\x32\xdb\x42\x82\x00\x9a".
"\x50\x22\xb8\xcb\xb7\x1b\x6d\x4c\x5a\x37\xff".
"\x0f\x0d\x01\x36\x6c\x96\xdc\x34\xfd\x28\x07".
"\x14\xcb\x6d\x0a\x08\x02\x69\x40\xbf\x70\x2f".
"\xdc\x6d\xb5\x31\x68\xdf\x5f\x87\x49\x7c\xa5".
"\x89\xd3\x4d\x1f";
$frames['data'] = "\x00\x00\x06".
"\x00".
"\x01".
"\x00\x00\x00\x01".
"\x48\x65\x6c\x6c\x6f\x0a";
return implode($frames);
}
Discussion