Fix api transport mult-line parsing (#13469)

* API transport fix parsing
parse templates after parsing user options, not before

* API transport tests

* fix style and lint

* remove accidental item

* fix more type issues
This commit is contained in:
Tony Murray
2021-11-03 13:37:57 -05:00
committed by GitHub
parent 01345b5fba
commit 0862496e26
8 changed files with 239 additions and 48 deletions

View File

@@ -3,6 +3,7 @@
namespace LibreNMS\Alert;
use App\Models\AlertTransport;
use App\View\SimpleTemplate;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Str;
use LibreNMS\Config;
@@ -24,6 +25,24 @@ abstract class Transport implements TransportInterface
return new $class();
}
/**
* Returns a list of all available transports
*
* @return array
*/
public static function list(): array
{
$list = [];
foreach (glob(base_path('LibreNMS/Alert/Transport/*.php')) as $file) {
$transport = strtolower(basename($file, '.php'));
$class = self::getClass($transport);
$instance = new $class;
$list[$transport] = $instance->name();
}
return $list;
}
/**
* Transport constructor.
*
@@ -63,15 +82,16 @@ abstract class Transport implements TransportInterface
* Helper function to parse free form text box defined in ini style to key value pairs
*
* @param string $input
* @param array $replacements for SimpleTemplate if desired
* @return array
*/
protected function parseUserOptions(string $input): array
protected function parseUserOptions(string $input, array $replacements = []): array
{
$options = [];
foreach (preg_split('/\\r\\n|\\r|\\n/', $input, -1, PREG_SPLIT_NO_EMPTY) as $option) {
if (Str::contains($option, '=')) {
[$k, $v] = explode('=', $option, 2);
$options[$k] = trim($v);
$options[$k] = empty($replacements) ? trim($v) : SimpleTemplate::parse(trim($v), $replacements);
}
}

View File

@@ -52,11 +52,11 @@ class Api extends Transport
$host = explode('?', $api, 2)[0]; //we don't use the parameter part, cause we build it out of options.
//get each line of key-values and process the variables for Headers;
$request_heads = $this->parseUserOptions(SimpleTemplate::parse($headers, $obj));
$request_heads = $this->parseUserOptions($headers, $obj);
//get each line of key-values and process the variables for Options;
$query = $this->parseUserOptions(SimpleTemplate::parse($options, $obj));
$query = $this->parseUserOptions($options, $obj);
$client = new \GuzzleHttp\Client();
$client = app(\GuzzleHttp\Client::class);
$request_opts['proxy'] = Proxy::forGuzzle();
if (isset($auth) && ! empty($auth[0])) {
$request_opts['auth'] = $auth;

View File

@@ -0,0 +1,49 @@
<?php
namespace Database\Factories;
use App\Models\AlertTransport;
use Illuminate\Database\Eloquent\Factories\Factory;
use LibreNMS\Alert\Transport;
class AlertTransportFactory extends Factory
{
protected $model = AlertTransport::class;
public function definition(): array
{
return [
'transport_name' => $this->faker->text(30),
'transport_type' => $this->faker->randomElement(Transport::list()),
'is_default' => 0,
'transport_config' => '',
];
}
public function api(
string $options = '',
string $method = 'get',
string $body = '',
string $url = 'https://librenms.org',
string $headers = 'test=header',
string $username = '',
string $password = ''
): AlertTransportFactory {
$config = [
'api-method' => $method,
'api-url' => $url,
'api-options' => $options,
'api-headers' => $headers,
'api-body' => $body,
'api-auth-username' => $username,
'api-auth-password' => $password,
];
return $this->state(function () use ($config) {
return [
'transport_type' => 'api',
'transport_config' => $config,
];
});
}
}

View File

@@ -11,7 +11,7 @@
* the source code distribution for details.
*/
use LibreNMS\Config;
use LibreNMS\Alert\Transport;
if (Auth::user()->hasGlobalAdmin()) {
?>
@@ -42,19 +42,9 @@ if (Auth::user()->hasGlobalAdmin()) {
<?php
// Create list of transport
$transport_dir = Config::get('install_dir') . '/LibreNMS/Alert/Transport';
$transports_list = [];
foreach (scandir($transport_dir) as $transport) {
$transport = strstr($transport, '.', true);
if (empty($transport)) {
continue;
}
$class = "\LibreNMS\Alert\Transport\\$transport";
$instance = new $class;
$transports_list[$transport] = $instance->name();
}
$transports_list = Transport::list();
foreach ($transports_list as $transport => $name) {
echo '<option value="' . strtolower($transport) . '-form">' . $name . '</option>';
echo '<option value="' . $transport . '-form">' . $name . '</option>';
} ?>
</select>
</div>
@@ -70,16 +60,16 @@ if (Auth::user()->hasGlobalAdmin()) {
$switches = []; // store names of bootstrap switches
foreach ($transports_list as $transport => $name) {
$class = 'LibreNMS\\Alert\\Transport\\' . $transport;
$class = Transport::getClass($transport);
if (! method_exists($class, 'configTemplate')) {
// Skip since support has not been added
continue;
}
echo '<form method="post" role="form" id="' . strtolower($transport) . '-form" class="form-horizontal transport">';
echo '<form method="post" role="form" id="' . $transport . '-form" class="form-horizontal transport">';
echo csrf_field();
echo '<input type="hidden" name="transport-type" value="' . strtolower($transport) . '">';
echo '<input type="hidden" name="transport-type" value="' . $transport . '">';
$tmp = call_user_func($class . '::configTemplate');

View File

@@ -9,7 +9,7 @@ if ($request->has('oauthtransport')) {
if ($validator->passes()) {
$transport_name = $request->get('oauthtransport');
$class = 'LibreNMS\\Alert\\Transport\\' . $transport_name;
$class = \LibreNMS\Alert\Transport::getClass($transport_name);
if (class_exists($class)) {
$transport = app($class);
if ($transport->handleOauth($request)) {

View File

@@ -0,0 +1,80 @@
<?php
namespace LibreNMS\Tests\Traits;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
/**
* @mixin \LibreNMS\Tests\TestCase
*/
trait MockGuzzleClient
{
/**
* @var MockHandler
*/
private $guzzleMockHandler;
/**
* @var array
*/
private $guzzleConfig;
/**
* @var array
*/
private $guzzleHistory = [];
/**
* Create a Guzzle MockHandler and bind Client with the handler to the Laravel container
*
* @param array $queue Sequential Responses to give to the client.
* @param array $config Guzzle config settings.
*/
public function mockGuzzleClient(array $queue, array $config = []): MockHandler
{
$this->guzzleConfig = $config;
$this->guzzleMockHandler = new MockHandler($queue);
$this->app->bind(Client::class, function () {
$handlerStack = HandlerStack::create($this->guzzleMockHandler);
$handlerStack->push(Middleware::history($this->guzzleHistory));
return new Client(array_merge($this->guzzleConfig, ['handler' => $handlerStack]));
});
return $this->guzzleMockHandler;
}
/**
* Get the request and response history to inspect
*
* @return array
*/
public function guzzleHistory(): array
{
return $this->guzzleHistory;
}
/**
* Get the request history to inspect
*
* @return \GuzzleHttp\Psr7\Request[]
*/
public function guzzleRequestHistory(): array
{
return array_column($this->guzzleHistory, 'request');
}
/**
* Get the response history to inspect
*
* @return \GuzzleHttp\Psr7\Response[]
*/
public function guzzleResponseHistory(): array
{
return array_column($this->guzzleHistory, 'response');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace LibreNMS\Tests\Unit;
use App\Models\AlertTransport;
use GuzzleHttp\Psr7\Response;
use LibreNMS\Config;
use LibreNMS\Tests\TestCase;
use LibreNMS\Tests\Traits\MockGuzzleClient;
class ApiTransportTest extends TestCase
{
use MockGuzzleClient;
public function testGetMultilineVariables(): void
{
/** @var AlertTransport $transport */
$transport = AlertTransport::factory()->api('text={{ $msg }}')->make();
$this->mockGuzzleClient([
new Response(200),
]);
$obj = ['msg' => "This is a multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type);
$result = $transport->instance()->deliverAlert($obj, $opts);
$this->assertTrue($result);
$history = $this->guzzleRequestHistory();
$this->assertCount(1, $history);
$this->assertEquals('GET', $history[0]->getMethod());
$this->assertEquals('text=This%20is%20a%20multi-line%0Aalert.', $history[0]->getUri()->getQuery());
}
public function testPostMultilineVariables(): void
{
/** @var AlertTransport $transport */
$transport = AlertTransport::factory()->api(
'text={{ $msg }}',
'post',
'bodytext={{ $msg }}',
)->make();
$this->mockGuzzleClient([
new Response(200),
]);
$obj = ['msg' => "This is a post multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type);
$result = $transport->instance()->deliverAlert($obj, $opts);
$this->assertTrue($result);
$history = $this->guzzleRequestHistory();
$this->assertCount(1, $history);
$this->assertEquals('POST', $history[0]->getMethod());
// FUBAR
$this->assertEquals('text=This%20is%20a%20post%20multi-line%0Aalert.', $history[0]->getUri()->getQuery());
$this->assertEquals("bodytext=This is a post multi-line\nalert.", (string) $history[0]->getBody());
}
}

View File

@@ -25,22 +25,21 @@
namespace LibreNMS\Tests\Unit\Data;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use LibreNMS\Config;
use LibreNMS\Data\Store\Prometheus;
use LibreNMS\Tests\TestCase;
use LibreNMS\Tests\Traits\MockGuzzleClient;
/**
* @group datastores
*/
class PrometheusStoreTest extends TestCase
{
use MockGuzzleClient;
protected function setUp(): void
{
parent::setUp();
@@ -51,13 +50,12 @@ class PrometheusStoreTest extends TestCase
public function testFailWrite()
{
$stack = HandlerStack::create(new MockHandler([
$this->mockGuzzleClient([
new Response(422, [], 'Bad response'),
new RequestException('Exception thrown', new Request('POST', 'test')),
]));
]);
$client = new Client(['handler' => $stack]);
$prometheus = new Prometheus($client);
$prometheus = app(Prometheus::class);
\Log::shouldReceive('debug');
\Log::shouldReceive('error')->once()->with("Prometheus Exception: Client error: `POST http://fake:9999/metrics/job/librenms/instance/test/measurement/none` resulted in a `422 Unprocessable Entity` response:\nBad response\n");
@@ -68,16 +66,11 @@ class PrometheusStoreTest extends TestCase
public function testSimpleWrite()
{
$stack = HandlerStack::create(new MockHandler([
$this->mockGuzzleClient([
new Response(200),
]));
]);
$container = [];
$history = Middleware::history($container);
$stack->push($history);
$client = new Client(['handler' => $stack]);
$prometheus = new Prometheus($client);
$prometheus = app(Prometheus::class);
$device = ['hostname' => 'testhost'];
$measurement = 'testmeasure';
@@ -89,15 +82,12 @@ class PrometheusStoreTest extends TestCase
$prometheus->put($device, $measurement, $tags, $fields);
$this->assertCount(1, $container, 'Did not receive the expected number of requests');
/** @var Request $request */
$request = $container[0]['request'];
$this->assertEquals('POST', $request->getMethod());
$this->assertEquals('/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype', $request->getUri()->getPath());
$this->assertEquals('fake', $request->getUri()->getHost());
$this->assertEquals(9999, $request->getUri()->getPort());
$this->assertEquals("ifIn 234234\nifOut 53453\n", (string) $request->getBody());
$history = $this->guzzleRequestHistory();
$this->assertCount(1, $history, 'Did not receive the expected number of requests');
$this->assertEquals('POST', $history[0]->getMethod());
$this->assertEquals('/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype', $history[0]->getUri()->getPath());
$this->assertEquals('fake', $history[0]->getUri()->getHost());
$this->assertEquals(9999, $history[0]->getUri()->getPort());
$this->assertEquals("ifIn 234234\nifOut 53453\n", (string) $history[0]->getBody());
}
}