You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

117 lines
3.2 KiB

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\Service;
use Psr\Log\LoggerInterface;
class CertificateService {
public function __construct(
private LoggerInterface $logger,
) {
}
/**
* Parse a url and only returns the host and optionally the port
*
* @param string $host The url to parse (e.g. 'https://hostname:port/directory')
* @return string|null null if the url has a non-tls scheme, otherwise the host and optionally the port (e.g. 'hostname:port')
*/
public function getParsedTlsHost(string $host): ?string {
$parsedUrl = parse_url($host);
// parse_url failed, $host is a seriously malformed URL
if ($parsedUrl === false) {
return null;
}
if (isset($parsedUrl['scheme'])) {
$scheme = strtolower($parsedUrl['scheme']);
// When we have a scheme specified which is different than https/wss, there's no tls host
if ($scheme !== 'https' && $scheme !== 'wss') {
return null;
}
}
// When we are unable to retrieve a host from the URL, just return the original host
if (!isset($parsedUrl['host'])) {
return $host;
}
$parsedHost = $parsedUrl['host'];
if (isset($parsedUrl['port'])) {
$parsedHost .= ':' . $parsedUrl['port'];
}
return $parsedHost;
}
/**
* Retrieve the hosts certificate expiration in days
*
* @param string $host The host to check the certificate of without scheme
* @return int|null Days until the certificate expires (negative when it's already expired)
*/
public function getCertificateExpirationInDays(string $host): ?int {
$parsedHost = $this->getParsedTlsHost($host);
if ($parsedHost === null) {
// Unable to parse the specified host
$this->logger->debug('Ignoring certificate check of non-tls host ' . $host);
return null;
}
// We need to disable verification here to also get an expired certificate
$streamContext = stream_context_create([
'ssl' => [
'capture_peer_cert' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
// In case no port was specified, use port 443 for the check
if (!str_contains($parsedHost, ':')) {
$parsedHost .= ':443';
}
$this->logger->debug('Checking certificate of ' . $parsedHost);
$streamClient = stream_socket_client('ssl://' . $parsedHost, $errorNumber, $errorString, 30, STREAM_CLIENT_CONNECT, $streamContext);
if ($streamClient === false || $errorNumber !== 0) {
// Unable to connect or invalid server address
$this->logger->debug('Unable to check certificate of ' . $parsedHost);
return null;
}
$streamCertificate = stream_context_get_params($streamClient);
$certificateInfo = openssl_x509_parse($streamCertificate['options']['ssl']['peer_certificate']);
$certificateValidTo = new \DateTime('@' . $certificateInfo['validTo_time_t']);
$now = new \DateTime();
$diff = $now->diff($certificateValidTo);
$days = $diff->days;
if ($days === false) {
return null;
}
// $days will always be positive -> invert it, when the end date of the certificate is in the past
if ($diff->invert) {
$days *= -1;
}
return $days;
}
}