Fantix 的开源小站
[翻译] 502 Bad Gateway Error 打印 E-mail

这是一个关于软件复杂性的小故事。

曾经,我碰到了 Subversion 的一个问题:使用 svn move 或是 svn copy 的时候,会出现下面这样的问题:

svn: Commit failed (details follow):
svn: COPY of newname.txt: 502 Bad Gateway (https://contrib.exoweb.net)


我推测,是那些 svn status 有小加号“+”的文件在作怪:

% svn move oldname.txt newname.txt
% svn status
D      oldname.txt
A    + newname.txt


小加号意味着,添加文件的操作其实是一个 COPY 操作,原始文件 oldname.txt 的历史会被一起拷贝入新添加的文件。


绕过这个问题

这个问题可以绕过去——只做添加操作,不做拷贝的操作。

svn revert newname.txt
svn add newname.txt


然而,这会丢失文件的历史信息。

WebDAV

好了,到底是什么问题呢?说到底,问题在于 mod_ssl。但是他们之间的联系非常的隐蔽,所以让我们一步一步得来吧。

首先,我们得找出来,到底是什么产生了“502 Bad Gateway”错误。我们的 Subversion 仓库使用了 HTTP 作为数据传输协议,或者换句话说,实际上用了更安全的 HTTPS。您甚至可以在仓库 URL 中看到协议名:https://contrib.exoweb.net/svn/repos(使用 SSH 作为数据传输协议是另外一种方案,URL 就会是:svn+ssh://contrib.exoweb.net/svn/repos,但我们不深究这个了)。确切地说,Subversion 使用了 HTTP 的一个扩展,叫做 WebDAV。

从技术上说,Subversion 的移动和复制操作,都转换成了 WebDAV 的复制(COPY)操作。请求看上去会是这样:

COPY /svn/repos/oldname.txt HTTP/1.1
Host: contrib.exoweb.net
Destination: https://
contrib.exoweb.net/svn/repos/newname.txt

不过,实际上的请求要复杂得多,因为真正的请求是用 XML 写的,并且 Subversion 把许多请求分成一个组,放到一个 HTTP 请求中,变成了像 COPY /svn/repos/!svn/act/6b2389f7-9223-4eca-b3fa-96cb3da83b78 HTTP/1.1 这样的东西。不过“你得到了大概的想法”(注:英式中文,源自 you get it ——你得到了它)。

Apache——我们的 web 服务器——会把上面的请求转换成:“将 https://contrib.exoweb.net/svn/repos/oldname.txt 拷贝到 https://contrib.exoweb.net/svn/repos/newname.txt”,用 Host 参数的值 contrib.exoweb.net,以及请求中的第一行 COPY /svn/repos/oldname.txt HTTP/1.1,来配出源文件的 URL——https://contrib.exoweb.net/svn/repos/oldname.txt。然而,在有些情况下,配源文件 URL 的过程会出错,比如错配成了 http://contrib.exoweb.net/svn/repos/oldname.txt(没有 s 了——应该是 https,错成 http 了)。Apache 很快认识到,它没法把 http://contrib.exoweb.net/svn/repos/oldname.txt 移动到 https://contrib.exoweb.net/svn/repos/newname.txt,因为只要 Apache 还有一口气,它就会把 http://contrib.exoweb.net 和 https://contrib.exoweb.net 当成完全不同的两个主机。于是,它会响应一个“502 Bad Gateway” 错误。

不匹配的源 URL 和目标 URL

在做 HTTP COPY 的时候,有很多种原因,都会造成 Apache 的转换错误,或者源 URL 与目标 URL 不匹配。最常见的两种原因是:

  • 多个服务器别名
  • 使用了 HTTP 反转代理服务器

如果一台机器在多个域名(比如,www.exoweb.net 和 contrib.exoweb.net)上监听,但在 Apache 配置文件中,它们中只有一个配置上了——比如 ServerName www.exoweb.net,那么 Apache 就会认为这是两台不匹配的服务器。解决的办法很简单,把另外一个域名配置成 ServerAlias 就好了:

ServerName www.exoweb.net
ServerAlias contrib.exoweb.net

另一种常见的原因,是在您的 HTTP 服务器前端,使用了 HTTPS 反转代理服务器。代理服务器会先接下请求,经过转换(包括转换 hostname),将其转发给后端的 HTTP 服务器。可是,因为代理服务器不会转换目标域,后端的 HTTP 服务器仍然看到的是 HTTPS 地址,然后就返回了“502 Bad Gateway”错误。

请参考 Subversion behind an Apache Reverse Proxy 来处理这种问题。

mod_ssl 曰,443 等于 80

经过一番挣扎,我们发现了第三个、不是那么常见的、造成 HTTP COPY 时源 URL 与目标 URL 不匹配原因:

  • 虚拟 HTTPS 主机配置错了

我们发现,原因在于 mod_ssl。不知怎么地,我们的 Apache 服务器相信,它在 80 端口使用 HTTP 通讯,尽管实际上应该是在 443 端口使用 HTTPS 通讯。没错,这挺诡异的,但是您可以用这一小段 Apache 配置,亲自重现这个问题:

# Bad configuration of Apache
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule info_module modules/mod_info.so

User www
Group www

DocumentRoot "/var/www"

<Location /server-info>
SetHandler server-info
</Location>

ErrorLog /var/log/httpd/error_log

Listen *:443

NameVirtualHost *:443
<VirtualHost *:443>
ServerName www.exoweb.net
SSLEngine on
SSLCertificateFile /usr/local/apache2/conf/ssl/servercert.pem
SSLCertificateKeyFile /usr/local/apache2/conf/ssl/serverkey.pem
</VirtualHost>

<VirtualHost *:443>
ServerName contrib.exoweb.net
</VirtualHost>


这段代码会在一台服务器上配置两个基于域名的“虚拟”主机:www.exoweb.net 和 contrib.exoweb.net。它们都监听同一个 IP 地址的 443 端口(HTTPS 使用的默认端口)。

另外,我们配置了 server-info 模块,所以如果您访问 https://www.exoweb.net/server-info 或者 https://contrib.exoweb.net/server-info,您将看到一张关于服务器配置的页面。您会注意到它们写道:

URL servername/port reported by Apache
https://www.exoweb.net/server-info www.exoweb.net:443
https://contrib.exoweb.net/server-info contrib.exoweb.net:80

看来 Apache 对于您访问的 https://contrib.exoweb.net/server-info 犯迷糊了:Apache 认为您访问的是 80 端口,但实际上却是 443 端口。上面的配置文件导致了这个问题,如果您换用下面的配置文件,问题就不复存在了:

# Good configuration of Apache
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule info_module modules/mod_info.so

User www
Group www

DocumentRoot "/var/www"

<Location /server-info>
SetHandler server-info
</Location>

ErrorLog /var/log/httpd/error_log

Listen *:443

SSLCertificateFile /usr/local/apache2/conf/ssl/servercert.pem
SSLCertificateKeyFile /usr/local/apache2/conf/ssl/serverkey.pem

NameVirtualHost *:443
<VirtualHost *:443>
ServerName www.exoweb.net
SSLEngine on
</VirtualHost>

<VirtualHost *:443>
ServerName contrib.exoweb.net
SSLEngine on
</VirtualHost>


欲知更多,请见 Apache 的问题报告 #42929

mod_ssl 处理 SSL 参数

您可能会想,mod_ssl 为啥这么做人。一方面来说,配置文件中第二个虚拟主机是“错的”——它没有 SSLEngine on 指令。那为什么我访问 https://contrib.exoweb.net 的时候,Apache 服务器还启用了 SSL 呢?原因在于 mod_ssl 忽略了虚拟主机定义的事实!乍一看或许诡异,但您会发现,其实 SSL 握手发生在客户端能发送 HTTP 头信息 Host 之前,也就是在能判断是哪一个虚拟主机之前。具体来说,一组“IP 地址:端口”只能用一份证书,前面的例子中,唯一的一组“IP 地址:端口”就是“*:443”。于是 mod_ssl 就简单地发现了第一个 IP 地址和端口匹配的虚拟主机,然后使用了它的 SSL 配置。所以实际上,例子中的所有虚拟主机都共享了这一份 SSL 配置:

SSLEngine on
SSLCertificateFile /usr/local/apache2/conf/ssl/servercert.pem
SSLCertificateKeyFile /usr/local/apache2/conf/ssl/serverkey.pem


使用 SSL 的虚拟主机

好了,精明的您可能会问,到底怎么在基于域名的虚拟主机(www.exoweb.net 和 contrib.exoweb.net,它们都监听同一个 IP 地址上的同一个端口)上部署 SSL 呢?毕竟,SSL 证书都有一个 CommonName(CN) 域——标识主机名称是 SSL 证书的特征之一。这个域的值要么是 www.exoweb.net,要么是 contrib.exoweb.net,反正不会两个都是。所以如果我的浏览器在打开 https://contrib.exoweb.net 时,得到了一张 CN 域是 www.exoweb.net 的证书,那么它会大声抱怨的。这就对了。

有两种办法可以绕过这个限制——一组 IP 地址:端口只能有一个主机名的限制。第一种,在您的证书中添加 SubjectAltName 域;第二种,在握手的时候商议用哪个证书的问题。

SubjectAltName 是一种支持相当好的方案,只可惜不是特别灵活,因为每次给一组 IP 地址:端口添加新的虚拟主机时,都需要创建新的证书。不管怎么说,SubjectAltName 的支持都是相当好。要创建带 SubjectAltName 的证书,先把下面的证书配置文件存到 ~/servercert.conf,然后执行这一句 openssl 命令:

openssl req -new -x509 -nodes -config ~/servercert.conf \
-out /usr/local/apache2/conf/ssl/servercert.pem \
-keyout /usr/local/apache2/conf/ssl/serverkey.pem

#
# www.exoweb.net certificate configuration file.
#

[ req ]
default_bits = 2048
default_days = 365
default_keyfile = serverkey.pem
prompt = no
encrypt_key = yes
distinguished_name = req_distinguished_name
x509_extensions = req_x509v3_ext

[ req_distinguished_name ]
countryName = CN
stateOrProvinceName = Beijing
localityName = Beijing
organizationName = Exoweb, ltd.
organizationalUnitName = Exoweb
commonName = www.exoweb.net

[ req_x509v3_ext ]
nsCertType = server
subjectAltName = DNS:www.exoweb.net,DNS:contrib.exoweb.net


而最近的一项提议是,在握手的时候商议用哪个证书的问题。但只有 TLS 握手支持这种方式,SSL 却不行。常规的做法叫做 Server Name Indication(SNI)。写着篇文章的时候(译注:非翻译的时候),SNI 还没有得到广泛的支持,尽管不乏有到来之势。

欲知更多,请见 Name-based SSL virtual hosts: how to tackle the problem by Kaspar Brand

好了,既然您都看到最后了,您肯定已经发现,Apache 的配置导致了 Subversion 中的错误。全都说通了。

原文地址:http://www.science.uva.nl/research/air/wiki/Subversion502BadGateway