117 lines
4.5 KiB
PHP
117 lines
4.5 KiB
PHP
<?php
|
|
namespace Xxx\Tests\Unit;
|
|
|
|
use Xxx\Tests\Support\TestCase;
|
|
|
|
// Counting failed logins in session.lib.php
|
|
class LoginThrottleTest extends TestCase {
|
|
private string $user;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
// A new username and address for each test, so that the files of
|
|
// other tests (and other runs) do not count
|
|
$this->user = 'Ed-' . bin2hex(random_bytes(4));
|
|
$_SERVER['REMOTE_ADDR'] = '198.51.100.' . random_int(1, 254) . '-' . bin2hex(random_bytes(4));
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
foreach (array($this->user, 'other-' . $this->user) as $user) {
|
|
foreach (array_keys(login_limits($user)) as $key)
|
|
@unlink(login_throttle_file($key));
|
|
}
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function failLogins(int $times, ?string $user = NULL): void {
|
|
for ($i = 0; $i < $times; $i++)
|
|
record_login_failure($user ?? $this->user);
|
|
}
|
|
|
|
public function testLimitsArePerUserAndAddressPerAddressAndPerUser(): void {
|
|
$address = $_SERVER['REMOTE_ADDR'];
|
|
$user = strtolower($this->user);
|
|
$this->assertSame(array("user-ip:$user|$address" => 5, "ip:$address" => 20, "user:$user" => 50),
|
|
login_limits($this->user));
|
|
}
|
|
|
|
public function testUsernameCaseDoesNotMatter(): void {
|
|
$this->assertSame(login_limits(strtoupper($this->user)), login_limits(strtolower($this->user)));
|
|
}
|
|
|
|
public function testThrottleFilesAreHashedOutsideTheWebRoot(): void {
|
|
$file = login_throttle_file('ip:192.0.2.1');
|
|
$this->assertSame(sys_get_temp_dir() . '/xxx-login-throttle/' . hash('sha256', 'ip:192.0.2.1'), $file);
|
|
$this->assertDirectoryExists(dirname($file));
|
|
$this->assertSame(0700, fileperms(dirname($file)) & 0777);
|
|
}
|
|
|
|
public function testFourFailuresDoNotBlock(): void {
|
|
$this->failLogins(4);
|
|
$this->assertSame(0, login_blocked_for($this->user));
|
|
}
|
|
|
|
public function testFiveFailuresBlockForTheWindow(): void {
|
|
$this->failLogins(5);
|
|
$wait = login_blocked_for($this->user);
|
|
$this->assertGreaterThan(LOGIN_WINDOW - 5, $wait);
|
|
$this->assertLessThanOrEqual(LOGIN_WINDOW, $wait);
|
|
}
|
|
|
|
public function testEachFailureCountsForEveryKey(): void {
|
|
$this->failLogins(2);
|
|
foreach (array_keys(login_limits($this->user)) as $key)
|
|
$this->assertCount(2, login_failures($key));
|
|
}
|
|
|
|
public function testOldFailuresAreForgotten(): void {
|
|
$keys = array_keys(login_limits($this->user));
|
|
$old = time() - LOGIN_WINDOW - 1;
|
|
file_put_contents(login_throttle_file($keys[0]), json_encode(array_fill(0, 5, $old)));
|
|
$this->assertSame(array(), login_failures($keys[0]));
|
|
$this->assertSame(0, login_blocked_for($this->user));
|
|
}
|
|
|
|
public function testTheWaitEndsAWindowAfterTheOldestFailure(): void {
|
|
$keys = array_keys(login_limits($this->user));
|
|
$oldest = time() - 600;
|
|
file_put_contents(login_throttle_file($keys[0]), json_encode(array($oldest, time(), time(), time(), time())));
|
|
$this->assertSame($oldest + LOGIN_WINDOW - time(), login_blocked_for($this->user));
|
|
}
|
|
|
|
public function testDamagedFilesCountAsNoFailures(): void {
|
|
$keys = array_keys(login_limits($this->user));
|
|
file_put_contents(login_throttle_file($keys[0]), 'not json');
|
|
$this->assertSame(array(), login_failures($keys[0]));
|
|
file_put_contents(login_throttle_file($keys[0]), json_encode(array('1', NULL, time())));
|
|
$this->assertCount(1, login_failures($keys[0]));
|
|
}
|
|
|
|
public function testTwentyFailuresFromOneAddressBlockEveryUser(): void {
|
|
$this->failLogins(4);
|
|
for ($i = 0; $i < 4; $i++)
|
|
$this->failLogins(4, "user$i-" . $this->user);
|
|
$this->assertGreaterThan(0, login_blocked_for('other-' . $this->user));
|
|
for ($i = 0; $i < 4; $i++) {
|
|
foreach (array_keys(login_limits("user$i-" . $this->user)) as $key)
|
|
@unlink(login_throttle_file($key));
|
|
}
|
|
}
|
|
|
|
public function testFiftyFailuresForAUserBlockEveryAddress(): void {
|
|
$keys = array_keys(login_limits($this->user));
|
|
file_put_contents(login_throttle_file($keys[2]), json_encode(array_fill(0, 50, time())));
|
|
$_SERVER['REMOTE_ADDR'] = '203.0.113.9-' . bin2hex(random_bytes(4));
|
|
$this->assertGreaterThan(0, login_blocked_for($this->user));
|
|
}
|
|
|
|
public function testClearingForgetsOnlyThisUserFromThisAddress(): void {
|
|
$this->failLogins(3);
|
|
clear_login_failures($this->user);
|
|
$keys = array_keys(login_limits($this->user));
|
|
$this->assertSame(array(), login_failures($keys[0]));
|
|
$this->assertCount(3, login_failures($keys[1]));
|
|
$this->assertCount(3, login_failures($keys[2]));
|
|
}
|
|
}
|