我正在编写一个 HTTP 代理,但无法理解通过 TLS 发出 CONNECT 请求的一些细节。 为了获得更好的了解,我正在尝试使用 Apache 来观察它如何与客户端交互。 这是来自我的默认虚拟主机。
NameVirtualHost *:443
<VirtualHost>
ServerName example.com
DocumentRoot htdocs/example.com
ProxyRequests On
AllowConnect 22
SSLEngine on
SSLCertificateFile /root/ssl/example.com-startssl.pem
SSLCertificateKeyFile /root/ssl/example.com-startssl.key
SSLCertificateChainFile /root/ssl/sub.class1.server.ca.pem
SSLStrictSNIVHostCheck off
</VirtualHost>
Apache 和我的客户之间的对话是这样的。
a.客户端连接到
example.com:443
并在 TLS 握手中发送 example.com
。
b.客户端发送 HTTP 请求。
CONNECT 192.168.1.1:22 HTTP/1.1
Host: example.com
Proxy-Connection: Keep-Alive
c.阿帕奇说
HTTP/1.1 400 Bad Request
。 Apache 错误日志显示
Hostname example.com provided via SNI and hostname 192.168.1.1
provided via HTTP are different.
Apache 似乎不会查看 Host 标头,只是查看它是否存在,因为 HTTP/1.1 需要它。 如果客户端发送
Host: foo
,我会得到相同的失败行为。 如果我在没有 TLS 的情况下向 example.com:80 发出 HTTP 请求,那么 Apache 会将我连接到 192.168.1.1:22。
我不完全理解这种行为。 CONNECT请求有问题吗? 我似乎无法找到 RFC 中解释这一切的相关部分。
不清楚您是否尝试使用 Apache Httpd 作为代理服务器,这可以解释您收到的 400 状态代码。
CONNECT
由客户端使用,并发送到代理服务器(可能是 Apache Httpd,但通常不是),而不是发送到目标 Web 服务器。
CONNECT
在客户端和终端服务器之间建立 TLS 连接之前,在客户端和代理服务器之间使用。客户端 (C) 连接到代理 (P) proxy.example.com
并发送此请求(包括空行):
C->P: CONNECT www.example.com:443 HTTP/1.1
C->P: Host: www.example.com:443
C->P:
代理打开到
www.example.com:443
(P-S) 的 TCP 连接,并使用 200 状态代码响应客户端,接受请求:
P->C: HTTP/1.1 200 OK
P->C:
此后,客户端和代理(C-P)之间的连接保持打开状态。代理服务器中继 C-P 连接上往返于 P-S 的所有内容。客户端通过在该通道上启动 TLS 握手,将其活动 (P-S) 连接升级为 SSL/TLS 连接。由于现在所有内容都转发到服务器,就好像 TLS 交换是直接使用
www.example.com:443
完成的。
代理在握手中不发挥任何作用(因此对于 SNI 而言)。 TLS 握手有效地直接发生在客户端和终端服务器之间。
如果您正在编写代理服务器,则允许客户端连接到 HTTPS 服务器所需要做的就是在
CONNECT
请求中读取,建立从代理到终端服务器的连接(在 CONNECT
中给出)请求),向客户端发送 200 OK
回复,然后将从客户端读取的所有内容转发到服务器,反之亦然。
RFC 2616 将
CONNECT
视为建立简单隧道的一种方法(事实确实如此)。尽管 RFC 2817 的其余部分(在非代理 HTTP 连接内升级到 TLS)很少使用,但 RFC 2817 中有更多相关内容。
看起来您正在尝试做的是通过 TLS 在客户端 (C) 和代理 (P) 之间建立连接。没关系,但客户端不会使用
CONNECT
连接到外部 Web 服务器(除非它也是到 HTTPS 服务器的连接)。
你做的一切都是对的。是 Apache 出了问题。最近才添加了对 CONNECT over TLS 的支持 (https://issues.apache.org/bugzilla/show_bug.cgi?id=29744),但仍有一些问题需要解决。您遇到的问题就是其中之一。
来自 RFC 2616(第 14.23 节):
Host 请求头字段指定 Internet 主机和端口 所请求资源的编号,从原始资源中获取 由用户或引用资源给出的 URI(通常是 HTTP URL, 如第 3.2.2 节所述)。主机字段值必须代表 源服务器或网关的命名权限由 原网址。
我的理解是,您需要将地址从 CONNECT 线复制到 HOST 线。总而言之,资源的地址是 192.168.1.1,并且从 RFC 的角度来看,您通过 example.com 连接的事实不会改变任何内容。
很少在 TLS (https) 中看到 CONNECT 方法。实际上我不认识任何这样做的客户(我很想知道它是谁,因为我认为这实际上是一个很好的功能)。
通常客户端使用http(普通tcp)连接到代理并将CONNECT方法(和主机标头)发送到host:443。然后代理将与端点建立透明连接,然后客户端发送 SSL 握手。
在这种情况下,数据受到“端到端”ssl 保护。
CONNECT 方法并未真正指定,仅在 HTTP RFC 中保留。但通常它非常简单,因此可以互操作。该方法指定主机[:端口]。 Host: 标头可以简单地忽略。可能需要一些额外的代理身份验证标头。当连接主体开始时,代理不再需要进行解析(有些会这样做,因为它们检查有效的 SSL 握手)。