feat: added some rate limiting

pull/26/head
db0 2023-09-17 23:42:11 +02:00
parent 5b4fe4f442
commit fbbda7a17c
8 changed files with 51 additions and 5 deletions

View File

@ -11,6 +11,7 @@ from fediseer.utils import hash_api_key
from fediseer.messaging import activitypub_pm from fediseer.messaging import activitypub_pm
from pythorhead import Lemmy from pythorhead import Lemmy
from fediseer.fediverse import get_admin_for_software, get_nodeinfo from fediseer.fediverse import get_admin_for_software, get_nodeinfo
from fediseer.limiter import limiter
api = Namespace('v1', 'API Version 1' ) api = Namespace('v1', 'API Version 1' )
@ -22,6 +23,7 @@ handle_bad_request = api.errorhandler(e.BadRequest)(e.handle_bad_requests)
handle_forbidden = api.errorhandler(e.Forbidden)(e.handle_bad_requests) handle_forbidden = api.errorhandler(e.Forbidden)(e.handle_bad_requests)
handle_unauthorized = api.errorhandler(e.Unauthorized)(e.handle_bad_requests) handle_unauthorized = api.errorhandler(e.Unauthorized)(e.handle_bad_requests)
handle_not_found = api.errorhandler(e.NotFound)(e.handle_bad_requests) handle_not_found = api.errorhandler(e.NotFound)(e.handle_bad_requests)
handle_too_many_requests = api.errorhandler(e.TooManyRequests)(e.handle_bad_requests)
handle_internal_server_error = api.errorhandler(e.InternalServerError)(e.handle_bad_requests) handle_internal_server_error = api.errorhandler(e.InternalServerError)(e.handle_bad_requests)
handle_service_unavailable = api.errorhandler(e.ServiceUnavailable)(e.handle_bad_requests) handle_service_unavailable = api.errorhandler(e.ServiceUnavailable)(e.handle_bad_requests)

View File

@ -108,6 +108,7 @@ class Censures(Resource):
return {"domains": [instance["domain"] for instance in instance_details]},200 return {"domains": [instance["domain"] for instance in instance_details]},200
return {"instances": instance_details},200 return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
put_parser = reqparse.RequestParser() put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -135,6 +136,8 @@ class Censures(Resource):
raise e.Forbidden("Only guaranteed instances can censure others.") raise e.Forbidden("Only guaranteed instances can censure others.")
if instance.domain == domain: if instance.domain == domain:
raise e.BadRequest("You're a mad lad, but you can't censure yourself.") raise e.BadRequest("You're a mad lad, but you can't censure yourself.")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id)
if not unbroken_chain: if not unbroken_chain:
raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!") raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!")
@ -173,6 +176,7 @@ class Censures(Resource):
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
patch_parser = reqparse.RequestParser() patch_parser = reqparse.RequestParser()
patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -195,6 +199,8 @@ class Censures(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance from which to modify censure not found") raise e.BadRequest("Instance from which to modify censure not found")
@ -228,6 +234,7 @@ class Censures(Resource):
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
delete_parser = reqparse.RequestParser() delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")

View File

@ -87,6 +87,7 @@ class Endorsements(Resource):
return {"domains": [instance["domain"] for instance in instance_details]},200 return {"domains": [instance["domain"] for instance in instance_details]},200
return {"instances": instance_details},200 return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
put_parser = reqparse.RequestParser() put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
put_parser.add_argument("reason", default=None, type=str, required=False, location="json") put_parser.add_argument("reason", default=None, type=str, required=False, location="json")
@ -113,6 +114,8 @@ class Endorsements(Resource):
raise e.Forbidden("Only guaranteed instances can endorse others.") raise e.Forbidden("Only guaranteed instances can endorse others.")
if instance.domain == domain: if instance.domain == domain:
raise e.BadRequest("Nice try, but you can't endorse yourself.") raise e.BadRequest("Nice try, but you can't endorse yourself.")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id)
if not unbroken_chain: if not unbroken_chain:
raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!") raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!")
@ -160,6 +163,7 @@ class Endorsements(Resource):
logger.info(f"{instance.domain} Endorsed {domain}") logger.info(f"{instance.domain} Endorsed {domain}")
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
patch_parser = reqparse.RequestParser() patch_parser = reqparse.RequestParser()
patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -181,6 +185,8 @@ class Endorsements(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance for which to modify endorsement not found") raise e.BadRequest("Instance for which to modify endorsement not found")
@ -208,7 +214,7 @@ class Endorsements(Resource):
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
delete_parser = reqparse.RequestParser() delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -227,6 +233,8 @@ class Endorsements(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance from which to withdraw endorsement not found") raise e.BadRequest("Instance from which to withdraw endorsement not found")

View File

@ -56,6 +56,7 @@ class Guarantees(Resource):
logger.debug(database.get_guarantor_chain(instance.id)) logger.debug(database.get_guarantor_chain(instance.id))
return {"instances": instance_details},200 return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
put_parser = reqparse.RequestParser() put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -83,6 +84,8 @@ class Guarantees(Resource):
raise e.Forbidden("Only guaranteed instances can guarantee others.") raise e.Forbidden("Only guaranteed instances can guarantee others.")
if len(instance.guarantees) >= 20 and instance.id != 0: if len(instance.guarantees) >= 20 and instance.id != 0:
raise e.Forbidden("You cannot guarantee for more than 20 instances") raise e.Forbidden("You cannot guarantee for more than 20 instances")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id)
if not unbroken_chain: if not unbroken_chain:
raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!") raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!")
@ -133,6 +136,7 @@ class Guarantees(Resource):
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
delete_parser = reqparse.RequestParser() delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -151,6 +155,8 @@ class Guarantees(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance from which to withdraw endorsement not found") raise e.BadRequest("Instance from which to withdraw endorsement not found")
@ -184,7 +190,7 @@ class Guarantees(Resource):
db.session.add(solicitation_report) db.session.add(solicitation_report)
db.session.delete(guarantee) db.session.delete(guarantee)
rejection_record = database.get_rejection_record(instance.id,target_instance.id) rejection_recorinstanced = database.get_rejection_record(instance.id,target_instance.id)
if rejection_record: if rejection_record:
rejection_record.refresh() rejection_record.refresh()
else: else:

View File

@ -93,6 +93,7 @@ class Hesitations(Resource):
return {"domains": [instance["domain"] for instance in instance_details]},200 return {"domains": [instance["domain"] for instance in instance_details]},200
return {"instances": instance_details},200 return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
put_parser = reqparse.RequestParser() put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -120,6 +121,8 @@ class Hesitations(Resource):
raise e.Forbidden("Only guaranteed instances can hesitation others.") raise e.Forbidden("Only guaranteed instances can hesitation others.")
if instance.domain == domain: if instance.domain == domain:
raise e.BadRequest("You're a mad lad, but you can't hesitation yourself.") raise e.BadRequest("You're a mad lad, but you can't hesitation yourself.")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id) unbroken_chain, chainbreaker = database.has_unbroken_chain(instance.id)
if not unbroken_chain: if not unbroken_chain:
raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!") raise e.Forbidden(f"Guarantee chain for this instance has been broken. Chain ends at {chainbreaker.domain}!")
@ -157,7 +160,7 @@ class Hesitations(Resource):
logger.info(f"{instance.domain} hesitated against {domain}") logger.info(f"{instance.domain} hesitated against {domain}")
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
patch_parser = reqparse.RequestParser() patch_parser = reqparse.RequestParser()
patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') patch_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -180,6 +183,8 @@ class Hesitations(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance from which to modify hesitation not found") raise e.BadRequest("Instance from which to modify hesitation not found")
@ -212,7 +217,7 @@ class Hesitations(Resource):
logger.info(f"{instance.domain} modIfied hesitation for {domain}") logger.info(f"{instance.domain} modIfied hesitation for {domain}")
return {"message":'Changed'}, 200 return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
delete_parser = reqparse.RequestParser() delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@ -231,6 +236,8 @@ class Hesitations(Resource):
instance = database.find_instance_by_api_key(self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
target_instance = database.find_instance_by_domain(domain=domain) target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance: if not target_instance:
raise e.BadRequest("Instance from which to withdraw hesitation not found") raise e.BadRequest("Instance from which to withdraw hesitation not found")

View File

@ -28,6 +28,7 @@ class Solicitations(Resource):
return {"domains": [instance["domain"] for instance in instance_details]},200 return {"domains": [instance["domain"] for instance in instance_details]},200
return {"instances": instance_details},200 return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
post_parser = reqparse.RequestParser() post_parser = reqparse.RequestParser()
post_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") post_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
post_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers') post_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
@ -54,6 +55,8 @@ class Solicitations(Resource):
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to claim it?") raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to claim it?")
if instance.is_guaranteed(): if instance.is_guaranteed():
raise e.BadRequest(f"Your instance is already guaranteed by {instance.get_guarantor().domain}") raise e.BadRequest(f"Your instance is already guaranteed by {instance.get_guarantor().domain}")
if database.has_too_many_actions_per_min(instance.domain):
raise e.TooManyRequests("Your instance is doing more than 20 actions per minute. Please slow down.")
guarantor_instance = None guarantor_instance = None
if self.args.guarantor: if self.args.guarantor:
guarantor_instance = database.find_instance_by_domain(self.args.guarantor) guarantor_instance = database.find_instance_by_domain(self.args.guarantor)

View File

@ -446,4 +446,12 @@ def find_latest_solicitation_by_source(source_id):
).filter( ).filter(
Solicitation.source_id == source_id, Solicitation.source_id == source_id,
) )
return query.order_by(Solicitation.created.desc()).first() return query.order_by(Solicitation.created.desc()).first()
def has_too_many_actions_per_min(source_domain):
query = Report.query.filter_by(
source_domain=source_domain
).filter(
Report.created > datetime.utcnow() - timedelta(minutes=1),
)
return query.count() > 20

View File

@ -26,6 +26,11 @@ class Locked(wze.Locked):
self.specific = message self.specific = message
self.log = log self.log = log
class TooManyRequests(wze.TooManyRequests):
def __init__(self, message, log=None):
self.specific = message
self.log = log
class InternalServerError(wze.InternalServerError): class InternalServerError(wze.InternalServerError):
def __init__(self, message, log=None): def __init__(self, message, log=None):
self.specific = message self.specific = message