Skip to content

Instantly share code, notes, and snippets.

@tayhimself
Created August 11, 2017 13:42
Show Gist options
  • Save tayhimself/131c9c9216be938c6ca06ed5f2be68b5 to your computer and use it in GitHub Desktop.
Save tayhimself/131c9c9216be938c6ca06ed5f2be68b5 to your computer and use it in GitHub Desktop.
Rate limits with FOSAuthServerBundle
<?php
namespace MyApp\ApiBundle\Listener;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Doctrine\ORM\EntityManager;
use FOS\OAuthServerBundle\Storage\OAuthStorage;
use Symfony\Component\Security\Core\SecurityContext;
use Doctrine\Bundle\DoctrineBundle\Registry;
use JMS\DiExtraBundle\Annotation AS DI;
use Predis\Client;
/**
* @DI\Service
*/
class RateLimitListener
{
/**
* @var \Predis\Client
*/
private $redis;
/**
* @var \Symfony\Component\Security\Core\SecurityContext
*/
private $securityContext;
/**
* @var \FOS\OAuthServerBundle\Storage\OAuthStorage
*/
private $storage;
/**
* @var int
*/
private $limit;
/**
* @var int
*/
private $remaining;
/**
* @var int
*/
private $reset;
/**
* @var string
*/
private $key_limit;
/**
* @var string
*/
private $key_remaining;
/**
* @DI\InjectParams({
* "redis" = @DI\Inject("redis"),
* "securityContext" = @DI\Inject("security.context"),
* "storage" = @DI\Inject("fos_oauth_server.storage"),
* "keyLimit" = @DI\Inject("%redis.keys.ratelimit_limit%"),
* "keyRemaining" = @DI\Inject("%redis.keys.ratelimit_remaining%")
* })
*
* @param \Symfony\Component\Security\Core\SecurityContext $securityContext
*/
public function __construct(Client $redis, SecurityContext $securityContext, OAuthStorage $storage, $keyLimit, $keyRemaining)
{
$this->redis = $redis;
$this->securityContext = $securityContext;
$this->storage = $storage;
$this->key_limit = $keyLimit;
$this->key_remaining = $keyRemaining;
}
private function setHeaders(Response $response)
{
$response->headers->add(array(
'X-RateLimit-Limit' => $this->limit,
'X-RateLimit-Remaining' => $this->remaining,
'X-RateLimit-Reset' => $this->reset
));
}
/**
* @DI\Observe("kernel.request", priority = 7)
*/
public function onKernelRequest(GetResponseEvent $event)
{
if(HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType())
return;
if(substr($event->getRequest()->getRequestUri(), 0, 4) != "/api")
return;
$access_token = $this->securityContext->getToken()->getCredentials();
$keyLimit = sprintf($this->key_limit, $access_token);
$keyRemaining = sprintf($this->key_remaining, $access_token);
if($this->redis->exists($keyLimit))
{
$this->limit = $this->redis->get($keyLimit);
$this->remaining = $this->redis->get($keyRemaining);
$this->reset = time() + $this->redis->ttl($keyRemaining);
}
else
{
$token = $this->storage->getAccessToken($access_token);
$limit = $token->getClient()->getRateLimit();
if($limit === null)
return;
$this->redis->set($keyLimit, $this->limit = $limit);
$this->redis->set($keyRemaining, $this->remaining = $limit);
$this->redis->expire($keyLimit, 3600);
$this->redis->expire($keyRemaining, 3600);
$this->reset = time() + 3600;
}
$remaining = $this->redis->decr($keyRemaining);
$this->remaining = $remaining > 0 ? $remaining : 0;
if($this->remaining == 0)
{
$response = new Response(sprintf(
'You exceeded the rate limit of %d requests per hour.',
$this->limit
), 403);
$this->setHeaders($response);
$event->setResponse($response);
$event->stopPropagation();
return;
}
}
/**
* @DI\Observe("kernel.response")
*/
public function onKernelResponse(FilterResponseEvent $event)
{
if(HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType())
return;
if(substr($event->getRequest()->getRequestUri(), 0, 4) != "/api")
return;
$this->setHeaders($event->getResponse());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment