81 lines
2 KiB
Python
81 lines
2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# MIT License. See license.txt
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
from datetime import datetime
|
|
import frappe
|
|
from frappe import _
|
|
from frappe.utils import cint
|
|
from werkzeug.wrappers import Response
|
|
|
|
|
|
def apply():
|
|
rate_limit = frappe.conf.rate_limit
|
|
if rate_limit:
|
|
frappe.local.rate_limiter = RateLimiter(rate_limit["limit"], rate_limit["window"])
|
|
frappe.local.rate_limiter.apply()
|
|
|
|
|
|
def update():
|
|
if hasattr(frappe.local, "rate_limiter"):
|
|
frappe.local.rate_limiter.update()
|
|
|
|
|
|
def respond():
|
|
if hasattr(frappe.local, "rate_limiter"):
|
|
return frappe.local.rate_limiter.respond()
|
|
|
|
|
|
class RateLimiter:
|
|
def __init__(self, limit, window):
|
|
self.limit = int(limit * 1000000)
|
|
self.window = window
|
|
|
|
self.start = datetime.utcnow()
|
|
timestamp = int(frappe.utils.now_datetime().timestamp())
|
|
|
|
self.window_number, self.spent = divmod(timestamp, self.window)
|
|
self.key = frappe.cache().make_key(f"rate-limit-counter-{self.window_number}")
|
|
self.counter = cint(frappe.cache().get(self.key))
|
|
self.remaining = max(self.limit - self.counter, 0)
|
|
self.reset = self.window - self.spent
|
|
|
|
self.end = None
|
|
self.duration = None
|
|
self.rejected = False
|
|
|
|
def apply(self):
|
|
if self.counter > self.limit:
|
|
self.rejected = True
|
|
self.reject()
|
|
|
|
def reject(self):
|
|
raise frappe.TooManyRequestsError
|
|
|
|
def update(self):
|
|
self.end = datetime.utcnow()
|
|
self.duration = int((self.end - self.start).total_seconds() * 1000000)
|
|
|
|
pipeline = frappe.cache().pipeline()
|
|
pipeline.incrby(self.key, self.duration)
|
|
pipeline.expire(self.key, self.window)
|
|
pipeline.execute()
|
|
|
|
def headers(self):
|
|
headers = {
|
|
"X-RateLimit-Reset": self.reset,
|
|
"X-RateLimit-Limit": self.limit,
|
|
"X-RateLimit-Remaining": self.remaining,
|
|
}
|
|
if self.rejected:
|
|
headers["Retry-After"] = self.reset
|
|
else:
|
|
headers["X-RateLimit-Used"] = self.duration
|
|
|
|
return headers
|
|
|
|
def respond(self):
|
|
if self.rejected:
|
|
return Response(_("Too Many Requests"), status=429)
|