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
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

];