Apache 模块 mod_proxy_ajp

Description:mod_proxy的 AJP 支持模块
Status:Extension
Module Identifier:proxy_ajp_module
Source File:mod_proxy_ajp.c
Compatibility:在 2.1 版和更高版本中可用

Summary

该模块需要 mod_proxy的服务。它提供对Apache JServ Protocol version 1.3(以下称为* AJP13 *)的支持。

因此,为了获得处理AJP13协议的能力,服务器中必须存在mod_proxymod_proxy_ajp

Warning

在拥有保护您的服务器之前,不要启用代理。开放式代理服务器对您的网络和整个 Internet 都是危险的。

Usage

此模块用于使用 AJP13 协议将代理反向代理到后端应用程序服务器(例如 Apache Tomcat)。用法类似于 HTTP 反向代理,但使用ajp://前缀:

简单反向代理

ProxyPass "/app" "ajp://backend.example.com:8009/app"

也可以使用平衡器:

平衡器反向代理

<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 请求包括提供给代理的原始主机 Headers,并且可以期望应用程序服务器生成相对于该主机的自引用 Headers,因此无需重写。

主要的 exception 是代理上的 URL 路径与后端上的 URL 路径不同。在这种情况下,可以相对于原始主机 URL(而不是后端ajp:// URL)重写重定向 Headers,例如:

重写代理路径

ProxyPass "/apps/foo" "ajp://backend.example.com:8009/foo"
ProxyPassReverse "/apps/foo" "http://www.example.com/foo"

但是,通常最好将应用程序部署在与代理相同的后端服务器上,而不是采用这种方法。

Environment Variables

名称带有前缀AJP_的环境变量将作为 AJP 请求属性转发到原始服务器(从密钥名称中删除 AJP 前缀)。

协议概述

AJP13协议是面向数据包的。出于性能考虑,大概选择了更具可读性的纯文本格式的二进制格式。 Web 服务器通过 TCP 连接与 Servlet 容器进行通信。为了减少昂贵的套接字创建过程,Web 服务器将尝试维护与 Servlet 容器的持久 TCP 连接,并在多个请求/响应周期中重用连接。

将连接分配给特定请求后,在请求处理周期终止之前,该连接将不再用于其他任何请求。换句话说,请求不在连接上多路复用。尽管这样做确实会导致同时打开更多连接,但是这会使连接两端的代码更加简单。

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

  • Idle
    没有通过该连接处理任何请求。

  • Assigned
    连接正在处理特定请求。

一旦分配了连接以处理特定请求,便会以高度压缩的形式(例如,通用字符串编码为整数)通过连接发送基本请求信息(例如 HTTP Headers 等)。该格式的详细信息在下面的“请求数据包结构”中。如果请求(content-length > 0)包含主体,则该主体将在紧随其后的单独数据包中发送。

至此,servlet 容器可能已准备好开始处理请求。这样,它可以将以下消息发送回 Web 服务器:

  • SEND_HEADERS
    将一组标题发送回浏览器。

  • SEND_BODY_CHUNK
    将大量的身体数据发送回浏览器。

  • GET_BODY_CHUNK
    如果请求尚未全部传输,请从请求中获取更多数据。这是必需的,因为数据包具有固定的最大大小,并且可以在请求的正文中包含任意数量的数据(例如,对于上载的文件)。 (注意:这与 HTTP 分块传输无关)。

  • END_RESPONSE
    完成请求处理周期。

每个消息都带有不同格式的数据包。有关详细信息,请参见下面的响应数据包结构。

基本封包结构

该协议有一些 XDR 传统,但是在很多方面都存在差异(例如,没有 4 字节对齐)。

AJP13 对所有数据类型使用网络字节 Sequences。

协议中有四种数据类型:字节,布尔值,整数和字符串。

  • Byte

    • 一个字节。
  • Boolean

    • 单个字节1 = true0 = false。在某些地方使用其他非零值作为 true(即 C 风格)可能会起作用,但在其他地方则不会。
  • Integer

    • 0 to 2^16 (32768)范围内的数字。存储在 2 个字节中,高位在前。
  • String

    • 可变大小的字符串(长度以 2 ^ 16 为界)。编码时先将其长度打包成两个字节,然后是字符串(包括终止符' 0')。请注意,编码后的长度 包括结尾的“\0”,就像strlen一样。在 Java 方面,这让人有些困惑,因为到处都是奇怪的 autoincrement 语句来跳过这些终止符。我相信这样做的原因是,允许 C 代码在读取 servlet 容器发送回的字符串时更加高效-带有终止符\ 0,C 代码可以将引用传递到单个缓冲区中,而无需复制。如果缺少\ 0,则 C 代码必须将内容复制出来才能获得其字符串概念。

Packet Size

根据大部分代码,最大数据包大小为8 * 1024 bytes (8K)。数据包的实际长度在 Headers 中编码。

Packet Headers

从服务器发送到容器的数据包以0x1234开头。从容器发送到服务器的数据包以AB开头(这是 A 的 ASCII 码,然后是 B 的 ASCII 码)。在前两个字节之后,有一个有效载荷长度的整数(按上面的编码)。尽管这可能表明最大有效载荷可能高达 2 ^ 16,但实际上,代码将最大有效载荷设置为 8K。

数据包格式(服务器->容器)
Byte01234...(n+3)
Contents0x120x34资料长度(n) Data
数据包格式(容器->服务器)
Byte01234...(n+3)
ContentsAB资料长度(n) Data

对于大多数数据包,有效载荷的第一个字节对消息的类型进行编码。从服务器发送到容器的请求主体数据包是一个 exception-它们以标准数据包 Headers(0x1234,然后是数据包的长度)发送,但之后没有任何前缀代码。

Web 服务器可以将以下消息发送到 Servlet 容器:

Code包类型Meaning
2Forward Request使用以下数据开始请求处理周期
7ShutdownWeb 服务器要求容器关闭自身。
8PingWeb 服务器要求容器进行控制(安全登录阶段)。
10CPingWeb 服务器要求容器用 CPong 快速响应。
noneData大小(2 个字节)和相应的正文数据。

为确保某些基本安全性,如果请求来自与其托管的同一台计算机,则该容器实际上只会执行Shutdown

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

Servlet 容器可以将以下类型的消息发送到 Web 服务器:

Code包类型Meaning
3发送身体块将主体的一部分从 servlet 容器发送到 Web 服务器(大概发送到浏览器)。
4Send Headers将响应 Headers 从 servlet 容器发送到 Web 服务器(大概发送到浏览器)。
5End Response标记响应的结束(并因此标记请求处理周期)。
6得到身体块如果请求尚未全部传输,请从请求中获取更多数据。
9CPong Reply对 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,因为它确定容器是否立即查找另一个数据包。

转发请求要素的详细说明

Request prefix

对于所有请求,该值为 2.有关其他前缀代码的详细信息,请参见上文。

Method

HTTP 方法,编码为单个字节:

Command NameCode
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

更高版本的 ajp13,将传输其他方法,即使它们不在此列表中。

协议,req_uri,remote_addr,remote_host,server_name,server_port,is_ssl

这些都是不言自明的。这些都是必需的,并将针对每个请求发送。

Headers

request_headers的结构如下:首先,对 Headersnum_headers的数量进行编码。然后,紧接着一系列标题名称req_header_name /值req_header_value对。通用 Headers 名称编码为整数,以节省空间。如果 Headers 名称不在基本 Headers 列表中,则它会正常编码(作为字符串,带有前缀长度)。通用 Headerssc_req_header_name的列表及其代码如下(均区分大小写):

NameCode valueCode 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',它将使用第二个字节中的整数作为 Headers 名称数组的索引。如果第一个字节不是0xA0,则假定两个字节的整数是字符串的长度,然后将其读入。

这是基于这样的假设,即任何标题名称的长度都不会大于0x9FFF (==0xA000 - 1),这是完全合理的,尽管有些武断。

Note:

content-lengthHeaders 非常重要。如果存在且不为零,则容器假定该请求具有主体(例如 POST 请求),并立即从 Importing 流中读取单独的数据包以获取该主体。

Attributes

带有?(例如?context)前缀的属性都是可选的。对于每个属性,只有一个字节码来指示属性的类型,然后是其值(字符串或整数)。它们可以按任何 Sequences 发送(尽管 C 代码始终按下面列出的 Sequences 发送它们)。发送一个特殊的终止代码来表示可选属性列表的结尾。字节码列表为:

InformationCode Value价值类型Note
?context0x01-目前尚未实施
?servlet_path0x02-目前尚未实施
?remote_user0x03String
?auth_type0x04String
?query_string0x05String
?jvm_route0x06String
?ssl_cert0x07String
?ssl_cipher0x08String
?ssl_session0x09String
?req_attribute0x0AString名称(属性名称如下)
?ssl_key_size0x0BInteger
?secret0x0CString从 2.4.42 开始支持
are_done0xFF-request_terminator

C 代码当前未设置contextservlet_path,大多数 Java 代码完全忽略了为这些字段发送的任何内容(如果在其中一个代码之后发送字符串,则其中的一些实际上会中断)。我不知道这是错误还是未实现的功能,或者仅仅是残留的代码,但是在连接的两边都没有。

remote_userauth_type大概是指 HTTP 级别的身份验证,并传达远程用户的用户名和用于构建其身份的身份验证类型(例如,基本,摘要)。

query_stringssl_certssl_cipherssl_session指的是 HTTP 和 HTTPS 的相应部分。

jvm_route用于支持粘性会话-在存在多个负载平衡服务器的情况下,将用户的 sesson 与特定的 Tomcat 实例相关联。

除了基本属性列表之外,还可以通过req_attribute代码0x0A发送任意数量的其他属性。在该代码的每个实例之后立即发送一对代表属性名称和值的字符串。环境值通过此方法传递。

最后,在发送完所有属性之后,发送属性终止符0xFF。这不仅向属性列表的末尾发出 signal,而且还向请求数据包的结束发出 signal。

响应报文结构

容器可以发送回服务器的消息。

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)

Details:

发送身体块

块基本上是二进制数据,并直接发送回浏览器。

Send Headers

状态代码和消息是常见的 HTTP 内容(例如200OK)。响应头名称的 encodings 与请求头名称的 encodings 相同。有关如何将代码与字符串区分开的详细信息,请参见上面的 header_encoding。
常见 Headers 的代码为:

NameCode value
Content-Type0xA001
Content-Language0xA002
Content-Length0xA003
Date0xA004
Last-Modified0xA005
Location0xA006
Set-Cookie0xA007
Set-Cookie20xA008
Servlet-Engine0xA009
Status0xA00A
WWW-Authenticate0xA00B

在代码或字符串标题名称之后,标题值立即被编码。

End Response

发出此请求处理周期结束的 signal。如果reuse标志为(anything other than 0 in the actual C code),则此 TCP 连接现在可用于处理新的传入请求。如果reuse为 false(== 0),则应关闭连接。

得到身体块

容器从请求中请求更多数据(如果主体太大而无法容纳发送过来的第一个数据包,或者请求被分块时)。服务器将发送回带有一个数据量的主体数据包,该数据量是request_length的最小值,最大发送主体大小(8186 (8 Kbytes - 6))以及从请求主体实际剩余要发送的字节数。
如果主体中没有更多数据(即 servlet 容器尝试读取主体的末尾),则服务器将发回* empty *数据包,该数据包是有效载荷长度为 0 的主体数据包。(0x12,0x34,0x00,0x00)