mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 17:44:36 +08:00
107 lines
4.4 KiB
PHP
107 lines
4.4 KiB
PHP
<?php
|
|
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Testing\TestResponse;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config()->set('app.maintenance.store', 'array');
|
|
config()->set('constants.coolify.self_hosted', true);
|
|
|
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true, 'allowed_ips' => '203.0.113.50,2001:db8::50']);
|
|
|
|
$team = Team::factory()->create();
|
|
$user = User::factory()->create();
|
|
$team->members()->attach($user->id, ['role' => 'owner']);
|
|
$this->apiToken = $user->createToken('client-ip', ['read'], teamId: $team->id)->plainTextToken;
|
|
});
|
|
|
|
/**
|
|
* An API call with a valid token: 403 when the allowed IPs block the client, 200 when they let it through.
|
|
*
|
|
* @param array<string, string> $headers
|
|
*/
|
|
function apiIpCheckFromPeer(string $peer, array $headers = []): TestResponse
|
|
{
|
|
return test()
|
|
->withToken(test()->apiToken)
|
|
->withHeaders($headers)
|
|
->withServerVariables(['REMOTE_ADDR' => $peer])
|
|
->getJson('/api/v1/version');
|
|
}
|
|
|
|
it('ignores forwarded headers from a public peer for the allowed IPs', function (array $headers) {
|
|
apiIpCheckFromPeer('198.51.100.7', $headers)->assertForbidden();
|
|
})->with([
|
|
'X-Forwarded-For' => [['X-Forwarded-For' => '203.0.113.50']],
|
|
'fake Cloudflare hop' => [['X-Forwarded-For' => '104.16.0.1', 'CF-Connecting-IP' => '203.0.113.50']],
|
|
]);
|
|
|
|
it('ignores forwarded headers from a public IPv6 peer for the allowed IPs', function (string $peer) {
|
|
apiIpCheckFromPeer($peer, ['X-Forwarded-For' => '2001:db8::50'])->assertForbidden();
|
|
})->with(['2001:db8:ffff::7', '::ffff:198.51.100.7']);
|
|
|
|
it('uses the forwarded client from a local reverse proxy for the allowed IPs', function (string $peer, string $client) {
|
|
apiIpCheckFromPeer($peer, ['X-Forwarded-For' => $client])->assertOk();
|
|
apiIpCheckFromPeer($peer, ['X-Forwarded-For' => '198.51.100.7'])->assertForbidden();
|
|
})->with([
|
|
'Docker network' => ['10.0.1.5', '203.0.113.50'],
|
|
'Docker bridge' => ['172.18.0.2', '203.0.113.50'],
|
|
'loopback' => ['127.0.0.1', '203.0.113.50'],
|
|
'IPv6 loopback' => ['::1', '2001:db8::50'],
|
|
'IPv6 unique local' => ['fd00::2', '2001:db8::50'],
|
|
'IPv6 link local' => ['fe80::2', '2001:db8::50'],
|
|
'IPv4-mapped private' => ['::ffff:10.0.1.5', '203.0.113.50'],
|
|
]);
|
|
|
|
it('uses the Cloudflare client IP behind a local reverse proxy only when the proxy received the request from Cloudflare', function () {
|
|
apiIpCheckFromPeer('10.0.1.5', [
|
|
'X-Forwarded-For' => '203.0.113.50, 162.158.79.140',
|
|
'CF-Connecting-IP' => '203.0.113.50',
|
|
])->assertOk();
|
|
|
|
apiIpCheckFromPeer('10.0.1.5', [
|
|
'X-Forwarded-For' => '104.16.0.1, 198.51.100.7',
|
|
'CF-Connecting-IP' => '203.0.113.50',
|
|
])->assertForbidden();
|
|
});
|
|
|
|
it('uses the Cloudflare client IP when Cloudflare connects directly', function () {
|
|
apiIpCheckFromPeer('162.158.79.140', ['CF-Connecting-IP' => '203.0.113.50'])->assertOk();
|
|
});
|
|
|
|
it('keys the API rate limit by the public peer, not by forwarded headers', function () {
|
|
config()->set('api.rate_limit', 2);
|
|
InstanceSettings::query()->update(['allowed_ips' => null]);
|
|
|
|
$push = fn (string $peer, array $headers) => test()
|
|
->withHeaders($headers)
|
|
->withServerVariables(['REMOTE_ADDR' => $peer])
|
|
->postJson('/api/v1/sentinel/push');
|
|
|
|
$push('198.51.100.7', ['X-Forwarded-For' => '203.0.113.1'])->assertUnauthorized();
|
|
$push('198.51.100.7', ['X-Forwarded-For' => '203.0.113.2', 'CF-Connecting-IP' => '203.0.113.2'])->assertUnauthorized();
|
|
$push('198.51.100.7', ['X-Forwarded-For' => '104.16.0.1', 'CF-Connecting-IP' => '203.0.113.3'])->assertStatus(429);
|
|
|
|
$push('10.0.1.5', ['X-Forwarded-For' => '203.0.113.10'])->assertUnauthorized();
|
|
$push('10.0.1.5', ['X-Forwarded-For' => '203.0.113.20'])->assertUnauthorized();
|
|
});
|
|
|
|
it('ignores X-Forwarded-For from a public peer on cloud', function () {
|
|
config()->set('constants.coolify.self_hosted', false);
|
|
config()->set('api.rate_limit', 2);
|
|
|
|
$push = fn (string $forwardedFor) => test()
|
|
->withHeader('X-Forwarded-For', $forwardedFor)
|
|
->withServerVariables(['REMOTE_ADDR' => '198.51.100.7'])
|
|
->postJson('/api/v1/sentinel/push');
|
|
|
|
$push('203.0.113.1')->assertUnauthorized();
|
|
$push('203.0.113.2')->assertUnauthorized();
|
|
$push('203.0.113.3')->assertStatus(429);
|
|
});
|