Skip to content

Instantly share code, notes, and snippets.

@MindSculpt
Created July 11, 2016 18:49
Show Gist options
  • Save MindSculpt/0a5cfc41ae3f39ebd8f9c801a71c2f24 to your computer and use it in GitHub Desktop.
Save MindSculpt/0a5cfc41ae3f39ebd8f9c801a71c2f24 to your computer and use it in GitHub Desktop.
Rate limiting concept approach representing an alternate method to IP blocking or user registration to keep spam voting under control. This is a loose outline of how you might accomplish this. Requires MySQL database and MVC project structure.
<?php
/*
Rate limiting concept approach. Determines if rating is valid and returns JSON response to an AJAX call to this class. Uses Fat-Free PHP framework syntax.
This does not guarantee 100% unique vote submissions. It represents an alternate method to IP blocking or user registration to keep spam voting under control. It provides a loose outline of how you might approach this.
Status codes are completely custom for use in testing only.
Based loosely on http://blog.thelonepole.com/2013/03/preventing-spam-votes-in-online-polls/
Tables used for this concept:
CREATE TABLE `ratings` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`rater_ip` varchar(128) DEFAULT NULL,
`form_id` varchar(128) DEFAULT NULL,
`rater_url` varchar(140) DEFAULT NULL,
`value` int(11) DEFAULT NULL,
`date_rated` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=69 DEFAULT CHARSET=latin1;
CREATE TABLE `rate_limit` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`form_id` varchar(128) DEFAULT NULL,
`ip` varchar(128) DEFAULT NULL,
`flagged` tinyint(1) DEFAULT NULL,
`timeout` int(11) DEFAULT NULL,
`timeout_period` datetime DEFAULT NULL,
`grace_period` datetime DEFAULT NULL,
`initial_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `forms` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`question_id` int(11) DEFAULT NULL,
`guid` varchar(36) DEFAULT NULL,
`date_modified` datetime DEFAULT NULL,
`active` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=latin1;
*/
namespace Controllers;
class Form extends Controller {
// api call for public ratings from iframe embed
public function publicRate($f3, $params) {
$guid = $f3->get('POST.guid'); // get the guid from the submitted form
$raterURL = $f3->get('POST.rater_url'); // get the url from the site the form is on
// assumes you have a Form model with a findByGuid method
$form = new \Models\Form(); // Init new form
// find this form in our database
$form->findByGuid($guid);
if (is_null($form->id)) { // If not found...
$f3->error(404); // throw a 404
$json_result = '({"error": "Error!"});';
header("Content-Type: application/json; charset=utf-8");
echo $json_result;
}
$user = new \Models\User(); // Init new User
$user->findById($form->user_id); // find User by the rated form's user_id so we can include company info
// ---------------------------------------------------------------------------------
// do your form post checking here first, create new user, or handle errors
// ---------------------------------------------------------------------------------
// 1. Check to see if the ip already exists in the database
// rate limit test
// the gist:
/*
Let's say the same user votes 5 times then the system locks them out for 5 mins. Then theres a 5 min grace period where if i break the threshold again it locks them out for 10 min and so on. If the grace period expires and the user hasn't broken the threshold then it's reset. So if they break it again it starts at a 5 min timeout again.
*/
// attempt to get the user's IP
$ipaddress = '';
if (getenv('HTTP_CLIENT_IP'))
$ipaddress = getenv('HTTP_CLIENT_IP');
else if(getenv('HTTP_X_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_X_FORWARDED_FOR');
else if(getenv('HTTP_X_FORWARDED'))
$ipaddress = getenv('HTTP_X_FORWARDED');
else if(getenv('HTTP_FORWARDED_FOR'))
$ipaddress = getenv('HTTP_FORWARDED_FOR');
else if(getenv('HTTP_FORWARDED'))
$ipaddress = getenv('HTTP_FORWARDED');
else if(getenv('REMOTE_ADDR'))
$ipaddress = getenv('REMOTE_ADDR');
else
$ipaddress = 'UNKNOWN';
$interval = '5'; // threshold as a string in minutes; interval to check duplicate ip records against
$grace_period = 5; // limit to 5 votes per $timeout
$timeout = 5; // initial time limit to check against
$time_multiplier = 1; // default amount to multiply grace_period and time_out by when spam is detected
$status_code = ''; // we'll send custom status codes back to the form so we can keep track of what's happening
$allow_rating = false; // assume guilty until proven innocent, this boolean ultimately controls if a rating can be submitted
// first check if the ip address of the user submitting the rating exists more than 4 times in the ratings table
// the threshold is hard-coded in the findByIP method so we can calculate it right in the sql:
/*
Example Model method:
public static function findByIP($form_id, $ip, $interval) {
$object = new self;
return $object->count(array('form_id = ? AND rater_ip = ? AND DATE_SUB(NOW(), INTERVAL ? MINUTE) <= date_rated',$form_id, $ip, $interval)); // get all ratings submitted today within the threshold of the last 5 minutes
}
*/
// check to see if there are more than 4 ratings for this form based on IP address in the last ($interval) minutes. Assumes you have a model called Ratings.
$ratings_check = \Models\Rating::findByIP($form->id, $ipaddress, $interval);
// check to see if the ip has already been flagged in the database. Assumes you have a model called RateLimit.
/*
Example Model method:
public static function findByIP($form_id, $ip) {
$object = new self;
return $object->load(array('form_id = ? AND ip = ?',$form_id, $ip)); // get all previously flagged IPs
}
*/
$rate_limit = \Models\RateLimit::findByIP($form->id, $ipaddress);
// set up default vars
$status_code = '200'; // assume the rating will return success
$message = ''; //
// first check for existing flagged ip to see if grace period has ended, if so, use the grace period timeframe as the check
if (!empty($rate_limit)) {
// check if the timeout or grace period has ended
$now = new \DateTime(); // get the current time
$timeout_ends = new \DateTime($rate_limit->timeout_period); // get the current timeout period end date
$grace_period_ends = new \DateTime($rate_limit->grace_period); // get the current grace period end date
// previously flagged ip was found
if ($now > $grace_period_ends) {
// if the grace period has ended, do some db cleanup and remove the IP
// the gist: either the user is new and happens to be on the same IP, or the original offender waited out the grace period
// reset the flag on this ip by removing it from the db
$rate_limit->erase(); // erase
// ok to accept rating
$allow_rating = true;
$status_code = '200 [GPE]'; // GPE = grace period ended
} else if ($now < $timeout_ends) {
// not ok to accept rating
$allow_rating = false;
$status_code = '503 [TO]'; // TO = Time out
$message = "You rated this form on ".date('l, F j, Y', strtotime($rate_limit->initial_date)).'.';
// since the grace period hasn't ended, figure out if this new rating is being submitted during the grace period, which is inbetween the timeout and the grace period
} else if ($now > $timeout_ends && $now < $grace_period_ends) {
// if more than 4 ratings were detected again, reset the rate limit and block the user again starting now
if ($ratings_check > 4) {
// double the period again
$timeout = $rate_limit->timeout; // get the current timeout period stored for this IP
$time_multiplier = 2; // set the time_multiplier to 2, so we can double the timeout/grace period for this IP
// set the timeout duration based on the timeout_period stored in the db
$timeout_duration = (string)($time_multiplier*$timeout);
// reset the new rate limit based on the current time
$date = new \DateTime();
$date->add(new \DateInterval('PT'.$timeout_duration.'M'));
$timeout_period = $date->format('Y-m-d H:i:s');
// add the same time_out value to the grace period, but start the grace period when the $time_out expires
$grace = new \DateTime($timeout_period);
$grace->add(new \DateInterval('PT'.$timeout_duration.'M'));
$grace_period = $grace->format('Y-m-d H:i:s');
$rate_limit->timeout = $timeout_duration;
$rate_limit->timeout_period = $timeout_period;
$rate_limit->grace_period = $grace_period;
$rate_limit->save();
// not ok to accept rating
$allow_rating = false;
$status_code = '503 [PR]'; // PR = Probation revoked during grace period
$message = 'You already rated this form on '.date('l, F j, Y', strtotime($rate_limit->initial_date)).'.';
} else {
// rating count is still within acceptable limit for grace period, ok to accept rating
// continue and rate
$allow_rating = true;
$status_code = '302 [P]'; // P = Probationary rating allowed during grace period
}
}
} else if ($ratings_check > 4) {
// general rate limiting check for abuse from the same IP within the last 5 minutes
//echo 'spam detected<br>';
// IP is flagged, so...
$allow_rating = false;
//$status_code = '503 [S]'; // S = Spam
// get the date_rated datetime value from the most recent rating attempt
$newest_rating = \Models\Rating::findAllByIP($form->id, $ipaddress, $interval);
// get most recent record's date time
$datetime = $newest_rating[0]['date_rated'];
// if $rate_limit is empty, this is a new ip we should flag it by creating a new $rate_limit obj
if (empty($rate_limit)) {
$f3->clear('rate_limit'); // make sure it's empty
$rate_limit = new \Models\RateLimit(); // create a new obj
} else {
// ip is not new, so we need to check a few things before proceeding
// spam possibly detected, or could simply be another user from an existing ip trying to submit a rating for the same form ID, which is OK
$timeout = $rate_limit->timeout; // get the current timeout period stored for this IP
$time_multiplier = 2; // set the time_multiplier to 2, so we can double the timeout/grace period for this IP
}
// set the timeout duration based on the timeout_period stored in the db
$timeout_duration = (string)($time_multiplier*$timeout);
// add default time_out value in datetime to most recent record's date
$date = new \DateTime($datetime);
$date->add(new \DateInterval('PT'.$timeout_duration.'M'));
$timeout_period = $date->format('Y-m-d H:i:s');
// add the same time_out value to the grace period, but start the grace period when the $time_out expires
$grace = new \DateTime($timeout_period);
$grace->add(new \DateInterval('PT'.$timeout_duration.'M'));
$grace_period = $grace->format('Y-m-d H:i:s');
// ...otherwise the ip already exists and we just need to update the values
$rate_limit->form_id = $form->id; // id of the form being rating
$rate_limit->ip = $ipaddress;
$rate_limit->flagged = 1;
$rate_limit->timeout = $timeout_duration;
$rate_limit->timeout_period = $timeout_period;
$rate_limit->grace_period = $grace_period;
$rate_limit->initial_date = $datetime;
$rate_limit->save();
$status_code = '503 [IPF1]'; // IPF1 = IP flagged
$message = 'Whoops! You already rated this on '.date('l, F j, Y', strtotime($rate_limit->initial_date)).'.';
} else {
$status_code = '200'; // clean rating
// public rating allowed
$allow_rating = true;
}
//-------------------------------------------------------------------------------------------
// public rating conditional, allows new ratings
//-------------------------------------------------------------------------------------------
if ($allow_rating) {
// set up a new rating object
$rating = new \Models\Rating();
// set values passed in from dynamic form
$rating->rater_ip = $ipaddress;
$rating->rater_url = $raterURL;
$rating->value = (int) $f3->get('POST.value');
$rating->date_rated = date('Y-m-d H:i:s');
$rating->save();
$json_result = '{"message": "Thanks!","status": "'.$status_code.'","success": true}';
header("Content-Type: application/json; charset=utf-8");
echo $json_result;
} else {
$json_result = '{"message": "Whoops!","status": "'.$status_code.'","error": true}';
header("Content-Type: application/json; charset=utf-8");
echo $json_result;
}
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment