mirror of https://github.com/waku-org/nwaku.git
136 lines
4.7 KiB
Nim
136 lines
4.7 KiB
Nim
{.push raises: [].}
|
|
|
|
import chronos/timer, std/[tables, strutils, options], regex, results
|
|
|
|
# Setting for TokenBucket defined as volume over period of time
|
|
type RateLimitSetting* = tuple[volume: int, period: Duration]
|
|
|
|
type RateLimitedProtocol* = enum
|
|
GLOBAL
|
|
STOREV2
|
|
STOREV3
|
|
LIGHTPUSH
|
|
PEEREXCHG
|
|
FILTER
|
|
|
|
type ProtocolRateLimitSettings* = Table[RateLimitedProtocol, RateLimitSetting]
|
|
|
|
# Set the default to switch off rate limiting for now
|
|
let DefaultGlobalNonRelayRateLimit*: RateLimitSetting = (0, 0.minutes)
|
|
let UnlimitedRateLimit*: RateLimitSetting = (0, 0.seconds)
|
|
|
|
# Acceptable call frequence from one peer using filter service
|
|
# Assumption is having to set up a subscription with max 30 calls than using ping in every min
|
|
# While subscribe/unsubscribe events are distributed in time among clients, pings will happen regularly from
|
|
# all subscribed peers
|
|
let FilterDefaultPerPeerRateLimit*: RateLimitSetting = (30, 1.minutes)
|
|
|
|
# For being used under GC-safe condition must use threadvar
|
|
var DefaultProtocolRateLimit* {.threadvar.}: ProtocolRateLimitSettings
|
|
DefaultProtocolRateLimit =
|
|
{GLOBAL: UnlimitedRateLimit, FILTER: FilterDefaultPerPeerRateLimit}.toTable()
|
|
|
|
proc isUnlimited*(t: RateLimitSetting): bool {.inline.} =
|
|
return t.volume <= 0 or t.period <= 0.seconds
|
|
|
|
func `$`*(t: RateLimitSetting): string {.inline.} =
|
|
return
|
|
if t.isUnlimited():
|
|
"no-limit"
|
|
else:
|
|
$t.volume & "/" & $t.period
|
|
|
|
proc translate(sProtocol: string): RateLimitedProtocol {.raises: [ValueError].} =
|
|
if sProtocol.len == 0:
|
|
return GLOBAL
|
|
|
|
case sProtocol
|
|
of "global":
|
|
return GLOBAL
|
|
of "storev2":
|
|
return STOREV2
|
|
of "storev3":
|
|
return STOREV3
|
|
of "lightpush":
|
|
return LIGHTPUSH
|
|
of "px":
|
|
return PEEREXCHG
|
|
of "filter":
|
|
return FILTER
|
|
else:
|
|
raise newException(ValueError, "Unknown protocol definition: " & sProtocol)
|
|
|
|
proc fillSettingTable(
|
|
t: var ProtocolRateLimitSettings, sProtocol: var string, setting: RateLimitSetting
|
|
) {.raises: [ValueError].} =
|
|
if sProtocol == "store":
|
|
# generic store will only applies to version which is not listed directly
|
|
discard t.hasKeyOrPut(STOREV2, setting)
|
|
discard t.hasKeyOrPut(STOREV3, setting)
|
|
else:
|
|
let protocol = translate(sProtocol)
|
|
# always overrides, last one wins if same protocol duplicated
|
|
t[protocol] = setting
|
|
|
|
proc parse*(
|
|
T: type ProtocolRateLimitSettings, settings: seq[string]
|
|
): Result[ProtocolRateLimitSettings, string] =
|
|
var settingsTable: ProtocolRateLimitSettings =
|
|
initTable[RateLimitedProtocol, RateLimitSetting]()
|
|
|
|
## Following regex can match the exact syntax of how rate limit can be set for different protocol or global.
|
|
## It uses capture groups
|
|
## group0: Will be check if protocol name is followed by a colon but only if protocol name is set.
|
|
## group1: Protocol name, if empty we take it as "global" setting
|
|
## group2: Volume of tokens - only integer
|
|
## group3: Duration of period - only integer
|
|
## group4: Unit of period - only h:hour, m:minute, s:second, ms:millisecond allowed
|
|
## whitespaces are allowed lazily
|
|
const parseRegex =
|
|
"""^\s*((store|storev2|storev3|lightpush|px|filter)\s*:)?\s*(\d+)\s*\/\s*(\d+)\s*(s|h|m|ms)\s*$"""
|
|
const regexParseSize = re2(parseRegex)
|
|
for settingStr in settings:
|
|
let aSetting = settingStr.toLower()
|
|
try:
|
|
var m: RegexMatch2
|
|
if aSetting.match(regexParseSize, m) == false:
|
|
return err("Invalid rate-limit setting: " & settingStr)
|
|
|
|
var sProtocol = aSetting[m.captures[1]]
|
|
let volume = aSetting[m.captures[2]].parseInt()
|
|
let duration = aSetting[m.captures[3]].parseInt()
|
|
let periodUnit = aSetting[m.captures[4]]
|
|
|
|
var period = 0.seconds
|
|
case periodUnit
|
|
of "ms":
|
|
period = duration.milliseconds
|
|
of "s":
|
|
period = duration.seconds
|
|
of "m":
|
|
period = duration.minutes
|
|
of "h":
|
|
period = duration.hours
|
|
|
|
fillSettingTable(settingsTable, sProtocol, (volume, period))
|
|
except ValueError:
|
|
return err("Invalid rate-limit setting: " & settingStr)
|
|
|
|
# If there were no global setting predefined, we set unlimited
|
|
# due it is taken for protocols not defined in the list - thus those will not apply accidentally wrong settings.
|
|
discard settingsTable.hasKeyOrPut(GLOBAL, UnlimitedRateLimit)
|
|
discard settingsTable.hasKeyOrPut(FILTER, FilterDefaultPerPeerRateLimit)
|
|
|
|
return ok(settingsTable)
|
|
|
|
proc getSetting*(
|
|
t: ProtocolRateLimitSettings, protocol: RateLimitedProtocol
|
|
): RateLimitSetting =
|
|
let default = t.getOrDefault(GLOBAL, UnlimitedRateLimit)
|
|
return t.getOrDefault(protocol, default)
|
|
|
|
proc calculateLimitPerSecond*(setting: RateLimitSetting): float64 =
|
|
if setting.isUnlimited():
|
|
return 0.float64
|
|
return (setting.volume.float64 / setting.period.milliseconds.float64) * 1000.float64
|