|
这是一个关于软件复杂性的小故事。
曾经,我碰到了 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 不匹配。最常见的两种原因是:
如果一台机器在多个域名(比如,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 不匹配原因:
我们发现,原因在于 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 |