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); } } }