Skip to content

Commit 1eac3e0

Browse files
committed
Merge remote-tracking branch 'Origin/main' into feature/queue-and-rate-tweaks
2 parents 0237838 + f446fdb commit 1eac3e0

File tree

6 files changed

+149
-3
lines changed

6 files changed

+149
-3
lines changed

app/Http/Requests/SiteRequest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
namespace App\Http\Requests;
44

5+
use App\Rules\RemoteURL;
56
use Illuminate\Foundation\Http\FormRequest;
67

78
class SiteRequest extends FormRequest
89
{
910
public function rules(): array
1011
{
1112
return [
12-
'url' => 'required|url',
13+
'url' => [
14+
'required',
15+
'string',
16+
'url',
17+
'max:255',
18+
new RemoteURL()
19+
],
1320
'key' => 'required|string|min:32|max:64',
1421
];
1522
}

app/Network/DNSLookup.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Network;
4+
5+
class DNSLookup
6+
{
7+
public function getIPs(string $hostname): array
8+
{
9+
// IP as host given
10+
$ips = filter_var($hostname, FILTER_VALIDATE_IP) ? [$hostname] : [];
11+
12+
// Hostname given, resolve IPs
13+
if (count($ips) === 0) {
14+
try {
15+
$dnsResults = dns_get_record($hostname, DNS_A + DNS_AAAA);
16+
} catch (\Throwable $e) {
17+
return [];
18+
}
19+
20+
if ($dnsResults) {
21+
$ips = array_map(function ($dnsResult) {
22+
return !empty($dnsResult['ip']) ? $dnsResult['ip'] : $dnsResult['ipv6'];
23+
}, $dnsResults);
24+
}
25+
}
26+
27+
return $ips;
28+
}
29+
}

app/Rules/RemoteURL.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Rules;
4+
5+
use App\Network\DNSLookup;
6+
use Closure;
7+
use Illuminate\Contracts\Validation\ValidationRule;
8+
use Illuminate\Support\Facades\App;
9+
10+
class RemoteURL implements ValidationRule
11+
{
12+
/**
13+
* Run the validation rule.
14+
*
15+
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
16+
*/
17+
public function validate(string $attribute, mixed $value, Closure $fail): void
18+
{
19+
if (!is_string($value)) {
20+
$fail("Invalid URL: URL must be a string.");
21+
22+
return;
23+
}
24+
25+
$host = (string) parse_url($value, PHP_URL_HOST);
26+
$ips = App::make(DNSLookup::class)->getIPs($host);
27+
28+
// Could not resolve given address
29+
if (count($ips) === 0) {
30+
$fail("Invalid URL: unresolvable site URL.");
31+
}
32+
33+
// Check each resolved IP
34+
foreach ($ips as $ip) {
35+
if (!filter_var(
36+
$ip,
37+
FILTER_VALIDATE_IP,
38+
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
39+
)
40+
) {
41+
$fail("Invalid URL: local address are disallowed as site URL.");
42+
}
43+
}
44+
}
45+
}

tests/Feature/Api/SiteControllerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public function testCheckingASiteReturns404ForInvalidSite(): void
123123
["url" => "https://www.joomlaf.org", "key" => "foobar123foobar123foobar123foobar123"]
124124
);
125125

126-
$response->assertStatus(404);
126+
$response->assertStatus(422);
127127
}
128128

129129
public function testDeleteASiteReturns404ForInvalidSite(): void
@@ -133,7 +133,7 @@ public function testDeleteASiteReturns404ForInvalidSite(): void
133133
["url" => "https://www.joomlaf.org", "key" => "foobar123foobar123foobar123foobar123"]
134134
);
135135

136-
$response->assertStatus(404);
136+
$response->assertStatus(422);
137137
}
138138

139139
public function testDeleteASiteRemovesRow(): void
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Tests\Unit\Network;
4+
5+
use App\Network\DNSLookup;
6+
use Tests\TestCase;
7+
8+
class DNSLookupTest extends TestCase
9+
{
10+
public function testIpAsHostIsReturned()
11+
{
12+
$object = new DNSLookup();
13+
$this->assertSame(['127.0.0.1'], $object->getIPs('127.0.0.1'));
14+
}
15+
16+
public function testEmptyArrayIsReturnedForInvalidHost()
17+
{
18+
$object = new DNSLookup();
19+
$this->assertSame([], $object->getIPs('invalid.host.with.bogus.tld'));
20+
}
21+
22+
public function testIpsAreReturned()
23+
{
24+
$object = new DNSLookup();
25+
$this->assertGreaterThan(5, $object->getIPs('joomla.org'));
26+
}
27+
}

tests/Unit/Rules/RemoteURLTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Tests\Unit\Rules;
4+
5+
use App\Rules\RemoteURL;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
use Tests\TestCase;
8+
9+
class RemoteURLTest extends TestCase
10+
{
11+
#[DataProvider('urlDataProvider')]
12+
public function testRuleHandlesIpsAndHosts($host, $expectedResult, $expectedMessage)
13+
{
14+
$object = new RemoteURL();
15+
16+
$object->validate('url', $host, function ($message) use ($expectedResult, $expectedMessage) {
17+
if (!$expectedResult) {
18+
$this->assertTrue(true);
19+
$this->assertSame($expectedMessage, $message);
20+
}
21+
});
22+
23+
if ($expectedResult) {
24+
$this->assertTrue(true);
25+
}
26+
}
27+
28+
public static function urlDataProvider(): array
29+
{
30+
return [
31+
['https://127.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'],
32+
['https://localhost', false, 'Invalid URL: local address are disallowed as site URL.'],
33+
['https://10.0.0.1', false, 'Invalid URL: local address are disallowed as site URL.'],
34+
['https://joomla.org', true, ''],
35+
['https://invalid.host.tld', false,'Invalid URL: unresolvable site URL.'],
36+
];
37+
}
38+
}

0 commit comments

Comments
 (0)