turbo
turbo

Reputation: 1273

A portable way of providing an IP-based cooldown period?

I have a PHP API front end running on a webserver. This specific PHP program is subject to distribution, thus it should be as portable as possible.

The feature I want to implement is an IP cooldown period, meaning that the same IP can only request the API a maximum of two times per second, meaning at least a 500ms delay.

The approach I had in mind is storing the IP in an MySQL database, along with the latest request timestamp. I get the IP by:

if (getenv('REMOTE_ADDR'))
    $ipaddress = getenv('REMOTE_ADDR');

But some servers might not have a MySQL database or the user installling this has no access. Another issue is the cleanup of the database.

Is there a more portable way of temporarily storing the IPs (keeping IPv6 in mind)?

and

How can I provide an automatic cleanup of IPs that are older than 500ms, with the least possible performance impact?

Also: I have no interest at looking at stored IPs, it is just about the delay.

Upvotes: 1

Views: 829

Answers (1)

turbo
turbo

Reputation: 1273

This is how I solved it for now, using a file.

Procedure

  1. Get client IP and hash it (to prevent file readout).
  2. Open IP file and scan each line
  3. Compare the time of the current record to the current time
  4. If difference is greater than set timeout goto 5., else 7.
  5. If IP matches client, create updated record, else
  6. drop record.
  7. If IP matches client, provide failure message, else copy record.

Example code

<?php

$sIPHash    = md5($_SERVER[REMOTE_ADDR]);
$iSecDelay  = 10;
$sPath      = "bucket.cache";
$bReqAllow  = false;
$iWait      = -1;
$sContent   = "";

if ($nFileHandle = fopen($sPath, "c+")) {
    flock($nFileHandle, LOCK_EX);
    $iCurLine = 0;
    while (($sCurLine = fgets($nFileHandle, 4096)) !== FALSE) {
        $iCurLine++;
        $bIsIPRec = strpos($sCurLine, $sIPHash);
        $iLastReq = strtok($sCurLine, '|');
        // this record expired anyway:
        if ( (time() - $iLastReq) > $iSecDelay ) {
            // is it also our IP?
            if ($bIsIPRec !== FALSE) {
                $sContent .= time()."|".$sIPHash.PHP_EOL;
                $bReqAllow = true;
            }
        } else {
            if ($bIsIPRec !== FALSE) $iWait = ($iSecDelay-(time()-$iLastReq));
            $sContent .= $sCurLine.PHP_EOL;
        }
    }
}

if ($iWait == -1 && $bReqAllow == false) {
    // no record yet, create one
    $sContent .= time()."|".$sIPHash.PHP_EOL;
    echo "Request from new user successful!";
} elseif ($bReqAllow == true) {
    echo "Request from old user successful!";
} else {
    echo "Request failed! Wait " . $iWait . " seconds!";
}

ftruncate($nFileHandle, 0);
rewind($nFileHandle);
fwrite($nFileHandle, $sContent);
flock($nFileHandle, LOCK_UN);
fclose($nFileHandle);
?>

Remarks

New users

If the IP hash doesn't match any record, a new record is created. Attention: Access might fail if you do not have rights to do that.

Memory

If you expect much traffic, switch to a database solution like this all together.

Redundant code

"But minxomat", you might say, "now each client loops through the whole file!". Yes, indeed, and that is how I want it for my solution. This way, every client is responsible for the cleanup of the whole file. Even so, the performance impact is held low, because if every client is cleaning, file size will be kept at the absolute minimum. Change this, if this way doesn't work for you.

Upvotes: 1

Related Questions