AJPv13

簡介

原始文件由 Dan Milstein 撰寫,danmil@shore.net,於 2000 年 12 月撰寫。目前的這份文件由 XML 檔產生,以便更輕鬆地整合到 Tomcat 文件中。

這份文件說明 Apache JServ 協定版本 1.3(以下簡稱 ajp13)。顯然沒有任何關於協定運作方式的現行文件。這份文件旨在解決這個問題,以便讓 JK 維護人員和想要將協定移植到其他地方的人(例如 jakarta 4.x)的生活更輕鬆。

作者

我不是這個協定的設計者之一——我相信 Gal Shachor 是最初的設計者。這份文件中的所有內容都是根據我在 tomcat 3.x 程式碼中找到的實際實作而來的。我希望這份文件有幫助,但我無法保證完全正確。我也不清楚為何做出某些設計決策。在可能的情況下,我提供了一些可能為某些選擇提出的理由,但那些只是我的猜測。總的來說,Shachor 編寫的 C 程式碼非常乾淨且易於理解(儘管幾乎完全沒有文件)。我整理了 Java 程式碼,我想它相當容易閱讀。

設計目標

根據 Gal Shachor 發送給 jakarta-dev 郵件清單的電子郵件,JK(以及因此而來的 ajp13)的原始目標是擴充 mod_jservajp12(我只包含與網路伺服器和 servlet 容器之間通訊相關的目標)

  • 提升效能(特別是速度)。
  • 新增對 SSL 的支援,以便 isSecure()getScheme() 能在 servlet 容器中正常運作。客戶端憑證和加密組將作為要求屬性提供給 servlet。

協定的概觀

ajp13 協定是封包導向的。二進位格式可能基於效能考量而選擇,而非較易讀取的純文字。網路伺服器透過 TCP 連線與 servlet 容器通訊。為了減少建立 socket 的昂貴程序,網路伺服器會嘗試維持與 servlet 容器的持續 TCP 連線,並重複使用連線進行多個要求/回應循環。

一旦連線指定給特定要求,在要求處理循環終止之前,它不會用於其他任何要求。換句話說,要求不會透過連線多工處理。雖然這會導致一次開啟更多連線,但它讓連線兩端的程式碼變得更簡單。

網路伺服器開啟與 servlet 容器的連線後,連線可以處於下列狀態之一

  • 閒置
    沒有要求透過此連線處理。
  • 已指定
    連線正在處理特定要求。

一旦連線指定用於處理特定要求,就會以高度濃縮的形式透過連線傳送基本要求資訊(例如 HTTP 標頭等)(例如,常見字串會編碼為整數)。該格式的詳細資訊如下方的「要求封包結構」。如果要求中有主體(內容長度 > 0),則會在緊接在後方的個別封包中傳送。

此時,servlet 容器應已準備好開始處理要求。在處理時,它可以將下列訊息傳送回網路伺服器

  • SEND_HEADERS
    將一組標頭傳送回瀏覽器。
  • SEND_BODY_CHUNK
    將一塊主體資料傳送回瀏覽器。
  • GET_BODY_CHUNK
    從要求取得進一步資料(如果尚未傳輸所有資料)。這是必要的,因為封包有固定的最大大小,而要求的主體可以包含任意數量的資料(例如,上傳的檔案)。(注意:這與 HTTP 分塊傳輸無關)。
  • END_RESPONSE
    完成要求處理循環。

每則訊息都附帶不同格式的資料封包。有關詳細資訊,請參閱下方的「回應封包結構」。

基本封包結構

此通訊協定承襲了 XDR 的一些特點,但在許多方面有所不同(例如,沒有 4 位元組對齊)。

AJP13 對所有資料類型使用網路位元組順序。

此通訊協定中有四種資料類型:位元組、布林值、整數和字串。

位元組
單一位元組。
布林值
單一位元組,1 = true,0 = false。在某些地方,使用其他非零值作為 true(例如 C 式)可能有效,但在其他地方則不行。
整數
介於 0 到 2^16 (32768) 之間的數字。儲存在 2 個位元組中,高位元組在前。
字串
變長字串(長度受 2^16 限制)。先將長度封裝成兩個位元組進行編碼,然後是字串(包括終止符 '\0')。請注意,編碼長度包含尾隨 '\0' -- 就像 strlen。這在 Java 端會造成一些混淆,其中充斥著奇怪的自動遞增陳述式來略過這些終止符。我相信這樣做的原因是為了讓 C 程式碼在讀取 Servlet 容器傳回的字串時能額外提高效率 -- 使用終止符 \0,C 程式碼可以在不複製的情況下傳遞對單一緩衝區的參照。如果沒有 \0,C 程式碼必須複製內容才能取得字串概念。請注意,大小 -1 (65535) 表示空字串,且在這種情況下,沒有資料會出現在長度之後。

封包大小

根據大部分程式碼,最大封包大小為 8 * 1024 位元組 (8K)。封包的實際長度編碼在標頭中。

封包標頭

從伺服器傳送至容器的封包以 0x1234 開頭。從容器傳送至伺服器的封包以 AB 開頭(這是 A 的 ASCII 碼,後面接著 B 的 ASCII 碼)。在頭兩個位元組之後,有一個整數(如上所述編碼)表示酬載長度。儘管這可能表示最大酬載可以大到 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 伺服器可以將下列訊息傳送至 Servlet 容器

代碼 封包類型 意義
2 轉發請求 使用下列資料開始請求處理週期
7 關閉 Web 伺服器要求容器關閉自身。
8 Ping Web 伺服器要求容器取得控制權 (安全登入階段)。
10 CPing Web 伺服器要求容器使用 CPong 快速回應。
資料 大小 (2 個位元組) 和對應的主體資料。

為了確保一些基本安全性,容器只會在請求來自其所寄存的相同機器時實際執行 關閉

第一個 資料 封包會在 Web 伺服器傳送 轉發請求 後立即傳送。

Servlet 容器可以將下列訊息類型傳送至 Web 伺服器

代碼 封包類型 意義
3 傳送主體區塊 從 Servlet 容器傳送主體區塊至 Web 伺服器 (並假設傳送至瀏覽器)。
4 傳送標頭 從 Servlet 容器傳送回應標頭至 Web 伺服器 (並假設傳送至瀏覽器)。
5 結束回應 標示回應結束 (因此也標示請求處理週期結束)。
6 取得主體區塊 如果尚未全部傳輸,則從請求取得進一步資料。
9 CPong 回應 對 CPing 請求的回應

上述每則訊息都有不同的內部結構,詳情如下。

請求封包結構

對於從伺服器傳送至容器,類型為「轉發請求」的訊息

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 코드는 처음 두 바이트 정수를 가져오고 가장 중요한 바이트에 '0xA0'이 있는 경우 두 번째 바이트의 정수를 헤더 이름 배열의 인덱스로 사용합니다. 첫 번째 바이트가 '0xA0'이 아닌 경우 두 바이트 정수를 문자열의 길이로 가정하고 읽습니다.

이는 헤더 이름의 길이가 0x9FFF(==0xA000 - 1)보다 크지 않다는 가정 하에 작동합니다. 이는 완전히 타당하지만 다소 임의적입니다. (저와 같이 쿠키 사양과 헤더의 길이에 대해 생각하기 시작했다면 걱정하지 마세요. 이 제한은 헤더 이름에 적용되며 헤더 에는 적용되지 않습니다. 엄청나게 큰 헤더 이름이 HTTP 사양에 곧 나타날 가능성은 거의 없습니다.)

注意:content-length 標頭非常重要。如果存在且非零,容器會假設要求有主體(例如 POST 要求),並立即從輸入串流讀取一個獨立封包以取得該主體。

屬性

? 為前綴的屬性(例如 ?context)都是選用的。對於每個屬性,有一個單一位元組碼表示屬性類型,然後用字串提供其值。它們可以按任何順序傳送(儘管 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

C 程式碼目前未設定 contextservlet_path,而且大部分 Java 程式碼完全忽略這些欄位傳送的任何內容(而且如果在這些碼之後傳送字串,其中一些程式碼實際上會中斷)。我不知道這是一個錯誤、未實作的功能還是只是殘留程式碼,但它在連線的兩端都遺失了。

remote_userauth_type 可能指的是 HTTP 層級驗證,並傳達遠端使用者的使用者名稱和用於建立其身分的驗證類型(例如 Basic、Digest)。我不清楚為什麼密碼沒有同時傳送,但我對 HTTP 驗證並不完全了解。

query_stringssl_certssl_cipherssl_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」和「確定」)。回應標頭名稱的編碼方式與要求標頭名稱相同。請參閱上方有關如何將代碼與字串區分的詳細資料。常見標頭的代碼為

이름코드 값
內容類型0xA001
內容語言0xA002
內容長度0xA003
日期0xA004
上次修改時間0xA005
位置0xA006
設定 Cookie0xA007
設定 Cookie20xA008
Servlet 引擎0xA009
狀態0xA00A
WWW 驗證0xA00B

在代碼或字串標頭名稱之後,標頭值會立即編碼。

結束回應

表示此要求處理週期的結束。如果 reuse 旗標為 true(實際 C 程式碼中為 0 以外的任何值),則此 TCP 連線現在可用于處理新的傳入要求。如果 reuse 為 false(==0),則應關閉連線。

取得主體區塊

容器要求要求提供更多資料(如果主體太大而無法放入傳送的第一個封包,或當要求被區塊化時)。伺服器會傳送一個主體封包,其中資料量為 request_length、最大傳送主體大小(8186(8 KB - 6))和實際上從要求主體傳送的位元組數量的最小值。
如果主體中沒有更多資料(即 Servlet 容器嘗試讀取超過主體的結尾),則伺服器會傳送一個「空」封包,也就是一個有效負載長度為 0 的主體封包。(0x12,0x34,0x00,0x00)

我的問題

如果要求標頭 > 最大封包大小,會發生什麼事?如果超過 8K,沒有提供傳送第二個封包要求標頭的規定(我認為這對回應標頭處理正確,但我並不確定)。我不知道是否有辦法將超過 8K 的資料放入那組初始要求標頭中,但我敢打賭有(結合長 Cookie、長 SSL 資訊和許多環境變數,你應該可以輕易達到 8K)。我認為在這種情況下,連接器會在嘗試傳送任何標頭之前就失敗,但我並不確定。

驗證如何?網際網路伺服器和容器之間的連線似乎沒有任何驗證。這對我來說似乎很危險。