SwiftFactory.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Files\ObjectStore;
  8. use GuzzleHttp\Client;
  9. use GuzzleHttp\Exception\ClientException;
  10. use GuzzleHttp\Exception\ConnectException;
  11. use GuzzleHttp\Exception\RequestException;
  12. use GuzzleHttp\HandlerStack;
  13. use OCP\Files\StorageAuthException;
  14. use OCP\Files\StorageNotAvailableException;
  15. use OCP\ICache;
  16. use OpenStack\Common\Auth\Token;
  17. use OpenStack\Common\Error\BadResponseError;
  18. use OpenStack\Common\Transport\Utils as TransportUtils;
  19. use OpenStack\Identity\v2\Models\Catalog;
  20. use OpenStack\Identity\v2\Service as IdentityV2Service;
  21. use OpenStack\Identity\v3\Service as IdentityV3Service;
  22. use OpenStack\ObjectStore\v1\Models\Container;
  23. use OpenStack\OpenStack;
  24. use Psr\Http\Message\RequestInterface;
  25. use Psr\Log\LoggerInterface;
  26. class SwiftFactory {
  27. private $cache;
  28. private $params;
  29. /** @var Container|null */
  30. private $container = null;
  31. private LoggerInterface $logger;
  32. public const DEFAULT_OPTIONS = [
  33. 'autocreate' => false,
  34. 'urlType' => 'publicURL',
  35. 'catalogName' => 'swift',
  36. 'catalogType' => 'object-store'
  37. ];
  38. public function __construct(ICache $cache, array $params, LoggerInterface $logger) {
  39. $this->cache = $cache;
  40. $this->params = $params;
  41. $this->logger = $logger;
  42. }
  43. /**
  44. * Gets currently cached token id
  45. *
  46. * @return string
  47. * @throws StorageAuthException
  48. */
  49. public function getCachedTokenId() {
  50. if (!isset($this->params['cachedToken'])) {
  51. throw new StorageAuthException('Unauthenticated ObjectStore connection');
  52. }
  53. // Is it V2 token?
  54. if (isset($this->params['cachedToken']['token'])) {
  55. return $this->params['cachedToken']['token']['id'];
  56. }
  57. return $this->params['cachedToken']['id'];
  58. }
  59. private function getCachedToken(string $cacheKey) {
  60. $cachedTokenString = $this->cache->get($cacheKey . '/token');
  61. if ($cachedTokenString) {
  62. return json_decode($cachedTokenString, true);
  63. } else {
  64. return null;
  65. }
  66. }
  67. private function cacheToken(Token $token, string $serviceUrl, string $cacheKey) {
  68. if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
  69. // for v3 the catalog is cached as part of the token, so no need to cache $serviceUrl separately
  70. $value = $token->export();
  71. } else {
  72. /** @var \OpenStack\Identity\v2\Models\Token $token */
  73. $value = [
  74. 'serviceUrl' => $serviceUrl,
  75. 'token' => [
  76. 'issued_at' => $token->issuedAt->format('c'),
  77. 'expires' => $token->expires->format('c'),
  78. 'id' => $token->id,
  79. 'tenant' => $token->tenant
  80. ]
  81. ];
  82. }
  83. $this->params['cachedToken'] = $value;
  84. $this->cache->set($cacheKey . '/token', json_encode($value));
  85. }
  86. /**
  87. * @return OpenStack
  88. * @throws StorageAuthException
  89. */
  90. private function getClient() {
  91. if (isset($this->params['bucket'])) {
  92. $this->params['container'] = $this->params['bucket'];
  93. }
  94. if (!isset($this->params['container'])) {
  95. $this->params['container'] = 'nextcloud';
  96. }
  97. if (isset($this->params['user']) && is_array($this->params['user'])) {
  98. $userName = $this->params['user']['name'];
  99. } else {
  100. if (!isset($this->params['username']) && isset($this->params['user'])) {
  101. $this->params['username'] = $this->params['user'];
  102. }
  103. $userName = $this->params['username'];
  104. }
  105. if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
  106. $this->params['tenantName'] = $this->params['tenant'];
  107. }
  108. if (isset($this->params['domain'])) {
  109. $this->params['scope']['project']['name'] = $this->params['tenant'];
  110. $this->params['scope']['project']['domain']['name'] = $this->params['domain'];
  111. }
  112. $this->params = array_merge(self::DEFAULT_OPTIONS, $this->params);
  113. $cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
  114. $token = $this->getCachedToken($cacheKey);
  115. $this->params['cachedToken'] = $token;
  116. $httpClient = new Client([
  117. 'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
  118. 'handler' => HandlerStack::create()
  119. ]);
  120. if (isset($this->params['user']) && is_array($this->params['user']) && isset($this->params['user']['name'])) {
  121. if (!isset($this->params['scope'])) {
  122. throw new StorageAuthException('Scope has to be defined for V3 requests');
  123. }
  124. return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
  125. } else {
  126. return $this->auth(SwiftV2CachingAuthService::factory($httpClient), $cacheKey);
  127. }
  128. }
  129. /**
  130. * @param IdentityV2Service|IdentityV3Service $authService
  131. * @param string $cacheKey
  132. * @return OpenStack
  133. * @throws StorageAuthException
  134. */
  135. private function auth($authService, string $cacheKey) {
  136. $this->params['identityService'] = $authService;
  137. $this->params['authUrl'] = $this->params['url'];
  138. $cachedToken = $this->params['cachedToken'];
  139. $hasValidCachedToken = false;
  140. if (\is_array($cachedToken)) {
  141. if ($authService instanceof IdentityV3Service) {
  142. $token = $authService->generateTokenFromCache($cachedToken);
  143. if (\is_null($token->catalog)) {
  144. $this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
  145. } elseif ($token->hasExpired()) {
  146. $this->logger->debug('Cached token for swift expired');
  147. } else {
  148. $hasValidCachedToken = true;
  149. }
  150. } else {
  151. try {
  152. /** @var \OpenStack\Identity\v2\Models\Token $token */
  153. $token = $authService->model(\OpenStack\Identity\v2\Models\Token::class, $cachedToken['token']);
  154. $now = new \DateTimeImmutable("now");
  155. if ($token->expires > $now) {
  156. $hasValidCachedToken = true;
  157. $this->params['v2cachedToken'] = $token;
  158. $this->params['v2serviceUrl'] = $cachedToken['serviceUrl'];
  159. } else {
  160. $this->logger->debug('Cached token for swift expired');
  161. }
  162. } catch (\Exception $e) {
  163. $this->logger->error($e->getMessage(), ['exception' => $e]);
  164. }
  165. }
  166. }
  167. if (!$hasValidCachedToken) {
  168. unset($this->params['cachedToken']);
  169. try {
  170. [$token, $serviceUrl] = $authService->authenticate($this->params);
  171. $this->cacheToken($token, $serviceUrl, $cacheKey);
  172. } catch (ConnectException $e) {
  173. throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
  174. } catch (ClientException $e) {
  175. $statusCode = $e->getResponse()->getStatusCode();
  176. if ($statusCode === 404) {
  177. throw new StorageAuthException('Keystone not found while connecting to object storage, verify the keystone url', $e);
  178. } elseif ($statusCode === 412) {
  179. throw new StorageAuthException('Precondition failed while connecting to object storage, verify the keystone url', $e);
  180. } elseif ($statusCode === 401) {
  181. throw new StorageAuthException('Authentication failed while connecting to object storage, verify the username, password and possibly tenant', $e);
  182. } else {
  183. throw new StorageAuthException('Unknown error while connecting to object storage', $e);
  184. }
  185. } catch (RequestException $e) {
  186. throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
  187. }
  188. }
  189. $client = new OpenStack($this->params);
  190. return $client;
  191. }
  192. /**
  193. * @return \OpenStack\ObjectStore\v1\Models\Container
  194. * @throws StorageAuthException
  195. * @throws StorageNotAvailableException
  196. */
  197. public function getContainer() {
  198. if (is_null($this->container)) {
  199. $this->container = $this->createContainer();
  200. }
  201. return $this->container;
  202. }
  203. /**
  204. * @return \OpenStack\ObjectStore\v1\Models\Container
  205. * @throws StorageAuthException
  206. * @throws StorageNotAvailableException
  207. */
  208. private function createContainer() {
  209. $client = $this->getClient();
  210. $objectStoreService = $client->objectStoreV1();
  211. $autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
  212. try {
  213. $container = $objectStoreService->getContainer($this->params['container']);
  214. if ($autoCreate) {
  215. $container->getMetadata();
  216. }
  217. return $container;
  218. } catch (BadResponseError $ex) {
  219. // if the container does not exist and autocreate is true try to create the container on the fly
  220. if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
  221. return $objectStoreService->createContainer([
  222. 'name' => $this->params['container']
  223. ]);
  224. } else {
  225. throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
  226. }
  227. } catch (ConnectException $e) {
  228. /** @var RequestInterface $request */
  229. $request = $e->getRequest();
  230. $host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
  231. $this->logger->error("Can't connect to object storage server at $host", ['exception' => $e]);
  232. throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
  233. }
  234. }
  235. }