💭

【PHP】HTTP/2 フレームを解析する

2024/07/19に公開

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