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 pythorhead import Lemmy
from fediseer.fediverse import get_admin_for_software, get_nodeinfo
from fediseer.limiter import limiter
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_unauthorized = api.errorhandler(e.Unauthorized)(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_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 {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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.")
if instance.domain == domain:
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)
if not unbroken_chain:
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
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
raise e.BadRequest("Instance from which to modify censure not found")
@ -228,6 +234,7 @@ class Censures(Resource):
return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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 {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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.")
if instance.domain == domain:
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)
if not unbroken_chain:
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}")
return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
raise e.BadRequest("Instance for which to modify endorsement not found")
@ -208,7 +214,7 @@ class Endorsements(Resource):
return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
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))
return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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.")
if len(instance.guarantees) >= 20 and instance.id != 0:
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)
if not unbroken_chain:
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
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
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.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:
rejection_record.refresh()
else:

View File

@ -93,6 +93,7 @@ class Hesitations(Resource):
return {"domains": [instance["domain"] for instance in instance_details]},200
return {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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.")
if instance.domain == domain:
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)
if not unbroken_chain:
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}")
return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
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}")
return {"message":'Changed'}, 200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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)
if not instance:
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)
if not target_instance:
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 {"instances": instance_details},200
decorators = [limiter.limit("20/minute", key_func = get_request_path)]
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("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?")
if instance.is_guaranteed():
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
if 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(
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.log = log
class TooManyRequests(wze.TooManyRequests):
def __init__(self, message, log=None):
self.specific = message
self.log = log
class InternalServerError(wze.InternalServerError):
def __init__(self, message, log=None):
self.specific = message