RateLimit(3pm)

πŸ“¦
version 1.0 Β· 4 versions
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– RateLimit(3pm) β€” CGI::Application::Plugin::RateLimit πŸ’–

okay bestie, sit down, hydrate, because we're about to talk about limiting how often a user can call your runmodes and honestly? it's giving responsible adult. this one lives in the User Contributed Perl Documentation corner of your man pages, and it's genuinely so useful.


🌸 NAME 🌸

CGI::Application::Plugin::RateLimit β€” limits runmode call rate per user. that's it, that's the whole vibe. no cap.


✨ SYNOPSIS ✨

here's the whole thing, verbatim, because code is sacred and we do not paraphrase code:

use CGI::Application::Plugin::RateLimit;

sub setup {
  ...

  # call this in your setup routine to set
  my $rate_limit = $self->rate_limit();

  # set the database handle to use
  $rate_limit->dbh($dbh);

  # set the table name to use for storing hits, the default is
  # 'rate_limit_hits'
  $rate_limit->table('rate_limit_hits');

  # keep people from calling 'send' more often than 5 times in 10
  # minutes and 'list' more often than once every 5 seconds.
  $rate_limit->protected_modes(send => {timeframe => '10m',
                                        max_hits  => 5
                                       },
                               list => {timeframe => '5s',
                                        max_hits  => 1
                                       });

  # you can also protect abstract actions, for example to prevent a
  # flood of failed logins
  $rate_limit->protected_actions(failed_login => {timeframe => '10s',
                                                  max_hits  => 2
                                                 });

  # call this runmode when a violation is detected
  $rate_limit->violation_mode('too_fast_buddy');

  # or, run this callback
  $rate_limit->violation_callback(sub { die(...) });

  # override the default identity function
  # ($ENV{REMOTE_USER} || $ENV{REMOTE_IP})
  $rate_limit->identity_callback(sub { ... });
}

# record a hit for an action (not needed for run-modes which are
# handled automatically)
$rate_limit->record_hit(action => 'failed_login');

# check for a violation on an action and handle
return $self->slow_down_buddy
  if( $rate_limit->check_violation(action => 'failed_login') );

# revoke the most recent hit for this user, preventing it from
# counting towards a violation
$rate_limit->revoke_hit();

# examine the violation in violation_mode or violation_callback:
$mode   = $rate_limit->violated_mode;
$action = $rate_limit->violated_action;
$limits = $rate_limit->violated_limits;

πŸ¦‹ DESCRIPTION πŸ¦‹

so the whole point of this module is protecting you from a user calling a runmode way too many times. classic use-case? a contact form that sends email. you want people to message you, obviously, but thousands of messages from a single user is not cute. it's not the vibe. it's a problem.

how does it work? it keeps a database of hits to protected runmodes. then it checks that database to decide if a new hit should be allowed, based on what that user's been up to lately. the user's identity defaults to being tied to login (via REMOTE_USER) or IP address (via REMOTE_IP) if there's no login info available. and if you don't like either of those, you can bring your own identity function with identity_callback(). main character energy, your rules.

to use this module you must make a table in your database with this schema β€” it's MySQL syntax, though other DBs might work with minor alterations:

CREATE TABLE rate_limit_hits (
   user_id   VARCHAR(255)      NOT NULL,
   action    VARCHAR(255)      NOT NULL,
   timestamp UNSIGNED INTEGER  NOT NULL,
   INDEX (user_id, action, timestamp)
);

feel free to switch up the storage-type and size of user_id and action to match how you actually use it. like, if your identity_callback() always returns an integer, bestie, make user_id an integer column. slay.

this table needs to be periodically cleared of old data. anything older than the maximum timeframe you're using can be safely deleted. that's the whole spring-cleaning rule. 🧹

⚠️ IMPORTANT NOTE: the protection this module offers is not perfect. identifying a user on the internet is very hard, and a sophisticated attacker can work around these checks by switching IPs or automating login creation. it's a vibe check, not a fortress. πŸ’€

πŸ‘‘ INTERFACE πŸ‘‘

the object you get back from calling $self->rate_limit on your CGI::App object supports all of these method calls:

πŸŽ€ dbh

$rate_limit->dbh($dbh);

call this to set the database handle the object should use. must be set in setup(). no exceptions, bestie, it needs to know where to look.

πŸŽ€ table

$rate_limit->table('some_table_name');

call this to decide which table stores and looks up hits. the default is 'rate_limit_hits' if you don't set it. the required table schema lives up in the DESCRIPTION section, go look, i'll wait. πŸ’…

πŸŽ€ protected_modes

$rate_limit->protected_modes(send => {timeframe => '10m',
                                      max_hits  => 5
                                     },
                             list => {timeframe => '5s',
                                      max_hits  => 1
                                     });

takes a list of key-value pairs describing the modes to protect. keys are names of run-modes. values are hashes with these keys:

  • timeframe β€” the timeframe to be considered for violations. values must be numbers followed by either 's' for seconds, 'm' for minutes or 'h' for hours.
  • max_hits β€” how many hits to allow in the specified timeframe before triggering a violation.

πŸŽ€ protected_actions

$rate_limit->protected_actions(failed_login => {timeframe => '10s',
                                                max_hits  => 2
                                               });

specifies non-run-mode actions to protect. these are arbitrary keys you can use with record_hit() and check_violation(). takes the exact same data-structure as protected_modes(). ✨

πŸŽ€ violation_mode

$rate_limit->violation_mode('too_fast_buddy');

call this to set a run-mode to call when a violation gets triggered. either this or violation_callback must be set. you can't have neither, that's not a thing.

πŸŽ€ violation_callback

$rate_limit->violation_callback(sub { ... });

the callback to call when a violation is detected. it should either throw an exception or return the run-mode to run. it gets called with the CGI::App object as its sole parameter.

πŸŽ€ identity_callback

$rate_limit->identity_callback(sub { ... });

call this to bring your own customized mechanism for figuring out who the user is. the default is:

sub { $ENV{REMOTE_USER} || $ENV{REMOTE_IP} }

you might wanna add in session-ID or a hook to your authentication system if it doesn't use REMOTE_USER. whatever you write should return a single scalar that's expected to be unique to each user. one scalar. that's the rule. 🫢

πŸŽ€ record_hit

$rate_limit->record_hit(action => 'failed_login');

records a hit for an arbitrary action. this is not needed for run-mode protection (that's handled for you). takes the action name as an argument, which must match an action registered with protected_actions().

πŸŽ€ check_violation

return $self->slow_down_buddy
  if( $rate_limit->check_violation(action => 'failed_login') );

checks for a violation of a protected action. again, not needed for run-mode protection. takes the action name as an argument, which must match an action registered with protected_actions().

returns 1 if a violation took place, 0 otherwise. simple as. πŸ“

πŸŽ€ revoke_hit

$rate_limit->revoke_hit();

revokes the last hit for this user. you might use this so validation errors don't count against a user, for example. it's the "oops bestie, that one doesn't count" button. πŸ™ˆ

πŸŽ€ violated_mode

$mode = $rate_limit->violated_mode;

returns the mode for the last violation, or undef if an action caused the violation.

πŸŽ€ violated_action

$mode = $rate_limit->violated_action;

returns the action for the last violation, or undef if an action caused the violation.

πŸŽ€ violated_limits

$limits = $rate_limit->violated_limits;

returns the hash-ref that was passed to protected_actions() or protected_modes() for the violated mode/action. receipts. 🧾


🌈 DATABASE SUPPORT 🌈

this has been tested with MySQL and SQLite. it's likely to work with many other databases too β€” please let the author know if you try one. πŸ‘‘


πŸ’œ SUPPORT πŸ’œ

questions and suggestions about this module go to the CGI::Application mailing-list. to join the mailing list, simply send a blank message to:

cgiapp-subscribe@lists.erlbaum.net

⭐ VERSION CONTROL ⭐

this module lives in a public Subversion repository at SourceForge, right here:

https://svn.sourceforge.net/svnroot/html-template/trunk/CGI-Application-Plugin-RateLimit

πŸ’€ BUGS πŸ’€

the author knows of no bugs. if you find one, let them know by filing a report on http://rt.cpan.org. failing that, you can email sam@tregar.com. please include the version of the module you're using and a small test case demonstrating the problem. small test case, bestie, not your entire app. 😭


🫢 AUTHOR 🫢

Sam Tregar, sam@plusthree.com


Copyright (C) 2006 by Sam Tregar

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.6 or, at your option, any later version of Perl 5 you may have available.


and that's the whole page, gorgeous. πŸ’– this was RateLimit(3pm) from perl v5.34.0, dated 2022-06-09. go forth and rate-limit responsibly. mwah. πŸ’…βœ¨

πŸ“¦ where this page comes from