129 lines
4.1 KiB
PHP
129 lines
4.1 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Jobs;
|
||
|
|
|
||
|
|
use App\Models\Notification;
|
||
|
|
use App\Models\Site;
|
||
|
|
use App\Models\SslCertificate;
|
||
|
|
use App\Notifications\SslExpiryAlert;
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Illuminate\Bus\Batchable;
|
||
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
|
use Illuminate\Foundation\Queue\Queueable;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
|
||
|
|
class CheckSslCertificate implements ShouldQueue
|
||
|
|
{
|
||
|
|
use Queueable, Batchable;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new job instance.
|
||
|
|
*/
|
||
|
|
public function __construct(
|
||
|
|
public Site $site
|
||
|
|
) {
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Execute the job.
|
||
|
|
*/
|
||
|
|
public function handle(): void
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
// Parse URL to get host
|
||
|
|
$parsedUrl = parse_url($this->site->url);
|
||
|
|
|
||
|
|
if (!isset($parsedUrl['host'])) {
|
||
|
|
Log::warning("Invalid URL for SSL check on site {$this->site->id}: {$this->site->url}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$host = $parsedUrl['host'];
|
||
|
|
$port = $parsedUrl['scheme'] === 'https' ? 443 : 443;
|
||
|
|
|
||
|
|
// Create SSL context
|
||
|
|
$context = stream_context_create([
|
||
|
|
'ssl' => [
|
||
|
|
'capture_peer_cert' => true,
|
||
|
|
'verify_peer' => false,
|
||
|
|
'verify_peer_name' => false,
|
||
|
|
],
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Connect to the server and get SSL certificate
|
||
|
|
$client = @stream_socket_client(
|
||
|
|
"ssl://{$host}:{$port}",
|
||
|
|
$errno,
|
||
|
|
$errstr,
|
||
|
|
10,
|
||
|
|
STREAM_CLIENT_CONNECT,
|
||
|
|
$context
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($client === false) {
|
||
|
|
Log::warning("Failed to connect to {$host}:{$port} for SSL check: {$errstr}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$params = stream_context_get_params($client);
|
||
|
|
fclose($client);
|
||
|
|
|
||
|
|
if (!isset($params['options']['ssl']['peer_certificate'])) {
|
||
|
|
Log::warning("No SSL certificate found for site {$this->site->id}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
|
||
|
|
|
||
|
|
if ($cert === false) {
|
||
|
|
Log::warning("Failed to parse SSL certificate for site {$this->site->id}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract certificate information
|
||
|
|
$issuer = $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? 'Unknown';
|
||
|
|
$expiresAt = Carbon::createFromTimestamp($cert['validTo_time_t']);
|
||
|
|
$checkedAt = now();
|
||
|
|
|
||
|
|
// Store the SSL certificate check result
|
||
|
|
SslCertificate::create([
|
||
|
|
'site_id' => $this->site->id,
|
||
|
|
'issuer' => $issuer,
|
||
|
|
'expires_at' => $expiresAt,
|
||
|
|
'checked_at' => $checkedAt,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Check if certificate expires within 30 days
|
||
|
|
$daysUntilExpiry = $checkedAt->diffInDays($expiresAt, false);
|
||
|
|
|
||
|
|
if ($daysUntilExpiry < 30 && $daysUntilExpiry >= 0) {
|
||
|
|
// Send notification to user
|
||
|
|
$this->site->user->notify(new SslExpiryAlert(
|
||
|
|
$this->site,
|
||
|
|
$issuer,
|
||
|
|
$expiresAt,
|
||
|
|
(int) $daysUntilExpiry,
|
||
|
|
false
|
||
|
|
));
|
||
|
|
|
||
|
|
Log::warning("SSL certificate expiring soon for site {$this->site->id}: {$daysUntilExpiry} days remaining");
|
||
|
|
} elseif ($daysUntilExpiry < 0) {
|
||
|
|
// Certificate already expired
|
||
|
|
$this->site->user->notify(new SslExpiryAlert(
|
||
|
|
$this->site,
|
||
|
|
$issuer,
|
||
|
|
$expiresAt,
|
||
|
|
(int) $daysUntilExpiry,
|
||
|
|
true
|
||
|
|
));
|
||
|
|
|
||
|
|
Log::error("SSL certificate expired for site {$this->site->id}");
|
||
|
|
}
|
||
|
|
|
||
|
|
Log::info("SSL check completed for site {$this->site->id} ({$this->site->url}): Issuer={$issuer}, Expires={$expiresAt->format('Y-m-d')}");
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
Log::error("SSL check failed for site {$this->site->id}: {$e->getMessage()}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|