aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Security/Ip
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Security/Ip')
-rw-r--r--lib/private/Security/Ip/Address.php49
-rw-r--r--lib/private/Security/Ip/BruteforceAllowList.php57
-rw-r--r--lib/private/Security/Ip/Factory.php23
-rw-r--r--lib/private/Security/Ip/Range.php40
-rw-r--r--lib/private/Security/Ip/RemoteAddress.php71
5 files changed, 240 insertions, 0 deletions
diff --git a/lib/private/Security/Ip/Address.php b/lib/private/Security/Ip/Address.php
new file mode 100644
index 00000000000..0e94ec2d9ea
--- /dev/null
+++ b/lib/private/Security/Ip/Address.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use InvalidArgumentException;
+use IPLib\Address\AddressInterface;
+use IPLib\Factory;
+use IPLib\ParseStringFlag;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+
+/**
+ * @since 30.0.0
+ */
+class Address implements IAddress {
+ private readonly AddressInterface $ip;
+
+ public function __construct(string $ip) {
+ $ip = Factory::parseAddressString($ip, ParseStringFlag::MAY_INCLUDE_ZONEID);
+ if ($ip === null) {
+ throw new InvalidArgumentException('Given IP address can’t be parsed');
+ }
+ $this->ip = $ip;
+ }
+
+ public static function isValid(string $ip): bool {
+ return Factory::parseAddressString($ip, ParseStringFlag::MAY_INCLUDE_ZONEID) !== null;
+ }
+
+ public function matches(IRange ... $ranges): bool {
+ foreach ($ranges as $range) {
+ if ($range->contains($this)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function __toString(): string {
+ return $this->ip->toString();
+ }
+}
diff --git a/lib/private/Security/Ip/BruteforceAllowList.php b/lib/private/Security/Ip/BruteforceAllowList.php
new file mode 100644
index 00000000000..fb837690a7b
--- /dev/null
+++ b/lib/private/Security/Ip/BruteforceAllowList.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Ip;
+
+use OCP\IAppConfig;
+use OCP\Security\Ip\IFactory;
+
+class BruteforceAllowList {
+ /** @var array<string, bool> */
+ protected array $ipIsAllowListed = [];
+
+ public function __construct(
+ private readonly IAppConfig $appConfig,
+ private readonly IFactory $factory,
+ ) {
+ }
+
+ /**
+ * Check if the IP is allowed to bypass bruteforce protection
+ */
+ public function isBypassListed(string $ip): bool {
+ if (isset($this->ipIsAllowListed[$ip])) {
+ return $this->ipIsAllowListed[$ip];
+ }
+
+ try {
+ $address = $this->factory->addressFromString($ip);
+ } catch (\InvalidArgumentException) {
+ $this->ipIsAllowListed[$ip] = false;
+ return false;
+ }
+
+ foreach ($this->appConfig->searchKeys('bruteForce', 'whitelist_') as $key) {
+ $rangeString = $this->appConfig->getValueString('bruteForce', $key);
+ try {
+ $range = $this->factory->rangeFromString($rangeString);
+ } catch (\InvalidArgumentException) {
+ continue;
+ }
+
+ $allowed = $range->contains($address);
+ if ($allowed) {
+ $this->ipIsAllowListed[$ip] = true;
+ return true;
+ }
+ }
+
+ $this->ipIsAllowListed[$ip] = false;
+ return false;
+ }
+}
diff --git a/lib/private/Security/Ip/Factory.php b/lib/private/Security/Ip/Factory.php
new file mode 100644
index 00000000000..1eedcf27a09
--- /dev/null
+++ b/lib/private/Security/Ip/Factory.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IFactory;
+use OCP\Security\Ip\IRange;
+
+class Factory implements IFactory {
+ public function rangeFromString(string $range): IRange {
+ return new Range($range);
+ }
+
+ public function addressFromString(string $ip): IAddress {
+ return new Address($ip);
+ }
+}
diff --git a/lib/private/Security/Ip/Range.php b/lib/private/Security/Ip/Range.php
new file mode 100644
index 00000000000..e32b7a5abc0
--- /dev/null
+++ b/lib/private/Security/Ip/Range.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use InvalidArgumentException;
+use IPLib\Factory;
+use IPLib\ParseStringFlag;
+use IPLib\Range\RangeInterface;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+
+class Range implements IRange {
+ private readonly RangeInterface $range;
+
+ public function __construct(string $range) {
+ $range = Factory::parseRangeString($range);
+ if ($range === null) {
+ throw new InvalidArgumentException('Given range can’t be parsed');
+ }
+ $this->range = $range;
+ }
+
+ public static function isValid(string $range): bool {
+ return Factory::parseRangeString($range) !== null;
+ }
+
+ public function contains(IAddress $address): bool {
+ return $this->range->contains(Factory::parseAddressString((string)$address, ParseStringFlag::MAY_INCLUDE_ZONEID));
+ }
+
+ public function __toString(): string {
+ return $this->range->toString();
+ }
+}
diff --git a/lib/private/Security/Ip/RemoteAddress.php b/lib/private/Security/Ip/RemoteAddress.php
new file mode 100644
index 00000000000..4eef8813898
--- /dev/null
+++ b/lib/private/Security/Ip/RemoteAddress.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+use OCP\Security\Ip\IRemoteAddress;
+
+class RemoteAddress implements IRemoteAddress, IAddress {
+ public const SETTING_NAME = 'allowed_admin_ranges';
+
+ private readonly ?IAddress $ip;
+
+ public function __construct(
+ private IConfig $config,
+ IRequest $request,
+ ) {
+ $remoteAddress = $request->getRemoteAddress();
+ $this->ip = $remoteAddress === ''
+ ? null
+ : new Address($remoteAddress);
+ }
+
+ public static function isValid(string $ip): bool {
+ return Address::isValid($ip);
+ }
+
+ public function matches(IRange ... $ranges): bool {
+ return $this->ip === null
+ ? true
+ : $this->ip->matches(... $ranges);
+ }
+
+ public function allowsAdminActions(): bool {
+ if ($this->ip === null) {
+ return true;
+ }
+
+ $allowedAdminRanges = $this->config->getSystemValue(self::SETTING_NAME, false);
+
+ // Don't apply restrictions on empty or invalid configuration
+ if (
+ $allowedAdminRanges === false
+ || !is_array($allowedAdminRanges)
+ || empty($allowedAdminRanges)
+ ) {
+ return true;
+ }
+
+ foreach ($allowedAdminRanges as $allowedAdminRange) {
+ if ((new Range($allowedAdminRange))->contains($this->ip)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function __toString(): string {
+ return (string)$this->ip;
+ }
+}