mirror of
https://github.com/librenms/librenms.git
synced 2024-09-21 02:18:39 +00:00
Add @signedGraphTag() and @signedGraphUrl() blade directives (#14269)
* More secure external graph access Add @signedGraphTag() and @signedGraphUrl() blade directives Takes either an array of graph variables or a url to a graph Uses a signed url that is accessible without user login, embeds signature in url to authenticate access See Laravel Signed Url for more details. Adds Laravel route to graphs (does not change links to use it yet) @graphImage requires the other PR Also APP_URL is required in .env * missing files from rebase * Fix url parsing with a get string * allow width and height to be omitted * Documentation * Add to, otherwise it will always be now * Doc note for to and from relative security * fix vars.inc.php (Laravel has a dummy url here)
This commit is contained in:
parent
a95b1c408c
commit
5c76890373
@ -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);
|
||||
|
43
app/Http/Controllers/GraphController.php
Normal file
43
app/Http/Controllers/GraphController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use LibreNMS\Config;
|
||||
use LibreNMS\Util\Debug;
|
||||
use LibreNMS\Util\Url;
|
||||
|
||||
class GraphController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, string $path = ''): Response
|
||||
{
|
||||
define('IGNORE_ERRORS', true);
|
||||
|
||||
include_once base_path('includes/dbFacile.php');
|
||||
include_once base_path('includes/common.php');
|
||||
include_once base_path('includes/html/functions.inc.php');
|
||||
include_once base_path('includes/rewrites.php');
|
||||
|
||||
$auth = \Auth::guest(); // if user not logged in, assume we authenticated via signed url, allow_unauth_graphs or allow_unauth_graphs_cidr
|
||||
$vars = array_merge(Url::parseLegacyPathVars($request->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);
|
||||
}
|
||||
}
|
100
app/Http/Middleware/AuthenticateGraph.php
Normal file
100
app/Http/Middleware/AuthenticateGraph.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/*
|
||||
* AuthenticateGraph.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @package LibreNMS
|
||||
* @link http://librenms.org
|
||||
* @copyright 2022 Tony Murray
|
||||
* @author Tony Murray <murraytony@gmail.com>
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class RedirectIfAuthenticated
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
return redirect()->intended(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,19 @@ class AppServiceProvider extends ServiceProvider
|
||||
Blade::directive('deviceUrl', function ($arguments) {
|
||||
return "<?php echo \LibreNMS\Util\Url::deviceUrl($arguments); ?>";
|
||||
});
|
||||
|
||||
// Graphing
|
||||
Blade::directive('signedGraphUrl', function ($vars) {
|
||||
return "<?php echo \LibreNMS\Util\Url::forExternalGraph($vars); ?>";
|
||||
});
|
||||
|
||||
Blade::directive('signedGraphTag', function ($vars) {
|
||||
return "<?php echo '<img class=\"librenms-graph\" src=\"' . \LibreNMS\Util\Url::forExternalGraph($vars) . '\" />'; ?>";
|
||||
});
|
||||
|
||||
Blade::directive('graphImage', function ($vars, $base64 = false) {
|
||||
return "<?php echo \LibreNMS\Util\Graph::get(is_array($vars) ? $vars : \LibreNMS\Util\Url::parseLegacyPathVars($vars), $base64); ?>";
|
||||
});
|
||||
}
|
||||
|
||||
private function configureMorphAliases()
|
||||
|
@ -25,6 +25,7 @@ return [
|
||||
'enabled' => env('DEBUGBAR_ENABLED', null),
|
||||
'except' => [
|
||||
'api*',
|
||||
'graph*',
|
||||
'push*',
|
||||
],
|
||||
|
||||
|
@ -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
|
||||
<img class="librenms-graph" src="https://librenms.org/graph?from=1662176216&height=250&id=20425&to=1662219416&type=port_bits&width=700&signature=f6e516e8fd893c772eeaba165d027cb400e15a515254de561a05b63bc6f360a4">
|
||||
```
|
||||
|
||||
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 <br>
|
||||
{{ $key }}: {{ $value['string'] }}<br>
|
||||
@endforeach
|
||||
@if ($alert->faults) <b>Faults:</b><br>
|
||||
@foreach ($alert->faults as $key => $value)<img src="https://server/graph.php?device={{ $value['device_id'] }}&type=device_processor&width=459&height=213&lazy_w=552&from=end-72h"><br>
|
||||
@foreach ($alert->faults as $key => $value)
|
||||
@signedGraphTag(['device_id' => $value['device_id'], 'type' => 'device_processor', 'width' => 459, 'height' => 213, 'from' => time() - 259200])<br>
|
||||
https://server/graphs/id={{ $value['device_id'] }}/type=device_processor/<br>
|
||||
@endforeach
|
||||
Template: CPU alert <br>
|
||||
|
@ -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()) {
|
||||
|
@ -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 {
|
||||
|
@ -1,46 +1,6 @@
|
||||
<?php
|
||||
|
||||
use LibreNMS\Config;
|
||||
|
||||
foreach ($_GET as $key => $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);
|
||||
|
@ -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 () {
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user