繁体   English   中英

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

[英]“no acceptable variant” from MultiViews in Apache

在基于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

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

简答

当以下所有内容同时为真时,可能会发生此错误:

  • 您的网络服务器已启用多视图
  • 您允许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为我设置Apache的方式,并且我已经启用了MultiViews而没有太多理解它的含义,除了它会让我离开.php离开我的URL的末尾。 您的情况可能相似或相同。

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

  • 请求标头(如AcceptAccept-Language允许客户端指定接收响应的MIME类型或语言,以及为可接受的类型或语言指定加权首选项。 (当然,这些仅在服务器具有或能够根据这些标头生成不同响应时才有用。)例如,每当我加载页面时,Chromium都会为我发送以下标题:

     Accept: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 
  • Apache的mod_negotiation允许您将多个文件(如myresource.html.enmyresource.html.frmyresource.pdf.enmyresource.pdf.fr在同一文件夹中,然后自动使用请求的Accept-*myresource.pdf.fr决定要提供哪些文件当客户端向myresource发送请求时。 有两种方法可以做到这一点。 第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言。 另一个是Multiviews。

  • 启用多视图时...

    在Multiviews

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

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

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

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

您可以轻松地测试此行为。 只需在启用了Multiview的Apache服务器的webroot中创建一个名为test.html的文件,其中包含字符串“Hello World”,然后尝试使用允许HTML响应的Accept标头与不响应HTML响应的标头请求它。 我在我的本地(Ubuntu)机器上用curl演示了这个:

$ 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在决定是否可以提供PHP文件时,如何确定PHP文件的MIME类型? 由于文件将被执行,并且可能会吐出它喜欢的任何Content-Type标头,因此在执行之前不知道该类型。

好吧,默认情况下,答案是MultiViews根本不会提供.php文件。 但是很可能你是按照互联网上很多很多帖子之一的建议(如果我谷歌'php apache multiviews' ,我在第一页上得到4, 明显的是这个问题的OP跟随的那个,因为他实际上评论过它)主张使用AddType标头绕过它,可能看起来像这样:

AddType application/x-httpd-php .php

咦? 为什么这会让Apache非常乐意为.php文件服务呢? 当然浏览器不包括application/x-httpd-php作为他们在Accept头中接受的类型之一?

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

例如,如果我只是在Chromium或Firefox的地址栏中输入一个URL,那么浏览器发送的Accept标头就是Chromium ...

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

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

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

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

如果检查导致406错误的请求的用户代理,您可能会发现它们来自相对不寻常的用户代理。 当我遇到这个错误时,就是当我的<img>元素的src指向一个动态提供图像的PHP脚本(从URL中省略了.php扩展名)时,我首先目睹了BlackBerry用户的失败:

Mozilla/5.0 (BlackBerry; U; BlackBerry 9320; fr) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.714 Mobile Safari/534.11+

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

NegotiatedOnly选项规定,基本名称后面的每个扩展名必须与用于内容协商的已识别mod_mime扩展名相关联,例如Charset,Content-Type,Language或Encoding。 这是最严格的实现,具有最少的意外副作用,并且是默认行为。

但是我们可以使用Any选项获得我们想要的东西:

即使mod_mime无法识别扩展名,您也可以最终允许Any扩展名匹配。

要限制此规则仅更改为.php文件,我们使用<Files>指令,如下所示:

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

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

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

您可以通过添加以下config-snippets来解决此错误:

你的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文件中定义了这个。 所以可以肯定的是,如果这个bug困扰你,只需将RemoveType .php添加到你的配置中。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM