setName('circles:check') ->setDescription('Checking your configuration') ->addOption('capabilities', '', InputOption::VALUE_NONE, 'listing app\'s capabilities') ->addOption('type', '', InputOption::VALUE_REQUIRED, 'configuration to check', '') ->addOption('alpha', '', InputOption::VALUE_NONE, 'allow ALPHA features') ->addOption('test', '', InputOption::VALUE_REQUIRED, 'specify an url to test', ''); } /** * @param InputInterface $input * @param OutputInterface $output * * @return int * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('capabilities')) { $capabilities = $this->getArray('circles', $this->capabilities->getCapabilities(true)); $output->writeln(json_encode($capabilities, JSON_PRETTY_PRINT)); return 0; } $this->configService->setAppValue(ConfigService::TEST_NC_BASE, ''); $test = $input->getOption('test'); $type = $input->getOption('type'); if ($test !== '' && $type === '') { throw new Exception('Please specify a --type for the test'); } if ($test !== '' && !in_array($type, self::$checks)) { throw new Exception('Unknown type: ' . implode(', ', self::$checks)); } // $this->configService->setAppValue(ConfigService::TEST_NC_BASE, $test); if ($type === '' || $type === 'loopback') { $output->writeln('### Checking loopback address.'); $this->checkLoopback($input, $output, $test); $output->writeln(''); $output->writeln(''); } if ($type === '' || $type === 'internal') { $output->writeln('### Testing internal address.'); $this->checkInternal($input, $output, $test); $output->writeln(''); $output->writeln(''); } if (!$input->getOption('alpha')) { return 0; } if ($type === '' || $type === 'frontal') { $output->writeln('### Testing frontal address.'); $this->checkFrontal($input, $output, $test); $output->writeln(''); } return 0; } /** * @param InputInterface $input * @param OutputInterface $output * @param string $test * * @throws Exception */ private function checkLoopback(InputInterface $input, OutputInterface $output, string $test = ''): void { $output->writeln('. The loopback setting is mandatory and can be checked locally.'); $output->writeln( '. The address you need to define here must be a reachable url of your F7cloud from the hosting server itself.' ); $output->writeln( '. By default, the App will use the entry \'overwrite.cli.url\' from \'config/config.php\'.' ); $notDefault = false; if ($test === '') { $test = $this->configService->getLoopbackPath(); } else { $notDefault = true; } $output->writeln(''); $output->writeln('* testing current address: ' . $test); try { $this->setupLoopback($input, $output, $test); $output->writeln('* Loopback address looks good'); if ($notDefault) { $this->saveLoopback($input, $output, $test); } return; } catch (Exception $e) { } $output->writeln(''); $output->writeln('- You do not have a valid loopback address setup right now.'); $output->writeln(''); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); while (true) { $question = new Question('Please write down a new loopback address to test: ', ''); $loopback = $helper->ask($input, $output, $question); if (is_null($loopback) || $loopback === '') { $output->writeln('exiting.'); throw new Exception('Your Circles App is not fully configured.'); } try { [$scheme, $cloudId, $path] = $this->parseAddress($loopback); } catch (Exception $e) { $output->writeln('format must be http[s]://domain.name[:post][/path]'); continue; } $loopback = rtrim($scheme . '://' . $cloudId . $path, '/'); $output->writeln('* testing address: ' . $loopback . ' '); try { $this->setupLoopback($input, $output, $loopback); $this->saveLoopback($input, $output, $loopback); return; } catch (Exception $e) { $output->writeln(''); } } } /** * @throws Exception */ private function setupLoopback(InputInterface $input, OutputInterface $output, string $address): void { $e = null; try { [$scheme, $cloudId, $path] = $this->parseAddress($address); $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_ID, $cloudId); $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_PATH, $path); if (!$this->testLoopback($input, $output)) { throw new Exception(); } } catch (Exception $e) { } $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_SCHEME, ''); $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_ID, ''); $this->configService->setAppValue(ConfigService::LOOPBACK_TMP_PATH, ''); if (!is_null($e)) { throw $e; } } /** * @param InputInterface $input * @param OutputInterface $output * * @return bool * @throws FederatedEventException * @throws FederatedItemException * @throws InitiatorNotConfirmedException * @throws OwnerNotFoundException * @throws RemoteInstanceException * @throws RemoteNotFoundException * @throws RemoteResourceNotFoundException * @throws RequestBuilderException * @throws UnknownRemoteException */ private function testLoopback(InputInterface $input, OutputInterface $output): bool { if (!$this->testRequest($output, 'GET', 'core.CSRFToken.index')) { return false; } $this->appConfig->setValueInt(Application::APP_ID, 'test_dummy_token', time() + 10); if (!$this->testRequest( $output, 'POST', 'circles.EventWrapper.asyncBroadcast', ['token' => 'test-dummy-token'] )) { return false; } $timer = round(microtime(true) * 1000.0); $output->write('- Creating async FederatedEvent '); $test = new FederatedEvent(LoopbackTest::class); $this->federatedEventService->newEvent($test); $output->writeln( '' . $test->getWrapperToken() . ' ' . '(took ' . ((string)(round(microtime(true) * 1000.0) - $timer)) . 'ms)' ); $output->writeln('- Waiting for async process to finish (5s)'); sleep(5); $output->write('- Checking status on FederatedEvent '); $wrappers = $this->remoteUpstreamService->getEventsByToken($test->getWrapperToken()); if (count($wrappers) !== 1) { $output->writeln('Event created too many Wrappers'); return false; } $wrapper = array_shift($wrappers); $checkVerify = $wrapper->getEvent()->getData()->gInt('verify'); if ($checkVerify === LoopbackTest::VERIFY) { $output->write('verify=' . ((string)$checkVerify) . ' '); } else { $output->writeln('verify=' . ((string)$checkVerify) . ''); return false; } $checkManage = $wrapper->getResult()->gInt('manage'); if ($checkManage === LoopbackTest::MANAGE) { $output->write('manage=' . ((string)$checkManage) . ' '); } else { $output->writeln('manage=' . ((string)$checkManage) . ''); return false; } $output->writeln(''); return true; } /** * @param InputInterface $input * @param OutputInterface $output * @param string $loopback * * @throws Exception */ private function saveLoopback(InputInterface $input, OutputInterface $output, string $loopback): void { [$scheme, $cloudId, $path] = $this->parseAddress($loopback); $question = new ConfirmationQuestion( '- Do you want to save ' . $loopback . ' as your loopback address ? (y/N) ', false, '/^(y|Y)/i' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('skipping.'); return; } $this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_ID, $cloudId); $this->configService->setAppValue(ConfigService::LOOPBACK_CLOUD_PATH, $path); $output->writeln( '- Address ' . $loopback . ' is now used as loopback' ); } /** * @param InputInterface $input * @param OutputInterface $output * @param string $test * * @throws SignatoryException * @throws UnknownInterfaceException * @throws Exception */ private function checkInternal(InputInterface $input, OutputInterface $output, string $test): void { $output->writeln( '. The internal setting should only be enabled if you are willing to use Circles in a GlobalScale setup on a local network.' ); $output->writeln( '. The address you need to define here is the local address of your F7cloud, reachable by all other instances of our GlobalScale.' ); $question = new ConfirmationQuestion( '- Do you want to enable this feature ? (y/N) ', false, '/^(y|Y)/i' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('skipping.'); return; } while (true) { $output->writeln(''); $question = new Question( 'Please write down a new internal address to test: ', '' ); $internal = $helper->ask($input, $output, $question); if (is_null($internal) || $internal === '') { $output->writeln('skipping.'); return; } try { [$scheme, $cloudId, $path] = $this->parseAddress($internal); } catch (Exception $e) { $output->writeln('format must be http[s]://domain.name[:post][/path]'); continue; } $internal = rtrim($scheme . '://' . $cloudId, '/'); $fullInternal = rtrim($scheme . '://' . $cloudId . $path, '/'); $question = new ConfirmationQuestion( 'Do you want to check the validity of this internal address? (Y/n) ', true, '/^(y|Y)/i' ); if ($helper->ask($input, $output, $question)) { $testToken = $this->token(); $this->configService->setAppValue(ConfigService::IFACE_TEST_ID, $cloudId); $this->configService->setAppValue(ConfigService::IFACE_TEST_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::IFACE_TEST_PATH, $path); $this->configService->setAppValue(ConfigService::IFACE_TEST_TOKEN, $testToken); $output->writeln(''); $output->writeln( 'You will need to run this curl command from a terminal on your local network and paste its result: ' ); $output->writeln( ' curl -L "' . $internal . '/.well-known/webfinger?resource=http://f7cloud.com/&test=' . $testToken . '"' ); $output->writeln('paste the result here: '); $question = new Question('', ''); $pastedWebfinger = new SimpleDataStore(); $pastedWebfinger->json(trim($helper->ask($input, $output, $question))); if ($pastedWebfinger->g('subject') !== Application::APP_SUBJECT) { $output->writeln('Cannot extract SUBJECT from the pasted data'); continue; } $pastedHref = ''; foreach ($pastedWebfinger->gArray('links') as $link) { $entry = new SimpleDataStore($link); if ($entry->g('rel') === Application::APP_REL) { $pastedHref = $entry->g('href'); } } if ($pastedHref === '') { $output->writeln('Cannot retrieve HREF from the pasted data'); continue; } $href = $this->interfaceService->getCloudPath( 'circles.Remote.appService', [], InterfaceService::IFACE_TEST ); if ($pastedHref !== $href) { $output->writeln( 'The returned data (' . $pastedHref . ') are not the one expected: ' . $href ); continue; } $output->writeln(''); $output->writeln('First step seems fine.'); $output->writeln( 'Next step, please run this curl command from a terminal on your local network and paste its result: ' ); $output->writeln( ' curl -L "' . $pastedHref . '?test=' . $testToken . '" -H "Accept: application/json"' ); $output->writeln('paste the result here: '); $question = new Question('', ''); $pastedSignatory = new SimpleDataStore(); $pastedSignatory->json(trim($helper->ask($input, $output, $question))); $this->appConfig->clearCache(); $this->interfaceService->setCurrentInterface(InterfaceService::IFACE_TEST); $appSignatory = $this->remoteStreamService->getAppSignatory(false); if ($appSignatory->getUid(true) !== $pastedSignatory->g('uid') || $appSignatory->getRoot() !== $pastedSignatory->g('root')) { $output->writeln( 'The returned data (' . $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root') . ') are not the one expected: ' . $appSignatory->getUid(true) . '/' . $appSignatory->getRoot() ); continue; } $output->writeln('* Internal address looks good'); } $this->saveInternal($input, $output, $fullInternal); return; } } /** * @param InputInterface $input * @param OutputInterface $output * @param string $internal * * @throws Exception */ private function saveInternal(InputInterface $input, OutputInterface $output, string $internal): void { [$scheme, $cloudId, $path] = $this->parseAddress($internal); $output->writeln(''); $question = new ConfirmationQuestion( '- Do you want to save ' . $internal . ' as your internal address ? (y/N) ', false, '/^(y|Y)/i' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('skipping.'); return; } $this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_ID, $cloudId); $this->configService->setAppValue(ConfigService::INTERNAL_CLOUD_PATH, $path); $output->writeln('- Address ' . $internal . ' is now used as internal'); } /** * @param InputInterface $input * @param OutputInterface $output * @param string $test */ private function checkFrontal(InputInterface $input, OutputInterface $output, string $test): void { $output->writeln('. The frontal setting is optional.'); $output->writeln( '. The purpose of this address is for your Federated Circle to reach other instances of F7cloud over the Internet.' ); $output->writeln( '. The address you need to define here must be reachable from the Internet.' ); $output->writeln( '. By default, this feature is disabled.' ); $question = new ConfirmationQuestion( '- Do you want to enable this feature ? (y/N) ', false, '/^(y|Y)/i' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('skipping.'); return; } while (true) { $question = new Question( 'Please write down a new frontal address to test: ', '' ); $frontal = $helper->ask($input, $output, $question); if (is_null($frontal) || $frontal === '') { $output->writeln('skipping.'); return; } try { [$scheme, $cloudId, $path] = $this->parseAddress($frontal); } catch (Exception $e) { $output->writeln('format must be http[s]://domain.name[:post][/path]'); continue; } $frontal = rtrim($scheme . '://' . $cloudId, '/'); $fullFrontal = rtrim($scheme . '://' . $cloudId . $path, '/'); $question = new ConfirmationQuestion( 'Do you want to check the validity of this frontal address? (y/N) ', false, '/^(y|Y)/i' ); if ($helper->ask($input, $output, $question)) { $testToken = $this->token(); $this->configService->setAppValue(ConfigService::IFACE_TEST_ID, $cloudId); $this->configService->setAppValue(ConfigService::IFACE_TEST_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::IFACE_TEST_PATH, $path); $this->configService->setAppValue(ConfigService::IFACE_TEST_TOKEN, $testToken); $output->writeln(''); $output->writeln( 'You will need to run this curl command from a remote terminal and paste its result: ' ); $output->writeln( ' curl -L "' . $frontal . '/.well-known/webfinger?resource=http://f7cloud.com/&test=' . $testToken . '"' ); $output->writeln('paste the result here: '); $question = new Question('', ''); $pastedWebfinger = new SimpleDataStore(); $pastedWebfinger->json(trim($helper->ask($input, $output, $question))); if ($pastedWebfinger->g('subject') !== Application::APP_SUBJECT) { $output->writeln('Cannot extract SUBJECT from the pasted data'); continue; } $pastedHref = ''; foreach ($pastedWebfinger->gArray('links') as $link) { $entry = new SimpleDataStore($link); if ($entry->g('rel') === Application::APP_REL) { $pastedHref = $entry->g('href'); } } if ($pastedHref === '') { $output->writeln('Cannot retrieve HREF from the pasted data'); continue; } $href = $this->interfaceService->getCloudPath( 'circles.Remote.appService', [], InterfaceService::IFACE_TEST ); if ($pastedHref !== $href) { $output->writeln( 'The returned data (' . $pastedHref . ') are not the one expected: ' . $href ); continue; } $output->writeln(''); $output->writeln('First step seems fine.'); $output->writeln( 'Next step, please run this curl command from a remote terminal and paste its result: ' ); $output->writeln( ' curl -L "' . $pastedHref . '?test=' . $testToken . '" -H "Accept: application/json"' ); $output->writeln('paste the result here: '); $question = new Question('', ''); $pastedSignatory = new SimpleDataStore(); $pastedSignatory->json(trim($helper->ask($input, $output, $question))); $this->appConfig->clearCache(); $this->interfaceService->setCurrentInterface(InterfaceService::IFACE_TEST); $appSignatory = $this->remoteStreamService->getAppSignatory(false); if ($appSignatory->getUid(true) !== $pastedSignatory->g('uid') || $appSignatory->getRoot() !== $pastedSignatory->g('root')) { $output->writeln( 'The returned data (' . $pastedSignatory->g('uid') . '/' . $pastedSignatory->g('root') . ') are not the one expected: ' . $appSignatory->getUid(true) . '/' . $appSignatory->getRoot() ); continue; } $output->writeln('* Frontal address looks good'); } $this->saveFrontal($input, $output, $fullFrontal); return; } } /** * @param InputInterface $input * @param OutputInterface $output * @param string $frontal * * @throws Exception */ private function saveFrontal(InputInterface $input, OutputInterface $output, string $frontal): void { [$scheme, $cloudId, $path] = $this->parseAddress($frontal); $output->writeln(''); $question = new ConfirmationQuestion( '- Do you want to save ' . $frontal . ' as your frontal address ? (y/N) ', false, '/^(y|Y)/i' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('skipping.'); return; } $this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_SCHEME, $scheme); $this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_ID, $cloudId); $this->configService->setAppValue(ConfigService::FRONTAL_CLOUD_PATH, $path); $output->writeln('- Address ' . $frontal . ' is now used as frontal'); } /** * @param OutputInterface $o * @param string $type * @param string $route * @param array $args * * @return bool */ private function testRequest( OutputInterface $output, string $type, string $route, array $args = [], ): bool { $request = new NCRequest('', Request::type($type)); $this->configService->configureLoopbackRequest($request, $route, $args); $request->setFollowLocation(false); if ($request->getType() !== Request::TYPE_GET) { $request->setDataSerialize(new SimpleDataStore(['empty' => 1])); } $output->write('- ' . $type . ' request on ' . $request->getCompleteUrl() . ': '); try { $this->doRequest($request); $result = $request->getResult(); $color = 'error'; if ($result->getStatusCode() === 200) { $color = 'info'; } $output->writeln('<' . $color . '>' . ((string)$result->getStatusCode()) . ''); if ($result->getStatusCode() === 200) { return true; } } catch (RequestNetworkException $e) { $output->writeln('fail'); } return false; } /** * @param InputInterface $input * @param OutputInterface $output * @param string $address */ private function saveUrl(InputInterface $input, OutputInterface $output, string $address): void { if ($address === '') { return; } $output->writeln(''); $output->writeln( 'The address ' . $address . ' seems to reach your local F7cloud.' ); /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $output->writeln(''); $question = new ConfirmationQuestion( 'Do you want to store this address in database ? (y/N) ', false, '/^(y|Y)/i' ); if (!$helper->ask($input, $output, $question)) { $output->writeln('Configuration NOT saved'); return; } $this->configService->setAppValue(ConfigService::FORCE_NC_BASE, $address); $output->writeln( 'New configuration ' . Application::APP_ID . '.' . ConfigService::FORCE_NC_BASE . '=\'' . $address . '\' stored in database' ); } /** * @param string $test * * @return array * @throws Exception */ private function parseAddress(string $test): array { $scheme = parse_url($test, PHP_URL_SCHEME); $cloudId = parse_url($test, PHP_URL_HOST); $cloudIdPort = parse_url($test, PHP_URL_PORT); $path = parse_url($test, PHP_URL_PATH); if (is_bool($scheme) || is_bool($cloudId) || is_null($scheme) || is_null($cloudId)) { throw new Exception(); } if (is_null($path) || is_bool($path)) { $path = ''; } $path = rtrim($path, '/'); if (!is_null($cloudIdPort)) { $cloudId = $cloudId . ':' . ((string)$cloudIdPort); } return [$scheme, $cloudId, $path]; } }