Skip to content
This repository was archived by the owner on Sep 26, 2022. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions cli/teos_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,46 @@ def get_all_appointments(teos_url):
return None


def delete_appointment(locator, cli_sk, teos_pk, teos_url):
"""
Deletes information about an appointment from the tower.

Args:
locator (:obj:`str`): the locator used to identify the appointment.
cli_sk (:obj:`PrivateKey`): the client's private key.
teos_pk (:obj:`PublicKey`): the tower's public key.
teos_url (:obj:`str`): the teos base url.

Returns:
:obj:`dict` or :obj:`None`: a dictionary containing the appointment data if the locator is valid and the tower
responds. ``None`` otherwise.

Raises:
:obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if ``locator`` is invalid.
:obj:`ConnectionError`: if the client cannot connect to the tower.
:obj:`TowerResponseError <cli.exceptions.TowerResponseError>`: if the tower responded with an error, or the
response was invalid.
"""

# FIXME: All responses from the tower should be signed. Not using teos_pk atm.

if not is_locator(locator):
raise InvalidParameter("The provided locator is not valid", locator=locator)

message = "delete appointment {}".format(locator)
signature = Cryptographer.sign(message.encode(), cli_sk)
data = {"locator": locator, "signature": signature}

# Send request to the server.
delete_appointment_endpoint = "{}/delete_appointment".format(teos_url)
logger.info("Sending appointment deletion request to the Eye of Satoshi")
server_response = post_request(data, delete_appointment_endpoint)

response_json = process_post_response(server_response)

return response_json


def load_keys(teos_pk_path, user_sk_path):
"""
Loads all the keys required so sign, send, and verify the appointment.
Expand Down
75 changes: 74 additions & 1 deletion teos/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from teos import LOG_PREFIX
import common.errors as errors
from teos.inspector import InspectionFailed
from teos.watcher import AppointmentLimitReached, AppointmentAlreadyTriggered
from teos.gatekeeper import NotEnoughSlots, AuthenticationFailure
from teos.watcher import AppointmentLimitReached, AppointmentAlreadyTriggered, AppointmentNotFound

from common.logger import Logger
from common.cryptographer import hash_160
Expand Down Expand Up @@ -87,6 +87,7 @@ def __init__(self, host, port, inspector, watcher):
"/add_appointment": (self.add_appointment, ["POST"]),
"/get_appointment": (self.get_appointment, ["POST"]),
"/get_all_appointments": (self.get_all_appointments, ["GET"]),
"/delete_appointment": (self.delete_appointment, ["POST"]),
}

for url, params in routes.items():
Expand Down Expand Up @@ -300,6 +301,78 @@ def get_all_appointments(self):

return response

def delete_appointment(self):
"""
Delete information about a given appointment state in the Watchtower.

The information is requested by ``locator``.

Returns:
:obj:`str`: A json formatted dictionary containing information about the appointment deletion request.

Returns not found if the user does not have the requested appointment or the locator is invalid.
Returns bad request if the appointment does not exist in the Watchtower.

A ``status`` flag is added to the data that signals the status of the deletion request.

- A successfully deleted appointment is flagged as ``deletion_accepted``.
- An appointment that did not exist (or was already deleted), or where the locator is invalid or the user
does not have the requested appointment, are flagged as ``deletion_rejected``.

:obj:`tuple`: A tuple containing the response (:obj:`str`) and response code (:obj:`int`). For accepted
appointments, the ``rcode`` is always 200 and the response contains the receipt signature (json). For
rejected appointments, the ``rcode`` is either 400 or 404:

If the appointment is not found: 404
If the request is invalid: 400
If the appointment is already in the responder: 400 + message
"""

# Getting the real IP if the server is behind a reverse proxy
remote_addr = get_remote_addr()

# Check that data type and content are correct. Abort otherwise.
try:
request_data = get_request_data_json(request)

except InvalidParameter as e:
logger.info("Received invalid delete_appointment request", from_addr="{}".format(remote_addr))
return jsonify({"error": str(e), "error_code": errors.INVALID_REQUEST_FORMAT}), HTTP_BAD_REQUEST

locator = request_data.get("locator")

try:
self.inspector.check_locator(locator)
logger.info("Received delete_appointment request", from_addr=remote_addr, locator=locator)

message = "delete appointment {}".format(locator).encode()
user_signature = request_data.get("signature")
user_id = self.watcher.gatekeeper.authenticate_user(message, user_signature)

tower_signature = self.watcher.pop_appointment(locator, user_id, user_signature)

rcode = HTTP_OK
response = {
"locator": locator,
"signature": tower_signature,
"available_slots": self.watcher.gatekeeper.registered_users[user_id].get("available_slots"),
"status": "deletion_accepted",
}

except AppointmentNotFound:
rcode = HTTP_NOT_FOUND
response = {"locator": locator, "status": "deletion_rejected"}

except AppointmentAlreadyTriggered as e:
rcode = HTTP_BAD_REQUEST
response = {"locator": locator, "status": "deletion_rejected", "error": e.msg}

except (InspectionFailed, AuthenticationFailure):
rcode = HTTP_BAD_REQUEST
response = {"locator": locator, "status": "deletion_rejected"}

return jsonify(response), rcode

def start(self):
""" This function starts the Flask server used to run the API """

Expand Down
51 changes: 50 additions & 1 deletion teos/watcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from queue import Queue
from threading import Thread
from threading import Thread, Lock
from collections import OrderedDict
from readerwriterlock import rwlock

Expand All @@ -22,6 +22,10 @@ class AppointmentLimitReached(BasicException):
"""Raised when the tower maximum appointment count has been reached"""


class AppointmentNotFound(BasicException):
"""Raised when an appointment is not found in the tower"""


class AppointmentAlreadyTriggered(BasicException):
"""Raised when an appointment is sent to the Watcher but that same data has already been sent to the Responder"""

Expand Down Expand Up @@ -335,6 +339,51 @@ def add_appointment(self, appointment, signature):
"subscription_expiry": self.gatekeeper.registered_users[user_id].subscription_expiry,
}

def delete_appointment(self, locator, user_id, user_signature):
"""
Deletes an appointment from the ``Watcher``. The tower will stop monitoring the deleted appointment.

Args:
locator (:obj:`str`): a 16-byte hex string identifying the appointment.
user_id (:obj:`str`): the public key that identifies the user who request the deletion (33-bytes hex str).
user_signature (:obj:`str`): the signature of the request provided by the user. The tower will sign the
signature deletion is accepted.

Returns:
:obj:`str`: the signature of the user's signature if the appointment if the deletion is accepted.

Rises:
:obj:`AppointmentAlreadyTriggered`: if the appointment is already in the Responder. The deletion is
therefore rejected.
:obj:`AppointmentNotFound`: if the appointment cannot be found in the tower. The deletion is therefore
rejected.
"""

uuid = hash_160("{}{}".format(locator, user_id))

# FIXME: We need to keep track of deletions

if uuid in self.appointments:
# Delete the appointment from both the Watcher and the Gatekeeper
Cleaner.delete_completed_appointments([uuid], self.appointments, self.locator_uuid_map, self.db_manager)
Cleaner.delete_gatekeeper_appointments(self.gatekeeper, {uuid: user_id})

# Sign over the user signature as acceptance of the deletion request.
signature = Cryptographer.sign(user_signature.encode(), self.signing_key)
logger.info("Appointment deleted", locator=locator)

return signature

elif uuid in self.responder.trackers:
message = "Cannot delete an already triggered appointment"
logger.info(message, locator=locator)
raise AppointmentAlreadyTriggered(message)

else:
message = "Appointment not found. Deletion rejected"
logger.info(message, locator=locator)
raise AppointmentNotFound(message)

def do_watch(self):
"""
Monitors the blockchain for channel breaches.
Expand Down