summaryrefslogtreecommitdiff
path: root/lib/Plack/Middleware/APIRateLimit.pm
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/Plack/Middleware/APIRateLimit.pm146
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