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

RemoteStreamService   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 426
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 21
dl 0
loc 426
rs 9.6
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getAppSignatory() 0 42 2
A resetAppSignatory() 0 8 2
A getCircles() 0 5 1
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\InvalidItemException;
36
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
37
use daita\MySmallPhpTools\Exceptions\SignatoryException;
38
use daita\MySmallPhpTools\Exceptions\SignatureException;
39
use daita\MySmallPhpTools\Exceptions\WellKnownLinkNotFoundException;
40
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Request;
41
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21Signatory;
42
use daita\MySmallPhpTools\Model\Nextcloud\nc21\NC21SignedRequest;
43
use daita\MySmallPhpTools\Model\Request;
44
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21Convert;
45
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21LocalSignatory;
46
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21WellKnown;
47
use daita\MySmallPhpTools\Traits\TStringTools;
48
use JsonSerializable;
49
use OCA\Circles\AppInfo\Application;
50
use OCA\Circles\Db\RemoteRequest;
51
use OCA\Circles\Exceptions\RemoteAlreadyExistsException;
52
use OCA\Circles\Exceptions\RemoteInstanceException;
53
use OCA\Circles\Exceptions\RemoteNotFoundException;
54
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
55
use OCA\Circles\Exceptions\RemoteUidException;
56
use OCA\Circles\Exceptions\UnknownRemoteException;
57
use OCA\Circles\Model\Circle;
58
use OCA\Circles\Model\Federated\RemoteInstance;
59
use OCP\AppFramework\Http;
60
use OCP\IURLGenerator;
61
62
63
/**
64
 * Class RemoteStreamService
65
 *
66
 * @package OCA\Circles\Service
67
 */
68
class RemoteStreamService extends NC21Signature {
69
70
71
	use TNC21Convert;
72
	use TNC21LocalSignatory;
73
	use TStringTools;
74
	use TNC21WellKnown;
75
76
77
	const UPDATE_DATA = 'data';
78
	const UPDATE_INSTANCE = 'instance';
79
	const UPDATE_HREF = 'href';
80
81
82
	/** @var IURLGenerator */
83
	private $urlGenerator;
84
85
	/** @var RemoteRequest */
86
	private $remoteRequest;
87
88
	/** @var ConfigService */
89
	private $configService;
90
91
92
	/**
93
	 * RemoteStreamService constructor.
94
	 *
95
	 * @param IURLGenerator $urlGenerator
96
	 * @param RemoteRequest $remoteRequest
97
	 * @param ConfigService $configService
98
	 */
99
	public function __construct(
100
		IURLGenerator $urlGenerator, RemoteRequest $remoteRequest, ConfigService $configService
101
	) {
102
		$this->setup('app', 'circles');
103
104
		$this->urlGenerator = $urlGenerator;
105
		$this->remoteRequest = $remoteRequest;
106
		$this->configService = $configService;
107
	}
108
109
110
	/**
111
	 * Returns the Signatory model for the Circles app.
112
	 * Can be signed with a confirmKey.
113
	 *
114
	 * @param bool $generate
115
	 * @param string $confirmKey
116
	 *
117
	 * @return RemoteInstance
118
	 * @throws SignatoryException
119
	 */
120
	public function getAppSignatory(bool $generate = true, string $confirmKey = ''): RemoteInstance {
121
		$app = new RemoteInstance($this->configService->getRemotePath());
122
		$this->fillSimpleSignatory($app, $generate);
123
		$app->setUidFromKey();
124
125
		if ($confirmKey !== '') {
126
			$app->setAuthSigned($this->signString($confirmKey, $app));
127
		}
128
129
		$app->setEvent($this->configService->getRemotePath('circles.Remote.event'));
130
		$app->setIncoming($this->configService->getRemotePath('circles.Remote.incoming'));
131
		$app->setTest($this->configService->getRemotePath('circles.Remote.test'));
132
		$app->setCircles($this->configService->getRemotePath('circles.Remote.circles'));
133
		$app->setCircle(
134
			urldecode(
135
				$this->configService->getRemotePath(
136
					'circles.Remote.circle',
137
					['circleId' => '{circleId}']
138
				)
139
			)
140
		);
141
		$app->setMembers(
142
			urldecode(
143
				$this->configService->getRemotePath(
144
					'circles.Remote.members',
145
					['circleId' => '{circleId}']
146
				)
147
			)
148
		);
149
		$app->setMember(
150
			urldecode(
151
				$this->configService->getRemotePath(
152
					'circles.Remote.member',
153
					['type' => '{type}', 'userId' => '{userId}']
154
				)
155
			)
156
		);
157
158
		$app->setOrigData($app->jsonSerialize());
159
160
		return $app;
161
	}
162
163
164
	/**
165
	 * Reset the Signatory (and the Identity) for the Circles app.
166
	 */
167
	public function resetAppSignatory(): void {
168
		try {
169
			$app = $this->getAppSignatory();
170
171
			$this->removeSimpleSignatory($app);
172
		} catch (SignatoryException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
173
		}
174
	}
175
176
177
	/**
178
	 * TODO: might move this out from this class
179
	 *
180
	 * @param string $instance
181
	 *
182
	 * @return Circle[]
183
	 * @throws RemoteNotFoundException
184
	 * @throws RemoteResourceNotFoundException
185
	 * @throws RequestNetworkException
186
	 * @throws SignatoryException
187
	 * @throws InvalidItemException
188
	 * @throws UnknownRemoteException
189
	 */
190
	public function getCircles(string $instance): array {
191
		$circles = $this->resultRequestRemoteInstance($instance, RemoteInstance::CIRCLES);
192
193
		return $this->convertArray($circles, Circle::class);
194
	}
195
196
197
	/**
198
	 * shortcut to requestRemoteInstance that return result if available, or exception.
199
	 *
200
	 * @param string $instance
201
	 * @param string $item
202
	 * @param int $type
203
	 * @param JsonSerializable|null $object
204
	 * @param array $params
205
	 *
206
	 * @return array
207
	 * @throws RemoteNotFoundException
208
	 * @throws RemoteResourceNotFoundException
209
	 * @throws RequestNetworkException
210
	 * @throws SignatoryException
211
	 * @throws UnknownRemoteException
212
	 * @throws RemoteInstanceException
213
	 */
214
	public function resultRequestRemoteInstance(
215
		string $instance,
216
		string $item,
217
		int $type = Request::TYPE_GET,
218
		?JsonSerializable $object = null,
219
		array $params = []
220
	): array {
221
		$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
222
		if (!$signedRequest->getOutgoingRequest()->hasResult()) {
223
			throw new RequestNetworkException();
224
		}
225
226
		$result = $signedRequest->getOutgoingRequest()->getResult();
227
		if ($result->getStatusCode() !== Http::STATUS_OK) {
228
			throw new RemoteInstanceException();
229
		}
230
231
		return $result->getAsArray();
232
	}
233
234
	/**
235
	 * Send a request to a remote instance, based on:
236
	 * - instance: address as saved in database,
237
	 * - item: the item to request (incoming, event, ...)
238
	 * - type: GET, POST
239
	 * - data: Serializable to be send if needed
240
	 *
241
	 * @param string $instance
242
	 * @param string $item
243
	 * @param int $type
244
	 * @param JsonSerializable|null $object
245
	 * @param array $params
246
	 *
247
	 * @return NC21SignedRequest
248
	 * @throws RemoteNotFoundException
249
	 * @throws RemoteResourceNotFoundException
250
	 * @throws RequestNetworkException
251
	 * @throws SignatoryException
252
	 * @throws UnknownRemoteException
253
	 */
254
	public function requestRemoteInstance(
255
		string $instance,
256
		string $item,
257
		int $type = Request::TYPE_GET,
258
		?JsonSerializable $object = null,
259
		array $params = []
260
	): NC21SignedRequest {
261
262
		$request = new NC21Request('', $type);
263
		if ($this->configService->isLocalInstance($instance)) {
264
			$this->configService->configureRequest($request, 'circles.Remote.' . $item, $params);
265
		} else {
266
			$this->configService->configureRequest($request);
267
			$link = $this->getRemoteInstanceEntry($instance, $item, $params);
268
			$request->basedOnUrl($link);
269
		}
270
271
		if (!is_null($object)) {
272
			$request->setDataSerialize($object);
273
		}
274
275
		$app = $this->getAppSignatory();
276
//		$app->setAlgorithm(NC21Signatory::SHA512);
277
		$signedRequest = $this->signOutgoingRequest($request, $app);
278
		$this->doRequest($signedRequest->getOutgoingRequest(), false);
0 ignored issues
show
Unused Code introduced by
The call to RemoteStreamService::doRequest() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The method feedStringWithParams() does not seem to exist on object<OCA\Circles\Service\RemoteStreamService>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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
		$remoteInstance = $this->retrieveSignatory($resource->g('id'), true);
342
		$remoteInstance->setInstance($instance);
343
344
		return $remoteInstance;
345
	}
346
347
348
	/**
349
	 * retrieve Signatory.
350
	 *
351
	 * @param string $keyId
352
	 * @param bool $refresh
353
	 *
354
	 * @return RemoteInstance
355
	 * @throws SignatoryException
356
	 * @throws SignatureException
357
	 */
358
	public function retrieveSignatory(string $keyId, bool $refresh = true): RemoteInstance {
359
		if (!$refresh) {
360
			try {
361
				return $this->remoteRequest->getFromHref(NC21Signatory::removeFragment($keyId));
0 ignored issues
show
Bug introduced by
The method removeFragment() does not seem to exist on object<daita\MySmallPhpT...oud\nc21\NC21Signatory>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
362
			} catch (RemoteNotFoundException $e) {
363
				throw new SignatoryException();
364
			}
365
		}
366
367
		$remoteInstance = new RemoteInstance($keyId);
368
		$confirm = $this->uuid();
369
370
		$this->downloadSignatory($remoteInstance, $keyId, ['auth' => $confirm]);
371
		$remoteInstance->setUidFromKey();
372
373
		$this->confirmAuth($remoteInstance, $confirm);
374
375
		return $remoteInstance;
376
	}
377
378
379
	/**
380
	 * Add a remote instance, based on the address
381
	 *
382
	 * @param string $instance
383
	 * @param string $type
384
	 * @param bool $overwrite
385
	 *
386
	 * @throws RequestNetworkException
387
	 * @throws SignatoryException
388
	 * @throws SignatureException
389
	 * @throws WellKnownLinkNotFoundException
390
	 * @throws RemoteAlreadyExistsException
391
	 * @throws RemoteUidException
392
	 */
393
	public function addRemoteInstance(
394
		string $instance, string $type = RemoteInstance::TYPE_EXTERNAL, bool $overwrite = false
395
	): void {
396
		if ($this->configService->isLocalInstance($instance)) {
397
			throw new RemoteAlreadyExistsException('instance is local');
398
		}
399
400
		$remoteInstance = $this->retrieveRemoteInstance($instance);
401
		$remoteInstance->setType($type);
402
403
		try {
404
			$known = $this->remoteRequest->searchDuplicate($remoteInstance);
405
			if ($overwrite) {
406
				$this->remoteRequest->deleteById($known);
407
			} else {
408
				throw new RemoteAlreadyExistsException('instance is already known');
409
			}
410
		} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
411
		}
412
413
		$this->remoteRequest->save($remoteInstance);
414
	}
415
416
417
	/**
418
	 * Confirm the Auth of a RemoteInstance, based on the result from a request
419
	 *
420
	 * @param RemoteInstance $remote
421
	 * @param string $auth
422
	 *
423
	 * @throws SignatureException
424
	 */
425
	private function confirmAuth(RemoteInstance $remote, string $auth): void {
426
		list($algo, $signed) = explode(':', $this->get('auth-signed', $remote->getOrigData()));
427
		try {
428
			if ($signed === null) {
429
				throw new SignatureException('invalid auth-signed');
430
			}
431
			$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
432
			$remote->setIdentityAuthed(true);
433
		} catch (SignatureException $e) {
434
			$this->e(
435
				$e,
436
				['auth' => $auth, 'signed' => $signed, 'signatory' => $remote, 'msg' => 'auth not confirmed']
437
			);
438
			throw new SignatureException('auth not confirmed');
439
		}
440
	}
441
442
443
	/**
444
	 * TODO: confirm if method is really needed
445
	 *
446
	 * @param RemoteInstance $remote
447
	 * @param RemoteInstance|null $stored
448
	 *
449
	 * @throws RemoteNotFoundException
450
	 * @throws RemoteUidException
451
	 */
452
	public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
453
		try {
454
			$stored = $this->remoteRequest->getFromHref($remote->getId());
455
		} catch (RemoteNotFoundException $e) {
456
			if ($remote->getInstance() === '') {
457
				throw new RemoteNotFoundException();
458
			}
459
460
			$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
461
		}
462
463
		if ($stored->getUid() !== $remote->getUid(true)) {
464
			throw new RemoteUidException();
465
		}
466
	}
467
468
469
	/**
470
	 * TODO: check if this method is not useless
471
	 *
472
	 * @param RemoteInstance $remote
473
	 * @param string $update
474
	 *
475
	 * @throws RemoteUidException
476
	 */
477
	public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
478
		switch ($update) {
479
			case self::UPDATE_DATA:
480
				$this->remoteRequest->update($remote);
481
				break;
482
483
			case self::UPDATE_HREF:
484
				$this->remoteRequest->updateHref($remote);
485
				break;
486
487
			case self::UPDATE_INSTANCE:
488
				$this->remoteRequest->updateInstance($remote);
489
				break;
490
		}
491
	}
492
493
}
494
495