1+ using System . Collections . Frozen ;
2+ using System . Net ;
3+ using System . Reflection ;
4+ using Bunkum . Core . RateLimit ;
5+ using Bunkum . Listener . Request ;
6+ using NotEnoughLogs ;
7+ using Refresh . Common ;
8+ using Refresh . Common . Time ;
9+ using Refresh . Core . Configuration ;
10+ using Refresh . Core . Configuration . Structs ;
11+ using Refresh . Core . RateLimits . Info ;
12+
13+ namespace Refresh . Core . RateLimits ;
14+
15+ public class BasicRateLimiter : IRateLimiter
16+ {
17+ private readonly Logger _logger ;
18+ private readonly IDateTimeProvider _timeProvider ;
19+ private readonly FrozenDictionary < string , ConfigRateLimitBucket > _buckets ;
20+
21+ public BasicRateLimiter ( IDateTimeProvider timeProvider , Logger logger , Dictionary < string , ConfigRateLimitBucket > buckets )
22+ {
23+ this . _timeProvider = timeProvider ;
24+ this . _logger = logger ;
25+ this . _buckets = buckets . ToFrozenDictionary ( ) ;
26+ }
27+
28+ private readonly List < RateLimitUserInfo > _userInfos = new ( 25 ) ;
29+ private readonly List < RateLimitRemoteEndpointInfo > _remoteEndpointInfos = new ( 25 ) ;
30+
31+ private BasicRateLimitBucket GetBucket ( MethodInfo ? method )
32+ {
33+ string bucketName = method ? . GetCustomAttribute < BasicRateLimitAttribute > ( ) ? . Bucket ?? "global" ;
34+ ConfigRateLimitBucket ? bucketData = this . _buckets . GetValueOrDefault ( bucketName ) ;
35+
36+ if ( bucketData == null )
37+ {
38+ this . _logger . LogDebug ( RefreshContext . RateLimit , $ "Could not find bucket '{ bucketName } ' in config, falling back to hardcoded defaults.") ;
39+ bucketData = BasicEndpointRateLimitConfig . DefaultBuckets . GetValueOrDefault ( bucketName ) ;
40+
41+ if ( bucketData == null )
42+ {
43+ throw new NotImplementedException ( $ "Could not find bucket '{ bucketName } ' in neither the config file nor the hardcoded defaults!") ;
44+ }
45+ }
46+
47+ return BasicRateLimitBucket . FromOld ( bucketData . Value , bucketName ) ;
48+ }
49+
50+ public bool UserViolatesRateLimit ( ListenerContext context , MethodInfo ? method , IRateLimitUser user )
51+ {
52+ BasicRateLimitBucket bucket = this . GetBucket ( method ) ;
53+
54+ lock ( this . _remoteEndpointInfos )
55+ {
56+ RateLimitUserInfo ? info = this . _userInfos
57+ . FirstOrDefault ( i =>
58+ user . RateLimitUserIdIsEqual ( i . User . RateLimitUserId ) && i . Bucket == bucket . BucketName ) ;
59+
60+ if ( info == null )
61+ {
62+ info = new RateLimitUserInfo ( user , bucket . BucketName ) ;
63+ lock ( this . _userInfos )
64+ {
65+ this . _userInfos . Add ( info ) ;
66+ }
67+ }
68+
69+ lock ( info )
70+ {
71+ return this . ViolatesRateLimit ( context , info , bucket ) ;
72+ }
73+ }
74+ }
75+
76+ public bool RemoteEndpointViolatesRateLimit ( ListenerContext context , MethodInfo ? method )
77+ {
78+ IPAddress ipAddress = context . RemoteEndpoint . Address ;
79+
80+ BasicRateLimitBucket bucket = this . GetBucket ( method ) ;
81+
82+ lock ( this . _remoteEndpointInfos )
83+ {
84+ RateLimitRemoteEndpointInfo ? info = this . _remoteEndpointInfos
85+ . FirstOrDefault ( i => ipAddress . Equals ( i . IpAddress ) && i . Bucket == bucket . BucketName ) ;
86+
87+ if ( info == null )
88+ {
89+ info = new RateLimitRemoteEndpointInfo ( ipAddress , bucket . BucketName ) ;
90+
91+ this . _remoteEndpointInfos . Add ( info ) ;
92+ }
93+
94+ lock ( info )
95+ {
96+ return this . ViolatesRateLimit ( context , info , bucket ) ;
97+ }
98+ }
99+ }
100+
101+ private bool ViolatesRateLimit ( ListenerContext context , IRateLimitInfo info , BasicRateLimitBucket bucket )
102+ {
103+ int now = ( int ) this . _timeProvider . TimestampSeconds ;
104+
105+ // Always clear all expired and add this request as a new one,
106+ // to make the block duration longer the more the client continues to spam
107+ info . RequestTimes . RemoveAll ( r => r <= now - bucket . TimeWindowSeconds ) ;
108+ info . RequestTimes . Add ( now ) ;
109+
110+ // Repeat block
111+ // If a rate-limit is already triggered, block regardless of whether MaxRequestCount has been reached
112+ if ( info . LimitedUntil != 0 && info . LimitedUntil > now )
113+ return true ;
114+
115+ // Initial block
116+ if ( info . RequestTimes . Count > bucket . MaxRequestCount )
117+ {
118+ info . LimitedUntil = now + bucket . BlockDurationSeconds ;
119+ context . ResponseHeaders . TryAdd ( "Retry-After" , bucket . BlockDurationSeconds . ToString ( ) ) ; // TODO also include overshot time
120+
121+ return true ;
122+ }
123+
124+ return false ;
125+ }
126+ }
0 commit comments