Django:从Apache mod_rewrite重定向时请求语法错误

5 投票
2 回答
3297 浏览
提问于 2025-04-17 20:58

我在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&timestamp=1394042480&nonce=1394079741Content-Length: 445Connection: closeContent-Type: text/html; charset=iso-8859-1 HTTP/1.0')

2 个回答

2

这个信息似乎是在你使用HTTPS链接和Django的时候出现的。你可能还需要在Apache2中配置HTTPS,参考这个问题的内容,比如:使用SSL的虚拟主机与Django和mod_wsgi

7

简而言之:这个问题是由于你使用的“外部客户端”有个bug。这个HTTP客户端设计得很糟糕,应该避免使用,因为它不仅会导致这个问题,还可能带来安全隐患。

为了理解发生了什么,我们需要从后往前分析。


首先,我们来看一下Django内置服务器的日志记录:

[05/Mar/2014 18:01:35] code 400, message Bad request syntax ('GET /checkout/wx_signature?signature=b226bb8f6e9ce2fdecb752c6808a979c62e235f7&echostr=5987526888415258224&timestamp=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头部的名称。ConnectionContent-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-TypeContent-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。


我认为问题显然出在“外部客户端”上,尽管没有足够的信息来深入分析。

我建议你使用不同的客户端或库来进行请求。

撰写回答