Skip to content

Commit f096c0a

Browse files
authored
[Feature] Skip test when Public IP is in an list (#1714)
* 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
1 parent 0bdd141 commit f096c0a

File tree

6 files changed

+187
-2
lines changed

6 files changed

+187
-2
lines changed

app/Enums/ResultStatus.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ enum ResultStatus: string implements HasColor, HasLabel
99
{
1010
case Completed = 'completed'; // a speedtest that ran successfully.
1111
case Failed = 'failed'; // a speedtest that failed to run successfully.
12-
case Started = 'started'; // a speedtest that has been started by a worker but has not finish running.
12+
case Started = 'started'; // a speedtest that has been started by a worker but has not finished running.
13+
case Skipped = 'skipped'; // a speedtest that was skipped.
1314

1415
public function getColor(): ?string
1516
{
1617
return match ($this) {
1718
self::Completed => 'success',
1819
self::Failed => 'danger',
1920
self::Started => 'warning',
21+
self::Skipped => 'info', // Adding Skipped state with a color
2022
};
2123
}
2224

@@ -26,6 +28,7 @@ public function getLabel(): ?string
2628
self::Completed => 'Completed',
2729
self::Failed => 'Failed',
2830
self::Started => 'Started',
31+
self::Skipped => 'Skipped',
2932
};
3033
}
3134
}

app/Events/SpeedtestSkipped.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Events;
4+
5+
use App\Models\Result;
6+
use Illuminate\Broadcasting\InteractsWithSockets;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class SpeedtestSkipped
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
/**
15+
* Create a new event instance.
16+
*/
17+
public function __construct(
18+
public Result $result,
19+
) {}
20+
}

app/Filament/Resources/ResultResource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ public static function form(Form $form): Form
121121
->hint(new HtmlString('&#x1f517;<a href="https://docs.speedtest-tracker.dev/help/error-messages" target="_blank" rel="nofollow">Error Messages</a>'))
122122
->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Failed)
123123
->columnSpanFull(),
124+
Forms\Components\Textarea::make('data.message')
125+
->label('Skip Message')
126+
->hidden(fn (Result $record): bool => $record->status !== ResultStatus::Skipped)
127+
->columnSpanFull(),
124128
])
125129
->columnSpan(2),
126130
Forms\Components\Section::make()

app/Jobs/Speedtests/ExecuteOoklaSpeedtest.php

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
use App\Enums\ResultStatus;
66
use App\Events\SpeedtestCompleted;
77
use App\Events\SpeedtestFailed;
8+
use App\Events\SpeedtestSkipped;
89
use App\Models\Result;
10+
use App\Services\PublicIpService;
911
use Illuminate\Bus\Queueable;
1012
use Illuminate\Contracts\Queue\ShouldBeUnique;
1113
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -35,6 +37,7 @@ class ExecuteOoklaSpeedtest implements ShouldBeUnique, ShouldQueue
3537
public function __construct(
3638
public Result $result,
3739
public ?int $serverId = null,
40+
protected PublicIpService $publicIpService = new PublicIpService
3841
) {}
3942

4043
/**
@@ -46,6 +49,24 @@ public function handle(): void
4649
return;
4750
}
4851

52+
// Fetch public IP data using the PublicIpService
53+
$ipData = $this->publicIpService->getPublicIp();
54+
$currentIp = $ipData['ip'] ?? 'unknown';
55+
$isp = $ipData['isp'] ?? 'unknown'; // Get the ISP value here
56+
57+
// Retrieve SPEEDTEST_SKIP_IP Settings
58+
$skipSettings = array_filter(array_map('trim', explode(';', config('speedtest.skip_ip'))));
59+
60+
// Check Each Skip Setting
61+
$skipMessage = $this->publicIpService->shouldSkipIp($currentIp, $skipSettings);
62+
if ($skipMessage) {
63+
// Pass the $isp along with $currentIp and $skipMessage
64+
$this->markAsSkipped($skipMessage, $currentIp, $isp);
65+
66+
return;
67+
}
68+
69+
// Execute Speedtest
4970
$options = array_filter([
5071
'speedtest',
5172
'--accept-license',
@@ -74,6 +95,30 @@ public function handle(): void
7495
// Filter out empty messages and concatenate
7596
$errorMessage = implode(' | ', array_filter($errorMessages));
7697

98+
// Add server ID to the error message if it exists
99+
if ($this->serverId !== null) {
100+
$this->result->update([
101+
'data' => [
102+
'type' => 'log',
103+
'level' => 'error',
104+
'message' => $errorMessage,
105+
'server' => [
106+
'id' => $this->serverId,
107+
],
108+
],
109+
'status' => ResultStatus::Failed,
110+
]);
111+
} else {
112+
$this->result->update([
113+
'data' => [
114+
'type' => 'log',
115+
'level' => 'error',
116+
'message' => $errorMessage,
117+
],
118+
'status' => ResultStatus::Failed,
119+
]);
120+
}
121+
77122
// Prepare the error message data
78123
$data = [
79124
'type' => 'log',
@@ -110,6 +155,27 @@ public function handle(): void
110155
SpeedtestCompleted::dispatch($this->result);
111156
}
112157

158+
/**
159+
* Mark the test as skipped with a specific message.
160+
*/
161+
protected function markAsSkipped(string $message, string $currentIp, string $isp): void
162+
{
163+
$this->result->update([
164+
'data' => [
165+
'type' => 'log',
166+
'level' => 'info',
167+
'message' => $message,
168+
'interface' => [
169+
'externalIp' => $currentIp,
170+
],
171+
'isp' => $isp,
172+
],
173+
'status' => ResultStatus::Skipped,
174+
]);
175+
176+
SpeedtestSkipped::dispatch($this->result);
177+
}
178+
113179
/**
114180
* Check for internet connection.
115181
*
@@ -120,6 +186,7 @@ protected function checkForInternetConnection(): bool
120186
$url = config('speedtest.ping_url');
121187

122188
// Skip checking for internet connection if ping url isn't set (disabled)
189+
123190
if (blank($url)) {
124191
return true;
125192
}
@@ -139,7 +206,6 @@ protected function checkForInternetConnection(): bool
139206
return false;
140207
}
141208

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

145211
$ping = new Ping(

app/Services/PublicIpService.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace App\Services;
4+
5+
use Illuminate\Support\Facades\Log;
6+
7+
class PublicIpService
8+
{
9+
/**
10+
* Get the public IP address and its associated details using ipapi.co.
11+
*/
12+
public function getPublicIp(): array
13+
{
14+
try {
15+
// Fetch location data from ifconfig.co using curl
16+
$ch = curl_init('https://ifconfig.co/json');
17+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
18+
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
19+
$response = curl_exec($ch);
20+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
21+
curl_close($ch);
22+
23+
// Validate the HTTP response
24+
if ($httpCode !== 200) {
25+
\Log::error("Failed to fetch public IP data from ifconfig.co. HTTP Status Code: $httpCode");
26+
27+
return ['ip' => 'unknown', 'isp' => 'unknown'];
28+
}
29+
30+
// Decode the JSON response
31+
$data = json_decode($response, true);
32+
33+
// Validate the response format
34+
if (json_last_error() === JSON_ERROR_NONE && isset($data['ip'])) {
35+
return [
36+
'ip' => $data['ip'],
37+
'isp' => $data['asn_org'] ?? 'unknown',
38+
];
39+
}
40+
41+
// Log error if the response is invalid
42+
\Log::error('Invalid response from ifconfig.co: '.$response);
43+
44+
return ['ip' => 'unknown', 'isp' => 'unknown'];
45+
} catch (\Exception $e) {
46+
\Log::error("Error fetching public IP data from ifconfig.co: {$e->getMessage()}");
47+
48+
// Fallback response
49+
return ['ip' => 'unknown', 'isp' => 'unknown'];
50+
}
51+
}
52+
53+
/**
54+
* Check if the current IP should be skipped.
55+
*/
56+
public function shouldSkipIp(string $currentIp, array $skipSettings): bool|string
57+
{
58+
foreach ($skipSettings as $setting) {
59+
// Check for exact IP match
60+
if (filter_var($setting, FILTER_VALIDATE_IP)) {
61+
if ($currentIp === $setting) {
62+
return "Current public IP address ($currentIp) is listed to be skipped for testing.";
63+
}
64+
}
65+
66+
// Check for subnet match
67+
if (strpos($setting, '/') !== false && $this->ipInSubnet($currentIp, $setting)) {
68+
return "Current public IP address ($currentIp) falls within the excluded subnet ($setting).";
69+
}
70+
}
71+
72+
return false;
73+
}
74+
75+
/**
76+
* Check if an IP is in a given subnet.
77+
*/
78+
protected function ipInSubnet(string $ip, string $subnet): bool
79+
{
80+
[$subnet, $mask] = explode('/', $subnet) + [1 => '32'];
81+
$subnetDecimal = ip2long($subnet);
82+
$ipDecimal = ip2long($ip);
83+
$maskDecimal = ~((1 << (32 - (int) $mask)) - 1);
84+
85+
return ($subnetDecimal & $maskDecimal) === ($ipDecimal & $maskDecimal);
86+
}
87+
}

config/speedtest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@
3535

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

38+
/**
39+
* IP filtering settings.
40+
*/
41+
'skip_ip' => env('SPEEDTEST_SKIP_IP', ''), // Comma-separated list of IPs, ISPs and subnets
42+
3843
];

0 commit comments

Comments
 (0)