Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

139 строки
4.0 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
  3. # License: MIT. See LICENSE
  4. from datetime import datetime
  5. from functools import wraps
  6. from typing import Union, Callable
  7. from werkzeug.wrappers import Response
  8. import frappe
  9. from frappe import _
  10. from frappe.utils import cint
  11. def apply():
  12. rate_limit = frappe.conf.rate_limit
  13. if rate_limit:
  14. frappe.local.rate_limiter = RateLimiter(rate_limit["limit"], rate_limit["window"])
  15. frappe.local.rate_limiter.apply()
  16. def update():
  17. if hasattr(frappe.local, "rate_limiter"):
  18. frappe.local.rate_limiter.update()
  19. def respond():
  20. if hasattr(frappe.local, "rate_limiter"):
  21. return frappe.local.rate_limiter.respond()
  22. class RateLimiter:
  23. def __init__(self, limit, window):
  24. self.limit = int(limit * 1000000)
  25. self.window = window
  26. self.start = datetime.utcnow()
  27. timestamp = int(frappe.utils.now_datetime().timestamp())
  28. self.window_number, self.spent = divmod(timestamp, self.window)
  29. self.key = frappe.cache().make_key(f"rate-limit-counter-{self.window_number}")
  30. self.counter = cint(frappe.cache().get(self.key))
  31. self.remaining = max(self.limit - self.counter, 0)
  32. self.reset = self.window - self.spent
  33. self.end = None
  34. self.duration = None
  35. self.rejected = False
  36. def apply(self):
  37. if self.counter > self.limit:
  38. self.rejected = True
  39. self.reject()
  40. def reject(self):
  41. raise frappe.TooManyRequestsError
  42. def update(self):
  43. self.end = datetime.utcnow()
  44. self.duration = int((self.end - self.start).total_seconds() * 1000000)
  45. pipeline = frappe.cache().pipeline()
  46. pipeline.incrby(self.key, self.duration)
  47. pipeline.expire(self.key, self.window)
  48. pipeline.execute()
  49. def headers(self):
  50. headers = {
  51. "X-RateLimit-Reset": self.reset,
  52. "X-RateLimit-Limit": self.limit,
  53. "X-RateLimit-Remaining": self.remaining,
  54. }
  55. if self.rejected:
  56. headers["Retry-After"] = self.reset
  57. else:
  58. headers["X-RateLimit-Used"] = self.duration
  59. return headers
  60. def respond(self):
  61. if self.rejected:
  62. return Response(_("Too Many Requests"), status=429)
  63. def rate_limit(key: str = None, limit: Union[int, Callable] = 5, seconds: int = 24*60*60, methods: Union[str, list] = 'ALL', ip_based: bool = True):
  64. """Decorator to rate limit an endpoint.
  65. This will limit Number of requests per endpoint to `limit` within `seconds`.
  66. Uses redis cache to track request counts.
  67. :param key: Key is used to identify the requests uniqueness (Optional)
  68. :param limit: Maximum number of requests to allow with in window time
  69. :type limit: Callable or Integer
  70. :param seconds: window time to allow requests
  71. :param methods: Limit the validation for these methods.
  72. `ALL` is a wildcard that applies rate limit on all methods.
  73. :type methods: string or list or tuple
  74. :param ip_based: flag to allow ip based rate-limiting
  75. :type ip_based: Boolean
  76. :returns: a decorator function that limit the number of requests per endpoint
  77. """
  78. def ratelimit_decorator(fun):
  79. @wraps(fun)
  80. def wrapper(*args, **kwargs):
  81. # Do not apply rate limits if method is not opted to check
  82. if methods != 'ALL' and frappe.request and frappe.request.method and frappe.request.method.upper() not in methods:
  83. return frappe.call(fun, **frappe.form_dict or kwargs)
  84. _limit = limit() if callable(limit) else limit
  85. ip = frappe.local.request_ip if ip_based is True else None
  86. user_key = frappe.form_dict[key] if key else None
  87. identity = None
  88. if key and ip_based:
  89. identity = ":".join([ip, user_key])
  90. identity = identity or ip or user_key
  91. if not identity:
  92. frappe.throw(_('Either key or IP flag is required.'))
  93. cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
  94. value = frappe.cache().get(cache_key) or 0
  95. if not value:
  96. frappe.cache().setex(cache_key, seconds, 0)
  97. value = frappe.cache().incrby(cache_key, 1)
  98. if value > _limit:
  99. frappe.throw(_("You hit the rate limit because of too many requests. Please try after sometime."))
  100. return frappe.call(fun, **frappe.form_dict or kwargs)
  101. return wrapper
  102. return ratelimit_decorator