summaryrefslogblamecommitdiff
path: root/lib/Plack/Middleware/APIRateLimit.pm
blob: e94407ce661bb7c2cee41e01cf927a853907f57a (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                                        





                 

                      























































































                                                                          




           
                                                                       


               







                                                                                                               


                  



































                                                                             


             
                                            








                                                                    
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<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@linkfluence.netE<gt>

=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