Passed
Push — master ( c69370...174a55 )
by Roeland
25:32 queued 11s
created
lib/private/Repair/AddBruteForceCleanupJob.php 1 patch
Indentation   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -32,18 +32,18 @@
 block discarded – undo
32 32
 
33 33
 class AddBruteForceCleanupJob implements IRepairStep {
34 34
 
35
-	/** @var IJobList */
36
-	protected $jobList;
35
+    /** @var IJobList */
36
+    protected $jobList;
37 37
 
38
-	public function __construct(IJobList $jobList) {
39
-		$this->jobList = $jobList;
40
-	}
38
+    public function __construct(IJobList $jobList) {
39
+        $this->jobList = $jobList;
40
+    }
41 41
 
42
-	public function getName() {
43
-		return 'Add job to cleanup the bruteforce entries';
44
-	}
42
+    public function getName() {
43
+        return 'Add job to cleanup the bruteforce entries';
44
+    }
45 45
 
46
-	public function run(IOutput $output) {
47
-		$this->jobList->add(CleanupJob::class);
48
-	}
46
+    public function run(IOutput $output) {
47
+        $this->jobList->add(CleanupJob::class);
48
+    }
49 49
 }
Please login to merge, or discard this patch.
lib/private/Repair.php 1 patch
Indentation   +176 added lines, -176 removed lines patch added patch discarded remove patch
@@ -71,180 +71,180 @@
 block discarded – undo
71 71
 
72 72
 class Repair implements IOutput {
73 73
 
74
-	/** @var IRepairStep[] */
75
-	private $repairSteps;
76
-
77
-	/** @var EventDispatcherInterface */
78
-	private $dispatcher;
79
-
80
-	/** @var string */
81
-	private $currentStep;
82
-
83
-	/**
84
-	 * Creates a new repair step runner
85
-	 *
86
-	 * @param IRepairStep[] $repairSteps array of RepairStep instances
87
-	 * @param EventDispatcherInterface $dispatcher
88
-	 */
89
-	public function __construct(array $repairSteps, EventDispatcherInterface $dispatcher) {
90
-		$this->repairSteps = $repairSteps;
91
-		$this->dispatcher = $dispatcher;
92
-	}
93
-
94
-	/**
95
-	 * Run a series of repair steps for common problems
96
-	 */
97
-	public function run() {
98
-		if (count($this->repairSteps) === 0) {
99
-			$this->emit('\OC\Repair', 'info', ['No repair steps available']);
100
-
101
-			return;
102
-		}
103
-		// run each repair step
104
-		foreach ($this->repairSteps as $step) {
105
-			$this->currentStep = $step->getName();
106
-			$this->emit('\OC\Repair', 'step', [$this->currentStep]);
107
-			$step->run($this);
108
-		}
109
-	}
110
-
111
-	/**
112
-	 * Add repair step
113
-	 *
114
-	 * @param IRepairStep|string $repairStep repair step
115
-	 * @throws \Exception
116
-	 */
117
-	public function addStep($repairStep) {
118
-		if (is_string($repairStep)) {
119
-			try {
120
-				$s = \OC::$server->query($repairStep);
121
-			} catch (QueryException $e) {
122
-				if (class_exists($repairStep)) {
123
-					$s = new $repairStep();
124
-				} else {
125
-					throw new \Exception("Repair step '$repairStep' is unknown");
126
-				}
127
-			}
128
-
129
-			if ($s instanceof IRepairStep) {
130
-				$this->repairSteps[] = $s;
131
-			} else {
132
-				throw new \Exception("Repair step '$repairStep' is not of type \\OCP\\Migration\\IRepairStep");
133
-			}
134
-		} else {
135
-			$this->repairSteps[] = $repairStep;
136
-		}
137
-	}
138
-
139
-	/**
140
-	 * Returns the default repair steps to be run on the
141
-	 * command line or after an upgrade.
142
-	 *
143
-	 * @return IRepairStep[]
144
-	 */
145
-	public static function getRepairSteps() {
146
-		return [
147
-			new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false),
148
-			new RepairMimeTypes(\OC::$server->getConfig()),
149
-			new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()),
150
-			new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
151
-			new MoveUpdaterStepFile(\OC::$server->getConfig()),
152
-			new FixMountStorages(\OC::$server->getDatabaseConnection()),
153
-			new AddLogRotateJob(\OC::$server->getJobList()),
154
-			new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OC::$server->query(SCSSCacher::class), \OC::$server->query(JSCombiner::class)),
155
-			new ClearGeneratedAvatarCache(\OC::$server->getConfig(), \OC::$server->query(AvatarManager::class)),
156
-			new AddPreviewBackgroundCleanupJob(\OC::$server->getJobList()),
157
-			new AddCleanupUpdaterBackupsJob(\OC::$server->getJobList()),
158
-			new CleanupCardDAVPhotoCache(\OC::$server->getConfig(), \OC::$server->getAppDataDir('dav-photocache'), \OC::$server->getLogger()),
159
-			new AddClenupLoginFlowV2BackgroundJob(\OC::$server->getJobList()),
160
-			new RemoveLinkShares(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig(), \OC::$server->getGroupManager(), \OC::$server->getNotificationManager(), \OC::$server->query(ITimeFactory::class)),
161
-			new ClearCollectionsAccessCache(\OC::$server->getConfig(), \OC::$server->query(IManager::class)),
162
-			\OC::$server->query(ResetGeneratedAvatarFlag::class),
163
-			\OC::$server->query(EncryptionLegacyCipher::class),
164
-			\OC::$server->query(EncryptionMigration::class),
165
-			\OC::$server->get(ShippedDashboardEnable::class),
166
-			\OC::$server->get(AddBruteForceCleanupJob::class),
167
-		];
168
-	}
169
-
170
-	/**
171
-	 * Returns expensive repair steps to be run on the
172
-	 * command line with a special option.
173
-	 *
174
-	 * @return IRepairStep[]
175
-	 */
176
-	public static function getExpensiveRepairSteps() {
177
-		return [
178
-			new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager())
179
-		];
180
-	}
181
-
182
-	/**
183
-	 * Returns the repair steps to be run before an
184
-	 * upgrade.
185
-	 *
186
-	 * @return IRepairStep[]
187
-	 */
188
-	public static function getBeforeUpgradeRepairSteps() {
189
-		$connection = \OC::$server->getDatabaseConnection();
190
-		$config = \OC::$server->getConfig();
191
-		$steps = [
192
-			new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true),
193
-			new SqliteAutoincrement($connection),
194
-			new SaveAccountsTableData($connection, $config),
195
-			new DropAccountTermsTable($connection)
196
-		];
197
-
198
-		return $steps;
199
-	}
200
-
201
-	/**
202
-	 * @param string $scope
203
-	 * @param string $method
204
-	 * @param array $arguments
205
-	 */
206
-	public function emit($scope, $method, array $arguments = []) {
207
-		if (!is_null($this->dispatcher)) {
208
-			$this->dispatcher->dispatch("$scope::$method",
209
-				new GenericEvent("$scope::$method", $arguments));
210
-		}
211
-	}
212
-
213
-	public function info($string) {
214
-		// for now just emit as we did in the past
215
-		$this->emit('\OC\Repair', 'info', [$string]);
216
-	}
217
-
218
-	/**
219
-	 * @param string $message
220
-	 */
221
-	public function warning($message) {
222
-		// for now just emit as we did in the past
223
-		$this->emit('\OC\Repair', 'warning', [$message]);
224
-	}
225
-
226
-	/**
227
-	 * @param int $max
228
-	 */
229
-	public function startProgress($max = 0) {
230
-		// for now just emit as we did in the past
231
-		$this->emit('\OC\Repair', 'startProgress', [$max, $this->currentStep]);
232
-	}
233
-
234
-	/**
235
-	 * @param int $step
236
-	 * @param string $description
237
-	 */
238
-	public function advance($step = 1, $description = '') {
239
-		// for now just emit as we did in the past
240
-		$this->emit('\OC\Repair', 'advance', [$step, $description]);
241
-	}
242
-
243
-	/**
244
-	 * @param int $max
245
-	 */
246
-	public function finishProgress() {
247
-		// for now just emit as we did in the past
248
-		$this->emit('\OC\Repair', 'finishProgress', []);
249
-	}
74
+    /** @var IRepairStep[] */
75
+    private $repairSteps;
76
+
77
+    /** @var EventDispatcherInterface */
78
+    private $dispatcher;
79
+
80
+    /** @var string */
81
+    private $currentStep;
82
+
83
+    /**
84
+     * Creates a new repair step runner
85
+     *
86
+     * @param IRepairStep[] $repairSteps array of RepairStep instances
87
+     * @param EventDispatcherInterface $dispatcher
88
+     */
89
+    public function __construct(array $repairSteps, EventDispatcherInterface $dispatcher) {
90
+        $this->repairSteps = $repairSteps;
91
+        $this->dispatcher = $dispatcher;
92
+    }
93
+
94
+    /**
95
+     * Run a series of repair steps for common problems
96
+     */
97
+    public function run() {
98
+        if (count($this->repairSteps) === 0) {
99
+            $this->emit('\OC\Repair', 'info', ['No repair steps available']);
100
+
101
+            return;
102
+        }
103
+        // run each repair step
104
+        foreach ($this->repairSteps as $step) {
105
+            $this->currentStep = $step->getName();
106
+            $this->emit('\OC\Repair', 'step', [$this->currentStep]);
107
+            $step->run($this);
108
+        }
109
+    }
110
+
111
+    /**
112
+     * Add repair step
113
+     *
114
+     * @param IRepairStep|string $repairStep repair step
115
+     * @throws \Exception
116
+     */
117
+    public function addStep($repairStep) {
118
+        if (is_string($repairStep)) {
119
+            try {
120
+                $s = \OC::$server->query($repairStep);
121
+            } catch (QueryException $e) {
122
+                if (class_exists($repairStep)) {
123
+                    $s = new $repairStep();
124
+                } else {
125
+                    throw new \Exception("Repair step '$repairStep' is unknown");
126
+                }
127
+            }
128
+
129
+            if ($s instanceof IRepairStep) {
130
+                $this->repairSteps[] = $s;
131
+            } else {
132
+                throw new \Exception("Repair step '$repairStep' is not of type \\OCP\\Migration\\IRepairStep");
133
+            }
134
+        } else {
135
+            $this->repairSteps[] = $repairStep;
136
+        }
137
+    }
138
+
139
+    /**
140
+     * Returns the default repair steps to be run on the
141
+     * command line or after an upgrade.
142
+     *
143
+     * @return IRepairStep[]
144
+     */
145
+    public static function getRepairSteps() {
146
+        return [
147
+            new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false),
148
+            new RepairMimeTypes(\OC::$server->getConfig()),
149
+            new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()),
150
+            new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
151
+            new MoveUpdaterStepFile(\OC::$server->getConfig()),
152
+            new FixMountStorages(\OC::$server->getDatabaseConnection()),
153
+            new AddLogRotateJob(\OC::$server->getJobList()),
154
+            new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OC::$server->query(SCSSCacher::class), \OC::$server->query(JSCombiner::class)),
155
+            new ClearGeneratedAvatarCache(\OC::$server->getConfig(), \OC::$server->query(AvatarManager::class)),
156
+            new AddPreviewBackgroundCleanupJob(\OC::$server->getJobList()),
157
+            new AddCleanupUpdaterBackupsJob(\OC::$server->getJobList()),
158
+            new CleanupCardDAVPhotoCache(\OC::$server->getConfig(), \OC::$server->getAppDataDir('dav-photocache'), \OC::$server->getLogger()),
159
+            new AddClenupLoginFlowV2BackgroundJob(\OC::$server->getJobList()),
160
+            new RemoveLinkShares(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig(), \OC::$server->getGroupManager(), \OC::$server->getNotificationManager(), \OC::$server->query(ITimeFactory::class)),
161
+            new ClearCollectionsAccessCache(\OC::$server->getConfig(), \OC::$server->query(IManager::class)),
162
+            \OC::$server->query(ResetGeneratedAvatarFlag::class),
163
+            \OC::$server->query(EncryptionLegacyCipher::class),
164
+            \OC::$server->query(EncryptionMigration::class),
165
+            \OC::$server->get(ShippedDashboardEnable::class),
166
+            \OC::$server->get(AddBruteForceCleanupJob::class),
167
+        ];
168
+    }
169
+
170
+    /**
171
+     * Returns expensive repair steps to be run on the
172
+     * command line with a special option.
173
+     *
174
+     * @return IRepairStep[]
175
+     */
176
+    public static function getExpensiveRepairSteps() {
177
+        return [
178
+            new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager())
179
+        ];
180
+    }
181
+
182
+    /**
183
+     * Returns the repair steps to be run before an
184
+     * upgrade.
185
+     *
186
+     * @return IRepairStep[]
187
+     */
188
+    public static function getBeforeUpgradeRepairSteps() {
189
+        $connection = \OC::$server->getDatabaseConnection();
190
+        $config = \OC::$server->getConfig();
191
+        $steps = [
192
+            new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true),
193
+            new SqliteAutoincrement($connection),
194
+            new SaveAccountsTableData($connection, $config),
195
+            new DropAccountTermsTable($connection)
196
+        ];
197
+
198
+        return $steps;
199
+    }
200
+
201
+    /**
202
+     * @param string $scope
203
+     * @param string $method
204
+     * @param array $arguments
205
+     */
206
+    public function emit($scope, $method, array $arguments = []) {
207
+        if (!is_null($this->dispatcher)) {
208
+            $this->dispatcher->dispatch("$scope::$method",
209
+                new GenericEvent("$scope::$method", $arguments));
210
+        }
211
+    }
212
+
213
+    public function info($string) {
214
+        // for now just emit as we did in the past
215
+        $this->emit('\OC\Repair', 'info', [$string]);
216
+    }
217
+
218
+    /**
219
+     * @param string $message
220
+     */
221
+    public function warning($message) {
222
+        // for now just emit as we did in the past
223
+        $this->emit('\OC\Repair', 'warning', [$message]);
224
+    }
225
+
226
+    /**
227
+     * @param int $max
228
+     */
229
+    public function startProgress($max = 0) {
230
+        // for now just emit as we did in the past
231
+        $this->emit('\OC\Repair', 'startProgress', [$max, $this->currentStep]);
232
+    }
233
+
234
+    /**
235
+     * @param int $step
236
+     * @param string $description
237
+     */
238
+    public function advance($step = 1, $description = '') {
239
+        // for now just emit as we did in the past
240
+        $this->emit('\OC\Repair', 'advance', [$step, $description]);
241
+    }
242
+
243
+    /**
244
+     * @param int $max
245
+     */
246
+    public function finishProgress() {
247
+        // for now just emit as we did in the past
248
+        $this->emit('\OC\Repair', 'finishProgress', []);
249
+    }
250 250
 }
Please login to merge, or discard this patch.
lib/private/Security/Bruteforce/CleanupJob.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -32,24 +32,24 @@
 block discarded – undo
32 32
 
33 33
 class CleanupJob extends TimedJob {
34 34
 
35
-	/** @var IDBConnection */
36
-	private $connection;
37
-
38
-	public function __construct(ITimeFactory $time, IDBConnection $connection) {
39
-		parent::__construct($time);
40
-		$this->connection = $connection;
41
-
42
-		// Run once a day
43
-		$this->setInterval(3600 * 24);
44
-	}
45
-
46
-	protected function run($argument) {
47
-		// Delete all entries more than 48 hours old
48
-		$time = $this->time->getTime() - (48 * 3600);
49
-
50
-		$qb = $this->connection->getQueryBuilder();
51
-		$qb->delete('bruteforce_attempts')
52
-			->where($qb->expr()->lt('occurred', $qb->createNamedParameter($time), IQueryBuilder::PARAM_INT));
53
-		$qb->execute();
54
-	}
35
+    /** @var IDBConnection */
36
+    private $connection;
37
+
38
+    public function __construct(ITimeFactory $time, IDBConnection $connection) {
39
+        parent::__construct($time);
40
+        $this->connection = $connection;
41
+
42
+        // Run once a day
43
+        $this->setInterval(3600 * 24);
44
+    }
45
+
46
+    protected function run($argument) {
47
+        // Delete all entries more than 48 hours old
48
+        $time = $this->time->getTime() - (48 * 3600);
49
+
50
+        $qb = $this->connection->getQueryBuilder();
51
+        $qb->delete('bruteforce_attempts')
52
+            ->where($qb->expr()->lt('occurred', $qb->createNamedParameter($time), IQueryBuilder::PARAM_INT));
53
+        $qb->execute();
54
+    }
55 55
 }
Please login to merge, or discard this patch.
lib/private/Security/Bruteforce/Throttler.php 1 patch
Indentation   +303 added lines, -303 removed lines patch added patch discarded remove patch
@@ -54,307 +54,307 @@
 block discarded – undo
54 54
  * @package OC\Security\Bruteforce
55 55
  */
56 56
 class Throttler {
57
-	public const LOGIN_ACTION = 'login';
58
-	public const MAX_DELAY = 25;
59
-	public const MAX_DELAY_MS = 25000; // in milliseconds
60
-	public const MAX_ATTEMPTS = 10;
61
-
62
-	/** @var IDBConnection */
63
-	private $db;
64
-	/** @var ITimeFactory */
65
-	private $timeFactory;
66
-	/** @var ILogger */
67
-	private $logger;
68
-	/** @var IConfig */
69
-	private $config;
70
-
71
-	/**
72
-	 * @param IDBConnection $db
73
-	 * @param ITimeFactory $timeFactory
74
-	 * @param ILogger $logger
75
-	 * @param IConfig $config
76
-	 */
77
-	public function __construct(IDBConnection $db,
78
-								ITimeFactory $timeFactory,
79
-								ILogger $logger,
80
-								IConfig $config) {
81
-		$this->db = $db;
82
-		$this->timeFactory = $timeFactory;
83
-		$this->logger = $logger;
84
-		$this->config = $config;
85
-	}
86
-
87
-	/**
88
-	 * Convert a number of seconds into the appropriate DateInterval
89
-	 *
90
-	 * @param int $expire
91
-	 * @return \DateInterval
92
-	 */
93
-	private function getCutoff(int $expire): \DateInterval {
94
-		$d1 = new \DateTime();
95
-		$d2 = clone $d1;
96
-		$d2->sub(new \DateInterval('PT' . $expire . 'S'));
97
-		return $d2->diff($d1);
98
-	}
99
-
100
-	/**
101
-	 *  Calculate the cut off timestamp
102
-	 *
103
-	 * @param float $maxAgeHours
104
-	 * @return int
105
-	 */
106
-	private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
107
-		return (new \DateTime())
108
-			->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
109
-			->getTimestamp();
110
-	}
111
-
112
-	/**
113
-	 * Register a failed attempt to bruteforce a security control
114
-	 *
115
-	 * @param string $action
116
-	 * @param string $ip
117
-	 * @param array $metadata Optional metadata logged to the database
118
-	 */
119
-	public function registerAttempt(string $action,
120
-									string $ip,
121
-									array $metadata = []): void {
122
-		// No need to log if the bruteforce protection is disabled
123
-		if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
124
-			return;
125
-		}
126
-
127
-		$ipAddress = new IpAddress($ip);
128
-		$values = [
129
-			'action' => $action,
130
-			'occurred' => $this->timeFactory->getTime(),
131
-			'ip' => (string)$ipAddress,
132
-			'subnet' => $ipAddress->getSubnet(),
133
-			'metadata' => json_encode($metadata),
134
-		];
135
-
136
-		$this->logger->notice(
137
-			sprintf(
138
-				'Bruteforce attempt from "%s" detected for action "%s".',
139
-				$ip,
140
-				$action
141
-			),
142
-			[
143
-				'app' => 'core',
144
-			]
145
-		);
146
-
147
-		$qb = $this->db->getQueryBuilder();
148
-		$qb->insert('bruteforce_attempts');
149
-		foreach ($values as $column => $value) {
150
-			$qb->setValue($column, $qb->createNamedParameter($value));
151
-		}
152
-		$qb->execute();
153
-	}
154
-
155
-	/**
156
-	 * Check if the IP is whitelisted
157
-	 *
158
-	 * @param string $ip
159
-	 * @return bool
160
-	 */
161
-	private function isIPWhitelisted(string $ip): bool {
162
-		if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
163
-			return true;
164
-		}
165
-
166
-		$keys = $this->config->getAppKeys('bruteForce');
167
-		$keys = array_filter($keys, function ($key) {
168
-			return 0 === strpos($key, 'whitelist_');
169
-		});
170
-
171
-		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
172
-			$type = 4;
173
-		} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
174
-			$type = 6;
175
-		} else {
176
-			return false;
177
-		}
178
-
179
-		$ip = inet_pton($ip);
180
-
181
-		foreach ($keys as $key) {
182
-			$cidr = $this->config->getAppValue('bruteForce', $key, null);
183
-
184
-			$cx = explode('/', $cidr);
185
-			$addr = $cx[0];
186
-			$mask = (int)$cx[1];
187
-
188
-			// Do not compare ipv4 to ipv6
189
-			if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
190
-				($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
191
-				continue;
192
-			}
193
-
194
-			$addr = inet_pton($addr);
195
-
196
-			$valid = true;
197
-			for ($i = 0; $i < $mask; $i++) {
198
-				$part = ord($addr[(int)($i / 8)]);
199
-				$orig = ord($ip[(int)($i / 8)]);
200
-
201
-				$bitmask = 1 << (7 - ($i % 8));
202
-
203
-				$part = $part & $bitmask;
204
-				$orig = $orig & $bitmask;
205
-
206
-				if ($part !== $orig) {
207
-					$valid = false;
208
-					break;
209
-				}
210
-			}
211
-
212
-			if ($valid === true) {
213
-				return true;
214
-			}
215
-		}
216
-
217
-		return false;
218
-	}
219
-
220
-	/**
221
-	 * Get the throttling delay (in milliseconds)
222
-	 *
223
-	 * @param string $ip
224
-	 * @param string $action optionally filter by action
225
-	 * @param float $maxAgeHours
226
-	 * @return int
227
-	 */
228
-	public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
229
-		if ($maxAgeHours > 48) {
230
-			$this->logger->error('Bruteforce has to use less than 48 hours');
231
-			$maxAgeHours = 48;
232
-		}
233
-
234
-		if ($ip === '') {
235
-			return 0;
236
-		}
237
-
238
-		$ipAddress = new IpAddress($ip);
239
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
240
-			return 0;
241
-		}
242
-
243
-		$cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
244
-
245
-		$qb = $this->db->getQueryBuilder();
246
-		$qb->select($qb->func()->count('*', 'attempts'))
247
-			->from('bruteforce_attempts')
248
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
249
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
250
-
251
-		if ($action !== '') {
252
-			$qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
253
-		}
254
-
255
-		$result = $qb->execute();
256
-		$row = $result->fetch();
257
-		$result->closeCursor();
258
-
259
-		return (int) $row['attempts'];
260
-	}
261
-
262
-	/**
263
-	 * Get the throttling delay (in milliseconds)
264
-	 *
265
-	 * @param string $ip
266
-	 * @param string $action optionally filter by action
267
-	 * @return int
268
-	 */
269
-	public function getDelay(string $ip, string $action = ''): int {
270
-		$attempts = $this->getAttempts($ip, $action);
271
-		if ($attempts === 0) {
272
-			return 0;
273
-		}
274
-
275
-		$firstDelay = 0.1;
276
-		if ($attempts > self::MAX_ATTEMPTS) {
277
-			// Don't ever overflow. Just assume the maxDelay time:s
278
-			return self::MAX_DELAY_MS;
279
-		}
280
-
281
-		$delay = $firstDelay * 2 ** $attempts;
282
-		if ($delay > self::MAX_DELAY) {
283
-			return self::MAX_DELAY_MS;
284
-		}
285
-		return (int) \ceil($delay * 1000);
286
-	}
287
-
288
-	/**
289
-	 * Reset the throttling delay for an IP address, action and metadata
290
-	 *
291
-	 * @param string $ip
292
-	 * @param string $action
293
-	 * @param array $metadata
294
-	 */
295
-	public function resetDelay(string $ip, string $action, array $metadata): void {
296
-		$ipAddress = new IpAddress($ip);
297
-		if ($this->isIPWhitelisted((string)$ipAddress)) {
298
-			return;
299
-		}
300
-
301
-		$cutoffTime = $this->getCutoffTimestamp();
302
-
303
-		$qb = $this->db->getQueryBuilder();
304
-		$qb->delete('bruteforce_attempts')
305
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
306
-			->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
307
-			->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
308
-			->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
309
-
310
-		$qb->execute();
311
-	}
312
-
313
-	/**
314
-	 * Reset the throttling delay for an IP address
315
-	 *
316
-	 * @param string $ip
317
-	 */
318
-	public function resetDelayForIP($ip) {
319
-		$cutoffTime = $this->getCutoffTimestamp();
320
-
321
-		$qb = $this->db->getQueryBuilder();
322
-		$qb->delete('bruteforce_attempts')
323
-			->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
324
-			->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
325
-
326
-		$qb->execute();
327
-	}
328
-
329
-	/**
330
-	 * Will sleep for the defined amount of time
331
-	 *
332
-	 * @param string $ip
333
-	 * @param string $action optionally filter by action
334
-	 * @return int the time spent sleeping
335
-	 */
336
-	public function sleepDelay(string $ip, string $action = ''): int {
337
-		$delay = $this->getDelay($ip, $action);
338
-		usleep($delay * 1000);
339
-		return $delay;
340
-	}
341
-
342
-	/**
343
-	 * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
344
-	 * In this case a "429 Too Many Request" exception is thrown
345
-	 *
346
-	 * @param string $ip
347
-	 * @param string $action optionally filter by action
348
-	 * @return int the time spent sleeping
349
-	 * @throws MaxDelayReached when reached the maximum
350
-	 */
351
-	public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
352
-		$delay = $this->getDelay($ip, $action);
353
-		if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
354
-			// If the ip made too many attempts within the last 30 mins we don't execute anymore
355
-			throw new MaxDelayReached('Reached maximum delay');
356
-		}
357
-		usleep($delay * 1000);
358
-		return $delay;
359
-	}
57
+    public const LOGIN_ACTION = 'login';
58
+    public const MAX_DELAY = 25;
59
+    public const MAX_DELAY_MS = 25000; // in milliseconds
60
+    public const MAX_ATTEMPTS = 10;
61
+
62
+    /** @var IDBConnection */
63
+    private $db;
64
+    /** @var ITimeFactory */
65
+    private $timeFactory;
66
+    /** @var ILogger */
67
+    private $logger;
68
+    /** @var IConfig */
69
+    private $config;
70
+
71
+    /**
72
+     * @param IDBConnection $db
73
+     * @param ITimeFactory $timeFactory
74
+     * @param ILogger $logger
75
+     * @param IConfig $config
76
+     */
77
+    public function __construct(IDBConnection $db,
78
+                                ITimeFactory $timeFactory,
79
+                                ILogger $logger,
80
+                                IConfig $config) {
81
+        $this->db = $db;
82
+        $this->timeFactory = $timeFactory;
83
+        $this->logger = $logger;
84
+        $this->config = $config;
85
+    }
86
+
87
+    /**
88
+     * Convert a number of seconds into the appropriate DateInterval
89
+     *
90
+     * @param int $expire
91
+     * @return \DateInterval
92
+     */
93
+    private function getCutoff(int $expire): \DateInterval {
94
+        $d1 = new \DateTime();
95
+        $d2 = clone $d1;
96
+        $d2->sub(new \DateInterval('PT' . $expire . 'S'));
97
+        return $d2->diff($d1);
98
+    }
99
+
100
+    /**
101
+     *  Calculate the cut off timestamp
102
+     *
103
+     * @param float $maxAgeHours
104
+     * @return int
105
+     */
106
+    private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
107
+        return (new \DateTime())
108
+            ->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
109
+            ->getTimestamp();
110
+    }
111
+
112
+    /**
113
+     * Register a failed attempt to bruteforce a security control
114
+     *
115
+     * @param string $action
116
+     * @param string $ip
117
+     * @param array $metadata Optional metadata logged to the database
118
+     */
119
+    public function registerAttempt(string $action,
120
+                                    string $ip,
121
+                                    array $metadata = []): void {
122
+        // No need to log if the bruteforce protection is disabled
123
+        if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
124
+            return;
125
+        }
126
+
127
+        $ipAddress = new IpAddress($ip);
128
+        $values = [
129
+            'action' => $action,
130
+            'occurred' => $this->timeFactory->getTime(),
131
+            'ip' => (string)$ipAddress,
132
+            'subnet' => $ipAddress->getSubnet(),
133
+            'metadata' => json_encode($metadata),
134
+        ];
135
+
136
+        $this->logger->notice(
137
+            sprintf(
138
+                'Bruteforce attempt from "%s" detected for action "%s".',
139
+                $ip,
140
+                $action
141
+            ),
142
+            [
143
+                'app' => 'core',
144
+            ]
145
+        );
146
+
147
+        $qb = $this->db->getQueryBuilder();
148
+        $qb->insert('bruteforce_attempts');
149
+        foreach ($values as $column => $value) {
150
+            $qb->setValue($column, $qb->createNamedParameter($value));
151
+        }
152
+        $qb->execute();
153
+    }
154
+
155
+    /**
156
+     * Check if the IP is whitelisted
157
+     *
158
+     * @param string $ip
159
+     * @return bool
160
+     */
161
+    private function isIPWhitelisted(string $ip): bool {
162
+        if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
163
+            return true;
164
+        }
165
+
166
+        $keys = $this->config->getAppKeys('bruteForce');
167
+        $keys = array_filter($keys, function ($key) {
168
+            return 0 === strpos($key, 'whitelist_');
169
+        });
170
+
171
+        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
172
+            $type = 4;
173
+        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
174
+            $type = 6;
175
+        } else {
176
+            return false;
177
+        }
178
+
179
+        $ip = inet_pton($ip);
180
+
181
+        foreach ($keys as $key) {
182
+            $cidr = $this->config->getAppValue('bruteForce', $key, null);
183
+
184
+            $cx = explode('/', $cidr);
185
+            $addr = $cx[0];
186
+            $mask = (int)$cx[1];
187
+
188
+            // Do not compare ipv4 to ipv6
189
+            if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
190
+                ($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
191
+                continue;
192
+            }
193
+
194
+            $addr = inet_pton($addr);
195
+
196
+            $valid = true;
197
+            for ($i = 0; $i < $mask; $i++) {
198
+                $part = ord($addr[(int)($i / 8)]);
199
+                $orig = ord($ip[(int)($i / 8)]);
200
+
201
+                $bitmask = 1 << (7 - ($i % 8));
202
+
203
+                $part = $part & $bitmask;
204
+                $orig = $orig & $bitmask;
205
+
206
+                if ($part !== $orig) {
207
+                    $valid = false;
208
+                    break;
209
+                }
210
+            }
211
+
212
+            if ($valid === true) {
213
+                return true;
214
+            }
215
+        }
216
+
217
+        return false;
218
+    }
219
+
220
+    /**
221
+     * Get the throttling delay (in milliseconds)
222
+     *
223
+     * @param string $ip
224
+     * @param string $action optionally filter by action
225
+     * @param float $maxAgeHours
226
+     * @return int
227
+     */
228
+    public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
229
+        if ($maxAgeHours > 48) {
230
+            $this->logger->error('Bruteforce has to use less than 48 hours');
231
+            $maxAgeHours = 48;
232
+        }
233
+
234
+        if ($ip === '') {
235
+            return 0;
236
+        }
237
+
238
+        $ipAddress = new IpAddress($ip);
239
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
240
+            return 0;
241
+        }
242
+
243
+        $cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
244
+
245
+        $qb = $this->db->getQueryBuilder();
246
+        $qb->select($qb->func()->count('*', 'attempts'))
247
+            ->from('bruteforce_attempts')
248
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
249
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
250
+
251
+        if ($action !== '') {
252
+            $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
253
+        }
254
+
255
+        $result = $qb->execute();
256
+        $row = $result->fetch();
257
+        $result->closeCursor();
258
+
259
+        return (int) $row['attempts'];
260
+    }
261
+
262
+    /**
263
+     * Get the throttling delay (in milliseconds)
264
+     *
265
+     * @param string $ip
266
+     * @param string $action optionally filter by action
267
+     * @return int
268
+     */
269
+    public function getDelay(string $ip, string $action = ''): int {
270
+        $attempts = $this->getAttempts($ip, $action);
271
+        if ($attempts === 0) {
272
+            return 0;
273
+        }
274
+
275
+        $firstDelay = 0.1;
276
+        if ($attempts > self::MAX_ATTEMPTS) {
277
+            // Don't ever overflow. Just assume the maxDelay time:s
278
+            return self::MAX_DELAY_MS;
279
+        }
280
+
281
+        $delay = $firstDelay * 2 ** $attempts;
282
+        if ($delay > self::MAX_DELAY) {
283
+            return self::MAX_DELAY_MS;
284
+        }
285
+        return (int) \ceil($delay * 1000);
286
+    }
287
+
288
+    /**
289
+     * Reset the throttling delay for an IP address, action and metadata
290
+     *
291
+     * @param string $ip
292
+     * @param string $action
293
+     * @param array $metadata
294
+     */
295
+    public function resetDelay(string $ip, string $action, array $metadata): void {
296
+        $ipAddress = new IpAddress($ip);
297
+        if ($this->isIPWhitelisted((string)$ipAddress)) {
298
+            return;
299
+        }
300
+
301
+        $cutoffTime = $this->getCutoffTimestamp();
302
+
303
+        $qb = $this->db->getQueryBuilder();
304
+        $qb->delete('bruteforce_attempts')
305
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
306
+            ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
307
+            ->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
308
+            ->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
309
+
310
+        $qb->execute();
311
+    }
312
+
313
+    /**
314
+     * Reset the throttling delay for an IP address
315
+     *
316
+     * @param string $ip
317
+     */
318
+    public function resetDelayForIP($ip) {
319
+        $cutoffTime = $this->getCutoffTimestamp();
320
+
321
+        $qb = $this->db->getQueryBuilder();
322
+        $qb->delete('bruteforce_attempts')
323
+            ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
324
+            ->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
325
+
326
+        $qb->execute();
327
+    }
328
+
329
+    /**
330
+     * Will sleep for the defined amount of time
331
+     *
332
+     * @param string $ip
333
+     * @param string $action optionally filter by action
334
+     * @return int the time spent sleeping
335
+     */
336
+    public function sleepDelay(string $ip, string $action = ''): int {
337
+        $delay = $this->getDelay($ip, $action);
338
+        usleep($delay * 1000);
339
+        return $delay;
340
+    }
341
+
342
+    /**
343
+     * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
344
+     * In this case a "429 Too Many Request" exception is thrown
345
+     *
346
+     * @param string $ip
347
+     * @param string $action optionally filter by action
348
+     * @return int the time spent sleeping
349
+     * @throws MaxDelayReached when reached the maximum
350
+     */
351
+    public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
352
+        $delay = $this->getDelay($ip, $action);
353
+        if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
354
+            // If the ip made too many attempts within the last 30 mins we don't execute anymore
355
+            throw new MaxDelayReached('Reached maximum delay');
356
+        }
357
+        usleep($delay * 1000);
358
+        return $delay;
359
+    }
360 360
 }
Please login to merge, or discard this patch.