Completed
Pull Request — master (#551)
by Maxence
84:52
created

RemoteStreamService::confirmValidRemote()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 4
nc 5
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\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\Model\SimpleDataStore;
44
use daita\MySmallPhpTools\Traits\Nextcloud\nc21\TNC21Deserialize;
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\Federated\RemoteInstance;
58
use OCP\AppFramework\Http;
59
use OCP\IURLGenerator;
60
61
62
/**
63
 * Class RemoteStreamService
64
 *
65
 * @package OCA\Circles\Service
66
 */
67
class RemoteStreamService extends NC21Signature {
68
69
70
	use TNC21Deserialize;
71
	use TNC21LocalSignatory;
72
	use TStringTools;
73
	use TNC21WellKnown;
74
75
76
	const UPDATE_DATA = 'data';
77
	const UPDATE_TYPE = 'type';
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
	 * shortcut to requestRemoteInstance that return result if available, or exception.
179
	 *
180
	 * @param string $instance
181
	 * @param string $item
182
	 * @param int $type
183
	 * @param JsonSerializable|null $object
184
	 * @param array $params
185
	 *
186
	 * @return array
187
	 * @throws RemoteNotFoundException
188
	 * @throws RemoteResourceNotFoundException
189
	 * @throws RequestNetworkException
190
	 * @throws SignatoryException
191
	 * @throws UnknownRemoteException
192
	 * @throws RemoteInstanceException
193
	 */
194
	public function resultRequestRemoteInstance(
195
		string $instance,
196
		string $item,
197
		int $type = Request::TYPE_GET,
198
		?JsonSerializable $object = null,
199
		array $params = []
200
	): array {
201
202
		$signedRequest = $this->requestRemoteInstance($instance, $item, $type, $object, $params);
203
		if (!$signedRequest->getOutgoingRequest()->hasResult()) {
204
			throw new RequestNetworkException();
205
		}
206
207
		$result = $signedRequest->getOutgoingRequest()->getResult();
208
		if ($result->getStatusCode() !== Http::STATUS_OK) {
209
			throw new RemoteInstanceException();
210
		}
211
212
		return $result->getAsArray();
213
	}
214
215
	/**
216
	 * Send a request to a remote instance, based on:
217
	 * - instance: address as saved in database,
218
	 * - item: the item to request (incoming, event, ...)
219
	 * - type: GET, POST
220
	 * - data: Serializable to be send if needed
221
	 *
222
	 * @param string $instance
223
	 * @param string $item
224
	 * @param int $type
225
	 * @param JsonSerializable|null $object
226
	 * @param array $params
227
	 *
228
	 * @return NC21SignedRequest
229
	 * @throws RemoteNotFoundException
230
	 * @throws RemoteResourceNotFoundException
231
	 * @throws RequestNetworkException
232
	 * @throws SignatoryException
233
	 * @throws UnknownRemoteException
234
	 */
235
	public function requestRemoteInstance(
236
		string $instance,
237
		string $item,
238
		int $type = Request::TYPE_GET,
239
		?JsonSerializable $object = null,
240
		array $params = []
241
	): NC21SignedRequest {
242
243
		$request = new NC21Request('', $type);
244
		if ($this->configService->isLocalInstance($instance)) {
245
			$this->configService->configureRequest($request, 'circles.Remote.' . $item, $params);
246
		} else {
247
			$this->configService->configureRequest($request);
248
			$link = $this->getRemoteInstanceEntry($instance, $item, $params);
249
			$request->basedOnUrl($link);
250
		}
251
252
		// TODO: on local, if object is empty, request takes 10s. check on other configuration
253
		if (is_null($object) || empty(json_decode(json_encode($object), true))) {
254
			$object = new SimpleDataStore(['empty' => 1]);
255
		}
256
257
		if (!is_null($object)) {
258
			$request->setDataSerialize($object);
259
		}
260
261
		$app = $this->getAppSignatory();
262
//		$app->setAlgorithm(NC21Signatory::SHA512);
263
		$signedRequest = $this->signOutgoingRequest($request, $app);
264
		$this->doRequest($signedRequest->getOutgoingRequest(), false);
265
266
		return $signedRequest;
267
	}
268
269
270
	/**
271
	 * get the value of an entry from the Signatory of the RemoteInstance.
272
	 *
273
	 * @param string $instance
274
	 * @param string $item
275
	 * @param array $params
276
	 *
277
	 * @return string
278
	 * @throws RemoteNotFoundException
279
	 * @throws RemoteResourceNotFoundException
280
	 * @throws UnknownRemoteException
281
	 */
282
	private function getRemoteInstanceEntry(string $instance, string $item, array $params = []): string {
283
		$remote = $this->getCachedRemoteInstance($instance);
284
285
		$value = $this->get($item, $remote->getOrigData());
286
		if ($value === '') {
287
			throw new RemoteResourceNotFoundException();
288
		}
289
290
		return $this->feedStringWithParams($value, $params);
291
	}
292
293
294
	/**
295
	 * get RemoteInstance with confirmed and known identity from database.
296
	 *
297
	 * @param string $instance
298
	 *
299
	 * @return RemoteInstance
300
	 * @throws RemoteNotFoundException
301
	 * @throws UnknownRemoteException
302
	 */
303
	public function getCachedRemoteInstance(string $instance): RemoteInstance {
304
		$remoteInstance = $this->remoteRequest->getFromInstance($instance);
305
		if ($remoteInstance->getType() === RemoteInstance::TYPE_UNKNOWN) {
306
			throw new UnknownRemoteException($instance . ' is set as \'unknown\' in database');
307
		}
308
309
		return $remoteInstance;
310
	}
311
312
313
	/**
314
	 * Add a remote instance, based on the address
315
	 *
316
	 * @param string $instance
317
	 *
318
	 * @return RemoteInstance
319
	 * @throws RequestNetworkException
320
	 * @throws SignatoryException
321
	 * @throws SignatureException
322
	 * @throws WellKnownLinkNotFoundException
323
	 */
324
	public function retrieveRemoteInstance(string $instance): RemoteInstance {
325
		$resource = $this->getResourceData($instance, Application::APP_SUBJECT, Application::APP_REL);
326
327
		$remoteInstance = $this->retrieveSignatory($resource->g('id'), true);
328
		$remoteInstance->setInstance($instance);
329
330
		return $remoteInstance;
331
	}
332
333
334
	/**
335
	 * retrieve Signatory.
336
	 *
337
	 * @param string $keyId
338
	 * @param bool $refresh
339
	 *
340
	 * @return RemoteInstance
341
	 * @throws SignatoryException
342
	 * @throws SignatureException
343
	 */
344
	public function retrieveSignatory(string $keyId, bool $refresh = true): RemoteInstance {
345
		if (!$refresh) {
346
			try {
347
				return $this->remoteRequest->getFromHref(NC21Signatory::removeFragment($keyId));
348
			} catch (RemoteNotFoundException $e) {
349
				throw new SignatoryException();
350
			}
351
		}
352
353
		$remoteInstance = new RemoteInstance($keyId);
354
		$confirm = $this->uuid();
355
356
		$this->downloadSignatory($remoteInstance, $keyId, ['auth' => $confirm]);
357
		$remoteInstance->setUidFromKey();
358
359
		$this->confirmAuth($remoteInstance, $confirm);
360
361
		return $remoteInstance;
362
	}
363
364
365
	/**
366
	 * Add a remote instance, based on the address
367
	 *
368
	 * @param string $instance
369
	 * @param string $type
370
	 * @param bool $overwrite
371
	 *
372
	 * @throws RequestNetworkException
373
	 * @throws SignatoryException
374
	 * @throws SignatureException
375
	 * @throws WellKnownLinkNotFoundException
376
	 * @throws RemoteAlreadyExistsException
377
	 * @throws RemoteUidException
378
	 */
379
	public function addRemoteInstance(
380
		string $instance, string $type = RemoteInstance::TYPE_EXTERNAL, bool $overwrite = false
381
	): void {
382
		if ($this->configService->isLocalInstance($instance)) {
383
			throw new RemoteAlreadyExistsException('instance is local');
384
		}
385
386
		$remoteInstance = $this->retrieveRemoteInstance($instance);
387
		$remoteInstance->setType($type);
388
389
		try {
390
			$known = $this->remoteRequest->searchDuplicate($remoteInstance);
391
			if ($overwrite) {
392
				$this->remoteRequest->deleteById($known);
393
			} else {
394
				throw new RemoteAlreadyExistsException('instance is already known');
395
			}
396
		} catch (RemoteNotFoundException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
397
		}
398
399
		$this->remoteRequest->save($remoteInstance);
400
	}
401
402
403
	/**
404
	 * Confirm the Auth of a RemoteInstance, based on the result from a request
405
	 *
406
	 * @param RemoteInstance $remote
407
	 * @param string $auth
408
	 *
409
	 * @throws SignatureException
410
	 */
411
	private function confirmAuth(RemoteInstance $remote, string $auth): void {
412
		list($algo, $signed) = explode(':', $this->get('auth-signed', $remote->getOrigData()));
413
		try {
414
			if ($signed === null) {
415
				throw new SignatureException('invalid auth-signed');
416
			}
417
			$this->verifyString($auth, base64_decode($signed), $remote->getPublicKey(), $algo);
418
			$remote->setIdentityAuthed(true);
419
		} catch (SignatureException $e) {
420
			$this->e(
421
				$e,
422
				['auth' => $auth, 'signed' => $signed, 'signatory' => $remote, 'msg' => 'auth not confirmed']
423
			);
424
			throw new SignatureException('auth not confirmed');
425
		}
426
	}
427
428
429
	/**
430
	 * TODO: confirm if method is really needed
431
	 *
432
	 * @param RemoteInstance $remote
433
	 * @param RemoteInstance|null $stored
434
	 *
435
	 * @throws RemoteNotFoundException
436
	 * @throws RemoteUidException
437
	 */
438
	public function confirmValidRemote(RemoteInstance $remote, ?RemoteInstance &$stored = null): void {
439
		try {
440
			$stored = $this->remoteRequest->getFromHref($remote->getId());
441
		} catch (RemoteNotFoundException $e) {
442
			if ($remote->getInstance() === '') {
443
				throw new RemoteNotFoundException();
444
			}
445
446
			$stored = $this->remoteRequest->getFromInstance($remote->getInstance());
447
		}
448
449
		if ($stored->getUid() !== $remote->getUid(true)) {
450
			throw new RemoteUidException();
451
		}
452
	}
453
454
455
	/**
456
	 * TODO: check if this method is not useless
457
	 *
458
	 * @param RemoteInstance $remote
459
	 * @param string $update
460
	 *
461
	 * @throws RemoteUidException
462
	 */
463
	public function update(RemoteInstance $remote, string $update = self::UPDATE_DATA): void {
464
		switch ($update) {
465
			case self::UPDATE_DATA:
466
				$this->remoteRequest->update($remote);
467
				break;
468
469
			case self::UPDATE_TYPE:
470
				$this->remoteRequest->updateType($remote);
471
				break;
472
473
			case self::UPDATE_HREF:
474
				$this->remoteRequest->updateHref($remote);
475
				break;
476
477
			case self::UPDATE_INSTANCE:
478
				$this->remoteRequest->updateInstance($remote);
479
				break;
480
		}
481
	}
482
483
}
484
485