AJPv13

简介

原始文档由 Dan Milstein 撰写,[email protected],时间为 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(我只包括与 Web 服务器和 servlet 容器之间的通信相关的目标)

  • 提高性能(特别是速度)。
  • 添加对 SSL 的支持,以便 isSecure()getScheme() 可以在 servlet 容器中正常运行。客户端证书和密码套件将作为请求属性提供给 servlet。

协议概述

ajp13 协议是面向数据包的。出于性能原因,可能选择二进制格式而不是更易读的纯文本。Web 服务器通过 TCP 连接与 servlet 容器通信。为了减少昂贵的套接字创建过程,Web 服务器将尝试保持与 servlet 容器的持久 TCP 连接,并对多个请求/响应周期重复使用连接。

一旦连接被分配给特定请求,在请求处理周期终止之前,它将不会用于任何其他请求。换句话说,请求不会通过连接多路复用。这使得连接两端的代码更加简单,尽管它确实导致一次打开更多连接。

一旦 Web 服务器打开与 servlet 容器的连接,该连接可以处于以下状态之一

  • 空闲
    没有请求通过此连接处理。
  • 已分配
    连接正在处理特定请求。

一旦连接被分配来处理特定请求,基本请求信息(例如 HTTP 头等)将以高度浓缩的形式通过连接发送(例如,通用字符串被编码为整数)。该格式的详细信息如下所示在请求数据包结构中。如果请求有正文(内容长度 > 0),则在紧随其后的单独数据包中发送该正文。

此时,servlet 容器可能已准备好开始处理请求。在处理过程中,它可以将以下消息发回 Web 服务器

  • 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 字节)和相应的主体数据。

为了确保一些基本安全性,只有当请求来自其托管的同一台机器时,容器才会实际执行 Shutdown

第一个 Data 数据包在 Web 服务器发出 Forward Request 后立即发送。

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”,因为它决定容器是否立即查找另一个数据包。

转发请求中元素的详细说明。

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
取消签出20
搜索21
创建工作区22
更新23
标签24
合并25
基准控制26
创建活动27

协议、请求 URI、远程地址、远程主机、服务器名称、服务器端口、是否为 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),这是完全合理的,尽管有些武断。(如果你像我一样,开始考虑这里的 cookie 规范以及标头可以有多长,请不要担心——此限制适用于标头名称,而不是标头。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”和“OK”)。响应头名称的编码方式与请求头名称的编码方式相同。有关如何将代码与字符串区分开的详细信息,请参见上方。常见标头的代码为

名称代码值
Content-Type0xA001
内容语言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)。我认为在这种情况下,连接器会在尝试发送任何标头之前失败,但我并不确定。

身份验证呢?Web 服务器和容器之间的连接似乎没有任何身份验证。这让我觉得有潜在危险。