1

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.

turbo
  • 1,233
  • 14
  • 36
  • 2
    Have a look [here](http://stackoverflow.com/questions/1375501/how-do-i-throttle-my-sites-api-users). It offers a pretty convenient way. – Andrei Jun 30 '15 at 13:10
  • @Andrew Thank, this helped creating my own file-based solution! – turbo Jun 30 '15 at 18:55

1 Answers1

1

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.

Community
  • 1
  • 1
turbo
  • 1,233
  • 14
  • 36
  • You have race conditions there... Two PHP requests will try to add its own IP, but while the 1st is cleaning, 2nd will write its IP into the file. Then 1st will replace it with its previously-read version (which doesn't contain the 2nd IP). When you get more req/s it will be even worse – Marki555 Jun 30 '15 at 19:01
  • @Marki555 Is that really that big of a problem? The file is re-loaded when updating / deleting a record, so the collision has to happen in that small timeframe, or am I missing something? – turbo Jun 30 '15 at 19:07
  • Depends.. If you have 500 req/s, then 10s worth of IPs will be 5000. Comparing long string hashes is inefficient (compared to IPs stored as 32bit numbers) and could take 4ms for example. That means at each time you will have 2 of those running at the same time. – Marki555 Jun 30 '15 at 19:11
  • 1
    If you need to be concerned about clients accessing more than twice per second, believe me you need to worry about race conditions too. Your basic method looks valid, but work with the code so you only use "standard" functions (ie. no `file_put_contents` etc), and make sure to `flock($nFileHandle, LOCK_EX);` right after your `fopen` call and `flock($nFileHandle, LOCK_UN);` right before any `fclose` call. You should also reduce to only open file once regardless of whether the "cleanup" loop found the IP or not (ie. `fwrite` the new IP before closing the handle) – Mikk3lRo Jun 30 '15 at 19:15
  • @Marki555 Still manageable for me. A successful API request might take up to 120s, so it shouldn't be a problem to just lock the file when some client is cleaning, as even a 2s delay is negligible for me. – turbo Jun 30 '15 at 19:16
  • @minxomat can't quite follow the business-logic anymore (it's late), but as far as file locking, optimization (as far as only opening the file once goes) and race conditions go I'm pretty sure you're golden :) – Mikk3lRo Jun 30 '15 at 21:17
  • Oh actually you do have a minor problem... If the `fopen` fails you shouldn't try to do stuff with the file handle - probably won't ever happen, but it's still a flaw worth fixing, since you do bother to check if it was successfull in the first place. – Mikk3lRo Jun 30 '15 at 21:19
  • @Mikk3lRo Yeah, I'll add error handling (this is also of course not the full code, just the relevant piece). Given that I never touched PHP before today, I consider this a successful attempt :) – turbo Jun 30 '15 at 21:23
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82020/discussion-between-mikk3lro-and-minxomat). – Mikk3lRo Jun 30 '15 at 21:31