SwiftFactory.php 8.4 KB

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