From e8bb33b24836985557238f5955437cb06240f7d3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Mar 2024 02:00:38 +0100 Subject: [PATCH] add rate limits to tx routes --- .env.example | 2 +- cashu/core/settings.py | 12 +++++++--- cashu/mint/limit.py | 39 +++++++++++++++++++++++++++++++++ cashu/mint/middleware.py | 36 +++++------------------------- cashu/mint/router.py | 26 +++++++++++++++++----- cashu/mint/router_deprecated.py | 14 ++++++++++-- 6 files changed, 86 insertions(+), 43 deletions(-) create mode 100644 cashu/mint/limit.py diff --git a/.env.example b/.env.example index 4eeb2fe4..459d1b67 100644 --- a/.env.example +++ b/.env.example @@ -98,4 +98,4 @@ LIGHTNING_RESERVE_FEE_MIN=2000 # Rate limit requests to mint. Make sure that you can see request IPs in the logs. # You may need to adjust your reverse proxy if you only see requests originating from 127.0.0.1 # MINT_RATE_LIMIT=TRUE -# MINT_RATE_LIMIT_PER_MINUTE=20 +# MINT_GLOBAL_RATE_LIMIT_PER_MINUTE=20 diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 1db8a8a7..1c0ee68b 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -83,11 +83,17 @@ class MintLimits(MintSettings): mint_rate_limit: bool = Field( default=False, title="Rate limit", description="IP-based rate limiter." ) - mint_rate_limit_per_minute: int = Field( + mint_global_rate_limit_per_minute: int = Field( + default=60, + gt=0, + title="Global rate limit per minute", + description="Number of requests an IP can make per minute to all endpoints.", + ) + mint_transaction_rate_limit_per_minute: int = Field( default=20, gt=0, - title="Rate limit per minute", - description="Number of requests an IP can make per minute.", + title="Transaction rate limit per minute", + description="Number of requests an IP can make per minute to transaction endpoints.", ) mint_max_request_length: int = Field( default=1000, diff --git a/cashu/mint/limit.py b/cashu/mint/limit.py new file mode 100644 index 00000000..9171dc64 --- /dev/null +++ b/cashu/mint/limit.py @@ -0,0 +1,39 @@ +from fastapi import status +from fastapi.responses import JSONResponse +from loguru import logger +from slowapi import Limiter +from slowapi.util import get_remote_address +from starlette.requests import Request + +from ..core.settings import settings + + +def _rate_limit_exceeded_handler(request: Request, exc: Exception) -> JSONResponse: + remote_address = get_remote_address(request) + logger.warning( + f"Rate limit {settings.mint_global_rate_limit_per_minute}/minute exceeded: {remote_address}" + ) + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={"detail": "Rate limit exceeded."}, + ) + + +def get_remote_address_excluding_local(request: Request) -> str: + remote_address = get_remote_address(request) + if remote_address == "127.0.0.1": + return "" + return remote_address + + +limiter_global = Limiter( + key_func=get_remote_address_excluding_local, + default_limits=[f"{settings.mint_global_rate_limit_per_minute}/minute"], + enabled=settings.mint_rate_limit, +) + +limiter = Limiter( + key_func=get_remote_address_excluding_local, + default_limits=[f"{settings.mint_transaction_rate_limit_per_minute}/minute"], + enabled=settings.mint_rate_limit, +) diff --git a/cashu/mint/middleware.py b/cashu/mint/middleware.py index 69fc396b..f70dbc9c 100644 --- a/cashu/mint/middleware.py +++ b/cashu/mint/middleware.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, status +from fastapi import FastAPI from fastapi.exception_handlers import ( request_validation_exception_handler as _request_validation_exception_handler, ) @@ -9,15 +9,13 @@ from starlette.requests import Request from ..core.settings import settings +from .limit import _rate_limit_exceeded_handler, limiter_global if settings.debug_profiling: from fastapi_profiler import PyInstrumentProfilerMiddleware -if settings.mint_rate_limit: - from slowapi import Limiter - from slowapi.errors import RateLimitExceeded - from slowapi.middleware import SlowAPIMiddleware - from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware def add_middlewares(app: FastAPI): @@ -34,31 +32,7 @@ def add_middlewares(app: FastAPI): app.add_middleware(PyInstrumentProfilerMiddleware) if settings.mint_rate_limit: - - def get_remote_address_excluding_local(request: Request) -> str: - remote_address = get_remote_address(request) - if remote_address == "127.0.0.1": - return "" - return remote_address - - limiter = Limiter( - key_func=get_remote_address_excluding_local, - default_limits=[f"{settings.mint_rate_limit_per_minute}/minute"], - ) - app.state.limiter = limiter - - def _rate_limit_exceeded_handler( - request: Request, exc: Exception - ) -> JSONResponse: - remote_address = get_remote_address(request) - logger.warning( - f"Rate limit {settings.mint_rate_limit_per_minute}/minute exceeded: {remote_address}" - ) - return JSONResponse( - status_code=status.HTTP_429_TOO_MANY_REQUESTS, - content={"detail": "Rate limit exceeded."}, - ) - + app.state.limiter = limiter_global app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 19e6f46b..cd7b6cf8 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -1,6 +1,6 @@ from typing import Any, Dict, List -from fastapi import APIRouter +from fastapi import APIRouter, Request from loguru import logger from ..core.base import ( @@ -28,6 +28,7 @@ from ..core.errors import CashuError from ..core.settings import settings from ..mint.startup import ledger +from .limit import limiter router: APIRouter = APIRouter() @@ -178,7 +179,10 @@ async def keysets() -> KeysetsResponse: response_model=PostMintQuoteResponse, response_description="A payment request to mint tokens of a denomination", ) -async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def mint_quote( + request: Request, payload: PostMintQuoteRequest +) -> PostMintQuoteResponse: """ Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow. @@ -203,7 +207,8 @@ async def mint_quote(payload: PostMintQuoteRequest) -> PostMintQuoteResponse: response_model=PostMintQuoteResponse, response_description="Get an existing mint quote to check its status.", ) -async def get_mint_quote(quote: str) -> PostMintQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse: """ Get mint quote state. """ @@ -228,7 +233,9 @@ async def get_mint_quote(quote: str) -> PostMintQuoteResponse: "A list of blinded signatures that can be used to create proofs." ), ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def mint( + request: Request, payload: PostMintRequest, ) -> PostMintResponse: """ @@ -250,7 +257,10 @@ async def mint( response_model=PostMeltQuoteResponse, response_description="Melt tokens for a payment on a supported payment method.", ) -async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def get_melt_quote( + request: Request, payload: PostMeltQuoteRequest +) -> PostMeltQuoteResponse: """ Request a quote for melting tokens. """ @@ -266,7 +276,8 @@ async def get_melt_quote(payload: PostMeltQuoteRequest) -> PostMeltQuoteResponse response_model=PostMeltQuoteResponse, response_description="Get an existing melt quote to check its status.", ) -async def melt_quote(quote: str) -> PostMeltQuoteResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse: """ Get melt quote state. """ @@ -296,7 +307,8 @@ async def melt_quote(quote: str) -> PostMeltQuoteResponse: " promises for change." ), ) -async def melt(payload: PostMeltRequest) -> PostMeltResponse: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def melt(request: Request, payload: PostMeltRequest) -> PostMeltResponse: """ Requests tokens to be destroyed and sent out via Lightning. """ @@ -320,7 +332,9 @@ async def melt(payload: PostMeltRequest) -> PostMeltResponse: "An array of blinded signatures that can be used to create proofs." ), ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def swap( + request: Request, payload: PostSplitRequest, ) -> PostSplitResponse: """ diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index e1901d7b..4a970c4f 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from fastapi import APIRouter +from fastapi import APIRouter, Request from loguru import logger from ..core.base import ( @@ -28,6 +28,7 @@ ) from ..core.errors import CashuError from ..core.settings import settings +from .limit import limiter from .startup import ledger router_deprecated: APIRouter = APIRouter() @@ -129,7 +130,10 @@ async def keysets_deprecated() -> KeysetsResponse_deprecated: ), deprecated=True, ) -async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated: +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") +async def request_mint_deprecated( + request: Request, amount: int = 0 +) -> GetMintResponse_deprecated: """ Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow. @@ -157,7 +161,9 @@ async def request_mint_deprecated(amount: int = 0) -> GetMintResponse_deprecated ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def mint_deprecated( + request: Request, payload: PostMintRequest_deprecated, hash: Optional[str] = None, payment_hash: Optional[str] = None, @@ -204,7 +210,9 @@ async def mint_deprecated( ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def melt_deprecated( + request: Request, payload: PostMeltRequest_deprecated, ) -> PostMeltResponse_deprecated: """ @@ -267,7 +275,9 @@ async def check_fees( ), deprecated=True, ) +@limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") async def split_deprecated( + request: Request, payload: PostSplitRequest_Deprecated, # ) -> Union[PostSplitResponse_Very_Deprecated, PostSplitResponse_Deprecated]: ):