Skip to content

Commit

Permalink
feat(setupcheck): Check several config values to be valid URLs (and m…
Browse files Browse the repository at this point in the history
…ore)

Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed May 16, 2024
1 parent 7bc4ccb commit 38912df
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 2 deletions.
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
'OCA\\Settings\\SetupChecks\\CheckServerResponseTrait' => $baseDir . '/../lib/SetupChecks/CheckServerResponseTrait.php',
'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => $baseDir . '/../lib/SetupChecks/CheckUserCertificates.php',
'OCA\\Settings\\SetupChecks\\CodeIntegrity' => $baseDir . '/../lib/SetupChecks/CodeIntegrity.php',
'OCA\\Settings\\SetupChecks\\ConfigValidation' => $baseDir . '/../lib/SetupChecks/ConfigValidation.php',
'OCA\\Settings\\SetupChecks\\CronErrors' => $baseDir . '/../lib/SetupChecks/CronErrors.php',
'OCA\\Settings\\SetupChecks\\CronInfo' => $baseDir . '/../lib/SetupChecks/CronInfo.php',
'OCA\\Settings\\SetupChecks\\DataDirectoryProtected' => $baseDir . '/../lib/SetupChecks/DataDirectoryProtected.php',
Expand Down
1 change: 1 addition & 0 deletions apps/settings/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\SetupChecks\\CheckServerResponseTrait' => __DIR__ . '/..' . '/../lib/SetupChecks/CheckServerResponseTrait.php',
'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => __DIR__ . '/..' . '/../lib/SetupChecks/CheckUserCertificates.php',
'OCA\\Settings\\SetupChecks\\CodeIntegrity' => __DIR__ . '/..' . '/../lib/SetupChecks/CodeIntegrity.php',
'OCA\\Settings\\SetupChecks\\ConfigValidation' => __DIR__ . '/..' . '/../lib/SetupChecks/ConfigValidation.php',
'OCA\\Settings\\SetupChecks\\CronErrors' => __DIR__ . '/..' . '/../lib/SetupChecks/CronErrors.php',
'OCA\\Settings\\SetupChecks\\CronInfo' => __DIR__ . '/..' . '/../lib/SetupChecks/CronInfo.php',
'OCA\\Settings\\SetupChecks\\DataDirectoryProtected' => __DIR__ . '/..' . '/../lib/SetupChecks/DataDirectoryProtected.php',
Expand Down
4 changes: 2 additions & 2 deletions apps/settings/composer/composer/installed.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'reference' => '7bc4ccba6ad85c31a1e500023a9514059d584548',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand All @@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '4ff660ca2e0baa02440ba07296ed7e75fa544c0e',
'reference' => '7bc4ccba6ad85c31a1e500023a9514059d584548',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand Down
2 changes: 2 additions & 0 deletions apps/settings/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use OCA\Settings\SetupChecks\BruteForceThrottler;
use OCA\Settings\SetupChecks\CheckUserCertificates;
use OCA\Settings\SetupChecks\CodeIntegrity;
use OCA\Settings\SetupChecks\ConfigValidation;
use OCA\Settings\SetupChecks\CronErrors;
use OCA\Settings\SetupChecks\CronInfo;
use OCA\Settings\SetupChecks\DatabaseHasMissingColumns;
Expand Down Expand Up @@ -182,6 +183,7 @@ public function register(IRegistrationContext $context): void {
$context->registerSetupCheck(BruteForceThrottler::class);
$context->registerSetupCheck(CheckUserCertificates::class);
$context->registerSetupCheck(CodeIntegrity::class);
$context->registerSetupCheck(ConfigValidation::class);
$context->registerSetupCheck(CronErrors::class);
$context->registerSetupCheck(CronInfo::class);
$context->registerSetupCheck(DatabaseHasMissingColumns::class);
Expand Down
230 changes: 230 additions & 0 deletions apps/settings/lib/SetupChecks/ConfigValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Settings\SetupChecks;

use OCP\IConfig;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;

class ConfigValidation implements ISetupCheck {
public const FAKE_DEFAULT_VALUE = '#$HOPEFULLY_NOONE_USES_THIS_DEFAULT_VALUE$#';

public function __construct(
private IL10N $l10n,
private IConfig $config,
) {
}

public function getCategory(): string {
return 'config';
}

public function getName(): string {
return $this->l10n->t('Config validation');
}

public function run(): SetupResult {
$proxyuserpwd = $this->config->getSystemValue('proxyuserpwd', self::FAKE_DEFAULT_VALUE);
if ($proxyuserpwd !== self::FAKE_DEFAULT_VALUE) {
if (!is_string($proxyuserpwd)) {
return SetupResult::error($this->l10n->t('Config value %s is not a string value.', ['proxyuserpwd']));
}

$parts = array_filter(explode(':', $proxyuserpwd));
if (count($parts) !== 2) {
return SetupResult::error($this->l10n->t('Config value %s is not well formatted, expected format: non-empty-string, colon, non-empty-string (sample: "user:password").', ['proxyuserpwd']));
}
}

$overwritehost = $this->config->getSystemValue('overwritehost', self::FAKE_DEFAULT_VALUE);
if ($overwritehost !== self::FAKE_DEFAULT_VALUE) {
$return = $this->checkDomainOnly('overwritehost', $overwritehost);
if ($return) {
return $return;
}
}

$trustedDomains = $this->config->getSystemValue('trusted_domains', self::FAKE_DEFAULT_VALUE);
if ($trustedDomains !== self::FAKE_DEFAULT_VALUE) {
if (!is_array($trustedDomains)) {
return SetupResult::error($this->l10n->t('Config value %s must be a list of strings with at least 1 entry.', ['trusted_domains']));
}

foreach ($trustedDomains as $key => $trustedDomain) {
if (!is_int($key)) {
return SetupResult::error($this->l10n->t('Config value %s must be a list of strings, but found a non-numeric key.', ['trusted_domains']));
}

// Resolve wildcards
$trustedDomain = str_replace('*', '1', $trustedDomain);

$return = $this->checkDomainWithPort('trusted_domains => ' . $key, $trustedDomain);
if ($return) {
return $return;
}
}
} else {
return SetupResult::error($this->l10n->t('Config value %s must be a list of strings with at least 1 entry.', ['trusted_domains']));
}

$trustedProxies = $this->config->getSystemValue('trusted_proxies', self::FAKE_DEFAULT_VALUE);
if ($trustedProxies !== self::FAKE_DEFAULT_VALUE) {
if (!is_array($trustedProxies)) {
return SetupResult::error($this->l10n->t('Config value %s must be a list of strings.', ['trusted_proxies']));
}

foreach ($trustedProxies as $key => $trustedProxy) {
if (!is_int($key)) {
return SetupResult::error($this->l10n->t('Config value %s must be a list of strings, but found a non-numeric key.', ['trusted_proxies']));
}

$return = $this->checkIPOrIPRange('trusted_proxies => ' . $key, $trustedProxy);
if ($return) {
return $return;
}
}
}

$fullURLConfigs = [
'overwrite.cli.url',
'lost_password_link',
'updater.server.url',
'logo_url',
'appstoreurl',
'upgrade.cli-upgrade-link',
'preview_imaginary_url',
'lookup_server',
'customclient_desktop',
'customclient_android',
'customclient_ios',
];
foreach ($fullURLConfigs as $configKey) {
$url = $this->config->getSystemValue($configKey, self::FAKE_DEFAULT_VALUE);
if ($url === self::FAKE_DEFAULT_VALUE) {
continue;
}

$parsed = parse_url($url);
if ($parsed === false) {
return SetupResult::error($this->l10n->t('Config value %s is not a valid URL.', [$configKey]));
}

$scheme = $parsed['scheme'] ?? '';
if ($scheme !== 'https' && $scheme !== 'http') {
return SetupResult::error($this->l10n->t('Config value %s should be a full URL but has no protocol.', [$configKey]));
}

if (isset($parsed['user']) || isset($parsed['pass'])) {
return SetupResult::error($this->l10n->t('Config value %s should not contain user nor password.', [$configKey]));
}
}

$booleanConfigs = [
'auth.bruteforce.protection.enabled' => true,
'auth.bruteforce.protection.testing' => false,
'ratelimit.protection.enabled' => true,
];

foreach ($booleanConfigs as $configKey => $expectedValue) {
$result = $this->checkBoolean($configKey, $expectedValue);
if ($result) {
return $result;
}
}

return SetupResult::success($this->l10n->t('All configuration values look OK'));
}

protected function checkBoolean(string $configKey, bool $shouldBe): ?SetupResult {
$value = $this->config->getSystemValue($configKey, self::FAKE_DEFAULT_VALUE);
if ($value === self::FAKE_DEFAULT_VALUE) {
return null;
}

if (!is_bool($value)) {
return SetupResult::error($this->l10n->t('Config value %s is not a boolean value.', [$configKey]));
}

if ($value !== $shouldBe) {
return SetupResult::warning($this->l10n->t('Config value %s should be %s in production instances.', [$configKey, json_encode($shouldBe)]));
}
return null;
}

protected function checkDomainOnly(string $configKey, mixed $configValue): ?SetupResult {
if (!is_string($configValue)) {
return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey]));
}

if (str_starts_with($configValue, 'http:') || str_starts_with($configValue, 'https:')) {
return SetupResult::error($this->l10n->t('Config value %s should not contain a protocol.', [$configKey]));
}

$url = 'https://' . $configValue;
$parts = parse_url($url);
if (!isset($parts['scheme'], $parts['host']) || count($parts) !== 2) {
return SetupResult::error($this->l10n->t('Config value %s should only contain a domain.', [$configKey]));
}

return null;
}

protected function checkDomainWithPort(string $configKey, mixed $configValue): ?SetupResult {
if (!is_string($configValue)) {
return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey]));
}

if (str_starts_with($configValue, 'http:') || str_starts_with($configValue, 'https:')) {
return SetupResult::error($this->l10n->t('Config value %s should not contain a protocol.', [$configKey]));
}

$url = 'https://' . $configValue;
$parts = parse_url($url);
if ($parts === false || !isset($parts['scheme'], $parts['host']) || (count($parts) !== 2 && count($parts) !== 3)) {
return SetupResult::error($this->l10n->t('Config value %s should contain a domain or a domain and port.', [$configKey]));
}
if (!isset($parts['port']) && count($parts) === 3) {
return SetupResult::error($this->l10n->t('Config value %s should only contain a domain or domain and port.', [$configKey]));
}

return null;
}

protected function checkIPOrIPRange(string $configKey, mixed $configValue): ?SetupResult {
if (!is_string($configValue)) {
return SetupResult::error($this->l10n->t('Config value %s is not a string value.', [$configKey]));
}

$parts = explode('/', $configValue);
if (count($parts) > 2) {
return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey]));
}

if (count($parts) === 1) {
if (filter_var($parts[0], FILTER_VALIDATE_IP) === false) {
return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey]));
}
return null;
}

if (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
if (!is_numeric($parts[1]) || $parts[1] > 32 || $parts[1] < 1) {

Check notice

Code scanning / Psalm

PossiblyUndefinedArrayOffset Note

Possibly undefined array key $parts[1] on list{0: string, 1?: string}
return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey]));
}
} elseif (filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
if (!is_numeric($parts[1]) || $parts[1] > 128 || $parts[1] < 1) {

Check notice

Code scanning / Psalm

PossiblyUndefinedArrayOffset Note

Possibly undefined array key $parts[1] on list{0: string, 1?: string}
return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey]));
}
} else {
return SetupResult::error($this->l10n->t('Config value %s is not an valid IP address or IP range.', [$configKey]));
}
return null;
}
}

0 comments on commit 38912df

Please sign in to comment.