diff --git a/cli/Valet/Site.php b/cli/Valet/Site.php index 023d08e5..6435e5da 100644 --- a/cli/Valet/Site.php +++ b/cli/Valet/Site.php @@ -2,6 +2,7 @@ namespace Valet; +use DateTime; use DomainException; use Illuminate\Support\Collection; use PhpFpm; @@ -435,6 +436,26 @@ public function secured(): array })->unique()->values()->all(); } + /** + * Get all of the URLs with expiration dates that are currently secured. + */ + public function securedWithDates(): array + { + return collect($this->secured())->map(function ($site) { + + $filePath = $this->certificatesPath() . '/' . $site . '.crt'; + + $expiration = $this->cli->run("openssl x509 -enddate -noout -in $filePath"); + + $expiration = str_replace('notAfter=', '', $expiration); + + return [ + 'site' => $site, + 'exp' => new DateTime($expiration), + ]; + })->unique()->values()->all(); + } + public function isSecured(string $site): bool { $tld = $this->config->read()['tld']; @@ -480,6 +501,20 @@ public function secure(string $url, ?string $siteConf = null, int $certificateEx $this->files->putAsUser($this->nginxPath($url), $siteConf); } + /** + * Renews all domains with a trusted TLS certificate. + */ + public function renew($expireIn): void + { + collect($this->securedWithDates())->each(function ($row) use ($expireIn) { + $url = $this->domain($row['site']); + + $this->secure($url, null, $expireIn); + + info('The [' . $url . '] site has been secured with a fresh TLS certificate.'); + }); + } + /** * If CA and root certificates are nonexistent, create them and trust the root cert. * diff --git a/cli/app.php b/cli/app.php index 61eeef51..74d102a2 100644 --- a/cli/app.php +++ b/cli/app.php @@ -61,6 +61,7 @@ function (ConsoleCommandEvent $event) { output(); DnsMasq::install(Configuration::read()['tld']); output(); + Site::renew(); Nginx::restart(); output(); Valet::symlinkToUsersBin(); @@ -278,13 +279,33 @@ function (ConsoleCommandEvent $event) { /** * Display all of the currently secured sites. */ - $app->command('secured', function (OutputInterface $output) { - $sites = collect(Site::secured())->map(function ($url) { - return ['Site' => $url]; - }); + $app->command('secured [--expiring] [--days=]', function (OutputInterface $output, $expiring = null, $days = 60) { + $now = (new Datetime())->add(new DateInterval('P' . $days . 'D')); + $sites = collect(Site::securedWithDates()) + ->when($expiring, fn ($collection) => $collection->filter(fn ($row) => $row['exp'] < $now)) + ->map(function ($row) { + return [ + 'Site' => $row['site'], + 'Valid Until' => $row['exp']->format('Y-m-d H:i:s T'), + ]; + }) + ->when($expiring, fn ($collection) => $collection->sortBy('Valid Until')); + + return table(['Site', 'Valid Until'], $sites->all()); + })->descriptions('Display all of the currently secured sites', [ + '--expiring' => 'Limits the results to only sites expiring within the next 60 days.', + '--days' => 'To be used with --expiring. Limits the results to only sites expiring within the next X days. Default is set to 60.', + ]); - table(['Site'], $sites->all()); - })->descriptions('Display all of the currently secured sites'); + /** + * Renews all domains with a trusted TLS certificate. + */ + $app->command('renew [--expireIn=]', function (OutputInterface $output, $expireIn = 368) { + Site::renew($expireIn); + Nginx::restart(); + })->descriptions('Renews all domains with a trusted TLS certificate.', [ + '--expireIn' => 'The amount of days the self signed certificate is valid for. Default is set to "368"', + ]); /** * Create an Nginx proxy config for the specified domain. diff --git a/tests/CliTest.php b/tests/CliTest.php index 59ece071..a963f36b 100644 --- a/tests/CliTest.php +++ b/tests/CliTest.php @@ -436,7 +436,12 @@ public function test_secured_command() [$app, $tester] = $this->appAndTester(); $site = Mockery::mock(RealSite::class); - $site->shouldReceive('secured')->andReturn(['tighten.test']); + $site->shouldReceive('securedWithDates')->andReturn([ + [ + 'site' => 'tighten.test', + 'exp' => new DateTime('Aug 2 13:16:40 2024 GMT') + ] + ]); swap(RealSite::class, $site); $tester->run(['command' => 'secured']); @@ -445,6 +450,22 @@ public function test_secured_command() $this->assertStringContainsString('tighten.test', $tester->getDisplay()); } + public function test_renew_command() + { + [$app, $tester] = $this->appAndTester(); + + $site = Mockery::mock(RealSite::class); + $site->shouldReceive('renew')->andReturn(); + swap(RealSite::class, $site); + + $nginx = Mockery::mock(Nginx::class); + $nginx->shouldReceive('restart')->once(); + swap(Nginx::class, $nginx); + + $tester->run(['command' => 'renew']); + $tester->assertCommandIsSuccessful(); + } + public function test_proxy_command() { [$app, $tester] = $this->appAndTester();