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

RemoteStreamService::resultRequestRemoteInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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