diff options
-rw-r--r-- | lib/Controller/HostedSignalingServerController.php | 365 | ||||
-rw-r--r-- | lib/DataObjects/AccountId.php | 40 | ||||
-rw-r--r-- | lib/DataObjects/RegisterAccountData.php | 67 | ||||
-rw-r--r-- | lib/Exceptions/HostedSignalingServerAPIException.php | 14 | ||||
-rw-r--r-- | lib/Exceptions/HostedSignalingServerInputException.php | 14 | ||||
-rw-r--r-- | lib/Service/HostedSignalingServerService.php | 413 |
6 files changed, 573 insertions, 340 deletions
diff --git a/lib/Controller/HostedSignalingServerController.php b/lib/Controller/HostedSignalingServerController.php index 9578ba7cb..0497f5229 100644 --- a/lib/Controller/HostedSignalingServerController.php +++ b/lib/Controller/HostedSignalingServerController.php @@ -25,7 +25,10 @@ declare(strict_types=1); namespace OCA\Talk\Controller; -use GuzzleHttp\Exception\ClientException; +use OCA\Talk\DataObjects\RegisterAccountData; +use OCA\Talk\Exceptions\HostedSignalingServerAPIException; +use OCA\Talk\Exceptions\HostedSignalingServerInputException; +use OCA\Talk\Service\HostedSignalingServerService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; @@ -34,7 +37,6 @@ use OCP\IConfig; use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; -use OCP\Security\ISecureRandom; class HostedSignalingServerController extends OCSController { @@ -44,24 +46,24 @@ class HostedSignalingServerController extends OCSController { protected $l10n; /** @var IConfig */ protected $config; - /** @var ISecureRandom */ - protected $secureRandom; /** @var ILogger */ protected $logger; + /** @var HostedSignalingServerService */ + private $hostedSignalingServerService; public function __construct(string $appName, IRequest $request, IClientService $clientService, IL10N $l10n, IConfig $config, - ISecureRandom $secureRandom, - ILogger $logger) { + ILogger $logger, + HostedSignalingServerService $hostedSignalingServerService) { parent::__construct($appName, $request); $this->clientService = $clientService; $this->l10n = $l10n; $this->config = $config; - $this->secureRandom = $secureRandom; $this->logger = $logger; + $this->hostedSignalingServerService = $hostedSignalingServerService; } /** @@ -82,342 +84,25 @@ class HostedSignalingServerController extends OCSController { } public function requestTrial(string $url, string $name, string $email, string $language, string $country): DataResponse { - $client = $this->clientService->newClient(); - $apiServer = $this->config->getSystemValue('talk_hardcoded_hpb_service', 'https://api.spreed.cloud'); - try { - $nonce = $this->secureRandom->generate(32); - $this->config->setAppValue('spreed', 'hosted-signaling-server-nonce', $nonce); - $response = $client->post($apiServer . '/v1/account', [ - 'json' => [ - 'url' => $url, - 'name' => $name, - 'email' => $email, - 'language' => $language, - 'country' => $country, - ], - 'headers' => [ - 'X-Account-Service-Nonce' => $nonce, - ], - 'timeout' => 10, - ]); - } catch(ClientException $e) { - $response = $e->getResponse(); - - if ($response === null) { - $this->logger->logException($e, [ - 'app' => 'spreed', - 'message' => 'Failed to request hosted signaling server trial', - ]); - return new DataResponse([ - 'message' => $this->l10n->t('Failed to request trial because the trial server is unreachable. Please try again later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $status = $response->getStatusCode(); - switch ($status) { - case Http::STATUS_UNAUTHORIZED: - // TODO log it - return new DataResponse([ - 'message' => $this->l10n->t('There is a problem with the authentication of this instance. Maybe it is not reachable from the outside to verify it\'s URL.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - case Http::STATUS_BAD_REQUEST: - $body = $response->getBody()->getContents(); - if ($body) { - $parsedBody = json_decode($body, true); - if (json_last_error() !== JSON_ERROR_NONE) { - $this->logger->error('Requesting hosted signaling server trial failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - if ($parsedBody['reason']) { - $message = ''; - switch($parsedBody['reason']) { - case 'invalid_content_type': - $log = 'The content type is invalid.'; - break; - case 'invalid_json': - $log = 'The JSON is invalid.'; - break; - case 'missing_url': - $log = 'The URL is missing.'; - break; - case 'missing_name': - $log = 'The name is missing.'; - break; - case 'missing_email': - $log = 'The email address is missing'; - break; - case 'missing_language': - $log = 'The language code is missing.'; - break; - case 'missing_country': - $log = 'The country code is missing.'; - break; - case 'invalid_url': - $message = $this->l10n->t('The URL is invalid.'); - $log = 'The entered URL is invalid.'; - break; - case 'https_required': - $message = $this->l10n->t('An HTTPS URL is required.'); - $log = 'An HTTPS URL is required.'; - break; - case 'invalid_email': - $message = $this->l10n->t('The email address is invalid.'); - $log = 'The email address is invalid.'; - break; - case 'invalid_language': - $message = $this->l10n->t('The language is invalid.'); - $log = 'The language is invalid.'; - break; - case 'invalid_country': - $message = $this->l10n->t('The country is invalid.'); - $log = 'The country is invalid.'; - break; - } - // user error - if ($message !== '') { - $this->logger->warning('Requesting hosted signaling server trial failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); - return new DataResponse([ - 'message' => $message - ], Http::STATUS_BAD_REQUEST); - } - $this->logger->error('Requesting hosted signaling server trial failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); - return new DataResponse([ - 'message' => $this->l10n->t('There is a problem with the request of the trial. Please check your logs for further information.') - ], Http::STATUS_BAD_REQUEST); - } - } - - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_BAD_REQUEST); - case Http::STATUS_TOO_MANY_REQUESTS: - $body = $response->getBody()->getContents(); - $this->logger->error('Requesting hosted signaling server trial failed: too many requests - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Too many requests are send from your servers address. Please try again later.') - ], Http::STATUS_TOO_MANY_REQUESTS); - case Http::STATUS_CONFLICT: - $body = $response->getBody()->getContents(); - $this->logger->error('Requesting hosted signaling server trial failed: already registered - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('There is already a trial registered for this Nextcloud instance.') - ], Http::STATUS_CONFLICT); - case Http::STATUS_INTERNAL_SERVER_ERROR: - $body = $response->getBody()->getContents(); - $this->logger->error('Requesting hosted signaling server trial failed: internal server error - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened. Please try again later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - default: - $body = $response->getBody()->getContents(); - $this->logger->error('Requesting hosted signaling server trial failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Failed to request trial because the trial server behaved wrongly. Please try again later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } catch (\Exception $e) { - $this->logger->logException($e, [ - 'app' => 'spreed', - 'message' => 'Failed to request hosted signaling server trial', - ]); - - return new DataResponse([ - 'message' => $this->l10n->t('Failed to request trial because the trial server is unreachable. Please try again later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $status = $response->getStatusCode(); - - if ($status !== Http::STATUS_CREATED) { - $body = $response->getBody(); - $this->logger->error('Requesting hosted signaling server trial failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $body = $response->getBody(); - $data = json_decode($body, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - $this->logger->error('Requesting hosted signaling server trial failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - if (!isset($data['account_id'])) { - $this->logger->error('Requesting hosted signaling server trial failed: no account ID transfered - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $this->config->setAppValue('spreed', 'hosted-signaling-server-account-id', $data['account_id']); - - $data = [ - 'account_id' => $this->config->getAppValue('spreed', 'hosted-signaling-server-account-id') - ]; - - // account is now properly requested - - // fetch account details - - try { - $nonce = $this->secureRandom->generate(32); - $this->config->setAppValue('spreed', 'hosted-signaling-server-nonce', $nonce); - $response = $client->get($apiServer . '/v1/account/' . $data['account_id'], [ - 'headers' => [ - 'X-Account-Service-Nonce' => $nonce, - ], - 'timeout' => 10, - ]); - } catch(ClientException $e) { - $response = $e->getResponse(); - - if ($response === null) { - $this->logger->logException($e, [ - 'app' => 'spreed', - 'message' => 'Trial requested but failed to get account information', - ]); - return new DataResponse([ - 'message' => $this->l10n->t('Trial requested but failed to get account information. Please check back later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $status = $response->getStatusCode(); - - switch ($status) { - case Http::STATUS_UNAUTHORIZED: - // TODO log it - return new DataResponse([ - 'message' => $this->l10n->t('There is a problem with the authentication of this request. Maybe the account was deleted.') // TODO deleted? - ], Http::STATUS_INTERNAL_SERVER_ERROR); - case Http::STATUS_BAD_REQUEST: - $body = $response->getBody()->getContents(); - if ($body) { - $parsedBody = json_decode($body, true); - if (json_last_error() !== JSON_ERROR_NONE) { - $this->logger->error('Getting the account information failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - if ($parsedBody['reason']) { - switch($parsedBody['reason']) { - case 'missing_account_id': - $log = 'The account ID is missing.'; - break; - default: - $body = $response->getBody()->getContents(); - $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Failed to fetch account information because the trial server behaved wrongly. Please check back later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - $this->logger->error('Getting the account information failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); - return new DataResponse([ - 'message' => $this->l10n->t('There is a problem with fetching the account information. Please check your logs for further information.') - ], Http::STATUS_BAD_REQUEST); - } - } - - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_BAD_REQUEST); - case Http::STATUS_TOO_MANY_REQUESTS: - $body = $response->getBody()->getContents(); - $this->logger->error('Getting the account information failed: too many requests - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Too many requests are send from your servers address. Please try again later.') - ], Http::STATUS_TOO_MANY_REQUESTS); - case Http::STATUS_NOT_FOUND: - $body = $response->getBody()->getContents(); - $this->logger->error('Getting the account information failed: account not found - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('There is no such account registered.') - ], Http::STATUS_CONFLICT); - case Http::STATUS_INTERNAL_SERVER_ERROR: - $body = $response->getBody()->getContents(); - $this->logger->error('Getting the account information failed: internal server error - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened. Please try again later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - default: - $body = $response->getBody()->getContents(); - $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Failed to fetch account information because the trial server behaved wrongly. Please check back later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } catch (\Exception $e) { - $this->logger->logException($e, [ - 'app' => 'spreed', - 'message' => 'Failed to request hosted signaling server trial', - ]); - - return new DataResponse([ - 'message' => $this->l10n->t('Failed to fetch account information because the trial server is unreachable. Please check back later.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $status = $response->getStatusCode(); - - if ($status !== Http::STATUS_OK) { - $body = $response->getBody(); - $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - $body = $response->getBody(); - $data = json_decode($body, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - $this->logger->error('Getting the account information failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); - } - - return $this->sanitizeAndCacheAccountData($data); - } - - protected function sanitizeAndCacheAccountData(array $data): DataResponse { - if (!isset($data['status']) - || !isset($data['created']) - || ($data['status'] === 'active' && ( - !isset($data['signaling']) - || !isset($data['signaling']['url']) - || !isset($data['signaling']['secret']) - ) - ) - || !isset($data['owner']) - || !isset($data['owner']['url']) - || !isset($data['owner']['name']) - || !isset($data['owner']['email']) - || !isset($data['owner']['language']) - || !isset($data['owner']['country']) - || ($data['status'] === 'active' && ( - !isset($data['limits']) - || !isset($data['limits']['users']) - ) - ) - || (in_array($data['status'], ['error', 'blocked']) && !isset($data['reason'])) - || !in_array($data['status'], ['error', 'blocked', 'pending', 'active', 'expired']) - ) { - $this->logger->error('Getting the account information failed: response is missing mandatory field - data: ' . json_encode($data), ['app' => 'spreed']); - return new DataResponse([ - 'message' => $this->l10n->t('Something unexpected happened.') - ], Http::STATUS_INTERNAL_SERVER_ERROR); + $registerAccountData = new RegisterAccountData( + $url, + $name, + $email, + $language, + $country + ); + + $accountId = $this->hostedSignalingServerService->registerAccount($registerAccountData); + $accountInfo = $this->hostedSignalingServerService->fetchAccountInfo($accountId); + $this->config->setAppValue('spreed', 'hosted-signaling-server-account', json_encode($accountInfo)); + } catch (HostedSignalingServerAPIException $e) { // API or connection issues + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (HostedSignalingServerInputException $e) { // user solvable issues + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } - $this->config->setAppValue('spreed', 'hosted-signaling-server-account', json_encode($data)); - return new DataResponse($data); + return new DataResponse($accountInfo); } } diff --git a/lib/DataObjects/AccountId.php b/lib/DataObjects/AccountId.php new file mode 100644 index 000000000..b600cef5d --- /dev/null +++ b/lib/DataObjects/AccountId.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de> + * + * @author Morris Jobke <hey@morrisjobke.de> + * + * @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\DataObjects; + +class AccountId { + + /** @var string */ + private $accountId; + + public function __construct(string $accountId) { + $this->accountId = $accountId; + } + + public function get(): string { + return $this->accountId; + } +} diff --git a/lib/DataObjects/RegisterAccountData.php b/lib/DataObjects/RegisterAccountData.php new file mode 100644 index 000000000..5742f870f --- /dev/null +++ b/lib/DataObjects/RegisterAccountData.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de> + * + * @author Morris Jobke <hey@morrisjobke.de> + * + * @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\DataObjects; + +class RegisterAccountData { + /** @var string */ + private $url; + /** @var string */ + private $name; + /** @var string */ + private $email; + /** @var string */ + private $language; + /** @var string */ + private $country; + + public function __construct(string $url, string $name, string $email, string $language, string $country) { + $this->url = $url; + $this->name = $name; + $this->email = $email; + $this->language = $language; + $this->country = $country; + } + + public function getUrl(): string { + return $this->url; + } + + public function getName(): string { + return $this->name; + } + + public function getEmail(): string { + return $this->email; + } + + public function getLanguage(): string { + return $this->language; + } + + public function getCountry(): string { + return $this->country; + } +} diff --git a/lib/Exceptions/HostedSignalingServerAPIException.php b/lib/Exceptions/HostedSignalingServerAPIException.php new file mode 100644 index 000000000..a23432530 --- /dev/null +++ b/lib/Exceptions/HostedSignalingServerAPIException.php @@ -0,0 +1,14 @@ +<?php + + +namespace OCA\Talk\Exceptions; + +/** + * Exception that is thrown when an API error happened. The message itself is already translated and can be handed out to the user. + * + * This exception should be used for the code flow and not for logging + * + * This exception indicates a problem with the server, API, conenction or responses. + */ +class HostedSignalingServerAPIException extends \Exception { +} diff --git a/lib/Exceptions/HostedSignalingServerInputException.php b/lib/Exceptions/HostedSignalingServerInputException.php new file mode 100644 index 000000000..23d284ef0 --- /dev/null +++ b/lib/Exceptions/HostedSignalingServerInputException.php @@ -0,0 +1,14 @@ +<?php + + +namespace OCA\Talk\Exceptions; + +/** + * Exception that is thrown when an API error happened. The message itself is already translated and can be handed out to the user. + * + * This exception should be used for the code flow and not for logging. + * + * This exception indicates user solvable issues - like an already existing account or invalid input. + */ +class HostedSignalingServerInputException extends \Exception { +} diff --git a/lib/Service/HostedSignalingServerService.php b/lib/Service/HostedSignalingServerService.php new file mode 100644 index 000000000..223236092 --- /dev/null +++ b/lib/Service/HostedSignalingServerService.php @@ -0,0 +1,413 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2020 Morris Jobke <hey@morrisjobke.de> + * + * @author Morris Jobke <hey@morrisjobke.de> + * + * @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\Service; + +use GuzzleHttp\Exception\ClientException; +use OC\Security\SecureRandom; +use OCA\Talk\DataObjects\AccountId; +use OCA\Talk\DataObjects\RegisterAccountData; +use OCA\Talk\Exceptions\HostedSignalingServerAPIException; +use OCA\Talk\Exceptions\HostedSignalingServerInputException; +use OCP\AppFramework\Http; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; + +class HostedSignalingServerService { + + /** @var IConfig */ + private $config; + /** @var mixed */ + private $apiServerUrl; + /** @var IClientService */ + private $clientService; + /** @var ILogger */ + private $logger; + /** @var IL10N */ + private $l10n; + /** @var SecureRandom */ + private $secureRandom; + + public function __construct(IConfig $config, + IClientService $clientService, + ILogger $logger, + IL10N $l10n, + SecureRandom $secureRandom) { + $this->config = $config; + $this->clientService = $clientService; + $this->logger = $logger; + $this->l10n = $l10n; + $this->secureRandom = $secureRandom; + + $this->apiServerUrl = $this->config->getSystemValue('talk_hardcoded_hpb_service', 'https://api.spreed.cloud'); + } + + /** + * @throws HostedSignalingServerAPIException + * @throws HostedSignalingServerInputException + */ + public function registerAccount(RegisterAccountData $registerAccountData): AccountId { + try { + $nonce = $this->secureRandom->generate(32); + $this->config->setAppValue('spreed', 'hosted-signaling-server-nonce', $nonce); + + $client = $this->clientService->newClient(); + $response = $client->post($this->apiServerUrl . '/v1/account', [ + 'json' => [ + 'url' => $registerAccountData->getUrl(), + 'name' => $registerAccountData->getName(), + 'email' => $registerAccountData->getEmail(), + 'language' => $registerAccountData->getLanguage(), + 'country' => $registerAccountData->getCountry(), + ], + 'headers' => [ + 'X-Account-Service-Nonce' => $nonce, + ], + 'timeout' => 10, + ]); + + // this is needed here because the delete happens in a concurrent request + // and thus the cached value in the config object would trigger an UPDATE + // instead of an INSERT if there is another request to the API server + $this->config->deleteAppValue('spreed', 'hosted-signaling-server-nonce'); + } catch (ClientException $e) { + $response = $e->getResponse(); + + if ($response === null) { + $this->logger->logException($e, [ + 'app' => 'spreed', + 'message' => 'Failed to request hosted signaling server trial', + ]); + $message = $this->l10n->t('Failed to request trial because the trial server is unreachable. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + } + + $status = $response->getStatusCode(); + switch ($status) { + case Http::STATUS_UNAUTHORIZED: + $body = $response->getBody()->getContents(); + $this->logger->error('Requesting hosted signaling server trial failed: unauthorized - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('There is a problem with the authentication of this instance. Maybe it is not reachable from the outside to verify it\'s URL.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_BAD_REQUEST: + $body = $response->getBody()->getContents(); + if ($body) { + $parsedBody = json_decode($body, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Requesting hosted signaling server trial failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + if ($parsedBody['reason']) { + $message = ''; + switch ($parsedBody['reason']) { + case 'invalid_content_type': + $log = 'The content type is invalid.'; + break; + case 'invalid_json': + $log = 'The JSON is invalid.'; + break; + case 'missing_url': + $log = 'The URL is missing.'; + break; + case 'missing_name': + $log = 'The name is missing.'; + break; + case 'missing_email': + $log = 'The email address is missing'; + break; + case 'missing_language': + $log = 'The language code is missing.'; + break; + case 'missing_country': + $log = 'The country code is missing.'; + break; + case 'invalid_url': + $message = $this->l10n->t('The URL is invalid.'); + $log = 'The entered URL is invalid.'; + break; + case 'https_required': + $message = $this->l10n->t('An HTTPS URL is required.'); + $log = 'An HTTPS URL is required.'; + break; + case 'invalid_email': + $message = $this->l10n->t('The email address is invalid.'); + $log = 'The email address is invalid.'; + break; + case 'invalid_language': + $message = $this->l10n->t('The language is invalid.'); + $log = 'The language is invalid.'; + break; + case 'invalid_country': + $message = $this->l10n->t('The country is invalid.'); + $log = 'The country is invalid.'; + break; + } + // user error + if ($message !== '') { + $this->logger->warning('Requesting hosted signaling server trial failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); + throw new HostedSignalingServerAPIException($message); + } + $this->logger->error('Requesting hosted signaling server trial failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); + + $message = $this->l10n->t('There is a problem with the request of the trial. Please check your logs for further information.'); + throw new HostedSignalingServerAPIException($message); + } + } + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_TOO_MANY_REQUESTS: + $body = $response->getBody()->getContents(); + $this->logger->error('Requesting hosted signaling server trial failed: too many requests - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Too many requests are send from your servers address. Please try again later.'); + throw new HostedSignalingServerInputException($message); + case Http::STATUS_CONFLICT: + $body = $response->getBody()->getContents(); + $this->logger->error('Requesting hosted signaling server trial failed: already registered - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('There is already a trial registered for this Nextcloud instance.'); + throw new HostedSignalingServerInputException($message); + case Http::STATUS_INTERNAL_SERVER_ERROR: + $body = $response->getBody()->getContents(); + $this->logger->error('Requesting hosted signaling server trial failed: internal server error - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + default: + $body = $response->getBody()->getContents(); + $this->logger->error('Requesting hosted signaling server trial failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Failed to request trial because the trial server behaved wrongly. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + } + } catch (\Exception $e) { + $this->logger->logException($e, [ + 'app' => 'spreed', + 'message' => 'Failed to request hosted signaling server trial', + ]); + + $message = $this->l10n->t('Failed to request trial because the trial server is unreachable. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + } + + $status = $response->getStatusCode(); + + if ($status !== Http::STATUS_CREATED) { + $body = $response->getBody(); + $this->logger->error('Requesting hosted signaling server trial failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + $body = $response->getBody(); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Requesting hosted signaling server trial failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + if (!isset($data['account_id'])) { + $this->logger->error('Requesting hosted signaling server trial failed: no account ID transfered - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + $accountId = (string)$data['account_id']; + $this->config->setAppValue('spreed', 'hosted-signaling-server-account-id', $accountId); + + return new AccountId($accountId); + } + + /** + * @throws HostedSignalingServerAPIException + */ + public function fetchAccountInfo(AccountId $accountId): array { + try { + $nonce = $this->secureRandom->generate(32); + $this->config->setAppValue('spreed', 'hosted-signaling-server-nonce', $nonce); + + $client = $this->clientService->newClient(); + $response = $client->get($this->apiServerUrl . '/v1/account/' . $accountId->get(), [ + 'headers' => [ + 'X-Account-Service-Nonce' => $nonce, + ], + 'timeout' => 10, + ]); + + // this is needed here because the delete happens in a concurrent request + // and thus the cached value in the config object would trigger an UPDATE + // instead of an INSERT if there is another request to the API server + $this->config->deleteAppValue('spreed', 'hosted-signaling-server-nonce'); + } catch (ClientException $e) { + $response = $e->getResponse(); + + if ($response === null) { + $this->logger->logException($e, [ + 'app' => 'spreed', + 'message' => 'Trial requested but failed to get account information', + ]); + + $message = $this->l10n->t('Trial requested but failed to get account information. Please check back later.'); + throw new HostedSignalingServerAPIException($message); + } + + $status = $response->getStatusCode(); + + switch ($status) { + case Http::STATUS_UNAUTHORIZED: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: unauthorized - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('There is a problem with the authentication of this request. Maybe it is not reachable from the outside to verify it\'s URL.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_BAD_REQUEST: + $body = $response->getBody()->getContents(); + if ($body) { + $parsedBody = json_decode($body, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Getting the account information failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + if ($parsedBody['reason']) { + switch ($parsedBody['reason']) { + case 'missing_account_id': + $log = 'The account ID is missing.'; + break; + default: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Failed to fetch account information because the trial server behaved wrongly. Please check back later.'); + throw new HostedSignalingServerAPIException($message); + } + $this->logger->error('Getting the account information failed: bad request - reason: ' . $parsedBody['reason'] . ' ' . $log); + + $message = $this->l10n->t('There is a problem with fetching the account information. Please check your logs for further information.'); + throw new HostedSignalingServerAPIException($message); + } + } + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_TOO_MANY_REQUESTS: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: too many requests - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Too many requests are send from your servers address. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_NOT_FOUND: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: account not found - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('There is no such account registered.'); + throw new HostedSignalingServerAPIException($message); + case Http::STATUS_INTERNAL_SERVER_ERROR: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: internal server error - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened. Please try again later.'); + throw new HostedSignalingServerAPIException($message); + default: + $body = $response->getBody()->getContents(); + $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Failed to fetch account information because the trial server behaved wrongly. Please check back later.'); + throw new HostedSignalingServerAPIException($message); + } + } catch (\Exception $e) { + $this->logger->logException($e, [ + 'app' => 'spreed', + 'message' => 'Failed to request hosted signaling server trial', + ]); + + $message = $this->l10n->t('Failed to fetch account information because the trial server is unreachable. Please check back later.'); + throw new HostedSignalingServerAPIException($message); + } + + $status = $response->getStatusCode(); + + if ($status !== Http::STATUS_OK) { + $body = $response->getBody(); + $this->logger->error('Getting the account information failed: something else happened - HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + $body = $response->getBody(); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->error('Getting the account information failed: cannot parse JSON response - JSON error: '. json_last_error() . ' ' . json_last_error_msg() . ' HTTP status: ' . $status . ' Response body: ' . $body, ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + if (!isset($data['status']) + || !isset($data['created']) + || ($data['status'] === 'active' && ( + !isset($data['signaling']) + || !isset($data['signaling']['url']) + || !isset($data['signaling']['secret']) + ) + ) + || !isset($data['owner']) + || !isset($data['owner']['url']) + || !isset($data['owner']['name']) + || !isset($data['owner']['email']) + || !isset($data['owner']['language']) + || !isset($data['owner']['country']) + || ($data['status'] === 'active' && ( + !isset($data['limits']) + || !isset($data['limits']['users']) + ) + ) + || (in_array($data['status'], ['error', 'blocked']) && !isset($data['reason'])) + || !in_array($data['status'], ['error', 'blocked', 'pending', 'active', 'expired']) + ) { + $this->logger->error('Getting the account information failed: response is missing mandatory field - data: ' . json_encode($data), ['app' => 'spreed']); + + $message = $this->l10n->t('Something unexpected happened.'); + throw new HostedSignalingServerAPIException($message); + } + + return $data; + } +} |