Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(setupcheck): Check several config values to be valid URLs (and more) #45382

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading