Completed
Push — master ( 98fb41...18d3f8 )
by Maxence
02:48 queued 10s
created

RemoteStreamService::update()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
cc 7
nc 12
nop 2
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\nc22\NC22Signature;
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\nc22\NC22Request;
40
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22RequestResult;
41
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22Signatory;
42
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22SignedRequest;
43
use daita\MySmallPhpTools\Model\Request;
44
use daita\MySmallPhpTools\Model\SimpleDataStore;
45
use daita\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22Deserialize;
46
use daita\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22LocalSignatory;
47
use daita\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22WellKnown;
48
use daita\MySmallPhpTools\Traits\TStringTools;
49
use JsonSerializable;
50
use OCA\Circles\AppInfo\Application;
51
use OCA\Circles\Db\RemoteRequest;
52
use OCA\Circles\Exceptions\FederatedItemException;
53
use OCA\Circles\Exceptions\RemoteAlreadyExistsException;
54
use OCA\Circles\Exceptions\RemoteInstanceException;
55
use OCA\Circles\Exceptions\RemoteNotFoundException;
56
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
57
use OCA\Circles\Exceptions\RemoteUidException;
58
use OCA\Circles\Exceptions\UnknownInterfaceException;
59
use OCA\Circles\Exceptions\UnknownRemoteException;
60
use OCA\Circles\Model\Federated\RemoteInstance;
61
use OCP\AppFramework\Http;
62
use OCP\IURLGenerator;
63
use ReflectionClass;
64
use ReflectionException;
65
66
67
/**
68
 * Class RemoteStreamService
69
 *
70
 * @package OCA\Circles\Service
71
 */
72
class RemoteStreamService extends NC22Signature {
73
74
75
	use TNC22Deserialize;
76
	use TNC22LocalSignatory;
77
	use TStringTools;
78
	use TNC22WellKnown;
79
80
81
	const UPDATE_DATA = 'data';
82
	const UPDATE_ITEM = 'item';
83
	const UPDATE_TYPE = 'type';
84
	const UPDATE_INSTANCE = 'instance';
85
	const UPDATE_HREF = 'href';
86
87
88
	/** @var IURLGenerator */
89
	private $urlGenerator;
90
91
	/** @var RemoteRequest */
92
	private $remoteRequest;
93
94
	/** @var InterfaceService */
95
	private $interfaceService;
96
97
	/** @var ConfigService */
98
	private $configService;
99
100
101
	/**
102
	 * RemoteStreamService constructor.
103
	 *
104
	 * @param IURLGenerator $urlGenerator
105
	 * @param RemoteRequest $remoteRequest
106
	 * @param InterfaceService $interfaceService
107
	 * @param ConfigService $configService
108
	 */
109
	public function __construct(
110
		IURLGenerator $urlGenerator,
111
		RemoteRequest $remoteRequest,
112
		InterfaceService $interfaceService,
113
		ConfigService $configService
114
	) {
115
		$this->setup('app', 'circles');
116
117
		$this->urlGenerator = $urlGenerator;
118
		$this->remoteRequest = $remoteRequest;
119
		$this->interfaceService = $interfaceService;
120
		$this->configService = $configService;
121
	}
122
123
124
	/**
125
	 * Returns the Signatory model for the Circles app.
126
	 * Can be signed with a confirmKey.
127
	 *
128
	 * @param bool $generate
129
	 * @param string $confirmKey
130
	 *
131
	 * @return RemoteInstance
132
	 * @throws SignatoryException
133
	 * @throws UnknownInterfaceException
134
	 */
135
	public function getAppSignatory(bool $generate = true, string $confirmKey = ''): RemoteInstance {
136
		$app = new RemoteInstance($this->interfaceService->getCloudPath('circles.Remote.appService'));
137
		$this->fillSimpleSignatory($app, $generate);
138
		$app->setUidFromKey();
139
140
		if ($confirmKey !== '') {
141
			$app->setAuthSigned($this->signString($confirmKey, $app));
142
		}
143
144
		$app->setRoot($this->interfaceService->getCloudPath());
145
		$app->setEvent($this->interfaceService->getCloudPath('circles.Remote.event'));
146
		$app->setIncoming($this->interfaceService->getCloudPath('circles.Remote.incoming'));
147
		$app->setTest($this->interfaceService->getCloudPath('circles.Remote.test'));
148
		$app->setCircles($this->interfaceService->getCloudPath('circles.Remote.circles'));
149
		$app->setCircle(
150
			urldecode(
151
				$this->interfaceService->getCloudPath('circles.Remote.circle', ['circleId' => '{circleId}'])
152
			)
153
		);
154
		$app->setMembers(
155
			urldecode(
156
				$this->interfaceService->getCloudPath('circles.Remote.members', ['circleId' => '{circleId}'])
157
			)
158
		);
159
		$app->setMember(
160
			urldecode(
161
				$this->interfaceService->getCloudPath(
162
					'circles.Remote.member', ['type' => '{type}', 'userId' => '{userId}']
163
				)
164
			)
165
		);
166
167
		if ($this->interfaceService->isCurrentInterfaceInternal()) {
168
			$app->setAliases(array_values(array_filter($this->interfaceService->getInterfaces(false))));
169
		}
170
171
		$app->setOrigData($this->serialize($app));
172
173
		return $app;
174
	}
175
176
177
	/**
178
	 * Reset the Signatory (and the Identity) for the Circles app.
179
	 */
180
	public function resetAppSignatory(): void {
181
		try {
182
			$app = $this->getAppSignatory();
183
184
			$this->removeSimpleSignatory($app);
185
		} catch (SignatoryException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
186
		}
187
	}
188
189
190
	/**
191
	 * shortcut to requestRemoteInstance that return result if available, or exception.
192
	 *
193
	 * @param string $instance
194
	 * @param string $item
195
	 * @param int $type
196
	 * @param JsonSerializable|null $object
197
	 * @param array $params
198
	 *
199
	 * @return array
200
	 * @throws RemoteInstanceException
201
	 * @throws RemoteNotFoundException
202
	 * @throws RemoteResourceNotFoundException
203
	 * @throws UnknownRemoteException
204
	 * @throws FederatedItemException
205
	 */
206
	public function resultRequestRemoteInstance(
207
		string $instance,
208
		string $item,
209
		int $type = Request::TYPE_GET,
210
		?JsonSerializable $object = null,
211
		array $params = []
212
	): array {
213
		$this->interfaceService->setCurrentInterfaceFromInstance($instance);
214
215
		$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
216
		if (!$signedRequest->getOutgoingRequest()->hasResult()) {
217
			throw new RemoteInstanceException();
218
		}
219
220
		$result = $signedRequest->getOutgoingRequest()->getResult();
221
222
		if ($result->getStatusCode() === Http::STATUS_OK) {
223
			return $result->getAsArray();
224
		}
225
226
		throw $this->getFederatedItemExceptionFromResult($result);
227
	}
228
229
230
	/**
231
	 * Send a request to a remote instance, based on:
232
	 * - instance: address as saved in database,
233
	 * - item: the item to request (incoming, event, ...)
234
	 * - type: GET, POST
235
	 * - data: Serializable to be send if needed
236
	 *
237
	 * @param string $instance
238
	 * @param string $item
239
	 * @param int $type
240
	 * @param JsonSerializable|null $object
241
	 * @param array $params
242
	 *
243
	 * @return NC22SignedRequest
244
	 * @throws RemoteNotFoundException
245
	 * @throws RemoteResourceNotFoundException
246
	 * @throws UnknownRemoteException
247
	 * @throws RemoteInstanceException
248
	 * @throws UnknownInterfaceException
249
	 */
250
	private function requestRemoteInstance(
251
		string $instance,
252
		string $item,
253
		int $type = Request::TYPE_GET,
254
		?JsonSerializable $object = null,
255
		array $params = []
256
	): NC22SignedRequest {
257
		$request = new NC22Request('', $type);
258
		$this->configService->configureRequest($request);
259
		$link = $this->getRemoteInstanceEntry($instance, $item, $params);
260
		$request->basedOnUrl($link);
261
262
		// TODO: Work Around: on local, if object is empty, request takes 10s. check on other configuration
263
		if (is_null($object) || empty($object->jsonSerialize())) {
264
			$object = new SimpleDataStore(['empty' => 1]);
265
		}
266
267
		if (!is_null($object)) {
268
			$request->setDataSerialize($object);
269
		}
270
271
		try {
272
			$app = $this->getAppSignatory();
273
//		$app->setAlgorithm(NC22Signatory::SHA512);
274
			$signedRequest = $this->signOutgoingRequest($request, $app);
275
			$this->doRequest($signedRequest->getOutgoingRequest(), false);
276
		} catch (RequestNetworkException | SignatoryException $e) {
277
			throw new RemoteInstanceException($e->getMessage());
278
		}
279
280
		return $signedRequest;
281
	}
282
283
284
	/**
285
	 * get the value of an entry from the Signatory of the RemoteInstance.
286
	 *
287
	 * @param string $instance
288
	 * @param string $item
289
	 * @param array $params
290
	 *
291
	 * @return string
292
	 * @throws RemoteNotFoundException
293
	 * @throws RemoteResourceNotFoundException
294
	 * @throws UnknownRemoteException
295
	 */
296
	private function getRemoteInstanceEntry(string $instance, string $item, array $params = []): string {
297
		$remote = $this->getCachedRemoteInstance($instance);
298
299
		$value = $this->get($item, $remote->getOrigData());
300
		if ($value === '') {
301
			throw new RemoteResourceNotFoundException();
302
		}
303
304
		return $this->feedStringWithParams($value, $params);
305
	}
306
307
308
	/**
309
	 * get RemoteInstance with confirmed and known identity from database.
310
	 *
311
	 * @param string $instance
312
	 *
313
	 * @return RemoteInstance
314
	 * @throws RemoteNotFoundException
315
	 * @throws UnknownRemoteException
316
	 */
317
	public function getCachedRemoteInstance(string $instance): RemoteInstance {
318
		$remoteInstance = $this->remoteRequest->getFromInstance($instance);
319
		if ($remoteInstance->getType() === RemoteInstance::TYPE_UNKNOWN) {
320
			throw new UnknownRemoteException($instance . ' is set as \'unknown\' in database');
321
		}
322
323
		return $remoteInstance;
324
	}
325
326
327
	/**
328
	 * Add a remote instance, based on the address
329
	 *
330
	 * @param string $instance
331
	 *
332
	 * @return RemoteInstance
333
	 * @throws RequestNetworkException
334
	 * @throws SignatoryException
335
	 * @throws SignatureException
336
	 * @throws WellKnownLinkNotFoundException
337
	 */
338
	public function retrieveRemoteInstance(string $instance): RemoteInstance {
339
		$resource = $this->getResourceData($instance, Application::APP_SUBJECT, Application::APP_REL);
340
341
		/** @var RemoteInstance $remoteInstance */
342
		$remoteInstance = $this->retrieveSignatory($resource->g('id'), true);
343
		$remoteInstance->setInstance($instance);
344
345
		return $remoteInstance;
346
	}
347
348
349
	/**
350
	 * retrieve Signatory.
351
	 *
352
	 * @param string $keyId
353
	 * @param bool $refresh
354
	 *
355
	 * @return RemoteInstance
356
	 * @throws SignatoryException
357
	 * @throws SignatureException
358
	 */
359
	public function retrieveSignatory(string $keyId, bool $refresh = true): NC22Signatory {
360
		if (!$refresh) {
361
			try {
362
				return $this->remoteRequest->getFromHref(NC22Signatory::removeFragment($keyId));
363
			} catch (RemoteNotFoundException $e) {
364
				throw new SignatoryException();
365
			}
366
		}
367
368
		$remoteInstance = new RemoteInstance($keyId);
369
		$confirm = $this->uuid();
370
371
		$request = new NC22Request();
372
		$this->configService->configureRequest($request);
373
374
		$this->downloadSignatory($remoteInstance, $keyId, ['auth' => $confirm], $request);
375
		$remoteInstance->setUidFromKey();
376
377
		$this->confirmAuth($remoteInstance, $confirm);
378
379
		return $remoteInstance;
380
	}
381
382
383
	/**
384
	 * Add a remote instance, based on the address
385
	 *
386
	 * @param string $instance
387
	 * @param string $type
388
	 * @param int $iface
389
	 * @param bool $overwrite
390
	 *
391
	 * @throws RemoteAlreadyExistsException
392
	 * @throws RemoteUidException
393
	 * @throws RequestNetworkException
394
	 * @throws SignatoryException
395
	 * @throws SignatureException
396
	 * @throws WellKnownLinkNotFoundException
397
	 */
398
	public function addRemoteInstance(
399
		string $instance,
400
		string $type = RemoteInstance::TYPE_EXTERNAL,
401
		int $iface = InterfaceService::IFACE_FRONTAL,
402
		bool $overwrite = false
403
	): void {
404
		if ($this->configService->isLocalInstance($instance)) {
405
			throw new RemoteAlreadyExistsException('instance is local');
406
		}
407
408
		$remoteInstance = $this->retrieveRemoteInstance($instance);
409
		$remoteInstance->setType($type)
410
					   ->setInterface($iface);
411
412
		if (!$this->interfaceService->isInterfaceInternal($remoteInstance->getInterface())) {
413
			$remoteInstance->setAliases([]);
414
		}
415
416
		try {
417
			$known = $this->remoteRequest->searchDuplicate($remoteInstance);
418
			if ($overwrite) {
419
				$this->remoteRequest->deleteById($known);
420
			} else {
421
				throw new RemoteAlreadyExistsException('instance is already known');
422
			}
423
		} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
424
		}
425
426
		$this->remoteRequest->save($remoteInstance);
427
	}
428
429
430
	/**
431
	 * Confirm the Auth of a RemoteInstance, based on the result from a request
432
	 *
433
	 * @param RemoteInstance $remote
434
	 * @param string $auth
435
	 *
436
	 * @throws SignatureException
437
	 */
438
	private function confirmAuth(RemoteInstance $remote, string $auth): void {
439
		list($algo, $signed) = explode(':', $this->get('auth-signed', $remote->getOrigData()));
440
		try {
441
			if ($signed === null) {
442
				throw new SignatureException('invalid auth-signed');
443
			}
444
			$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
445
			$remote->setIdentityAuthed(true);
446
		} catch (SignatureException $e) {
447
			$this->e(
448
				$e,
449
				['auth' => $auth, 'signed' => $signed, 'signatory' => $remote, 'msg' => 'auth not confirmed']
450
			);
451
			throw new SignatureException('auth not confirmed');
452
		}
453
	}
454
455
456
	/**
457
	 * @param NC22RequestResult $result
458
	 *
459
	 * @return FederatedItemException
460
	 */
461
	private function getFederatedItemExceptionFromResult(NC22RequestResult $result): FederatedItemException {
462
		$data = $result->getAsArray();
463
464
		$message = $this->get('message', $data);
465
		$code = $this->getInt('code', $data);
466
		$class = $this->get('class', $data);
467
468
		try {
469
			$test = new ReflectionClass($class);
470
			$this->confirmFederatedItemExceptionFromClass($test);
471
			$e = $class;
472
		} catch (ReflectionException | FederatedItemException $_e) {
473
			$e = $this->getFederatedItemExceptionFromStatus($result->getStatusCode());
474
		}
475
476
		return new $e($message, $code);
477
	}
478
479
480
	/**
481
	 * @param ReflectionClass $class
482
	 *
483
	 * @return void
484
	 * @throws FederatedItemException
485
	 */
486
	private function confirmFederatedItemExceptionFromClass(ReflectionClass $class): void {
487
		while (true) {
488
			foreach (FederatedItemException::$CHILDREN as $e) {
0 ignored issues
show
Bug introduced by
The property CHILDREN cannot be accessed from this context as it is declared private in class OCA\Circles\Exceptions\FederatedItemException.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
489
				if ($class->getName() === $e) {
490
					return;
491
				}
492
			}
493
			$class = $class->getParentClass();
494
			if (!$class) {
495
				throw new FederatedItemException();
496
			}
497
		}
498
	}
499
500
501
	/**
502
	 * @param int $statusCode
503
	 *
504
	 * @return string
505
	 */
506
	private function getFederatedItemExceptionFromStatus(int $statusCode): string {
507
		foreach (FederatedItemException::$CHILDREN as $e) {
0 ignored issues
show
Bug introduced by
The property CHILDREN cannot be accessed from this context as it is declared private in class OCA\Circles\Exceptions\FederatedItemException.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
508
			if ($e::STATUS === $statusCode) {
509
				return $e;
510
			}
511
		}
512
513
		return FederatedItemException::class;
514
	}
515
516
517
	/**
518
	 * TODO: confirm if method is really needed
519
	 *
520
	 * @param RemoteInstance $remote
521
	 * @param RemoteInstance|null $stored
522
	 *
523
	 * @throws RemoteNotFoundException
524
	 * @throws RemoteUidException
525
	 */
526
	public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
527
		try {
528
			$stored = $this->remoteRequest->getFromHref($remote->getId());
529
		} catch (RemoteNotFoundException $e) {
530
			if ($remote->getInstance() === '') {
531
				throw new RemoteNotFoundException();
532
			}
533
534
			$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
535
		}
536
537
		if ($stored->getUid() !== $remote->getUid(true)) {
538
			throw new RemoteUidException();
539
		}
540
	}
541
542
543
	/**
544
	 * TODO: check if this method is not useless
545
	 *
546
	 * @param RemoteInstance $remote
547
	 * @param string $update
548
	 *
549
	 * @throws RemoteUidException
550
	 */
551
	public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
552
		if (!$this->interfaceService->isInterfaceInternal($remote->getInterface())) {
553
			$remote->setAliases([]);
554
		}
555
556
		switch ($update) {
557
			case self::UPDATE_DATA:
558
				$this->remoteRequest->update($remote);
559
				break;
560
561
			case self::UPDATE_ITEM:
562
				$this->remoteRequest->updateItem($remote);
563
				break;
564
565
			case self::UPDATE_TYPE:
566
				$this->remoteRequest->updateType($remote);
567
				break;
568
569
			case self::UPDATE_HREF:
570
				$this->remoteRequest->updateHref($remote);
571
				break;
572
573
			case self::UPDATE_INSTANCE:
574
				$this->remoteRequest->updateInstance($remote);
575
				break;
576
		}
577
	}
578
579
}
580
581