Alert transport cleanup, no_proxy support and other proxy cleanups (#14763)

* Add no_proxy and other proxy related settings
Set user agent on all http client requests
Unify http client usage

* Style fixes

* Remove useless use statements

* Correct variable, good job phpstan

* Add tests
fix https_proxy bug
add tcp:// to the config settings format

* style and lint fixes

* Remove guzzle from the direct dependencies

* Use built in Laravel testing functionality

* update baseline
This commit is contained in:
Tony Murray
2023-05-23 09:25:17 -05:00
committed by GitHub
parent 02896172bd
commit 04bb75f5f3
78 changed files with 1545 additions and 2314 deletions

View File

@@ -27,12 +27,11 @@ namespace LibreNMS\Tests;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
class AlertingTest extends TestCase
{
public function testJsonAlertCollection()
public function testJsonAlertCollection(): void
{
$rules = get_rules_from_json();
$this->assertIsArray($rules);
@@ -41,7 +40,7 @@ class AlertingTest extends TestCase
}
}
public function testTransports()
public function testTransports(): void
{
foreach ($this->getTransportFiles() as $file => $_unused) {
$parts = explode('/', $file);
@@ -52,10 +51,10 @@ class AlertingTest extends TestCase
}
}
private function getTransportFiles()
private function getTransportFiles(): RegexIterator
{
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('LibreNMS/Alert/Transport'));
return new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
return new RegexIterator($iterator, '/^.+\.php$/i', RegexIterator::GET_MATCH);
}
}

View File

@@ -22,21 +22,70 @@
namespace LibreNMS\Tests;
use LibreNMS\Util\Proxy;
use LibreNMS\Config;
use LibreNMS\Util\Http;
use LibreNMS\Util\Version;
class ProxyTest extends TestCase
{
public function testShouldBeUsed(): void
public function testClientAgentIsCorrect(): void
{
$this->assertTrue(Proxy::shouldBeUsed('http://example.com/foobar'));
$this->assertTrue(Proxy::shouldBeUsed('foo/bar'));
$this->assertTrue(Proxy::shouldBeUsed('192.168.0.1'));
$this->assertTrue(Proxy::shouldBeUsed('2001:db8::8a2e:370:7334'));
$this->assertEquals('LibreNMS/' . Version::VERSION, Http::client()->getOptions()['headers']['User-Agent']);
}
$this->assertFalse(Proxy::shouldBeUsed('http://localhost/foobar'));
$this->assertFalse(Proxy::shouldBeUsed('localhost/foobar'));
$this->assertFalse(Proxy::shouldBeUsed('127.0.0.1'));
$this->assertFalse(Proxy::shouldBeUsed('127.0.0.1:1337'));
$this->assertFalse(Proxy::shouldBeUsed('::1'));
public function testProxyIsNotSet(): void
{
Config::set('http_proxy', '');
Config::set('https_proxy', '');
Config::set('no_proxy', '');
$client_options = Http::client()->getOptions();
$this->assertEmpty($client_options['proxy']['http']);
$this->assertEmpty($client_options['proxy']['https']);
$this->assertEmpty($client_options['proxy']['no']);
}
public function testProxyIsSet(): void
{
Config::set('http_proxy', 'http://proxy:5000');
Config::set('https_proxy', 'tcp://proxy:5183');
Config::set('no_proxy', 'localhost,127.0.0.1,::1,.domain.com');
$client_options = Http::client()->getOptions();
$this->assertEquals('http://proxy:5000', $client_options['proxy']['http']);
$this->assertEquals('tcp://proxy:5183', $client_options['proxy']['https']);
$this->assertEquals([
'localhost',
'127.0.0.1',
'::1',
'.domain.com',
], $client_options['proxy']['no']);
}
public function testProxyIsSetFromEnv(): void
{
Config::set('http_proxy', '');
Config::set('https_proxy', '');
Config::set('no_proxy', '');
putenv('HTTP_PROXY=someproxy:3182');
putenv('HTTPS_PROXY=https://someproxy:3182');
putenv('NO_PROXY=.there.com');
$client_options = Http::client()->getOptions();
$this->assertEquals('someproxy:3182', $client_options['proxy']['http']);
$this->assertEquals('https://someproxy:3182', $client_options['proxy']['https']);
$this->assertEquals([
'.there.com',
], $client_options['proxy']['no']);
putenv('http_proxy=otherproxy:3182');
putenv('https_proxy=otherproxy:3183');
putenv('no_proxy=dontproxymebro');
$client_options = Http::client()->getOptions();
$this->assertEquals('otherproxy:3182', $client_options['proxy']['http']);
$this->assertEquals('otherproxy:3183', $client_options['proxy']['https']);
$this->assertEquals([
'dontproxymebro',
], $client_options['proxy']['no']);
}
}

View File

@@ -1,80 +0,0 @@
<?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,155 @@
<?php
/**
* SlackTest.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 <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Tests\Unit\Alert\Transports;
use App\Models\AlertTransport;
use App\Models\Device;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use LibreNMS\Alert\AlertData;
use LibreNMS\Alert\Transport;
use LibreNMS\Tests\TestCase;
class SlackTest extends TestCase
{
public function testSlackNoConfigDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport);
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == '' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => null,
],
],
'channel' => null,
'icon_emoji' => null,
];
});
}
public function testSlackLegacyDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport([
'transport_config' => [
'slack-url' => 'https://slack.com/some/webhook',
'slack-options' => "icon_emoji=smile\nauthor=Me\nchannel=Alerts",
],
]));
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == 'https://slack.com/some/webhook' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => 'Me',
],
],
'channel' => 'Alerts',
'icon_emoji' => ':smile:',
];
});
}
public function testSlackDelivery(): void
{
Http::fake();
$slack = new Transport\Slack(new AlertTransport([
'transport_config' => [
'slack-url' => 'https://slack.com/some/webhook',
'slack-options' => "icon_emoji=smile\nauthor=Me\nchannel=Alerts",
'slack-icon_emoji' => ':slight_smile:',
'slack-author' => 'Other',
'slack-channel' => 'Critical',
],
]));
/** @var Device $mock_device */
$mock_device = Device::factory()->make();
$slack->deliverAlert(AlertData::testData($mock_device));
Http::assertSent(function (Request $request) {
return
$request->url() == 'https://slack.com/some/webhook' &&
$request->method() == 'POST' &&
$request->hasHeader('Content-Type', 'application/json') &&
$request->data() == [
'attachments' => [
[
'fallback' => 'This is a test alert',
'color' => '#ff0000',
'title' => 'Testing transport from LibreNMS',
'text' => 'This is a test alert',
'mrkdwn_in' => [
'text',
'fallback',
],
'author_name' => 'Other',
],
],
'channel' => 'Critical',
'icon_emoji' => ':slight_smile:',
];
});
}
}

View File

@@ -3,34 +3,31 @@
namespace LibreNMS\Tests\Unit;
use App\Models\AlertTransport;
use GuzzleHttp\Psr7\Response;
use LibreNMS\Config;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http as LaravelHttp;
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),
LaravelHttp::fake([
'*' => LaravelHttp::response(),
]);
$obj = ['msg' => "This is a multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type);
$result = $transport->instance()->deliverAlert($obj, $opts);
$result = $transport->instance()->deliverAlert($obj);
$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());
LaravelHttp::assertSentCount(1);
LaravelHttp::assertSent(function (Request $request) {
return $request->method() == 'GET' &&
$request->url() == 'https://librenms.org?text=This%20is%20a%20multi-line%0Aalert.';
});
}
public function testPostMultilineVariables(): void
@@ -42,21 +39,20 @@ class ApiTransportTest extends TestCase
'bodytext={{ $msg }}',
)->make();
$this->mockGuzzleClient([
new Response(200),
LaravelHttp::fake([
'*' => LaravelHttp::response(),
]);
$obj = ['msg' => "This is a post multi-line\nalert."];
$opts = Config::get('alert.transports.' . $transport->transport_type);
$result = $transport->instance()->deliverAlert($obj, $opts);
$result = $transport->instance()->deliverAlert($obj);
$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());
LaravelHttp::assertSentCount(1);
LaravelHttp::assertSent(function (Request $request) {
return $request->method() == 'POST' &&
$request->url() == 'https://librenms.org?text=This%20is%20a%20post%20multi-line%0Aalert.' &&
$request->body() == "bodytext=This is a post multi-line\nalert.";
});
}
}

View File

@@ -25,21 +25,16 @@
namespace LibreNMS\Tests\Unit\Data;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Illuminate\Support\Facades\Http as LaravelHttp;
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();
@@ -48,26 +43,20 @@ class PrometheusStoreTest extends TestCase
Config::set('prometheus.url', 'http://fake:9999');
}
public function testFailWrite()
public function testFailWrite(): void
{
$this->mockGuzzleClient([
new Response(422, [], 'Bad response'),
new RequestException('Exception thrown', new Request('POST', 'test')),
]);
LaravelHttp::fakeSequence()->push('Bad response', 422);
$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");
\Log::shouldReceive('error')->once()->with('Prometheus Exception: Exception thrown');
$prometheus->put(['hostname' => 'test'], 'none', [], ['one' => 1]);
\Log::shouldReceive('error')->once()->with('Prometheus Error: Bad response');
$prometheus->put(['hostname' => 'test'], 'none', [], ['one' => 1]);
}
public function testSimpleWrite()
public function testSimpleWrite(): void
{
$this->mockGuzzleClient([
new Response(200),
LaravelHttp::fake([
'*' => LaravelHttp::response(),
]);
$prometheus = app(Prometheus::class);
@@ -82,12 +71,11 @@ class PrometheusStoreTest extends TestCase
$prometheus->put($device, $measurement, $tags, $fields);
$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());
LaravelHttp::assertSentCount(1);
LaravelHttp::assertSent(function (\Illuminate\Http\Client\Request $request) {
return $request->method() == 'POST' &&
$request->url() == 'http://fake:9999/metrics/job/librenms/instance/testhost/measurement/testmeasure/ifName/testifname/type/testtype' &&
$request->body() == "ifIn 234234\nifOut 53453\n";
});
}
}