来自Apache的MultiViews的“无可接受的变体”

时间:2013-05-03 11:31:23

标签: php apache multiviews

在基于PHP的应用程序的一个部署中,Apache的MultiViews选项用于隐藏请求调度程序脚本的.php扩展名。例如。

的请求
/page/about

......将由

处理
/page.php

... PATH_INFO中提供了请求URI的尾部。

大多数时候这种方法很好,但偶尔会导致像

这样的错误
[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page

我的问题是:偶尔触发此错误的原因是什么,以及如何解决问题?

2 个答案:

答案 0 :(得分:9)

简答

如果同时满足以下所有条件,则会发生此错误:

  • 您的网络服务器已启用Multiviews
  • 您允许Multiviews通过使用AddType指令为其分配任意类型来提供PHP文件,很可能使用如下所示的行:

    AddType application/x-httpd-php .php
    
  • 您的客户端浏览器向请求发送Accept标头,其中不包含*/*作为可接受的MIME类型(这非常不寻常,这就是您很少看到错误的原因)
  • 您的MultiviewsMatch指令设置为默认值NegotiatedOnly

您可以通过将以下咒语添加到Apache配置来解决错误:

<Files "*.php">
    MultiviewsMatch Any
</Files>

解释

了解这里发生的事情需要至少对Apache的mod_negotiation和HTTP AcceptAccept-Foo标题的工作情况进行表面的概述。在遇到OP描述的错误之前,我对这两者中的任何一个都一无所知;我mod_negotiation启用了apt-get而不是故意选择,但因为MultiViews为我设置了Apache,我启用了.php而不了解其中的含义除此之外它会让我离开我的网址末尾Accept。您的情况可能相似或相同。

所以这里有一些我不知道的重要基础知识:

  • 请求标头Accept-LanguageAccept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-GB,en-US;q=0.8,en;q=0.6 让客户端指定接收响应的MIME类型或语言,以及指定可接受类型的加权首选项或语言。 (当然,这些仅在服务器具有或能够基于这些标头生成不同响应时才有用。)例如,每当我加载页面时,Chromium都会为我发送以下标题:

    mod_negotiation
  • Apache的myresource.html.en允许您将多个文件存储在同一文件夹中,例如myresource.html.frmyresource.pdf.enmyresource.pdf.frAccept-*然后,当客户端向myresource发送请求时,会自动使用请求的/some/dir/foo标头来决定投放哪个标头。有两种方法可以做到这一点。第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言。另一个是Multiviews。

  • 启用多视图时...

      

    在Multiviews

         

    ...如果服务器收到/some/dir/foo的请求且foo.*不存在,则服务器会读取目录,查找名为Accept的所有文件,并有效地伪造一个键入所有这些文件的地图,为它们分配相同的媒体类型和内容编码,如果客户端通过名称请求其中一个文件。然后,它会根据客户的要求选择最佳匹配,并返回该文档。

这里需要注意的重要一点是,即使启用了Multiview,Apache仍然会尊重Accept标题。与类型映射方法的唯一区别是Apache从文件扩展名中推断出文件的MIME类型,而不是通过在类型映射中明确声明它。

如果存在已收到的URL文件,则会抛出无可接受的变种错误(并发送406响应),但不允许其提供任何服务因为他们的MIME类型与请求的test.html标头中提供的任何可能性都不匹配。 (例如,如果没有可接受语言的变体,也会发生同样的事情。)这符合HTTP规范,其中规定:

  

如果存在Accept头字段,并且服务器无法根据组合的Accept字段值发送可接受的响应,则服务器应该发送406(不可接受)响应。

您可以轻松地测试此行为。只需创建一个名为curl的文件,其中包含字符串&#34; Hello World&#34;在启用了Multiview的Apache服务器的webroot中,然后尝试使用允许HTML响应的Accept标头与不响应的标头请求它。我在我的本地(Ubuntu)机器上用$ curl --header "Accept: text/html" localhost/test Hello World $ curl --header "Accept: image/png" localhost/test <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate representation of the requested resource /test could not be found on this server.</p> Available variants: <ul> <li><a href="test.html">test.html</a> , type text/html</li> </ul> <hr> <address>Apache/2.4.6 (Ubuntu) Server at localhost Port 80</address> </body></html>

来演示这一点
mod_negotiate

这给我们带来了一个我们尚未解决的问题:Content-Type在决定是否可以提供PHP文件时,如何确定PHP文件的MIME类型?由于文件将被执行,并且可以吐出它喜欢的任何.php标题,因此在执行之前不知道该类型。

嗯,默认情况下,答案是MultiViews只是不会提供AddType application/x-httpd-php .php 个文件。但有可能你是按照互联网上很多很多帖子之一的建议(如果我是谷歌'php apache multiviews',我在第一页上有4个,top one显然是这个的OP问题随之而来,因为他实际上评论过它)主张使用AddType标题解决这个问题,可能看起来像这样:

.php

咦?为什么这会让Apache非常乐意提供application/x-httpd-php个文件?当然浏览器不包括Accept作为他们*/*标题中接受的类型之一?

嗯,不完全是。但所有主要版本都包含Accept(因此允许任何MIME类型的响应 - 他们仅使用mod_negotiation标头表示偏好权重,限制他们接受的类型。)这会导致.php愿意选择并提供Accept文件,只要有一些MIME类型 - 任何一个! - 与他们有关。

例如,如果我只是在Chromium或Firefox的地址栏中键入一个网址,那么浏览器发送的Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 标题就是Chromium ...

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

......以及Firefox的情况:

*/*

这两个标头都包含*/*作为可接受的内容类型,因此允许服务器提供其喜欢的任何内容类型的文件。但是一些不那么流行的浏览器接受<script> - 或者可能只包含它用于页面请求,而不是在加载<img>或{{1}的内容时你也可能通过PHP提供的标签 - 以及我们的问题来自哪里。

如果您检查导致406错误的请求的用户代理,您可能会发现他们来自相对不寻常的用户代理。当我遇到此错误时,就是当src <img>.php元素指向动态提供图像的PHP脚本时(URL中省略了Mozilla/5.0 (BlackBerry; U; BlackBerry 9320; fr) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.714 Mobile Safari/534.11+ 扩展名),以及我第一次目睹BlackBerry用户失败了:

mod_negotiate

为了解决这个问题,我们需要让Accept: */*通过一些方式提供PHP脚本,而不是给它们任意类型,然后依靠浏览器发送MultiviewsMatch标题。为此,我们使用Accept指令指定多视图可以为PHP文件提供服务,无论它们是否与请求的NegotiatedOnly标头匹配。默认选项为NegotiatedOnly

  

Any选项规定,基本名称后面的每个扩展名都必须与用于内容协商的已识别mod_mime扩展程序相关联,例如:字符集,内容类型,语言或编码。这是最严格的实现,具有最少的意外副作用,并且是默认行为。

但我们可以使用Any选项获得我们想要的内容:

  

即使mod_mime无法识别扩展程序,您最终也可以允许.php个扩展程序匹配。

要将此规则更改为仅限<Files "*.php"> MultiviewsMatch Any </Files> 个文件,我们使用<Files>指令,如下所示:

{{1}}

随着这一微小(但难以弄清楚)的改变,我们已经完成了!

答案 1 :(得分:3)

马克·阿梅里给出的答案几乎已经完成,但它缺少最佳位置,并没有解决“请求中没有给出延期,因此协商失败的替代方案。”

您可以通过添加以下配置片段来解决此错误:

你的PHP配置应该是这样的:

<FilesMatch "\.ph(p3?|tml)$">
    SetHandler application/x-httpd-php
</FilesMatch>

请勿使用AddType application/x-httpd-php .php或任何其他AddType

您的附加配置应该是这样的:

RemoveType .php
<Files "*.php">
    MultiviewsMatch Any
</Files>

如果您使用AddType,您将收到如下错误:

GET /index/123/434 HTTP/1.1
Host: test.net
Accept: image/*

HTTP/1.1 406 Not Acceptable
Date: Tue, 15 Jul 2014 13:08:27 GMT
Server: Apache
Alternates: {"index.php" 1 {type application/x-httpd-php}}
Vary: Accept-Encoding
Content-Length: 427
Connection: close
Content-Type: text/html; charset=iso-8859-1

正如您所看到的,它确实找到了index.php,但它没有使用此替代方法,因为它无法将Accept: image/*application/x-httpd-php匹配。如果您请求/index.php/1/2/3/4,则可以正常使用。

我在mod_negotiation模块的源代码中找到了这个的原因。我试图找出为什么如果.php类型是'cgi'而Apache不会工作的原因(提示:application/x-httpd-cgi是硬编码的......)。在源代码中,我注意到如果该文件的Content-Type与Accept标头匹配,或者该文件的Content-Type为空,则apache只会将该文件视为匹配。

如果使用SetHandler而不是apache,则不会将.php文件看作application/x-httpd-php,但不幸的是,许多发行版也在/etc/mime.types文件中定义了这个。所以可以肯定的是,如果这个错误困扰你,只需将RemoveType .php添加到你的配置中。