AJPv13

はじめに

オリジナルのドキュメントは Dan Milstein により、danmil@shore.net2000年12月に執筆されました。本ドキュメントは、Tomcatドキュメントへのより容易な統合を可能にするため、XMLファイルから生成されています。

これは Apache JServ Protocol バージョン1.3 (以下ajp13) について記述します。プロトコルがどのように機能するかに関する現在のドキュメントは、どうやら存在しないようです。本ドキュメントは、JKのメンテナーや、プロトコルをどこかに移植したい人 (例えば jakarta 4.x へ) の作業を容易にするために、その状況を改善しようとする試みです。

著者

私はこのプロトコルの設計者の一人ではありません。オリジナルの設計者は Gal Shachor だと私は信じています。このドキュメントのすべては、Tomcat 3.x コードで見つけた実際の実装から派生しています。役立つことを願っていますが、完璧な正確さを保証することはできません。また、特定の設計上の決定がなぜなされたのかも知りません。私ができた範囲で、特定の選択についていくつかの可能な正当化を提示しましたが、それらは単なる私の推測にすぎません。一般的に、Shachorが書いたCコードは非常にクリーンで理解しやすいものですが(ほとんど全く文書化されていません)。私はJavaコードを整理し、合理的に読みやすいものになったと思います。

設計目標

Gal Shachor から jakarta-dev メーリングリストへのメールによると、JK (ひいてはajp13) の当初の目標は、(Webサーバーとサーブレットコンテナ間の通信に関連する目標のみを含みます)

  • パフォーマンス (特に速度) の向上。
  • SSL のサポートを追加し、サーブレットコンテナ内で isSecure() および getScheme() が正しく機能するようにすること。クライアント証明書と暗号スイートは、リクエスト属性としてサーブレットで利用可能になります。

プロトコルの概要

ajp13 プロトコルはパケット指向です。より読みやすいプレーンテキストではなくバイナリ形式が選択されたのは、おそらくパフォーマンス上の理由によるものです。Webサーバーは TCP 接続を介してサーブレットコンテナと通信します。ソケット作成という高価なプロセスを削減するため、Webサーバーはサーブレットコンテナへの永続的な TCP 接続を維持し、複数のリクエスト/レスポンスサイクルで接続を再利用しようとします。

一度特定の要求に割り当てられた接続は、要求処理サイクルが終了するまで他の要求には使用されません。言い換えれば、要求は接続上で多重化されません。これにより、接続の両端でのコードがはるかに単純になりますが、同時に開かれる接続の数が増えることになります。

Webサーバーがサーブレットコンテナへの接続を開いた後、接続は次のいずれかの状態になります。

  • アイドル
    この接続ではリクエストが処理されていません。
  • 割り当て済み
    接続が特定の要求を処理しています。

一度接続が特定の要求を処理するために割り当てられると、基本的な要求情報(HTTPヘッダーなど)は、極めて凝縮された形式(例:共通の文字列は整数としてエンコードされる)で接続を介して送信されます。その形式の詳細は、後述の「要求パケット構造」にあります。要求に本文がある場合(content-length > 0)、それは直後に別のパケットで送信されます。

この時点で、サーブレットコンテナはリクエストの処理を開始する準備ができています。処理中に、以下のメッセージをWebサーバーに送り返すことができます。

  • SEND_HEADERS
    ヘッダーのセットをブラウザに送り返します。
  • SEND_BODY_CHUNK
    ボディデータの一部をブラウザに送り返します。
  • GET_BODY_CHUNK
    まだすべて転送されていない場合、リクエストからさらにデータを取得します。これは、パケットの最大サイズが固定されており、リクエストのボディに任意の量のデータ(アップロードされたファイルなど)を含めることができるため、必要です。(注: これはHTTPのチャンク転送とは関係ありません)。
  • END_RESPONSE
    リクエスト処理サイクルを終了します。

各メッセージには、異なる形式のデータパケットが付属しています。詳細は以下のレスポンスパケット構造を参照してください。

基本パケット構造

このプロトコルにはXDRの伝統が少しありますが、多くの点で異なります(例えば、4バイトアライメントはありません)。

AJP13 は、すべてのデータ型にネットワークバイトオーダーを使用します。

プロトコルには、バイト、ブール値、整数、文字列の4つのデータ型があります。

バイト
単一のバイト。
ブール値
単一のバイト、1 = true、0 = false。他のゼロ以外の値を true (C言語スタイル) として使用すると一部の場所では機能するかもしれませんが、他の場所では機能しません。
整数
0 から 2^16 (32768) の範囲の数値。上位バイトを先頭に2バイトで格納されます。
文字列
可変サイズの文字列(長さは2^16で制限されます)。まず長さが2バイトにパックされ、次に文字列(終端の「\0」を含む)が続きます。エンコードされた長さには末尾の「\0」は含まれないことに注意してください。これは strlen のようです。これはJava側では少し混乱を招き、これらの終端文字をスキップするために奇妙な自動インクリメント文が散見されます。これがなされた理由は、サーブレットコンテナが送り返す文字列を読み取る際に、Cコードが非常に効率的になるようにするためだと私は考えています。終端の\0文字があれば、Cコードはコピーせずに単一のバッファへの参照を渡すことができます。\0が欠落している場合、Cコードは文字列の概念を得るために内容をコピーする必要があるでしょう。この場合、サイズが -1 (65535) の場合はヌル文字列を示し、長さの後にデータは続きません。

パケットサイズ

多くのコードによると、最大パケットサイズは 8 * 1024 バイト (8K) です。パケットの実際の長さはヘッダーにエンコードされています。

パケットヘッダー

サーバーからコンテナに送信されるパケットは 0x1234 で始まります。コンテナからサーバーに送信されるパケットは AB (これはAのASCIIコードの後にBのASCIIコードが続くもの) で始まります。最初の2バイトの後には、ペイロードの長さを示す整数(上記のようにエンコードされる)が続きます。これにより、最大ペイロードが2^16にもなる可能性が示唆されますが、実際にはコードが最大値を8Kに設定しています。

パケット形式(サーバー → コンテナ)
バイト 0 1 2 3 4...(n+3)
内容 0x12 0x34 データ長 (n) データ
パケット形式(コンテナ → サーバー)
バイト 0 1 2 3 4...(n+3)
内容 A B データ長 (n) データ

ほとんどのパケットでは、ペイロードの最初のバイトがメッセージのタイプをエンコードします。例外は、サーバーからコンテナに送信されるリクエストボディパケットです。これらは標準のパケットヘッダー (0x1234 とその後のパケット長) で送信されますが、その後のプレフィックスコードは含まれません (これは私には間違いのように見えます)。

Webサーバーはサーブレットコンテナに次のメッセージを送信できます。

コード パケットのタイプ 意味
2 リクエスト転送 以下のデータでリクエスト処理サイクルを開始します。
7 シャットダウン Webサーバーはコンテナにシャットダウンを要求します。
8 Ping Webサーバーはコンテナに制御を引き継ぐよう要求します (セキュアログインフェーズ)。
10 CPing WebサーバーはコンテナにCPongですばやく応答するよう要求します。
なし データ サイズ(2バイト)と対応するボディデータ。

基本的なセキュリティを確保するため、コンテナはリクエストがホストされているマシンと同じマシンから来た場合にのみ Shutdown を実行します。

最初のDataパケットは、WebサーバーによってForward Requestの直後に送信されます。

サーブレットコンテナはWebサーバーに次の種類のメッセージを送信できます。

コード パケットのタイプ 意味
3 ボディチャンク送信 サーブレットコンテナからWebサーバーへ(そしておそらくブラウザへ)ボディのチャンクを送信します。
4 ヘッダー送信 サーブレットコンテナからWebサーバーへ(そしておそらくブラウザへ)レスポンスヘッダーを送信します。
5 レスポンス終了 レスポンスの終了(したがってリクエスト処理サイクルの終了)を示します。
6 ボディチャンク取得 まだすべて転送されていない場合、リクエストからさらにデータを取得します。
9 CPong応答 CPingリクエストへの返答

上記の各メッセージには、以下に詳述する異なる内部構造があります。

リクエストパケット構造

サーバーからコンテナへの「Forward Request」タイプのメッセージの場合

AJP13_FORWARD_REQUEST :=
    prefix_code      (byte) 0x02 = JK_AJP13_FORWARD_REQUEST
    method           (byte)
    protocol         (string)
    req_uri          (string)
    remote_addr      (string)
    remote_host      (string)
    server_name      (string)
    server_port      (integer)
    is_ssl           (boolean)
    num_headers      (integer)
    request_headers *(req_header_name req_header_value)
    attributes      *(attribut_name attribute_value)
    request_terminator (byte) OxFF

request_headersは以下の構造を持ちます。

req_header_name := 
    sc_req_header_name | (string)  [see below for how this is parsed]

sc_req_header_name := 0xA0xx (integer)

req_header_value := (string)

attributesはオプションで、以下の構造を持ちます。

attribute_name := sc_a_name | (sc_a_req_attribute string)

attribute_value := (string)

コンテナが直ちに別のパケットを探すかどうかを決定するため、最も重要なヘッダーは「content-length」です。

Forward Requestの要素の詳細説明。

request_prefix

すべてのリクエストについて、これは2になります。他のプレフィックスコードの詳細については上記を参照してください。

method

HTTPメソッド、単一バイトとしてエンコード

コマンド名コード
OPTIONS1
GET2
HEAD3
POST4
PUT5
DELETE6
TRACE7
PROPFIND8
PROPPATCH9
MKCOL10
COPY11
MOVE12
LOCK13
UNLOCK14
ACL15
REPORT16
VERSION-CONTROL17
CHECKIN18
CHECKOUT19
UNCHECKOUT20
SEARCH21
MKWORKSPACE22
UPDATE23
LABEL24
MERGE25
BASELINE_CONTROL26
MKACTIVITY27

protocol, req_uri, remote_addr, remote_host, server_name, server_port, is_ssl

これらはすべてかなり自明です。これらのそれぞれが必須であり、すべてのリクエストで送信されます。

ヘッダー

request_headersの構造は次のとおりです。まず、ヘッダーの数num_headersがエンコードされます。次に、ヘッダー名req_header_name/値req_header_valueのペアが続きます。一般的なヘッダー名は、スペースを節約するために整数としてエンコードされます。ヘッダー名が基本ヘッダーのリストにない場合、それは通常通り(プレフィックス長付きの文字列として)エンコードされます。共通ヘッダーsc_req_header_nameとそのコードのリストは次のとおりです(すべて大文字小文字を区別します)。

名前コード値コード名
accept0xA001SC_REQ_ACCEPT
accept-charset0xA002SC_REQ_ACCEPT_CHARSET
accept-encoding0xA003SC_REQ_ACCEPT_ENCODING
accept-language0xA004SC_REQ_ACCEPT_LANGUAGE
authorization0xA005SC_REQ_AUTHORIZATION
connection0xA006SC_REQ_CONNECTION
content-type0xA007SC_REQ_CONTENT_TYPE
content-length0xA008SC_REQ_CONTENT_LENGTH
cookie0xA009SC_REQ_COOKIE
cookie20xA00ASC_REQ_COOKIE2
host0xA00BSC_REQ_HOST
pragma0xA00CSC_REQ_PRAGMA
referer0xA00DSC_REQ_REFERER
user-agent0xA00ESC_REQ_USER_AGENT

これを読み取るJavaコードは、最初の2バイト整数を取得し、最上位バイトに'0xA0'が見つかった場合、2番目のバイトの整数をヘッダー名配列へのインデックスとして使用します。最初のバイトが'0xA0'でない場合、2バイト整数が文字列の長さであると仮定し、その文字列を読み込みます。

これは、ヘッダー名の長さが 0x9FFF (==0xA000 - 1) を超えないという仮定に基づいています。これは全く合理的ですが、やや恣意的です。(もし私のようにここでCookieの仕様とヘッダーの長さについて考え始めたとしても、心配はいりません。この制限はヘッダーに対するものであり、ヘッダーに対するものではありません。扱いにくいほど巨大なヘッダー名がHTTP仕様にすぐ現れることは考えにくいです)。

注意: content-length ヘッダーは極めて重要です。このヘッダーが存在し、ゼロ以外の場合、コンテナはリクエストにボディがあるとみなし(例:POSTリクエスト)、すぐに別のパケットを入力ストリームから読み込んでそのボディを取得します。

属性

? (例: ?context) でプレフィックスされた属性はすべてオプションです。それぞれについて、属性のタイプを示す1バイトコードがあり、その値を示す文字列が続きます。これらは任意の順序で送信できます(ただし、Cコードは常に以下のリストの順序で送信します)。オプション属性のリストの終わりを示す特別な終了コードが送信されます。バイトコードのリストは次のとおりです。

情報コード値注記
?context0x01現在未実装
?servlet_path0x02現在未実装
?remote_user0x03
?auth_type0x04
?query_string0x05
?route0x06
?ssl_cert0x07
?ssl_cipher0x08
?ssl_session0x09
?req_attribute0x0A名前 (属性の名前が続きます)
?ssl_key_size0x0B
?secret0x0C
?stored_method0x0D
are_done0xFFrequest_terminator

contextservlet_pathは現在Cコードで設定されておらず、Javaコードのほとんどはこれらのフィールドに送信されるものを完全に無視します(そして一部のコードは、これらのコードの後に文字列が送信されると実際に壊れます)。これがバグなのか未実装の機能なのか、あるいは単に残存コードなのかはわかりませんが、接続の両側で欠落しています。

remote_user および auth_type はおそらく HTTP レベルの認証を指し、リモートユーザーのユーザー名と、その身元を確立するために使用された認証タイプ (例: Basic, Digest) を伝達します。パスワードも送信されない理由については明確ではありませんが、HTTP 認証の内外を知っているわけではありません。

query_string, ssl_cert, ssl_cipher, ssl_session は、HTTP および HTTPS の対応する部分を参照しています。

routeは、私の理解では、複数のロードバランシングサーバーが存在する場合に、ユーザーのセッションを特定のTomcatインスタンスに関連付ける、スティッキーセッションをサポートするために使用されます。詳細は不明です。

この基本属性リスト以外にも、req_attribute コード (0x0A) を介して任意の数の他の属性を送信できます。このコードの各インスタンスの直後に、属性名と値を表す文字列のペアが送信されます。環境変数はこの方法で渡されます。

最後に、すべての属性が送信された後、属性ターミネータである0xFFが送信されます。これは属性リストの終わりと、リクエストパケットの終わりを両方示します。

レスポンスパケット構造

コンテナがサーバーに送り返すことができるメッセージの場合。

AJP13_SEND_BODY_CHUNK := 
  prefix_code   3
  chunk_length  (integer)
  chunk        *(byte)


AJP13_SEND_HEADERS :=
  prefix_code       4
  http_status_code  (integer)
  http_status_msg   (string)
  num_headers       (integer)
  response_headers *(res_header_name header_value)

res_header_name := 
    sc_res_header_name | (string)   [see below for how this is parsed]

sc_res_header_name := 0xA0 (byte)

header_value := (string)

AJP13_END_RESPONSE :=
  prefix_code       5
  reuse             (boolean)


AJP13_GET_BODY_CHUNK :=
  prefix_code       6
  requested_length  (integer)

詳細

ボディチャンク送信

このチャンクは基本的にバイナリデータであり、直接ブラウザに送り返されます。

ヘッダー送信

ステータスコードとメッセージは通常のHTTPのものです(例:「200」と「OK」)。レスポンスヘッダー名は、リクエストヘッダー名と同じ方法でエンコードされます。コードが文字列とどのように区別されるかについては、上記を参照してください。共通ヘッダーのコードは次のとおりです。

名前コード値
Content-Type0xA001
Content-Language0xA002
Content-Length0xA003
Date0xA004
Last-Modified0xA005
Location0xA006
Set-Cookie0xA007
Set-Cookie20xA008
Servlet-Engine0xA009
Status0xA00A
WWW-Authenticate0xA00B

コードまたは文字列ヘッダー名の後、ヘッダー値が直ちにエンコードされます。

レスポンス終了

このリクエスト処理サイクルの終了を通知します。reuse フラグが true (実際のCコードでは0以外の値) の場合、このTCP接続は新しい受信リクエストの処理に再利用できます。reuse が false (==0) の場合、接続は閉じられるべきです。

ボディチャンク取得

コンテナはリクエストから追加のデータを要求します(ボディが最初のパケットに収まらないほど大きい場合や、リクエストがチャンク化されている場合)。サーバーは、request_length、最大送信ボディサイズ(8186 (8 Kbytes - 6))、およびリクエストボディから実際に送信される残りのバイト数のうち、最小のデータ量を含むボディパケットを返送します。
ボディにデータがもうない場合(つまり、サーブレットコンテナがボディの終端を超えて読み取ろうとしている場合)、サーバーはペイロード長が0の「空の」パケット(0x12, 0x34, 0x00, 0x00)を返送します。

私の疑問

リクエストヘッダーが最大パケットサイズを超えた場合、どうなりますか?8Kを超えるリクエストヘッダーを2番目のパケットとして送信する規定がありません(レスポンスヘッダーについては正しく処理されていると思いますが、確信はありません)。初期のリクエストヘッダーのセットに8K以上のデータを含める方法があるかどうかはわかりませんが、おそらくあるでしょう(長いCookieと長いSSL情報、多くの環境変数を組み合わせれば、簡単に8Kに達するはずです)。この場合、コネクタはヘッダーを送信する前に失敗するだけだと思いますが、確信はありません。

認証についてはどうでしょうか?Webサーバーとコンテナ間の接続には認証がないようです。これは潜在的に危険だと感じます。