mirror of
https://github.com/standardebooks/web.git
synced 2025-07-20 13:24:48 -04:00
Limit ebook downloads per IP address
This commit is contained in:
parent
6c69ee0bdf
commit
0a71382a25
4 changed files with 58 additions and 9 deletions
|
@ -45,10 +45,7 @@ class EbookDownload{
|
||||||
$this->UserAgent = null;
|
$this->UserAgent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The `IpAddress` column expects IPv6 address strings.
|
$this->IpAddress = Formatter::ToIpv6($this->IpAddress);
|
||||||
if(is_string($this->IpAddress) && filter_var($this->IpAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){
|
|
||||||
$this->IpAddress = '::ffff:' . $this->IpAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
if($error->HasExceptions){
|
if($error->HasExceptions){
|
||||||
throw $error;
|
throw $error;
|
||||||
|
@ -86,4 +83,26 @@ class EbookDownload{
|
||||||
and Created < ?
|
and Created < ?
|
||||||
', [$startDate, $endDate], EbookDownload::class);
|
', [$startDate, $endDate], EbookDownload::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the count of `EbookDownload` objects by the given IP address newer than the given created time.
|
||||||
|
*
|
||||||
|
* @param string $ipAddress The IP address to search for.
|
||||||
|
* @param DateTimeImmutable $startDateTime The minimum creation timestamp (inclusive).
|
||||||
|
* @return int The total number of `EbookDownload` objects matching the criteria.
|
||||||
|
*/
|
||||||
|
public static function GetCountByIpAddressSince(?string $ipAddress, DateTimeImmutable $startDateTime): int{
|
||||||
|
if(!isset($ipAddress)){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddress = Formatter::ToIpv6($ipAddress);
|
||||||
|
|
||||||
|
return Db::QueryInt('
|
||||||
|
SELECT count(*)
|
||||||
|
from EbookDownloads
|
||||||
|
where IpAddress = ?
|
||||||
|
and Created >= ?
|
||||||
|
', [$ipAddress, $startDateTime]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ enum HttpCode: int{
|
||||||
case Conflict = 409;
|
case Conflict = 409;
|
||||||
case Gone = 410;
|
case Gone = 410;
|
||||||
case UnprocessableContent = 422;
|
case UnprocessableContent = 422;
|
||||||
|
case TooManyRequests = 429;
|
||||||
|
|
||||||
case InternalServerError = 500;
|
case InternalServerError = 500;
|
||||||
case ServiceUnavailable = 503;
|
case ServiceUnavailable = 503;
|
||||||
|
|
|
@ -149,6 +149,17 @@ class Formatter{
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a valid IPv4 address to IPv6. Other strings are unchanged.
|
||||||
|
*/
|
||||||
|
public static function ToIpv6(?string $ipAddress): ?string{
|
||||||
|
if(filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){
|
||||||
|
return '::ffff:' . $ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a float into a USD currency string. The result is prepended with `$`.
|
* Format a float into a USD currency string. The result is prepended with `$`.
|
||||||
*
|
*
|
||||||
|
|
|
@ -7,6 +7,11 @@ $downloadUrl = null;
|
||||||
$downloadCount = HttpInput::Int(COOKIE, 'download-count') ?? 0;
|
$downloadCount = HttpInput::Int(COOKIE, 'download-count') ?? 0;
|
||||||
$source = Enums\EbookDownloadSource::tryFrom(HttpInput::Str(GET, 'source') ?? '');
|
$source = Enums\EbookDownloadSource::tryFrom(HttpInput::Str(GET, 'source') ?? '');
|
||||||
|
|
||||||
|
$shortDownloadLimit = 10;
|
||||||
|
$shortDownloadTime = NOW->modify('-30 seconds');
|
||||||
|
$longDownloadLimit = 100;
|
||||||
|
$longDownloadTime = NOW->modify('-1 day');
|
||||||
|
|
||||||
// Skip the thank you page if any of these are true:
|
// Skip the thank you page if any of these are true:
|
||||||
// * The user is logged in.
|
// * The user is logged in.
|
||||||
// * Their `download-count` cookie is above some amount.
|
// * Their `download-count` cookie is above some amount.
|
||||||
|
@ -40,11 +45,17 @@ try{
|
||||||
/** @var string|null $userAgent */
|
/** @var string|null $userAgent */
|
||||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||||
|
|
||||||
try{
|
if(!isset(Session::$User)){
|
||||||
$ebook->AddDownload($ipAddress, $userAgent);
|
// Check for excessive downloads.
|
||||||
}
|
$shortDownloadCount = EbookDownload::GetCountByIpAddressSince($ipAddress, $shortDownloadTime);
|
||||||
catch(Exceptions\InvalidEbookDownloadException){
|
if($shortDownloadCount > $shortDownloadLimit){
|
||||||
// Pass. Allow the download to continue even if it isn't recorded.
|
Template::ExitWithCode(Enums\HttpCode::TooManyRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
$longDownloadCount = EbookDownload::GetCountByIpAddressSince($ipAddress, $longDownloadTime);
|
||||||
|
if($longDownloadCount > $longDownloadLimit){
|
||||||
|
Template::ExitWithCode(Enums\HttpCode::TooManyRequests);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($skipThankYouPage){
|
if($skipThankYouPage){
|
||||||
|
@ -56,6 +67,13 @@ try{
|
||||||
throw new Exceptions\InvalidFileException();
|
throw new Exceptions\InvalidFileException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
$ebook->AddDownload($ipAddress, $userAgent);
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidEbookDownloadException){
|
||||||
|
// Pass. Allow the download to continue even if it isn't recorded.
|
||||||
|
}
|
||||||
|
|
||||||
// Everything OK, serve the file using Apache.
|
// Everything OK, serve the file using Apache.
|
||||||
// The `xsendfile` Apache module tells Apache to serve the file, including `not-modified` or `etag` headers.
|
// The `xsendfile` Apache module tells Apache to serve the file, including `not-modified` or `etag` headers.
|
||||||
// Much more efficient than reading it in PHP and outputting it that way.
|
// Much more efficient than reading it in PHP and outputting it that way.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue