diff options
Diffstat (limited to 'lib/Plack/Middleware/APIRateLimit.pm')
-rw-r--r-- | lib/Plack/Middleware/APIRateLimit.pm | 146 |
1 files changed, 140 insertions, 6 deletions
diff --git a/lib/Plack/Middleware/APIRateLimit.pm b/lib/Plack/Middleware/APIRateLimit.pm index 905e944..e94407c 100644 --- a/lib/Plack/Middleware/APIRateLimit.pm +++ b/lib/Plack/Middleware/APIRateLimit.pm @@ -1,27 +1,161 @@ package Plack::Middleware::APIRateLimit; -use strict; -use warnings; +use Moose; +use Carp; +use Scalar::Util; +use Plack::Util; +use DateTime; + our $VERSION = '0.01'; +extends 'Plack::Middleware'; + +has backend => + ( is => 'rw', isa => 'Plack::Middleware::APIRateLimit::Backend', ); +has requests_per_hour => + ( is => 'rw', isa => 'Int', lazy => 1, default => 60 ); +has key => ( is => 'rw', isa => 'Str', predicate => 'has_key' ); + +sub prepare_app { + my $self = shift; + $self->backend( $self->_create_backend( $self->backend ) ); +} + +sub _create_backend { + my ( $self, $backend ) = @_; + + return $backend if defined $backend && Scalar::Util::blessed $backend; + + my ( $backend_name, $backend_options ) = ( undef, {} ); + + if ( !defined $backend ) { + $backend_name = "Hash"; + } + elsif ( ref $backend eq 'ARRAY' ) { + $backend_name = shift @$backend; + $backend_options = shift @$backend; + } + else { + $backend_name = $backend; + } + + Plack::Util::load_class( + "Plack::Middleware::APIRateLimit::Backend::" . $backend_name ) + ->new($backend_options); +} + +sub call { + my ( $self, $env ) = @_; + + my $res = $self->app->($env); + + my $key = $self->_generate_key($env); + + $self->backend->incr($key); + my $request_done = $self->backend->get($key); + + return $self->over_rate_limit() + if $request_done > $self->requests_per_hour; + + my $headers = $res->[1]; + Plack::Util::header_set( $headers, 'X-RateLimit-Limit', + $self->requests_per_hour ); + Plack::Util::header_set( $headers, 'X-RateLimit-Remaining', + ( $self->requests_per_hour - $request_done ) ); + Plack::Util::header_set( $headers, 'X-RateLimit-Reset', + $self->_reset_time ); + return $res; +} + +sub _generate_key { + my ( $self, $env ) = @_; + return $self->key if $self->has_key; + if ( $env->{REMOTE_USER} ) { + return $env->{REMOTE_USER} . "_" + . DateTime->now->strftime("%Y-%m-%d-%H"); + } + else { + return $env->{REMOTE_ADDR} . "_" + . DateTime->now->strftime("%Y-%m-%d-%H"); + } +} + +sub _reset_time { + my $reset = time + ( 60 - DateTime->now->minute ) * 60; +} + +sub over_rate_limit { + my ($self) = @_; + return [ + 503, + [ + 'Content-Type' => 'text/plain', + 'X-RateLimit-Reset' => $self->_reset_time + ], + ['Over Rate Limit'] + ]; +} + 1; __END__ =head1 NAME -Plack::Middleware::APIRateLimit - +Plack::Middleware::APIRateLimit - A Plack Middleware for API Throttling =head1 SYNOPSIS - use Plack::Middleware::APIRateLimit; + my $handler = builder { + enable "APIRateLimit"; + # or + enable "APIRateLimit", requests_per_hour => 2, backend => "Hash"; + # or + enable "APIRateLimit", requests_per_hour => 2, backend => ["Redis", {port => 6379, server => '127.0.0.1'}]; + sub { [ '200', [ 'Content-Type' => 'text/html' ], ['hello world'] ] }; + }; =head1 DESCRIPTION -Plack::Middleware::APIRateLimit is +Plack::Middleware::APIRateLimit is a Plack middleware for controlling API +access. + +Set a limit on how many requests per hour is allowed on your API. In the case +of a authorized request, 3 headers are added: + +=over 2 + +=item B<X-RateLimit-Limit> + +How many requests are authorized by hours + +=item B<X-RateLimit-Remaining> + +How many remaining requests + +=item B<X-RateLimit-Reset> + +When will the counter be reseted (in epoch) + +=back + +=head2 VARIABLES + +=over 4 + +=item B<backend> + +Which backend to use. Currently only Hash and Redis are supported. If no +backend is specified, Hash is used by default. + +=item B<requests_per_hour> + +How many requests is allowed by hour. + +=back =head1 AUTHOR -franck cuny E<lt>franck.cuny@rtgi.frE<gt> +franck cuny E<lt>franck@linkfluence.netE<gt> =head1 SEE ALSO |