初めに
いま、とあることを理由に、python で proxy を書いてます。
そのついでに、これまでに覚えたことをを共有したいと思います。
つきましては、本稿では、まず、HTTPプロトコルの全様について、端々の内容も含め、私自身が理解した内容を述べるとともに、特にリクエストメッセージの全体のサイズはどうやって決定しているのかという点についてフォーカスして説明致します。
なお、若干、言動が おじさん ですが許してやってください(m-_-;m)
URLとURI
一番最初に出て来るのはこれ、URLとURI。
- URL というのは、ブラウザでアドレスバーに指定するやつ
-
Uniform Resource Locator、統一資源位置特定子とでもいうべきか?
- Locator は、位置を特定するという意味。
- URI と言うのは、URL を含めた総称。
- Uniform Resource Identifier、統一資源識別子。
- ネットワーク上のリソースの位置を特定する場合は全てURLである
- 位置を特定しない物、例えば
mailto:robin@crousou.com
は URI か?
URLの形式
URLの形式:
スキーマー://ホスト名/パス[?クエリ]
名前 | 英名 | 説明 |
---|---|---|
スキーマ | schema | httpとかhttps、file、ftp、gopher等々、プロトコル種類を表す |
ホスト名 | hostname |
ホスト名(hostname)は、基本は以下の形式を持つ
ホストアドレス[:ポート番号][@ユーザ名[:パスワード]] Apache-httpd では制限を解除した場合には、ユーザ名・パスワードを含めた形式でアクセスすることが可能である |
パス | path |
/index.html とかURLのうち、境目の / より前がホスト名/ を含め
? の直前までがパス( ? は区切り文字であり、パスにもクエリにも含まない) |
クエリ | query |
query_string(クエリ・ストリング、クエリ文字列)とも呼ばれるもので、省略可能で、GET メソッドでリクエストするときに、リクエストパラメータを送るときに使う。 また、POSTメソッドでは、リクエストボディを送ることができるため、Content-Type の MIME に application/x-www-form-urlencoded を指定し、リクエストボディに同様の形式で、このクエリ文字列を指定することもできる。リクエストパラメータは、 name=value の形式をアンパサンド(& )で区切り複数指定できる。名前(name)も値(value)も、基本、全て英数字と特定の記号([-_.~0-9a-zA-Z] )のみで構成し、それらに含まれない文字(半角空白や漢字などほかの文字全て)は全てURLエンコーディングによってパーセントエンコーディング(1文字が %XX の形式。XXは16進数。16進数の英字部分は英大文字)される。また、同じname=value が重なっても構わない。受け手のサーバ側のアプリケーションは、重複したデータをどのように扱うかはアプリーケーションに任される。さらに、クエリ文字列に使われる漢字のエンコーディングは、HTTPプロトコルの範疇ではなく、ブラウザ上のアプリと、サーバウェアの仕様で決まる。例)空白= %20 、アンパサンド& =%26
|
クッキーとは何か?
- HTTPは、TCPセッションで開設される
- TCPセッションは、開いたら閉じるまで開きっぱなしのやつ
- TCPセッションは、開いたら閉じるまで開きっぱなしのやつ
- 基本、送って(リクエスト)・受けて(レスポンス)で、1セッションがクローズする
- この手続きによって通信は一度で分断されるので、Webアプリケーションでは、ステートレス(状態が続かない状態)になる
- Webアプリケーションでは、ステートフル(状態を保つ)にするために、クッキーを活用する
- クッキーはブラウザ側のアプリがクレクレして、サーバがくれる情報。
- 予め、クッキーをクレクレするときには、クッキーにユーザーIDなど留めておきたい情報とか、有効期間を書いてリクエスト・メッセージに便乗させる
- サーバが返したクッキーは有効期間の間は、ブラウザで覚えておくことができる仕組みなので、クッキーの識別子を送れば、同じ人だってのが分かるという仕組み。
どうでもいいことだけども、このクッキーは、セサミストリート に出てたクッキーをあげるとおとなしくなるあの青いクッキーモンスターをイメージしているそうだ。みんな見てた?セサミストリート!見てないか...。
リクエストメッセージ
- リクエストメッセージとは、HTTPサーバに送るメッセージ
- リクエストメッセージは、以下の構造を持つ
リクエストライン
リクエストヘッダ
区切り(ヘッダとボディの区切り)
[リクエストボディ]
※ [...]
は省略可能なことを示す
リクエストライン
- リクエストラインは最初の1行
<メソッド> _ <リクエストターゲット> _ <HTTPバージョン>\r\n
- 各要素は空白(0x20)1文字(ここでは
_
表記)で区切る
メソッド |
メソッドは、何をしたいかをサーバに伝える一番重要なメッセージ
|
|||||||||||||||||||||||||||||||||||||
リクエスターゲット | URL のうち、パスの部分 | |||||||||||||||||||||||||||||||||||||
HTTPバージョン |
ブラウザが対応しているHTTPバージョンHTTP/1.1 ... こんな文字列HTTP/1.0 もあるが、今はほぼ無いので割愛レスポンスメッセージのステータスラインに記載のHTTPバージョンは ここで指定したHTTPバージョンが返ってくる |
リクエストヘッダ
- 各要素の区切りはコロン(:)+空白(0x20)1文字
リクエストヘッダ1: 値\r\n
リクエストヘッダ2: 値\r\n
:
- 本稿での
\r\n
は説明上の表記(復帰文字=\r
、改行文字=\n
)のため、実際は折り返すだけで表示されないので注意
ヘッダ名 | 概要 |
---|---|
リクエストとレスポンスの両方で利用される共通のヘッダ | |
Content-Type | レスポンスのコンテンツタイプを示すヘッダ。 |
Transfer-Encoding | コンテンツの転送方法を示すヘッダ。 |
Content-Length | コンテンツのサイズを示すヘッダ。 |
リクエストヘッダに特有のヘッダ(一部) | |
Host | 接続先のホスト、または、ホストとポート番号を表すヘッダ。 |
Accept | クライアントが受け入れ可能なコンテンツタイプを指定するヘッダ。 |
Authorization | 認証情報を指定するヘッダ。 |
User-Agent | クライアントの情報を指定するヘッダ。 |
Cookie | クッキー情報を指定するヘッダ。 |
クライアント側の仕様の通知
- リクエストヘッダ
Accpet
またはAccept-*
などは、クライアントで受け取ることができる仕様の通知を表す
Host リクエストヘッダについて
- リクエストメッセージには必ず、Host ヘッダがある
-
Host ヘッダは、URL のホスト名部分
ポート番号が付いていればポート番号の記述も付く - 通常ブラウザが、リクエストヘッダを送る頃には、既にTCPセッションが始まっていて、ホストは開かれているから、普通のブラウザは関係ないのだけど、間を仲介するプロクシサーバとかは、誰が宛先かわからない。
そのためという訳じゃないと思うが、必ず指定されるリクエストヘッダとして Host ヘッダがあり、ここには、URL のうちの ホスト名 が書かれている
区切り
- 区切りは空行(\r\n)で、ヘッダとボディの区切りを表す
- ボディが無くても必ず存在する
リクエストボディ
-
リクエストボディは、省略可能である
-
リクエストボディには、必要に応じて、 POST/PUT/PATCH/DELETE の4つのメソッドで送信するコンテンツを指定する
-
PUT/PATCH メソッドでは、アップロードする文書のコンテンツをここに指定する
-
DELETE メソッドでも同様にコンテンツを送ることができる
-
リクエストボディの解釈は、サーバサイドのアプリケーションに任されておりHTTPサーバ自体は関与しない。 基本的なプロトコルの実装では、すべてのメソッドで、リクエストボディを送信することができる。つまり、例えば GET メソッドでリクエストボディを送ってもエラーにはならない。しかし、メソッドと言う考え方を導入したからには、4つのメソッド POST/PUT/PATCH/DELETE 以外ではリクエストボディは送信しないでね、という約束の下に、ルール付けが行われているに過ぎない
-
リクエストボディが送信可能な各メソッドにて送れるリクエスト内容は以下の通り
Content-Type の MIME | リクエストボディの内容 |
---|---|
application/x-www-form-urlencoded | GETメソッドで指定した場合とまったく同じクエリパラメータを指定する。URLエンコードも行う |
multipart/form-data | マルチパート形式でデータを送る。ファイルのアップロードもこの形式で送信される。フォームの各要素(GET メソッドでの名前と値のペア)についても、マルチパートで分割されるため、大きなデータを送ることができる。マルチパート送信時には、Content-Typeの第2引数にバウンダリを指定する。本例については次の節「マルチパートの例」を参照 |
application/json | json形式のまま指定する |
application/xml | xml形式のまま指定する |
そのほか | 上位機以外にも、MIMEに準拠した形式のデータを送ることができる |
マルチパートの例
- マルチパートでデータを送信する際にはバウンダリの生成が必要となる
- バウンダリの値はその時々で一意になることが望ましい
- 生成したバウンダリは Content-Type の第2パラメータに指定する
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
- 以下の例に挙げるように、マルチパートの各パーティションでは、個々に、Content-Type が指定できるようになっているため、個々のコンテンツに適した内容でデータを送ることができる
- マルチパートのコンテンツが、リクエストヘッダ作成時に確定できていない場合は、後述「メッセージ全体の長さ」で述べる方式に則り、マルチパートの各個別にではなく、マルチパート全体を1つの大きな塊として、Content-Length は指定せず、Transfer-Encoding に chunked を指定し、チャンク方式で送ることができる
- このマルチパートはレスポンスメッセージのレスポンスボディの際にも同様の形式で使用が可能である。すなわち、ファイルのダウンロードは、レスポンスメッセージにおけるこのマルチパートで行われる。
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="fieldName"
fieldValue
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
(ファイルの内容)
----WebKitFormBoundary7MA4YWxkTrZu0gW
レスポンスメッセージ
- レスポンスメッセージとは、リクエストメッセージを送った結果、HTTPサーバから戻ってくるメッセージ
- レスポンスメッセージは、サーバからクライアント(通常はブラウザ)へと、方向性が異なる点と、レスポンスヘッダのヘッダ名や値の解釈が一部異なるのみで(レスポンスだけに含まれるレスポンスヘッダなどがある)、ヘッダの指定形式、ボディとの境界線、マルチパートの受信方法、コンテンツの長さの取り方など、おおよその仕様は全て共通である。
- レスポンスメッセージは、以下の構造を持つ
ステータストライン
レスポンスヘッダ
区切り(ヘッダとボディの区切り)
[レスポンスボディ]
※ [...]
は省略可能なことを示す
ステータスライン
<HTTPバージョン> _ <ステータス番号> _ <理由>\r\n
レスポンスヘッダ
- 基本、送り(リクエスト)と帰り(レスポンス)は同じ
- 各要素の区切りはコロン(:)+空白(0x20)1文字
リクエストヘッダ1: 値\r\n
リクエストヘッダ2: 値\r\n
:
ヘッダ名 | 概要 |
---|---|
リクエストとレスポンスの両方で利用される共通のヘッダ | |
Content-Type | レスポンスのコンテンツタイプを示すヘッダ。 |
Transfer-Encoding | コンテンツの転送方法を示すヘッダ。 |
Content-Length | コンテンツのサイズを示すヘッダ。 |
レスポンスヘッダに特有のヘッダ(一部) | |
Location | リダイレクト先URLを指定するヘッダ。 |
Set-Cookie | クッキーを設定するヘッダ。 |
Server | サーバーの情報を指定するヘッダ。 |
区切り(ヘッダとボディの区切り)
- これもまた、送り(リクエスト)と帰り(レスポンス)は同じ
- 区切りはレスポンスボディが無くても必ず付く
レスポンスボディ
- これもまた、送り(リクエスト)と帰り(レスポンス)は同じ
メッセージ全体の長さ
- 電文の長さに関係するリクエストヘッダは以下の3つ
Content-Length
Transfer-Encoding
Content-Type
- 本節ではリクエストヘッダを例に挙げ述べているが、
レスポンスヘッダについても基本同じ
Content-Length、Transfer-Encoding 共にないケース
- リクエストボディが存在しない場合は、
Content-Length
も、Transfer-Encoding
も無い(GET
、HEAD
、DELETE
、OPTIONS
メソッドなど)
Content-Length、Transfer-Encoding 共にあるケース
- サイズの計算方法の基本は、
Content-Length
のみのケースと同じ -
Transfer-Encoding
には転送方法などが記述される -
Transfer-Encoding
に、chunked
の記述はない
Content-Length のみのケース
-
Content-Length
には、文字エンコーディングでエンコードした後の、ボディの長さが指定されている(10進数) - サイズの1単位は1オクテット(8ビット=1バイト)
- その文字エンコーディングは、省略可能な
Content-Type
に書かれている -
Content-Type
の値は、セミコロン(;)区切り- 1つ目がMIME(ほかの呼び方をする人がいるが、MIME(マイム)で問題ない)
text/plain
がデフォルト。 - 2つ目が文字セット
;charset=xxx
で指定し、文字エンコーディングを表す。utf-8
がデフォルト。
- 1つ目がMIME(ほかの呼び方をする人がいるが、MIME(マイム)で問題ない)
-
Content-Type
が無い場合は、各デフォルトが選択される - ボディの長さは、ヘッダとボディを区切っている区切り(\r\n)の次の位置からカウントされる
-
UTF-8
の場合、漢字は、3~6バイトになるが、漢字1文字でカウントするのではなく、この3~6バイトの数でカウントする - コンテンツとしての改行が、最後にあっても、RFC上の仕様に規定はなく、基本は、トリムはされず、コンテンツとしてカウントされる(各自ブラウザなどの実装による)
Transfer-Encoding のみのケース
-
Transfer-Encoding
の値は、カンマ(,)区切り - この場合、値の中に、
chunked
(チャンク) が指定される - チャンクが指定されるのは、予め転送するデータ容量が分からない場合(
Content-Length
が確定できないケース)が多い - チャンクは、チャンクヘッダとチャンクデータに分かれ、交互に定義される
- チャンクは、ボディ(リクエストボディやレスポンスボディ)に指定され、その形式は、
チャンクヘッダ\r\n
チャンクデータ\r\n
:
0\r\n
\r\n(空行) ←ここで終わり
- 最後の
0\r\n
は、必ず指定され、チャンクサイズが0
であることを示し、続くチャンクデータは存在しない。また、最後の空行(\r\n)も必ず最後に付く - 各要素の末尾の復帰・改行は要素の終わりを示すのではなく、次に続く要素との区切りを示す。
- チャンクヘッダは、16 進数表記のサイズを記述する
- 1バイトなら
1
、46バイトなら2e
など
- 1バイトなら
- チャンクデータは
Transfer-Encoding
にBase64
などの指定がない限りは、 直のバイナリデータが指定される - 容量が多い場合に圧縮転送するケースがあり、
Transfer-Encoding
にcompress
やgzip
などが指定される -
Transfer-Encoding
にgzip
がある場合は、元のデータがgzip
形式で、それをチャンク単位で分割したデータが連続する
ここで無駄な豆知識
- 何ゆえに1オクテットという単位を使うかと言うと、
CISC
などのプロセッサにおいては、1バイトが32ビットや64ビット、詰まり最小単位が64ビットだったりするケースがある - すなわち、世界は、1バイト=8ビット固定ではないのである