🍻

golangで作るHTTP2プロトコル

2022/05/16に公開

はじめに

前回まででTLS1.3+HTTPのプロトコルスタックの自作に成功しました。

自作したのはHTTP1.1です。皆さんご存知のように新しいVersionのHTTP2が普及されています。
今回はHTTP2プロトコルスタックを自作してみようと思います。

今回の方針です。

  • net/http2 は使わない
  • 自作したコードでリクエストをnginxに送りhtmlが返ってくればヨシ!

HTTP2でGETを送るgoのコードの処理を自作したということなので、HTTP2自体を全部作ってるわけではなく一部になります、ご承知おきください🙇‍♂️🙇‍♂️🙇‍♂️
またHTTP2自体の解説より実装中心の説明となるので、HTTP2自体を知りたい方は参考文献で適時補ってください。

参考文献

コード自体は以下にあります。お見苦しいコードですがよろしくお願いします。

https://github.com/sat0ken/go-tcpip

はじめてのHTTP2

クライアントサーバのコードを書いたら実行してパケットをキャプって観察します。

1.1と2の大きな違いはデータの送信です。
1.1はテキストベースでデータをやり取りしていました。

2ではデータ通信量を低減するため、データをバイナリに圧縮してやり取りすることになります。
その圧縮するための仕様が、RFC7541のHPACK: Header Compression for HTTP/2となります。
クライアントとサーバはこのHPACKの仕様に基づいて、データを圧縮、展開します。

例えば1.1でGETリクエストを送る時はHTTPヘッダで"GET"と文字をセットして送りますが、2ではRFC7541のAppendix A. Static Table Definitionに従ってIndex番号を送信します。
そうすることで文字を送るよりデータ量を減らしています。

文字自体もAppendix B. Huffman Codeに従って変換します。"A"という文字はHuffman Codeの変換ルールによって"100001"になります。
文字をバイナリに変換して送ることでデータ量を減らしています。

まずはこのAppendix A.のStatic TableとAppendix BのHuffman Codeを実装する必要があります。

次の違いはストリームの処理です。
HTTP2では1つのTCP接続上で複数のリクエストとレスポンスをやり取りできます。

例えばcss, js, htmlをサーバから1.1で取得すると3回HTTPのGETが送られその都度TCP接続が発生しますが、HTTP2では1つのTCP接続上でやり取りが行われます。
ブラウザでは1つのドメインに対して、最大6つまでしかリクエストを行えないHTTP1.1の制限を超える仕組みになりその分読み込みが早くなります。

この辺のお話、HTTP1.1の問題点→HTTP2による解決などについては詳解HTTP/2に書いてあるので、そちらをお読みください。わかりやすい神本です。

HPACKの実装

Appendix BのHuffman Codeの実装

まずHuffman Codeから実装してみます。

詳解HTTP/2のサンプルレポジトリにHuffman Codeの実装例のperlスクリプトがあります。

このスクリプトを実行すると以下のようになります。

$ ./hpack_huffman_encoding.pl DELETE
Huffman Encoding Flag + Length: 86
Huffman Encoding Value        : bf833e0df83f

perlスクリプトの処理内容は、引数で渡した文字を1文字ずつHuffman Codeのテーブルで定義されているbitに変換して最後に16進数でprintしています。
perlの処理をgoで書いてみます。

まず map を利用してHuffman Codeのテーブルを表現しておきます。
ASCIIコードの32から126までの文字をセットしておきます。

var HuffmanCodeTable = map[string]string{
	...
	"A":   "100001",
	"B":   "1011101",
	"C":   "1011110",
	"D":   "1011111",
	...
}

引数で渡した文字列を1文字ずつテーブルを検索したら、ヒットした2進数の文字列を結合していって最後に16進数にして返します。

func HuffmanEncode(str string) string {
	split := strings.Split(str, "")

	var encstr string
	// 1文字ずつに分解した文字をハフマン符号化テーブルを参照して検索
	// 検索してヒットした結果を変数に追加する
	for _, v := range split {
		encstr += HuffmanCodeTable[v]
	}
	// バイナリ文字列をパディングして8=オクテットで割り切れるようにする
	// 割り切れなかったら末尾に1を入れて埋める
	for {
		if len(encstr)%8 != 0 {
			encstr += "1"
		} else {
			break
		}
	}

	var result string
	for i, _ := range encstr {
		// 各4bit値を繰り返し処理して、16進数に変換
		if i%4 == 0 {
			bin, _ := strconv.ParseUint(encstr[i:i+4], 2, 4)
			result += fmt.Sprintf("%x", bin)
		}
	}

	return result
}

これを呼んで実行してみます。
同じ出力が得られました。

$ go run huffman.go 
Huffman Encoding Value : bf833e0df83f

文字→16進数のエンコード処理が出来ましたが、16進数→文字のデコード処理も実装する必要があります。
エンコード処理の流れを逆にして、16進数→2進数→テーブルを検索→文字列という流れで実装しました。

16進数→2進数にしたら、2進数5文字でテーブルを検索、ヒットしなければ6文字で検索...という処理を行いデコードしています。

var bitLength = []int{5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 19}

func HuffmanDecode(hpackBytes []byte) string {
	var binstr string
	// bitフォーマットのstringにする
	for _, v := range hpackBytes {
		binstr += fmt.Sprintf("%08b", v)
	}

	var decstr string
	for {
		for _, v := range bitLength {
			// 残り文字数より多いbitはskipする
			if len(binstr) < v {
				continue
			}
			result := getHuffmanTable(binstr[0:v])
			if result != "" {
				binstr = binstr[v:]
				decstr += result
				//fmt.Printf("remain str is %s, length is %d\n", encstr, len(encstr))
			}
		}
		if len(binstr) == 0 {
			break
		} else if !strings.Contains(binstr, "0") {
			// 残りの文字が全部1なら全部Paddingだからbreak
			break
		}
	}
	return decstr
}

func getHuffmanTable(str string) (hit string) {
	for k, v := range HuffmanCodeTable {
		if v == str {
			//fmt.Printf("key is %s, value is %s\n", k, v)
			hit = k
		}
	}
	return hit
}

これを実行してみます。
デコードが出来ました。

$ go run huffman.go 
Huffman Encoding Value : bf833e0df83f
Huffman Decoding Value : DELETE

Appendix A.のStatic Table

Huffman codeが実装できたので次にStatic Tableを実装します。
Static Tableは一般的なHTTPヘッダをIndex番号で定義したもので、0~61番まで定義されています。
ユーザ独自の値を使用する時は、62番以降にDynnamic tableとして値を追加します。

ここでは61番までの値を配列で持つようにしました。
ヘッダの生成、読み取り時にIndex番号で値を取得します。

type Http2Header struct {
	Name  string
	Value string
}

var StaticHttp2Table = []Http2Header{
	{Name: ":authority"},
	{Name: ":method", Value: "GET"},
	{Name: ":method", Value: "POST"},
	{Name: ":path", Value: "/"},
	{Name: ":path", Value: "/index.html"},
	{Name: ":scheme", Value: "http"},
	{Name: ":scheme", Value: "https"},
	{Name: ":status", Value: "200"},
	{Name: ":status", Value: "204"},
	{Name: ":status", Value: "206"},
	{Name: ":status", Value: "304"},
	{Name: ":status", Value: "400"},
	{Name: ":status", Value: "404"},
	{Name: ":status", Value: "500"},
	{Name: "accept-charset"},
	{Name: "accept-encoding", Value: "gzip, deflate"},
	{Name: "accept-language"},
	{Name: "accept-ranges"},
	{Name: "accept"},
	{Name: "access-control-allow-origin"},
	{Name: "age"},
	{Name: "allow"},
	{Name: "authorization"},
	{Name: "cache-control"},
	{Name: "content-disposition"},
	{Name: "content-encoding"},
	{Name: "content-language"},
	{Name: "content-length"},
	{Name: "content-location"},
	{Name: "content-range"},
	{Name: "content-type"},
	{Name: "cookie"},
	{Name: "date"},
	{Name: "etag"},
	{Name: "except"},
	{Name: "expires"},
	{Name: "from"},
	{Name: "host"},
	{Name: "if-match"},
	{Name: "if-modified-since"},
	{Name: "if-none-match"},
	{Name: "if-range"},
	{Name: "if-unmodified-since"},
	{Name: "last-modified"},
	{Name: "link"},
	{Name: "location"},
	{Name: "max-forwards"},
	{Name: "proxy-authenticate"},
	{Name: "proxy-authorization"},
	{Name: "range"},
	{Name: "referer"},
	{Name: "refresh"},
	{Name: "retry-after"},
	{Name: "server"},
	{Name: "set-cookie"},
	{Name: "strict-transport-security"},
	{Name: "transfer-encoding"},
	{Name: "user-agent"},
	{Name: "vary"},
	{Name: "via"},
	{Name: "www-authenticate"},
}

ここまででHPACKの実装を行いました。

TLS ClientHelloメッセージの修正

TLSのClientHelloメッセージでHTTP2を使うことを知らせる必要があります。
ALPN(Application Layer Protocol Negotiation)をTLS Extensionsにセットするように修正します。

単にhttp2を使う時はキャプチャしたbyteデータを追加で差し込むだけです。

3.1. HTTP / 2バージョンの識別より h2 を意味する0x68, 0x32をセットしてます。

// renagotiation_info
tlsExtension = append(tlsExtension, []byte{0xff, 0x01, 0x00, 0x01, 0x00}...)

if http2 {
	tlsExtension = append(tlsExtension, []byte{0x00, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32}...)
}

HTTP2リクエストの送信

前回までで自作TLS1.3+HTTP1.1でやり取りができていました。
TLSハンドシェイクが完了した後、HTTP1.1の箇所をHTTP2に置き換えます。

全部のクライアントコードはこちらです。

まず最初にサーバに送信するメッセージ、ストリーム番号=0のデータを作成します。

	// HTTP2 リクエストを作成する
	// Magic, Settings, Window_update
	appData := tcpip.CreateFirstFrametoServer()
	appData = append(appData, tcpip.ContentTypeApplicationData)
	encAppData := tcpip.EncryptChacha20(appData, tlsinfo)

prefaceのメッセージ、ストリームの設定値、Window_Updateメッセージを送っています。
キャプチャしたgoクライアントのパケットと同じもの作成して送ります。

最初のprefaceのメッセージというのは、これからの通信はHTTP2であることを示すためのものです。
3.5. HTTP/2 Connection Prefaceに記載されている以下の文字列です。

"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

これをサーバに送った時、HTTP2に対応していないサーバはエラーになりますので、HTTP2でのやり取りは続行不可能となります。

次のストリームの設定値では、サーバプッシュの許可、初期Windowサイズ、最大ヘッダサイズをセットしておくります。
最後にWindow_Updateメッセージを付け足します。

処理を関数化しました。
preface, frame, updateのメッセージを作成したらbyteにして返します。

func CreateFirstFrametoServer() []byte {
	var packet []byte

	preface := createConnectionPreface()
	frame := createSettings()
	update := Http2Frame{
		Length: UintTo3byte(4),
		Type:   []byte{FrameTypeWindowUpdate},
		Flags:  []byte{0x00},
		// 最初なのでストリーム番号は0
		StreamIdentifier: []byte{0x00, 0x00, 0x00, 0x00},
		// Windows Size Increment
		Value: []byte{0x40, 0x00, 0x00, 0x00},
	}

	// パケットデータにする
	packet = append(packet, preface...)
	packet = append(packet, toByteArr(frame)...)
	packet = append(packet, toByteArr(update)...)

	return packet
}

パケットを作成したら暗号化した後、ソケットにWriteしてサーバに送ります。

	appData := tcpip.CreateFirstFrametoServer()
	appData = append(appData, tcpip.ContentTypeApplicationData)
	encAppData := tcpip.EncryptChacha20(appData, tlsinfo)

	// h2リクエストを送る
	syscall.Write(sock, encAppData)
	tlsinfo.ClientAppSeq++

次にHTTPヘッダのフレームを作成してサーバに送ります。

	// Header Frameを作成して送る
	headerFrame := tcpip.CreateHeaderFrame()
	headerFrame = append(headerFrame, tcpip.ContentTypeApplicationData)
	encHeaderFrame := tcpip.EncryptChacha20(headerFrame, tlsinfo)

ヘッダフレームを作成する関数です。
CreateHttp2Headerに値を渡して、必要なヘッダを作成していき最後に値としてフレームにセットします。
ヘッダの値は引数で渡さないとダメでしょうが後で実装しましょう(・ω<) てへぺろ

func CreateHeaderFrame() []byte {
	var headers []byte

	headers = append(headers, CreateHttp2Header(":authority", "127.0.0.1:18443")...)
	headers = append(headers, CreateHttp2Header("", "GET")...)
	headers = append(headers, CreateHttp2Header("", "/")...)
	headers = append(headers, CreateHttp2Header("", "https")...)
	headers = append(headers, CreateHttp2Header("accept-encoding", "gzip")...)
	headers = append(headers, CreateHttp2Header("user-agent", "Go-http-client/2.0")...)

	headerFrame := Http2Frame{
		Length: UintTo3byte(uint32(len(headers))),
		Type:   []byte{FrameTypeHeaders},
		// End HeadersとStreamがTrueなので5をセット
		Flags: []byte{0x05},
		// ストリーム番号は1になる
		StreamIdentifier: []byte{0x00, 0x00, 0x00, 0x01},
		// ヘッダを値としてセット
		Value: headers,
	}

	return toByteArr(headerFrame)
}

CreateHttp2Headerの中身です。
RFC7541、HPACKの仕様によってヘッダの形式は以下があります

  • 6.1. インデックス付きヘッダーフィールドの表現
  • 6.2.1. インクリメンタルインデックスを使用したリテラルヘッダーフィールド
  • 6.2.2. インデックスなしのリテラルヘッダーフィールド
  • 6.2.3. リテラルヘッダーフィールドがインデックス付けされない

・6.1. インデックス付きヘッダーフィールドの表現

これは Static Table Definition のIndex番号のみで表現できるものです。
例えば GET はIndex番号の2, https はIndex番号の7という番号だけで表現できます。

なので先頭の 1 + Index番号を7ケタの2進数にした8byteを16進数にしたら終了です。

・6.2.1. インクリメンタルインデックスを使用したリテラルヘッダーフィールド

これはIndex番号のヘッダと値のセットで表現が必要となるものです。

:authority 127.0.0.1:18443 はHTTP1.1でいうHostヘッダになりますが、これを表現するためにはIndex番号の1と
127.0.0.1:18443 をHuffmanエンコードした値が必要になります。

なのでRFCに従い、
 1byte目に 01 + Index番号
 2byte目に Huffmanエンコードされてる文字が続くことを示す 1 + エンコードした文字列の長さ
 3byte目〜 エンコードした文字列
でヘッダを作成します。

関数は以下のようになりました。
今は必要だった6.1と6.2.1しか実装していません。

func CreateHttp2Header(name, value string) (headerByte []byte) {

	if name == "" {
		// インデックスヘッダフィールド表現(1で始まる)
		//  0   1   2   3   4   5   6   7
		// +---+---+---+---+---+---+---+---+
		// | 1 |        Index (7+)         |
		// +---+---------------------------+
		
		index := getHttp2HeaderIndexByValue(value)
		headerIndex, _ := strconv.ParseUint(fmt.Sprintf("1%07b", index+1), 2, 8)
		headerByte = append(headerByte, byte(headerIndex))
	} else {
		// インデックス更新を伴うリテラルヘッダフィールド(01で始まる)
		//  0   1   2   3   4   5   6   7
		// +---+---+---+---+---+---+---+---+
		// | 0 | 1 |      Index (6+)       |
		// +---+---+-----------------------+
		// | H |     Value Length (7+)     |
		// +---+---------------------------+
		// | Value String (Length octets)  |
		// +-------------------------------+

		index := getHttp2HeaderIndexByName(name)
		headerIndex, _ := strconv.ParseUint(fmt.Sprintf("01%06b", index+1), 2, 8)

		// Huffman codingを意味する1のbitとcodingされたLengthを意味する7bit
		encodeVal := HuffmanEncode(value)
		headerVal, _ := strconv.ParseUint(fmt.Sprintf("1%07b", len(encodeVal)), 2, 8)

		headerByte = append(headerByte, byte(headerIndex))
		headerByte = append(headerByte, byte(headerVal))
		headerByte = append(headerByte, encodeVal...)

	}

	return headerByte
}

このようにヘッダフレームを作ったら暗号化して、ソケットにWriteします。

HTTP2リクエストの受信

ヘッダフレームまでをサーバに送ったらレスポンスが返ってくるので処理します。
TLS1.3で暗号化されているので、HTTP2のパケットに復号したらパース処理します。

受信して復号するところまでは、前回のHTTP1.1と同じなので説明は省略します。

frame := tcpip.ParseHttp2Packet(plaintext[0 : len(plaintext)-1])

ParseHttp2PacketはparseHTTP2Frameを呼んで各HTTP2のフレームにパースして返します。

func parseHTTP2Frame(packet []byte) ParsedHttp2Frame {
	var frame ParsedHttp2Frame

	frameType := packet[3:4]
	//flags := packet[4:5]
	si := packet[5:9]

	switch int(frameType[0]) {
	case FrameTypeSettings:
		frame = ParsedHttp2Frame{
			Type:  FrameTypeSettings,
			Frame: getServerSettings(packet[9:]),
		}
	case FrameTypeWindowUpdate:
		updateFrame := WindowsUpdateFrame{
			StreamIdentifier:     si,
			WindowsSizeIncrement: packet[9:],
		}

		frame = ParsedHttp2Frame{
			Type:  FrameTypeWindowUpdate,
			Frame: updateFrame,
		}

		fmt.Printf("FrameTypeWindowUpdate : %+v\n", updateFrame)
	case FrameTypeHeaders:
		headers := DecodeHttp2Header(packet[9:])
		frame = ParsedHttp2Frame{
			Type:  FrameTypeHeaders,
			Frame: headers,
		}
		fmt.Printf("FrameTypeHeaders : %+v\n", headers)
	case FrameTypeData:
		frame = ParsedHttp2Frame{
			Type:  FrameTypeData,
			Frame: packet[9:],
		}
		fmt.Printf("FrameTypeData : %s\n", packet[9:])
	}

	return frame
}

func ParseHttp2Packet(packet []byte) (http2Frames []ParsedHttp2Frame) {
	// Lengthが0, Flagsが1, ACKS=trueだったらSkipする
	if bytes.Equal(packet[0:3], []byte{0x00, 0x00, 0x00}) {
		fmt.Println("Recv ACK for Settings")
		packet = packet[9:]
	}

	totalLen := len(packet)

	for i := 0; i < len(packet); i++ {
		length := sum3BytetoLength(packet[i : i+3])
		frame := parseHTTP2Frame(packet[i : i+int(length)+9])
		http2Frames = append(http2Frames, frame)
		// Frameが続いてるならiをインクリメントして次のFrameに進める
		if i+int(length)+9 < totalLen {
			i += (int(length) + 9) - 1
		} else {
			break
		}
	}

	return http2Frames
}

frameをパースしたら、ヘッダフレームからHttpステータスを取り出し、データフレームからnginxのhtmlをprintしたら処理を抜けます。

frame := tcpip.ParseHttp2Packet(plaintext[0 : len(plaintext)-1])
for _, v := range frame {
	if v.Type == tcpip.FrameTypeHeaders {
		for _, v := range v.Frame.([]tcpip.Http2Header) {
			if v.Name == ":status" {
				fmt.Printf("Http status code is %s\n", v.Value)
			}
		}
	} else if v.Type == tcpip.FrameTypeData {
		fmt.Printf("Data is %s\n", v.Frame.([]byte))
		break exit_loop2
	}
}

ここまで書いたら実行してみます。

$ go run http2-client.go 
ServerHello : {HandshakeType:[2] Length:[0 0 118] Version:[3 3] Random:[222 182 106 72 116 180 102 174 20 126 73 127 192 99 92 245 145 243 208 191 82 182 217 174 130 13 47 204 30 53 24 27] SessionIDLength:[32] SessionID:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] CipherSuites:[19 3] CompressionMethod:[0] ExtensionLength:[0 46] TLSExtensions:[{Type:[0 43] Length:[0 2] Value:[3 4]} {Type:[0 51] Length:[0 36] Value:map[Group:[0 29] KeyExchange:[99 183 136 71 127 167 144 233 8 228 95 196 125 160 100 49 254 234 230 199 253 219 253 248 46 38 125 68 64 145 61 30] KeyExchangeLength:[0 32]]}]}
server key share is 63b788477fa790e908e45fc47da06431feeae6c7fddbfdf82e267d4440913d1e
sharedkey is dc145d1263944b4a7c1dff76aa4197a4a8672a5f95c846cc433ca14988c0de57
derivedSecretForhs 6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba
handshake_secret is : f51a080ec9d805b57c69e70a8009529563aaa8dd08d468c08b5426f2a10a11b9
hashed messages is d8b43a8b64493f08f549e10976717911ae0fca8b646628d23a4f45be098b4dd8
CLIENT_HANDSHAKE_TRAFFIC_SECRET 0000000000000000000000000000000000000000000000000000000000000000 41cce63d1219cf6027f0abae3c1119c7996491e8a8b7882659c70f03bd3d1604
SERVER_HANDSHAKE_TRAFFIC_SECRET 0000000000000000000000000000000000000000000000000000000000000000 9607823423916ef3eb170b56ff00bae06b4d08459196769f618f8deaba47d97f
serverfinkey is : 6d9641006d2082015d0bb10849171b246a2c10614d53f3e1b61db050bbc3b1e0
derivedSecretFormaster is : 06a8fccc55838de5dafa7395324020aeb8f1e24a7439edcbda2f0cde5ffb6b67
extractSecretMaster is : 1087362fb3c9fbf93083d9b10f6dec88d38acd4984949c06f36aa9e2621a13ed
client traffic key is : d1fd950b08ee4e646e67e31f8f66fdc85de6b1f5c4543c18a1a3a30efc73c1e4
client traffic iv is : d9917dbc63dde4fda72497f5
server traffic key is : 1648d2ef5f822027bba39eb48c3c15b299ba2c35835b41f160c81e879cf65908
server traffic iv is : 3316c199f0a31ef4a5d19693
read ChangeCipherSpec is 140303000101, これから暗号化するんやでー
EncryptedExtensions : {HandshakeType:[8] Length:[0 0 11] ExtensionLength:[0 9] TLSExtensions:[]}
証明書マジ正しい!
Certificate : {HandshakeType:[11] Length:[0 4 36] CertificatesRequestContextLength:[0] CertificatesLength:[0 4 32] Certificates:[0xc000520100]}
CertificateVerify : {HandshakeType:[15] Length:[0 1 4] SignatureHashAlgorithms:[8 4] SignatureLength:[1 0] Signature:[67 112 182 205 10 78 156 70 148 31 91 216 87 199 83 82 117 84 251 161 43 188 230 145 108 218 177 127 249 247 173 137 213 228 255 67 67 247 190 41 116 41 195 242 34 128 98 181 188 225 111 85 217 186 134 85 16 69 240 196 174 203 30 119 26 56 197 57 183 206 202 184 125 240 98 156 152 31 228 18 160 173 195 91 179 16 17 6 111 126 62 60 237 134 124 229 159 85 41 235 225 144 236 89 44 25 41 224 161 85 122 167 126 21 18 129 240 58 50 0 173 73 139 84 22 45 27 239 212 108 159 141 41 218 41 176 73 44 18 98 89 238 167 84 44 244 5 141 208 100 165 147 102 155 178 194 71 122 186 133 103 239 18 107 220 186 91 132 177 222 11 27 60 226 38 53 125 115 112 156 130 84 243 158 126 223 218 3 74 226 120 6 58 217 97 137 117 233 205 242 188 175 91 107 32 181 13 26 234 227 231 114 176 189 237 189 231 145 230 248 51 75 195 9 72 76 201 152 123 81 241 68 33 100 233 107 61 167 214 138 6 136 228 53 23 121 233 120 236 91 116 137 24 198 33 39]}
hash_messages is 6c366427b957e7ff069e3c11689ed929126da6d810259008760ba0b73498d0c2, signed is ba4cf0dd6a9d9f52a7f2ff614351193cc0254bddd093c1641f07074be41d37b3
Server Certificate Verify is OK !!
FinishedMessage : {HandshakeType:[20] Length:[0 0 32] VerifyData:[169 167 115 168 59 87 139 199 33 231 215 155 203 13 196 175 151 35 183 54 235 71 224 155 184 235 226 198 239 242 57 75]}
Server Verify data is correct !!
hashed messages is 46367895f7a1d6c76e97c3db86c77fd04aa0b805293b54c842c51dbff78aa79e
CLIENT_TRAFFIC_SECRET_0 0000000000000000000000000000000000000000000000000000000000000000 377bcf5bae41851091e7613861b66e972025cd51e9b3e663562336386232f7d3
SERVER_TRAFFIC_SECRET_0 0000000000000000000000000000000000000000000000000000000000000000 14b269f22b4d570ad1b612f5b3437014ad23af43add659cf162d5de885f5c766
clientAppKey and IV is : 72f5e29737d7885d069b3e57c2038b486522295c38d132ec384aff43e339e03f, 1a97512c311507dbde2aac7d
serverAppkey and IV is : 7c93576e1a812fad426e5435ad80609afe10a114c1cc32bc15f5906c8644fbe0, ade2eeaaf7e2de35cb771291
fin message 1400002067bb0561d6b9ab446f552595b53c3650e7d1f1534621ecf5dc036071e68e14dd16
key is d1fd950b08ee4e646e67e31f8f66fdc85de6b1f5c4543c18a1a3a30efc73c1e4, iv is d9917dbc63dde4fda72497f5
encrypt now nonce is 0000000000000000 xornonce is d9917dbc63dde4fda72497f5, plaintext is 1400002067bb0561d6b9ab446f552595b53c3650e7d1f1534621ecf5dc036071e68e14dd16, add is 1703030035
fin message 1703030035cb6716eaae9e4259baabb97b7a19a7e985126998c713250e64d0824f284e8e597f830db8fbf2b64e7c0dda525eab9a09a1b2c2d01e
send finished message
key is 72f5e29737d7885d069b3e57c2038b486522295c38d132ec384aff43e339e03f, iv is 1a97512c311507dbde2aac7d
encrypt now nonce is 0000000000000000 xornonce is 1a97512c311507dbde2aac7d, plaintext is 505249202a20485454502f322e300d0a0d0a534d0d0a0d0a000012040000000000000200000000000400400000000600a000000000040800000000004000000017, add is 1703030051
key is 72f5e29737d7885d069b3e57c2038b486522295c38d132ec384aff43e339e03f, iv is 1a97512c311507dbde2aac7d
encrypt now nonce is 0000000000000001 xornonce is 1a97512c311507dbde2aac7c, plaintext is 000024010500000001418b089d5c0b8170dc0bcd34cf82848750839bd9ab7a8dc475a74a6b589418b525812e0f17, add is 170303003e
send Http2 Magic frame and Header frame
FrameTypeSettings is [{SettingsIdentifier:[3] Value:[0 0 0 128]} {SettingsIdentifier:[4] Value:[0 1 0 0]} {SettingsIdentifier:[4] Value:[0 255 255 255]}]
FrameTypeWindowUpdate : {StreamIdentifier:[0 0 0 0] WindowsSizeIncrement:[127 255 0 0]}
FrameTypeSettings is []
FrameTypeHeaders : [{Name::status Value:200} {Name:server Value:nginx/1.21.6} {Name:date Value:Mon, 16 May 2022 08:27:45 GMT} {Name:content-type Value:text/html} {Name:last-modified Value:Tue, 25 Jan 2022 15:03:52 GMT} {Name:etag Value:"61f01158-267"} {Name:accept-ranges Value:bytes}]
Http status code is 200
FrameTypeData : <!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Data is <!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Http2 Connection is close...
key is 72f5e29737d7885d069b3e57c2038b486522295c38d132ec384aff43e339e03f, iv is 1a97512c311507dbde2aac7d
encrypt now nonce is 0000000000000002 xornonce is 1a97512c311507dbde2aac7f, plaintext is 010015, add is 1703030013
send close notify

オーイエス、HTTP2でちゃんと返ってきました。

おわりに

というわけで、HTTP2のGetの処理を自作してみました。

読んで頂けた方はお分かりのように、HTTPヘッダの動的テーブル、サーバプッシュ、プライオリティ制御などは実装されてないですし、
僕自身もまだよくわかっていないので、引き続きRFCと詳解HTTP/2を読んで学んでいこうと思います。

今回はHTTP1.1と2の違いがちょっとわかったのでヨシ!

Discussion