1 module bancho.ratelimit; 2 @safe: 3 4 import core.time; 5 6 import vibe.core.core; 7 import vibe.core.sync; 8 9 /// Params: 10 /// messages = Maximum allowed messages in the given timespan 11 /// timespan = Timespan to check messages in 12 struct ChatRatelimiter(int maxMessages, Duration timespan) 13 { 14 /// Delay to always apply to avoid large gaps. 15 Duration baseDelay = timespan / maxMessages / 2; // default to half an average cycle so if messages are going to be spammed, half of timespan is the maximum idle time 16 /// Log of all sent message times. 17 MonoTime[maxMessages] times; 18 /// Index where to insert in times array right now. 19 int cycle; 20 InterruptibleTaskMutex mutex; 21 22 void load() 23 { 24 mutex = new InterruptibleTaskMutex(); 25 } 26 27 /// Throws: InterruptException if task got interrupted 28 void putWait(bool put) 29 { 30 if (!putWaitNothrow(put)) 31 throw new InterruptException(); 32 } 33 34 /// Returns: false if it was interrupted 35 bool putWaitNothrow(bool put) nothrow 36 { 37 try 38 { 39 mutex.lock(); 40 } 41 catch (InterruptException) 42 { 43 return false; 44 } 45 catch (Exception e) 46 { 47 import core.exception : AssertError; 48 49 throw new AssertError("InterruptibleTaskMutex.lock is not expected to throw anything else than InterruptException", __FILE__, __LINE__, e); 50 } 51 52 scope (exit) 53 mutex.unlock(); 54 55 auto sinceFirst = MonoTime.currTime - times[cycle]; 56 auto sinceLast = MonoTime.currTime - times[(cycle + $ - 1) % $]; 57 if (sinceLast >= baseDelay && (sinceFirst >= timespan || times[cycle] == MonoTime.zero)) 58 { 59 // do nothing 60 } 61 else if (sinceFirst >= timespan || times[cycle] == MonoTime.zero) 62 { 63 // sleep remaining of baseDelay 64 if (waitForInterrupt(baseDelay - sinceLast)) 65 return false; 66 } 67 else 68 { 69 if (waitForInterrupt(baseDelay + (timespan - sinceFirst))) 70 return false; 71 } 72 73 if (put) 74 { 75 times[cycle] = MonoTime.currTime; 76 cycle = (cycle + 1) % times.length; 77 } 78 79 return true; 80 } 81 82 /// Returns true if a message can be sent instantly. 83 bool peekInstant() nothrow 84 { 85 if (!mutex.tryLock()) 86 return false; 87 mutex.unlock(); 88 auto sinceFirst = MonoTime.currTime - times[cycle]; 89 auto sinceLast = MonoTime.currTime - times[(cycle + $ - 1) % $]; 90 return sinceLast >= baseDelay && (sinceFirst >= timespan || times[cycle] == MonoTime.zero); 91 } 92 93 /// Returns true if a message can be after waiting at most the baseDelay. 94 bool peekBase() nothrow 95 { 96 if (!mutex.tryLock()) 97 return false; 98 mutex.unlock(); 99 auto sinceFirst = MonoTime.currTime - times[cycle]; 100 return sinceFirst >= timespan || times[cycle] == MonoTime.zero; 101 } 102 } 103 104 unittest 105 { 106 import std.conv; 107 import vibe.core.log; 108 109 auto start = MonoTime.currTime; 110 ChatRatelimiter!(5, 1.seconds) ratelimit; // 6 messages in 3 seconds 111 ratelimit.load(); 112 113 assert(ratelimit.baseDelay == 100.msecs); 114 115 foreach (i; 0 .. 4) 116 { 117 logInfo("waiting at %s", MonoTime.currTime - start); 118 assert(ratelimit.peekBase, i.to!string); 119 ratelimit.putWait(true); 120 assert(ratelimit.peekBase, i.to!string); 121 assert(!ratelimit.peekInstant, i.to!string); 122 sleep(ratelimit.baseDelay); 123 assert(ratelimit.peekInstant, i.to!string); 124 } 125 ratelimit.putWait(true); 126 assert(!ratelimit.peekInstant); 127 sleep(ratelimit.baseDelay); 128 assert(!ratelimit.peekInstant); 129 assert(!ratelimit.peekBase); 130 131 logInfo("done at %s", MonoTime.currTime - start); 132 assert(MonoTime.currTime - start > 450.msecs && MonoTime.currTime - start < 550.msecs, 133 "took " ~ (MonoTime.currTime - start).toString ~ " instead of expected 0.5s"); 134 logInfo("waiting at %s", MonoTime.currTime - start); 135 ratelimit.putWait(true); 136 assert(MonoTime.currTime - start > 1050.msecs && MonoTime.currTime - start < 1150.msecs, 137 "took " ~ (MonoTime.currTime - start).toString ~ " instead of expected 1s"); 138 } 139 140 // public rooms: 60 messages / minute 141 // private channels: 300 messages / minute 142 struct BanchoRatelimiter(int publicMax = 6, Duration publicTime = 6.seconds, 143 int privateMax = 5, Duration privateTime = 1.seconds) 144 { 145 ChatRatelimiter!(publicMax, publicTime) publicLimit; 146 ChatRatelimiter!(privateMax, privateTime) privateLimit; 147 148 void load() 149 { 150 publicLimit.load(); 151 privateLimit.load(); 152 } 153 154 void putWait(string channel, bool put) 155 { 156 isPublic(channel) ? publicLimit.putWait(put) : privateLimit.putWait(put); 157 } 158 159 bool putWaitNothrow(string channel, bool put) nothrow 160 { 161 return isPublic(channel) ? publicLimit.putWaitNothrow(put) : privateLimit.putWaitNothrow(put); 162 } 163 164 bool peekInstant(string channel) nothrow 165 { 166 return isPublic(channel) ? publicLimit.peekInstant() : privateLimit.peekInstant(); 167 } 168 169 bool peekBase(string channel) nothrow 170 { 171 return isPublic(channel) ? publicLimit.peekBase() : privateLimit.peekBase(); 172 } 173 174 } 175 176 private bool isPublic(string channel) nothrow 177 { 178 if (channel.length && channel[0] == '#') 179 { 180 // channel, so public 181 return true; 182 } 183 else 184 { 185 // doesn't start with #, must be a player, so private 186 return false; 187 } 188 } 189 190 package bool waitForInterrupt(Duration d) nothrow 191 { 192 try 193 { 194 sleep(d); 195 return false; 196 } 197 catch (InterruptException) 198 { 199 return true; 200 } 201 catch (Exception e) 202 { 203 import core.exception : AssertError; 204 205 throw new AssertError("sleep is not expected to throw anything else than InterruptException", __FILE__, __LINE__, e); 206 } 207 }