Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['8.2', '8.3', '8.4']
php-versions: ['8.4', '8.5']

steps:
- name: Checkout repository
Expand All @@ -31,4 +31,4 @@ jobs:
sleep 10

- name: Run Tests
run: docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml
run: docker compose exec tests vendor/bin/phpunit --configuration phpunit.xml
27 changes: 27 additions & 0 deletions Dockerfile.php-8.5
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM composer:2.0 as step0

WORKDIR /src/

COPY composer.lock /src/
COPY composer.json /src/

RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist

FROM appwrite/utopia-base:php-8.5-2.1.0 as final

LABEL maintainer="team@appwrite.io"

RUN docker-php-ext-install pdo_mysql

WORKDIR /code

COPY --from=step0 /src/vendor /code/vendor

# Add Source Code
COPY ./tests /code/tests
COPY ./src /code/src
COPY ./phpunit.xml /code/phpunit.xml
COPY ./phpbench.json /code/phpbench.json

CMD [ "tail", "-f", "/dev/null" ]
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
"bench": "vendor/bin/phpbench run --report=aggregate"
},
"require": {
"php": ">=8.2",
"php": ">=8.4",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
"ext-pdo": "*",
"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
4 changes: 2 additions & 2 deletions composer.lock

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

2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ services:
tests:
build:
context: .
dockerfile: Dockerfile.php-${PHP_VERSION:-8.3}
dockerfile: Dockerfile.php-${PHP_VERSION:-8.4}
networks:
- abuse
depends_on:
Expand Down
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;
}
}
191 changes: 191 additions & 0 deletions src/Abuse/Adapters/TimeLimit/RedisPool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

namespace Utopia\Abuse\Adapters\TimeLimit;

use RuntimeException;
use Throwable;
use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Pools\Connection;
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->useRedis(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->useRedis(function (\Redis $redis) use ($key, $ttl): void {
$redis->multi();
$redis->incr($key);
$redis->expire($key, $ttl);
$result = $redis->exec();

if (!\is_array($result) || \in_array(false, $result, true)) {
throw new RuntimeException('Redis transaction failed.');
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}, true);

$this->count = ($this->count ?? 0) + 1;
}

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

$this->useRedis(function (\Redis $redis) use ($key, $ttl, $value): void {
$redis->multi();
$redis->set($key, (string) $value);
$redis->expire($key, $ttl);
$result = $redis->exec();

if (!\is_array($result) || \in_array(false, $result, true)) {
throw new RuntimeException('Redis transaction failed.');
}
}, true);

$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
{
$offset = $offset ?? 0;
$limit = $limit ?? 25;

/** @var array<string, mixed> $result */
$result = $this->useRedis(function (\Redis $redis) use ($offset, $limit) {
$cursor = null;
$matches = [];
$pattern = Redis::NAMESPACE . '__*';

do {
$keys = $redis->scan($cursor, $pattern, 100);
if ($keys !== false) {
\array_push($matches, ...$keys);
}
} while ($cursor > 0);

\sort($matches);
$matches = \array_slice($matches, $offset, $limit);

if (empty($matches)) {
return [];
}

$logs = [];
foreach ($matches 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;
}

/**
* @template T
* @param callable(\Redis): T $callback
* @return T
* @throws Throwable
*/
private function useRedis(callable $callback, bool $discardTransaction = false): mixed
{
/** @var Connection<\Redis> $connection */
$connection = $this->pool->pop();
$redis = $connection->getResource();

try {
$result = $callback($redis);
} catch (Throwable $th) {
if ($discardTransaction) {
$this->discard($redis);
}
try {
$connection->destroy();
} catch (Throwable) {
}
throw $th;
}

$connection->reclaim();

return $result;
}

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());
}
}
Loading
Loading