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

CirclesRemote::execute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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