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;
|
||||
}
|
||||
|
||||
// The `IpAddress` column expects IPv6 address strings.
|
||||
if(is_string($this->IpAddress) && filter_var($this->IpAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){
|
||||
$this->IpAddress = '::ffff:' . $this->IpAddress;
|
||||
}
|
||||
$this->IpAddress = Formatter::ToIpv6($this->IpAddress);
|
||||
|
||||
if($error->HasExceptions){
|
||||
throw $error;
|
||||
|
@ -86,4 +83,26 @@ class EbookDownload{
|
|||
and Created < ?
|
||||
', [$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 Gone = 410;
|
||||
case UnprocessableContent = 422;
|
||||
case TooManyRequests = 429;
|
||||
|
||||
case InternalServerError = 500;
|
||||
case ServiceUnavailable = 503;
|
||||
|
|
|
@ -149,6 +149,17 @@ class Formatter{
|
|||
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 `$`.
|
||||
*
|
||||
|
|
|
@ -7,6 +7,11 @@ $downloadUrl = null;
|
|||
$downloadCount = HttpInput::Int(COOKIE, 'download-count') ?? 0;
|
||||
$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:
|
||||
// * The user is logged in.
|
||||
// * Their `download-count` cookie is above some amount.
|
||||
|
@ -40,11 +45,17 @@ try{
|
|||
/** @var string|null $userAgent */
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? null;
|
||||
|
||||
try{
|
||||
$ebook->AddDownload($ipAddress, $userAgent);
|
||||
if(!isset(Session::$User)){
|
||||
// Check for excessive downloads.
|
||||
$shortDownloadCount = EbookDownload::GetCountByIpAddressSince($ipAddress, $shortDownloadTime);
|
||||
if($shortDownloadCount > $shortDownloadLimit){
|
||||
Template::ExitWithCode(Enums\HttpCode::TooManyRequests);
|
||||
}
|
||||
|
||||
$longDownloadCount = EbookDownload::GetCountByIpAddressSince($ipAddress, $longDownloadTime);
|
||||
if($longDownloadCount > $longDownloadLimit){
|
||||
Template::ExitWithCode(Enums\HttpCode::TooManyRequests);
|
||||
}
|
||||
catch(Exceptions\InvalidEbookDownloadException){
|
||||
// Pass. Allow the download to continue even if it isn't recorded.
|
||||
}
|
||||
|
||||
if($skipThankYouPage){
|
||||
|
@ -56,6 +67,13 @@ try{
|
|||
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.
|
||||
// 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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue