diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 41cfcd4339d..4cc6d25dff6 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -132,6 +132,7 @@ return array( 'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => $baseDir . '/../lib/SetupChecks/ReadOnlyConfig.php', 'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => $baseDir . '/../lib/SetupChecks/SchedulingTableSize.php', 'OCA\\Settings\\SetupChecks\\SecurityHeaders' => $baseDir . '/../lib/SetupChecks/SecurityHeaders.php', + 'OCA\\Settings\\SetupChecks\\ServerIdConfig' => $baseDir . '/../lib/SetupChecks/ServerIdConfig.php', 'OCA\\Settings\\SetupChecks\\SupportedDatabase' => $baseDir . '/../lib/SetupChecks/SupportedDatabase.php', 'OCA\\Settings\\SetupChecks\\SystemIs64bit' => $baseDir . '/../lib/SetupChecks/SystemIs64bit.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => $baseDir . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index edda60d4e82..d51d1785463 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -147,6 +147,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ReadOnlyConfig.php', 'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => __DIR__ . '/..' . '/../lib/SetupChecks/SchedulingTableSize.php', 'OCA\\Settings\\SetupChecks\\SecurityHeaders' => __DIR__ . '/..' . '/../lib/SetupChecks/SecurityHeaders.php', + 'OCA\\Settings\\SetupChecks\\ServerIdConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ServerIdConfig.php', 'OCA\\Settings\\SetupChecks\\SupportedDatabase' => __DIR__ . '/..' . '/../lib/SetupChecks/SupportedDatabase.php', 'OCA\\Settings\\SetupChecks\\SystemIs64bit' => __DIR__ . '/..' . '/../lib/SetupChecks/SystemIs64bit.php', 'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => __DIR__ . '/..' . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php', diff --git a/apps/settings/lib/AppInfo/Application.php b/apps/settings/lib/AppInfo/Application.php index de007a6978f..7a3f2a9316f 100644 --- a/apps/settings/lib/AppInfo/Application.php +++ b/apps/settings/lib/AppInfo/Application.php @@ -69,6 +69,7 @@ use OCA\Settings\SetupChecks\RandomnessSecure; use OCA\Settings\SetupChecks\ReadOnlyConfig; use OCA\Settings\SetupChecks\SchedulingTableSize; use OCA\Settings\SetupChecks\SecurityHeaders; +use OCA\Settings\SetupChecks\ServerIdConfig; use OCA\Settings\SetupChecks\SupportedDatabase; use OCA\Settings\SetupChecks\SystemIs64bit; use OCA\Settings\SetupChecks\TaskProcessingPickupSpeed; @@ -207,6 +208,7 @@ class Application extends App implements IBootstrap { $context->registerSetupCheck(RandomnessSecure::class); $context->registerSetupCheck(ReadOnlyConfig::class); $context->registerSetupCheck(SecurityHeaders::class); + $context->registerSetupCheck(ServerIdConfig::class); $context->registerSetupCheck(SchedulingTableSize::class); $context->registerSetupCheck(SupportedDatabase::class); $context->registerSetupCheck(SystemIs64bit::class); diff --git a/apps/settings/lib/SetupChecks/ServerIdConfig.php b/apps/settings/lib/SetupChecks/ServerIdConfig.php new file mode 100644 index 00000000000..ba7d26a0e4d --- /dev/null +++ b/apps/settings/lib/SetupChecks/ServerIdConfig.php @@ -0,0 +1,57 @@ +l10n->t('Configuration server ID'); + } + + #[Override] + public function getCategory(): string { + return 'config'; + } + + #[Override] + public function run(): SetupResult { + $serverid = $this->config->getSystemValueInt('serverid', PHP_INT_MIN); + $linkToDoc = $this->urlGenerator->linkToDocs('admin-update'); + + if ($serverid === PHP_INT_MIN) { + return SetupResult::info( + $this->l10n->t('server identifier isn’t configured. It is recommended if your Nextcloud instance is running on several PHP servers. Add a serverid in your configuration.'), + $linkToDoc, + ); + } + + if ($serverid < 0 || $serverid > 1023) { + return SetupResult::error( + $this->l10n->t('"%d" is not a valid server identifier. It must be between 0 and 1023.', [$serverid]), + $linkToDoc, + ); + } + + return SetupResult::success($this->l10n->t('server identifier is configured and valid.')); + } +} diff --git a/config/config.sample.php b/config/config.sample.php index 411e46c46c4..c775c3e5a24 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -45,6 +45,20 @@ $CONFIG = [ */ 'instanceid' => '', + /** + * This is a unique identifier for your server. + * It is useful when your Nextcloud instance is using different PHP servers. + * Once it's set it shouldn't be changed. + * + * Value must be an integer, comprised between 0 and 1023. + * + * When config.php is shared between different servers, this value should be overriden with "NC_serverid=" on each server. + * Note that it must be overriden for CLI and for your webserver. + * + * Example for CLI: NC_serverid=42 occ config:list system + */ + 'serverid' => -1, + /** * The salt used to hash all passwords, auto-generated by the Nextcloud * installer. (There are also per-user salts.) If you lose this salt, you lose diff --git a/lib/private/Snowflake/Generator.php b/lib/private/Snowflake/Generator.php index 7f4c91e105e..46e6e43b326 100644 --- a/lib/private/Snowflake/Generator.php +++ b/lib/private/Snowflake/Generator.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OC\Snowflake; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\Snowflake\IGenerator; use Override; @@ -23,6 +24,7 @@ use Override; final class Generator implements IGenerator { public function __construct( private readonly ITimeFactory $timeFactory, + private readonly IConfig $config, private readonly ISequence $sequenceGenerator, ) { } @@ -100,8 +102,14 @@ final class Generator implements IGenerator { ]; } + /** + * Return configured serverid or generate one if not set + */ private function getServerId(): int { - return crc32(gethostname() ?: random_bytes(8)); + $serverid = $this->config->getSystemValueInt('serverid', -1); + return $serverid > 0 + ? $serverid + : crc32(gethostname() ?: random_bytes(8)); } private function isCli(): bool { diff --git a/tests/lib/Snowflake/GeneratorTest.php b/tests/lib/Snowflake/GeneratorTest.php index 1e2369ee5c9..ce1160b2867 100644 --- a/tests/lib/Snowflake/GeneratorTest.php +++ b/tests/lib/Snowflake/GeneratorTest.php @@ -14,6 +14,7 @@ use OC\Snowflake\Decoder; use OC\Snowflake\Generator; use OC\Snowflake\ISequence; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\Snowflake\IGenerator; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -24,17 +25,25 @@ use Test\TestCase; */ class GeneratorTest extends TestCase { private Decoder $decoder; + private IConfig&MockObject $config; private ISequence&MockObject $sequence; public function setUp():void { $this->decoder = new Decoder(); + + $this->config = $this->createMock(IConfig::class); + $this->config->method('getSystemValueInt') + ->with('serverid') + ->willReturn(42); + $this->sequence = $this->createMock(ISequence::class); $this->sequence->method('isAvailable')->willReturn(true); $this->sequence->method('nextId')->willReturn(421); + } public function testGenerator(): void { - $generator = new Generator(new TimeFactory(), $this->sequence); + $generator = new Generator(new TimeFactory(), $this->config, $this->sequence); $snowflakeId = $generator->nextId(); $data = $this->decoder->decode($generator->nextId()); @@ -52,6 +61,9 @@ class GeneratorTest extends TestCase { // Check CLI $this->assertTrue($data['isCli']); + + // Check serverId + $this->assertEquals(42, $data['serverId']); } #[DataProvider('provideSnowflakeData')] @@ -60,11 +72,12 @@ class GeneratorTest extends TestCase { $timeFactory = $this->createMock(ITimeFactory::class); $timeFactory->method('now')->willReturn($dt); - $generator = new Generator($timeFactory, $this->sequence); + $generator = new Generator($timeFactory, $this->config, $this->sequence); $data = $this->decoder->decode($generator->nextId()); $this->assertEquals($expectedSeconds, ($data['createdAt']->format('U') - IGenerator::TS_OFFSET)); $this->assertEquals($expectedMilliseconds, (int)$data['createdAt']->format('v')); + $this->assertEquals(42, $data['serverId']); } public static function provideSnowflakeData(): array {