Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117878084
RateLimit.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
7 KB
Referenced Files
None
Subscribers
None
RateLimit.php
View Options
<?php
namespace
App\Policy
;
use
App\Traits\BelongsToUserTrait
;
use
App\Transaction
;
use
App\User
;
use
App\UserAlias
;
use
App\Utils
;
use
Carbon\Carbon
;
use
Illuminate\Database\Eloquent\Model
;
class
RateLimit
extends
Model
{
use
BelongsToUserTrait
;
/** @var list<string> The attributes that are mass assignable */
protected
$fillable
=
[
'user_id'
,
'owner_id'
,
'recipient_hash'
,
'recipient_count'
,
];
/** @var string Database table name */
protected
$table
=
'policy_ratelimit'
;
/**
* Check the submission request agains rate limits
*
* @param array $data Request data
*
* @return Response Policy respone
*/
public
static
function
handle
(
$data
):
Response
{
[
$local
,
$domain
]
=
Utils
::
normalizeAddress
(
$data
[
'sender'
],
true
);
if
(
empty
(
$local
)
||
empty
(
$domain
))
{
return
new
Response
(
Response
::
ACTION_HOLD
,
'Invalid sender email'
,
403
);
}
$sender
=
$local
.
'@'
.
$domain
;
if
(
in_array
(
$sender
,
\config
(
'app.ratelimit_whitelist'
,
[]),
true
))
{
return
new
Response
();
// DUNNO
}
// Find the Kolab user
$user
=
User
::
withTrashed
()->
where
(
'email'
,
$sender
)->
first
();
if
(!
$user
)
{
$alias
=
UserAlias
::
where
(
'alias'
,
$sender
)->
first
();
if
(!
$alias
)
{
// TODO: How about sender is a distlist address?
// external sender through where this policy is applied
return
new
Response
();
// DUNNO
}
$user
=
$alias
->
user
()->
withTrashed
()->
first
();
}
return
self
::
verifyRequest
(
$user
,
(
array
)
$data
[
'recipients'
]);
}
/**
* Check the submission request agains rate limits
*
* @param User $user Sender user
* @param array $recipients List of mail recipients
*
* @return Response Policy respone
*/
public
static
function
verifyRequest
(
User
$user
,
array
$recipients
=
[]):
Response
{
if
(
$user
->
trashed
()
||
$user
->
isSuspended
())
{
// use HOLD, so that it is silent (as opposed to REJECT)
return
new
Response
(
Response
::
ACTION_HOLD
,
'Sender deleted or suspended'
,
403
);
}
// Examine the domain
$domain
=
$user
->
domain
();
if
(!
$domain
)
{
// external sender through where this policy is applied
return
new
Response
();
// DUNNO
}
if
(
$domain
->
trashed
()
||
$domain
->
isSuspended
())
{
// use HOLD, so that it is silent (as opposed to REJECT)
return
new
Response
(
Response
::
ACTION_HOLD
,
'Sender domain deleted or suspended'
,
403
);
}
// see if the user or domain is whitelisted
// use ./artisan policy:ratelimit:whitelist:create <email|namespace>
if
(
RateLimit\Whitelist
::
isListed
(
$user
)
||
RateLimit\Whitelist
::
isListed
(
$domain
))
{
return
new
Response
();
// DUNNO
}
// user nor domain whitelisted, continue scrutinizing the request
sort
(
$recipients
);
$recipientCount
=
count
(
$recipients
);
$recipientHash
=
hash
(
'sha256'
,
implode
(
','
,
$recipients
));
// Retrieve the wallet to get to the owner
$wallet
=
$user
->
wallet
();
// wait, there is no wallet?
if
(!
$wallet
||
!
$wallet
->
owner
)
{
return
new
Response
(
Response
::
ACTION_HOLD
,
'Sender without a wallet'
,
403
);
}
$owner
=
$wallet
->
owner
;
// find or create the request
$request
=
self
::
where
(
'recipient_hash'
,
$recipientHash
)
->
where
(
'user_id'
,
$user
->
id
)
->
where
(
'updated_at'
,
'>='
,
Carbon
::
now
()->
subHour
())
->
first
();
if
(!
$request
)
{
$request
=
self
::
create
([
'user_id'
=>
$user
->
id
,
'owner_id'
=>
$owner
->
id
,
'recipient_hash'
=>
$recipientHash
,
'recipient_count'
=>
$recipientCount
,
]);
}
else
{
// ensure the request has an up to date timestamp
$request
->
updated_at
=
Carbon
::
now
();
$request
->
save
();
}
// exempt owners that have 100% discount.
if
(
$wallet
->
discount
&&
$wallet
->
discount
->
discount
==
100
)
{
return
new
Response
();
// DUNNO
}
// exempt owners that currently maintain a positive balance and made any payments.
// Because there might be users that pay via external methods (and don't have Payment records)
// we can't check only the Payments table. Instead we assume that a credit/award transaction
// is enough to consider the user a "paying user" for purpose of the rate limit.
if
(
$wallet
->
balance
>
0
)
{
$isPayer
=
$wallet
->
transactions
()
->
whereIn
(
'type'
,
[
Transaction
::
WALLET_AWARD
,
Transaction
::
WALLET_CREDIT
])
->
where
(
'amount'
,
'>'
,
0
)
->
exists
();
if
(
$isPayer
)
{
return
new
Response
();
}
}
// Examine the rates at which the owner (or its users) is sending
$ownerRates
=
self
::
where
(
'owner_id'
,
$owner
->
id
)
->
where
(
'updated_at'
,
'>='
,
Carbon
::
now
()->
subHour
());
if
((
$count
=
$ownerRates
->
count
())
>=
10
)
{
// automatically suspend (recursively) if 2.5 times over the original limit and younger than two months
$ageThreshold
=
Carbon
::
now
()->
subMonthsWithoutOverflow
(
2
);
if
(
$count
>=
25
&&
$owner
->
created_at
>
$ageThreshold
)
{
$owner
->
suspendAccount
();
}
return
new
Response
(
Response
::
ACTION_DEFER_IF_PERMIT
,
'The account is at 10 messages per hour, cool down.'
,
403
);
}
if
((
$recipientCount
=
$ownerRates
->
sum
(
'recipient_count'
))
>=
100
)
{
// automatically suspend if 2.5 times over the original limit and younger than two months
$ageThreshold
=
Carbon
::
now
()->
subMonthsWithoutOverflow
(
2
);
if
(
$recipientCount
>=
250
&&
$owner
->
created_at
>
$ageThreshold
)
{
$owner
->
suspendAccount
();
}
return
new
Response
(
Response
::
ACTION_DEFER_IF_PERMIT
,
'The account is at 100 recipients per hour, cool down.'
,
403
);
}
// Examine the rates at which the user is sending (if not also the owner)
if
(
$user
->
id
!=
$owner
->
id
)
{
$userRates
=
self
::
where
(
'user_id'
,
$user
->
id
)
->
where
(
'updated_at'
,
'>='
,
Carbon
::
now
()->
subHour
());
if
((
$count
=
$userRates
->
count
())
>=
10
)
{
// automatically suspend if 2.5 times over the original limit and younger than two months
$ageThreshold
=
Carbon
::
now
()->
subMonthsWithoutOverflow
(
2
);
if
(
$count
>=
25
&&
$user
->
created_at
>
$ageThreshold
)
{
$user
->
suspend
();
}
return
new
Response
(
Response
::
ACTION_DEFER_IF_PERMIT
,
'User is at 10 messages per hour, cool down.'
,
403
);
}
if
((
$recipientCount
=
$userRates
->
sum
(
'recipient_count'
))
>=
100
)
{
// automatically suspend if 2.5 times over the original limit
$ageThreshold
=
Carbon
::
now
()->
subMonthsWithoutOverflow
(
2
);
if
(
$recipientCount
>=
250
&&
$user
->
created_at
>
$ageThreshold
)
{
$user
->
suspend
();
}
return
new
Response
(
Response
::
ACTION_DEFER_IF_PERMIT
,
'The account is at 100 recipients per hour, cool down.'
,
403
);
}
}
return
new
Response
();
// DUNNO
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Apr 5, 9:57 PM (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823729
Default Alt Text
RateLimit.php (7 KB)
Attached To
Mode
rK kolab
Attached
Detach File
Event Timeline