mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Co-authored-by: Aditya Tripathi <aditya@climactic.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.8 KiB
PHP
124 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Server;
|
|
use App\Models\Team;
|
|
use App\Services\SentinelTrafficClient;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Symfony\Component\Process\Process as SymfonyProcess;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* Process arguments are readable by every user on the server (/proc/<pid>/cmdline). The Sentinel
|
|
* token must reach curl over stdin, never as an argument of docker, the container shell, or curl.
|
|
*/
|
|
beforeEach(function () {
|
|
Server::flushIdentityMap();
|
|
config(['constants.ssh.mux_enabled' => false, 'constants.ssh.max_retries' => 1]);
|
|
Process::fake(fn () => Process::result(output: '{"requests":1}'));
|
|
|
|
$team = Team::factory()->create();
|
|
$this->server = Server::factory()->create([
|
|
'team_id' => $team->id,
|
|
'private_key_id' => PrivateKey::factory()->create(['team_id' => $team->id])->id,
|
|
]);
|
|
// Only a functional server is queried.
|
|
$this->server->settings->update(['is_reachable' => true, 'is_usable' => true]);
|
|
|
|
$this->client = new class($this->server) extends SentinelTrafficClient
|
|
{
|
|
public function fetch(string $url): string
|
|
{
|
|
return $this->remoteFetch($url);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $urls
|
|
*/
|
|
public function batch(array $urls): string
|
|
{
|
|
return $this->batchRemoteFetch($urls);
|
|
}
|
|
};
|
|
|
|
$this->bin = sys_get_temp_dir().'/sentinel-token-stdin-'.uniqid();
|
|
File::makeDirectory($this->bin);
|
|
$log = "{$this->bin}/calls.log";
|
|
// Each fake logs its arguments; curl also logs what it reads on stdin (the -H @- headers).
|
|
File::put("{$this->bin}/sudo", "#!/bin/sh\necho \"argv: sudo \$*\" >> {$log}\nexec \"\$@\"\n");
|
|
// `docker exec [options] <container> <command...>` runs the command here.
|
|
File::put("{$this->bin}/docker", "#!/bin/sh\necho \"argv: docker \$*\" >> {$log}\nshift\nwhile [ \"\${1#-}\" != \"\$1\" ]; do shift; done\nshift\nexec \"\$@\"\n");
|
|
File::put("{$this->bin}/curl", "#!/bin/sh\necho \"argv: curl \$*\" >> {$log}\necho \"stdin: \$(cat)\" >> {$log}\nprintf '{}'\n");
|
|
File::put("{$this->bin}/sh", "#!/bin/sh\necho \"argv: sh \$*\" >> {$log}\nexec /bin/sh \"\$@\"\n");
|
|
foreach (['sudo', 'docker', 'curl', 'sh'] as $fake) {
|
|
chmod("{$this->bin}/{$fake}", 0755);
|
|
}
|
|
});
|
|
|
|
afterEach(function () {
|
|
File::deleteDirectory($this->bin);
|
|
});
|
|
|
|
/**
|
|
* Runs the script that Coolify sent over SSH in a real shell with the fake binaries first on PATH.
|
|
*
|
|
* @return array{argv: array<int, string>, stdin: array<int, string>}
|
|
*/
|
|
function runSentSentinelScriptWithFakes(string $bin): array
|
|
{
|
|
$script = null;
|
|
Process::assertRan(function ($process) use (&$script) {
|
|
// The script goes to ssh on stdin.
|
|
$script = rtrim((string) $process->input, "\n");
|
|
|
|
return true;
|
|
});
|
|
|
|
$run = new SymfonyProcess(['/bin/sh', '-s'], env: ['PATH' => "{$bin}:/usr/bin:/bin"]);
|
|
$run->setInput($script."\n");
|
|
$run->mustRun();
|
|
|
|
$log = collect(file("{$bin}/calls.log", FILE_IGNORE_NEW_LINES));
|
|
|
|
return [
|
|
'argv' => $log->filter(fn (string $line) => str_starts_with($line, 'argv: '))->values()->all(),
|
|
'stdin' => $log->filter(fn (string $line) => str_starts_with($line, 'stdin: '))->values()->all(),
|
|
];
|
|
}
|
|
|
|
it('sends the sentinel token to curl over stdin for a single fetch', function (string $user) {
|
|
$this->server->update(['user' => $user]);
|
|
$url = 'http://localhost:8888/api/traffic/overview?from=2026-08-01T00:00:00Z&to=2026-08-02T00:00:00Z';
|
|
|
|
$this->client->fetch($url);
|
|
$token = $this->server->settings->fresh()->sentinel_token;
|
|
$calls = runSentSentinelScriptWithFakes($this->bin);
|
|
|
|
expect($calls['stdin'])->toBe(["stdin: Authorization: Bearer {$token}"])
|
|
->and(collect($calls['argv'])->filter(fn (string $line) => str_contains($line, $token)))->toBeEmpty()
|
|
->and(collect($calls['argv'])->filter(fn (string $line) => str_starts_with($line, 'argv: curl ')))
|
|
->toHaveCount(1)
|
|
->each->toContain($url);
|
|
})->with(['root', 'non-root' => 'cooluser']);
|
|
|
|
it('sends the sentinel token to every curl over stdin for a batched fetch', function (string $user) {
|
|
$this->server->update(['user' => $user]);
|
|
$urls = [
|
|
'http://localhost:8888/api/traffic/overview?from=F&to=T',
|
|
'http://localhost:8888/api/traffic/apps',
|
|
];
|
|
|
|
$this->client->batch($urls);
|
|
$token = $this->server->settings->fresh()->sentinel_token;
|
|
$calls = runSentSentinelScriptWithFakes($this->bin);
|
|
|
|
expect($calls['stdin'])->toBe([
|
|
"stdin: Authorization: Bearer {$token}",
|
|
"stdin: Authorization: Bearer {$token}",
|
|
])
|
|
->and(collect($calls['argv'])->filter(fn (string $line) => str_contains($line, $token)))->toBeEmpty();
|
|
})->with(['root', 'non-root' => 'cooluser']);
|