Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat Redis Adapter #70

Merged
merged 21 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
17 changes: 17 additions & 0 deletions Dockerfile.php-8.1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ LABEL maintainer="team@appwrite.io"

RUN docker-php-ext-install pdo_mysql

RUN \
apk update \
&& apk add --no-cache make automake autoconf gcc g++ git zlib-dev libmemcached-dev \
&& rm -rf /var/cache/apk/*

RUN \
# Redis Extension
git clone https://github.com/phpredis/phpredis.git && \
cd phpredis && \
git checkout $PHP_REDIS_VERSION && \
phpize && \
./configure && \
make && make install && \
cd ..

RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini

WORKDIR /code

COPY --from=step0 /src/vendor /code/vendor
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"php": ">=8.0",
"ext-pdo": "*",
"ext-curl": "*",
"ext-redis": "*",
"utopia-php/database": "0.49.*"
},
"require-dev": {
Expand Down
8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ services:
- abuse
ports:
- "9307:3306"


redis:
image: redis:6.0-alpine
container_name: redis
networks:
- abuse

tests:
build:
context: .
Expand Down
60 changes: 56 additions & 4 deletions src/Abuse/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,68 @@

namespace Utopia\Abuse;

interface Adapter
abstract class Adapter
{
/**
* @var array<string, string>
*/
protected array $params = [];

/**
* @var string
*/
protected string $key = '';

/**
* Check
*
* Checks if number of counts is bigger or smaller than current limit
*
* @return bool
*/
public function check(): bool;
abstract public function check(): bool;

/**
* Set Param
*
* Set custom param for key pattern parsing
*
* @param string $key
* @param string $value
* @return $this
*/
public function setParam(string $key, string $value): self
{
$this->params[$key] = $value;

return $this;
}

/**
* Get Params
*
* Return array of all key params
*
* @return array<string, string>
*/
protected function getParams(): array
{
return $this->params;
}

/**
* Parse key with all custom attached params
*
* @return string
*/
protected function parseKey(): string
{
foreach ($this->getParams() as $key => $value) {
$this->key = \str_replace($key, $value, $this->key);
}

return $this->key;
}

/**
* Get abuse logs
Expand All @@ -22,13 +74,13 @@ public function check(): bool;
* @param int|null $limit
* @return array<string, mixed>
*/
public function getLogs(?int $offset = null, ?int $limit = 25): array;
abstract public function getLogs(?int $offset = null, ?int $limit = 25): array;

/**
* Delete all logs older than $datetime
*
* @param string $datetime
* @return bool
*/
public function cleanup(string $datetime): bool;
abstract public function cleanup(string $datetime): bool;
}
2 changes: 1 addition & 1 deletion src/Abuse/Adapters/ReCaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Exception;
use Utopia\Abuse\Adapter;

class ReCaptcha implements Adapter
class ReCaptcha extends Adapter
{
/**
* Use this for communication between your site and Google.
Expand Down
177 changes: 177 additions & 0 deletions src/Abuse/Adapters/Redis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php

namespace Utopia\Abuse\Adapters;

use Utopia\Abuse\Adapter;
use Redis as Client;

class Redis extends Adapter
{
public const NAMESPACE = 'abuse';

/**
* @var Client
*/
protected Client $redis;

/**
* @var int
*/
protected int $time;

/**
* @var int
*/
protected int $limit = 0;

/**
* @var int|null
*/
protected ?int $count = null;

public function __construct(string $key, int $limit, int $seconds, Client $redis)
{
$this->redis = $redis;
$this->key = $key;
$time = (int) \date('U', (int) (\floor(\time() / $seconds)) * $seconds); // todo: any good Idea without time()?
eldadfux marked this conversation as resolved.
Show resolved Hide resolved
$this->time = $time;
$this->limit = $limit;
}

/**
* Undocumented function
*
* @param string $key
* @param int $datetime
* @return integer
*/
protected function count(string $key, int $datetime): int
{
if (0 == $this->limit) { // No limit no point for counting
return 0;
}

if (! \is_null($this->count)) { // Get fetched result
return $this->count;
}

$count = $this->redis->get(self::NAMESPACE . ':'. $key .':'. $datetime);
if (!$count) {
$this->count = 0;
} else {
$this->count = (int) $count;
}

return $this->count;
}

/**
* @param string $key
* @param int $datetime
* @return void
*
*/
protected function hit(string $key, int $datetime): void
{
if (0 == $this->limit) { // No limit no point for counting
return;
}

$count = $this->redis->get(self::NAMESPACE . ':'. $key .':'. $datetime);
eldadfux marked this conversation as resolved.
Show resolved Hide resolved
if (!$count) {
$this->count = 0;
} else {
$this->count = (int) $count;
}

$this->redis->incr(self::NAMESPACE . ':'. $key .':'. $datetime);
$this->count++;
}

/**
* Check
*
* Checks if number of counts is bigger or smaller than current limit
*
* @return bool
*/
public function check(): bool
{
if (0 == $this->limit) {
return false;
}

$key = $this->parseKey();

if ($this->limit > $this->count($key, $this->time)) {
$this->hit($key, $this->time);

return false;
}

return true;
}

/**
* 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
{
// TODO limit potential is SCAN but needs cursor no offset
$cursor = null;
$keys = $this->redis->scan($cursor, self::NAMESPACE . ':*', $limit);
if (!$keys) {
return [];
}

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

/**
* Delete all logs older than $datetime
*
* @param string $datetime
* @return bool
*/
public function cleanup(string $datetime): bool
{
// TODO
$iterator = null;
while ($iterator !== 0) {
$keys = $this->redis->scan($iterator, self::NAMESPACE . ':*:*', 1000);
$keys = $this->filterKeys($keys ? $keys : [], (int) $datetime);
$this->redis->del($keys);
}
return true;
}

/**
* Filter keys
*
* @param array<string> $keys
* @param integer $timestamp
* @return array<string>
*/
protected function filterKeys(array $keys, int $timestamp): array
{
$filteredKeys = [];
foreach ($keys as $key) {
$parts = explode(':', $key);
$keyTimestamp = (int)end($parts); // Assuming the last part is always the timestamp
if ($keyTimestamp < $timestamp) {
$filteredKeys[] = $key;
}
}
return $filteredKeys;
}
}
Loading
Loading