Completed
Pull Request — master (#597)
by Maxence
02:57
created

RemoteStreamService::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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