From 57cbca4700d5a99a312c6730c0959241a3269337 Mon Sep 17 00:00:00 2001 From: shtlrs Date: Sun, 10 Mar 2024 22:33:56 +0100 Subject: [PATCH] decorate all request handler's callback functions with the ratelimiter --- src/pinnwand/error.py | 2 +- src/pinnwand/handler/api_curl.py | 3 +- src/pinnwand/handler/api_deprecated.py | 18 +++-------- src/pinnwand/handler/api_v1.py | 22 ++++++------- src/pinnwand/handler/website.py | 44 +++++++------------------- 5 files changed, 27 insertions(+), 62 deletions(-) diff --git a/src/pinnwand/error.py b/src/pinnwand/error.py index 2de2c97..6751cc8 100644 --- a/src/pinnwand/error.py +++ b/src/pinnwand/error.py @@ -1,5 +1,5 @@ class ValidationError(ValueError): - """This exception is used to indicate that a certain requst is lacking or + """This exception is used to indicate that a certain request is lacking or has unacceptable data aboard.""" pass diff --git a/src/pinnwand/handler/api_curl.py b/src/pinnwand/handler/api_curl.py index 0de25ed..813c328 100644 --- a/src/pinnwand/handler/api_curl.py +++ b/src/pinnwand/handler/api_curl.py @@ -29,9 +29,8 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: else: super().write_error(status_code, **kwargs) + @defensive.ratelimit_endpoint(area="create") def post(self) -> None: - if defensive.ratelimit(self.request, area="create"): - raise error.RatelimitError configuration: Configuration = ConfigurationProvider.get_config() lexer = self.get_body_argument("lexer", "text") diff --git a/src/pinnwand/handler/api_deprecated.py b/src/pinnwand/handler/api_deprecated.py index fbc9106..8fe98de 100644 --- a/src/pinnwand/handler/api_deprecated.py +++ b/src/pinnwand/handler/api_deprecated.py @@ -75,10 +75,8 @@ async def post(self) -> None: class Show(Base): """Show a paste on the deprecated API.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, slug: str) -> None: # type: ignore - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: paste = ( session.query(models.Paste) @@ -121,9 +119,8 @@ def check_xsrf_cookie(self) -> None: async def get(self) -> None: raise tornado.web.HTTPError(405) + @defensive.ratelimit_endpoint(area="create") async def post(self) -> None: - if defensive.ratelimit(self.request, area="create"): - raise error.RatelimitError() configuration: Configuration = ConfigurationProvider.get_config() @@ -178,9 +175,8 @@ def check_xsrf_cookie(self) -> None: """No XSRF cookies on the API.""" return + @defensive.ratelimit_endpoint(area="delete") async def post(self) -> None: - if defensive.ratelimit(self.request, area="delete"): - raise error.RatelimitError() with manager.DatabaseManager.get_session() as session: paste = ( @@ -211,20 +207,16 @@ async def post(self) -> None: class Lexer(Base): """List lexers through the deprecated API.""" + @defensive.ratelimit_endpoint(area="read") async def get(self) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - self.write(utility.list_languages()) class Expiry(Base): """List expiries through the deprecated API.""" + @defensive.ratelimit_endpoint(area="read") async def get(self) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - configuration: Configuration = ConfigurationProvider.get_config() self.write( diff --git a/src/pinnwand/handler/api_v1.py b/src/pinnwand/handler/api_v1.py index 8f82770..b1b46b7 100644 --- a/src/pinnwand/handler/api_v1.py +++ b/src/pinnwand/handler/api_v1.py @@ -19,18 +19,16 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: class Lexer(Base): - async def get(self) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() + @defensive.ratelimit_endpoint(area="read") + async def get(self) -> None: self.write(utility.list_languages()) class Expiry(Base): - async def get(self) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() + @defensive.ratelimit_endpoint(area="read") + async def get(self) -> None: configuration: Configuration = ConfigurationProvider.get_config() self.write( @@ -48,17 +46,16 @@ def check_xsrf_cookie(self) -> None: async def get(self) -> None: raise tornado.web.HTTPError(405) + @defensive.ratelimit_endpoint(area="create") async def post(self) -> None: - if defensive.ratelimit(self.request, area="create"): - raise error.RatelimitError() - - configuration: Configuration = ConfigurationProvider.get_config() try: data = tornado.escape.json_decode(self.request.body) except json.decoder.JSONDecodeError: raise tornado.web.HTTPError(400, "could not parse json body") + configuration: Configuration = ConfigurationProvider.get_config() + expiry = data.get("expiry") if expiry not in configuration.expiries: @@ -130,10 +127,9 @@ async def post(self) -> None: class PasteDetail(Base): - async def get(self, slug: str) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() + @defensive.ratelimit_endpoint(area="read") + async def get(self, slug: str) -> None: with manager.DatabaseManager.get_session() as session: paste = ( session.query(models.Paste) diff --git a/src/pinnwand/handler/website.py b/src/pinnwand/handler/website.py index 2e2b613..387b51a 100644 --- a/src/pinnwand/handler/website.py +++ b/src/pinnwand/handler/website.py @@ -82,15 +82,13 @@ class Create(Base): """The index page shows the new paste page with a list of all available lexers from Pygments.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, lexers: str = "") -> None: """Render the new paste form, optionally have a lexer preselected from the URL.""" configuration: Configuration = ConfigurationProvider.get_config() - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - lexers_available = utility.list_languages() lexers_selected = [ lexer for lexer in lexers.split("+") if lexer.strip() @@ -114,6 +112,7 @@ async def get(self, lexers: str = "") -> None: paste=None, ) + @defensive.ratelimit_endpoint(area="create") async def post(self) -> None: """This is a historical endpoint to create pastes, pastes are marked as old-web and will get a warning on top of them to remove any access to @@ -129,9 +128,6 @@ async def post(self) -> None: expiry = self.get_body_argument("expiry") configuration: Configuration = ConfigurationProvider.get_config() - if defensive.ratelimit(self.request, area="create"): - raise error.RatelimitError() - if lexer not in utility.list_languages(): log.info("Paste.post: a paste was submitted with an invalid lexer") raise tornado.web.HTTPError(400) @@ -178,12 +174,10 @@ class CreateAction(Base): """The create action is the 'new' way to create pastes and supports multi file pastes.""" + @defensive.ratelimit_endpoint(area="create") def post(self) -> None: # type: ignore """POST handler for the 'web' side of things.""" - if defensive.ratelimit(self.request, area="create"): - raise error.RatelimitError() - configuration: Configuration = ConfigurationProvider.get_config() expiry = self.get_body_argument("expiry") @@ -266,13 +260,11 @@ class Repaste(Base): """Repaste is a specific case of the paste page. It only works for pre- existing pastes and will prefill the textarea and lexer.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, slug: str) -> None: # type: ignore """Render the new paste form, optionally have a lexer preselected from the URL.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - configuration: Configuration = ConfigurationProvider.get_config() with manager.DatabaseManager.get_session() as session: @@ -301,12 +293,10 @@ async def get(self, slug: str) -> None: # type: ignore class Show(Base): """Show a paste.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, slug: str) -> None: # type: ignore """Fetch paste from database by slug and render the paste.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: paste = ( session.query(models.Paste) @@ -370,12 +360,10 @@ async def get(self, slug: str) -> None: # type: ignore class FileRaw(Base): """Show a file as plaintext.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and show it in the plain.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: file = ( session.query(models.File) @@ -403,12 +391,10 @@ async def get(self, file_id: str) -> None: # type: ignore class FileHex(Base): """Show a file as hexadecimal.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and show it in hex.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: file = ( session.query(models.File) @@ -436,12 +422,10 @@ async def get(self, file_id: str) -> None: # type: ignore class PasteDownload(Base): """Download an entire paste.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, paste_id: str) -> None: # type: ignore """Get all files from the database and download them as a zipfile.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: paste = ( session.query(models.Paste) @@ -485,12 +469,10 @@ async def get(self, paste_id: str) -> None: # type: ignore class FileDownload(Base): """Download a file.""" + @defensive.ratelimit_endpoint(area="read") async def get(self, file_id: str) -> None: # type: ignore """Get a file from the database and download it in the plain.""" - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: file = ( session.query(models.File) @@ -529,13 +511,11 @@ async def get(self, file_id: str) -> None: # type: ignore class Remove(Base): """Remove a paste.""" + @defensive.ratelimit_endpoint(area="delete") async def get(self, removal: str) -> None: # type: ignore """Look up if the user visiting this page has the removal id for a certain paste. If they do they're authorized to remove the paste.""" - if defensive.ratelimit(self.request, area="delete"): - raise error.RatelimitError() - with manager.DatabaseManager.get_session() as session: paste = ( session.query(models.Paste) @@ -569,10 +549,8 @@ class RestructuredTextPage(Base): def initialize(self, file: str) -> None: self.file = file + @defensive.ratelimit_endpoint(area="read") async def get(self) -> None: - if defensive.ratelimit(self.request, area="read"): - raise error.RatelimitError() - try: with open(path.page / self.file) as f: html = docutils.core.publish_parts(