Completed
Pull Request — master (#551)
by Maxence
02:15
created

getFederatedItemExceptionFromStatus()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 1
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\UnknownRemoteException;
59
use OCA\Circles\Model\Federated\RemoteInstance;
60
use OCP\AppFramework\Http;
61
use OCP\IL10N;
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_TYPE = 'type';
83
	const UPDATE_INSTANCE = 'instance';
84
	const UPDATE_HREF = 'href';
85
86
87
	/** @var IL10N */
88
	private $l10n;
89
90
	/** @var IURLGenerator */
91
	private $urlGenerator;
92
93
	/** @var RemoteRequest */
94
	private $remoteRequest;
95
96
	/** @var ConfigService */
97
	private $configService;
98
99
100
	/**
101
	 * RemoteStreamService constructor.
102
	 *
103
	 * @param IL10N $l10n
104
	 * @param IURLGenerator $urlGenerator
105
	 * @param RemoteRequest $remoteRequest
106
	 * @param ConfigService $configService
107
	 */
108
	public function __construct(
109
		IL10N $l10n, IURLGenerator $urlGenerator, RemoteRequest $remoteRequest, ConfigService $configService
110
	) {
111
		$this->setup('app', 'circles');
112
113
		$this->l10n = $l10n;
114
		$this->urlGenerator = $urlGenerator;
115
		$this->remoteRequest = $remoteRequest;
116
		$this->configService = $configService;
117
	}
118
119
120
	/**
121
	 * Returns the Signatory model for the Circles app.
122
	 * Can be signed with a confirmKey.
123
	 *
124
	 * @param bool $generate
125
	 * @param string $confirmKey
126
	 *
127
	 * @return RemoteInstance
128
	 * @throws SignatoryException
129
	 */
130
	public function getAppSignatory(bool $generate = true, string $confirmKey = ''): RemoteInstance {
131
		$app = new RemoteInstance($this->configService->getFrontalPath());
132
		$this->fillSimpleSignatory($app, $generate);
133
		$app->setUidFromKey();
134
135
		if ($confirmKey !== '') {
136
			$app->setAuthSigned($this->signString($confirmKey, $app));
137
		}
138
139
		$app->setRoot($this->configService->getFrontalPath(''));
140
		$app->setEvent($this->configService->getFrontalPath('circles.Remote.event'));
141
		$app->setIncoming($this->configService->getFrontalPath('circles.Remote.incoming'));
142
		$app->setTest($this->configService->getFrontalPath('circles.Remote.test'));
143
		$app->setCircles($this->configService->getFrontalPath('circles.Remote.circles'));
144
		$app->setCircle(
145
			urldecode(
146
				$this->configService->getFrontalPath(
147
					'circles.Remote.circle',
148
					['circleId' => '{circleId}']
149
				)
150
			)
151
		);
152
		$app->setMembers(
153
			urldecode(
154
				$this->configService->getFrontalPath(
155
					'circles.Remote.members',
156
					['circleId' => '{circleId}']
157
				)
158
			)
159
		);
160
		$app->setMember(
161
			urldecode(
162
				$this->configService->getFrontalPath(
163
					'circles.Remote.member',
164
					['type' => '{type}', 'userId' => '{userId}']
165
				)
166
			)
167
		);
168
169
		$app->setOrigData($app->jsonSerialize());
170
171
		return $app;
172
	}
173
174
175
	/**
176
	 * Reset the Signatory (and the Identity) for the Circles app.
177
	 */
178
	public function resetAppSignatory(): void {
179
		try {
180
			$app = $this->getAppSignatory();
181
182
			$this->removeSimpleSignatory($app);
183
		} catch (SignatoryException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
184
		}
185
	}
186
187
188
	/**
189
	 * shortcut to requestRemoteInstance that return result if available, or exception.
190
	 *
191
	 * @param string $instance
192
	 * @param string $item
193
	 * @param int $type
194
	 * @param JsonSerializable|null $object
195
	 * @param array $params
196
	 *
197
	 * @return array
198
	 * @throws RemoteInstanceException
199
	 * @throws RemoteNotFoundException
200
	 * @throws RemoteResourceNotFoundException
201
	 * @throws UnknownRemoteException
202
	 * @throws FederatedItemException
203
	 */
204
	public function resultRequestRemoteInstance(
205
		string $instance,
206
		string $item,
207
		int $type = Request::TYPE_GET,
208
		?JsonSerializable $object = null,
209
		array $params = []
210
	): array {
211
		// TODO: check what is happening if website is down...
212
		$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
213
		if (!$signedRequest->getOutgoingRequest()->hasResult()) {
214
			throw new RemoteInstanceException();
215
		}
216
217
		$result = $signedRequest->getOutgoingRequest()->getResult();
218
219
		if ($result->getStatusCode() === Http::STATUS_OK) {
220
			return $result->getAsArray();
221
		}
222
223
		throw $this->getFederatedItemExceptionFromResult($result);
224
	}
225
226
227
	/**
228
	 * Send a request to a remote instance, based on:
229
	 * - instance: address as saved in database,
230
	 * - item: the item to request (incoming, event, ...)
231
	 * - type: GET, POST
232
	 * - data: Serializable to be send if needed
233
	 *
234
	 * @param string $instance
235
	 * @param string $item
236
	 * @param int $type
237
	 * @param JsonSerializable|null $object
238
	 * @param array $params
239
	 *
240
	 * @return NC22SignedRequest
241
	 * @throws RemoteNotFoundException
242
	 * @throws RemoteResourceNotFoundException
243
	 * @throws UnknownRemoteException
244
	 * @throws RemoteInstanceException
245
	 */
246
	private function requestRemoteInstance(
247
		string $instance,
248
		string $item,
249
		int $type = Request::TYPE_GET,
250
		?JsonSerializable $object = null,
251
		array $params = []
252
	): NC22SignedRequest {
253
254
		$request = new NC22Request('', $type);
255
		if ($this->configService->isLocalInstance($instance)) {
256
			$this->configService->configureRequest($request, 'circles.Remote.' . $item, $params);
257
		} else {
258
			$this->configService->configureRequest($request);
259
			$link = $this->getRemoteInstanceEntry($instance, $item, $params);
260
			$request->basedOnUrl($link);
261
		}
262
263
		// TODO: Work Around: on local, if object is empty, request takes 10s. check on other configuration
264
		if (is_null($object) || empty($object->jsonSerialize())) {
265
			$object = new SimpleDataStore(['empty' => 1]);
266
		}
267
268
		if (!is_null($object)) {
269
			$request->setDataSerialize($object);
270
		}
271
272
		try {
273
			$app = $this->getAppSignatory();
274
//		$app->setAlgorithm(NC22Signatory::SHA512);
275
			$signedRequest = $this->signOutgoingRequest($request, $app);
276
			$this->doRequest($signedRequest->getOutgoingRequest(), false);
277
		} catch (RequestNetworkException | SignatoryException $e) {
278
			throw new RemoteInstanceException($e->getMessage());
279
		}
280
281
		return $signedRequest;
282
	}
283
284
285
	/**
286
	 * get the value of an entry from the Signatory of the RemoteInstance.
287
	 *
288
	 * @param string $instance
289
	 * @param string $item
290
	 * @param array $params
291
	 *
292
	 * @return string
293
	 * @throws RemoteNotFoundException
294
	 * @throws RemoteResourceNotFoundException
295
	 * @throws UnknownRemoteException
296
	 */
297
	private function getRemoteInstanceEntry(string $instance, string $item, array $params = []): string {
298
		$remote = $this->getCachedRemoteInstance($instance);
299
300
		$value = $this->get($item, $remote->getOrigData());
301
		if ($value === '') {
302
			throw new RemoteResourceNotFoundException();
303
		}
304
305
		return $this->feedStringWithParams($value, $params);
306
	}
307
308
309
	/**
310
	 * get RemoteInstance with confirmed and known identity from database.
311
	 *
312
	 * @param string $instance
313
	 *
314
	 * @return RemoteInstance
315
	 * @throws RemoteNotFoundException
316
	 * @throws UnknownRemoteException
317
	 */
318
	public function getCachedRemoteInstance(string $instance): RemoteInstance {
319
		$remoteInstance = $this->remoteRequest->getFromInstance($instance);
320
		if ($remoteInstance->getType() === RemoteInstance::TYPE_UNKNOWN) {
321
			throw new UnknownRemoteException($instance . ' is set as \'unknown\' in database');
322
		}
323
324
		return $remoteInstance;
325
	}
326
327
328
	/**
329
	 * Add a remote instance, based on the address
330
	 *
331
	 * @param string $instance
332
	 *
333
	 * @return RemoteInstance
334
	 * @throws RequestNetworkException
335
	 * @throws SignatoryException
336
	 * @throws SignatureException
337
	 * @throws WellKnownLinkNotFoundException
338
	 */
339
	public function retrieveRemoteInstance(string $instance): RemoteInstance {
340
		$resource = $this->getResourceData($instance, Application::APP_SUBJECT, Application::APP_REL);
341
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): RemoteInstance {
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 bool $overwrite
389
	 *
390
	 * @throws RequestNetworkException
391
	 * @throws SignatoryException
392
	 * @throws SignatureException
393
	 * @throws WellKnownLinkNotFoundException
394
	 * @throws RemoteAlreadyExistsException
395
	 * @throws RemoteUidException
396
	 */
397
	public function addRemoteInstance(
398
		string $instance, string $type = RemoteInstance::TYPE_EXTERNAL, bool $overwrite = false
399
	): void {
400
		if ($this->configService->isLocalInstance($instance)) {
401
			throw new RemoteAlreadyExistsException('instance is local');
402
		}
403
404
		$remoteInstance = $this->retrieveRemoteInstance($instance);
405
		$remoteInstance->setType($type);
406
407
		try {
408
			$known = $this->remoteRequest->searchDuplicate($remoteInstance);
409
			if ($overwrite) {
410
				$this->remoteRequest->deleteById($known);
411
			} else {
412
				throw new RemoteAlreadyExistsException('instance is already known');
413
			}
414
		} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
415
		}
416
417
		$this->remoteRequest->save($remoteInstance);
418
	}
419
420
421
	/**
422
	 * Confirm the Auth of a RemoteInstance, based on the result from a request
423
	 *
424
	 * @param RemoteInstance $remote
425
	 * @param string $auth
426
	 *
427
	 * @throws SignatureException
428
	 */
429
	private function confirmAuth(RemoteInstance $remote, string $auth): void {
430
		list($algo, $signed) = explode(':', $this->get('auth-signed', $remote->getOrigData()));
431
		try {
432
			if ($signed === null) {
433
				throw new SignatureException('invalid auth-signed');
434
			}
435
			$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
436
			$remote->setIdentityAuthed(true);
437
		} catch (SignatureException $e) {
438
			$this->e(
439
				$e,
440
				['auth' => $auth, 'signed' => $signed, 'signatory' => $remote, 'msg' => 'auth not confirmed']
441
			);
442
			throw new SignatureException('auth not confirmed');
443
		}
444
	}
445
446
447
	/**
448
	 * @param NC22RequestResult $result
449
	 *
450
	 * @return FederatedItemException
451
	 */
452
	private function getFederatedItemExceptionFromResult(NC22RequestResult $result): FederatedItemException {
453
		$data = $result->getAsArray();
454
455
		$message = $this->get('message', $data);
456
		$code = $this->getInt('code', $data);
457
		$class = $this->get('class', $data);
458
459
		try {
460
			$test = new ReflectionClass($class);
461
			$this->confirmFederatedItemExceptionFromClass($test);
462
			$e = $class;
463
		} catch (ReflectionException | FederatedItemException $_e) {
464
			$e = $this->getFederatedItemExceptionFromStatus($result->getStatusCode());
465
		}
466
467
		return new $e($message, $code);
468
	}
469
470
471
	/**
472
	 * @param ReflectionClass $class
473
	 *
474
	 * @return void
475
	 * @throws FederatedItemException
476
	 */
477
	private function confirmFederatedItemExceptionFromClass(ReflectionClass $class): void {
478
		while (true) {
479
			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...
480
				if ($class->getName() === $e) {
481
					return;
482
				}
483
			}
484
			$class = $class->getParentClass();
485
			if (!$class) {
486
				throw new FederatedItemException();
487
			}
488
		}
489
	}
490
491
492
	/**
493
	 * @param int $statusCode
494
	 *
495
	 * @return string
496
	 */
497
	private function getFederatedItemExceptionFromStatus(int $statusCode): string {
498
		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...
499
			if ($e::STATUS === $statusCode) {
500
				return $e;
501
			}
502
		}
503
504
		return FederatedItemException::class;
505
	}
506
507
508
	/**
509
	 * TODO: confirm if method is really needed
510
	 *
511
	 * @param RemoteInstance $remote
512
	 * @param RemoteInstance|null $stored
513
	 *
514
	 * @throws RemoteNotFoundException
515
	 * @throws RemoteUidException
516
	 */
517
	public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
518
		try {
519
			$stored = $this->remoteRequest->getFromHref($remote->getId());
520
		} catch (RemoteNotFoundException $e) {
521
			if ($remote->getInstance() === '') {
522
				throw new RemoteNotFoundException();
523
			}
524
525
			$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
526
		}
527
528
		if ($stored->getUid() !== $remote->getUid(true)) {
529
			throw new RemoteUidException();
530
		}
531
	}
532
533
534
	/**
535
	 * TODO: check if this method is not useless
536
	 *
537
	 * @param RemoteInstance $remote
538
	 * @param string $update
539
	 *
540
	 * @throws RemoteUidException
541
	 */
542
	public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
543
		switch ($update) {
544
			case self::UPDATE_DATA:
545
				$this->remoteRequest->update($remote);
546
				break;
547
548
			case self::UPDATE_TYPE:
549
				$this->remoteRequest->updateType($remote);
550
				break;
551
552
			case self::UPDATE_HREF:
553
				$this->remoteRequest->updateHref($remote);
554
				break;
555
556
			case self::UPDATE_INSTANCE:
557
				$this->remoteRequest->updateInstance($remote);
558
				break;
559
		}
560
	}
561
562
}
563
564