Django:从Apache mod_rewrite重定向时请求语法错误
我在8000端口上运行Django,而在80端口上运行Apache。我在Apache中配置了一个重写规则,用来把请求转发到Django:
RewriteRule ^/?checkout/ http://%{HTTP_HOST}:8000/checkout/ [L,QSA]
当我在浏览器中打开一个网址时,一切正常,重定向也很顺利。
但是,当外部客户端连接到Django时(如果直接连接Django是没问题的),总是会在Django服务器上出现“错误的请求语法”的错误。这里是Django日志中的一段信息。看起来Apache自动在请求中添加了一些“内容长度”的东西,为什么会这样呢?
[05/Mar/2014 18:01:35] code 400, message Bad request syntax ('GET /checkout/wx_signature?signature=b226bb8f6e9ce2fdecb752c6808a979c62e235f7&echostr=5987526888415258224×tamp=1394042480&nonce=1394079741Content-Length: 445Connection: closeContent-Type: text/html; charset=iso-8859-1 HTTP/1.0')
2 个回答
这个信息似乎是在你使用HTTPS链接和Django的时候出现的。你可能还需要在Apache2中配置HTTPS,参考这个问题的内容,比如:使用SSL的虚拟主机与Django和mod_wsgi
简而言之:这个问题是由于你使用的“外部客户端”有个bug。这个HTTP客户端设计得很糟糕,应该避免使用,因为它不仅会导致这个问题,还可能带来安全隐患。
为了理解发生了什么,我们需要从后往前分析。
首先,我们来看一下Django内置服务器的日志记录:
[05/Mar/2014 18:01:35] code 400, message Bad request syntax ('GET /checkout/wx_signature?signature=b226bb8f6e9ce2fdecb752c6808a979c62e235f7&echostr=5987526888415258224×tamp=1394042480&nonce=1394079741Content-Length: 445Connection: closeContent-Type: text/html; charset=iso-8859-1 HTTP/1.0')
“代码400”指的是HTTP状态码400。这意味着实际的HTTP请求构造得很糟糕,服务器无法理解。幸运的是,Django会记录错误的输入,这样我们就可以进行分析。
现在我们了解了问题的本质,接下来我们去掉一些无关的日志信息,专注于实际的请求:
GET /checkout/wx_signature?[SIGNATURE REMOVED]Content-Length: 445Connection: closeContent-Type: text/html; charset=iso-8859-1 HTTP/1.0
在这里我们看到一个无效的HTTP请求的第一行。
根据RFC2616第5.1节:
请求行以一个方法标记开始,接着是请求的URI和协议版本,最后以CRLF结束。各个部分之间用空格分隔。除了最后的CRLF序列外,不允许有CR或LF。
Request-Line = Method SP Request-URI SP HTTP-Version CRLF
在这个无效的请求中,我们可以看到HTTP动词GET
和版本HTTP/1.0
都是存在的,所以这些不是问题。中间部分应该是URL,内容如下:
/checkout/wx_signature?[SIGNATURE REMOVED]Content-Length: 445Connection: closeContent-Type: text/html; charset=iso-8859-1
在URL中的空格通常会被替换为+
或%20
,然后再发送到服务器。但在这里并没有这样处理,这就是导致请求无效的原因。一个好的HTTP客户端绝对不会这样做,因为它会自动处理URL中的特殊字符。这表明你使用的“外部客户端”质量很差。
注意到空格旁边还有一些看起来很奇怪的字段。
如果你查看RFC2616第14.13节,你会发现Content-Length
实际上是HTTP 1.1头部的名称。Connection
和Content-Type
也是如此。
这些显然不应该出现在这里,那么为什么它们会和URL连接在一起呢?
在这里我只能猜测,因为我无法访问你的代码。不过,我觉得我对发生了什么有个不错的想法。
让我们先了解一下HTTP头部的性质。我们将发送一个原始请求,模拟访问“http://google.com”时发生的事情。这会触发Google将我们重定向到“http://www.google.com”。
原始请求:
GET / HTTP/1.1
Host: google.com
原始响应:
HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Thu, 15 May 2014 21:28:46 GMT
Expires: Sat, 14 Jun 2014 21:28:46 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 219
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Alternate-Protocol: 80:quic
[HTML content removed]
哇,Google返回了一大堆头部信息!不过我们只关心前几行:
HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
...
Content-Length: 219
在这里你可以看到Content-Type
、Content-Length
和其他头部跟在Location
头部后面。通常情况下,实际的顺序并不重要,因为HTTP客户端或服务器足够聪明,能够理解每个头部的意思。但是,如果你在Location
头部后面去掉了行结束符呢?
你会得到这样的结果:
HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/Content-Type: text/html; charset=UTF-8Content-Length: 219
糟糕……如果你是一个HTTP客户端,你会认为我想把你重定向到http://www.google.com/Content-Type: text/html; charset=UTF-8Content-Length: 219
。
这看起来正是你遇到的问题……但为什么会这样呢?
很不太可能是Apache以这种损坏的形式返回了头部(除非你自己编写了一个插件之类的东西)。
同样也不太可能是你的“外部客户端”故意去掉了接收到的头部中的行结束符。
一个可能的情况是,“外部客户端”被编写成将内容之前和Location:
之后的所有内容都视为URL,并在之后的某个地方去掉了CRLF字符(这通常是出于安全原因在处理HTTP头部时进行的,讽刺的是在这个案例中做得不对)。客户端尝试使用HTTP/1.0而不是HTTP/1.1发送请求这一点也支持了这一点,因为HTTP/1.0客户端通常在功能上非常有限,并且往往基于过时的知识做出重大的假设。
你的“外部客户端”很可能在请求行之后将整个头部读取到一个字符串中,而字符串处理程序自动去掉了CRLF。
我认为问题显然出在“外部客户端”上,尽管没有足够的信息来深入分析。
我建议你使用不同的客户端或库来进行请求。