Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"ext-curl": "*",
"ext-redis": "*",
"utopia-php/database": "5.*",
"utopia-php/pools": "1.*",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
premtsd-code marked this conversation as resolved.
"appwrite/appwrite": "^26.0"
},
"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions src/Abuse/Adapters/TimeLimit/None.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Utopia\Abuse\Adapters\TimeLimit;

use Utopia\Abuse\Adapters\TimeLimit;

class None extends TimeLimit
{
/**
* @var int
*/
protected int $ttl;

public function __construct(string $key, int $limit, int $seconds)
{
$this->key = $key;
$this->ttl = $seconds;
$now = \time();
$this->timestamp = (int) ($now - ($now % $seconds));
$this->limit = $limit;
}

protected function count(string $key, int $timestamp): int
{
return 0;
}

protected function hit(string $key, int $timestamp): void
{
}

protected function set(string $key, int $timestamp, int $value): void
{
}

/**
* Get abuse logs
*
* Return logs with an offset and limit
*
* @param int|null $offset
* @param int|null $limit
* @return array<string, mixed>
*/
public function getLogs(?int $offset = null, ?int $limit = 25): array
{
return [];
}

/**
* Delete all logs older than $timestamp
*
* @param int $timestamp
* @return bool
*/
public function cleanup(int $timestamp): bool
{
return true;
}
}
146 changes: 146 additions & 0 deletions src/Abuse/Adapters/TimeLimit/RedisPool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace Utopia\Abuse\Adapters\TimeLimit;

use Throwable;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Pools\Pool as UtopiaPool;

class RedisPool extends TimeLimit
{
/**
* @var int
*/
protected int $ttl;

/**
* @param UtopiaPool<\Redis> $pool
*/
public function __construct(
string $key,
int $limit,
int $seconds,
protected UtopiaPool $pool
) {
$this->key = $key;
$this->ttl = $seconds;
$now = \time();
$this->timestamp = (int) ($now - ($now % $seconds));
$this->limit = $limit;
}

protected function count(string $key, int $timestamp): int
{
if (0 == $this->limit) {
return 0;
}

if (!\is_null($this->count)) {
return $this->count;
}

/** @var int $count */
$count = $this->pool->use(function (\Redis $redis) use ($key, $timestamp): int {
$count = $redis->get(Redis::NAMESPACE . '__' . $key . '__' . $timestamp);

return \is_numeric($count) ? (int) $count : 0;
});

$this->count = $count;

return $this->count;
}

protected function hit(string $key, int $timestamp): void
{
if (0 == $this->limit) {
return;
}

$ttl = $this->ttl;
$key = Redis::NAMESPACE . '__' . $key . '__' . $timestamp;

$this->pool->use(function (\Redis $redis) use ($key, $ttl): void {
$redis->multi();
try {
$redis->incr($key);
$redis->expire($key, $ttl);
$redis->exec();
} catch (Throwable $th) {
$this->discard($redis);
throw $th;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
});

$this->count = ($this->count ?? 0) + 1;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
}

protected function set(string $key, int $timestamp, int $value): void
{
$ttl = $this->ttl;
$key = Redis::NAMESPACE . '__' . $key . '__' . $timestamp;

$this->pool->use(function (\Redis $redis) use ($key, $ttl, $value): void {
$redis->multi();
try {
$redis->set($key, (string) $value);
$redis->expire($key, $ttl);
$redis->exec();
} catch (Throwable $th) {
$this->discard($redis);
throw $th;
}
});

$this->count = $value;
}

/**
* Get abuse logs
*
* Return logs with an offset and limit
*
* @param int|null $offset
* @param int|null $limit
* @return array<string, mixed>
*/
public function getLogs(?int $offset = null, ?int $limit = 25): array
{
/** @var array<string, mixed> $result */
$result = $this->pool->use(function (\Redis $redis) use ($limit) {
$cursor = null;
$keys = $redis->scan($cursor, Redis::NAMESPACE . '__*', $limit);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
if (!$keys) {
return [];
}

$logs = [];
foreach ($keys as $key) {
$logs[$key] = $redis->get($key);
}

return $logs;
});

return $result;
}

/**
* Delete all logs older than $timestamp
*
* @param int $timestamp
* @return bool
*/
public function cleanup(int $timestamp): bool
{
return true;
}

private function discard(\Redis $redis): void
{
try {
$redis->discard();
} catch (Throwable) {
}
}
}
38 changes: 38 additions & 0 deletions tests/Abuse/NoneTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Utopia\Tests;

use PHPUnit\Framework\TestCase;
use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit\None;

class NoneTest extends TestCase
{
public function testNeverLimitsRequests(): void
{
$adapter = new None('none-key', 1, 60);
$abuse = new Abuse($adapter);

$this->assertSame(false, $abuse->check());
$this->assertSame(false, $abuse->check());
$this->assertSame(false, $abuse->check());
}

public function testReturnsNoLogsAndCleanupSucceeds(): void
{
$adapter = new None('none-key', 1, 60);

$this->assertSame([], $adapter->getLogs());
$this->assertSame(true, $adapter->cleanup(time()));
}

public function testResetIsNoop(): void
{
$adapter = new None('none-key', 1, 60);
$abuse = new Abuse($adapter);

$abuse->reset();

$this->assertSame(false, $abuse->check());
}
}
53 changes: 53 additions & 0 deletions tests/Abuse/RedisPoolTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Utopia\Tests;

use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Abuse\Adapters\TimeLimit\RedisPool as AdapterRedisPool;
use Utopia\Pools\Adapter\Stack;
use Utopia\Pools\Pool;

class RedisPoolTest extends Base
{
/**
* @var Pool<\Redis>|null
*/
protected static ?Pool $pool = null;

public static function setUpBeforeClass(): void
{
if (isset(self::$pool)) {
return;
}

self::$pool = new Pool(new Stack(), 'abuse-redis', 2, function (): \Redis {
$redis = new \Redis();
$redis->connect('redis', 6379);

return $redis;
});
}

public function getAdapter(string $key, int $limit, int $seconds): TimeLimit
{
$pool = self::$pool;
$this->assertInstanceOf(Pool::class, $pool);

/** @var Pool<\Redis> $pool */
return new AdapterRedisPool('redis-pool-' . $key, $limit, $seconds, $pool);
}

public static function tearDownAfterClass(): void
{
if (!isset(self::$pool)) {
return;
}

self::$pool->use(function (mixed $redis): void {
if ($redis instanceof \Redis) {
$redis->close();
}
});
self::$pool = null;
}
}
Loading