Completed
Push — master ( c7d566...e2979b )
by Maxence
03:33 queued 01:09
created

CirclesRemote::syncGSInstance()   A

Complexity

Conditions 4
Paths 6

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 4
nc 6
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
32
namespace OCA\Circles\Command;
33
34
use daita\MySmallPhpTools\Exceptions\RequestNetworkException;
35
use daita\MySmallPhpTools\Exceptions\SignatoryException;
36
use daita\MySmallPhpTools\Exceptions\SignatureException;
37
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22Request;
38
use daita\MySmallPhpTools\Model\Nextcloud\nc22\NC22SignedRequest;
39
use daita\MySmallPhpTools\Traits\Nextcloud\nc22\TNC22WellKnown;
40
use daita\MySmallPhpTools\Traits\TStringTools;
41
use Exception;
42
use OC\Core\Command\Base;
43
use OCA\Circles\AppInfo\Application;
44
use OCA\Circles\Db\RemoteRequest;
45
use OCA\Circles\Exceptions\RemoteNotFoundException;
46
use OCA\Circles\Exceptions\RemoteUidException;
47
use OCA\Circles\Model\Federated\RemoteInstance;
48
use OCA\Circles\Service\ConfigService;
49
use OCA\Circles\Service\GlobalScaleService;
50
use OCA\Circles\Service\InterfaceService;
51
use OCA\Circles\Service\RemoteStreamService;
52
use Symfony\Component\Console\Helper\Table;
53
use Symfony\Component\Console\Input\InputArgument;
54
use Symfony\Component\Console\Input\InputInterface;
55
use Symfony\Component\Console\Input\InputOption;
56
use Symfony\Component\Console\Output\ConsoleOutput;
57
use Symfony\Component\Console\Output\OutputInterface;
58
use Symfony\Component\Console\Question\ConfirmationQuestion;
59
60
61
/**
62
 * Class CirclesRemote
63
 *
64
 * @package OCA\Circles\Command
65
 */
66
class CirclesRemote extends Base {
67
68
69
	use TNC22WellKnown;
70
	use TStringTools;
71
72
73
	/** @var RemoteRequest */
74
	private $remoteRequest;
75
76
	/** @var GlobalScaleService */
77
	private $globalScaleService;
78
79
	/** @var RemoteStreamService */
80
	private $remoteStreamService;
81
82
	/** @var InterfaceService */
83
	private $interfaceService;
84
85
	/** @var ConfigService */
86
	private $configService;
87
88
89
	/** @var InputInterface */
90
	private $input;
91
92
	/** @var OutputInterface */
93
	private $output;
94
95
96
	/**
97
	 * CirclesRemote constructor.
98
	 *
99
	 * @param RemoteRequest $remoteRequest
100
	 * @param GlobalScaleService $globalScaleService
101
	 * @param RemoteStreamService $remoteStreamService
102
	 * @param InterfaceService $interfaceService
103
	 * @param ConfigService $configService
104
	 */
105
	public function __construct(
106
		RemoteRequest $remoteRequest,
107
		GlobalScaleService $globalScaleService,
108
		RemoteStreamService $remoteStreamService,
109
		InterfaceService $interfaceService,
110
		ConfigService $configService
111
	) {
112
		parent::__construct();
113
114
		$this->remoteRequest = $remoteRequest;
115
		$this->globalScaleService = $globalScaleService;
116
		$this->remoteStreamService = $remoteStreamService;
117
		$this->interfaceService = $interfaceService;
118
		$this->configService = $configService;
119
120
		$this->setup('app', 'circles');
121
	}
122
123
124
	/**
125
	 *
126
	 */
127
	protected function configure() {
128
		parent::configure();
129
		$this->setName('circles:remote')
130
			 ->setDescription('remote features')
131
			 ->addArgument('host', InputArgument::OPTIONAL, 'host of the remote instance of Nextcloud')
132
			 ->addOption(
133
				 'type', '', InputOption::VALUE_REQUIRED, 'set type of remote', RemoteInstance::TYPE_UNKNOWN
134
			 )
135
			 ->addOption(
136
				 'iface', '', InputOption::VALUE_REQUIRED, 'set interface to use to contact remote',
137
				 InterfaceService::$LIST_IFACE[InterfaceService::IFACE_FRONTAL]
138
			 )
139
			 ->addOption('yes', '', InputOption::VALUE_NONE, 'silently add the remote instance')
140
			 ->addOption('all', '', InputOption::VALUE_NONE, 'display all information');
141
	}
142
143
144
	/**
145
	 * @param InputInterface $input
146
	 * @param OutputInterface $output
147
	 *
148
	 * @return int
149
	 * @throws Exception
150
	 */
151
	protected function execute(InputInterface $input, OutputInterface $output): int {
152
		$host = $input->getArgument('host');
153
154
		$this->input = $input;
155
		$this->output = $output;
156
157
		if ($host) {
158
			$this->requestInstance($host);
159
		} else {
160
			$this->checkKnownInstance();
161
		}
162
163
		return 0;
164
	}
165
166
167
	/**
168
	 * @param string $host
169
	 *
170
	 * @throws Exception
171
	 */
172
	private function requestInstance(string $host): void {
173
		$remoteType = $this->getRemoteType();
174
		$remoteIface = $this->getRemoteInterface();
175
		$this->interfaceService->setCurrentInterface($remoteIface);
176
177
		$webfinger = $this->getWebfinger($host, Application::APP_SUBJECT);
178 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
179
			$this->output->writeln('- Webfinger on <info>' . $host . '</info>');
180
			$this->output->writeln(json_encode($webfinger, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
181
			$this->output->writeln('');
182
		}
183
184 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
			$circleLink = $this->extractLink(Application::APP_REL, $webfinger);
186
			$this->output->writeln('- Information about Circles app on <info>' . $host . '</info>');
187
			$this->output->writeln(json_encode($circleLink, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
188
			$this->output->writeln('');
189
		}
190
191
		$this->output->writeln('- Available services on <info>' . $host . '</info>');
192
		foreach ($webfinger->getLinks() as $link) {
193
			$app = $link->getProperty('name');
194
			$ver = $link->getProperty('version');
195
			if ($app !== '') {
196
				$app .= ' ';
197
			}
198
			if ($ver !== '') {
199
				$ver = 'v' . $ver;
200
			}
201
202
			$this->output->writeln(' * ' . $link->getRel() . ' ' . $app . $ver);
203
		}
204
		$this->output->writeln('');
205
206
		$this->output->writeln('- Resources related to Circles on <info>' . $host . '</info>');
207
		$resource = $this->getResourceData($host, Application::APP_SUBJECT, Application::APP_REL);
208
		$this->output->writeln(json_encode($resource, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
209
		$this->output->writeln('');
210
211
212
		$tempUid = $resource->g('uid');
213
		$this->output->writeln(
214
			'- Confirming UID=' . $tempUid . ' from parsed Signatory at <info>' . $host . '</info>'
215
		);
216
217
		try {
218
			$remoteSignatory = $this->remoteStreamService->retrieveSignatory($resource->g('id'), true);
219
			$this->output->writeln(' * No SignatureException: <info>Identity authed</info>');
220
		} catch (SignatureException $e) {
221
			$this->output->writeln(
222
				'<error>' . $host . ' cannot auth its identity: ' . $e->getMessage() . '</error>'
223
			);
224
225
			return;
226
		}
227
228
		$this->output->writeln(' * Found <info>' . $remoteSignatory->getUid() . '</info>');
229
		if ($remoteSignatory->getUid(true) !== $tempUid) {
230
			$this->output->writeln('<error>looks like ' . $host . ' is faking its identity');
231
232
			return;
233
		}
234
235
		$this->output->writeln('');
236
237
		$testUrl = $resource->g('test');
238
		$this->output->writeln('- Testing signed payload on <info>' . $testUrl . '</info>');
239
240
		try {
241
			$localSignatory = $this->remoteStreamService->getAppSignatory();
242
		} catch (SignatoryException $e) {
243
			$this->output->writeln(
244
				'<error>Federated Circles not enabled locally. Please run ./occ circles:remote:init</error>'
245
			);
246
247
			return;
248
		}
249
250
		$payload = [
251
			'test'  => 42,
252
			'token' => $this->uuid()
253
		];
254
		$signedRequest = $this->outgoingTest($testUrl, $payload);
255
		$this->output->writeln(' * Payload: ');
256
		$this->output->writeln(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
257
		$this->output->writeln('');
258
259
		$this->output->writeln(' * Clear Signature: ');
260
		$this->output->writeln('<comment>' . $signedRequest->getClearSignature() . '</comment>');
261
		$this->output->writeln('');
262
263
		$this->output->writeln(' * Signed Signature (base64 encoded): ');
264
		$this->output->writeln(
265
			'<comment>' . base64_encode($signedRequest->getSignedSignature()) . '</comment>'
266
		);
267
		$this->output->writeln('');
268
269
		$result = $signedRequest->getOutgoingRequest()->getResult();
270
		$code = $result->getStatusCode();
271
		$this->output->writeln(' * Result: ' . (($code === 200) ? '<info>' . $code . '</info>' : $code));
272
		$this->output->writeln(
273
			json_encode(json_decode($result->getContent(), true), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
274
		);
275
		$this->output->writeln('');
276
277 View Code Duplication
		if ($this->input->getOption('all')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
278
			$this->output->writeln('');
279
			$this->output->writeln('<info>### Complete report ###</info>');
280
			$this->output->writeln(json_encode($signedRequest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
281
			$this->output->writeln('');
282
		}
283
284
		if ($remoteSignatory->getUid() !== $localSignatory->getUid()) {
285
			$remoteSignatory->setInstance($host)
286
							->setType($remoteType)
287
							->setInterface($remoteIface);
288
289
			try {
290
				$stored = new RemoteInstance();
291
				$this->remoteStreamService->confirmValidRemote($remoteSignatory, $stored);
292
				$this->output->writeln(
293
					'<info>The remote instance ' . $host
294
					. ' is already known with this current identity</info>'
295
				);
296
297 View Code Duplication
				if ($remoteSignatory->getType() !== $stored->getType()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
					$this->output->writeln(
299
						'- updating type from ' . $stored->getType() . ' to '
300
						. $remoteSignatory->getType()
301
					);
302
					$this->remoteStreamService->update(
303
						$remoteSignatory, RemoteStreamService::UPDATE_TYPE
304
					);
305
				}
306
307 View Code Duplication
				if ($remoteSignatory->getInstance() !== $stored->getInstance()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
308
					$this->output->writeln(
309
						'- updating host from ' . $stored->getInstance() . ' to '
310
						. $remoteSignatory->getInstance()
311
					);
312
					$this->remoteStreamService->update(
313
						$remoteSignatory, RemoteStreamService::UPDATE_INSTANCE
314
					);
315
				}
316 View Code Duplication
				if ($remoteSignatory->getId() !== $stored->getId()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
					$this->output->writeln(
318
						'- updating href/Id from ' . $stored->getId() . ' to '
319
						. $remoteSignatory->getId()
320
					);
321
					$this->remoteStreamService->update($remoteSignatory, RemoteStreamService::UPDATE_HREF);
322
				}
323
324
			} catch (RemoteUidException $e) {
325
				$this->updateRemote($remoteSignatory);
326
			} catch (RemoteNotFoundException $e) {
327
				$this->saveRemote($remoteSignatory);
328
			}
329
		}
330
331
	}
332
333
334
	/**
335
	 * @param RemoteInstance $remoteSignatory
336
	 *
337
	 * @throws RemoteUidException
338
	 */
339
	private function saveRemote(RemoteInstance $remoteSignatory) {
340
		$this->output->writeln('');
341
		$helper = $this->getHelper('question');
342
343
		$this->output->writeln(
344
			'The remote instance <info>' . $remoteSignatory->getInstance() . '</info> looks good.'
345
		);
346
		$question = new ConfirmationQuestion(
347
			'Would you like to identify this remote instance as \'<comment>' . $remoteSignatory->getType()
348
			. '</comment>\' using interface \'<comment>'
349
			. InterfaceService::$LIST_IFACE[$remoteSignatory->getInterface()]
350
			. '</comment>\' ? (y/N) ',
351
			false,
352
			'/^(y|Y)/i'
353
		);
354
355
		if ($this->input->getOption('yes') || $helper->ask($this->input, $this->output, $question)) {
356
			$this->remoteRequest->save($remoteSignatory);
357
			$this->output->writeln('<info>remote instance saved</info>');
358
		}
359
	}
360
361
362
	/**
363
	 * @param RemoteInstance $remoteSignatory
364
	 *
365
	 * @throws RemoteUidException
366
	 */
367
	private function updateRemote(RemoteInstance $remoteSignatory): void {
368
		$this->output->writeln('');
369
		$helper = $this->getHelper('question');
370
371
		$this->output->writeln(
372
			'The remote instance <info>' . $remoteSignatory->getInstance()
373
			. '</info> is known but <error>its identity has changed.</error>'
374
		);
375
		$this->output->writeln(
376
			'<comment>If you are not sure on why identity changed, please say No to the next question and contact the admin of the remote instance</comment>'
377
		);
378
		$question = new ConfirmationQuestion(
379
			'Do you consider this new identity as valid and update the entry in the database? (y/N) ',
380
			false,
381
			'/^(y|Y)/i'
382
		);
383
384
		if ($helper->ask($this->input, $this->output, $question)) {
385
			$this->remoteStreamService->update($remoteSignatory);
386
			$this->output->writeln('remote instance updated');
387
		}
388
	}
389
390
391
	/**
392
	 * @param string $remote
393
	 * @param array $payload
394
	 *
395
	 * @return NC22SignedRequest
396
	 * @throws RequestNetworkException
397
	 * @throws SignatoryException
398
	 */
399
	private function outgoingTest(string $remote, array $payload): NC22SignedRequest {
400
		$request = new NC22Request();
401
		$request->basedOnUrl($remote);
402
		$request->setFollowLocation(true);
403
		$request->setLocalAddressAllowed(true);
404
		$request->setTimeout(5);
405
		$request->setData($payload);
406
407
		$app = $this->remoteStreamService->getAppSignatory();
408
		$signedRequest = $this->remoteStreamService->signOutgoingRequest($request, $app);
409
		$this->doRequest($signedRequest->getOutgoingRequest());
410
411
		return $signedRequest;
412
	}
413
414
415
	/**
416
	 *
417
	 */
418
	private function checkKnownInstance(): void {
419
		$this->verifyGSInstances();
420
		$this->checkRemoteInstances();
421
	}
422
423
424
	/**
425
	 *
426
	 */
427
	private function verifyGSInstances(): void {
428
		$instances = $this->globalScaleService->getGlobalScaleInstances();
429
		$known = array_map(
430
			function(RemoteInstance $instance): string {
431
				return $instance->getInstance();
432
			}, $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBALSCALE)
433
		);
434
435
		$missing = array_diff($instances, $known);
436
		foreach ($missing as $instance) {
437
			$this->syncGSInstance($instance);
438
		}
439
	}
440
441
442
	/**
443
	 * @param string $instance
444
	 */
445
	private function syncGSInstance(string $instance): void {
446
		if ($this->configService->isLocalInstance($instance)) {
447
			return;
448
		}
449
450
		$this->output->write('Adding <comment>' . $instance . '</comment>: ');
451
		try {
452
			$this->remoteStreamService->addRemoteInstance(
453
				$instance,
454
				RemoteInstance::TYPE_GLOBALSCALE,
455
				InterfaceService::IFACE_INTERNAL,
456
				true
457
			);
458
			$this->output->writeln('<info>ok</info>');
459
		} catch (Exception $e) {
460
			$msg = ($e->getMessage() === '') ? '' : ' (' . $e->getMessage() . ')';
461
			$this->output->writeln('<error>' . get_class($e) . $msg . '</error>');
462
		}
463
	}
464
465
466
	private function checkRemoteInstances(): void {
467
		$instances = $this->remoteRequest->getAllInstances();
468
469
		$output = new ConsoleOutput();
470
		$output = $output->section();
471
		$table = new Table($output);
472
		$table->setHeaders(['instance', 'type', 'iface', 'UID', 'Authed']);
473
		$table->render();
474
475
		foreach ($instances as $instance) {
476
			try {
477
				$current = $this->remoteStreamService->retrieveRemoteInstance($instance->getInstance());
478
				if ($current->getUid(true) === $instance->getUid(true)) {
479
					$currentUid = '<info>' . $current->getUid(true) . '</info>';
480
				} else {
481
					$currentUid = '<error>' . $current->getUid(true) . '</error>';
482
				}
483
			} catch (Exception $e) {
484
				$currentUid = '<error>' . $e->getMessage() . '</error>';
485
			}
486
487
			$table->appendRow(
488
				[
489
					$instance->getInstance(),
490
					$instance->getType(),
491
					InterfaceService::$LIST_IFACE[$instance->getInterface()],
492
					$instance->getUid(),
493
					$currentUid
494
				]
495
			);
496
		}
497
	}
498
499
500
	/**
501
	 * @throws Exception
502
	 */
503 View Code Duplication
	private function getRemoteType(): string {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
504
		foreach (RemoteInstance::$LIST_TYPE as $type) {
505
			if (strtolower($this->input->getOption('type')) === strtolower($type)) {
506
				return $type;
507
			}
508
		}
509
510
		throw new Exception('Unknown type: ' . implode(', ', RemoteInstance::$LIST_TYPE));
511
	}
512
513
	/**
514
	 * @throws Exception
515
	 */
516 View Code Duplication
	private function getRemoteInterface(): int {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
517
		foreach (InterfaceService::$LIST_IFACE as $iface => $def) {
518
			if (strtolower($this->input->getOption('iface')) === strtolower($def)) {
519
				return $iface;
520
			}
521
		}
522
523
		throw new Exception('Unknown interface: ' . implode(', ', InterfaceService::$LIST_IFACE));
524
	}
525
526
}
527
528