From 3c7d3502d0162b66d55c741be1eefd4ce31955ad Mon Sep 17 00:00:00 2001 From: Manuel Faux Date: Thu, 9 Dec 2021 13:08:04 +0100 Subject: [PATCH] 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 --- www/nginx/Makefile | 2 +- www/nginx/pkg-descr | 4 + .../OPNsense/Nginx/Api/LogsController.php | 128 +++++++++++--- .../OPNsense/Nginx/IndexController.php | 8 - .../OPNsense/Nginx/LogsController.php | 87 ++++++++++ .../OPNsense/Nginx/AccessLogParser.php | 33 +--- .../library/OPNsense/Nginx/ErrorLogParser.php | 22 +-- .../library/OPNsense/Nginx/LogParserBase.php | 123 ++++++++++++++ .../OPNsense/Nginx/StreamAccessLogParser.php | 34 +--- .../app/models/OPNsense/Nginx/Menu/Menu.xml | 6 +- .../mvc/app/views/OPNsense/Nginx/logs.volt | 4 +- .../src/opnsense/scripts/nginx/list_logs.php | 106 ++++++++++++ .../src/opnsense/scripts/nginx/read_log.php | 156 ++++++++++-------- .../service/conf/actions.d/actions_nginx.conf | 8 +- www/nginx/src/opnsense/www/css/nginx/logs.css | 43 +++++ .../www/js/nginx/dist/logviewer.min.js | 2 +- .../src/opnsense/www/js/nginx/src/config.js | 18 -- .../nginx/src/controller/LogCategoryList.js | 50 ++++-- .../www/js/nginx/src/controller/LogView.js | 100 ++++++++--- .../www/js/nginx/src/controller/TabLogList.js | 22 ++- .../opnsense/www/js/nginx/src/logviewer.js | 30 +++- .../www/js/nginx/src/models/LogCollection.js | 3 +- .../js/nginx/src/models/LogLinesCollection.js | 36 ++-- .../nginx/src/models/LogServerCollection.js | 13 ++ .../www/js/nginx/src/models/LogServerMenu.js | 2 + .../js/nginx/src/templates/TabCollection.html | 9 +- .../www/js/nginx/src/templates/logviewer.html | 66 ++++---- 27 files changed, 802 insertions(+), 313 deletions(-) create mode 100644 www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php create mode 100644 www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php create mode 100755 www/nginx/src/opnsense/scripts/nginx/list_logs.php create mode 100644 www/nginx/src/opnsense/www/css/nginx/logs.css delete mode 100644 www/nginx/src/opnsense/www/js/nginx/src/config.js create mode 100644 www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js create mode 100644 www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js diff --git a/www/nginx/Makefile b/www/nginx/Makefile index 1fa6c45f0..290dec87f 100644 --- a/www/nginx/Makefile +++ b/www/nginx/Makefile @@ -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 diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index 3afedea29..165ae90fc 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -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 diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php index 765c1be82..c340ba5fb 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/LogsController.php @@ -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"); } /** diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index e80537625..410e04989 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -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 */ diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php new file mode 100644 index 000000000..fec65f18d --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/LogsController.php @@ -0,0 +1,87 @@ +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'); + } +} diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php index 5f6ed187c..f2d94de63 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/AccessLogParser.php @@ -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; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php index fd272a4d0..99336e2b6 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/ErrorLogParser.php @@ -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; - } } diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php new file mode 100644 index 000000000..9c38adcfa --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/LogParserBase.php @@ -0,0 +1,123 @@ +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; + } +} + diff --git a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php index 37726d704..f6974d6e0 100644 --- a/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php +++ b/www/nginx/src/opnsense/mvc/app/library/OPNsense/Nginx/StreamAccessLogParser.php @@ -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; - } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml index d9f582220..bbe625bd7 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Menu/Menu.xml @@ -5,7 +5,11 @@ - + + + + + diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt index 125fc5d63..b920f706b 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt @@ -25,7 +25,9 @@ # POSSIBILITY OF SUCH DAMAGE. #} -
+ + +
diff --git a/www/nginx/src/opnsense/scripts/nginx/list_logs.php b/www/nginx/src/opnsense/scripts/nginx/list_logs.php new file mode 100755 index 000000000..c3b94e9dd --- /dev/null +++ b/www/nginx/src/opnsense/scripts/nginx/list_logs.php @@ -0,0 +1,106 @@ +#!/usr/local/bin/php + 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); diff --git a/www/nginx/src/opnsense/scripts/nginx/read_log.php b/www/nginx/src/opnsense/scripts/nginx/read_log.php index 488c018ef..1ee5e4030 100755 --- a/www/nginx/src/opnsense/scripts/nginx/read_log.php +++ b/www/nginx/src/opnsense/scripts/nginx/read_log.php @@ -2,7 +2,8 @@ 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); diff --git a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf index 1cb7927e5..ed34d4971 100644 --- a/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf +++ b/www/nginx/src/opnsense/service/conf/actions.d/actions_nginx.conf @@ -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 diff --git a/www/nginx/src/opnsense/www/css/nginx/logs.css b/www/nginx/src/opnsense/www/css/nginx/logs.css new file mode 100644 index 000000000..c8bf446a0 --- /dev/null +++ b/www/nginx/src/opnsense/www/css/nginx/logs.css @@ -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; +} diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js index 8bdd4e6c7..a52439ce8 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.min.js @@ -1 +1 @@ -!function(e){var t={};function n(l){if(t[l])return t[l].exports;var i=t[l]={i:l,l:!1,exports:{}};return e[l].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(l,i,function(t){return e[t]}.bind(null,i));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=26)}([function(e,t,n){var l=n(2),i=n(4),o=/[&<>"']/g,r=RegExp(o.source);e.exports=function(e){return(e=i(e))&&r.test(e)?e.replace(o,l):e}},function(e,t,n){var l=n(6).Symbol;e.exports=l},function(e,t,n){var l=n(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=l},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var l=n(5);e.exports=function(e){return null==e?"":l(e)}},function(e,t,n){var l=n(1),i=n(9),o=n(10),r=n(11),_=1/0,a=l?l.prototype:void 0,s=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(o(t))return i(t,e)+"";if(r(t))return s?s.call(t):"";var n=t+"";return"0"==n&&1/t==-_?"-0":n}},function(e,t,n){var l=n(7),i="object"==typeof self&&self&&self.Object===Object&&self,o=l||i||Function("return this")();e.exports=o},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,l=null==e?0:e.length,i=Array(l);++n\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_sent"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_received"))?"":__t)+'\n'+(null==(__t=model.escape("session_time"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj){let count=0;__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?(count=5,__p+="\n \n \n \n \n \n "):"accesses"===log_type?(count=9,__p+="\n \n \n \n \n \n \n \n \n \n "):(count=6,__p+="\n \n \n \n \n \n \n "),__p+='\n \n \n ',"errors"===log_type||"stream_errors"===log_type?__p+='\n \n \n \n \n \n ':"accesses"===log_type?__p+='\n \n \n \n \n \n \n \n \n \n ':__p+='\n \n \n \n \n \n \n ',__p+='\n \n \n \n \n \n \n \n \n \n \n \n
DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest LineTimeRemote IPStatusBytes SentBytes ReceivedSession Time
\n \n \n \n Entries per page:\n \n \n \n
\n'}return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
\n No data available...\n
\n';return __p}},,,,function(e,t,n){"use strict";n.r(t);const l=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"}]);var i=Backbone.Model.extend({});var o=Backbone.Collection.extend({model:i,url:function(){return"/api/nginx/logs/"+this.logType},initialize:function(e){this.logType=e.logType}}),r=n(16),_=n.n(r);var a=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick"},initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),this.$el.html(""),this.$el.append(_()({model:this.collection,name:this.model.attributes.name}))},mainMenuClick:function(){this.collection.models[0]&&this.handleElementClick(this.collection.models[0].id)},menuEntryClick:function(e){this.handleElementClick(e.target.dataset.modelUuid)},handleElementClick:function(e){this.logview.get_log(this.model.get("logType"),e)}}),s=n(17),c=n.n(s);var u=Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(c()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var d=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"sync",this.render),this.listenTo(this.collection,"update",this.render),this.logview=e.logview},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),this.collection.forEach(e=>this.render_one(e)),this.render_single_tabs()},render_one:function(e){const t=new o({uuid:e.get("url"),logType:e.get("logType")}),n=new a({collection:t,model:e,logview:this.logview});this.$el.append(n.$el),t.fetch()},render_single_tabs:function(){const e=new u({logview:this.logview,log_name:"global",visible_name:"Global Error Log",log_type:"errors"});e.render(),this.$el.append(e.$el)}}),p=n(18),m=n.n(p),h=n(19),f=n.n(h),b=n(20),g=n.n(b),v=n(21),y=n.n(v);var x=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:x,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(){this.logType="none",this.uuid="none"},parse:function(e){return"error"in e?[]:e},filter_collection:function(e){const t=e.keys();return this.filter(function(n){if(!n)return!1;for(let l=0;lthis.render_one(e,t))}},render_one:function(e,t){const n=new T({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t){this.collection.uuid=t,this.collection.logType=e,this.type=e,this.$el.html(""),this.filter_model.clear(),this.update()},update:function(){this.collection.fetch()},clear_and_render:function(){this.current_filtered_collection=null,this.render()},update_filter:function(e){const t=e.target;this.filter_model.set(t.name,$(t).val())},page_back:function(){this.current_page>0&&(this.current_page--,this.render())},page_forward:function(){(this.current_page+1)*this.page_entry_count"']/g,o=RegExp(r.source);e.exports=function(e){return(e=l(e))&&o.test(e)?e.replace(r,i):e}},function(e,t,n){var i=n(6).Symbol;e.exports=i},function(e,t,n){var i=n(3)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=i},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var i=n(5);e.exports=function(e){return null==e?"":i(e)}},function(e,t,n){var i=n(1),l=n(9),r=n(10),o=n(11),a=1/0,s=i?i.prototype:void 0,_=s?s.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(r(t))return l(t,e)+"";if(o(t))return _?_.call(t):"";var n=t+"";return"0"==n&&1/t==-a?"-0":n}},function(e,t,n){var i=n(7),l="object"==typeof self&&self&&self.Object===Object&&self,r=i||l||Function("return this")();e.exports=r},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(8))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,i=null==e?0:e.length,l=Array(i);++n\r\n \r\n\r\n'+(null==(__t=_.escape(name))?"":__t)+'\r\n\r\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n '+(null==(__t=_.escape(name))?"":__t)+"\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_sent"))?"":__t)+'\n'+(null==(__t=model.escape("bytes_received"))?"":__t)+'\n'+(null==(__t=model.escape("session_time"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj){let count=0;__p+='\r\n \r\n \r\n ',"errors"===log_type||"stream_errors"===log_type?(count=5,__p+="\r\n \r\n \r\n \r\n \r\n \r\n "):"accesses"===log_type?(count=9,__p+="\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n "):(count=6,__p+="\r\n \r\n \r\n \r\n \r\n \r\n \r\n "),__p+='\r\n \r\n \r\n ',"errors"===log_type||"stream_errors"===log_type?__p+='\r\n \r\n \r\n \r\n \r\n \r\n ':"accesses"===log_type?__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ':__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n ',__p+='\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest LineTimeRemote IPStatusBytes SentBytes ReceivedSession Time
\r\n \r\n \r\n \r\n \r\n \r\n \r\n Page 1/0\r\n \r\n \r\n \r\n \r\n \r\n Found lines: 0/0
\r\n'}return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+='
\n No data available...\n
\n';return __p}},,,,function(e,t,n){"use strict";n.r(t);var i=Backbone.Model.extend({});var l=Backbone.Collection.extend({model:i,url:function(){return`/api/nginx/logs/${this.logType}`},initialize:function(e){this.logType=e.logType}}),r=Backbone.Model.extend({});var o=Backbone.Collection.extend({model:r,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(e){this.logType=e.logType,this.uuid=e.uuid}}),a=n(16),s=n.n(a);var _=Backbone.View.extend({tagName:"li",events:{"click .mainentry":"mainMenuClick","click .menuEntry":"menuEntryClick"},initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logType=e.logType,this.logview=e.logview},render:function(){this.$el.html(""),this.renderCollection()},renderCollection:function(){this.$el.addClass("dropdown"),"global"==this.model.get("id")&&(this.$el.addClass("active"),this.logview.get_log("errors","global",0)),this.$el.html(""),this.$el.append(s()({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(){this.collection.models[0]&&(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(e){this.handleElementClick(e.target.dataset.modelUuid,e.target.dataset.modelFileno)},handleElementClick:function(e,t){this.logview.get_log(this.logType,e,t)}}),c=n(17),u=n.n(c);Backbone.View.extend({tagName:"li",initialize:function(e){this.logview=e.logview,this.log_name=e.log_name,this.visible_name=e.visible_name,this.log_type=e.log_type},events:{"click .mainentry":"handleElementClick"},log_name:null,log_type:null,visible_name:null,render:function(){this.$el.html(u()({name:this.visible_name}))},handleElementClick:function(){this.logview.get_log(this.log_type,this.log_name)}});var p=Backbone.View.extend({tagName:"ul",className:"nav nav-tabs",initialize:function(e){this.listenTo(this.collection,"update",this.render),this.logview=e.logview,this.logType=e.logType},render:function(){this.$el.attr("role","tablist"),this.$el.html(""),"global"==this.logType?this.render_global_error_tab():this.collection.forEach(e=>this.render_one_server(e))},render_one_server:function(e){const t=new o({uuid:e.get("id"),logType:this.logType}),n=new _({collection:t,model:e,logType:this.logType,logview:this.logview});this.$el.append(n.$el),t.fetch()},render_global_error_tab:function(){const e=new o({uuid:"global",logType:"errors"}),t=new _({collection:e,model:new Backbone.Model({server_name:"Global Error Log",id:"global"}),logType:"errors",logview:this.logview});this.$el.append(t.$el),e.fetch()}}),d=n(18),h=n.n(d),m=n(19),g=n.n(m),b=n(20),f=n.n(b),y=n(21),v=n.n(y);var x=Backbone.Model.extend({});var w=Backbone.Collection.extend({model:x,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`},initialize:function(){this.logType="none",this.uuid="none",this.fileNo=-1,this.page=0,this.pageSize=0,this.filter_model=new Backbone.Model},parse:function(e){return"error"in e?[]:(this.page_count=e.pages,this.total_entries=e.total,this.displayed_entries=e.found,e.lines)},create_filter:function(){return encodeURIComponent(JSON.stringify(this.filter_model))}}),k=n(22),j=n.n(k);const T=Backbone.View.extend({tagName:"tr",initialize:function(e){this.type=e.type},render:function(){this.$el.html(this.get_template()({model:this.model}))},get_template:function(){return"accesses"===this.type?h.a:"stream_accesses"===this.type?g.a:f.a}});const C=new(Backbone.View.extend({tagName:"div",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,filter_delay:-1,initialize:function(){this.collection=new w,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 e=this.$("tbody");e.length<1?0!==this.collection.length?(this.$el.html(v()({log_type:this.type,model:this.collection.filter_model})),e=this.$("tbody")):this.$el.html(j.a):e.html(""),0!==this.collection.length&&null==this.current_filtered_collection&&this.collection.forEach(t=>this.render_one(e,t)),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),this.current_page>=this.collection.page_count-1?(this.$("#paging_last").addClass("disabled"),this.$("#paging_forward").addClass("disabled")):(this.$("#paging_last").removeClass("disabled"),this.$("#paging_forward").removeClass("disabled")),this.current_page<=0?(this.$("#paging_back").addClass("disabled"),this.$("#paging_first").addClass("disabled")):(this.$("#paging_back").removeClass("disabled"),this.$("#paging_first").removeClass("disabled"))},render_one:function(e,t){const n=new T({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t,n){this.collection.uuid=t,this.collection.logType=e,this.collection.fileNo=n,this.type=e,this.current_page=0,this.$el.html(""),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()},update_filter:function(e){clearTimeout(this.filter_delay);const t=e.target;this.collection.filter_model.set(t.name,$(t).val()),this.current_page=0,this.filter_delay=setTimeout(function(e){e.update()},500,this)},page_first:function(){this.current_page=0,this.update()},page_back:function(){this.current_page>0&&(this.current_page--,this.update())},page_forward:function(){this.current_page 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(); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js index fe82540f8..c0792ce47 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/LogView.js @@ -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; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js index 60e1d3ba2..3533df639 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/TabLogList.js @@ -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; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js index dad07be5f..0051e00e4 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/logviewer.js @@ -1,15 +1,33 @@ -import {defaultEndpoints} from './config'; +import LogServerCollection from './models/LogServerCollection'; import LogCategoryList from './controller/LogCategoryList'; import LogView from './controller/LogView'; +// Skeleton with header (navigation) and footer (pagination) const logview = new LogView(); - +// Get type of log to display from volt view (data-log HTML attribute) +const type = $('#logapplication').data('log'); +// Query (HTTP or stream) server list +const servers = new LogServerCollection({ + logType: type +}); +// Render tabs with server logs (one tab per server) const menu = new LogCategoryList({ - collection: defaultEndpoints, - logview: logview + collection: servers, + logview: logview, + logType: type // 'errors', 'accesses' or 'global' }); -$(document.getElementById('logapplication')) +// Place log application to volt template +$('#logapplication') .append(menu.$el) .append(logview.$el); -menu.render(); + +if (type != 'global') { + // Global error log does not require server list + servers.fetch(); +} +else { + // Update of server list triggers render() which does not + // occur for global error log + menu.render(); +} diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js index 19febb1f3..0221235e1 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogCollection.js @@ -3,10 +3,11 @@ import LogFileMenuEntry from './LogFileMenuEntry'; const LogCollection = Backbone.Collection.extend({ model: LogFileMenuEntry, url: function () { - return '/api/nginx/logs/' + this.logType; + return `/api/nginx/logs/${this.logType}/${this.uuid}`; }, initialize: function (params) { this.logType = params.logType; + this.uuid = params.uuid; } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js index 2e3eb2921..806f0c077 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogLinesCollection.js @@ -3,39 +3,29 @@ import LogLine from "./LogLine"; const LogLinesCollection = Backbone.Collection.extend({ model: LogLine, url: function () { - return `/api/nginx/logs/${this.logType}/${this.uuid}`; + return `/api/nginx/logs/${this.logType}/${this.uuid}/${this.fileNo}/${this.page}/${this.pageSize}/${this.create_filter()}`; }, initialize: function () { this.logType = 'none'; this.uuid = 'none'; + this.fileNo = -1; + this.page = 0; + this.pageSize = 0; + this.filter_model = new Backbone.Model(); }, parse: function(response) { if ('error' in response) { return []; } - return response; + else { + this.page_count = response.pages; + this.total_entries = response.total; + this.displayed_entries = response.found; + return response.lines; + } }, - filter_collection: function(filter_model) { - const filter_model_keys = filter_model.keys(); - return this.filter(function (model) { - if (!model) { - return false; - } - for (let i = 0; i < filter_model_keys.length; i++) { - const property = filter_model_keys[i]; - if (typeof(filter_model.get(property)) !== "string" - || filter_model.get(property).length === 0) { - continue; - } - if (!model.has(property)) { - return false; - } - if (!model.get(property).includes(filter_model.get(property))) { - return false; - } - } - return true; - }); + create_filter: function() { + return encodeURIComponent(JSON.stringify(this.filter_model)); } }); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js new file mode 100644 index 000000000..9fdfdc75b --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerCollection.js @@ -0,0 +1,13 @@ +import LogServerMenu from './LogServerMenu'; + +const LogServerCollection = Backbone.Collection.extend({ + model: LogServerMenu, + url: function () { + return `/api/nginx/logs/${this.logType}`; + }, + initialize: function (params) { + this.logType = params.logType; + } +}); + +export default LogServerCollection; diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js new file mode 100644 index 000000000..170a06af6 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/LogServerMenu.js @@ -0,0 +1,2 @@ +export default Backbone.Model.extend({ +}); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html index ee317a10c..d5ea04354 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html +++ b/www/nginx/src/opnsense/www/js/nginx/src/templates/TabCollection.html @@ -37,15 +37,16 @@ <%= _.escape(name) %> -