Skip to content

Commit

Permalink
[Feature] Skip test when Public IP is in an list (#1714)
Browse files Browse the repository at this point in the history
* first commit

* Commit it

* lint

* update-all-charts

* push_local_git

* add-timepicker

* add-some-predefined-ranges

* remove whiteline

* Add_env_for_chart_start

* change-env-and_time-ranges

* change_average_to_orange

* Revert "Add failed and thresholds"

* first commit

* change_api

* update comments

* Simplify

* Seperate the IP check
  • Loading branch information
svenvg93 authored Nov 19, 2024
1 parent 0bdd141 commit f096c0a
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 2 deletions.
5 changes: 4 additions & 1 deletion app/Enums/ResultStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ enum ResultStatus: string implements HasColor, HasLabel
{
case Completed = 'completed'; // a speedtest that ran successfully.
case Failed = 'failed'; // a speedtest that failed to run successfully.
case Started = 'started'; // a speedtest that has been started by a worker but has not finish running.
case Started = 'started'; // a speedtest that has been started by a worker but has not finished running.
case Skipped = 'skipped'; // a speedtest that was skipped.

public function getColor(): ?string
{
return match ($this) {
self::Completed => 'success',
self::Failed => 'danger',
self::Started => 'warning',
self::Skipped => 'info', // Adding Skipped state with a color
};
}

Expand All @@ -26,6 +28,7 @@ public function getLabel(): ?string
self::Completed => 'Completed',
self::Failed => 'Failed',
self::Started => 'Started',
self::Skipped => 'Skipped',
};
}
}
20 changes: 20 additions & 0 deletions app/Events/SpeedtestSkipped.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Events;

use App\Models\Result;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SpeedtestSkipped
{
use Dispatchable, InteractsWithSockets, SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(
public Result $result,
) {}
}
4 changes: 4 additions & 0 deletions app/Filament/Resources/ResultResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ public static function form(Form $form): Form
->hint(new HtmlString('&#x1f517;<a href="https://docs.speedtest-tracker.dev/help/error-messages" target="_blank" rel="nofollow">Error Messages</a>'))
->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Failed)
->columnSpanFull(),
Forms\Components\Textarea::make('data.message')
->label('Skip Message')
->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Skipped)
->columnSpanFull(),
])
->columnSpan(2),
Forms\Components\Section::make()
Expand Down
68 changes: 67 additions & 1 deletion app/Jobs/Speedtests/ExecuteOoklaSpeedtest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use App\Enums\ResultStatus;
use App\Events\SpeedtestCompleted;
use App\Events\SpeedtestFailed;
use App\Events\SpeedtestSkipped;
use App\Models\Result;
use App\Services\PublicIpService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
Expand Down Expand Up @@ -35,6 +37,7 @@ class ExecuteOoklaSpeedtest implements ShouldBeUnique, ShouldQueue
public function __construct(
public Result $result,
public ?int $serverId = null,
protected PublicIpService $publicIpService = new PublicIpService
) {}

/**
Expand All @@ -46,6 +49,24 @@ public function handle(): void
return;
}

// Fetch public IP data using the PublicIpService
$ipData = $this->publicIpService->getPublicIp();
$currentIp = $ipData['ip'] ?? 'unknown';
$isp = $ipData['isp'] ?? 'unknown'; // Get the ISP value here

// Retrieve SPEEDTEST_SKIP_IP Settings
$skipSettings = array_filter(array_map('trim', explode(';', config('speedtest.skip_ip'))));

// Check Each Skip Setting
$skipMessage = $this->publicIpService->shouldSkipIp($currentIp, $skipSettings);
if ($skipMessage) {
// Pass the $isp along with $currentIp and $skipMessage
$this->markAsSkipped($skipMessage, $currentIp, $isp);

return;
}

// Execute Speedtest
$options = array_filter([
'speedtest',
'--accept-license',
Expand Down Expand Up @@ -74,6 +95,30 @@ public function handle(): void
// Filter out empty messages and concatenate
$errorMessage = implode(' | ', array_filter($errorMessages));

// Add server ID to the error message if it exists
if ($this->serverId !== null) {
$this->result->update([
'data' => [
'type' => 'log',
'level' => 'error',
'message' => $errorMessage,
'server' => [
'id' => $this->serverId,
],
],
'status' => ResultStatus::Failed,
]);
} else {
$this->result->update([
'data' => [
'type' => 'log',
'level' => 'error',
'message' => $errorMessage,
],
'status' => ResultStatus::Failed,
]);
}

// Prepare the error message data
$data = [
'type' => 'log',
Expand Down Expand Up @@ -110,6 +155,27 @@ public function handle(): void
SpeedtestCompleted::dispatch($this->result);
}

/**
* Mark the test as skipped with a specific message.
*/
protected function markAsSkipped(string $message, string $currentIp, string $isp): void
{
$this->result->update([
'data' => [
'type' => 'log',
'level' => 'info',
'message' => $message,
'interface' => [
'externalIp' => $currentIp,
],
'isp' => $isp,
],
'status' => ResultStatus::Skipped,
]);

SpeedtestSkipped::dispatch($this->result);
}

/**
* Check for internet connection.
*
Expand All @@ -120,6 +186,7 @@ protected function checkForInternetConnection(): bool
$url = config('speedtest.ping_url');

// Skip checking for internet connection if ping url isn't set (disabled)

if (blank($url)) {
return true;
}
Expand All @@ -139,7 +206,6 @@ protected function checkForInternetConnection(): bool
return false;
}

// Remove http:// or https:// from the URL if present
$url = preg_replace('/^https?:\/\//', '', $url);

$ping = new Ping(
Expand Down
87 changes: 87 additions & 0 deletions app/Services/PublicIpService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;

class PublicIpService
{
/**
* Get the public IP address and its associated details using ipapi.co.
*/
public function getPublicIp(): array
{
try {
// Fetch location data from ifconfig.co using curl
$ch = curl_init('https://ifconfig.co/json');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Validate the HTTP response
if ($httpCode !== 200) {
\Log::error("Failed to fetch public IP data from ifconfig.co. HTTP Status Code: $httpCode");

return ['ip' => 'unknown', 'isp' => 'unknown'];
}

// Decode the JSON response
$data = json_decode($response, true);

// Validate the response format
if (json_last_error() === JSON_ERROR_NONE && isset($data['ip'])) {
return [
'ip' => $data['ip'],
'isp' => $data['asn_org'] ?? 'unknown',
];
}

// Log error if the response is invalid
\Log::error('Invalid response from ifconfig.co: '.$response);

return ['ip' => 'unknown', 'isp' => 'unknown'];
} catch (\Exception $e) {
\Log::error("Error fetching public IP data from ifconfig.co: {$e->getMessage()}");

// Fallback response
return ['ip' => 'unknown', 'isp' => 'unknown'];
}
}

/**
* Check if the current IP should be skipped.
*/
public function shouldSkipIp(string $currentIp, array $skipSettings): bool|string
{
foreach ($skipSettings as $setting) {
// Check for exact IP match
if (filter_var($setting, FILTER_VALIDATE_IP)) {
if ($currentIp === $setting) {
return "Current public IP address ($currentIp) is listed to be skipped for testing.";
}
}

// Check for subnet match
if (strpos($setting, '/') !== false && $this->ipInSubnet($currentIp, $setting)) {
return "Current public IP address ($currentIp) falls within the excluded subnet ($setting).";
}
}

return false;
}

/**
* Check if an IP is in a given subnet.
*/
protected function ipInSubnet(string $ip, string $subnet): bool
{
[$subnet, $mask] = explode('/', $subnet) + [1 => '32'];
$subnetDecimal = ip2long($subnet);
$ipDecimal = ip2long($ip);
$maskDecimal = ~((1 << (32 - (int) $mask)) - 1);

return ($subnetDecimal & $maskDecimal) === ($ipDecimal & $maskDecimal);
}
}
5 changes: 5 additions & 0 deletions config/speedtest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@

'servers' => env('SPEEDTEST_SERVERS', ''),

/**
* IP filtering settings.
*/
'skip_ip' => env('SPEEDTEST_SKIP_IP', ''), // Comma-separated list of IPs, ISPs and subnets

];

0 comments on commit f096c0a

Please sign in to comment.