diff options
-rw-r--r-- | appinfo/info.xml | 1 | ||||
-rw-r--r-- | lib/BackgroundJob/CheckTurnCertificate.php | 176 | ||||
-rw-r--r-- | lib/Notification/Notifier.php | 27 |
3 files changed, 204 insertions, 0 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index 3d8f0f536..3230b8574 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -63,6 +63,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m <job>OCA\Talk\BackgroundJob\CheckHostedSignalingServer</job> <job>OCA\Talk\BackgroundJob\CheckMatterbridges</job> <job>OCA\Talk\BackgroundJob\ExpireChatMessages</job> + <job>OCA\Talk\BackgroundJob\CheckTurnCertificate</job> </background-jobs> <repair-steps> diff --git a/lib/BackgroundJob/CheckTurnCertificate.php b/lib/BackgroundJob/CheckTurnCertificate.php new file mode 100644 index 000000000..2bc17c19f --- /dev/null +++ b/lib/BackgroundJob/CheckTurnCertificate.php @@ -0,0 +1,176 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 Marcel Müller <marcel.mueller@nextcloud.com> + * + * @author Marcel Müller <marcel.mueller@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\BackgroundJob; + +use OCA\Talk\AppInfo\Application; +use OCA\Talk\Config; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; +use OCP\BackgroundJob\TimedJob; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\Notification\IManager; +use Psr\Log\LoggerInterface; + +class CheckTurnCertificate extends TimedJob { + private Config $talkConfig; + private ITimeFactory $timeFactory; + private IGroupManager $groupManager; + private IManager $notificationManager; + private LoggerInterface $logger; + + public function __construct( + ITimeFactory $timeFactory, + Config $talkConfig, + IGroupManager $groupManager, + IManager $notificationManager, + LoggerInterface $logger, + ) { + parent::__construct($timeFactory); + $this->talkConfig = $talkConfig; + $this->timeFactory = $timeFactory; + $this->groupManager = $groupManager; + $this->notificationManager = $notificationManager; + $this->logger = $logger; + + // Run once a week + $this->setInterval(60 * 60 * 24 * 7); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + } + + /* + * @return string[] + */ + private function getUsersToNotify(): array { + $users = []; + + $groupToNotify = $this->groupManager->get('admin'); + if ($groupToNotify instanceof IGroup) { + foreach ($groupToNotify->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + + return $users; + } + + /** + * Create a notification and inform admins about the certificate which is about to expire + * + * @param string $turnHost The host which was checked + * @param int $days Number of days until the certificate expires + */ + private function createNotifications(string $turnHost, int $days): void { + $notification = $this->notificationManager->createNotification(); + + try { + $notification->setApp(Application::APP_ID) + ->setDateTime(new \DateTime()) + ->setObject('turn_certificate_expiration', $turnHost); + + $notification->setSubject('turn_certificate_expiration', [ + 'host' => $turnHost, + 'days_to_expire' => $days, + ]); + + foreach ($this->getUsersToNotify() as $uid) { + $notification->setUser($uid); + $this->notificationManager->notify($notification); + } + } catch (\InvalidArgumentException $e) { + return; + } + } + + /** + * Check the certificate of the specified TURN host + * + * @param string $turnHost The TURN host to check the certificate + */ + private function checkTurnServerCertificate(string $turnHost): void { + // 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, + ], + ]); + + $this->logger->debug('Checking certificate of ' . $turnHost); + + // In case no port was specified, use port 443 for the check + if (!str_contains($turnHost, ':')) { + $turnHost .= ':443'; + } + + $streamClient = stream_socket_client('ssl://' . $turnHost, $errorNumber, $errorString, 30, STREAM_CLIENT_CONNECT, $streamContext); + + if ($errorNumber !== 0) { + // Unable to connect or invalid server address + $this->logger->debug('Unable to check certificate of ' . $turnHost); + return; + } + + $streamCertificate = stream_context_get_params($streamClient); + $certificateInfo = openssl_x509_parse($streamCertificate['options']['ssl']['peer_certificate']); + $certificateValidTo = $this->timeFactory->getDateTime('@' . $certificateInfo['validTo_time_t']); + + $now = $this->timeFactory->getDateTime(); + $diff = $now->diff($certificateValidTo); + $days = $diff->days; + + // $days will always be positive -> invert it, when the end date of the certificate is in the past + if ($diff->invert) { + $days *= -1; + } + + if ($days < 10) { + $this->logger->warning('Certificate of ' . $turnHost . ' expires in less than ' . $days . ' days'); + + $this->createNotifications($turnHost, $days); + } else { + $this->logger->debug('Certificate of ' . $turnHost . ' is valid for ' . $days . ' days'); + } + } + + /** + * @inheritDoc + */ + protected function run($argument): void { + $turnServers = $this->talkConfig->getTurnServers(false); + + foreach ($turnServers as $turnServer) { + // Only check server which support the 'turns' protocol + if (!str_contains($turnServer['schemes'], 'turns')) { + continue; + } + + $this->checkTurnServerCertificate($turnServer['server']); + } + } +} diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 0b0f62e12..e22413990 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -239,6 +239,10 @@ class Notifier implements INotifier { return $this->parseRemoteInvitationMessage($notification, $l); } + if ($notification->getObjectType() === 'turn_certificate_expiration') { + return $this->parseTurnCertificateExpiration($notification, $l); + } + try { $room = $this->getRoom($notification->getObjectId(), $userId); } catch (RoomNotFoundException $e) { @@ -1030,4 +1034,27 @@ class Notifier implements INotifier { ->setIcon($notification->getIcon()) ->addParsedAction($action); } + + protected function parseTurnCertificateExpiration(INotification $notification, IL10N $l): INotification { + $subjectParameters = $notification->getSubjectParameters(); + + $host = $subjectParameters['host']; + $daysToExpire = $subjectParameters['days_to_expire']; + + if ($daysToExpire > 0) { + $subject = $l->t('The certificate of {host} expires in {days} days'); + } else { + $subject = $l->t('The certificate of {host} expired'); + } + + $subject = str_replace( + ['{host}', '{days}'], + [$host, $daysToExpire], + $subject + ); + + $notification->setParsedSubject($subject); + + return $notification; + } } |