Completed
Pull Request — master (#597)
by Maxence
07:37 queued 05:01
created

CirclesRemote::getRemoteType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.9666
c 0
b 0
f 0
cc 3
nc 3
nop 0
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\RemoteStreamService;
51
use Symfony\Component\Console\Helper\Table;
52
use Symfony\Component\Console\Input\InputArgument;
53
use Symfony\Component\Console\Input\InputInterface;
54
use Symfony\Component\Console\Input\InputOption;
55
use Symfony\Component\Console\Output\ConsoleOutput;
56
use Symfony\Component\Console\Output\OutputInterface;
57
use Symfony\Component\Console\Question\ConfirmationQuestion;
58
59
60
/**
61
 * Class CirclesRemote
62
 *
63
 * @package OCA\Circles\Command
64
 */
65
class CirclesRemote extends Base {
66
67
68
	use TNC22WellKnown;
69
	use TStringTools;
70
71
72
	/** @var RemoteRequest */
73
	private $remoteRequest;
74
75
	/** @var GlobalScaleService */
76
	private $globalScaleService;
77
78
	/** @var RemoteStreamService */
79
	private $remoteStreamService;
80
81
	/** @var ConfigService */
82
	private $configService;
83
84
85
	/** @var InputInterface */
86
	private $input;
87
88
	/** @var OutputInterface */
89
	private $output;
90
91
92
	/**
93
	 * CirclesRemote constructor.
94
	 *
95
	 * @param RemoteRequest $remoteRequest
96
	 * @param GlobalScaleService $globalScaleService
97
	 * @param RemoteStreamService $remoteStreamService
98
	 * @param ConfigService $configService
99
	 */
100
	public function __construct(
101
		RemoteRequest $remoteRequest, GlobalScaleService $globalScaleService,
102
		RemoteStreamService $remoteStreamService,
103
		ConfigService $configService
104
	) {
105
		parent::__construct();
106
107
		$this->remoteRequest = $remoteRequest;
108
		$this->globalScaleService = $globalScaleService;
109
		$this->remoteStreamService = $remoteStreamService;
110
		$this->configService = $configService;
111
112
		$this->setup('app', 'circles');
113
	}
114
115
116
	/**
117
	 *
118
	 */
119
	protected function configure() {
120
		parent::configure();
121
		$this->setName('circles:remote')
122
			 ->setDescription('remote features')
123
			 ->addArgument('host', InputArgument::OPTIONAL, 'host of the remote instance of Nextcloud')
124
			 ->addOption(
125
				 'type', '', InputOption::VALUE_REQUIRED, 'set type of remote', RemoteInstance::TYPE_UNKNOWN
126
			 )
127
			 ->addOption(
128
				 'iface', '', InputOption::VALUE_REQUIRED, 'set interface to use to contact remote',
129
				 RemoteInstance::$LIST_IFACE[RemoteInstance::IFACE_FRONTAL]
130
			 )
131
			 ->addOption('yes', '', InputOption::VALUE_NONE, 'silently add the remote instance')
132
			 ->addOption('all', '', InputOption::VALUE_NONE, 'display all information');
133
	}
134
135
136
	/**
137
	 * @param InputInterface $input
138
	 * @param OutputInterface $output
139
	 *
140
	 * @return int
141
	 * @throws Exception
142
	 */
143
	protected function execute(InputInterface $input, OutputInterface $output): int {
144
		$host = $input->getArgument('host');
145
146
		$this->input = $input;
147
		$this->output = $output;
148
149
		if ($host) {
150
			$this->requestInstance($host);
151
		} else {
152
			$this->checkKnownInstance();
153
		}
154
155
		return 0;
156
	}
157
158
159
	/**
160
	 * @param string $host
161
	 *
162
	 * @throws Exception
163
	 */
164
	private function requestInstance(string $host): void {
165
		$remoteType = $this->getRemoteType();
166
		$remoteIface = $this->getRemoteInterface();
167
168
		$webfinger = $this->getWebfinger($host, Application::APP_SUBJECT);
169 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...
170
			$this->output->writeln('- Webfinger on <info>' . $host . '</info>');
171
			$this->output->writeln(json_encode($webfinger, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
172
			$this->output->writeln('');
173
		}
174
175 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...
176
			$circleLink = $this->extractLink(Application::APP_REL, $webfinger);
177
			$this->output->writeln('- Information about Circles app on <info>' . $host . '</info>');
178
			$this->output->writeln(json_encode($circleLink, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
179
			$this->output->writeln('');
180
		}
181
182
		$this->output->writeln('- Available services on <info>' . $host . '</info>');
183
		foreach ($webfinger->getLinks() as $link) {
184
			$app = $link->getProperty('name');
185
			$ver = $link->getProperty('version');
186
			if ($app !== '') {
187
				$app .= ' ';
188
			}
189
			if ($ver !== '') {
190
				$ver = 'v' . $ver;
191
			}
192
193
			$this->output->writeln(' * ' . $link->getRel() . ' ' . $app . $ver);
194
		}
195
		$this->output->writeln('');
196
197
		$this->output->writeln('- Resources related to Circles on <info>' . $host . '</info>');
198
		$resource = $this->getResourceData($host, Application::APP_SUBJECT, Application::APP_REL);
199
		$this->output->writeln(json_encode($resource, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
200
		$this->output->writeln('');
201
202
203
		$tempUid = $resource->g('uid');
204
		$this->output->writeln(
205
			'- Confirming UID=' . $tempUid . ' from parsed Signatory at <info>' . $host . '</info>'
206
		);
207
208
		try {
209
			$remoteSignatory = $this->remoteStreamService->retrieveSignatory($resource->g('id'), true);
210
			$this->output->writeln(' * No SignatureException: <info>Identity authed</info>');
211
		} catch (SignatureException $e) {
212
			$this->output->writeln(
213
				'<error>' . $host . ' cannot auth its identity: ' . $e->getMessage() . '</error>'
214
			);
215
216
			return;
217
		}
218
219
		$this->output->writeln(' * Found <info>' . $remoteSignatory->getUid() . '</info>');
220
		if ($remoteSignatory->getUid(true) !== $tempUid) {
221
			$this->output->writeln('<error>looks like ' . $host . ' is faking its identity');
222
223
			return;
224
		}
225
226
		$this->output->writeln('');
227
228
		$testUrl = $resource->g('test');
229
		$this->output->writeln('- Testing signed payload on <info>' . $testUrl . '</info>');
230
231
		try {
232
			$localSignatory = $this->remoteStreamService->getAppSignatory();
233
		} catch (SignatoryException $e) {
234
			$this->output->writeln(
235
				'<error>Federated Circles not enabled locally. Please run ./occ circles:remote:init</error>'
236
			);
237
238
			return;
239
		}
240
241
		$payload = [
242
			'test'  => 42,
243
			'token' => $this->uuid()
244
		];
245
		$signedRequest = $this->outgoingTest($testUrl, $payload);
246
		$this->output->writeln(' * Payload: ');
247
		$this->output->writeln(json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
248
		$this->output->writeln('');
249
250
		$this->output->writeln(' * Clear Signature: ');
251
		$this->output->writeln('<comment>' . $signedRequest->getClearSignature() . '</comment>');
252
		$this->output->writeln('');
253
254
		$this->output->writeln(' * Signed Signature (base64 encoded): ');
255
		$this->output->writeln(
256
			'<comment>' . base64_encode($signedRequest->getSignedSignature()) . '</comment>'
257
		);
258
		$this->output->writeln('');
259
260
		$result = $signedRequest->getOutgoingRequest()->getResult();
261
		$code = $result->getStatusCode();
262
		$this->output->writeln(' * Result: ' . (($code === 200) ? '<info>' . $code . '</info>' : $code));
263
		$this->output->writeln(
264
			json_encode(json_decode($result->getContent(), true), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
265
		);
266
		$this->output->writeln('');
267
268 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...
269
			$this->output->writeln('');
270
			$this->output->writeln('<info>### Complete report ###</info>');
271
			$this->output->writeln(json_encode($signedRequest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
272
			$this->output->writeln('');
273
		}
274
275
		if ($remoteSignatory->getUid() !== $localSignatory->getUid()) {
276
			$remoteSignatory->setInstance($host)
277
							->setType($remoteType)
278
							->setInterface($remoteIface);
279
280
			try {
281
				$stored = new RemoteInstance();
282
				$this->remoteStreamService->confirmValidRemote($remoteSignatory, $stored);
283
				$this->output->writeln(
284
					'<info>The remote instance ' . $host
285
					. ' is already known with this current identity</info>'
286
				);
287
288 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...
289
					$this->output->writeln(
290
						'- updating type from ' . $stored->getType() . ' to '
291
						. $remoteSignatory->getType()
292
					);
293
					$this->remoteStreamService->update(
294
						$remoteSignatory, RemoteStreamService::UPDATE_TYPE
295
					);
296
				}
297
298 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...
299
					$this->output->writeln(
300
						'- updating host from ' . $stored->getInstance() . ' to '
301
						. $remoteSignatory->getInstance()
302
					);
303
					$this->remoteStreamService->update(
304
						$remoteSignatory, RemoteStreamService::UPDATE_INSTANCE
305
					);
306
				}
307 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...
308
					$this->output->writeln(
309
						'- updating href/Id from ' . $stored->getId() . ' to '
310
						. $remoteSignatory->getId()
311
					);
312
					$this->remoteStreamService->update($remoteSignatory, RemoteStreamService::UPDATE_HREF);
313
				}
314
315
			} catch (RemoteUidException $e) {
316
				$this->updateRemote($remoteSignatory);
317
			} catch (RemoteNotFoundException $e) {
318
				$this->saveRemote($remoteSignatory);
319
			}
320
		}
321
322
	}
323
324
325
	/**
326
	 * @param RemoteInstance $remoteSignatory
327
	 *
328
	 * @throws RemoteUidException
329
	 */
330
	private function saveRemote(RemoteInstance $remoteSignatory) {
331
		$this->output->writeln('');
332
		$helper = $this->getHelper('question');
333
334
		$this->output->writeln(
335
			'The remote instance <info>' . $remoteSignatory->getInstance() . '</info> looks good.'
336
		);
337
		$question = new ConfirmationQuestion(
338
			'Would you like to identify this remote instance as \'<comment>' . $remoteSignatory->getType()
339
			. '</comment>\' using interface \'<comment>' . RemoteInstance::$LIST_IFACE[$remoteSignatory->getInterface()]
340
			. '</comment>\' ? (y/N) ',
341
			false,
342
			'/^(y|Y)/i'
343
		);
344
345
		if ($this->input->getOption('yes') || $helper->ask($this->input, $this->output, $question)) {
346
			$this->remoteRequest->save($remoteSignatory);
347
			$this->output->writeln('<info>remote instance saved</info>');
348
		}
349
	}
350
351
352
	/**
353
	 * @param RemoteInstance $remoteSignatory
354
	 *
355
	 * @throws RemoteUidException
356
	 */
357
	private function updateRemote(RemoteInstance $remoteSignatory): void {
358
		$this->output->writeln('');
359
		$helper = $this->getHelper('question');
360
361
		$this->output->writeln(
362
			'The remote instance <info>' . $remoteSignatory->getInstance()
363
			. '</info> is known but <error>its identity has changed.</error>'
364
		);
365
		$this->output->writeln(
366
			'<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>'
367
		);
368
		$question = new ConfirmationQuestion(
369
			'Do you consider this new identity as valid and update the entry in the database? (y/N) ',
370
			false,
371
			'/^(y|Y)/i'
372
		);
373
374
		if ($helper->ask($this->input, $this->output, $question)) {
375
			$this->remoteStreamService->update($remoteSignatory);
376
			$this->output->writeln('remote instance updated');
377
		}
378
	}
379
380
381
	/**
382
	 * @param string $remote
383
	 * @param array $payload
384
	 *
385
	 * @return NC22SignedRequest
386
	 * @throws RequestNetworkException
387
	 * @throws SignatoryException
388
	 */
389
	private function outgoingTest(string $remote, array $payload): NC22SignedRequest {
390
		$request = new NC22Request();
391
		$request->basedOnUrl($remote);
392
		$request->setFollowLocation(true);
393
		$request->setLocalAddressAllowed(true);
394
		$request->setTimeout(5);
395
		$request->setData($payload);
396
397
		$app = $this->remoteStreamService->getAppSignatory();
398
		$signedRequest = $this->remoteStreamService->signOutgoingRequest($request, $app);
399
		$this->doRequest($signedRequest->getOutgoingRequest());
400
401
		return $signedRequest;
402
	}
403
404
405
	/**
406
	 *
407
	 */
408
	private function checkKnownInstance(): void {
409
		$this->verifyGSInstances();
410
		$this->checkRemoteInstances();
411
	}
412
413
414
	/**
415
	 *
416
	 */
417
	private function verifyGSInstances(): void {
418
		$instances = $this->globalScaleService->getGlobalScaleInstances();
419
		$known = array_map(
420
			function(RemoteInstance $instance): string {
421
				return $instance->getInstance();
422
			}, $this->remoteRequest->getFromType(RemoteInstance::TYPE_GLOBALSCALE)
423
		);
424
425
		$missing = array_diff($instances, $known);
426
		foreach ($missing as $instance) {
427
			$this->syncGSInstance($instance);
428
		}
429
	}
430
431
432
	/**
433
	 * @param string $instance
434
	 */
435
	private function syncGSInstance(string $instance): void {
436
		if ($this->configService->isLocalInstance($instance)) {
437
			return;
438
		}
439
		$this->output->write('Adding <comment>' . $instance . '</comment>: ');
440
		try {
441
			$this->remoteStreamService->addRemoteInstance(
442
				$instance,
443
				RemoteInstance::TYPE_GLOBALSCALE,
444
				RemoteInstance::IFACE_INTERNAL,
445
				true
446
			);
447
			$this->output->writeln('<info>ok</info>');
448
		} catch (Exception $e) {
449
			$msg = ($e->getMessage() === '') ? '' : ' (' . $e->getMessage() . ')';
450
			$this->output->writeln('<error>' . get_class($e) . $msg . '</error>');
451
		}
452
	}
453
454
455
	private function checkRemoteInstances(): void {
456
		$instances = $this->remoteRequest->getAllInstances();
457
458
		$output = new ConsoleOutput();
459
		$output = $output->section();
460
		$table = new Table($output);
461
		$table->setHeaders(['instance', 'type', 'iface', 'UID', 'Authed']);
462
		$table->render();
463
464
		foreach ($instances as $instance) {
465
			try {
466
				$current = $this->remoteStreamService->retrieveRemoteInstance($instance->getInstance());
467
				if ($current->getUid(true) === $instance->getUid(true)) {
468
					$currentUid = '<info>' . $current->getUid(true) . '</info>';
469
				} else {
470
					$currentUid = '<error>' . $current->getUid(true) . '</error>';
471
				}
472
			} catch (Exception $e) {
473
				$currentUid = '<error>' . $e->getMessage() . '</error>';
474
			}
475
476
			$table->appendRow(
477
				[
478
					$instance->getInstance(),
479
					$instance->getType(),
480
					RemoteInstance::$LIST_IFACE[$instance->getInterface()],
481
					$instance->getUid(),
482
					$currentUid
483
				]
484
			);
485
		}
486
	}
487
488
489
	/**
490
	 * @throws Exception
491
	 */
492 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...
493
		foreach (RemoteInstance::$LIST_TYPE as $type) {
494
			if (strtolower($this->input->getOption('type')) === strtolower($type)) {
495
				return $type;
496
			}
497
		}
498
499
		throw new Exception('Unknown type: ' . implode(', ', RemoteInstance::$LIST_TYPE));
500
	}
501
502
	/**
503
	 * @throws Exception
504
	 */
505 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...
506
		foreach (RemoteInstance::$LIST_IFACE as $iface => $def) {
507
			if (strtolower($this->input->getOption('iface')) === strtolower($def)) {
508
				return $iface;
509
			}
510
		}
511
512
		throw new Exception('Unknown interface: ' . implode(', ', RemoteInstance::$LIST_IFACE));
513
	}
514
515
}
516
517