Completed
Pull Request — master (#551)
by Maxence
03:07
created

RemoteStreamService   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 22
dl 0
loc 406
rs 9.68
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getAppSignatory() 0 42 2
A resetAppSignatory() 0 8 2
A resultRequestRemoteInstance() 0 19 3
A requestRemoteInstance() 0 28 3
A getRemoteInstanceEntry() 0 10 2
A getCachedRemoteInstance() 0 8 2
A retrieveRemoteInstance() 0 8 1
A retrieveSignatory() 0 19 3
A addRemoteInstance() 0 22 4
A confirmAuth() 0 16 3
A confirmValidRemote() 0 15 4
A update() 0 15 4
1
<?php
2
3
declare(strict_types=1);
4
5
6
/**
7
 * Circles - Bring cloud-users closer together.
8
 *
9
 * This file is licensed under the Affero General Public License version 3 or
10
 * later. See the COPYING file.
11
 *
12
 * @author Maxence Lange <[email protected]>
13
 * @copyright 2021
14
 * @license GNU AGPL version 3 or any later version
15
 *
16
 * This program is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License as
18
 * published by the Free Software Foundation, either version 3 of the
19
 * License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
28
 *
29
 */
30
31
namespace OCA\Circles\Service;
32
33
34
use daita\MySmallPhpTools\ActivityPub\Nextcloud\nc21\NC21Signature;
35
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
36
use daita\MySmallPhpTools\Exceptions\SignatoryException;
37
use daita\MySmallPhpTools\Exceptions\SignatureException;
38
use daita\MySmallPhpTools\Exceptions\WellKnownLinkNotFoundException;
39
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Request;
40
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Signatory;
41
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21SignedRequest;
42
use daita\MySmallPhpTools\Model\Request;
43
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21Deserialize;
44
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21LocalSignatory;
45
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21WellKnown;
46
use daita\MySmallPhpTools\Traits\TStringTools;
47
use JsonSerializable;
48
use OCA\Circles\AppInfo\Application;
49
use OCA\Circles\Db\RemoteRequest;
50
use OCA\Circles\Exceptions\RemoteAlreadyExistsException;
51
use OCA\Circles\Exceptions\RemoteInstanceException;
52
use OCA\Circles\Exceptions\RemoteNotFoundException;
53
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
54
use OCA\Circles\Exceptions\RemoteUidException;
55
use OCA\Circles\Exceptions\UnknownRemoteException;
56
use OCA\Circles\Model\Federated\RemoteInstance;
57
use OCP\AppFramework\Http;
58
use OCP\IURLGenerator;
59
60
61
/**
62
 * Class RemoteStreamService
63
 *
64
 * @package OCA\Circles\Service
65
 */
66
class RemoteStreamService extends NC21Signature {
67
68
69
	use TNC21Deserialize;
70
	use TNC21LocalSignatory;
71
	use TStringTools;
72
	use TNC21WellKnown;
73
74
75
	const UPDATE_DATA = 'data';
76
	const UPDATE_INSTANCE = 'instance';
77
	const UPDATE_HREF = 'href';
78
79
80
	/** @var IURLGenerator */
81
	private $urlGenerator;
82
83
	/** @var RemoteRequest */
84
	private $remoteRequest;
85
86
	/** @var ConfigService */
87
	private $configService;
88
89
90
	/**
91
	 * RemoteStreamService constructor.
92
	 *
93
	 * @param IURLGenerator $urlGenerator
94
	 * @param RemoteRequest $remoteRequest
95
	 * @param ConfigService $configService
96
	 */
97
	public function __construct(
98
		IURLGenerator $urlGenerator, RemoteRequest $remoteRequest, ConfigService $configService
99
	) {
100
		$this->setup('app', 'circles');
101
102
		$this->urlGenerator = $urlGenerator;
103
		$this->remoteRequest = $remoteRequest;
104
		$this->configService = $configService;
105
	}
106
107
108
	/**
109
	 * Returns the Signatory model for the Circles app.
110
	 * Can be signed with a confirmKey.
111
	 *
112
	 * @param bool $generate
113
	 * @param string $confirmKey
114
	 *
115
	 * @return RemoteInstance
116
	 * @throws SignatoryException
117
	 */
118
	public function getAppSignatory(bool $generate = true, string $confirmKey = ''): RemoteInstance {
119
		$app = new RemoteInstance($this->configService->getRemotePath());
120
		$this->fillSimpleSignatory($app, $generate);
121
		$app->setUidFromKey();
122
123
		if ($confirmKey !== '') {
124
			$app->setAuthSigned($this->signString($confirmKey, $app));
125
		}
126
127
		$app->setEvent($this->configService->getRemotePath('circles.Remote.event'));
128
		$app->setIncoming($this->configService->getRemotePath('circles.Remote.incoming'));
129
		$app->setTest($this->configService->getRemotePath('circles.Remote.test'));
130
		$app->setCircles($this->configService->getRemotePath('circles.Remote.circles'));
131
		$app->setCircle(
132
			urldecode(
133
				$this->configService->getRemotePath(
134
					'circles.Remote.circle',
135
					['circleId' => '{circleId}']
136
				)
137
			)
138
		);
139
		$app->setMembers(
140
			urldecode(
141
				$this->configService->getRemotePath(
142
					'circles.Remote.members',
143
					['circleId' => '{circleId}']
144
				)
145
			)
146
		);
147
		$app->setMember(
148
			urldecode(
149
				$this->configService->getRemotePath(
150
					'circles.Remote.member',
151
					['type' => '{type}', 'userId' => '{userId}']
152
				)
153
			)
154
		);
155
156
		$app->setOrigData($app->jsonSerialize());
157
158
		return $app;
159
	}
160
161
162
	/**
163
	 * Reset the Signatory (and the Identity) for the Circles app.
164
	 */
165
	public function resetAppSignatory(): void {
166
		try {
167
			$app = $this->getAppSignatory();
168
169
			$this->removeSimpleSignatory($app);
170
		} catch (SignatoryException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
171
		}
172
	}
173
174
175
	/**
176
	 * shortcut to requestRemoteInstance that return result if available, or exception.
177
	 *
178
	 * @param string $instance
179
	 * @param string $item
180
	 * @param int $type
181
	 * @param JsonSerializable|null $object
182
	 * @param array $params
183
	 *
184
	 * @return array
185
	 * @throws RemoteNotFoundException
186
	 * @throws RemoteResourceNotFoundException
187
	 * @throws RequestNetworkException
188
	 * @throws SignatoryException
189
	 * @throws UnknownRemoteException
190
	 * @throws RemoteInstanceException
191
	 */
192
	public function resultRequestRemoteInstance(
193
		string $instance,
194
		string $item,
195
		int $type = Request::TYPE_GET,
196
		?JsonSerializable $object = null,
197
		array $params = []
198
	): array {
199
		$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
200
		if (!$signedRequest->getOutgoingRequest()->hasResult()) {
201
			throw new RequestNetworkException();
202
		}
203
204
		$result = $signedRequest->getOutgoingRequest()->getResult();
205
		if ($result->getStatusCode() !== Http::STATUS_OK) {
206
			throw new RemoteInstanceException();
207
		}
208
209
		return $result->getAsArray();
210
	}
211
212
	/**
213
	 * Send a request to a remote instance, based on:
214
	 * - instance: address as saved in database,
215
	 * - item: the item to request (incoming, event, ...)
216
	 * - type: GET, POST
217
	 * - data: Serializable to be send if needed
218
	 *
219
	 * @param string $instance
220
	 * @param string $item
221
	 * @param int $type
222
	 * @param JsonSerializable|null $object
223
	 * @param array $params
224
	 *
225
	 * @return NC21SignedRequest
226
	 * @throws RemoteNotFoundException
227
	 * @throws RemoteResourceNotFoundException
228
	 * @throws RequestNetworkException
229
	 * @throws SignatoryException
230
	 * @throws UnknownRemoteException
231
	 */
232
	public function requestRemoteInstance(
233
		string $instance,
234
		string $item,
235
		int $type = Request::TYPE_GET,
236
		?JsonSerializable $object = null,
237
		array $params = []
238
	): NC21SignedRequest {
239
240
		$request = new NC21Request('', $type);
241
		if ($this->configService->isLocalInstance($instance)) {
242
			$this->configService->configureRequest($request, 'circles.Remote.' . $item, $params);
243
		} else {
244
			$this->configService->configureRequest($request);
245
			$link = $this->getRemoteInstanceEntry($instance, $item, $params);
246
			$request->basedOnUrl($link);
247
		}
248
249
		if (!is_null($object)) {
250
			$request->setDataSerialize($object);
251
		}
252
253
		$app = $this->getAppSignatory();
254
//		$app->setAlgorithm(NC21Signatory::SHA512);
255
		$signedRequest = $this->signOutgoingRequest($request, $app);
256
		$this->doRequest($signedRequest->getOutgoingRequest(), false);
257
258
		return $signedRequest;
259
	}
260
261
262
	/**
263
	 * get the value of an entry from the Signatory of the RemoteInstance.
264
	 *
265
	 * @param string $instance
266
	 * @param string $item
267
	 * @param array $params
268
	 *
269
	 * @return string
270
	 * @throws RemoteNotFoundException
271
	 * @throws RemoteResourceNotFoundException
272
	 * @throws UnknownRemoteException
273
	 */
274
	private function getRemoteInstanceEntry(string $instance, string $item, array $params = []): string {
275
		$remote = $this->getCachedRemoteInstance($instance);
276
277
		$value = $this->get($item, $remote->getOrigData());
278
		if ($value === '') {
279
			throw new RemoteResourceNotFoundException();
280
		}
281
282
		return $this->feedStringWithParams($value, $params);
283
	}
284
285
286
	/**
287
	 * get RemoteInstance with confirmed and known identity from database.
288
	 *
289
	 * @param string $instance
290
	 *
291
	 * @return RemoteInstance
292
	 * @throws RemoteNotFoundException
293
	 * @throws UnknownRemoteException
294
	 */
295
	public function getCachedRemoteInstance(string $instance): RemoteInstance {
296
		$remoteInstance = $this->remoteRequest->getFromInstance($instance);
297
		if ($remoteInstance->getType() === RemoteInstance::TYPE_UNKNOWN) {
298
			throw new UnknownRemoteException($instance . ' is set as \'unknown\' in database');
299
		}
300
301
		return $remoteInstance;
302
	}
303
304
305
	/**
306
	 * Add a remote instance, based on the address
307
	 *
308
	 * @param string $instance
309
	 *
310
	 * @return RemoteInstance
311
	 * @throws RequestNetworkException
312
	 * @throws SignatoryException
313
	 * @throws SignatureException
314
	 * @throws WellKnownLinkNotFoundException
315
	 */
316
	public function retrieveRemoteInstance(string $instance): RemoteInstance {
317
		$resource = $this->getResourceData($instance, Application::APP_SUBJECT, Application::APP_REL);
318
319
		$remoteInstance = $this->retrieveSignatory($resource->g('id'), true);
320
		$remoteInstance->setInstance($instance);
321
322
		return $remoteInstance;
323
	}
324
325
326
	/**
327
	 * retrieve Signatory.
328
	 *
329
	 * @param string $keyId
330
	 * @param bool $refresh
331
	 *
332
	 * @return RemoteInstance
333
	 * @throws SignatoryException
334
	 * @throws SignatureException
335
	 */
336
	public function retrieveSignatory(string $keyId, bool $refresh = true): RemoteInstance {
337
		if (!$refresh) {
338
			try {
339
				return $this->remoteRequest->getFromHref(NC21Signatory::removeFragment($keyId));
340
			} catch (RemoteNotFoundException $e) {
341
				throw new SignatoryException();
342
			}
343
		}
344
345
		$remoteInstance = new RemoteInstance($keyId);
346
		$confirm = $this->uuid();
347
348
		$this->downloadSignatory($remoteInstance, $keyId, ['auth' => $confirm]);
349
		$remoteInstance->setUidFromKey();
350
351
		$this->confirmAuth($remoteInstance, $confirm);
352
353
		return $remoteInstance;
354
	}
355
356
357
	/**
358
	 * Add a remote instance, based on the address
359
	 *
360
	 * @param string $instance
361
	 * @param string $type
362
	 * @param bool $overwrite
363
	 *
364
	 * @throws RequestNetworkException
365
	 * @throws SignatoryException
366
	 * @throws SignatureException
367
	 * @throws WellKnownLinkNotFoundException
368
	 * @throws RemoteAlreadyExistsException
369
	 * @throws RemoteUidException
370
	 */
371
	public function addRemoteInstance(
372
		string $instance, string $type = RemoteInstance::TYPE_EXTERNAL, bool $overwrite = false
373
	): void {
374
		if ($this->configService->isLocalInstance($instance)) {
375
			throw new RemoteAlreadyExistsException('instance is local');
376
		}
377
378
		$remoteInstance = $this->retrieveRemoteInstance($instance);
379
		$remoteInstance->setType($type);
380
381
		try {
382
			$known = $this->remoteRequest->searchDuplicate($remoteInstance);
383
			if ($overwrite) {
384
				$this->remoteRequest->deleteById($known);
385
			} else {
386
				throw new RemoteAlreadyExistsException('instance is already known');
387
			}
388
		} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
389
		}
390
391
		$this->remoteRequest->save($remoteInstance);
392
	}
393
394
395
	/**
396
	 * Confirm the Auth of a RemoteInstance, based on the result from a request
397
	 *
398
	 * @param RemoteInstance $remote
399
	 * @param string $auth
400
	 *
401
	 * @throws SignatureException
402
	 */
403
	private function confirmAuth(RemoteInstance $remote, string $auth): void {
404
		list($algo, $signed) = explode(':', $this->get('auth-signed', $remote->getOrigData()));
405
		try {
406
			if ($signed === null) {
407
				throw new SignatureException('invalid auth-signed');
408
			}
409
			$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
410
			$remote->setIdentityAuthed(true);
411
		} catch (SignatureException $e) {
412
			$this->e(
413
				$e,
414
				['auth' => $auth, 'signed' => $signed, 'signatory' => $remote, 'msg' => 'auth not confirmed']
415
			);
416
			throw new SignatureException('auth not confirmed');
417
		}
418
	}
419
420
421
	/**
422
	 * TODO: confirm if method is really needed
423
	 *
424
	 * @param RemoteInstance $remote
425
	 * @param RemoteInstance|null $stored
426
	 *
427
	 * @throws RemoteNotFoundException
428
	 * @throws RemoteUidException
429
	 */
430
	public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
431
		try {
432
			$stored = $this->remoteRequest->getFromHref($remote->getId());
433
		} catch (RemoteNotFoundException $e) {
434
			if ($remote->getInstance() === '') {
435
				throw new RemoteNotFoundException();
436
			}
437
438
			$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
439
		}
440
441
		if ($stored->getUid() !== $remote->getUid(true)) {
442
			throw new RemoteUidException();
443
		}
444
	}
445
446
447
	/**
448
	 * TODO: check if this method is not useless
449
	 *
450
	 * @param RemoteInstance $remote
451
	 * @param string $update
452
	 *
453
	 * @throws RemoteUidException
454
	 */
455
	public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
456
		switch ($update) {
457
			case self::UPDATE_DATA:
458
				$this->remoteRequest->update($remote);
459
				break;
460
461
			case self::UPDATE_HREF:
462
				$this->remoteRequest->updateHref($remote);
463
				break;
464
465
			case self::UPDATE_INSTANCE:
466
				$this->remoteRequest->updateInstance($remote);
467
				break;
468
		}
469
	}
470
471
}
472
473