diff --git a/LibreNMS/Util/Url.php b/LibreNMS/Util/Url.php index 852a89f2d1..408f089212 100644 --- a/LibreNMS/Util/Url.php +++ b/LibreNMS/Util/Url.php @@ -32,6 +32,7 @@ use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; use LibreNMS\Config; +use Request; use Symfony\Component\HttpFoundation\ParameterBag; class Url @@ -324,6 +325,20 @@ class Url return $url; } + /** + * @param array|string $args + */ + public static function forExternalGraph($args): string + { + // handle pasted string + if (is_string($args)) { + $path = str_replace(url('/') . '/', '', $args); + $args = self::parseLegacyPathVars($path); + } + + return \URL::signedRoute('graph', $args); + } + /** * @param array $args * @return string @@ -584,6 +599,57 @@ class Url return is_null($key) ? $options : $options[$key] ?? $default; } + /** + * Parse variables from legacy path /key=value/key=value or regular get/post variables + */ + public static function parseLegacyPathVars(?string $path = null): array + { + $vars = []; + $parsed_get_vars = []; + if (empty($path)) { + $path = Request::path(); + } elseif (Str::startsWith($path, 'http') || str_contains($path, '?')) { + $parsed_url = parse_url($path); + $path = $parsed_url['path'] ?? ''; + parse_str($parsed_url['query'] ?? '', $parsed_get_vars); + } + + // don't parse the subdirectory, if there is one in the path + $base_url = parse_url(Config::get('base_url'))['path'] ?? ''; + if (strlen($base_url) > 1) { + $segments = explode('/', trim(str_replace($base_url, '', $path), '/')); + } else { + $segments = explode('/', trim($path, '/')); + } + + // parse the path + foreach ($segments as $pos => $segment) { + $segment = urldecode($segment); + if ($pos === 0) { + $vars['page'] = $segment; + } else { + [$name, $value] = array_pad(explode('=', $segment), 2, null); + if (! $value) { + if ($vars['page'] == 'device' && $pos < 3) { + // translate laravel device routes properly + $vars[$pos === 1 ? 'device' : 'tab'] = $name; + } elseif ($name) { + $vars[$name] = 'yes'; + } + } else { + $vars[$name] = $value; + } + } + } + + $vars = array_merge($vars, $parsed_get_vars); + + // don't leak login data + unset($vars['username'], $vars['password']); + + return $vars; + } + private static function escapeBothQuotes($string) { return str_replace(["'", '"'], "\'", $string); diff --git a/app/Http/Controllers/GraphController.php b/app/Http/Controllers/GraphController.php new file mode 100644 index 0000000000..52833b513e --- /dev/null +++ b/app/Http/Controllers/GraphController.php @@ -0,0 +1,43 @@ +path()), $request->except(['username', 'password'])); + if (\Auth::check()) { + // only allow debug for logged in users + Debug::set(! empty($vars['debug'])); + } + + // TODO, import graph.inc.php code and call Rrd::graph() directly + chdir(base_path()); + ob_start(); + include base_path('includes/html/graphs/graph.inc.php'); + $output = ob_get_clean(); + ob_end_clean(); + + $headers = []; + if (! Debug::isEnabled()) { + $headers['Content-type'] = (Config::get('webui.graph_type') == 'svg' ? 'image/svg+xml' : 'image/png'); + } + + return response($output, 200, $headers); + } +} diff --git a/app/Http/Middleware/AuthenticateGraph.php b/app/Http/Middleware/AuthenticateGraph.php new file mode 100644 index 0000000000..b75ecec7f5 --- /dev/null +++ b/app/Http/Middleware/AuthenticateGraph.php @@ -0,0 +1,100 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace App\Http\Middleware; + +use Closure; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Http\Request; +use LibreNMS\Config; +use LibreNMS\Exceptions\InvalidIpException; +use LibreNMS\Util\IP; + +class AuthenticateGraph +{ + /** @var string[] */ + protected $auth = [ + \App\Http\Middleware\LegacyExternalAuth::class, + \App\Http\Middleware\Authenticate::class, + \App\Http\Middleware\VerifyTwoFactor::class, + \App\Http\Middleware\LoadUserPreferences::class, + ]; + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string|null $relative + * @return \Illuminate\Http\Response + * + * @throws \Illuminate\Auth\AuthenticationException + */ + public function handle($request, Closure $next, $relative = null) + { + // if user is logged in, allow + if (\Auth::check()) { + return $next($request); + } + + // bypass normal auth if signed + if ($request->hasValidSignature($relative !== 'relative')) { + return $next($request); + } + + // bypass normal auth if ip is allowed (or all IPs) + if ($this->isAllowed($request)) { + return $next($request); + } + + // unauthenticated, force login + throw new AuthenticationException('Unauthenticated.'); + } + + protected function isAllowed(Request $request): bool + { + if (Config::get('allow_unauth_graphs', false)) { + d_echo("Unauthorized graphs allowed\n"); + + return true; + } + + $ip = $request->getClientIp(); + try { + $client_ip = IP::parse($ip); + foreach (Config::get('allow_unauth_graphs_cidr', []) as $range) { + if ($client_ip->inNetwork($range)) { + d_echo("Unauthorized graphs allowed from $range\n"); + + return true; + } + } + } catch (InvalidIpException $e) { + d_echo("Client IP ($ip) is invalid.\n"); + } + + return false; + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 362b48b0dc..693056e8af 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -23,7 +23,7 @@ class RedirectIfAuthenticated foreach ($guards as $guard) { if (Auth::guard($guard)->check()) { - return redirect(RouteServiceProvider::HOME); + return redirect()->intended(RouteServiceProvider::HOME); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a5ed0a5089..95334dc4d1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -70,6 +70,19 @@ class AppServiceProvider extends ServiceProvider Blade::directive('deviceUrl', function ($arguments) { return ""; }); + + // Graphing + Blade::directive('signedGraphUrl', function ($vars) { + return ""; + }); + + Blade::directive('signedGraphTag', function ($vars) { + return "'; ?>"; + }); + + Blade::directive('graphImage', function ($vars, $base64 = false) { + return ""; + }); } private function configureMorphAliases() diff --git a/config/debugbar.php b/config/debugbar.php index 2142e99ce0..b86bd578b9 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -25,6 +25,7 @@ return [ 'enabled' => env('DEBUGBAR_ENABLED', null), 'except' => [ 'api*', + 'graph*', 'push*', ], diff --git a/doc/Alerting/Templates.md b/doc/Alerting/Templates.md index ae497b91cf..63ddf4e717 100644 --- a/doc/Alerting/Templates.md +++ b/doc/Alerting/Templates.md @@ -284,12 +284,61 @@ Note: To use HTML emails you must set HTML email to Yes in the WebUI under Global Settings > Alerting Settings > Email transport > Use HTML emails -Note: To include Graphs you must enable unauthorized graphs in -config.php. Allow_unauth_graphs_cidr is optional, but more secure. +## Graphs + +There are two helpers for graphs that will use a signed url to allow secure external +access. Anyone using the signed url will be able to view the graph. Your LibreNMS web +must be accessible from the location where the graph is viewed. + +You may specify the graph one of two ways, a php array of parameters, or +a direct url to a graph. + +Note that to and from can be specified either as timestamps with `time()` +or as relative time `-3d` or `-36h`. When using relative time, the graph +will show based on when the user views the graph, not when the event happened. +Sharing a graph image with a relative time will always give the recipient access +to current data, where a specific timestamp will only allow access to that timeframe. + +### @signedGraphTag + +This will insert a specially formatted html img tag linking to the graph. +Some transports may search the template for this tag to attach images properly +for that transport. ``` -$config['allow_unauth_graphs_cidr'] = array('127.0.0.1/32'); -$config['allow_unauth_graphs'] = true; +@signedGraphTag([ + 'id' => $value['port_id'], + 'type' => 'port_bits', + 'from' => time() - 43200, + 'to' => time(), + 'width' => 700, + 'height' => 250 +]) +``` + +Output: +```html + +``` + +Specific graph using url input: + +``` +@signedGraphTag('https://librenms.org/graph.php?type=device_processor&from=-2d&device=2&legend=no&height=400&width=1200') +``` + +### @signedGraphUrl + +This is used when you need the url directly. One example is using the +API Transport, you may want to include the url only instead of a html tag. + +``` +@signedGraphUrl([ + 'id' => $value['port_id'], + 'type' => 'port_bits', + 'from' => time() - 43200, + 'to' => time(), +]) ``` ## Using models for optional data @@ -355,7 +404,8 @@ Rule: @if ($alert->name) {{ $alert->name }} @else {{ $alert->rule }} @endif
{{ $key }}: {{ $value['string'] }}
@endforeach @if ($alert->faults) Faults:
-@foreach ($alert->faults as $key => $value)
+@foreach ($alert->faults as $key => $value) +@signedGraphTag(['device_id' => $value['device_id'], 'type' => 'device_processor', 'width' => 459, 'height' => 213, 'from' => time() - 259200])
https://server/graphs/id={{ $value['device_id'] }}/type=device_processor/
@endforeach Template: CPU alert
diff --git a/html/graph.php b/html/graph.php index cd0e9c7911..a86e805f51 100644 --- a/html/graph.php +++ b/html/graph.php @@ -13,7 +13,7 @@ use LibreNMS\Util\Debug; $auth = false; $start = microtime(true); -$init_modules = ['web', 'graphs', 'auth']; +$init_modules = ['web', 'auth']; require realpath(__DIR__ . '/..') . '/includes/init.php'; if (! Auth::check()) { diff --git a/includes/html/graphs/graph.inc.php b/includes/html/graphs/graph.inc.php index 1682f25e72..21f6dea89c 100644 --- a/includes/html/graphs/graph.inc.php +++ b/includes/html/graphs/graph.inc.php @@ -4,11 +4,6 @@ use LibreNMS\Config; global $debug; -// Push $_GET into $vars to be compatible with web interface naming -foreach ($_GET as $name => $value) { - $vars[$name] = $value; -} - [$type, $subtype] = extract_graph_type($vars['type']); if (isset($vars['device'])) { @@ -18,14 +13,14 @@ if (isset($vars['device'])) { } // FIXME -- remove these -$width = $vars['width']; -$height = $vars['height']; +$width = $vars['width'] ?? 400; +$height = $vars['height'] ?? round($width / 3); $title = $vars['title'] ?? ''; $vertical = $vars['vertical'] ?? ''; $legend = $vars['legend'] ?? false; $output = (! empty($vars['output']) ? $vars['output'] : 'default'); -$from = empty($_GET['from']) ? Config::get('time.day') : parse_at_time($_GET['from']); -$to = empty($_GET['to']) ? Config::get('time.now') : parse_at_time($_GET['to']); +$from = empty($vars['from']) ? Config::get('time.day') : parse_at_time($vars['from']); +$to = empty($vars['to']) ? Config::get('time.now') : parse_at_time($vars['to']); $period = ($to - $from); $prev_from = ($from - $period); @@ -113,6 +108,10 @@ try { echo $output === 'base64' ? base64_encode($image_data) : $image_data; } } catch (\LibreNMS\Exceptions\RrdGraphException $e) { + if (\LibreNMS\Util\Debug::isEnabled()) { + throw $e; + } + if (isset($rrd_filename) && ! Rrd::checkRrdExists($rrd_filename)) { graph_error($width < 200 ? 'No Data' : 'No Data file ' . basename($rrd_filename)); } else { diff --git a/includes/html/vars.inc.php b/includes/html/vars.inc.php index 3e095f96ec..cadd941738 100644 --- a/includes/html/vars.inc.php +++ b/includes/html/vars.inc.php @@ -1,46 +1,6 @@ $get_var) { - if (strstr($key, 'opt')) { - [$name, $value] = explode('|', $get_var); - if (! isset($value)) { - $value = 'yes'; - } - - $vars[$name] = strip_tags($value); - } -} - -$base_url = parse_url(Config::get('base_url')); -$uri = explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? ''; // remove query, that is handled below with $_GET - -// don't parse the subdirectory, if there is one in the path -if (isset($base_url['path']) && strlen($base_url['path']) > 1) { - $segments = explode('/', trim(str_replace($base_url['path'], '', $uri), '/')); -} else { - $segments = explode('/', trim($uri, '/')); -} - -foreach ($segments as $pos => $segment) { - $segment = urldecode($segment); - if ($pos === 0) { - $vars['page'] = $segment; - } else { - [$name, $value] = array_pad(explode('=', $segment), 2, null); - if (! $value) { - if ($vars['page'] == 'device' && $pos < 3) { - // translate laravel device routes properly - $vars[$pos === 1 ? 'device' : 'tab'] = $name; - } else { - $vars[$name] = 'yes'; - } - } else { - $vars[$name] = $value; - } - } -} +$vars = \LibreNMS\Util\Url::parseLegacyPathVars($_SERVER['REQUEST_URI']); foreach ($_GET as $name => $value) { $vars[$name] = strip_tags($value); diff --git a/routes/web.php b/routes/web.php index d6a0e5a06f..8016e1106d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,10 @@ Route::prefix('auth')->name('socialite.')->group(function () { Route::get('{provider}/metadata', [\App\Http\Controllers\Auth\SocialiteController::class, 'metadata'])->name('metadata'); }); +Route::get('graph/{path?}', 'GraphController') + ->where('path', '.*') + ->middleware(['web', \App\Http\Middleware\AuthenticateGraph::class])->name('graph'); + // WebUI Route::group(['middleware' => ['auth'], 'guard' => 'auth'], function () {