PHP中的访问日志记录


Access logging in PHP

我想记录对/files文件夹中任何文件的访问,这样我就可以用PHP处理它来生成一些统计信息。

我不想编写通过RewriteRule调用的自定义PHP处理程序,因为我不想处理状态代码、MIME类型和缓存头以及文件锁定问题。

我没有访问服务器配置的权限,所以我不能使用CustomLog(我确实有访问.htacess的权限)。

我无法使用X-Sendfile,因为它未启用。

我没有访问access.log的权限。


寻找一个权威的答案。

这是您在那里设置的相当多的限制。

您可以使用一个自定义处理程序来实现这一点,该处理程序通过在每个适用(或者,使用__FILE__解析,不适用)脚本顶部安装一个PHP include您必须有一个脚本,当每个文件都被命中时运行,并且您已经排除了对服务器配置的更改(我相信,当您说RewriteRule不够好时,包括.htaccess),所以这意味着您将通过基于脚本的看门人来执行此操作。不可能有一个解决方案满足您的限制,并让用户在不首先使用PHP(或另一种服务器端动态语言)的情况下访问文件。缓存可以通过将用户重定向到实际文件来保留,而不是通过PHP运行静态内容。

您可以将日志信息存储在数据库中,也可以将文件存储在服务器可写的位置(如果使用文件,请注意争用——附加模式很棘手)。

EDIT:quickshiftin指出了两种方法,无需手动添加include调用即可调用PHP。

创建一个auto_prepend_file并定义一个函数以进行日志记录。您需要访问.htaccess才能设置这些(webhost需要vhost中的AllowOverride-all之类的东西),或者使用PHP 5.3,您可以使用每个目录的INI功能。

.htaccess

php_value auto_prepend_file/path/to/file.php

每个目录php.ini(php 5.3 CGI/Fast CGI SAPI)

user_ini.auto_prepend_file=/path/to/file.php

然后是你的文件/path/to/file.php(我相信是更优雅的;)

file_put_contents(
    LOG_FILE,
    implode(PHP_EOL . PHP_EOL, array(
                'SERVER: ' . PHP_EOL . print_r($_SERVER, true),
                'REQUEST: ' . PHP_EOL . print_r($_REQUEST, true)
            )),
    FILE_APPEND
);

这种方法的美妙之处在于,您可能可以逃脱惩罚,并且只需要在一个地方定义/包含日志代码。

编辑:

经过回顾,我发现您希望这适用于任意类型的文件。。。是的,那会很粗糙。我能想到的最好的办法是将这些文件标记为.php或在.htaccess中定义自定义mime类型。其想法是通过php解释器运行这些文件,从而执行auto_prepend_file,因为文件中没有php标记,所以内容直接发送到客户端。甚至可能在每个内容文件的顶部添加一点PHP,设置ContentType标头。我甚至不确定这是否可行,但可能会。

考虑到不需要限制访问,这很简单。

构建一个页面logger.php,该页面接收所请求的文件的输入,如:

logger.php?file=abc.exe

logger.php中,您只需记录此访问,然后重新显示到文件:

file_put_contents('log', $_GET['file'] . ' requested',FILE_APPEND);
header('Location: files/'.$_GET['file']);

只需检查$_GET['file']中的恶意文件

当然,你必须替换你的网站中的链接,从:

<a href="files/abc.exe">

<a href="logger.php?file=abc.exe">

这里的意图似乎是绕过Apache和PHP中固有的所有系统。如果这些限制确实存在于您的服务器实例中,那么您最好要求更改您的权限,而不是设计一个您的系统管理员可能不满意您实现的变通方法。

可能不是您想要的,但为什么不完全使用不同的解决方案呢?

您可以使用Google Analytics VirtualPageviews通过Javascript跟踪文件下载。

有关详细信息,请参阅此处:http://support.google.com/googleanalytics/bin/answer.py?hl=en&答案=55529

您甚至可以创建自己的JS来通过浏览器跟踪文件下载,而无需使用GA。

更新

正如我所说的,你可以很容易地创建自己的JS来跟踪它们,而不必麻烦GA

代码示例:

JS侧:

$(document).ready(function() {
  $("a").click(function() {
    if( $(this).attr('href').match(/'/files'/(.*)/) ) {
      $.ajax({
        url: '/tracking/the/file/downloads.php'
        data: {
          'ok': 'let''s',
          'add': 'some information',
          'about': 'the user that initiated',
          'the': 'request',
          'file': $(this).attr('href')
        }
      });
    }
    return true;
  });
});

仅适用于mod_php情况。有一些性能问题——apache_lookup_uri()执行额外的apache内部子请求。

正如其他人指出的那样,你需要像一样的.htaccess

RewriteEngine On
RewriteRule ^/handler.php$ - [L]
RewriteRule ^/([a-zA-Z0-9'.]+)$ /handler.php?filename=$1 [L]

在handler.php文件中,使用virtual()函数来执行apache子请求。此处的示例:http://www.php.net/manual/en/function.virtual.php#88722

更新和测试(但相当小)的解决方案:

<?php
//add some request logging here
$file = $_GET["filename"];
$file_info = apache_lookup_uri($file);
header('content-type: ' . $file_info -> content_type);
// add other headers?
virtual($file);
exit(0);
?>

好吧,这是一个想法。请耐心听我说,它一开始可能看起来不合适,但最后要读一读。希望它能与你现有的相配合。在包含文件的文件夹中,放置一个.htaccess,它将所有请求重写到同一目录中的PHP处理程序脚本中,类似于以下内容(未经测试):

RewriteEngine On
RewriteRule ^/handler.php$ - [L]
RewriteRule ^/([a-zA-Z0-9'.]+)$ /handler.php?filename=$1 [L]

在PHP脚本中,您可以使用file_put_contents()执行任何必要的日志记录。然后,使用以下代码创建handler.php:

<?php
if (!file_exists) {
    header("Status: 404 Not Found");
    //if you have a 404 error page, you can use an include here to show it
    exit(0);
}
header("Content-disposition: attachment; filename={$_GET["filename"]}");
header("Content-type: ".get_mime_type($_GET["filename"]));
readfile($filename);
function get_mime_type($filename, $mimePath = '/etc') {
    $fileext = substr(strrchr($filename, '.'), 1);
    if (empty($fileext)) return (false);
    $regex = "/^(['w'+'-'.'/]+)'s+('w+'s)*($fileext's)/i";
    $lines = file("$mimePath/mime.types");
    foreach($lines as $line) {
        if (substr($line, 0, 1) == '#') continue; // skip comments
        $line = rtrim($line) . " ";
        if (!preg_match($regex, $line, $matches)) continue; // no match to the extension
        return ($matches[1]);
    }
    return (false); // no match at all
}
?>

基本上,您是在文件请求和文件的实际服务之间创建一个层。这个PHP层记录文件访问,然后为文件提供服务。你说过你不想在状态码和MIME类型上乱来,但这一切的美妙之处在于它都得到了处理。如果该文件不存在,它只生成一个标准404,并且可以包含一个自定义404错误页面。是的,这里的状态标题正在更改,但并不复杂。至于MIME类型,它们是根据Apache使用的相同MIME类型规则为您检测的。将get_mime_type函数指向服务器上的mime.types文件。如果你不知道它在哪里,只需从这里下载一份。我承认,这个解决方案可能比你想要的更具技术性,但考虑到你的限制,这是一个很好的解决方案。最棒的是,它对最终用户以及上传内容的人来说是完全透明的。

在不通过PHP过滤内容的情况下,唯一可以做的不引人注目的监控就是检查所有文件,并在每次请求任何PHP文件时记下它们的文件访问时间(只需在PHP文件中添加一个函数或使用重写)。这会产生一些开销,但这是你能得到的唯一不引人注目的统计数据。

显然,通过这种方式,你无法获得确切的访问次数,但更像是频率,所以这也是某种(可行的)统计数据。要获得像命中数这样的数据(3月25日凌晨2点,它被打开了1000k次),你需要访问日志或通过PHP或cgi脚本将其全部传输出去——只需要手动计数。

假设您使用PHP作为已编译的Apache模块,那么virtual()函数可以实现这一点。请参阅:http://www.php.net/manual/en/function.virtual.php

<?php
$fn = $_GET['fn'];
log_file_access($fn); // You define how you want this to happen    
virtual($fn);

然后,您可以通过以下途径引用文件:

http://example.com/file.php?fn=files/lolcat.jpg

我尝试了很多方法,但似乎没有简单的解决方案。

我的解决方案使用了@yes123提出的Location标头技巧,但我已经对其进行了调整以匹配我的偏好。

文件的链接保持不变,所以它仍然是:/files/path/to/my/file.abc我有一个RewriteRule:

RewriteRule ^files/(.*) path/to/tracker.php?path=/$1

然后,在文件中,我通过将?track=no添加到URL来发布Location标头,并将异常添加到早期的RewriteRule:

RewriteCond %{QUERY_STRING} !(&|^)track=no(&|$)

我又添加了一个优化。我已经启用了E-Tag,所以如果客户端发送E-Tag标头,请查看它是否与文件匹配,并返回304 Not Modified而不是Location

$fs = stat($document_root . $path);
$apache_etag = calculate_apache_etag($fs);
if ((isset($_SERVER["HTTP_IF_MATCH"]) && etag_within_range($_SERVER["HTTP_IF_MATCH"], $apache_etag))
    || (isset($_SERVER["HTTP_IF_NONE_MATCH"]) && etag_within_range($_SERVER["HTTP_IF_NONE_MATCH"], $apache_etag))
) {
    header("ETag: " . $apache_etag, true, 304);
    exit;
}
function etag_within_range($etag1, $etag2) {
    list($size1, $mtime1) = explode("-", $etag1);
    list($size2, $mtime2) = explode("-", $etag2);
    $mtime1 = floor(hexdec($mtime1) / 1000000);
    $mtime2 = floor(hexdec($mtime2) / 1000000);
    return $mtime1 === $mtime2 && $size1 === $size2;
}

calculate_apache_etag的实现可以在这里找到:如何制作与Apache匹配的etag?

CCD_ 27解决了在Apache中与更高精度的CCD_ 28进行比较的问题。


关于不起作用的解决方案的说明

virtual

测试脚本:

var_dump(apache_response_headers());
virtual("/path/to/image.jpg");
var_dump(apache_response_headers());

输出:

array(1) { ["X-Powered-By"]=> string(10) "PHP/5.2.11" }
[[binary junk]]
array(5) { ["X-Powered-By"]=> string(10) "PHP/5.2.11" ["Keep-Alive"]=> string(18) "timeout=5, max=100" ["Connection"]=> string(10) "Keep-Alive" ["Transfer-Encoding"]=> string(7) "chunked" ["Content-Type"]=> string(9) "text/html" }

Content-Type: text/html真的?:(

也许PHP5.3的header_remove功能可以解决这个问题?我还没试过。