Skip to content

Commit

Permalink
feat(setupcheck): Show an admin check if the feature is not supported…
Browse files Browse the repository at this point in the history
… by the HPB

Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed Jan 13, 2025
1 parent 164e795 commit 4ef25fd
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 94 deletions.
88 changes: 4 additions & 84 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

namespace OCA\Talk\Controller;

use GuzzleHttp\Exception\ConnectException;
use OCA\Talk\Config;
use OCA\Talk\Events\BeforeSignalingResponseSentEvent;
use OCA\Talk\Exceptions\ForbiddenException;
Expand All @@ -22,7 +21,6 @@
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCA\Talk\Service\BanService;
use OCA\Talk\Service\CertificateService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Signaling\Messages;
Expand Down Expand Up @@ -59,7 +57,6 @@ public function __construct(
private \OCA\Talk\Signaling\Manager $signalingManager,
private TalkSession $session,
private Manager $manager,
private CertificateService $certificateService,
private ParticipantService $participantService,
private SessionService $sessionService,
private IDBConnection $dbConnection,
Expand Down Expand Up @@ -272,89 +269,12 @@ private function getFederationSettings(?Room $room): ?array {
*/
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION, tags: ['settings'])]
public function getWelcomeMessage(int $serverId): DataResponse {
$signalingServers = $this->talkConfig->getSignalingServers();
if (empty($signalingServers) || !isset($signalingServers[$serverId])) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}

$url = rtrim($signalingServers[$serverId]['server'], '/');
$url = strtolower($url);

if (str_starts_with($url, 'wss://')) {
$url = 'https://' . substr($url, 6);
}

if (str_starts_with($url, 'ws://')) {
$url = 'http://' . substr($url, 5);
}

$verifyServer = (bool)$signalingServers[$serverId]['verify'];

if ($verifyServer && str_contains($url, 'https://')) {
$expiration = $this->certificateService->getCertificateExpirationInDays($url);

if ($expiration < 0) {
return new DataResponse(['error' => 'CERTIFICATE_EXPIRED'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

$client = $this->clientService->newClient();
try {
$timeBefore = $this->timeFactory->getTime();
$response = $client->get($url . '/api/v1/welcome', [
'verify' => $verifyServer,
'nextcloud' => [
'allow_local_address' => true,
],
]);
$timeAfter = $this->timeFactory->getTime();

$body = $response->getBody();
$data = json_decode($body, true);

if (!is_array($data)) {
return new DataResponse([
'error' => 'JSON_INVALID',
], Http::STATUS_INTERNAL_SERVER_ERROR);
}

if (!isset($data['version'])) {
return new DataResponse([
'error' => 'UPDATE_REQUIRED',
'version' => '',
], Http::STATUS_INTERNAL_SERVER_ERROR);
}

if (!$this->signalingManager->isCompatibleSignalingServer($response)) {
return new DataResponse([
'error' => 'UPDATE_REQUIRED',
'version' => $data['version'] ?? '',
], Http::STATUS_INTERNAL_SERVER_ERROR);
}

$responseTime = $this->timeFactory->getDateTime($response->getHeader('date'))->getTimestamp();
if (($timeBefore - Config::ALLOWED_BACKEND_TIMEOFFSET) > $responseTime
|| ($timeAfter + Config::ALLOWED_BACKEND_TIMEOFFSET) < $responseTime) {
return new DataResponse([
'error' => 'TIME_OUT_OF_SYNC',
], Http::STATUS_INTERNAL_SERVER_ERROR);
}

$missingFeatures = $this->signalingManager->getSignalingServerMissingFeatures($response);
if (!empty($missingFeatures)) {
return new DataResponse([
'warning' => 'UPDATE_OPTIONAL',
'features' => $missingFeatures,
'version' => $data['version'] ?? '',
]);
}

return new DataResponse($data);
} catch (ConnectException $e) {
return new DataResponse(['error' => 'CAN_NOT_CONNECT'], Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (\Exception $e) {
return new DataResponse(['error' => (string)$e->getCode()], Http::STATUS_INTERNAL_SERVER_ERROR);
$testResult = $this->signalingManager->checkServerCompatibility($serverId);
} catch (\OutOfBoundsException) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
return new DataResponse($testResult['data'], $testResult['status']);
}

/**
Expand Down
61 changes: 55 additions & 6 deletions lib/SetupCheck/HighPerformanceBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
namespace OCA\Talk\SetupCheck;

use OCA\Talk\Config;
use OCA\Talk\Signaling\Manager;
use OCP\AppFramework\Http;
use OCP\ICacheFactory;
use OCP\IL10N;
use OCP\IURLGenerator;
Expand All @@ -21,6 +23,7 @@ public function __construct(
readonly protected ICacheFactory $cacheFactory,
readonly protected IURLGenerator $urlGenerator,
readonly protected IL10N $l,
readonly protected Manager $signalManager,
) {
}

Expand Down Expand Up @@ -54,12 +57,58 @@ public function run(): SetupResult {
);
}

if ($this->cacheFactory->isAvailable()) {
return SetupResult::success();
try {
$testResult = $this->signalManager->checkServerCompatibility(0);
} catch (\OutOfBoundsException) {
return SetupResult::error($this->l->t('High-performance backend not configured correctly'));
}
return SetupResult::warning(
$this->l->t('It is highly recommended to configure a memory cache when running Nextcloud Talk with a High-performance backend.'),
$this->urlGenerator->linkToDocs('admin-cache'),
);
if ($testResult['status'] === Http::STATUS_INTERNAL_SERVER_ERROR) {
$error = $testResult['data']['error'];
if ($error === 'CAN_NOT_CONNECT') {
return SetupResult::error($this->l->t('Error: Cannot connect to server'));
}
if ($error === 'JSON_INVALID') {
return SetupResult::error($this->l->t('Error: Server did not respond with proper JSON'));
}
if ($error === 'CERTIFICATE_EXPIRED') {
return SetupResult::error($this->l->t('Error: Certificate expired'));
}
if ($error === 'TIME_OUT_OF_SYNC') {
return SetupResult::error($this->l->t('Error: System times of Nextcloud server and High-performance backend server are out of sync. Please make sure that both servers are connected to a time-server or manually synchronize their time.'));
}
if ($error === 'UPDATE_REQUIRED') {
$version = $testResult['data']['version'] ?? $this->l->t('Could not get version');
return SetupResult::error(str_replace(
'{version}',
$version,
$this->l->t('Error: Running version: {version}; Server needs to be updated to be compatible with this version of Talk'),
));
}
if ($error) {
return SetupResult::error(str_replace('{error}', $error, $this->l->t('Error: Server responded with: {error}')));
}
return SetupResult::error($this->l->t('Error: Unknown error occurred'));
}
if ($testResult['status'] === Http::STATUS_OK
&& isset($testResult['data']['warning'])
&& $testResult['data']['warning'] === 'UPDATE_OPTIONAL'
) {
$version = $testResult['data']['version'] ?? $this->l->t('Could not get version');
$features = implode(', ', $testResult['data']['features'] ?? []);
return SetupResult::warning(str_replace(
['{version}', '{features}'],
[$version, $features],
$this->l->t('Warning: Running version: {version}; Server does not support all features of this Talk version, missing features: {features}')
));
}

if (!$this->cacheFactory->isAvailable()) {
return SetupResult::warning(
$this->l->t('It is highly recommended to configure a memory cache when running Nextcloud Talk with a High-performance backend.'),
$this->urlGenerator->linkToDocs('admin-cache'),
);
}

return SetupResult::success();
}
}
134 changes: 134 additions & 0 deletions lib/Signaling/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@

namespace OCA\Talk\Signaling;

use GuzzleHttp\Exception\ConnectException;
use OCA\Talk\CachePrefix;
use OCA\Talk\Config;
use OCA\Talk\Room;
use OCA\Talk\Service\CertificateService;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\ICache;
use OCP\ICacheFactory;
Expand All @@ -26,11 +31,139 @@ public function __construct(
protected IConfig $serverConfig,
protected Config $talkConfig,
protected RoomService $roomService,
protected ITimeFactory $timeFactory,
protected IClientService $clientService,
protected CertificateService $certificateService,
ICacheFactory $cacheFactory,
) {
$this->cache = $cacheFactory->createDistributed(CachePrefix::SIGNALING_ASSIGNED_SERVER);
}

/**
* @param int $serverId
* @return array{status: Http::STATUS_OK, data: array<string, mixed>}|array{status: Http::STATUS_INTERNAL_SERVER_ERROR, data: array{error: string, version?: string}}
* @throws \OutOfBoundsException When the serverId is not found
*/
public function checkServerCompatibility(int $serverId): array {
$signalingServers = $this->talkConfig->getSignalingServers();
if (empty($signalingServers) || !isset($signalingServers[$serverId])) {
throw new \OutOfBoundsException();
}

$url = rtrim($signalingServers[$serverId]['server'], '/');
$url = strtolower($url);

if (str_starts_with($url, 'wss://')) {
$url = 'https://' . substr($url, 6);
}

if (str_starts_with($url, 'ws://')) {
$url = 'http://' . substr($url, 5);
}

$verifyServer = (bool)$signalingServers[$serverId]['verify'];

if ($verifyServer && str_contains($url, 'https://')) {
$expiration = $this->certificateService->getCertificateExpirationInDays($url);

if ($expiration < 0) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'CERTIFICATE_EXPIRED',
],
];
}
}

$client = $this->clientService->newClient();
try {
$timeBefore = $this->timeFactory->getTime();
$response = $client->get($url . '/api/v1/welcome', [
'verify' => $verifyServer,
'nextcloud' => [
'allow_local_address' => true,
],
]);
$timeAfter = $this->timeFactory->getTime();

$body = $response->getBody();
$data = json_decode($body, true);

if (!is_array($data)) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'JSON_INVALID',
],
];
}

if (!isset($data['version'])) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'UPDATE_REQUIRED',
'version' => '',
],
];
}

if (!$this->isCompatibleSignalingServer($response)) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'UPDATE_REQUIRED',
'version' => $data['version'] ?? '',
],
];
}

$responseTime = $this->timeFactory->getDateTime($response->getHeader('date'))->getTimestamp();
if (($timeBefore - Config::ALLOWED_BACKEND_TIMEOFFSET) > $responseTime
|| ($timeAfter + Config::ALLOWED_BACKEND_TIMEOFFSET) < $responseTime) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'TIME_OUT_OF_SYNC',
],
];
}

$missingFeatures = $this->getSignalingServerMissingFeatures($response);
if (!empty($missingFeatures)) {
return [
'status' => Http::STATUS_OK,
'data' => [
'warning' => 'UPDATE_OPTIONAL',
'features' => $missingFeatures,
'version' => $data['version'],
],
];
}

return [
'status' => Http::STATUS_OK,
'data' => $data,
];
} catch (ConnectException) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => 'CAN_NOT_CONNECT',
],
];
} catch (\Exception $e) {
return [
'status' => Http::STATUS_INTERNAL_SERVER_ERROR,
'data' => [
'error' => (string)$e->getCode(),
],
];
}

}

public function isCompatibleSignalingServer(IResponse $response): bool {
$featureHeader = $response->getHeader(self::FEATURE_HEADER);
$features = explode(',', $featureHeader);
Expand All @@ -49,6 +182,7 @@ public function getSignalingServerMissingFeatures(IResponse $response): array {

return array_values(array_diff([
'dialout',
'join-features',
], $features));
}

Expand Down
2 changes: 2 additions & 0 deletions tests/php/CapabilitiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public function testGetCapabilitiesGuest(): void {
'start-without-media' => false,
'max-duration' => 0,
'blur-virtual-background' => false,
'end-to-end-encryption' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',
Expand Down Expand Up @@ -267,6 +268,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea
'start-without-media' => false,
'max-duration' => 0,
'blur-virtual-background' => false,
'end-to-end-encryption' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',
Expand Down
Loading

0 comments on commit 4ef25fd

Please sign in to comment.