www/nginx: Log display improvements (#2165)

Move pagination and filtering of Nginx logs (error
and access logs for HTTP, Stream and Nginx) from
client browser to server to allow efficient display
of big log files in web UI.

- Allow displaying of archived log files via web UI
- Added default parameters to LogParserBase
- Pass text query base64 encoded to configd to prevent shell injection
- Encode "/" in query before sending via AJAX
- Display correct values (total/found/displayed/...) in case of no results
This commit is contained in:
Manuel Faux
2021-12-09 13:08:04 +01:00
committed by GitHub
parent 04835c3edc
commit 3c7d3502d0
27 changed files with 802 additions and 313 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PLUGIN_NAME= nginx
PLUGIN_VERSION= 1.24
PLUGIN_VERSION= 1.25
PLUGIN_COMMENT= Nginx HTTP server and reverse proxy
PLUGIN_DEPENDS= nginx
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com
+4
View File
@@ -10,6 +10,10 @@ WWW: https://nginx.org/
Plugin Changelog
================
1.25
* Reworked logging frontent to support filtering and historic view (contributed by Manuel Faux)
1.24
* Change all Listen Port directives to Listen Address and migrate the Port data to Addresses
@@ -2,7 +2,8 @@
/*
Copyright (C) 2018 Fabian Franz
Copyright (C) 2018-2020 Fabian Franz
Copyright (C) 2020 Manuel Faux
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -40,18 +41,26 @@ class LogsController extends ApiControllerBase
* "/" -> list of access logs
* "/uuid" -> conent of access log
* @param null|string $uuid log uuid of the HTTP server from which the error log should be returned
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return array if feasible, otherwise null and the data is sent directly back
* @throws \OPNsense\Base\ModelException ?
*/
public function accessesAction($uuid = null)
public function accessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "")
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /accesses delivers a list of servers with access logs
return $this->list_vhosts();
} else {
}
elseif (!isset($fileno)) {
return $this->list_logfiles('access', $uuid);
}
else {
// emulate REST call for a specific log /accesses/uuid
$this->call_configd('access', $uuid);
$this->get_logs('access', $uuid, $fileno, $page, $perPage, $query);
}
}
@@ -68,18 +77,26 @@ class LogsController extends ApiControllerBase
* "/" -> list of error logs
* "/uuid" -> conent of error log
* @param null|string $uuid uuid of the HTTP server from which the error log should be returned
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return array if feasible, otherwise null and the data is sent directly back
* @throws \OPNsense\Base\ModelException ?
*/
public function errorsAction($uuid = null)
public function errorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "")
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /errors delivers a list of servers with error logs
return $this->list_vhosts();
} else {
}
elseif (!isset($fileno)) {
return $this->list_logfiles('error', $uuid);
}
else {
// emulate REST call for a specific log /errors/uuid
$this->call_configd('error', $uuid);
$this->get_logs('error', $uuid, $fileno, $page, $perPage, $query);
}
}
@@ -87,18 +104,26 @@ class LogsController extends ApiControllerBase
* "/" -> list of access logs
* "/uuid" -> conent of access log
* @param null|string $uuid log uuid of the stream server from which the error log should be returned
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return array if feasible, otherwise null and the data is sent directly back
* @throws \OPNsense\Base\ModelException ?
*/
public function streamAccessesAction($uuid = null)
public function streamaccessesAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "")
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /stream_accesses delivers a list of servers with access logs
return $this->list_streams();
} else {
}
elseif (!isset($fileno)) {
return $this->list_stream_logfiles('streamaccess', $uuid);
}
else {
// emulate REST call for a specific log /stream_accesses/uuid
$this->call_configd_stream('streamaccess', $uuid);
$this->get_stream_logs('streamaccess', $uuid, $fileno, $page, $perPage, $query);
}
}
@@ -106,49 +131,112 @@ class LogsController extends ApiControllerBase
* "/" -> list of access logs
* "/uuid" -> conent of error log
* @param null $uuid uuid of the stream server from which the error log should be returned
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return array if feasible, otherwise null and the data is sent directly back
* @throws \OPNsense\Base\ModelException ?
*/
public function streamErrorsAction($uuid = null)
public function streamerrorsAction($uuid = null, $fileno = null, $page = 0, $perPage = 0, $query = "")
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /stream_errors delivers a list of servers with error logs
return $this->list_streams();
} else {
}
elseif (!isset($fileno)) {
return $this->list_stream_logfiles('streamerror', $uuid);
}
else {
// emulate REST call for a specific log /stream_errors/uuid
$this->call_configd_stream('streamerror', $uuid);
$this->get_stream_logs('streamerror', $uuid, $fileno, $page, $perPage, $query);
}
}
/**
* Retrieve log content for HTTP server.
*
* @param $type string access or error for the used log type
* @param $uuid string uuid of the server
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return |null
* @throws \Exception ?
*/
private function call_configd($type, $uuid)
private function get_logs($type, $uuid, $fileno, $page, $perPage, $query)
{
if (!($this->vhost_exists($uuid) || $uuid == 'global')) {
$this->response->setStatusCode(404, "Not Found");
return $this->response->setStatusCode(404, "Not Found");
}
return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid);
$page = intval($page);
$perPage = intval($perPage);
$query = base64_encode(urldecode($query));
return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query");
}
/**
* Retrieve available log files for specific HTTP server uuid.
*
* @param $type string access or error for the used log type
* @param $uuid string uuid of the server
* @return |null
* @throws \Exception ?
*/
private function call_configd_stream($type, $uuid)
private function list_logfiles($type, $uuid)
{
if (!$this->stream_exists($uuid)) {
$this->response->setStatusCode(404, "Not Found");
if (!($this->vhost_exists($uuid) || $uuid == 'global')) {
return $this->response->setStatusCode(404, "Not Found");
}
return $this->sendConfigdToClient('nginx log ' . $type . ' ' . $uuid);
return $this->sendConfigdToClient("nginx listlogs $type $uuid");
}
/**
* Retrieve log content for stream server.
*
* @param $type string access or error for the used log type
* @param $uuid string uuid of the server
* @param $fileno int number of logfile to retrieve
* @param $page int pagination page to retrieve
* @param $perPage int number of entries per page
* @param $query string filter string to apply
* @return |null
* @throws \Exception ?
*/
private function get_stream_logs($type, $uuid, $fileno, $page, $perPage, $query)
{
if (!$this->stream_exists($uuid)) {
return $this->response->setStatusCode(404, "Not Found");
}
$page = intval($page);
$perPage = intval($perPage);
$query = base64_encode(urldecode($query));
return $this->sendConfigdToClient("nginx log $type $uuid $fileno $page $perPage $query");
}
/**
* Retrieve available log files for specific stream server uuid.
*
* @param $type string access or error for the used log type
* @param $uuid string uuid of the server
* @return |null
* @throws \Exception ?
*/
private function list_stream_logfiles($type, $uuid)
{
if (!$this->stream_exists($uuid)) {
return $this->response->setStatusCode(404, "Not Found");
}
return $this->sendConfigdToClient("nginx listlogs $type $uuid");
}
/**
@@ -71,14 +71,6 @@ class IndexController extends \OPNsense\Base\IndexController
$this->view->pick('OPNsense/Nginx/index');
}
/**
* show the nginx logs page /ui/nginx/index/logs
*/
public function logsAction()
{
$this->view->pick('OPNsense/Nginx/logs');
}
/**
* show the nginx TLS handshakes page /ui/nginx/index/tls_handshakes
*/
@@ -0,0 +1,87 @@
<?php
/*
Copyright (C) 2020 Manuel Faux
Copyright (C) 2018-2020 Fabian Franz
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Nginx;
/**
* Class IndexController
* @package OPNsense/Nginx
*/
class LogsController extends \OPNsense\Base\IndexController
{
/**
* show the configuration page /ui/nginx/logs
* @throws \Exception when a form cannot be loaded
*/
public function indexAction()
{
$this->view->log = 'global';
$this->view->pick('OPNsense/Nginx/logs');
}
/**
* show the nginx logs page /ui/nginx/logs/accesses
*/
public function accessesAction()
{
$this->view->log = 'accesses';
$this->view->pick('OPNsense/Nginx/logs');
}
/**
* show the nginx logs page /ui/nginx/logs/errors
*/
public function errorsAction()
{
$this->view->log = 'errors';
$this->view->pick('OPNsense/Nginx/logs');
}
/**
* show the nginx logs page /ui/nginx/logs/stream_accesses
*/
public function streamaccessesAction()
{
$this->view->log = 'stream_accesses';
$this->view->pick('OPNsense/Nginx/logs');
}
/**
* show the nginx logs page /ui/nginx/logs/stream_errors
*/
public function streamerrorsAction()
{
$this->view->log = 'stream_errors';
$this->view->pick('OPNsense/Nginx/logs');
}
}
@@ -2,7 +2,8 @@
/*
Copyright (C) 2018 Fabian Franz
Copyright (C) 2018-2020 Fabian Franz
Copyright (C) 2020 Manuel Faux
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -29,32 +30,11 @@
namespace OPNsense\Nginx;
class AccessLogParser
class AccessLogParser extends LogParserBase
{
private $file_name;
private $result;
private const LogLineRegex = '/(\S+) - (\S+) \[([\d\sa-z\:\-\/\+]+)\] "([^"]+?)" (\d+) (\d+) "([^"]*?)" "([^"]*?)" "([^"]*?)"/i';
function __construct($file_name)
{
$this->file_name = $file_name;
$this->result = array();
$this->parse_file();
}
private function parse_file()
{
$handle = @fopen($this->file_name, 'r');
if ($handle) {
while (($buffer = fgets($handle)) !== false) {
$this->result[] = $this->parse_line($buffer);
}
fclose($handle);
}
}
private function parse_line($line)
protected function parse_line($line)
{
$container = new AccessLogLine();
if (preg_match(self::LogLineRegex, $line, $data)) {
@@ -70,9 +50,4 @@ class AccessLogParser
}
return $container;
}
public function get_result()
{
return $this->result;
}
}
@@ -2,7 +2,8 @@
/*
Copyright (C) 2018 Fabian Franz
Copyright (C) 2018-2020 Fabian Franz
Copyright (C) 2020 Manuel Faux
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -29,21 +30,11 @@
namespace OPNsense\Nginx;
class ErrorLogParser
class ErrorLogParser extends LogParserBase
{
private $file_name;
private $lines;
private $result;
private const LogLineRegex = '/(\S+) (\S+) \[([\d\sa-z\:\-\/\+\#]+)\] ([\S:]+): (.+)/i';
function __construct($file_name)
{
$this->file_name = $file_name;
$this->lines = file($this->file_name);
$this->result = array_map([$this, 'parse_line'], $this->lines);
}
private function parse_line($line)
protected function parse_line($line)
{
$container = new ErrorLogLine();
if (preg_match(self::LogLineRegex, $line, $data)) {
@@ -55,9 +46,4 @@ class ErrorLogParser
}
return $container;
}
public function get_result()
{
return $this->result;
}
}
@@ -0,0 +1,123 @@
<?php
/*
Copyright (C) 2018-2020 Fabian Franz
Copyright (C) 2020 Manuel Faux
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Nginx;
abstract class LogParserBase
{
protected $result;
/**
* Constructs a new LogParserBase instance.
*
* @param $file_name string path to log file of the HTTP server to be parsed
* @param $page int pagination page to retrieve (first page is 0)
* @param $per_page int number of entries per page if positive, retrieve
* all elements otherwise
* @param $query string filter string to apply
*/
function __construct($file_name, $page = 0, $per_page = 0, $query = array())
{
$this->file_name = $file_name;
$this->page = $page;
$this->per_page = $per_page;
$this->query = $query;
$this->page_count = 0;
$this->total_lines = 0;
$this->query_lines = 0;
$this->result = array();
$this->parse_file();
}
/**
* Forward read complete gz compressed logfile into memory, reverse file,
* count lines, save lines which match filter and count matching lines.
*/
private function parse_file()
{
$lines = gzfile($this->file_name);
if ($lines !== false && count($lines) > 0) {
$lines = array_reverse($lines);
$filtering = false;
// Did we receive a non-zero filtering query?
foreach ($this->query as $key => $val) {
if (strlen($val) > 0) {
$filtering = true;
}
}
$cnt = 0;
foreach ($lines as $line) {
$pass = true;
$parsed_line = '';
// Perform filtering if needed
if ($filtering) {
$parsed_line = $this->parse_line($line);
foreach ($this->query as $key => $val) {
$val = (string)$val;
if (!empty($val) && strpos($parsed_line->{$key}, (string)$val) === false) {
$pass = false;
}
}
}
if ($pass) {
if ($this->per_page <= 0 || floor($cnt / $this->per_page) == $this->page) {
// Only parse line if not already parsed due to filtering
if (!$filtering) {
$parsed_line = $this->parse_line($line);
}
$this->result[] = $parsed_line;
}
$cnt++;
}
}
$this->page_count = $this->per_page > 0 ? ceil($cnt / $this->per_page) : 1;
$this->total_lines = count($lines);
$this->query_lines = $cnt;
}
unset($lines);
}
abstract protected function parse_line($line);
public function get_result()
{
return $this->result;
}
}
@@ -2,7 +2,8 @@
/*
Copyright (C) 2018 Fabian Franz
Copyright (C) 2018-2020 Fabian Franz
Copyright (C) 2020 Manuel Faux
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -29,33 +30,11 @@
namespace OPNsense\Nginx;
class StreamAccessLogParser
class StreamAccessLogParser extends LogParserBase
{
private $file_name;
private $result;
private const LogLineRegex = '/(\S+) \[([\d\sa-z\:\-\/\+]+)\] (\S+?) (\d+) (\d+) (\d+) (\d+(?:\.\d+)?)/i';
function __construct($file_name)
{
$this->file_name = $file_name;
$this->result = array();
$this->parse_file();
}
private function parse_file()
{
$handle = @fopen($this->file_name, 'r');
if ($handle) {
while (($buffer = fgets($handle)) !== false) {
$this->result[] = $this->parse_line($buffer);
}
fclose($handle);
}
}
private function parse_line($line)
protected function parse_line($line)
{
$container = new StreamAccessLogLine();
if (preg_match(self::LogLineRegex, $line, $data)) {
@@ -68,9 +47,4 @@ class StreamAccessLogParser
}
return $container;
}
public function get_result()
{
return $this->result;
}
}
@@ -5,7 +5,11 @@
<Banned url="/ui/nginx/index/ban" order="20"/>
<TLSFingerprints VisibleName="TLS Fingerprints" url="/ui/nginx/index/tls_handshakes" order="30"/>
<VTS VisibleName="Traffic Statistic" url="/ui/nginx/index/vts" order="90"/>
<Logs url="/ui/nginx/index/logs" order="100"/>
<HTTPAccessLogs VisibleName="Logs / HTTP Access" url="/ui/nginx/logs/accesses" order="100"/>
<HTTPErrorLogs VisibleName="Logs / HTTP Error" url="/ui/nginx/logs/errors" order="110"/>
<StreamAccessLogs VisibleName="Logs / Stream Access" url="/ui/nginx/logs/stream_accesses" order="120"/>
<StreamErrorLogs VisibleName="Logs / Stream Error" url="/ui/nginx/logs/stream_errors" order="130"/>
<ErrorLogs VisibleName="Logs / Global Error" url="/ui/nginx/logs" order="140"/>
</Nginx>
</Services>
</menu>
@@ -25,7 +25,9 @@
# POSSIBILITY OF SUCH DAMAGE.
#}
<div id="logapplication"></div>
<link rel="stylesheet" href="{{ cache_safe('/ui/css/nginx/logs.css') }}" type="text/css" />
<div id="logapplication" data-log="{{ log }}"></div>
<script src="{{ cache_safe('/ui/js/nginx/lib/lodash.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/lib/backbone-min.js') }}"></script>
+106
View File
@@ -0,0 +1,106 @@
#!/usr/local/bin/php
<?php
/*
* Copyright (C) 2020 Manuel Faux
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once 'config.inc';
use OPNsense\Nginx\Nginx;
$log_prefix = '/var/log/nginx/';
$log_suffix = '.log';
function list_logfiles($prefix) {
global $log_prefix;
$filename = $log_prefix . $prefix;
$result = [];
$files = glob("$filename*", GLOB_NOSORT);
foreach ($files as $file) {
// Extract number of logrotate (e.g. error.log.4.gz) and set -1 for currently active file
$number = (strlen($file) > strlen($filename)) ? substr($file, strlen($filename) + 1, -3) : -1;
$result[$number] = array(
'filename' => substr($file, strlen($log_prefix)),
'date' => ($number >= 0) ? date('d/M/Y', filemtime($file) - 3600) : 'current',
'number' => $number
);
}
ksort($result, SORT_NUMERIC);
$result = array_values($result);
return $result;
}
if ($_SERVER['argc'] < 3) {
die('{"error": "Incorrect amount of parameters given"}');
}
// first parameter: error|access
$mode = $_SERVER['argv'][1];
// second parameter: uuid of server
$server = $_SERVER['argv'][2];
$nginx = new Nginx();
$result = [];
// special case: the global error log
if ($server == 'global') {
$result = list_logfiles('error.log');
}
else {
switch ($mode) {
case 'error':
case 'access':
if ($data = $nginx->getNodeByReference('http_server.' . $server)) {
$server_names = (string)$data->servername;
if (empty($server_names)) {
die('{"error": "The server entry has no server name"}');
}
$log_file_name = basename($server_names) . '.' . $mode . $log_suffix;
$result = list_logfiles($log_file_name);
}
else {
die('{"error": "UUID not found"}');
}
break;
case 'streamerror':
case 'streamaccess':
if ($data = $nginx->getNodeByReference('stream_server.' . $server)) {
$mode = str_replace('stream', '', $mode);
$log_file_name = 'stream_' . $server . '.' . $mode . $log_suffix;
$result = list_logfiles($log_file_name);
} else {
die('{"error": "UUID not found"}');
}
break;
default:
die('{"error": "action (' . $mode . ') not found"}');
}
}
echo json_encode($result);
@@ -2,7 +2,8 @@
<?php
/*
* Copyright (C) 2018 Fabian Franz
* Copyright (C) 2018-2020 Fabian Franz
* Copyright (C) 2020 Manuel Faux
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -37,7 +38,7 @@ use OPNsense\Nginx\StreamAccessLogParser;
$log_prefix = '/var/log/nginx/';
$log_suffix = '.log';
if ($_SERVER['argc'] != 3) {
if ($_SERVER['argc'] < 6) {
die('{"error": "Incorrect amount of parameters given"}');
}
@@ -45,81 +46,92 @@ if ($_SERVER['argc'] != 3) {
$mode = $_SERVER['argv'][1];
// second parameter: uuid of server
$server = $_SERVER['argv'][2];
// third parameter: file number
$file_no = (strlen($_SERVER['argv'][3]) > 0) ? max(intval($_SERVER['argv'][3]), -1) : -1;
// third parameter: current page
$page = max(intval($_SERVER['argv'][4]), 0);
// fourth parameter: lines per page
$per_page = max(intval($_SERVER['argv'][5]), 0);
// fifth parameter: filter query
$query = json_decode(base64_decode($_SERVER['argv'][6]), true);
$nginx = new Nginx();
if (!is_array($query)) {
$query = array();
}
if ($file_no >= 0) {
$log_suffix .= ".$file_no.gz";
}
$result = [];
// special case: the global error log
if ($server == 'global') {
$logparser = new ErrorLogParser($log_prefix . 'error.log');
echo json_encode(empty($logparser->get_result()) ?
array('error' => 'no lines found') :
$logparser->get_result());
exit(0);
$logparser = new ErrorLogParser($log_prefix . 'error' . $log_suffix, $page, $per_page, $query);
}
else {
switch ($mode) {
case 'error':
case 'access':
if ($data = $nginx->getNodeByReference('http_server.' . $server)) {
$server_names = (string)$data->servername;
if (empty($server_names)) {
die('{"error": "The server entry has no server name"}');
}
$log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
break;
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query);
} elseif ($mode == 'access') {
$logparser = new AccessLogParser($log_file_name, $page, $per_page, $query);
}
}
else {
die('{"error": "UUID not found"}');
}
break;
case 'streamerror':
case 'streamaccess':
if ($data = $nginx->getNodeByReference('stream_server.' . $server)) {
$mode = str_replace('stream', '', $mode);
$log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
die('{"error": "file not found"}');
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name, $page, $per_page, $query);
} elseif ($mode == 'access') {
$logparser = new StreamAccessLogParser($log_file_name, $page, $per_page, $query);
}
} else {
die('{"error": "UUID not found"}');
}
break;
default:
die('{"error": "action (' . $mode . ') not found"}');
}
}
switch ($mode) {
case 'error':
case 'access':
if ($data = $nginx->getNodeByReference('http_server.' . $server)) {
$server_names = (string)$data->servername;
if (empty($server_names)) {
die('{"error": "The server entry has no server name"}');
}
$lines = [];
$log_file_name = $log_prefix . basename($server_names) . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
break;
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name);
} elseif ($mode == 'access') {
$logparser = new AccessLogParser($log_file_name);
}
// we cannot parse the file - something went wrong
if ($logparser == null) {
break;
}
$lines = array_merge($lines, $logparser->get_result());
if (empty($lines)) {
$lines['error'] = 'no lines found';
}
echo json_encode($lines);
} else {
die('{"error": "UUID not found"}');
}
break;
case 'streamerror':
case 'streamaccess':
if ($data = $nginx->getNodeByReference('stream_server.' . $server)) {
$lines = [];
$mode = str_replace('stream', '', $mode);
$log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
die('{"error": "file not found"}');
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name);
} elseif ($mode == 'access') {
$logparser = new StreamAccessLogParser($log_file_name);
}
// we cannot parse the file - something went wrong
if ($logparser == null) {
break;
}
$lines = array_merge($lines, $logparser->get_result());
if (empty($lines)) {
$lines['error'] = 'no lines found';
}
echo json_encode($lines);
} else {
die('{"error": "UUID not found"}');
}
break;
default:
die('{"error": "action (' . $mode . ') not found"}');
// we cannot parse the file - something went wrong
if ($logparser === null) {
$result['error'] = 'cannot retrieve requested logs';
}
else {
$result['lines'] = $logparser->get_result();
$result['pages'] = $logparser->page_count;
$result['total'] = $logparser->total_lines;
$result['found'] = $logparser->query_lines;
$result['returned'] = count($result['lines']);
$result['query'] = json_encode($query);
}
echo json_encode($result);
@@ -28,9 +28,15 @@ type:script_output
[log]
command:/usr/local/opnsense/scripts/nginx/read_log.php
parameters: %s %s %s %s %s %s
type:script_output
message:querying nginx %s log for %s rotate %s (page %s of %s with filter %s)
[listlogs]
command:/usr/local/opnsense/scripts/nginx/list_logs.php
parameters: %s %s
type:script_output
message:restarting nginx
message:listing nginx %s log for %s
[tls_handshakes]
command:cat /var/log/nginx/handshakes.json
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 Manuel Faux
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
thead.sticky-top th {
position: sticky;
top: 80px;
background-color: white;
}
thead.sticky-top tr:first-child th {
top: 50px;
}
tfoot.sticky-bottom th {
position: sticky;
bottom: 50px;
background-color: white;
font-weight: normal;
font-family: inherit;
}
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
export const defaultEndpoints = new Backbone.Collection([
{
"name": 'HTTP Access Logs',
"logType" : 'accesses'
},
{
"name": 'HTTP Error Logs',
"logType" : 'errors'
},
{
"name": 'Stream Access Logs',
"logType" : 'stream_accesses'
},
{
"name": 'Stream Error Logs',
"logType" : 'stream_errors'
}
]);
@@ -7,42 +7,58 @@ let LogCategoryList = Backbone.View.extend({
className: "nav nav-tabs",
initialize: function(data) {
this.listenTo(this.collection, "sync", this.render);
this.listenTo(this.collection, "update", this.render);
this.logview = data.logview;
this.logType = data.logType;
},
render: function() {
this.$el.attr('role', 'tablist');
this.$el.html('');
this.collection.forEach((element) => this.render_one(element));
this.render_single_tabs();
if (this.logType == 'global') {
this.render_global_error_tab();
}
else {
this.collection.forEach((element) => this.render_one_server(element));
}
},
render_one: function(element) {
const servers = new LogCollection(
render_one_server: function(element) {
const files = new LogCollection(
{
uuid: element.get('url'),
logType: element.get('logType')
uuid: element.get('id'),
logType: this.logType
}
);
const logList = new TabLogList({
collection: servers,
collection: files,
model: element,
logType: this.logType,
logview: this.logview
});
this.$el.append(logList.$el);
servers.fetch();
files.fetch();
},
render_single_tabs: function () {
const single_tab = new SingleTab({
logview: this.logview,
log_name: 'global',
visible_name: 'Global Error Log',
log_type: 'errors'});
single_tab.render();
this.$el.append(single_tab.$el);
render_global_error_tab: function () {
const files = new LogCollection(
{
uuid: 'global',
logType: 'errors'
}
);
const logList = new TabLogList({
collection: files,
model: new Backbone.Model({
server_name: 'Global Error Log',
id: 'global'
}),
logType: 'errors',
logview: this.logview
});
this.$el.append(logList.$el);
files.fetch();
}
});
@@ -33,29 +33,30 @@ const LogView = Backbone.View.extend({
className: 'content-box tab-content',
events: {
"keyup .filter input": "update_filter",
"click #paging_first": "page_first",
"click #paging_back": "page_back",
"click #refresh": "update",
"click #paging_forward": "page_forward",
"click #paging_last": "page_last",
"change #entrycount": "change_entry_count",
},
page_entry_count: 100,
current_page: 0,
current_filtered_collection: null,
filter_delay: -1,
initialize: function() {
this.collection = new LogLinesCollection();
this.filter_model = new Backbone.Model();
this.listenTo(this.collection, "sync", this.clear_and_render);
this.listenTo(this.collection, "update", this.clear_and_render);
this.listenTo(this.filter_model, "change", this.clear_and_render);
this.listenTo(this.collection, "sync", this.render);
this.listenTo(this.collection, "update", this.render);
this.listenTo(this.collection.filter_model, "change", this.render);
this.type = '';
},
render: function() {
let tbody = this.$el.find('tbody');
let tbody = this.$('tbody');
if (tbody.length < 1) {
if (this.collection.length !== 0) {
this.$el.html(logViewer({log_type: this.type, model: this.filter_model}));
tbody = this.$el.find('tbody');
this.$el.html(logViewer({log_type: this.type, model: this.collection.filter_model}));
tbody = this.$('tbody');
} else {
this.$el.html(noDataAvailable);
}
@@ -63,57 +64,104 @@ const LogView = Backbone.View.extend({
else {
tbody.html('');
}
if (this.collection.length !== 0) {
if (this.current_filtered_collection == null) {
this.current_filtered_collection = this.collection.filter_collection(this.filter_model);
this.collection.forEach(
(model) => this.render_one(tbody, model)
);
}
const index_begin = this.current_page * this.page_entry_count;
const index_end = index_begin + this.page_entry_count;
this.current_filtered_collection.slice(index_begin, index_end).forEach(
(model) => this.render_one(tbody, model)
);
}
this.$('#entrycountdisplay').html(this.page_entry_count);
this.$('#currentpage').html(this.current_page + 1);
this.$('#pagecount').html(this.collection.page_count);
this.$('#totalcount').html(this.collection.total_entries);
this.$('#resultcount').html(this.collection.displayed_entries);
if (this.current_page >= this.collection.page_count - 1) {
this.$('#paging_last').addClass("disabled");
this.$('#paging_forward').addClass("disabled");
}
else {
this.$('#paging_last').removeClass("disabled");
this.$('#paging_forward').removeClass("disabled");
}
if (this.current_page <= 0) {
this.$('#paging_back').addClass("disabled");
this.$('#paging_first').addClass("disabled");
}
else {
this.$('#paging_back').removeClass("disabled");
this.$('#paging_first').removeClass("disabled");
}
},
render_one: function(parent_element, model) {
const logline = new LogViewLine({type: this.type, model: model});
logline.render();
parent_element.append(logline.$el);
},
get_log: function(type, uuid) {
get_log: function(type, uuid, fileNo) {
this.collection.uuid = uuid;
this.collection.logType = type;
this.collection.fileNo = fileNo;
this.type = type;
this.current_page = 0;
this.$el.html('');
this.filter_model.clear();
this.collection.filter_model.clear();
this.update();
},
update: function () {
this.collection.page = this.current_page;
this.collection.pageSize = this.page_entry_count;
this.collection.fetch();
},
clear_and_render: function() {
this.current_filtered_collection = null;
this.render();
},
update_filter: function (event) {
clearTimeout(this.filter_delay);
const element = event.target;
this.filter_model.set(element.name, $(element).val());
this.collection.filter_model.set(element.name, $(element).val());
this.current_page = 0;
// Delay update to avoid multiple requests during typing
this.filter_delay = setTimeout(function(instance) {
instance.update();
}, 500, this);
},
page_first: function () {
this.current_page = 0;
this.update();
},
page_back: function () {
if (this.current_page > 0) {
this.current_page--;
this.render();
this.update();
}
},
page_forward: function () {
if ((this.current_page + 1) * this.page_entry_count < this.collection.length) {
if (this.current_page < this.collection.page_count) {
this.current_page++;
this.render();
this.update();
}
},
page_last: function () {
this.current_page = this.collection.page_count - 1;
this.update();
},
change_entry_count: function (event) {
this.page_entry_count = event.target.value;
this.current_page = 0;
this.render();
this.update();
}
});
export default LogView;
@@ -9,8 +9,8 @@ let TabLogList = Backbone.View.extend({
},
initialize: function(data) {
this.listenTo(this.collection, "sync", this.render);
this.listenTo(this.collection, "update", this.render);
this.logType = data.logType;
this.logview = data.logview;
},
@@ -21,21 +21,31 @@ let TabLogList = Backbone.View.extend({
renderCollection: function() {
this.$el.addClass('dropdown');
if (this.model.get('id') == "global") {
this.$el.addClass('active');
this.logview.get_log('errors', 'global', -1);
}
this.$el.html('');
this.$el.append(
TabTemplateCollection({model: this.collection, name: this.model.attributes.name})
TabTemplateCollection({
model: this.collection,
id: this.model.get('id'),
name: this.model.has('server_name') ? this.model.get('server_name') : "Port " + this.model.get('port')
})
);
},
mainMenuClick: function () {
if (this.collection.models[0]) {
this.handleElementClick(this.collection.models[0].id);
this.handleElementClick(this.model.get('id'), this.collection.models[0].get('number'));
$(`#tab_${this.model.get('id')} li`).removeClass('active');
$(`#subtab_item_${this.model.get('id')}_${this.collection.models[0].get('number')}`).parent().addClass('active');
}
},
menuEntryClick: function (event) {
this.handleElementClick(event.target.dataset['modelUuid']);
this.handleElementClick(event.target.dataset['modelUuid'], event.target.dataset['modelFileno']);
},
handleElementClick: function (uuid) {
this.logview.get_log(this.model.get('logType'), uuid);
handleElementClick: function (uuid, fileNo) {
this.logview.get_log(this.logType, uuid, fileNo);
}
});
export default TabLogList;

Some files were not shown because too many files have changed in this diff Show More