Apache HTTP Server 版本 2.4
說明 | mod_proxy 的 AJP 支援模組 |
---|---|
狀態 | 擴充功能 |
模組識別碼 | proxy_ajp_module |
原始檔 | mod_proxy_ajp.c |
相容性 | 可用於版本 2.1 及後續版本 |
此模組需要mod_proxy
的服務。它提供對Apache JServ Protocol 版本 1.3
(以下簡稱 AJP13)的支援。
因此,若要取得處理 AJP13
協定的能力,伺服器中必須同時存在 mod_proxy
和 mod_proxy_ajp
。
請在 確保伺服器安全 之前,不要啟用代理。開放式代理伺服器對您的網路和 Internet 整體而言都是危險的。
此模組用於使用 AJP13 協定,反向代理到後端應用程式伺服器(例如 Apache Tomcat)。用法類似於 HTTP 反向代理,但使用 ajp://
前置詞
ProxyPass "/app" "ajp://backend.example.com:8009/app"
自 Tomcat 8.5.51 和 9.0.31 起,Tomcat 的 secret
等選項(預設為必要)可以新增為 ProxyPass
或 BalancerMember
末端的獨立參數。此參數適用於 Apache HTTP Server 2.4.42 及後續版本
secret
選項的簡單反向代理ProxyPass "/app" "ajp://backend.example.com:8009/app" secret=YOUR_AJP_SECRET
也可以使用平衡器
<Proxy "balancer://cluster"> BalancerMember "ajp://app1.example.com:8009" loadfactor=1 BalancerMember "ajp://app2.example.com:8009" loadfactor=2 ProxySet lbmethod=bytraffic </Proxy> ProxyPass "/app" "balancer://cluster/app"
請注意,通常不需要 ProxyPassReverse
指令。AJP 請求包含代理伺服器提供的原始主機標頭,並且可以預期應用程式伺服器會針對此主機產生自我參考標頭,因此不需要改寫。
主要的例外是在代理伺服器上的 URL 路徑與後端路徑不同時。在這種情況下,重新導向標頭可以針對原始主機 URL(而不是後端 ajp://
URL)改寫,例如
ProxyPass "/apps/foo" "ajp://backend.example.com:8009/foo" ProxyPassReverse "/apps/foo" "http://www.example.com/foo"
不過,通常比採用這種方式來得更好的是將應用程式部署在後端伺服器上,並使用與代理伺服器相同的路徑。
名稱以 AJP_
開頭的環境變數會轉發到始端伺服器,作為 AJP 請求屬性(從金鑰名稱移除 AJP_
開頭)。
AJP13
協定是封包導向。二進位格式可想而知是基於效能考量,而選擇比可讀性較高的純文字來使用。網路伺服器透過 TCP 連線與 servlet 容器進行通訊。為了減少建置通訊埠的昂貴程序,網路伺服器會嘗試維持到 servlet 容器的持久 TCP 連線,並重複使用連線以供多個請求/回應循環使用。
連線指派給某項特定請求後,在請求處理循環終止之前,不會用於任何其他請求。換句話說,請求並未透過連線多工化。雖然這樣會導致需要開啟更多連線才會使用,不過這會讓連線兩端的程式碼變得更簡單。
網路伺服器開啟到 servlet 容器的連線後,連線會處於下列狀態之一
連線指派給處理特定請求後,會在請求封包結構中,以極度濃縮的形式透過連線傳送基本請求資訊(例如 HTTP 標頭等)。如果請求中有一個本文 (content-length > 0)
,會在緊接著的個別封包中傳送。
在這個時候,presumably servlet 容器準備好開始處理請求了。在處理過程中,servlet 容器可以將下列訊息傳回網路伺服器
每一則訊息都附帶格式不同的資料封包。詳情請參閱下列回應封包結構。
這個通訊協定有點承襲 XDR,但在許多方面有所不同(例如沒有 4 位元組對齊)。
AJP13 使用網路位元組順序處理所有資料類型。
協定中包含四種資料類型:位元組、布林、整數和字串。
1 = true
、0 = false
。在有些地方使用其他非零值作為 true(例如 C 風格)可能是可行的,但在其他地方卻不行。0 到 2^16 (32768)
的數字。以兩個位元組儲存,高位元組在先。strlen
類似。這會讓 Java 端有點混淆,因為到處充斥著奇特的自動遞增敘述,用來略過這些結尾字元。我相信這麼做的原因是讓 C 程式碼在讀取 servlet 容器傳回的字串時更有效率,這樣一來,C 程式碼可以傳遞單一緩衝區中的參照,而不用複製。如果沒有 '\0',C 程式碼就必須複製內容才能取得字串概念。根據大部分程式碼,最大封包大小是 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
,後接封包長度),但之後沒有任何前置碼。
網頁伺服器可以傳送下列訊息給 Servlet 容器
代碼 | 封包類型 | 意義 |
2 | 轉送請求 | 使用下列資料開始請求處理循環 |
7 | 關閉 | 網頁伺服器要求容器關閉自身。 |
8 | PING | 網頁伺服器要求容器取得控制權(安全登入階段)。 |
10 | CPING | 網頁伺服器要求容器使用 CPONG 快速回應。 |
無 | 資料 | 大小(2 位元組)和對應的主體資料。 |
為了確保一些基本安全性,容器只有在請求來自它所寄存的機器時才會實際執行關閉
。
第一個資料
封包會在網頁伺服器傳送轉送請求
後立即傳送。
Servlet 容器可以傳送下列類型訊息給網頁伺服器
代碼 | 封包類型 | 意義 |
3 | 傳送主體區塊 | 從 Servlet 容器傳送主體區塊給網頁伺服器(並假設也會傳送到瀏覽器)。 |
4 | 傳送標頭 | 從 Servlet 容器傳送回應標頭給網頁伺服器(並假設也會傳送到瀏覽器)。 |
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
,因為它會決定容器是否立即尋找其他封包。
對於所有請求,這將為 2。有關其他前置碼的詳細資訊,請參閱上方內容。
以單一位元組編碼的 HTTP 方法
命令名稱 | 代碼 |
OPTIONS | 1 |
GET | 2 |
HEAD | 3 |
POST | 4 |
PUT | 5 |
DELETE | 6 |
TRACE | 7 |
PROPFIND | 8 |
PROPPATCH | 9 |
MKCOL | 10 |
COPY | 11 |
MOVE | 12 |
LOCK | 13 |
UNLOCK | 14 |
ACL | 15 |
REPORT | 16 |
VERSION-CONTROL | 17 |
CHECKIN | 18 |
CHECKOUT | 19 |
UNCHECKOUT | 20 |
SEARCH | 21 |
MKWORKSPACE | 22 |
UPDATE | 23 |
LABEL | 24 |
MERGE | 25 |
BASELINE_CONTROL | 26 |
MKACTIVITY | 27 |
更新版本的 ajp13 將傳輸其他方法,即使它們不在此清單中。
這些欄位應該都很好理解。每個欄位都是必要的,且會傳送至每個要求。
request_headers
的結構如下:首先,會編碼標頭數目 num_headers
。其次,接下來是標頭名稱 req_header_name
/ 值 req_header_value
的數個配對。常見的標頭名稱會編碼為整數,以節省空間。如果標頭名稱不在基本標頭清單中,則會以一般方式編碼(先指定長度,後為字串)。常見標頭清單 sc_req_header_name
和對應代碼如下(所有代碼都區分大小寫)
名稱 | 代碼值 | 代碼名稱 |
accept | 0xA001 | SC_REQ_ACCEPT |
accept-charset | 0xA002 | SC_REQ_ACCEPT_CHARSET |
accept-encoding | 0xA003 | SC_REQ_ACCEPT_ENCODING |
accept-language | 0xA004 | SC_REQ_ACCEPT_LANGUAGE |
authorization | 0xA005 | SC_REQ_AUTHORIZATION |
connection | 0xA006 | SC_REQ_CONNECTION |
content-type | 0xA007 | SC_REQ_CONTENT_TYPE |
content-length | 0xA008 | SC_REQ_CONTENT_LENGTH |
cookie | 0xA009 | SC_REQ_COOKIE |
cookie2 | 0xA00A | SC_REQ_COOKIE2 |
host | 0xA00B | SC_REQ_HOST |
pragma | 0xA00C | SC_REQ_PRAGMA |
referer | 0xA00D | SC_REQ_REFERER |
user-agent | 0xA00E | SC_REQ_USER_AGENT |
會讀取此內容的 Java 程式碼會擷取前兩位元的整數,如果在最高位元元組中看到 '0xA0'
,它會將第二位元組中的整數當作標頭名稱陣列中的指標。如果第一個位元組不是 0xA0
,則程式會假設這兩位元的整數為一個字串的長度,然後讀入此字串。
此機制基於一個假設,即沒有標頭名稱的長度會大於 0x9FFF (==0xA000 - 1)
,這是合理的假設,儘管有些武斷。
content-length
標頭非常重要。如果標頭存在且非零,則容器會假設要求有一個主體(例如 POST 要求),並立即從輸入串流讀取一個單獨的封包以取得該主體。開頭為 ?
的屬性(例如 ?context
)都是選用的。對於每個屬性,會有一個單一位元組碼表示屬性的類型,然後是其值(字串或整數)。可以按任何順序傳送屬性(儘管 C 程式碼總是以下方列出的順序傳送屬性)。會傳送一個特殊終止碼,以提示選用屬性清單結束。位元組碼清單如下
資訊 | 代碼值 | 值類型 | 注意事項 |
?context | 0x01 | - | 目前尚未實作 |
?servlet_path | 0x02 | - | 目前尚未實作 |
?remote_user | 0x03 | 字串 | |
?auth_type | 0x04 | 字串 | |
?query_string | 0x05 | 字串 | |
?jvm_route | 0x06 | 字串 | |
?ssl_cert | 0x07 | 字串 | |
?ssl_cipher | 0x08 | 字串 | |
?ssl_session | 0x09 | 字串 | |
?req_attribute | 0x0A | 字串 | 名稱(隨後是屬性的名稱) |
?ssl_key_size | 0x0B | 整數 | |
?secret | 0x0C | 字串 | 支援 2.4.42 以降版本 |
are_done | 0xFF | - | request_terminator |
目前的 C 程式碼並未設定 `context` 和 `servlet_path`,而大部分的 Java 程式碼則完全忽略那些欄位傳送過來的資料(其中有些如果在這些程式碼之後傳送字串,程式碼還會中斷)。我不知道這是不是臭蟲,還是未實作的功能,或者是僅留著的程式碼,但兩端的連線都不存在這個程式碼。
`remote_user` 和 `auth_type` 應該是參考 HTTP 等級的驗證,並傳達遠端使用者的使用者名稱和用於確認其身分的驗證類型(例如,Basic、Digest)。
`query_string`、`ssl_cert`、`ssl_cipher`、`ssl_session` 和 `ssl_key_size` 參考 HTTP 和 HTTPS 的對應部分。
`jvm_route` 用於支援固定會話(在存在多個負載平衡伺服器的情況下,將使用者的會話與特定 Tomcat 執行個體相連結)。
當 `secret=secret_keyword` 參數用於 `ProxyPass
` 或 `BalancerMember
` 指令時,會傳送 `secret`。後端需要支援 `secret`,而且值必須相符。`request.secret` 或 `requiredSecret` 記錄在 Apache Tomcat 的 AJP 設定檔中。
除了這些基本屬性清單之外,還能透過 `req_attribute` 程式碼 `0x0A` 傳送任何數量的其他屬性。一組用於表示屬性名稱和值的字串會在該程式碼的每個執行個體之後立即傳送。環境值是透過這個方法傳遞的。
最後,在傳送完所有屬性之後會傳送屬性終止程式碼 `0xFF`。這表示屬性清單的結尾,也是 Request Packet 的結尾。
供容器傳送回伺服器使用的訊息。
AJP13_SEND_BODY_CHUNK := prefix_code 3 chunk_length (integer) chunk *(byte) chunk_terminator (byte) Ox00 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-Type | 0xA001 |
Content-Language | 0xA002 |
Content-Length | 0xA003 |
Date | 0xA004 |
Last-Modified | 0xA005 |
Location | 0xA006 |
Set-Cookie | 0xA007 |
Set-Cookie2 | 0xA008 |
Servlet-Engine | 0xA009 |
狀態 | 0xA00A |
WWW-Authenticate | 0xA00B |
在程式碼或字串標頭名稱之後,會立即對標頭值進行編碼。
指出此要求處理週期的結束。如果 reuse
旗標為 true(實際 C 程式碼中為 0 以外的數字),此 TCP 連線現在可接受新的要求。如果 reuse
為 false(==0),則應該關閉連線。
容器要求更多來自請求的資料(如果主體過大而無法放入第一個已傳送的封包,或在請求分塊時)。伺服器會根據 request_length
、可傳送最大主體大小(8186(8 KB - 6))和請求主體實際上留下多少位元組要傳送中的最小值,傳送一個有資料量的回應主體封包。
如果主體中沒有更多資料(也就是說,servlet 容器試圖在主體結束之後進行讀取),伺服器會傳送一個空的封包,這是一個具有長度為 0 的有效負載的主體封包。(0x12,0x34,0x00,0x00)