Skip to content

Commit

Permalink
feat(decorators): add rate limit decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
Ethosa committed Dec 18, 2024
1 parent c42302d commit e94d23c
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 4 deletions.
4 changes: 4 additions & 0 deletions examples/website/src/docs/decorators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ proc Decorators*(): TagRef =

CodeBlock("nim", nimCachedDecorator, "cached_decorator")

tP: { translate"Besides caching and authorization, HappyX also has a RateLimit decorator:" }

CodeBlock("nim", nimRateLimitDecorator, "rate_limit_decorator")

tH2: { translate"Custom Decorators 💡"}
tP: { translate"You can create your own decorators also:" }

Expand Down
8 changes: 8 additions & 0 deletions examples/website/src/ui/code/nim_ssr.nim
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,12 @@ serve ... :
post "/cached/[m:TestModel]":
await sleepAsync(1000)
return m.username
"""
nimRateLimitDecorator* = """serve ... :
# default values is perSecond=60, fromAll=false
@RateLimit(perSecond = 2, fromAll = true)
get "/test/rate-limit":
outHeaders["Test"] = 10
outHeaders["HappyXHeader"] = "Hello"
return "Hello, world!"
"""
6 changes: 6 additions & 0 deletions examples/website/src/ui/translations.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2720,6 +2720,12 @@ translatable:
"zh" -> "请输入点什么"
"fr" -> "Entrez quelque chose"
"ko" -> "무언가를 입력하세요"
"Besides caching and authorization, HappyX also has a RateLimit decorator:":
"ru" -> "Помимо кэширования и авторизации HappyX имеет также декоратор RateLimit:"
"ja" -> "キャッシングや認証に加えて、HappyXにはRateLimitデコレーターもあります:"
"zh" -> "除了缓存和授权,HappyX还具有RateLimit装饰器:"
"fr" -> "En plus de la mise en cache et de l'autorisation, HappyX dispose également d'un décorateur RateLimit :"
"ko" -> "캐싱 및 인증 외에도 HappyX에는 RateLimit 데코레이터가 있습니다:"


var spokenLang: cstring
Expand Down
2 changes: 1 addition & 1 deletion happyx.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

description = "Macro-oriented asynchronous web-framework written with ♥"
author = "HapticX"
version = "4.7.0"
version = "4.7.1"
license = "MIT"
srcDir = "src"
installExt = @["nim"]
Expand Down
2 changes: 1 addition & 1 deletion src/happyx/core/constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ const
# Framework version
HpxMajor* = 4
HpxMinor* = 7
HpxPatch* = 0
HpxPatch* = 1
HpxVersion* = $HpxMajor & "." & $HpxMinor & "." & $HpxPatch


Expand Down
53 changes: 51 additions & 2 deletions src/happyx/routing/decorators.nim
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type
CachedRoute* = object
create_at*: float
res*: CachedResult
RateLimitInfo* = object
amount*: int
update_at*: float


var decorators* {.compileTime.} = newTable[string, DecoratorImpl]()
Expand Down Expand Up @@ -93,6 +96,9 @@ when enableDefaultDecorators:
var cachedRoutes* {.threadvar.}: Table[string, CachedRoute]
cachedRoutes = initTable[string, CachedRoute]()

var rateLimits* {.threadvar.}: Table[string, RateLimitInfo]
rateLimits = initTable[string, RateLimitInfo]()

proc authBasicDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
statementList.insert(0, parseStmt"""
var (username, password) = ("", "")
Expand Down Expand Up @@ -142,16 +148,58 @@ var userAgent = navigator.userAgent
)


proc rateLimitDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
var
fromAll = true
perSecond = 60

const intLits = { nnkIntLit..nnkInt64Lit }
let boolean = [newLit(true), newLit(false)]

for argument in arguments:
if argument.kind == nnkExprEqExpr and argument[0] == ident"fromAll" and argument[1] in boolean:
fromAll = argument[1].boolVal
elif argument.kind == nnkExprEqExpr and argument[0] == ident"perSecond" and argument[1].kind in intLits:
perSecond = argument[1].intVal.int

statementList.insert(0, parseStmt(fmt"""
let key =
when {not fromAll}:
if hostname != "":
hostname & "{routePath}"
elif headers.hasKey("X-Forwarded-For"):
headers["X-Forwarded-For"].split(",", 1)[0] & "{routePath}"
elif headers.hasKey("X-Real-Ip"):
headers["X-Real-Ip"] & "{routePath}"
else:
"{routePath}"
else:
"{routePath}"
if not rateLimits.hasKey(key):
rateLimits[key] = RateLimitInfo(amount: 1, update_at: cpuTime())
elif cpuTime() - rateLimits[key].update_at < 1.0:
inc rateLimits[key].amount
else:
rateLimits[key].update_at = cpuTime()
rateLimits[key].amount = 1
if rateLimits[key].amount > {perSecond}:
var statusCode = 429
return "Too many requests"
""")
)


proc cachedDecoratorImpl(httpMethods: seq[string], routePath: string, statementList: NimNode, arguments: seq[NimNode]) =
let
route = handleRoute(routePath)
purePath = route.purePath.replace('{', '_').replace('}', '_')

let expiresIn =
if arguments.len == 1 and arguments[0].kind in {nnkIntLit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit, nnkInt8Lit}:
if arguments.len == 1 and arguments[0].kind in { nnkIntLit..nnkInt64Lit }:
newLit(arguments[0].intVal.int)
elif arguments.len == 1 and arguments[0].kind == nnkExprEqExpr and arguments[0][0] == ident"expires":
if arguments[0][1].kind in {nnkIntLit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit, nnkInt8Lit}:
if arguments[0][1].kind in { nnkIntLit..nnkInt64Lit }:
newLit(arguments[0][1].intVal.int)
else:
newLit(60)
Expand Down Expand Up @@ -239,3 +287,4 @@ var userAgent = navigator.userAgent
regDecorator("AuthJWT", authJwtDecoratorImpl)
regDecorator("GetUserAgent", getUserAgentDecoratorImpl)
regDecorator("Cached", cachedDecoratorImpl)
regDecorator("RateLimit", rateLimitDecoratorImpl)
7 changes: 7 additions & 0 deletions tests/testc16.nim
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ serve "127.0.0.1", 5000:
outHeaders["Test"] = 10
outHeaders["HappyXHeader"] = "Hello"
return "Hello, world!"

# default values is perSecond=60, fromAll=false
@RateLimit(perSecond = 2, fromAll = true)
get "/test/rate-limit":
outHeaders["Test"] = 10
outHeaders["HappyXHeader"] = "Hello"
return "Hello, world!"

post "/post":
## Creates a new post
Expand Down

0 comments on commit e94d23c

Please sign in to comment.