SwiftFactory.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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\Service as IdentityV2Service;
  35. use OpenStack\Identity\v3\Service as IdentityV3Service;
  36. use OpenStack\OpenStack;
  37. use OpenStack\Common\Transport\Utils as TransportUtils;
  38. use Psr\Http\Message\RequestInterface;
  39. use OpenStack\ObjectStore\v1\Models\Container;
  40. class SwiftFactory {
  41. private $cache;
  42. private $params;
  43. /** @var Container|null */
  44. private $container = null;
  45. private $logger;
  46. public function __construct(ICache $cache, array $params, ILogger $logger) {
  47. $this->cache = $cache;
  48. $this->params = $params;
  49. $this->logger = $logger;
  50. }
  51. private function getCachedToken(string $cacheKey) {
  52. $cachedTokenString = $this->cache->get($cacheKey . '/token');
  53. if ($cachedTokenString) {
  54. return json_decode($cachedTokenString, true);
  55. } else {
  56. return null;
  57. }
  58. }
  59. private function cacheToken(Token $token, string $cacheKey) {
  60. if ($token instanceof \OpenStack\Identity\v3\Models\Token) {
  61. $value = json_encode($token->export());
  62. } else {
  63. $value = json_encode($token);
  64. }
  65. $this->cache->set($cacheKey . '/token', $value);
  66. }
  67. /**
  68. * @return OpenStack
  69. * @throws StorageAuthException
  70. */
  71. private function getClient() {
  72. if (isset($this->params['bucket'])) {
  73. $this->params['container'] = $this->params['bucket'];
  74. }
  75. if (!isset($this->params['container'])) {
  76. $this->params['container'] = 'nextcloud';
  77. }
  78. if (!isset($this->params['autocreate'])) {
  79. // should only be true for tests
  80. $this->params['autocreate'] = false;
  81. }
  82. if (isset($this->params['user']) && is_array($this->params['user'])) {
  83. $userName = $this->params['user']['name'];
  84. } else {
  85. if (!isset($this->params['username']) && isset($this->params['user'])) {
  86. $this->params['username'] = $this->params['user'];
  87. }
  88. $userName = $this->params['username'];
  89. }
  90. if (!isset($this->params['tenantName']) && isset($this->params['tenant'])) {
  91. $this->params['tenantName'] = $this->params['tenant'];
  92. }
  93. $cacheKey = $userName . '@' . $this->params['url'] . '/' . $this->params['container'];
  94. $token = $this->getCachedToken($cacheKey);
  95. $this->params['cachedToken'] = $token;
  96. $httpClient = new Client([
  97. 'base_uri' => TransportUtils::normalizeUrl($this->params['url']),
  98. 'handler' => HandlerStack::create()
  99. ]);
  100. if (isset($this->params['user']) && isset($this->params['user']['name'])) {
  101. if (!isset($this->params['scope'])) {
  102. throw new StorageAuthException('Scope has to be defined for V3 requests');
  103. }
  104. return $this->auth(IdentityV3Service::factory($httpClient), $cacheKey);
  105. } else {
  106. return $this->auth(IdentityV2Service::factory($httpClient), $cacheKey);
  107. }
  108. }
  109. /**
  110. * @param IdentityV2Service|IdentityV3Service $authService
  111. * @param string $cacheKey
  112. * @return OpenStack
  113. * @throws StorageAuthException
  114. */
  115. private function auth($authService, string $cacheKey) {
  116. $this->params['identityService'] = $authService;
  117. $this->params['authUrl'] = $this->params['url'];
  118. $client = new OpenStack($this->params);
  119. $cachedToken = $this->params['cachedToken'];
  120. $hasValidCachedToken = false;
  121. if (\is_array($cachedToken) && ($authService instanceof IdentityV3Service)) {
  122. $token = $authService->generateTokenFromCache($cachedToken);
  123. if (\is_null($token->catalog)) {
  124. $this->logger->warning('Invalid cached token for swift, no catalog set: ' . json_encode($cachedToken));
  125. } else if ($token->hasExpired()) {
  126. $this->logger->debug('Cached token for swift expired');
  127. } else {
  128. $hasValidCachedToken = true;
  129. }
  130. }
  131. if (!$hasValidCachedToken) {
  132. try {
  133. $token = $authService->generateToken($this->params);
  134. $this->cacheToken($token, $cacheKey);
  135. } catch (ConnectException $e) {
  136. throw new StorageAuthException('Failed to connect to keystone, verify the keystone url', $e);
  137. } catch (ClientException $e) {
  138. $statusCode = $e->getResponse()->getStatusCode();
  139. if ($statusCode === 404) {
  140. throw new StorageAuthException('Keystone not found, verify the keystone url', $e);
  141. } else if ($statusCode === 412) {
  142. throw new StorageAuthException('Precondition failed, verify the keystone url', $e);
  143. } else if ($statusCode === 401) {
  144. throw new StorageAuthException('Authentication failed, verify the username, password and possibly tenant', $e);
  145. } else {
  146. throw new StorageAuthException('Unknown error', $e);
  147. }
  148. } catch (RequestException $e) {
  149. throw new StorageAuthException('Connection reset while connecting to keystone, verify the keystone url', $e);
  150. }
  151. }
  152. return $client;
  153. }
  154. /**
  155. * @return \OpenStack\ObjectStore\v1\Models\Container
  156. * @throws StorageAuthException
  157. * @throws StorageNotAvailableException
  158. */
  159. public function getContainer() {
  160. if (is_null($this->container)) {
  161. $this->container = $this->createContainer();
  162. }
  163. return $this->container;
  164. }
  165. /**
  166. * @return \OpenStack\ObjectStore\v1\Models\Container
  167. * @throws StorageAuthException
  168. * @throws StorageNotAvailableException
  169. */
  170. private function createContainer() {
  171. $client = $this->getClient();
  172. $objectStoreService = $client->objectStoreV1();
  173. $autoCreate = isset($this->params['autocreate']) && $this->params['autocreate'] === true;
  174. try {
  175. $container = $objectStoreService->getContainer($this->params['container']);
  176. if ($autoCreate) {
  177. $container->getMetadata();
  178. }
  179. return $container;
  180. } catch (BadResponseError $ex) {
  181. // if the container does not exist and autocreate is true try to create the container on the fly
  182. if ($ex->getResponse()->getStatusCode() === 404 && $autoCreate) {
  183. return $objectStoreService->createContainer([
  184. 'name' => $this->params['container']
  185. ]);
  186. } else {
  187. throw new StorageNotAvailableException('Invalid response while trying to get container info', StorageNotAvailableException::STATUS_ERROR, $ex);
  188. }
  189. } catch (ConnectException $e) {
  190. /** @var RequestInterface $request */
  191. $request = $e->getRequest();
  192. $host = $request->getUri()->getHost() . ':' . $request->getUri()->getPort();
  193. \OC::$server->getLogger()->error("Can't connect to object storage server at $host");
  194. throw new StorageNotAvailableException("Can't connect to object storage server at $host", StorageNotAvailableException::STATUS_ERROR, $e);
  195. }
  196. }
  197. }