Apache中的多视图“无可接受的变体”

k75qkfdt  于 2022-12-19  发布在  Apache
关注(0)|答案(2)|浏览(144)

在一个基于PHP的应用程序的部署中,Apache的MultiViews选项被用来隐藏请求分派器脚本的. php扩展名。

/page/about

...将由

/page.php

...请求URI的尾部在PATH_INFO中可用。
大多数情况下,这可以正常工作,但偶尔会导致以下错误

[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page

我的问题是:是什么原因偶尔会触发此错误?如何修复此问题?

ujv3wf0j

ujv3wf0j1#

简短回答

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

  • 您的Web服务器已启用多视图
  • 通过使用AddType指令为PHP文件指定任意类型,您将允许“多视图”为PHP文件提供服务,最常见的方式是使用如下一行:
AddType application/x-httpd-php .php
  • 您的客户端浏览器随请求发送的Accept标头不包括*/*作为可接受的MIME类型(这是非常不寻常的,这就是为什么您很少看到该错误)。
  • 您已将MultiviewsMatch指令设置为其默认值NegotiatedOnly

您可以通过在Apache配置中添加以下内容来解决该错误:

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

解释

要理解这里发生的事情,至少需要对Apache的mod_negotiation和HTTP的AcceptAccept-Foo头的工作原理有一个粗略的了解,在遇到OP描述的bug之前,我对这两个头一无所知;我启用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发送请求时提供哪个服务。第一种方法是在同一个文件夹中创建一个类型Map文件,显式声明每个可用文档的MIME类型和语言。
  • 启用多视图时...

多视图

...如果服务器接收到对/some/dir/foo的请求并且/some/dir/foo不存在,则服务器读取目录以查找名为foo.*的所有文件,并且有效地伪造命名所有这些文件的类型Map,向它们分配如果客户端通过名称请求它们中的一个则将具有的相同媒体类型和内容编码。然后,它选择与客户机需求最匹配的文档,并返回该文档。
这里需要注意的重要一点是,即使启用了Multiviews,Apache仍然会使用Accept头;与类型Map方法的唯一区别是Apache从文件扩展名推断文件的MIME类型,而不是通过在类型Map中显式声明。
抛出 no acceptable variant 错误(并发送406响应),但不允许服务其中任何一个,因为它们的MIME类型与请求的Accept头中提供的任何可能性都不匹配。(例如,如果在可接受的语言中没有变体,也会发生同样的事情。)这符合HTTP规范,该规范规定:
如果存在接受报头字段,并且如果服务器不能发送根据组合的接受字段值可接受的响应,则服务器应当发送406(不可接受)响应。
您可以很容易地测试这种行为。只需在启用了Multiviews的Apache服务器的webroot中创建一个名为test.html的文件,其中包含字符串“Hello World”,然后尝试使用Accept头请求该文件,Accept头允许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文件时,如何确定该文件的MIME类型?由于该文件将被执行,并且可以输出它喜欢的任何Content-Type标头,因此在执行之前不知道该类型。
默认情况下,答案是“多视图”根本不支持.php文件,但您可能会听从互联网上许多帖子中的某个建议(如果我谷歌'php apache multiviews',我在第一页得到4,top one显然是这个问题的OP所遵循的,因为他实际上对它进行了评论),主张使用AddType指令来解决这个问题,可能看起来像这样:

AddType application/x-httpd-php .php

哈?为什么这会神奇地使Apache乐于提供.php文件?浏览器肯定不会在Accept头文件中包含application/x-httpd-php作为它们接受的类型之一吧?

也不完全是。但是所有主要的都包括*/*(因此允许任何MIME类型的响应-他们使用Accept报头仅用于表示偏好权重,not 用于限制它们将接受的类型。)这使得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扩展名),我第一次看到它在黑莓用户中失败:

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指令来指定多视图可以提供PHP文件,而不管它们是否与请求的Accept头匹配。默认选项为NegotiatedOnly
NegotiatedOnly选项规定基名称后面的每个扩展名必须与可识别的mod_mime扩展名相关联,以便进行内容协商,例如Charset、Content-Type、Language或Encoding。这是最严格的实现,意外副作用最少,也是默认行为。
但是我们可以通过Any选项得到我们想要的:
您最终可能会允许Any扩展名匹配,即使mod_mime无法识别该扩展名。
为了将此规则更改仅限于.php文件,我们使用<Files>指令,如下所示:

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

有了这个微小的(但很难弄清楚的)变化,我们就完成了!

k97glaaz

k97glaaz2#

马克·阿梅里给出的答案几乎是完整的,但它错过了甜蜜点,没有解决"请求中没有给出延期,因此与替代方案的谈判失败。
您可以通过添加以下配置片段来解决此错误:
您的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
我在mod_negotiation模块的源代码中找到了这个原因,我试图找出为什么Apache在.php类型为'cgi'时可以工作,而在其他情况下则不行(提示:application/x-httpd-cgi是硬编码的......)。在源代码中,我注意到只有当文件的内容类型与Accept头匹配,或者文件的内容类型为空时,apache才会将文件视为匹配。
如果你使用SetHandler,apache就不会看到.php文件为application/x-httpd-php,但不幸的是,许多发行版也在/etc/mime.types文件中定义了application/x-httpd-php。所以,如果这个bug困扰着你,你可以在配置文件中添加RemoveType .php

相关问题