diff --git a/teos/api.py b/teos/api.py index 79a5317..24016a5 100644 --- a/teos/api.py +++ b/teos/api.py @@ -4,6 +4,7 @@ from math import ceil from flask import Flask, request, abort, jsonify import teos.errors as errors +from teos.gatekeeper import NotEnoughSlots from teos import HOST, PORT, LOG_PREFIX from common.logger import Logger @@ -117,52 +118,60 @@ class API: if request.is_json: # Check content type once if properly defined request_data = request.get_json() + user_pk = self.gatekeeper.identify_user(request_data.get("appointment"), request_data.get("signature")) - rcode, message = self.gatekeeper.identify_user( - request_data.get("appointment"), request_data.get("signature") - ) + if user_pk: + appointment = self.inspector.inspect(request_data.get("appointment")) - if rcode: - rcode = HTTP_BAD_REQUEST - error = "appointment rejected. Error {}: {}".format(rcode, message) - return jsonify({"error": error}), rcode + if type(appointment) == Appointment: + # An appointment will fill 1 slot per ENCRYPTED_BLOB_MAX_SIZE_HEX block. + required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX) - else: - user_pk = message + # Temporarily taking out slots to avoid abusing this via race conditions + # DISCUSS: It may be worth using signals here to avoid race conditions anyway + try: + self.gatekeeper.fill_slots(user_pk, required_slots) - appointment = self.inspector.inspect(request_data.get("appointment")) + appointment_added, signature = self.watcher.add_appointment(appointment) - if type(appointment) == Appointment: - # An appointment will fill 1 slot per ENCRYPTED_BLOB_MAX_SIZE_HEX block. - required_slots = ceil(len(appointment.encrypted_blob.data) / ENCRYPTED_BLOB_MAX_SIZE_HEX) + if appointment_added: + rcode = HTTP_OK + response = {"locator": appointment.locator, "signature": signature} - if self.gatekeeper.get_slots(user_pk) >= required_slots: - appointment_added, signature = self.watcher.add_appointment(appointment) + else: + # Adding back the slots since they were not used + self.gatekeeper.free_slots(user_pk, required_slots) + rcode = HTTP_SERVICE_UNAVAILABLE + error = "appointment rejected" + response = {"error": error} - if appointment_added: - rcode = HTTP_OK - response = {"locator": appointment.locator, "signature": signature} - self.gatekeeper.fill_subscription_slots(user_pk, required_slots) - - else: - rcode = HTTP_SERVICE_UNAVAILABLE - error = "appointment rejected" + except NotEnoughSlots: + # Adding back the slots since they were not used + self.gatekeeper.free_slots(user_pk, required_slots) + rcode = HTTP_BAD_REQUEST + error = "appointment rejected. Error {}: {}".format( + errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS, + "Invalid signature or the user does not have enough slots available", + ) response = {"error": error} - else: - rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS - error = "invalid signature or the user does not have enough slots available" + elif type(appointment) == tuple: + rcode = HTTP_BAD_REQUEST + error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1]) response = {"error": error} - elif type(appointment) == tuple: - rcode = HTTP_BAD_REQUEST - error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1]) - response = {"error": error} + else: + # We should never end up here, since inspect only returns appointments or tuples. Just in case. + rcode = HTTP_BAD_REQUEST + error = "appointment rejected. Request does not match the standard" + response = {"error": error} else: - # We should never end up here, since inspect only returns appointments or tuples. Just in case. rcode = HTTP_BAD_REQUEST - error = "appointment rejected. Request does not match the standard" + error = "appointment rejected. Error {}: {}".format( + errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS, + "Invalid signature or the user does not have enough slots available", + ) response = {"error": error} else: diff --git a/teos/gatekeeper.py b/teos/gatekeeper.py index 79eb34c..67dbdc2 100644 --- a/teos/gatekeeper.py +++ b/teos/gatekeeper.py @@ -1,13 +1,18 @@ import re -import teos.errors as errors - from common.appointment import Appointment from common.cryptographer import Cryptographer SUBSCRIPTION_SLOTS = 1 + # TODO: UNITTEST, DOCS +class NotEnoughSlots(ValueError): + """Raise this when trying to subtract more slots than a user has available""" + + pass + + class Gatekeeper: def __init__(self): self.registered_users = {} @@ -36,15 +41,6 @@ class Gatekeeper: return self.registered_users[user_pk] - def fill_subscription_slots(self, user_pk, n): - slots = self.registered_users.get(user_pk) - - # FIXME: This looks pretty dangerous. I'm guessing race conditions can happen here. - if slots == n: - self.registered_users.pop(user_pk) - else: - self.registered_users[user_pk] -= n - def identify_user(self, appointment_data, signature): """ Checks if the provided user signature is comes from a registered user with available appointment slots. @@ -54,33 +50,21 @@ class Gatekeeper: signature (:obj:`str`): the user's signature (hex encoded). Returns: - :obj:`tuple`: A tuple (return code, message) as follows: - - - ``(0, None)`` if the user can be identified (recovered pk belongs to a registered user) and the user has - available slots. - - ``!= (0, None)`` otherwise. - - The possible return errors are: ``APPOINTMENT_EMPTY_FIELD`` and ``APPOINTMENT_INVALID_SIGNATURE``. + :obj:`str` or `None`: a compressed key if it can be recovered from the signature and matches a registered + user. ``None`` otherwise. """ - if signature is None: - rcode = errors.APPOINTMENT_EMPTY_FIELD - message = "empty signature received" + user_pk = None - else: + if signature is not None: appointment = Appointment.from_dict(appointment_data) rpk = Cryptographer.recover_pk(appointment.serialize(), signature) - compressed_user_pk = Cryptographer.get_compressed_pk(rpk) + compressed_pk = Cryptographer.get_compressed_pk(rpk) - if compressed_user_pk and compressed_user_pk in self.registered_users: - rcode = 0 - message = compressed_user_pk + if compressed_pk in self.registered_users and self.registered_users.get(compressed_pk) > 0: + user_pk = compressed_pk - else: - rcode = errors.APPOINTMENT_INVALID_SIGNATURE_OR_INSUFFICIENT_SLOTS - message = "invalid signature or the user does not have enough slots available" - - return rcode, message + return user_pk def get_slots(self, user_pk): """ @@ -95,3 +79,12 @@ class Gatekeeper: """ slots = self.registered_users.get(user_pk) return slots if slots is not None else 0 + + def fill_slots(self, user_pk, n): + if n >= self.registered_users.get(user_pk): + self.registered_users[user_pk] -= n + else: + raise NotEnoughSlots("No enough empty slots") + + def free_slots(self, user_pk, n): + self.registered_users[user_pk] += n