示例#1
0
    def __init__(self, inspector, internal_api_endpoint):

        self.logger = get_logger(component=API.__name__)
        self.app = Flask(__name__)
        self.inspector = inspector
        self.internal_api_endpoint = internal_api_endpoint
        channel = grpc.insecure_channel(internal_api_endpoint)
        self.stub = TowerServicesStub(channel)

        # Adds all the routes to the functions listed above.
        routes = {
            "/register": (self.register, ["POST"]),
            "/add_appointment": (self.add_appointment, ["POST"]),
            "/get_appointment": (self.get_appointment, ["POST"]),
            "/get_subscription_info": (self.get_subscription_info, ["POST"]),
        }

        for url, params in routes.items():
            self.app.add_url_rule(url, view_func=params[0], methods=params[1])
示例#2
0
class RPCClient:
    """
    Creates and keeps a connection to the an RPC serving TowerServices. It has methods to call each of the
    available grpc services, and it returns a pretty-printed json response.
    Errors from the grpc calls are not handled.

    Args:
        rpc_host (:obj:`str`): the IP or host where the RPC server is hosted.
        rpc_port (:obj:`int`): the port where the RPC server is hosted.

    Attributes:
        stub: The rpc client stub.
    """
    def __init__(self, rpc_host, rpc_port):
        self.rpc_host = rpc_host
        self.rpc_port = rpc_port
        channel = grpc.insecure_channel(f"{rpc_host}:{rpc_port}")
        self.stub = TowerServicesStub(channel)

    @formatted
    def get_all_appointments(self):
        """Gets a list of all the appointments in the watcher, and trackers in the responder."""
        result = self.stub.get_all_appointments(Empty())
        return result.appointments

    @formatted
    def get_tower_info(self):
        """Gets generic information about the tower."""
        return self.stub.get_tower_info(Empty())

    def get_users(self):
        """Gets the list of registered user ids."""
        result = self.stub.get_users(Empty())
        return to_json(list(result.user_ids))

    @formatted
    def get_user(self, user_id):
        """
        Gets information about a specific user.

        Args:
            user_id (:obj:`str`): the id of the requested user.

        Raises:
            :obj:`InvalidParameter`: if `user_id` is not in the valid format.
        """

        if not is_compressed_pk(user_id):
            raise InvalidParameter("Invalid user id")

        result = self.stub.get_user(GetUserRequest(user_id=user_id))
        return result.user

    def stop(self):
        """Stops TEOS gracefully."""
        self.stub.stop(Empty())
        print("Closing the Eye of Satoshi")
示例#3
0
 def __init__(self, rpc_host, rpc_port):
     self.rpc_host = rpc_host
     self.rpc_port = rpc_port
     channel = grpc.insecure_channel(f"{rpc_host}:{rpc_port}")
     self.stub = TowerServicesStub(channel)
def stub():
    return TowerServicesStub(grpc.insecure_channel(internal_api_endpoint))
示例#5
0
class API:
    """
    The :class:`API` is in charge of the interface between the user and the tower. It handles and serves user requests.
    The API is connected with the :class:`InternalAPI <teos.internal_api.InternalAPI>` via gRPC.

    Args:
        inspector (:obj:`Inspector <teos.inspector.Inspector>`): an :obj:`Inspector` instance to check the correctness
            of the received appointment data.
        internal_api_endpoint (:obj:`str`): the endpoint where the internal api is served.

    Attributes:
        logger (:obj:`Logger <teos.logger.Logger>`): The logger for this component.
        app: The Flask app of the API server.
        stub (:obj:`TowerServicesStub`): The rpc client stub.
    """
    def __init__(self, inspector, internal_api_endpoint):

        self.logger = get_logger(component=API.__name__)
        self.app = Flask(__name__)
        self.inspector = inspector
        self.internal_api_endpoint = internal_api_endpoint
        channel = grpc.insecure_channel(internal_api_endpoint)
        self.stub = TowerServicesStub(channel)

        # Adds all the routes to the functions listed above.
        routes = {
            "/register": (self.register, ["POST"]),
            "/add_appointment": (self.add_appointment, ["POST"]),
            "/get_appointment": (self.get_appointment, ["POST"]),
            "/get_subscription_info": (self.get_subscription_info, ["POST"]),
        }

        for url, params in routes.items():
            self.app.add_url_rule(url, view_func=params[0], methods=params[1])

    def register(self):
        """
        Registers a user by creating a subscription.

        Registration is pretty straightforward for now, since it does not require payments.
        The amount of slots and expiry of the subscription cannot be requested by the user yet either. This is linked to
        the previous point.
        Users register by sending a public key to the proper endpoint. This is exploitable atm, but will be solved when
        payments are introduced.

        Returns:
            :obj:`tuple`: A tuple containing the response (:obj:`str`) and response code (:obj:`int`). For accepted
            requests, the ``rcode`` is always 200 and the response contains a json with the public key and number of
            slots in the subscription. For rejected requests, the ``rcode`` is a 404 and the value contains an
            application error, and an error message. Error messages can be found at ``common.errors``.
        """

        remote_addr = get_remote_addr()
        self.logger.info("Received register request",
                         from_addr="{}".format(remote_addr))

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

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

        user_id = request_data.get("public_key")

        if user_id:
            try:
                r = self.stub.register(RegisterRequest(user_id=user_id))

                rcode = HTTP_OK
                response = json_format.MessageToDict(
                    r,
                    including_default_value_fields=True,
                    preserving_proto_field_name=True)
                response["public_key"] = user_id

            except grpc.RpcError as e:
                rcode = HTTP_BAD_REQUEST
                response = {
                    "error": e.details(),
                    "error_code": errors.REGISTRATION_MISSING_FIELD
                }

        else:
            rcode = HTTP_BAD_REQUEST
            response = {
                "error": "public_key not found in register message",
                "error_code": errors.REGISTRATION_WRONG_FIELD_FORMAT,
            }

        self.logger.info("Sending response and disconnecting",
                         from_addr="{}".format(remote_addr),
                         response=response)

        return jsonify(response), rcode

    def add_appointment(self):
        """
        Main endpoint of the Watchtower.

        The client sends requests (appointments) to this endpoint to request a job to the Watchtower. Requests must be
        json encoded and contain an ``appointment`` and ``signature`` fields.

        Returns:
            :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`` contains an application error, and an error message. Error messages can
            be found at ``common.errors``.
        """

        # Getting the real IP if the server is behind a reverse proxy
        remote_addr = get_remote_addr()
        self.logger.info("Received add_appointment request",
                         from_addr="{}".format(remote_addr))

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

        except InvalidParameter as e:
            return jsonify({
                "error": str(e),
                "error_code": errors.INVALID_REQUEST_FORMAT
            }), HTTP_BAD_REQUEST

        try:
            appointment = self.inspector.inspect(
                request_data.get("appointment"))
            r = self.stub.add_appointment(
                AddAppointmentRequest(
                    appointment=Appointment(
                        locator=appointment.locator,
                        encrypted_blob=appointment.encrypted_blob,
                        to_self_delay=appointment.to_self_delay,
                    ),
                    signature=request_data.get("signature"),
                ))

            rcode = HTTP_OK
            response = json_format.MessageToDict(
                r,
                including_default_value_fields=True,
                preserving_proto_field_name=True)
        except InspectionFailed as e:
            rcode = HTTP_BAD_REQUEST
            response = {
                "error": "appointment rejected. {}".format(e.reason),
                "error_code": e.erno
            }

        except grpc.RpcError as e:
            if e.code() == grpc.StatusCode.UNAUTHENTICATED:
                rcode = HTTP_BAD_REQUEST
                response = {
                    "error":
                    f"appointment rejected. {e.details()}",
                    "error_code":
                    errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR,
                }
            elif e.code() == grpc.StatusCode.ALREADY_EXISTS:
                rcode = HTTP_BAD_REQUEST
                response = {
                    "error": f"appointment rejected. {e.details()}",
                    "error_code": errors.APPOINTMENT_ALREADY_TRIGGERED,
                }
            else:
                # This covers grpc.StatusCode.RESOURCE_EXHAUSTED (and any other return).
                rcode = HTTP_SERVICE_UNAVAILABLE
                response = {"error": "appointment rejected"}

        self.logger.info("Sending response and disconnecting",
                         from_addr="{}".format(remote_addr),
                         response=response)
        return jsonify(response), rcode

    def get_appointment(self):
        """
        Gives 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 requested appointment.

            Returns not found if the user does not have the requested appointment or the locator is invalid.

            A ``status`` flag is added to the data provided by either the :obj:`Watcher <teos.watcher.Watcher>` or the
            :obj:`Responder <teos.responder.Responder>` that signals the status of the appointment.

            - Appointments held by the :obj:`Watcher <teos.watcher.Watcher>` are flagged as
              ``AppointmentStatus.BEING_WATCHED``.
            - Appointments held by the :obj:`Responder <teos.responder.Responder>` are flagged as
              ``AppointmentStatus.DISPUTE_RESPONDED``.
            - Unknown appointments are flagged as ``AppointmentStatus.NOT_FOUND``.
        """

        # 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:
            self.logger.info("Received invalid get_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)
            self.logger.info("Received get_appointment request",
                             from_addr="{}".format(remote_addr),
                             locator=locator)

            r = self.stub.get_appointment(
                GetAppointmentRequest(locator=locator,
                                      signature=request_data.get("signature")))
            data = (r.appointment_data.appointment
                    if r.appointment_data.WhichOneof("appointment_data")
                    == "appointment" else r.appointment_data.tracker)

            rcode = HTTP_OK
            response = {
                "locator":
                locator,
                "status":
                r.status,
                "appointment":
                json_format.MessageToDict(data,
                                          including_default_value_fields=True,
                                          preserving_proto_field_name=True),
            }

        except (InspectionFailed, grpc.RpcError) as e:
            if isinstance(e, grpc.RpcError) and e.code(
            ) == grpc.StatusCode.UNAUTHENTICATED:
                rcode = HTTP_BAD_REQUEST
                response = {
                    "error":
                    e.details(),
                    "error_code":
                    errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR,
                }
            else:
                rcode = HTTP_NOT_FOUND
                response = {
                    "locator": locator,
                    "status": AppointmentStatus.NOT_FOUND
                }

        return jsonify(response), rcode

    def get_subscription_info(self):
        """
        Gives information about a user's subscription from the watchtower.

        Only a user who has registered can retrieve this information. User must provide the correct signature.

        Returns:
            :obj:`str`: A json formatted dictionary containing information about the user's subscription.

            Returns not found if the user is not yet registered with the watchtower.
        """

        # 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:
            self.logger.info("Received invalid get_subscription_info request",
                             from_addr="{}".format(remote_addr))
            return jsonify({
                "error": str(e),
                "error_code": errors.INVALID_REQUEST_FORMAT
            }), HTTP_BAD_REQUEST

        self.logger.info("Received get_subscription_info request",
                         from_addr="{}".format(remote_addr))

        try:
            r = self.stub.get_subscription_info(
                GetSubscriptionInfoRequest(
                    signature=request_data.get("signature")))

            response = intify(
                json_format.MessageToDict(r.user,
                                          including_default_value_fields=True,
                                          preserving_proto_field_name=True))
            rcode = HTTP_OK

        except grpc.RpcError as e:
            rcode = HTTP_BAD_REQUEST
            response = {
                "error":
                e.details(),
                "error_code":
                errors.APPOINTMENT_INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR,
            }

        return jsonify(response), rcode