From 607a567090a243df2592019cfb57736b0bd867b3 Mon Sep 17 00:00:00 2001 From: Tony Murray Date: Thu, 18 Oct 2018 21:08:46 -0500 Subject: [PATCH] Don't check file permissions on every request, handle failures (#9264) * Don't check file permissions on every request, handle failures Improve error page visually * only print minimal mkdir * invert file_exists check, whoops * docblock * revert accidental changes * rename variable * Change database errors to use the new layout * Add support url to the default layout * Replaced \n for && in fix for user perms * fix web output --- .../Exceptions/DatabaseConnectException.php | 7 +- LibreNMS/ValidationResult.php | 12 +- LibreNMS/Validations/User.php | 8 +- app/Checks.php | 189 +++++++++--------- app/Exceptions/Handler.php | 44 ++-- html/pages/validate.inc.php | 6 +- resources/views/auth/2fa.blade.php | 2 +- .../views/errors/file_permissions.blade.php | 16 ++ resources/views/errors/generic.blade.php | 72 +------ .../views/errors/static/file_permissions.html | 78 ++++++++ resources/views/layouts/error.blade.php | 75 +++++++ resources/views/layouts/librenmsv1.blade.php | 100 ++++----- validate.php | 2 +- 13 files changed, 382 insertions(+), 229 deletions(-) create mode 100644 resources/views/errors/file_permissions.blade.php create mode 100644 resources/views/errors/static/file_permissions.html create mode 100644 resources/views/layouts/error.blade.php diff --git a/LibreNMS/Exceptions/DatabaseConnectException.php b/LibreNMS/Exceptions/DatabaseConnectException.php index 76e566c398..c8dccfa8be 100644 --- a/LibreNMS/Exceptions/DatabaseConnectException.php +++ b/LibreNMS/Exceptions/DatabaseConnectException.php @@ -41,9 +41,10 @@ class DatabaseConnectException extends \Exception 'message' => 'Error connecting to database: ' . $this->getMessage(), ]); } else { - $message = "

Error connecting to database.

"; - $message .= $this->getMessage(); - return response($message); + return response()->view('errors.generic', [ + 'title' => 'Error connecting to database.', + 'content' => $this->getMessage(), + ]); } } } diff --git a/LibreNMS/ValidationResult.php b/LibreNMS/ValidationResult.php index 7dfe96bf8f..25aee7a918 100644 --- a/LibreNMS/ValidationResult.php +++ b/LibreNMS/ValidationResult.php @@ -132,6 +132,13 @@ class ValidationResult return $this->fix; } + /** + * The commands (generally) to fix the issue. + * If there are multiple, use an array. + * + * @param string|array $fix + * @return ValidationResult $this + */ public function setFix($fix) { $this->fix = $fix; @@ -146,7 +153,10 @@ class ValidationResult c_echo(str_pad('[' . $this->getStatusText($this->status) . ']', 12) . $this->message . PHP_EOL); if (isset($this->fix)) { - c_echo("\t[%BFIX%n] %B$this->fix%n\n"); + c_echo("\t[%BFIX%n]: \n"); + foreach ((array)$this->fix as $fix) { + c_echo("\t%B$fix%n\n"); + } } if (!empty($this->list)) { diff --git a/LibreNMS/Validations/User.php b/LibreNMS/Validations/User.php index 74ce2fc572..b11a8932e9 100644 --- a/LibreNMS/Validations/User.php +++ b/LibreNMS/Validations/User.php @@ -67,9 +67,11 @@ class User extends BaseValidation $rrd_dir = Config::get('rrd_dir', "$dir/rrd"); // generic fix - $fix = "sudo chown -R $lnms_username:$lnms_groupname $dir\n" . - "sudo setfacl -d -m g::rwx $rrd_dir $log_dir $dir/bootstrap/cache/ $dir/storage/\n" . - "sudo chmod -R ug=rwX $rrd_dir $log_dir $dir/bootstrap/cache/ $dir/storage/\n"; + $fix = [ + "sudo chown -R $lnms_username:$lnms_groupname $dir", + "sudo setfacl -d -m g::rwx $rrd_dir $log_dir $dir/bootstrap/cache/ $dir/storage/", + "sudo chmod -R ug=rwX $rrd_dir $log_dir $dir/bootstrap/cache/ $dir/storage/", + ]; $find_result = rtrim(`find $dir \! -user $lnms_username -o \! -group $lnms_groupname 2> /dev/null`); if (!empty($find_result)) { diff --git a/app/Checks.php b/app/Checks.php index a19027f19a..0e08879d20 100644 --- a/app/Checks.php +++ b/app/Checks.php @@ -30,8 +30,8 @@ use App\Models\Notification; use Auth; use Cache; use Carbon\Carbon; -use Dotenv\Dotenv; use LibreNMS\Config; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Toastr; class Checks @@ -39,97 +39,13 @@ class Checks public static function preBoot() { // check php extensions - $missing = self::missingPhpExtensions(); - - if (!empty($missing)) { + if ($missing = self::missingPhpExtensions()) { self::printMessage( "Missing PHP extensions. Please install and enable them on your LibreNMS server.", $missing, true ); } - - // check file/folder permissions - $check_folders = [ - self::basePath('bootstrap/cache'), - self::basePath('storage'), - self::basePath('storage/framework/sessions'), - self::basePath('storage/framework/views'), - self::basePath('storage/framework/cache'), - self::basePath('logs'), - ]; - - $check_files = [ - self::basePath('logs/librenms.log'), // This file is important because Laravel needs to be able to write to it - ]; - - // check that each is writable - $check_folders = array_filter($check_folders, function ($path) { - return !is_writable($path); - }); - - $check_files = array_filter($check_files, function ($path) { - return file_exists($path) xor is_writable($path); - }); - - if (!empty($check_folders) || !empty($check_files)) { - // only operate on parent directories, not files - $check = array_unique(array_merge($check_folders, array_map('dirname', $check_files))); - - // load .env, it isn't loaded - $dotenv = new Dotenv(__DIR__ . '/../'); - $dotenv->load(); - - $user = env('LIBRENMS_USER', 'librenms'); - $group = env('LIBRENMS_GROUP', $user); - - // build chown message - $dirs = implode(' ', $check); - $chown_commands = [ - "chown -R $user:$group $dirs", - "setfacl -R -m g::rwx $dirs", - "setfacl -d -m g::rwx $dirs", - ]; - - $current_groups = explode(' ', trim(exec('groups'))); - if (!in_array($group, $current_groups)) { - $current_user = trim(exec('whoami')); - $chown_commands[] = "usermod -a -G $group $current_user"; - } - - - //check for missing directories - $missing = array_filter($check, function ($file) { - return !file_exists($file); - }); - - if (!empty($missing)) { - array_unshift($chown_commands, 'mkdir -p ' . implode(' ', $missing)); - } - - $short_dirs = implode(', ', array_map(function ($dir) { - return str_replace(self::basePath(), '', $dir); - }, $check)); - - self::printMessage( - "Error: $short_dirs not writable! Run these commands as root on your LibreNMS server to fix:", - $chown_commands - ); - - // build SELinux output - $selinux_commands = []; - foreach ($check as $dir) { - $selinux_commands[] = "semanage fcontext -a -t httpd_sys_content_t '$dir(/.*)?'"; - $selinux_commands[] = "semanage fcontext -a -t httpd_sys_rw_content_t '$dir(/.*)?'"; - $selinux_commands[] = "restorecon -RFvv $dir"; - } - - self::printMessage( - "If using SELinux you may also need:", - $selinux_commands, - true - ); - } } /** @@ -207,12 +123,6 @@ class Checks } } - private static function basePath($path = '') - { - $base_dir = realpath(__DIR__ . '/..'); - return "$base_dir/$path"; - } - private static function missingPhpExtensions() { // allow mysqli, but prefer mysqlnd @@ -226,4 +136,99 @@ class Checks return !extension_loaded($module); }); } + + /** + * Check exception for errors related to not being able to write to the filesystem + * + * @param \Exception $e + * @return bool|SymfonyResponse + */ + public static function filePermissionsException($e) + { + if ($e instanceof \ErrorException) { + // cannot write to storage directory + if (starts_with($e->getMessage(), 'file_put_contents(') && str_contains($e->getMessage(), '/storage/')) { + return self::filePermissionsResponse(); + } + } + + if ($e instanceof \Exception) { + // cannot write to bootstrap directory + if ($e->getMessage() == 'The bootstrap/cache directory must be present and writable.') { + return self::filePermissionsResponse(); + } + } + + if ($e instanceof \UnexpectedValueException) { + // monolog cannot init log file + if (str_contains($e->getFile(), 'Monolog/Handler/StreamHandler.php')) { + return self::filePermissionsResponse(); + } + } + + return false; + } + + /** + * Generate a semi generic list of commands for the user to run to fix file permissions + * and render it to a nice html response + * + * @return SymfonyResponse + */ + private static function filePermissionsResponse() + { + $user = config('librenms.user'); + $group = config('librenms.group'); + $install_dir = base_path(); + $commands = []; + $dirs = [ + base_path('bootstrap/cache'), + base_path('storage'), + Config::get('log_dir', base_path('logs')), + Config::get('rrd_dir', base_path('rrd')), + ]; + + // check if folders are missing + $mkdirs = [ + base_path('bootstrap/cache'), + base_path('storage/framework/sessions'), + base_path('storage/framework/views'), + base_path('storage/framework/cache'), + Config::get('log_dir', base_path('logs')), + Config::get('rrd_dir', base_path('rrd')), + ]; + + $mk_dirs = array_filter($mkdirs, function ($file) { + return !file_exists($file); + }); + + if (!empty($mk_dirs)) { + $commands[] = 'sudo mkdir -p ' . implode(' ', $mk_dirs); + } + + // always print chwon/setfacl/chmod commands + $commands[] = "sudo chown -R $user:$group $install_dir"; + $commands[] = 'sudo setfacl -d -m g::rwx ' . implode(' ', $dirs); + $commands[] = 'sudo chmod -R ug=rwX ' . implode(' ', $dirs); + + // check if webserver is in the librenms group + $current_groups = explode(' ', trim(exec('groups'))); + if (!in_array($group, $current_groups)) { + $current_user = trim(exec('whoami')); + $commands[] = "usermod -a -G $group $current_user"; + } + + // selinux: + $commands[] = '

If using SELinux you may also need:

'; + foreach ($dirs as $dir) { + $commands[] = "semanage fcontext -a -t httpd_sys_rw_content_t '$dir(/.*)?'"; + } + $commands[] = "restorecon -RFv $install_dir"; + + // use pre-compiled template because we probably can't compile it. + $template = file_get_contents(base_path('resources/views/errors/static/file_permissions.html')); + $content = str_replace('!!!!CONTENT!!!!', '

' . implode('

', $commands) . '

', $template); + + return SymfonyResponse::create($content); + } } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 6f82b8fea0..a63695166b 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,6 +2,7 @@ namespace App\Exceptions; +use App\Checks; use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\QueryException; @@ -36,19 +37,14 @@ class Handler extends ExceptionHandler protected function convertExceptionToResponse(Exception $e) { - if ($e instanceof QueryException) { - // connect exception, convert to our standard connection exception - if (config('app.debug')) { - // get message form PDO exception, it doesn't contain the query - $message = $e->getMessage(); - } else { - $message = $e->getPrevious()->getMessage(); - } + // handle database exceptions + if ($db_response = $this->dbExceptionToResponse($e)) { + return $db_response; + } - if (in_array($e->getCode(), [1044, 1045, 2002])) { - throw new DatabaseConnectException($message, $e->getCode(), $e); - } - return response('Unhandled MySQL Error [' . $e->getCode() . "] $message"); + // check for exceptions relating to not being able to write to the filesystem + if ($fs_response = Checks::filePermissionsException($e)) { + return $fs_response; } // show helpful response if debugging, otherwise print generic error so we don't leak information @@ -74,4 +70,28 @@ class Handler extends ExceptionHandler return redirect()->guest(route('login')); } + + protected function dbExceptionToResponse(Exception $e) + { + if ($e instanceof QueryException) { + // connect exception, convert to our standard connection exception + if (config('app.debug')) { + // get message form PDO exception, it doesn't contain the query + $message = $e->getMessage(); + } else { + $message = $e->getPrevious()->getMessage(); + } + + if (in_array($e->getCode(), [1044, 1045, 2002])) { + // this Exception has it's own render function + throw new DatabaseConnectException($message, $e->getCode(), $e); + } + return response()->view('errors.generic', [ + 'title' => 'Unhandled MySQL Error [' . $e->getCode() . ']', + 'content' => $message + ]); + } + + return false; + } } diff --git a/html/pages/validate.inc.php b/html/pages/validate.inc.php index 9d262a0bfe..0654fe8c89 100644 --- a/html/pages/validate.inc.php +++ b/html/pages/validate.inc.php @@ -88,7 +88,11 @@ foreach ($validator->getAllResults() as $group => $results) { if ($result->hasFix() || $result->hasList()) { echo '
'; if ($result->hasFix()) { - echo 'Fix: ' . linkify($result->getFix()) . ''; + echo 'Fix: '; + foreach ((array)$result->getFix() as $fix) { + echo '
' . linkify($fix) . PHP_EOL; + } + echo '
'; if ($result->hasList()) { echo '

'; } diff --git a/resources/views/auth/2fa.blade.php b/resources/views/auth/2fa.blade.php index e31ebc0e4c..649077db5b 100644 --- a/resources/views/auth/2fa.blade.php +++ b/resources/views/auth/2fa.blade.php @@ -1,7 +1,7 @@ @extends('layouts.librenmsv1') @section('javascript') - + @endsection @section('content') diff --git a/resources/views/errors/file_permissions.blade.php b/resources/views/errors/file_permissions.blade.php new file mode 100644 index 0000000000..f3ac4730e3 --- /dev/null +++ b/resources/views/errors/file_permissions.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.error') + +@section('title') + @lang('Whoops, the web server could not write required files to the filesystem.') +@endsection + +@section('content') +

@lang('Running the following commands will fix the issue most of the time:')

+ + @foreach($commands as $command) +

{{ $command }}

+ @endforeach +
+ +

@lang("If that doesn't fix the issue. You can find how to get help at") https://docs.librenms.org/Support.

+@endsection diff --git a/resources/views/errors/generic.blade.php b/resources/views/errors/generic.blade.php index 9a6585193d..8dc3822e54 100644 --- a/resources/views/errors/generic.blade.php +++ b/resources/views/errors/generic.blade.php @@ -1,67 +1,9 @@ - - - - - - - - -
-
-
-

Whoops, looks like something went wrong. Check your librenms.log.

-
-
-
-
- -
- -
- - +@section('content') + {{ isset($content) ? $content : '' }} +@endsection diff --git a/resources/views/errors/static/file_permissions.html b/resources/views/errors/static/file_permissions.html new file mode 100644 index 0000000000..b6c1f207ab --- /dev/null +++ b/resources/views/errors/static/file_permissions.html @@ -0,0 +1,78 @@ + + + + + + + + +
+
+
+

Whoops, the web server could not write required files to the filesystem.

+
+ +
+
+
+
+ +
+

Running the following commands will fix the issue most of the time:

+ + !!!!CONTENT!!!! + +
+ +

If that doesn't fix the issue. You can find how to get help at https://docs.librenms.org/Support.

+
+ + + diff --git a/resources/views/layouts/error.blade.php b/resources/views/layouts/error.blade.php new file mode 100644 index 0000000000..afd926c11d --- /dev/null +++ b/resources/views/layouts/error.blade.php @@ -0,0 +1,75 @@ + + + + + + + + +
+
+
+

@yield('title')

+
+ +
+
+
+
+ +
+ @yield('content') + +
+ +

@lang("If you need additional help, you can find how to get help at") https://docs.librenms.org/Support.

+
+ + diff --git a/resources/views/layouts/librenmsv1.blade.php b/resources/views/layouts/librenmsv1.blade.php index 5a058df6af..4dea6ef8d0 100644 --- a/resources/views/layouts/librenmsv1.blade.php +++ b/resources/views/layouts/librenmsv1.blade.php @@ -8,12 +8,12 @@ @if(!LibreNMS\Config::get('favicon', false)) - - - - - - + + + + + + @@ -21,53 +21,53 @@ @endif - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @foreach(LibreNMS\Config::get('webui.custom_css', []) as $custom_css) - + @endforeach @yield('css') - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @if(LibreNMS\Config::get('enable_lazy_load', true)) - - + + @endif - - + + - - - + + + @yield('javascript') diff --git a/validate.php b/validate.php index 9ce2adfea0..fee03351c7 100755 --- a/validate.php +++ b/validate.php @@ -95,7 +95,7 @@ if (str_contains(`tail config.php`, '?>')) { // Composer checks if (!file_exists('vendor/autoload.php')) { - print_fail('Composer has not been run, dependencies are missing', 'composer install --no-dev'); + print_fail('Composer has not been run, dependencies are missing', './scripts/composer_wrapper.php install --no-dev'); exit; }