123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- <?php
- declare(strict_types=1);
- /**
- * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- namespace OC\Files\ObjectStore;
- use GuzzleHttp\Client;
- use GuzzleHttp\Exception\ClientException;
- use GuzzleHttp\Exception\ConnectException;
- use GuzzleHttp\Exception\RequestException;
- use GuzzleHttp\HandlerStack;
- use OCP\Files\StorageAuthException;
- use OCP\Files\StorageNotAvailableException;
- use OCP\ICache;
- use OpenStack\Common\Auth\Token;
- use OpenStack\Common\Error\BadResponseError;
- use OpenStack\Common\Transport\Utils as TransportUtils;
- use OpenStack\Identity\v2\Models\Catalog;
- use OpenStack\Identity\v2\Service as IdentityV2Service;
- use OpenStack\Identity\v3\Service as IdentityV3Service;
- use OpenStack\ObjectStore\v1\Models\Container;
- use OpenStack\OpenStack;
- use Psr\Http\Message\RequestInterface;
- use Psr\Log\LoggerInterface;
- class SwiftFactory {
- private $cache;
- private $params;
- /** @var Container|null */
- private $container = null;
- private LoggerInterface $logger;
- public const DEFAULT_OPTIONS = [
- 'autocreate' => false,
- 'urlType' => 'publicURL',
- 'catalogName' => 'swift',
- 'catalogType' => 'object-store'
- ];
- public function __construct(ICache $cache, array $params, LoggerInterface $logger) {
- $this->cache = $cache;
- $this->params = $params;
- $this->logger = $logger;
- }
- /**
- * Gets currently cached token id
- *
- * @return string
- * @throws StorageAuthException
- */
- public function getCachedTokenId() {
- if (!isset($this->params['cachedToken'])) {
- throw new StorageAuthException('Unauthenticated ObjectStore connection');
- }
- // Is it V2 token?
- if (isset($this->params['cachedToken']['token'])) {
- return $this->params['cachedToken']['token']['id'];
- }
- return $this->params['cachedToken']['id'];
- }
- private function getCachedToken(string $cacheKey) {
- $cachedTokenString = $this->cache->get($cacheKey . '/token');
- if ($cachedTokenString) {
- return json_decode($cachedTokenString, true);
- } else {
- return null;
- }
- }
- private function cacheToken(Token $token, string $serviceUrl, string $cacheKey) {
- if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
- // for v3 the catalog is cached as part of the token, so no need to cache $serviceUrl separately
- $value = $token->export();
- } else {
- /** @var \OpenStack\Identity\v2\Models\Token $token */
- $value = [
- 'serviceUrl' => $serviceUrl,
- 'token' => [
- 'issued_at' => $token->issuedAt->format('c'),
- 'expires' => $token->expires->format('c'),
- 'id' => $token->id,
- 'tenant' => $token->tenant
- ]
- ];
- }
- $this->params['cachedToken'] = $value;
- $this->cache->set($cacheKey . '/token', json_encode($value));
- }
- /**
- * @return OpenStack
- * @throws StorageAuthException
- */
- private function getClient() {
- if (isset($this->params['bucket'])) {
- $this->params['container'] = $this->params['bucket'];
- }
- if (!isset($this->params['container'])) {
- $this->params['container'] = 'nextcloud';
- }
- if (isset($this->params['user']) && is_array($this->params['user'])) {
- $userName = $this->params['user']['name'];
- } else {
- if (!isset($this->params['username']) && isset($this->params['user'])) {
- $this->params['username'] = $this->params['user'];
- }
- $userName = $this->params['username'];
- }
- if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
- $this->params['tenantName'] = $this->params['tenant'];
- }
- if (isset($this->params['domain'])) {
- $this->params['scope']['project']['name'] = $this->params['tenant'];
- $this->params['scope']['project']['domain']['name'] = $this->params['domain'];
- }
- $this->params = array_merge(self::DEFAULT_OPTIONS, $this->params);
- $cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
- $token = $this->getCachedToken($cacheKey);
- $this->params['cachedToken'] = $token;
- $httpClient = new Client([
- 'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
- 'handler' => HandlerStack::create()
- ]);
- if (isset($this->params['user']) && is_array($this->params['user']) && isset($this->params['user']['name'])) {
- if (!isset($this->params['scope'])) {
- throw new StorageAuthException('Scope has to be defined for V3 requests');
- }
- return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
- } else {
- return $this->auth(SwiftV2CachingAuthService::factory($httpClient), $cacheKey);
- }
- }
- /**
- * @param IdentityV2Service|IdentityV3Service $authService
- * @param string $cacheKey
- * @return OpenStack
- * @throws StorageAuthException
- */
- private function auth($authService, string $cacheKey) {
- $this->params['identityService'] = $authService;
- $this->params['authUrl'] = $this->params['url'];
- $cachedToken = $this->params['cachedToken'];
- $hasValidCachedToken = false;
- if (\is_array($cachedToken)) {
- if ($authService instanceof IdentityV3Service) {
- $token = $authService->generateTokenFromCache($cachedToken);
- if (\is_null($token->catalog)) {
- $this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
- } elseif ($token->hasExpired()) {
- $this->logger->debug('Cached token for swift expired');
- } else {
- $hasValidCachedToken = true;
- }
- } else {
- try {
- /** @var \OpenStack\Identity\v2\Models\Token $token */
- $token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']);
- $now = new \DateTimeImmutable('now');
- if ($token->expires > $now) {
- $hasValidCachedToken = true;
- $this->params['v2cachedToken'] = $token;
- $this->params['v2serviceUrl'] = $cachedToken['serviceUrl'];
- } else {
- $this->logger->debug('Cached token for swift expired');
- }
- } catch (\Exception $e) {
- $this->logger->error($e->getMessage(), ['exception' => $e]);
- }
- }
- }
- if (!$hasValidCachedToken) {
- unset($this->params['cachedToken']);
- try {
- [$token, $serviceUrl] = $authService->authenticate($this->params);
- $this->cacheToken($token, $serviceUrl, $cacheKey);
- } catch (ConnectException $e) {
- throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
- } catch (ClientException $e) {
- $statusCode = $e->getResponse()->getStatusCode();
- if ($statusCode === 404) {
- throw new StorageAuthException('Keystone not found while connecting to object storage, verify the keystone url', $e);
- } elseif ($statusCode === 412) {
- throw new StorageAuthException('Precondition failed while connecting to object storage, verify the keystone url', $e);
- } elseif ($statusCode === 401) {
- throw new StorageAuthException('Authentication failed while connecting to object storage, verify the username, password and possibly tenant', $e);
- } else {
- throw new StorageAuthException('Unknown error while connecting to object storage', $e);
- }
- } catch (RequestException $e) {
- throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
- }
- }
- $client = new OpenStack($this->params);
- return $client;
- }
- /**
- * @return \OpenStack\ObjectStore\v1\Models\Container
- * @throws StorageAuthException
- * @throws StorageNotAvailableException
- */
- public function getContainer() {
- if (is_null($this->container)) {
- $this->container = $this->createContainer();
- }
- return $this->container;
- }
- /**
- * @return \OpenStack\ObjectStore\v1\Models\Container
- * @throws StorageAuthException
- * @throws StorageNotAvailableException
- */
- private function createContainer() {
- $client = $this->getClient();
- $objectStoreService = $client->objectStoreV1();
- $autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
- try {
- $container = $objectStoreService->getContainer($this->params['container']);
- if ($autoCreate) {
- $container->getMetadata();
- }
- return $container;
- } catch (BadResponseError $ex) {
- // if the container does not exist and autocreate is true try to create the container on the fly
- if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
- return $objectStoreService->createContainer([
- 'name' => $this->params['container']
- ]);
- } else {
- throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
- }
- } catch (ConnectException $e) {
- /** @var RequestInterface $request */
- $request = $e->getRequest();
- $host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
- $this->logger->error("Can't connect to object storage server at $host", ['exception' => $e]);
- throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
- }
- }
- }
|