Apache HTTP 服务器版本 2.4
描述 | 用于 mod_proxy 的 AJP 支持模块 |
---|---|
状态 | 扩展 |
模块标识符 | proxy_ajp_module |
源文件 | mod_proxy_ajp.c |
兼容性 | 在版本 2.1 及更高版本中可用 |
此模块需要 mod_proxy
的服务。它提供对Apache JServ 协议版本 1.3
(以下简称AJP13)的支持。
因此,为了获得处理AJP13
协议的能力,mod_proxy
和 mod_proxy_ajp
必须存在于服务器中。
在您保护服务器之前,请勿启用代理。开放代理服务器对您的网络和整个互联网都构成危险。
此模块用于使用 AJP13 协议反向代理到后端应用程序服务器(例如 Apache Tomcat)。用法类似于 HTTP 反向代理,但使用ajp://
前缀
ProxyPass "/app" "ajp://backend.example.com:8009/app"
诸如 Tomcat 的secret
选项(从 Tomcat 8.5.51 和 9.0.31 开始默认需要)之类的选项可以作为单独的参数添加到 ProxyPass
或 BalancerMember
的末尾。此参数在 Apache HTTP 服务器 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 路径不同。在这种情况下,可以相对于原始主机 URL(而不是后端ajp://
URL)重写重定向标头,例如
ProxyPass "/apps/foo" "ajp://backend.example.com:8009/foo" ProxyPassReverse "/apps/foo" "http://www.example.com/foo"
但是,通常最好将应用程序部署在后端服务器上与代理相同的路径上,而不是采用这种方法。
名称带有前缀AJP_
的环境变量将作为 AJP 请求属性转发到源服务器(从键的名称中删除AJP_
前缀)。
AJP13
协议是面向数据包的。出于性能原因,二进制格式可能比更易读的纯文本格式更受欢迎。Web 服务器通过 TCP 连接与 Servlet 容器通信。为了减少昂贵的套接字创建过程,Web 服务器将尝试维护与 Servlet 容器的持久 TCP 连接,并为多个请求/响应周期重用连接。
一旦将连接分配给特定请求,它将不会用于任何其他请求,直到请求处理周期终止。换句话说,请求不会在连接上进行多路复用。这使得连接两端的代码变得更加简单,尽管它会导致一次打开更多连接。
一旦 Web 服务器打开了与 Servlet 容器的连接,连接就可以处于以下状态之一
一旦将连接分配给处理特定请求,基本请求信息(例如 HTTP 标头等)将以高度压缩的形式通过连接发送(例如,公共字符串被编码为整数)。该格式的详细信息在下面的请求数据包结构中给出。如果请求有正文(content-length > 0)
,则该正文将在紧随其后的单独数据包中发送。
此时,Servlet 容器可能已准备好开始处理请求。在处理请求时,它可以将以下消息发送回 Web 服务器
每条消息都附带一个格式不同的数据包。有关详细信息,请参见下面的响应数据包结构。
此协议有一些 XDR 遗产,但它在很多方面有所不同(例如,没有 4 字节对齐)。
AJP13 对所有数据类型使用网络字节序。
协议中有四种数据类型:字节、布尔值、整数和字符串。
1 = true
,0 = false
。使用其他非零值作为 true(即 C 样式)可能在某些地方有效,但在其他地方无效。0 到 2^16 (32768)
之间的数字。存储在 2 个字节中,高位字节在前。strlen
。这在 Java 方面有点令人困惑,Java 中充斥着奇怪的自动递增语句来跳过这些终止符。我相信这样做是为了让 C 代码在读取 Servlet 容器发送回来的字符串时更加高效 - 由于存在终止符 \0 字符,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
然后是数据包的长度)发送,但之后没有任何前缀代码。
Web 服务器可以将以下消息发送到 Servlet 容器
代码 | 数据包类型 | 含义 |
2 | 转发请求 | 使用以下数据开始请求处理周期 |
7 | 关闭 | Web 服务器要求容器关闭自身。 |
8 | Ping | Web 服务器要求容器接管(安全登录阶段)。 |
10 | CPing | Web 服务器要求容器快速响应 CPong。 |
无 | 数据 | 大小(2 个字节)和相应正文数据。 |
为了确保基本安全性,容器只会从其托管的同一台机器接收请求时才会实际执行Shutdown
。
第一个Data
数据包在 Web 服务器发送Forward Request
之后立即发送。
Servlet 容器可以将以下类型的消息发送到 Web 服务器
代码 | 数据包类型 | 含义 |
3 | 发送正文块 | 将 Servlet 容器到 Web 服务器(以及浏览器)的正文块发送。 |
4 | 发送标头 | 将 Servlet 容器到 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
,因为它决定了容器是否立即查找另一个数据包。
对于所有请求,这将是 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 |
连接 | 0xA006 | SC_REQ_CONNECTION |
内容类型 | 0xA007 | SC_REQ_CONTENT_TYPE |
内容长度 | 0xA008 | SC_REQ_CONTENT_LENGTH |
cookie | 0xA009 | SC_REQ_COOKIE |
cookie2 | 0xA00A | SC_REQ_COOKIE2 |
主机 | 0xA00B | SC_REQ_HOST |
pragma | 0xA00C | SC_REQ_PRAGMA |
引用 | 0xA00D | SC_REQ_REFERER |
用户代理 | 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 | - | 请求终止符 |
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 实例关联起来。
当在 ProxyPass
或 BalancerMember
指令中使用 secret=secret_keyword
参数时,会发送 secret
。后端需要支持 secret,并且值必须匹配。request.secret
或 requiredSecret
在 Apache Tomcat 的 AJP 配置中有所说明。
除了这些基本属性列表之外,还可以通过 req_attribute
代码 0x0A
发送任意数量的其他属性。在每个代码实例之后,会立即发送一对字符串来表示属性名称和值。环境值通过这种方式传递。
最后,在发送完所有属性之后,会发送属性终止符 0xFF
。这表示属性列表的结束,也表示请求数据包的结束。
用于容器可以发送回服务器的消息。
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
)。响应头名称的编码方式与请求头名称相同。有关代码如何与字符串区分的详细信息,请参见上面的 header_encoding。
常见头的代码如下:
名称 | 代码值 |
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
标志为真 (在实际的 C 代码中,除了 0 之外的任何值)
,则此 TCP 连接现在可以用于处理新的传入请求。如果 reuse
为假 (==0),则应关闭连接。
容器请求更多请求数据(如果主体太大而无法放入发送的第一个数据包中,或者当请求被分块时)。服务器将发送一个主体数据包,其中包含的数据量是 request_length
、最大发送主体大小 (8186 (8 Kbytes - 6))
和请求主体中实际剩余的字节数的最小值。
如果主体中没有更多数据(即 servlet 容器试图读取主体末尾之后的数据),服务器将发送一个空数据包,这是一个有效载荷长度为 0 的主体数据包。(0x12,0x34,0x00,0x00)