rosell-dk/htaccess-capability-tester

使用实时测试来测试服务器上 .htaccess 文件的功能

1.0.0 2021-11-25 13:21 UTC

This package is auto-updated.

Last update: 2024-09-06 17:56:50 UTC


README

Latest Stable Version Minimum PHP Version Build Status Coverage Quality Score Software License

通过实时测试检测 .htaccess 功能。

在某些情况下,唯一了解某个 .htaccess 功能是否在系统上启用/受支持的方法是通过通过 HTTP 请求“从外部”检查它。这个库就是为了轻松处理此类测试而构建的。

幕后发生了什么

  1. 将一些测试文件放置在服务器上(至少一个 .htaccess 文件)
  2. 通过执行 HTTP 请求触发测试
  3. 解释响应

用法

要使用此库,您必须提供测试文件将要放置的路径以及它们可以到达的相应 URL。除此之外,您只需选择您想要运行的测试之一。

require 'vendor/autoload.php';
use HtaccessCapabilityTester\HtaccessCapabilityTester;

$hct = new HtaccessCapabilityTester($baseDir, $baseUrl);

if ($hct->moduleLoaded('headers')) {
    // mod_headers is loaded (tested in a real .htaccess by using the "IfModule" directive)
}
if ($hct->rewriteWorks()) {    
    // rewriting works

}
if ($hct->htaccessEnabled() === false) {
    // Apache has been configured to ignore .htaccess files
}

// A bunch of other tests are available - see API

虽然有一个可靠的 moduleLoaded() 方法比当前情况有巨大改进,但请注意,服务器可能已启用例如 mod_rewrite,但同时禁止在 .htaccess 文件中使用例如“RewriteRule”指令。这就是为什么这个库有 rewriteWorks() 方法以及测试各种能力的类似方法(请参阅下面的 API 概览)。提供所有类型功能的测试对任何库来说都可能太多。相反,此库使您能够轻松定义自定义测试并通过 customTest($def) 方法运行它。要了解更多信息,请参阅运行您自己的自定义测试文档。

API 概览

HtaccessCapabilityTester 中的测试方法

所有测试方法都返回一个测试结果,对于成功为 true,对于失败为 false,对于不确定为 null

测试有以下共同点

  • 如果服务器已设置为完全忽略 .htaccess 文件,则结果为 失败
  • 如果服务器已设置为禁止测试指令(AllowOverride),则结果为 失败(无论是配置为忽略还是配置为致命)
  • 403 禁止访问 导致 不确定。为什么?因为服务器可能已设置为禁止访问与测试文件不幸匹配的模式的文件。在大多数情况下,这不太可能,因为大多数测试请求具有无害外观的文件扩展名(通常是“request-me.txt”)。但是,一些测试请求“test.php”,这更有可能被拒绝。
  • 404 未找到 导致 不确定
  • 如果请求完全失败(即超时),则结果为 不确定

大多数测试都实现为类似于在 customTest() 中接受的定义。这意味着如果您想要此库提供的某个测试稍微不同一点,您可以轻松地获取在 Testers 目录中相应类中的代码,进行修改,然后调用 customTest()

addTypeWorks()


测试 *AddType* 指令是否工作。

实现(YAML 定义)

subdir: add-type
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_mime.c>
          AddType image/gif .test
      </IfModule>
  - filename: 'request-me.test'
    content: 'hi'
request:
  url: 'request-me.test'

interpretation:
 - ['success', 'headers', 'contains-key-value', 'Content-Type', 'image/gif']
 - ['inconclusive', 'status-code', 'not-equals', '200']
 - ['failure', 'headers', 'not-contains-key-value', 'Content-Type', 'image/gif']

contentDigestWorks()

实现(YAML 定义)

subdir: content-digest
subtests:
  - subdir: on
    files:
    - filename: '.htaccess'
      content: |
        ContentDigest On
    - filename: 'request-me.txt'
      content: 'hi'
    request:
      url: 'request-me.txt'
    interpretation:
      - ['failure', 'headers', 'not-contains-key', 'Content-MD5'],

    - subdir: off
      files:
        - filename: '.htaccess'
          content: |
             ContentDigest Off
        - filename: 'request-me.txt'
          content: 'hi'
      request:
        url: 'request-me.txt'

      interpretation:
        - ['failure', 'headers', 'contains-key', 'Content-MD5']
        - ['inconclusive', 'status-code', 'not-equals', '200']
        - ['success', 'status-code', 'equals', '200']

crashTest($rules, $subdir)


测试某些规则是否会导致服务器“崩溃”(对文件夹中的文件请求返回500内部服务器错误)。您传递要检查的规则。您可以可选地传递一个子目录进行测试。如果您不这样做,将使用规则的哈希值。

实现(PHP)

/**
 * @param string $htaccessRules  The rules to check
 * @param string $subSubDir      subdir for the test files. If not supplied, a fingerprint of the rules will be used
 */
public function __construct($htaccessRules, $subSubDir = null)
{
    if (is_null($subSubDir)) {
        $subSubDir = hash('md5', $htaccessRules);
    }

    $test = [
        'subdir' => 'crash-tester/' . $subSubDir,
        'subtests' => [
            [
                'subdir' => 'the-suspect',
                'files' => [
                    ['.htaccess', $htaccessRules],
                    ['request-me.txt', 'thanks'],
                ],
                'request' => [
                    'url' => 'request-me.txt',
                    'bypass-standard-error-handling' => ['all']
                ],
                'interpretation' => [
                    ['success', 'status-code', 'not-equals', '500'],
                ]
            ],
            [
                'subdir' => 'the-innocent',
                'files' => [
                    ['.htaccess', '# I am no trouble'],
                    ['request-me.txt', 'thanks'],
                ],
                'request' => [
                    'url' => 'request-me.txt',
                    'bypass-standard-error-handling' => ['all']
                ],
                'interpretation' => [
                    // The suspect crashed. But if the innocent crashes too, we cannot judge
                    ['inconclusive', 'status-code', 'equals', '500'],

                    // The innocent did not crash. The suspect is guilty!
                    ['failure'],
                ]
            ],
        ]
    ];

    parent::__construct($test);
}

customTest($definition)

允许您运行自定义测试。请查看README.md以获取说明。

directoryIndexWorks()


测试目录索引是否工作。

实现(YAML 定义)

subdir: directory-index
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_dir.c>
          DirectoryIndex index2.html
      </IfModule>
  - filename: 'index.html'
    content: '0'
  - filename: 'index2.html'
    content: '1'

request:
  url: ''   # We request the index, that is why its empty
  bypass-standard-error-handling: ['404']

interpretation:
  - ['success', 'body', 'equals', '1']
  - ['failure', 'body', 'equals', '0']
  - ['failure', 'status-code', 'equals', '404']  # "index.html" might not be set to index

headerSetWorks()


测试使用*Header*指令设置响应头是否工作。

实现(YAML 定义)

subdir: header-set
files:
    - filename: '.htaccess'
      content: |
          <IfModule mod_headers.c>
              Header set X-Response-Header-Test: test
          </IfModule>
    - filename: 'request-me.txt'
      content: 'hi'

request:
    url: 'request-me.txt'

interpretation:
    - [success, headers, contains-key-value, 'X-Response-Header-Test', 'test'],
    - [failure]

htaccessEnabled()


Apache可以配置为完全忽略*.htaccess*文件。此方法测试是否处理了*.htaccess*文件。

该方法通过尝试一系列子测试直到得出结论。它永远不会得出不确定的结论。

它如何工作?

  • 第一种策略是测试一系列功能,例如rewriteWorks()。如果有任何一个工作,那么.htaccess肯定已经被处理。
  • 其次,测试serverSignatureWorks()。由于“ServerSignature”指令在核心中,并且无法通过AllowOverride禁用,因此它特别。如果此测试失败,那么非常可能,.htaccess没有被处理,因此我们得出结论,它没有被处理。
  • 最后,如果所有其他方法都失败了,我们将尝试在包含故意放置的语法错误的.htaccess文件上调用crashTest()。如果它崩溃,.htaccess文件肯定已经被处理。如果没有崩溃,则没有。这种方法是万无一失的——为什么不首先这样做呢?因为可能会导致错误日志中生成一个条目。

实现的主要部分

// If we can find anything that works, well the .htaccess must have been proccesed!
if ($hct->serverSignatureWorks()    // Override: None,  Status: Core, REQUIRES PHP
    || $hct->contentDigestWorks()   // Override: Options,  Status: Core
    || $hct->addTypeWorks()         // Override: FileInfo, Status: Base, Module: mime
    || $hct->directoryIndexWorks()  // Override: Indexes,  Status: Base, Module: mod_dir
    || $hct->rewriteWorks()         // Override: FileInfo, Status: Extension, Module: rewrite
    || $hct->headerSetWorks()       // Override: FileInfo, Status: Extension, Module: headers
) {
    $status = true;
} else {
    // The serverSignatureWorks() test is special because if it comes out as a failure,
    // we can be *almost* certain that the .htaccess has been completely disabled

    $serverSignatureWorks = $hct->serverSignatureWorks();
    if ($serverSignatureWorks === false) {
        $status = false;
        $info = 'ServerSignature directive does not work - and it is in core';
    } else {
        // Last bullet in the gun:
        // Try an .htaccess with syntax errors in it.
        // (we do this lastly because it may generate an entry in the error log)
        $crashTestResult = $hct->crashTest('aoeu', 'htaccess-enabled-malformed-htaccess');
        if ($crashTestResult === false) {
            // It crashed, - which means .htaccess is processed!
            $status = true;
            $info = 'syntax error in an .htaccess causes crash';
        } elseif ($crashTestResult === true) {
            // It did not crash. So the .htaccess is not processed, as syntax errors
            // makes servers crash
            $status = false;
            $info = 'syntax error in an .htaccess does not cause crash';
        } elseif (is_null($crashTestResult)) {
            // It did crash. But so did a request to an innocent text file in a directory
            // without a .htaccess file in it. Something is making all requests fail and
            // we cannot judge.
            $status = null;
            $info = 'all requests results in 500 Internal Server Error';
        }
    }
}
return new TestResult($status, $info);

innocentRequestWorks()


测试对文本文件的普通请求是否工作。大多数测试在收到500内部错误时使用此测试,以决定这是否是一个普遍问题(普遍问题 => 不确定,具体问题 => 失败)。

实现(YAML 定义)

subdir: innocent-request
files:
  - filename: 'request-me.txt'
    content: 'thank you my dear'

request:
  url: 'request-me.txt'
  bypass-standard-error-handling: 'all'

interpretation:
  - ['success', 'status-code', 'equals', '200']
  - ['inconclusive', 'status-code', 'equals', '403']
  - ['inconclusive', 'status-code', 'equals', '404']
  - ['failure']

moduleLoaded($moduleName)


测试指定的模块是否加载。请注意,在大多数情况下,您不仅想知道模块是否已加载,还必须确保您正在使用的指令是被允许的。因此,例如,您可能不会调用`moduleLoaded("rewrite")`,而是应该调用`rewriteWorks()`;

实现

根据什么工作,该方法有多种测试模块是否加载的方法。例如,如果已经确定设置头部是有效的,并且您想知道“setenvif”模块是否已加载,则将测试以下.htaccess规则,并检查响应。

<IfModule mod_setenvif.c>
    Header set X-Response-Header-Test: 1
</IfModule>
<IfModule !mod_setenvif.c>
    Header set X-Response-Header-Test: 0
</IfModule>

passingInfoFromRewriteToScriptThroughEnvWorks()


假设您有一个指向PHP脚本的重写规则,并且您希望将一些信息传递给PHP。通常,您只需在查询字符串中传递它。但是,如果信息是敏感的,这就不起作用了。在这种情况下,有一些技巧可用。这里测试的技巧告诉RewriteRule指令设置一个环境变量,这在许多配置中可以在脚本中获取。

实现(YAML 定义)

subdir: pass-info-from-rewrite-to-script-through-env
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_rewrite.c>

          # Testing if we can pass environment variable from .htaccess to script in a RewriteRule
          # We pass document root, because that can easily be checked by the script

          RewriteEngine On
          RewriteRule ^test\.php$ - [E=PASSTHROUGHENV:%{DOCUMENT_ROOT},L]

      </IfModule>
  - filename: 'test.php'
    content: |
      <?php

      /**
       *  Get environment variable set with mod_rewrite module
       *  Return false if the environment variable isn't found
       */
      function getEnvPassedInRewriteRule($envName) {
          // Environment variables passed through the REWRITE module have "REWRITE_" as a prefix
          // (in Apache, not Litespeed, if I recall correctly).
          // Multiple iterations causes multiple REWRITE_ prefixes, and we get many environment variables set.
          // We simply look for an environment variable that ends with what we are looking for.
          // (so make sure to make it unique)
          $len = strlen($envName);
          foreach ($_SERVER as $key => $item) {
              if (substr($key, -$len) == $envName) {
                  return $item;
              }
          }
          return false;
      }

      $result = getEnvPassedInRewriteRule('PASSTHROUGHENV');
      if ($result === false) {
          echo '0';
          exit;
      }
      echo ($result == $_SERVER['DOCUMENT_ROOT'] ? '1' : '0');

request:
  url: 'test.php'

interpretation:
  - ['success', 'body', 'equals', '1']
  - ['failure', 'body', 'equals', '0']
  - ['inconclusive', 'body', 'begins-with', '<?php']
  - ['inconclusive']

passingInfoFromRewriteToScriptThroughRequestHeaderWorks()


假设您有一个指向PHP脚本的重写规则,并且您希望将一些信息传递给PHP。通常,您只需在查询字符串中传递它。但是,如果信息是敏感的,这就不起作用了。在这种情况下,有一些技巧可用。这里测试的技巧告诉RewriteRule指令设置一个环境变量,该变量由RequestHeader指令获取并传递到脚本中的请求头。

实现(YAML 定义)

subdir: pass-info-from-rewrite-to-script-through-request-header
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_rewrite.c>
          RewriteEngine On

          # Testing if we can pass an environment variable through a request header
          # We pass document root, because that can easily be checked by the script

          <IfModule mod_headers.c>
            RequestHeader set PASSTHROUGHHEADER "%{PASSTHROUGHHEADER}e" env=PASSTHROUGHHEADER
          </IfModule>
          RewriteRule ^test\.php$ - [E=PASSTHROUGHHEADER:%{DOCUMENT_ROOT},L]

      </IfModule>
  - filename: 'test.php'
    content: |
      <?php
      if (isset($_SERVER['HTTP_PASSTHROUGHHEADER'])) {
          echo ($_SERVER['HTTP_PASSTHROUGHHEADER'] == $_SERVER['DOCUMENT_ROOT'] ? 1 : 0);
          exit;
      }
      echo '0';

request:
  url: 'test.php'

interpretation:
  - ['success', 'body', 'equals', '1']
  - ['failure', 'body', 'equals', '0']
  - ['inconclusive', 'body', 'begins-with', '<?php']
  - ['inconclusive']

rewriteWorks()


测试重写是否工作。

实现(YAML 定义)

subdir: rewrite
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_rewrite.c>
          RewriteEngine On
          RewriteRule ^0\.txt$ 1\.txt [L]
      </IfModule>
  - filename: '0.txt'
    content: '0'
  - filename: '1.txt'
    content: '1'

request:
  url: '0.txt'

interpretation:
  - [success, body, equals, '1']
  - [failure, body, equals, '0']

requestHeaderWorks()


测试是否可以使用*RequestHeader*指令设置请求头。

实现(YAML 定义)

subdir: request-header
files:
  - filename: '.htaccess'
    content: |
      <IfModule mod_headers.c>
          # Certain hosts seem to strip non-standard request headers,
          # so we use a standard one to avoid a false negative
          RequestHeader set User-Agent "request-header-test"
      </IfModule>
  - filename: 'test.php'
    content: |
      <?php
      if (isset($_SERVER['HTTP_USER_AGENT'])) {
          echo  $_SERVER['HTTP_USER_AGENT'] == 'request-header-test' ? 1 : 0;
      } else {
          echo 0;
      }

request:
  url: 'test.php'

interpretation:
  - ['success', 'body', 'equals', '1']
  - ['failure', 'body', 'equals', '0']
  - ['inconclusive', 'body', 'begins-with', '<?php']

serverSignatureWorks()


测试*ServerSignature*指令是否工作。

实现(YAML 定义)

subdir: server-signature
subtests:
  - subdir: on
    files:
    - filename: '.htaccess'
      content: |
        ServerSignature On
    - filename: 'test.php'
      content: |
      <?php
      if (isset($_SERVER['SERVER_SIGNATURE']) && ($_SERVER['SERVER_SIGNATURE'] != '')) {
          echo 1;
      } else {
          echo 0;
      }
    request:
      url: 'test.php'
    interpretation:
      - ['inconclusive', 'body', 'isEmpty']
      - ['inconclusive', 'status-code', 'not-equals', '200']
      - ['failure', 'body', 'equals', '0']

  - subdir: off
    files:
    - filename: '.htaccess'
      content: |
        ServerSignature Off
    - filename: 'test.php'
      content: |
      <?php
      if (isset($_SERVER['SERVER_SIGNATURE']) && ($_SERVER['SERVER_SIGNATURE'] != '')) {
          echo 0;
      } else {
          echo 1;
      }
    request:
      url: 'test.php'
    interpretation:
      - ['inconclusive', 'body', 'isEmpty']
      - ['success', 'body', 'equals', '1']
      - ['failure', 'body', 'equals', '0']
      - ['inconclusive']

HtaccessCapabilityTester中的其他方法

setHttpRequester($requester)


这允许您使用另一个对象来发送HTTP请求,而不是使用此库提供的标准对象。标准对象使用`file_get_contents`来发送请求,并在`SimpleHttpRequester.php`中实现。例如,您可能更愿意使用*curl*,或者如果您正在制作Wordpress插件,您可能希望使用Wordpress框架提供的对象。

setTestFilesLineUpper($testFilesLineUpper)


这允许您使用另一个对象来对测试文件进行排队,而不是使用此库提供的标准对象。标准对象使用`file_put_contents`来保存文件,并在`SimpleTestFileLineUpper.php`中实现。您可能不需要更换测试文件排队器。

稳定的API?

0.9版本刚刚好。我不期望上述API部分的任何更改。因此,如果您坚持使用它,当1.0版本发布时,它仍然应该工作。

新0.9版本的更改

  • 请求失败(例如超时)导致结果为不确定
  • 如果您已经实现了自己的HttpRequester而不是使用默认的,则需要更新它。如果请求失败(即超时),它现在必须返回状态代码"0"。

1.0版本预期的更改

  • TestResult类可能会被废弃,因此"内部"的Tester类也返回bool|null。
  • 当无法创建测试文件时抛出自定义异常

平台

在以下平台上工作(至少)

  • 操作系统:Ubuntu (22.04, 20.04), Windows (2022, 2019), Mac OS (13, 12, 11, 10.15)
  • PHP:5.6 - 8.2

每个新版本都将测试GitHub托管运行器所支持的操作系统和PHP版本的组合。但我们不支持低于PHP 5.6的版本。
状态: Giant test

测试包括运行单元测试。此库中的代码有相当好的代码覆盖率(约90%覆盖率)。

我们还每月测试PHP的未来版本,以便尽早发现问题。
状态: PHP 8.3 PHP 8.4

安装

使用Composer要求库,如下所示

composer require rosell-dk/htaccess-capability-tester