0

I have the following PHP code which concerns the download methods for my website. I am trying to rate-limit the download speed to something specific since it is currently being abused and milked by download managers.

I am not experienced in coding, unfortunately.

public function download(
    $file,
    $filename,
    $file_size,
    $content_type,
    $disposition = 'inline',
    $android = false
) {
    // Gzip enabled may set the wrong file size.
    if (function_exists('apache_setenv')) {
        @apache_setenv('no-gzip', 1);
    }
    if (ini_get('zlib.output_compression')) {
        @ini_set('zlib.output_compression', 'Off');
    }
    @set_time_limit(0);
    session_write_close();
    header("Content-Length: ".$file_size);

    if ($android) {
        header("Content-Type: application/octet-stream");
        header("Content-Disposition: attachment; filename=\"".$filename."\"");
    } else {
        header("Content-Type: $content_type");
        header("Content-Disposition: $disposition; filename=\"".$filename."\"");
        header("Content-Transfer-Encoding: binary");
        header("Expires: -1");
    }
    if (ob_get_level()) {
        ob_end_clean();
    }
    readfile($file);
    return true;
}
RiggsFolly
  • 93,638
  • 21
  • 103
  • 149
Mozzie
  • 3
  • 4
  • 1
    You must replace your `readfile` with a timer-controlled loop – Eugen Rieck Apr 03 '19 at 13:52
  • Instead of doing this through PHP, can't you do this through the web server? – M. Eriksson Apr 03 '19 at 13:58
  • Do you use apache or nginx? If nginx, then read this https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http/ – Dimi Apr 03 '19 at 13:59
  • You could also create a landing page where you generate a "one-off" csrf-token which you pass along to the download code you've got. It the token isn't valid, then they can't download the file. That way, users must visit the landing page before they can download anything but you won't throttle the download speed for legit downloads. – M. Eriksson Apr 03 '19 at 14:01
  • @EugenRieck sorry for the dumb question, but since this is a non-profit open directory, is there a way to do it for my code above? the site is an open directory run solely by donations. i don't know how to code PHP... I was wondering if I could get a written solution for it. – Mozzie Apr 03 '19 at 14:23
  • 1
    This is not a contractor site, but a Q&A site for developers - if you need a contractor, try the mechanical turk – Eugen Rieck Apr 03 '19 at 14:31
  • @Dimi that is pretty cool actually, especially that I can limit it for a specific directory and per IP. I'll test it out :) – Mozzie Apr 03 '19 at 14:31
  • You should just put up a reverse proxy, enable throttling and gg. This is not something you do in PHP (it is possible tho, just use `usleep()` with a tiny amount after every x bytes sent to the client). – Daniel W. Apr 03 '19 at 15:10
  • @Mozzie It is best to remember that if your clients are businesses, then it is common for people to share same IP, but not username. If your users are required to login in order to download things, then it is best to implement user level ratelimits. Nginx allows you to set different ratelimits for different user types, and many more other things if you are willing to invest time into researching your options and implementing them. – Dimi Apr 03 '19 at 15:42

1 Answers1

0

Code:

<?php 
public function download(
    $file,
    $filename,
    $file_size,
    $content_type,
    $disposition = 'inline',
    $android = false
) {
    // Gzip enabled may set the wrong file size.
    if (function_exists('apache_setenv')) {
        @apache_setenv('no-gzip', 1);
    }
    if (ini_get('zlib.output_compression')) {
        @ini_set('zlib.output_compression', 'Off');
    }
    @set_time_limit(0);
    session_write_close();
    header("Content-Length: ".$file_size);

    if ($android) {
        header("Content-Type: application/octet-stream");
        header("Content-Disposition: attachment; filename=\"".$filename."\"");
    } else {
        header("Content-Type: $content_type");
        header("Content-Disposition: $disposition; filename=\"".$filename."\"");
        header("Content-Transfer-Encoding: binary");
        header("Expires: -1");
    }
    if (ob_get_level()) {
        ob_end_clean();
    }

    $this->readSlow($file, 1);

    return true;
}

private function readSlow(string $filename, int $sleepAmount = 0, int $chunkSize = 409600)
{
    $handle = fopen($filename, "r");

    while (!feof($handle)) {
        echo fread($handle, $chunkSize);
        ob_flush();
        sleep($sleepAmount);
    }
    fclose($handle);
}

Note:

I am assuming you posted a part of your class. If you want to use readSlow() outside class then you use it this way:

readSlow('pathToFile', $amountOfSleep, $amountOfBytesToReadAtOneRead);

This is optional,

ob_flush();

I did it so once the buffer is read it is immediately send to client who requested data but in your case it may be better to remove it - test if it is better with it or not for different values of $amountSleep and $chunkSize

sleep() does not consume the time of the total execution of your script so if the file is downloaded without any sleep in 10 seconds and your max execution time for php script is 30 sec then you can easly make script running 120sec per download by using sleep.
Instead of using sleep() you can use usleep() that should give you more precise control over the speed limit because it takes sleep time in microsecond than in second so keep that in mind when using it and if you want to go into sleep between reads for 1 sec then $sleepAmount should be set to 1000000 when using usleep()

$chunkSize is the amount of bytes that will be read and then your script will go into sleep. Play with this value to get optimal speed.

I did not test the code, so it may work may not but at least this is the idea to start with.

Jimmix
  • 5,644
  • 6
  • 44
  • 71