package Plack::Middleware::APIRateLimit; 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 - A Plack Middleware for API Throttling =head1 SYNOPSIS 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 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 How many requests are authorized by hours =item B How many remaining requests =item B When will the counter be reseted (in epoch) =back =head2 VARIABLES =over 4 =item B Which backend to use. Currently only Hash and Redis are supported. If no backend is specified, Hash is used by default. =item B How many requests is allowed by hour. =back =head1 AUTHOR franck cuny Efranck@linkfluence.netE =head1 SEE ALSO =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut