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) OxFFrequest_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)