Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions app/Actions/Helpers/GetExternalIpAddress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace App\Actions\Helpers;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;

class GetExternalIpAddress
{
use AsAction;

public function handle(): bool|string
{
$response = Http::retry(3, 100)
->get('https://icanhazip.com/');

if ($response->failed()) {
$message = sprintf('Failed to fetch public IP address, %d', $response->status());

Log::warning($message);

return false;
}

return Str::trim($response->body());
}
}
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,
) {}
}
3 changes: 1 addition & 2 deletions app/Filament/Resources/ResultResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ public static function form(Form $form): Form
return number_format((float) $state, 2, '.', '').' %';
}),
Forms\Components\Textarea::make('data.message')
->label('Error Message')
->label('Message')
->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(),
])
->columnSpan(2),
Expand Down
22 changes: 22 additions & 0 deletions app/Helpers/Network.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Helpers;

class Network
{
/**
* Check if the given ip is in a network range.
*/
public static function ipInRange(string $ip, string $range): bool
{
[$range, $mask] = explode('/', $range) + [1 => '32'];

$rangeDecimal = ip2long($range);

$ipDecimal = ip2long($ip);

$maskDecimal = ~((1 << (32 - (int) $mask)) - 1);

return ($rangeDecimal & $maskDecimal) === ($ipDecimal & $maskDecimal);
}
}
68 changes: 66 additions & 2 deletions app/Jobs/Speedtests/ExecuteOoklaSpeedtest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

namespace App\Jobs\Speedtests;

use App\Actions\Helpers\GetExternalIpAddress;
use App\Enums\ResultStatus;
use App\Events\SpeedtestCompleted;
use App\Events\SpeedtestFailed;
use App\Events\SpeedtestSkipped;
use App\Helpers\Network;
use App\Models\Result;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
Expand All @@ -13,7 +16,6 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\URL;
use JJG\Ping;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
Expand Down Expand Up @@ -46,6 +48,20 @@ public function handle(): void
return;
}

$externalIp = GetExternalIpAddress::run();

$shouldSkip = $this->shouldSkip($externalIp);

if ($shouldSkip !== false) {
$this->markAsSkipped(
message: $shouldSkip,
externalIp: $externalIp,
);

return;
}

// Execute Speedtest
$options = array_filter([
'speedtest',
'--accept-license',
Expand Down Expand Up @@ -110,6 +126,26 @@ public function handle(): void
SpeedtestCompleted::dispatch($this->result);
}

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

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

/**
* Check for internet connection.
*
Expand All @@ -120,6 +156,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 +176,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 Expand Up @@ -167,6 +203,8 @@ protected function checkForInternetConnection(): bool

/**
* Check if the given URL is a valid ping URL.
*
* TODO: move to Network helper
*/
public function isValidPingUrl(string $url): bool
{
Expand All @@ -180,4 +218,30 @@ public function isValidPingUrl(string $url): bool
|| (filter_var('https://'.$url, FILTER_VALIDATE_URL) && $hasTLD($url))
|| filter_var($url, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 || FILTER_FLAG_IPV6) !== false;
}

/**
* Check if the speedtest should be skipped based on the skip ips list.
*/
public function shouldSkip(string $externalIp): bool|string
{
if (blank(config('speedtest.skip_ips'))) {
return false;
}

$skipIPs = array_map('trim', explode(',', config('speedtest.skip_ips')));

foreach ($skipIPs as $ip) {
// Check for exact IP match
if (filter_var($ip, FILTER_VALIDATE_IP) && $externalIp === $ip) {
return sprintf('"%s" was found in public IP address skip list.', $externalIp);
}

// Check for IP range match
if (strpos($ip, '/') !== false && Network::ipInRange($externalIp, $ip)) {
return sprintf('"%s" was found in public IP address skip list within range "%s".', $externalIp, $ip);
}
}

return false;
}
}
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_ips' => env('SPEEDTEST_SKIP_IPS'),

];