From 2ecc7cae65225519a500088d8d3797b948544b61 Mon Sep 17 00:00:00 2001 From: Mark Gallagher Date: Thu, 17 Mar 2022 12:09:23 +0000 Subject: [PATCH] fpm: Implement access log filtering Adds a setting "access.suppress_path" to php-fpm pool configurations which causes successful GET requests to the specified URIs to be excluded from the access log. This is to reduce noise caused by automated health checks. Requests with response codes outwith the successful range 200 - 299, requests made with query parameters and requests which have a Content-Length other than 0 will ignore this setting as a security precaution. Closes GH-8174, #80428 [1] [1] https://bugs.php.net/bug.php?id=80428 --- sapi/fpm/fpm/fpm_conf.c | 39 +++++- sapi/fpm/fpm/fpm_conf.h | 1 + sapi/fpm/fpm/fpm_log.c | 53 ++++++++ ...config-array-validation-php-value-key.phpt | 34 +++++ ...-array-validation-suppress-path-key-2.phpt | 34 +++++ ...ig-array-validation-suppress-path-key.phpt | 34 +++++ ...validation-suppress-path-starts-slash.phpt | 34 +++++ sapi/fpm/tests/config-array.phpt | 49 +++++++ .../log-suppress-output-request-body.phpt | 107 +++++++++++++++ sapi/fpm/tests/log-suppress-output.phpt | 126 +++++++++++++++++ sapi/fpm/tests/logtool.inc | 7 +- sapi/fpm/tests/pm-max-spawn-rate-config.phpt | 4 +- sapi/fpm/tests/tester.inc | 127 +++++++++++++++--- sapi/fpm/www.conf.in | 16 +++ 14 files changed, 636 insertions(+), 29 deletions(-) create mode 100644 sapi/fpm/tests/config-array-validation-php-value-key.phpt create mode 100644 sapi/fpm/tests/config-array-validation-suppress-path-key-2.phpt create mode 100644 sapi/fpm/tests/config-array-validation-suppress-path-key.phpt create mode 100644 sapi/fpm/tests/config-array-validation-suppress-path-starts-slash.phpt create mode 100644 sapi/fpm/tests/config-array.phpt create mode 100644 sapi/fpm/tests/log-suppress-output-request-body.phpt create mode 100644 sapi/fpm/tests/log-suppress-output.phpt diff --git a/sapi/fpm/fpm/fpm_conf.c b/sapi/fpm/fpm/fpm_conf.c index 197c5b1c2b26e..5d088df501275 100644 --- a/sapi/fpm/fpm/fpm_conf.c +++ b/sapi/fpm/fpm/fpm_conf.c @@ -557,11 +557,13 @@ static char *fpm_conf_set_array(zval *key, zval *value, void **config, int conve } memset(kv, 0, sizeof(*kv)); - kv->key = strdup(Z_STRVAL_P(key)); + if (key) { + kv->key = strdup(Z_STRVAL_P(key)); - if (!kv->key) { - free(kv); - return "fpm_conf_set_array: strdup(key) failed"; + if (!kv->key) { + free(kv); + return "fpm_conf_set_array: strdup(key) failed"; + } } if (convert_to_bool) { @@ -663,6 +665,11 @@ int fpm_worker_pool_config_free(struct fpm_worker_pool_config_s *wpc) /* {{{ */ free(wpc->apparmor_hat); #endif + for (kv = wpc->access_suppress_paths; kv; kv = kv_next) { + kv_next = kv->next; + free(kv->value); + free(kv); + } for (kv = wpc->php_values; kv; kv = kv_next) { kv_next = kv->next; free(kv->key); @@ -1497,11 +1504,24 @@ static void fpm_conf_ini_parser_array(zval *name, zval *key, zval *value, void * char *err = NULL; void *config; - if (!*Z_STRVAL_P(key)) { - zlog(ZLOG_ERROR, "[%s:%d] Misspelled array ?", ini_filename, ini_lineno); + if (zend_string_equals_literal(Z_STR_P(name), "access.suppress_path")) { + if (!(*Z_STRVAL_P(key) == '\0')) { + zlog(ZLOG_ERROR, "[%s:%d] Keys provided to field 'access.suppress_path' are ignored", ini_filename, ini_lineno); + *error = 1; + } + if (!(*Z_STRVAL_P(value)) || (*Z_STRVAL_P(value) != '/')) { + zlog(ZLOG_ERROR, "[%s:%d] Values provided to field 'access.suppress_path' must begin with '/'", ini_filename, ini_lineno); + *error = 1; + } + if (*error) { + return; + } + } else if (!*Z_STRVAL_P(key)) { + zlog(ZLOG_ERROR, "[%s:%d] You must provide a key for field '%s'", ini_filename, ini_lineno, Z_STRVAL_P(name)); *error = 1; return; } + if (!current_wp || !current_wp->config) { zlog(ZLOG_ERROR, "[%s:%d] Array are not allowed in the global section", ini_filename, ini_lineno); *error = 1; @@ -1533,6 +1553,10 @@ static void fpm_conf_ini_parser_array(zval *name, zval *key, zval *value, void * config = (char *)current_wp->config + WPO(php_admin_values); err = fpm_conf_set_array(key, value, &config, 1); + } else if (zend_string_equals_literal(Z_STR_P(name), "access.suppress_path")) { + config = (char *)current_wp->config + WPO(access_suppress_paths); + err = fpm_conf_set_array(NULL, value, &config, 0); + } else { zlog(ZLOG_ERROR, "[%s:%d] unknown directive '%s'", ini_filename, ini_lineno, Z_STRVAL_P(name)); *error = 1; @@ -1735,6 +1759,9 @@ static void fpm_conf_dump(void) /* {{{ */ zlog(ZLOG_NOTICE, "\tping.response = %s", STR2STR(wp->config->ping_response)); zlog(ZLOG_NOTICE, "\taccess.log = %s", STR2STR(wp->config->access_log)); zlog(ZLOG_NOTICE, "\taccess.format = %s", STR2STR(wp->config->access_format)); + for (kv = wp->config->access_suppress_paths; kv; kv = kv->next) { + zlog(ZLOG_NOTICE, "\taccess.suppress_path[] = %s", kv->value); + } zlog(ZLOG_NOTICE, "\tslowlog = %s", STR2STR(wp->config->slowlog)); zlog(ZLOG_NOTICE, "\trequest_slowlog_timeout = %ds", wp->config->request_slowlog_timeout); zlog(ZLOG_NOTICE, "\trequest_slowlog_trace_depth = %d", wp->config->request_slowlog_trace_depth); diff --git a/sapi/fpm/fpm/fpm_conf.h b/sapi/fpm/fpm/fpm_conf.h index 4c0addc2a950f..47116376bdfdc 100644 --- a/sapi/fpm/fpm/fpm_conf.h +++ b/sapi/fpm/fpm/fpm_conf.h @@ -79,6 +79,7 @@ struct fpm_worker_pool_config_s { char *ping_response; char *access_log; char *access_format; + struct key_value_s *access_suppress_paths; char *slowlog; int request_slowlog_timeout; int request_slowlog_trace_depth; diff --git a/sapi/fpm/fpm/fpm_log.c b/sapi/fpm/fpm/fpm_log.c index d90d98b545683..bb66c081258d5 100644 --- a/sapi/fpm/fpm/fpm_log.c +++ b/sapi/fpm/fpm/fpm_log.c @@ -27,6 +27,9 @@ static char *fpm_log_format = NULL; static int fpm_log_fd = -1; +static struct key_value_s *fpm_access_suppress_paths = NULL; + +static int fpm_access_log_suppress(struct fpm_scoreboard_proc_s *proc); int fpm_log_open(int reopen) /* {{{ */ { @@ -79,6 +82,17 @@ int fpm_log_init_child(struct fpm_worker_pool_s *wp) /* {{{ */ } } + for (struct key_value_s *kv = wp->config->access_suppress_paths; kv; kv = kv->next) { + struct key_value_s *kvcopy = calloc(1, sizeof(*kvcopy)); + if (kvcopy == NULL) { + zlog(ZLOG_ERROR, "unable to allocate memory while opening the access log"); + return -1; + } + kvcopy->value = strdup(kv->value); + kvcopy->next = fpm_access_suppress_paths; + fpm_access_suppress_paths = kvcopy; + } + if (fpm_log_fd == -1) { fpm_log_fd = wp->log_fd; } @@ -136,6 +150,10 @@ int fpm_log_write(char *log_format) /* {{{ */ } proc = *proc_p; fpm_scoreboard_proc_release(proc_p); + + if (UNEXPECTED(fpm_access_log_suppress(&proc))) { + return -1; + } } token = 0; @@ -474,3 +492,38 @@ int fpm_log_write(char *log_format) /* {{{ */ return 0; } /* }}} */ + +static int fpm_access_log_suppress(struct fpm_scoreboard_proc_s *proc) +{ + // Never suppress when query string is passed + if (proc->query_string[0] != '\0') { + return 0; + } + + // Never suppress if request method is not GET or HEAD + if ( + strcmp(proc->request_method, "GET") != 0 + && strcmp(proc->request_method, "HEAD") != 0 + ) { + return 0; + } + + // Never suppress when response code does not indicate success + if (SG(sapi_headers).http_response_code < 200 || SG(sapi_headers).http_response_code > 299) { + return 0; + } + + // Never suppress when a body has been sent + if (SG(request_info).content_length > 0) { + return 0; + } + + // Suppress when request URI is an exact match for one of our entries + for (struct key_value_s *kv = fpm_access_suppress_paths; kv; kv = kv->next) { + if (kv->value && strcmp(kv->value, proc->request_uri) == 0) { + return 1; + } + } + + return 0; +} \ No newline at end of file diff --git a/sapi/fpm/tests/config-array-validation-php-value-key.phpt b/sapi/fpm/tests/config-array-validation-php-value-key.phpt new file mode 100644 index 0000000000000..53daec70c3828 --- /dev/null +++ b/sapi/fpm/tests/config-array-validation-php-value-key.phpt @@ -0,0 +1,34 @@ +--TEST-- +FPM: Validates arrays in configuration are correctly set - php_value array must be passed a key +--SKIPIF-- + +--FILE-- +start(['-tt']); +$tester->expectLogError("\[%s:%d\] You must provide a key for field 'php_value'"); + +?> +Done +--EXPECT-- + +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/config-array-validation-suppress-path-key-2.phpt b/sapi/fpm/tests/config-array-validation-suppress-path-key-2.phpt new file mode 100644 index 0000000000000..68ef0c322a146 --- /dev/null +++ b/sapi/fpm/tests/config-array-validation-suppress-path-key-2.phpt @@ -0,0 +1,34 @@ +--TEST-- +FPM: Validates arrays in configuration are correctly set - access.suppress_path doesn't accept key with forward slash +--SKIPIF-- + +--FILE-- +start(['-tt']); +$tester->expectLogError("\[%s:%d\] Keys provided to field 'access.suppress_path' are ignored"); + +?> +Done +--EXPECT-- + +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/config-array-validation-suppress-path-key.phpt b/sapi/fpm/tests/config-array-validation-suppress-path-key.phpt new file mode 100644 index 0000000000000..5b7a141a14a7b --- /dev/null +++ b/sapi/fpm/tests/config-array-validation-suppress-path-key.phpt @@ -0,0 +1,34 @@ +--TEST-- +FPM: Validates arrays in configuration are correctly set - access.suppress_path doesn't allow key +--SKIPIF-- + +--FILE-- +start(['-tt']); +$tester->expectLogError("\[%s:%d\] Keys provided to field 'access.suppress_path' are ignored"); + +?> +Done +--EXPECT-- + +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/config-array-validation-suppress-path-starts-slash.phpt b/sapi/fpm/tests/config-array-validation-suppress-path-starts-slash.phpt new file mode 100644 index 0000000000000..400c3621a85a9 --- /dev/null +++ b/sapi/fpm/tests/config-array-validation-suppress-path-starts-slash.phpt @@ -0,0 +1,34 @@ +--TEST-- +FPM: Validates arrays in configuration are correctly set - access.suppress_path begins with forward slash +--SKIPIF-- + +--FILE-- +start(['-tt']); +$tester->expectLogError("\[%s:%d\] Values provided to field 'access.suppress_path' must begin with '\/'"); + +?> +Done +--EXPECT-- + +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/config-array.phpt b/sapi/fpm/tests/config-array.phpt new file mode 100644 index 0000000000000..f2ddabac4b7f0 --- /dev/null +++ b/sapi/fpm/tests/config-array.phpt @@ -0,0 +1,49 @@ +--TEST-- +FPM: Set arrays in configuration +--SKIPIF-- + +--FILE-- +start(['-tt']); +$tester->expectLogConfigOptions([ + 'access.suppress_path[] = /ping', + 'access.suppress_path[] = /health_check.php', + 'php_value[error_reporting] = 32767', + 'php_value[date.timezone] = Europe/London', + 'php_value[display_errors] = 1', + 'php_admin_value[disable_functions] = eval', + 'php_admin_value[log_errors] = 1', +]); + + +?> +Done +--EXPECT-- + +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/log-suppress-output-request-body.phpt b/sapi/fpm/tests/log-suppress-output-request-body.phpt new file mode 100644 index 0000000000000..c878b40f964cb --- /dev/null +++ b/sapi/fpm/tests/log-suppress-output-request-body.phpt @@ -0,0 +1,107 @@ +--TEST-- +FPM: Test URIs are not excluded from access log when there is a request body +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->expectSuppressableAccessLogEntries(false); +$tester->ping(); + +// Should not suppress POST with no body +$tester->request( + uri: '/request-1', + headers: ['REQUEST_METHOD' => 'POST'] +)->expectBody('0'); +$tester->expectAccessLog("'POST /request-1' 200", suppressable: false); + +// Should not suppress POST with body +$tester->request( + uri: '/request-2', + stdin: $body +)->expectBody('100'); +$tester->expectAccessLog("'POST /request-2' 200", suppressable: false); + +// Should not suppress GET with body +$tester->request( + uri: '/request-3', + headers: ['REQUEST_METHOD' => 'GET'], + stdin: $body +)->expectBody('100'); +$tester->expectAccessLog("'GET /request-3' 200", suppressable: false); + +// Should suppress GET with no body +$tester->request( + uri: '/request-4' +)->expectBody('0'); +$tester->expectAccessLog("'GET /request-4' 200", suppressable: true); + +// Should not suppress GET with no body but incorrect content length +$tester->request( + uri: '/request-5', + headers: ['REQUEST_METHOD' => 'GET', 'CONTENT_LENGTH' => 100] +)->expectBody('0'); +$tester->expectAccessLog("'GET /request-5' 200", suppressable: false); + +// Should suppress GET with body but 0 content length (no stdin readable) +$tester->request( + uri: '/request-6', + headers: ['REQUEST_METHOD' => 'GET', 'CONTENT_LENGTH' => 0], + stdin: $body +)->expectBody('0'); +$tester->expectAccessLog("'GET /request-6' 200", suppressable: true); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +$tester->expectNoFile(FPM\Tester::FILE_EXT_PID); +$tester->checkAccessLog(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/log-suppress-output.phpt b/sapi/fpm/tests/log-suppress-output.phpt new file mode 100644 index 0000000000000..5a5e7bb9544ba --- /dev/null +++ b/sapi/fpm/tests/log-suppress-output.phpt @@ -0,0 +1,126 @@ +--TEST-- +FPM: Test excluding URIs from access log +--SKIPIF-- + +--FILE-- +expectSuppressableAccessLogEntries($expectSuppressableEntries); + + $tester->ping(); + $tester->expectAccessLog("'GET /ping' 200", suppressable: true); + + $tester->request()->expectBody('OK'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php' 200", suppressable: true); + + $tester->ping(); + $tester->expectAccessLog("'GET /ping' 200", suppressable: true); + + $tester->request()->expectBody('OK'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php' 200", suppressable: true); + + $tester->ping(); + $tester->expectAccessLog("'GET /ping' 200", suppressable: true); + + $tester->request(query: 'test=output')->expectBody('output'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php?test=output' 200", suppressable: false); + + $tester->ping(); + $tester->expectAccessLog("'GET /ping' 200", suppressable: true); + + $tester->request()->expectBody('OK'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php' 200", suppressable: true); + + $tester->request(query: 'test=output', uri: '/ping')->expectBody('pong', 'text/plain'); + $tester->expectAccessLog("'GET /ping?test=output' 200", suppressable: false); + + $tester->request(headers: ['X_ERROR' => 1])->expectBody('Not OK'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php' 500", suppressable: false); + + $tester->request()->expectBody('OK'); + $tester->expectAccessLog("'GET /log-suppress-output.src.php' 200", suppressable: true); + + $tester->request(query: 'test=output', uri: '/ping')->expectBody('pong', 'text/plain'); + $tester->expectAccessLog("'GET /ping?test=output' 200", suppressable: false); + + $tester->ping(); + $tester->expectAccessLog("'GET /ping' 200", suppressable: true); +} + +$src = <<start(['--prefix', $prefix]); +$tester->expectLogStartNotices(); +doTestCalls($tester, expectSuppressableEntries: true); +// Add source file and ping to ignore list +$cfg = <<reload($cfg); +$tester->expectLogReloadingNotices(); +doTestCalls($tester, expectSuppressableEntries: false); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +$tester->expectNoFile(FPM\Tester::FILE_EXT_PID, $prefix); +$tester->checkAccessLog(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/logtool.inc b/sapi/fpm/tests/logtool.inc index e085ae291beab..9555212683e58 100644 --- a/sapi/fpm/tests/logtool.inc +++ b/sapi/fpm/tests/logtool.inc @@ -363,8 +363,13 @@ class LogTool $expectedMessage = '\[pool ' . $pool . '\] ' . $expectedMessage; } + // Allow expected message to contain %s and %s for any string or number + // as in run-tests.php + $expectRe = str_replace('%s', '[^\r\n]+', $expectedMessage); + $expectRe = str_replace('%d', '\d+', $expectRe); + $line = rtrim($line); - $pattern = sprintf('/^(%s )?%s: %s$/', self::P_TIME, $type, $expectedMessage); + $pattern = sprintf('/^(%s )?%s: %s$/', self::P_TIME, $type, $expectRe); if (preg_match($pattern, $line, $matches) === 0) { return $this->error( diff --git a/sapi/fpm/tests/pm-max-spawn-rate-config.phpt b/sapi/fpm/tests/pm-max-spawn-rate-config.phpt index 8b5f6b4eb6404..999c1d747092d 100644 --- a/sapi/fpm/tests/pm-max-spawn-rate-config.phpt +++ b/sapi/fpm/tests/pm-max-spawn-rate-config.phpt @@ -24,8 +24,8 @@ pm.max_spawn_rate = 64 EOT; $tester = new FPM\Tester($cfg); -$tester->start(['-t', '-t']); -$tester->expectLogConfigOptions(['pm.max_spawn_rate' => 64]); +$tester->start(['-tt']); +$tester->expectLogConfigOptions(['pm.max_spawn_rate = 64']); $tester->close(); ?> diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc index e607035df1b31..0956dd969aaba 100644 --- a/sapi/fpm/tests/tester.inc +++ b/sapi/fpm/tests/tester.inc @@ -120,6 +120,16 @@ class Tester */ private $response; + /** + * @var string[] + */ + private $expectedAccessLogs; + + /** + * @var bool + */ + private $expectSuppressableAccessLogEntries; + /** * Clean all the created files up * @@ -345,7 +355,7 @@ class Tester public function testConfig() { $configFile = $this->createConfig(); - $cmd = self::findExecutable() . ' -t -y ' . $configFile . ' 2>&1'; + $cmd = self::findExecutable() . ' -tt -y ' . $configFile . ' 2>&1'; exec($cmd, $output, $code); if ($code) { return preg_replace("/\[.+?\]/", "", $output[0]); @@ -534,16 +544,20 @@ class Tester string $query = '', array $headers = [], string $uri = null, - string $scriptFilename = null + string $scriptFilename = null, + ?string $stdin = null ) { + if (is_null($scriptFilename)) { + $scriptFilename = $this->makeSourceFile(); + } if (is_null($uri)) { - $uri = $this->makeSourceFile(); + $uri = "/" . basename($scriptFilename); } $params = array_merge( [ 'GATEWAY_INTERFACE' => 'FastCGI/1.0', - 'REQUEST_METHOD' => 'GET', + 'REQUEST_METHOD' => is_null($stdin) ? 'GET' : 'POST', 'SCRIPT_FILENAME' => $scriptFilename ?: $uri, 'SCRIPT_NAME' => $uri, 'QUERY_STRING' => $query, @@ -558,7 +572,7 @@ class Tester 'SERVER_PROTOCOL' => 'HTTP/1.1', 'DOCUMENT_ROOT' => __DIR__, 'CONTENT_TYPE' => '', - 'CONTENT_LENGTH' => 0 + 'CONTENT_LENGTH' => strlen($stdin ?? "") // Default to 0 ], $headers ); @@ -589,17 +603,18 @@ class Tester string $successMessage = null, string $errorMessage = null, bool $connKeepAlive = false, - string $scriptFilename = null + string $scriptFilename = null, + string $stdin = null ) { if ($this->hasError()) { return new Response(null, true); } - $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename); + $params = $this->getRequestParams($query, $headers, $uri, $scriptFilename, $stdin); try { $this->response = new Response( - $this->getClient($address, $connKeepAlive)->request_data($params, false) + $this->getClient($address, $connKeepAlive)->request_data($params, $stdin) ); $this->message($successMessage); } catch (\Exception $exception) { @@ -1159,7 +1174,6 @@ class Tester { $filePath = $this->getFile($extension, $dir, $name); file_put_contents($filePath, $content); - return $filePath; } @@ -1437,18 +1451,28 @@ class Tester * @param array $options * @return bool */ - public function expectLogConfigOptions(array $options) + public function expectLogConfigOptions(array $expectedOptions) { $configOptions = $this->getConfigOptions(); - foreach ($options as $name => $value) { - if (!isset($configOptions[$name])) { - return $this->error("Expected config option: {$name} = {$value} but {$name} is not set"); + foreach ($expectedOptions as $expectedOption) { + if (array_search($expectedOption, $configOptions, true)) { + // Exact match found, no error + continue; } - if ($configOptions[$name] != $value) { - return $this->error( - "Expected config option: {$name} = {$value} but got: {$name} = {$configOptions[$name]}" - ); + + // Try to find similar key + $key = substr($expectedOption, 0, strpos($expectedOption, " = ")); + $matches = array_filter($configOptions, fn($configOption) => substr($configOption, 0, strlen($key)) == $key); + + if (empty($matches)) { + return $this->error("Expected config option: $expectedOption but {$key} is not set"); } + + return $this->error(sprintf( + "Expected config option: %s but got: %s", + $expectedOption, + implode("; ", $matches) + )); } return true; @@ -1462,14 +1486,13 @@ class Tester private function getConfigOptions() { $options = []; - foreach ($this->getLogLines(-1) as $line) { preg_match('/.+NOTICE:\s+(.+)\s=\s(.+)/', $line, $matches); if ($matches) { - $options[$matches[1]] = $matches[2]; + // normalize format for consistent checking + $options[] = sprintf("%s = %s", $matches[1], $matches[2]); } } - return $options; } @@ -1483,4 +1506,68 @@ class Tester print file_get_contents($accessLog); } } + + /** + * Return content of access log. + * + * @return string|false + */ + public function getAccessLog() + { + $accessLog = $this->getFile('acc.log'); + if (is_file($accessLog)) { + return file_get_contents($accessLog); + } + return false; + } + + /** + * Expect a single access log line. + * + * @param string $LogLine + * @param bool $suppressable see expectSuppressableAccessLogEntries + */ + public function expectAccessLog( + string $logLine, + bool $suppressable = false + ) { + if (!$suppressable || $this->expectSuppressableAccessLogEntries) { + $this->expectedAccessLogs[] = $logLine; + } + } + + /** + * Checks that all access log entries previously listed as expected by + * calling "expectAccessLog" are in the access log. + */ + public function checkAccessLog() + { + if (isset($this->expectedAccessLogs)) { + $expectedAccessLog = implode("\n", $this->expectedAccessLogs) . "\n"; + } else { + $this->error("Called checkAccessLog but did not previous call expectAccessLog"); + } + if ($accessLog = $this->getAccessLog()) { + if ($expectedAccessLog !== $accessLog) { + $this->error(sprintf( + "Access log was not as expected.\nEXPECTED:\n%s\n\nACTUAL:\n%s", + $expectedAccessLog, + $accessLog + )); + } + } else { + $this->error("Called checkAccessLog but access log does not exist"); + } + } + + /** + * Flags whether the access log check should expect to see suppressable + * log entries, i.e. the URL is not in access.suppress_path[] config + * + * @param bool + */ + public function expectSuppressableAccessLogEntries(bool $expectSuppressableAccessLogEntries) + { + $this->expectSuppressableAccessLogEntries = $expectSuppressableAccessLogEntries; + } } diff --git a/sapi/fpm/www.conf.in b/sapi/fpm/www.conf.in index 32705b9f3d378..fd774c6161295 100644 --- a/sapi/fpm/www.conf.in +++ b/sapi/fpm/www.conf.in @@ -343,6 +343,22 @@ pm.max_spare_servers = 3 ; Default: "%R - %u %t \"%m %r\" %s" ;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%" +; A list of request_uri values which should be filtered from the access log. +; +; As a security precuation, this setting will be ignored if: +; - the request method is not GET or HEAD; or +; - there is a request body; or +; - there are query parameters; or +; - the response code is outwith the successful range of 200 to 299 +; +; Note: The paths are matched against the output of the access.format tag "%r". +; On common configurations, this may look more like SCRIPT_NAME than the +; expected pre-rewrite URI. +; +; Default Value: not set +;access.suppress_path[] = /ping +;access.suppress_path[] = /health_check.php + ; The log file for slow requests ; Default Value: not set ; Note: slowlog is mandatory if request_slowlog_timeout is set