HTTPリクエストの解釈の不一致
中継前後で同じ対象と本文を使うための検査条件を説明します。
説明
中継装置とアプリで同じHTTP要求を異なる形に解釈すると、宛先や要求の区切りがずれる場合があります。重複したHostや、本文長を表す複数の情報を曖昧なまま転送しないようにします。
固定のHTTP/1.1バイト列を標準ヘッダーパーサーで読み、最初・最後の値を取る処理の違いを比較します。次にこのAPIの限定的な受入契約を適用し、Hostと本文の長さが一つに定まる要求だけを通します。
中継前後で同じ対象と本文を使うための検査条件を説明します。
1要求行・ヘッダー・本文を分解する
本文item=penは8バイトです。
from sd11 import *
wire = request(["Host: api.example.test","Content-Length: 8"])
method,target,version,headers,body = parse(wire)生の要求を構成要素へ分解する
print(method,target,version)
print(list(headers.items()),body,len(body))
print(strict(wire)限定した受入契約へ照合する)POST /orders、単一Host、長さ8の本文へ分かれます。受入後も同じHostと本文を渡します。
2重複Hostで値の選び方が変わる様子を見る
同じ名前のヘッダーが二つある試料を作ります。
duplicate = request(["Host: api.example.test","Host: other.example.test","Content-Length: 8"])
values = parse(duplicate)[3].get_all("Host")重複値を捨てずに取得する
print(values[0],values[-1]値の選択方法による違いを示す)
print(strict(duplicate))最初と最後で宛先が違い、受入器はhost_countで拒否します。中継とアプリが別の値を選ぶ余地を残しません。
3Hostと要求ターゲットの受入形式を固定する
このAPIはorigin-formのパスだけを受け付けます。別Hostと絶対URL形式を比較します。
print(strict(request(["Host: other.example.test"宛先Hostを変える,"Content-Length: 8"])))
print(strict(request(["Host: api.example.test","Content-Length: 8"],target="http://other.example.test/orders"絶対URL形式のターゲットを渡す)))host_rejectedとtarget_rejectedです。汎用forward proxyの全形式を扱う例ではなく、固定APIの受入形式を限定しています。
4重複Content-Lengthを拒否する
8と3を同じ要求へ入れます。
conflict = request(["Host: api.example.test","Content-Length: 8","Content-Length: 3"])
lengths = parse(conflict)[3].get_all("Content-Length")本文長の候補をすべて取得する
print(lengths)
print(body[:int(lengths[0])],body[:int(lengths[-1])]異なる境界で区切った本文を比較する)
print(strict(conflict))最初の値を採用するとitem=pen、最後の値を採用するとiteになります。ここではlength_rejectedとして拒否します。同じ値の重複も拒否する仕様です。
5Transfer-Encodingとの混在を拒否する
このAPIは固定長だけを扱い、Transfer-Encodingを受け付けません。
mixed = request(["Host: api.example.test","Content-Length: 8","Transfer-Encoding: chunked"本文の別の区切り方を同時に指定する])
print(strict(mixed))transfer_encoding_rejectedです。HTTP全般でchunkedを禁止する規則ではなく、この受入経路の契約です。両方を受け付ける中継ではRFCに沿った処理と下流への正規化を揃えます。
6不足・余剰・上限超過の本文を比較する
固定長と実バイト列の不一致を調べます。
for length in (7,9,17)実本文と異なる申告長を与える:
print(length,strict(request(["Host: api.example.test",f"Content-Length: {length}"])))7と9はbody_length_mismatch、17はbody_limitです。この試料は受信済みバイト列の照合です。ストリーム受信器では長さに応じて読み、余剰を次要求とするか切断するかも統一します。
7受入後の値から下流用要求を再構成する
正常要求の受入結果を使い、重複しない下流向け要求を組み立てます。
accepted = strict(wire)
method,target,host,text = accepted
forwarded = request受入済みの値から単一ヘッダーを再構成する([f"Host: {host}",f"Content-Length: {len(text.encode())}"],text.encode(),target)
print(strict(forwarded))
print(wire==forwarded固定試料の一致を確認する)再解析結果とバイト列が一致します。実中継の全挙動は実装ごとに確認しますが、この資料では検査した値と下流へ使った値を一致させています。
まとめ
重複値を捨てる前に曖昧さを検出します。
Host・ターゲット・本文長の解釈を中継前後で揃えます。
固定バイト列の検査と実ストリームの区切りを混同しません。
操作履歴と解説を振り返れます。
Python 3.15 / 固定の公開試料
from sd11 import *
wire = request(["Host: api.example.test","Content-Length: 8"])
method,target,version,headers,body = parse(wire)
print(method,target,version)
print(list(headers.items()),body,len(body))
print(strict(wire))POST /orders HTTP/1.1
[('Host', 'api.example.test'), ('Content-Length', '8')] b'item=pen' 8
('POST', '/orders', 'api.example.test', 'item=pen')duplicate = request(["Host: api.example.test","Host: other.example.test","Content-Length: 8"])
values = parse(duplicate)[3].get_all("Host")
print(values[0],values[-1])
print(strict(duplicate))api.example.test other.example.test host_count
print(strict(request(["Host: other.example.test","Content-Length: 8"])))
print(strict(request(["Host: api.example.test","Content-Length: 8"],target="http://other.example.test/orders")))host_rejected target_rejected
conflict = request(["Host: api.example.test","Content-Length: 8","Content-Length: 3"])
lengths = parse(conflict)[3].get_all("Content-Length")
print(lengths)
print(body[:int(lengths[0])],body[:int(lengths[-1])])
print(strict(conflict))['8', '3'] b'item=pen' b'ite' length_rejected
mixed = request(["Host: api.example.test","Content-Length: 8","Transfer-Encoding: chunked"])
print(strict(mixed))transfer_encoding_rejected
for length in (7,9,17):
print(length,strict(request(["Host: api.example.test",f"Content-Length: {length}"])))7 body_length_mismatch 9 body_length_mismatch 17 body_limit
accepted = strict(wire)
method,target,host,text = accepted
forwarded = request([f"Host: {host}",f"Content-Length: {len(text.encode())}"],text.encode(),target)
print(strict(forwarded))
print(wire==forwarded)('POST', '/orders', 'api.example.test', 'item=pen')
True演習完了