Passed
Push — master ( 447277...a1c1b3 )
by Roeland
23:12 queued 12:05
created

SwiftFactory::getCachedTokenId()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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