def create_appointment(appointment_data): """ Creates an appointment object from an appointment data dictionary provided by the user. Performs all the required sanity checks on the input data: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. Returns: :obj:`common.appointment.Appointment`: An appointment built from the appointment data provided by the user. """ tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not tx_id: raise InvalidParameter("Missing tx_id, locator cannot be computed") elif not is_256b_hex_str(tx_id): raise InvalidParameter("Wrong tx_id, locator cannot be computed") elif not tx: raise InvalidParameter("The tx field is missing in the provided data") elif not isinstance(tx, str): raise InvalidParameter("The provided tx field is not a string") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) return Appointment.from_dict(appointment_data)
def create_appointment_receipt(user_signature, start_block): """ Creates an appointment receipt. The receipt has the following format: ``user_signature | start_block (4-byte)`` All values are big endian. Args: user_signature (:obj:`str`): the signature of the appointment by the user. start_block (:obj:`int`): the block height at which the tower will start watching for the appointment. Returns: :obj:`bytes`: The serialized data to be signed. """ if not isinstance(user_signature, str): raise InvalidParameter("Provided user_signature is invalid") elif not is_u4int(start_block): raise InvalidParameter( "Provided start_block must be a 4-byte unsigned integer") return pyzbase32.decode_bytes(user_signature) + struct.pack( ">I", start_block)
def load_key_file(file_path): """ Loads a key from a key file. Args: file_path (:obj:`str`): the path to the key file to be loaded. Returns: :obj:`bytes`: the key file data if the file can be found and read. Raises: :obj:`InvalidParameter`: if the file_path has wrong format or cannot be found. :obj:`InvalidKey`: if the key cannot be loaded from the file. It covers temporary I/O errors. """ if not isinstance(file_path, str): raise InvalidParameter( "Key file path was expected, {} received".format( type(file_path))) try: with open(file_path, "rb") as key_file: key = key_file.read() return key except FileNotFoundError: raise InvalidParameter( "Key file not found at {}. Please check your settings".format( file_path)) except IOError as e: raise InvalidKey("Key file cannot be loaded", exception=e)
def create_registration_receipt(user_id, available_slots, subscription_expiry): """ Creates a registration receipt. The receipt has the following format: ``user_id (33-byte) | available_slots (4-byte) | subscription_expiry (4-byte)`` All values are big endian. Args: user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). available_slots (:obj:`int`): the number of slots assigned to a user subscription (4-byte unsigned int). subscription_expiry (:obj:`int`): the expiry assigned to a user subscription (4-byte unsigned int). Returns: :obj:`bytes`: The serialized data to be signed. """ if not is_compressed_pk(user_id): raise InvalidParameter( "Provided public key does not match expected format (33-byte hex string)" ) elif not is_u4int(available_slots): raise InvalidParameter( "Provided available_slots must be a 4-byte unsigned integer") elif not is_u4int(subscription_expiry): raise InvalidParameter( "Provided subscription_expiry must be a 4-byte unsigned integer") return bytes.fromhex(user_id) + available_slots.to_bytes( 4, "big") + subscription_expiry.to_bytes(4, "big")
def save_key_file(key, name, data_dir): """ Saves a key to disk in DER format. Args: key (:obj:`bytes`): the key to be saved to disk. name (:obj:`str`): the name of the key file to be generated. data_dir (:obj:`str`): the data directory where the file will be saved. Raises: :obj:`InvalidParameter`: If the given key is not bytes or the name or data_dir are not strings. """ if not isinstance(key, bytes): raise InvalidParameter("Key must be bytes, {} received".format( type(key))) if not isinstance(name, str): raise InvalidParameter("Key name must be str, {} received".format( type(name))) if not isinstance(data_dir, str): raise InvalidParameter("Data dir must be str, {} received".format( type(data_dir))) # Create the output folder it it does not exist (and all the parents if they don't either) Path(data_dir).mkdir(parents=True, exist_ok=True) with open(os.path.join(data_dir, "{}.der".format(name)), "wb") as der_out: der_out.write(key)
def parse_add_appointment_arguments(kwargs): """ Parses the arguments of the add_appointment command and checks that they are correct. The expected arguments are a commitment transaction id (32-byte hex string) and the penalty transaction. Args: kwargs (:obj:`dict`): a dictionary of arguments. Returns: :obj:`tuple`: the commitment transaction id and the penalty transaction. Raises: :obj:`common.exceptions.InvalidParameter`: if any of the parameters is wrong or missing. """ # Arguments to add_appointment come from c-lightning and they have been sanitised. Checking this just in case. commitment_txid = kwargs.get("commitment_txid") penalty_tx = kwargs.get("penalty_tx") if commitment_txid is None: raise InvalidParameter("missing required parameter: commitment_txid") if penalty_tx is None: raise InvalidParameter("missing required parameter: penalty_tx") if not is_256b_hex_str(commitment_txid): raise InvalidParameter("commitment_txid has invalid format") # Checking the basic stuff for the penalty transaction for now if type(penalty_tx) is not str or re.search(r"^[0-9A-Fa-f]+$", penalty_tx) is None: raise InvalidParameter("penalty_tx has invalid format") return commitment_txid, penalty_tx
def parse_register_arguments(tower_id, host, port, config): """ Parses the arguments of the register command and checks that they are correct. Args: tower_id (:obj:`str`): the identifier of the tower to connect to (a compressed public key). host (:obj:`str`): the ip or hostname to connect to, optional. host (:obj:`int`): the port to connect to, optional. config: (:obj:`dict`): the configuration dictionary. Returns: :obj:`tuple`: the tower id and tower network address. Raises: :obj:`common.exceptions.InvalidParameter`: if any of the parameters is wrong or missing. """ if not isinstance(tower_id, str): raise InvalidParameter( f"tower id must be a compressed public key (33-byte hex value) not {str(tower_id)}" ) # tower_id is of the form tower_id@[ip][:][port] if "@" in tower_id: if not (host and port): tower_id, tower_netaddr = tower_id.split("@") if not tower_netaddr: raise InvalidParameter("no tower endpoint was provided") # Only host was specified or colons where specified but not port if ":" not in tower_netaddr: tower_netaddr = f"{tower_netaddr}:{config.get('DEFAULT_PORT')}" elif tower_netaddr.endswith(":"): tower_netaddr = f"{tower_netaddr}{config.get('DEFAULT_PORT')}" else: raise InvalidParameter( "cannot specify host as both xxx@yyy and separate arguments") # host was specified, but no port, defaulting elif host: tower_netaddr = f"{host}:{config.get('DEFAULT_PORT')}" # host and port specified elif host and port: tower_netaddr = f"{host}:{port}" else: raise InvalidParameter("tower host is missing") if not is_compressed_pk(tower_id): raise InvalidParameter( "tower id must be a compressed public key (33-byte hex value)") return tower_id, tower_netaddr
def run(rpc_client, opts_args): opts, args = opts_args if not args: raise InvalidParameter("No user_id was given") if len(args) > 1: raise InvalidParameter( f"Expected only one argument, not {len(args)}") return rpc_client.get_user(args[0])
def add_update_user(self, user_id): """ Adds a new user or updates the subscription of an existing one, by adding additional slots. Args: user_id(:obj:`str`): the public key that identifies the user (33-bytes hex str). Returns: :obj:`tuple`: A tuple with the number of available slots in the user subscription, the subscription expiry (in absolute block height), and the registration_receipt. Raises: :obj:`InvalidParameter`: if the user_pk does not match the expected format. """ if not is_compressed_pk(user_id): raise InvalidParameter( "Provided public key does not match expected format (33-byte hex string)" ) with self.rw_lock.gen_wlock(): if user_id not in self.registered_users: self.registered_users[user_id] = UserInfo( self.subscription_slots, self.block_processor.get_block_count() + self.subscription_duration) else: # FIXME: For now new calls to register add subscription_slots to the current count and reset the expiry # time if not is_u4int( self.registered_users[user_id].available_slots + self.subscription_slots): raise InvalidParameter( "Maximum slots reached for the subscription") self.registered_users[ user_id].available_slots += self.subscription_slots self.registered_users[user_id].subscription_expiry = ( self.block_processor.get_block_count() + self.subscription_duration) self.user_db.store_user(user_id, self.registered_users[user_id].to_dict()) receipt = create_registration_receipt( user_id, self.registered_users[user_id].available_slots, self.registered_users[user_id].subscription_expiry, ) return ( self.registered_users[user_id].available_slots, self.registered_users[user_id].subscription_expiry, receipt, )
def parse_add_appointment_args(args): """ Parses the arguments of the add_appointment command. Args: args (:obj:`list`): a list of command line arguments that must contain a json encoded appointment, or the file option and the path to a file containing a json encoded appointment. Returns: :obj:`dict`: A dictionary containing the appointment data. Raises: :obj:`InvalidParameter`: if the appointment data is not JSON encoded. :obj:`FileNotFoundError`: if ``-f`` is passed and the appointment file is not found. :obj:`IOError`: if ``-f`` was passed and the file cannot be read. """ use_help = "Use 'help add_appointment' for help of how to use the command" if not args: raise InvalidParameter("No appointment data provided. " + use_help) arg_opt = args.pop(0) try: if arg_opt in ["-h", "--help"]: sys.exit(help_add_appointment()) if arg_opt in ["-f", "--file"]: fin = args.pop(0) if not os.path.isfile(fin): raise FileNotFoundError("Cannot find {}".format(fin)) try: with open(fin) as f: appointment_data = json.load(f) except IOError as e: raise IOError("Cannot read appointment file. {}".format( str(e))) else: appointment_data = json.loads(arg_opt) if not appointment_data: raise InvalidParameter("The provided appointment JSON is empty") except json.JSONDecodeError: raise InvalidParameter( "Non-JSON encoded data provided as appointment. " + use_help) return appointment_data
def recover_pk(message, zb32_sig): """ Recovers an ECDSA public key from a given message and zbase32 signature. Args: message(:obj:`bytes`): original message from where the signature was generated. zb32_sig(:obj:`str`): the zbase32 signature of the message. Returns: :obj:`PublicKey`: The public key if it can be recovered. Raises: :obj:`InvalidParameter`: if the message and/or signature have a wrong value. :obj:`SignatureError`: if a public key cannot be recovered from the given signature. """ if not isinstance(message, bytes): raise InvalidParameter( "Wrong value passed as message. Received {}, expected (bytes)". format(type(message))) if not isinstance(zb32_sig, str): raise InvalidParameter( "Wrong value passed as zbase32_sig. Received {}, expected (str)" .format(type(zb32_sig))) sigrec = pyzbase32.decode_bytes(zb32_sig) try: rsig_recid = sigrec_decode(sigrec) pk = PublicKey.from_signature_and_message(rsig_recid, LN_MESSAGE_PREFIX + message, hasher=sha256d) return pk except ValueError as e: # Several errors fit here: Signature length != 65, wrong recover id and failed to parse signature. # All of them return raise ValueError. raise SignatureError( "Cannot recover a public key from the given signature. " + str(e)) except Exception as e: if "failed to recover ECDSA public key" in str(e): raise SignatureError( "Cannot recover a public key from the given signature") else: raise SignatureError("Unknown exception. " + str(e))
def get_compressed_pk(pk): """ Computes a compressed, hex-encoded, public key given a ``PublicKey``. Args: pk(:obj:`PublicKey`): a given public key. Returns: :obj:`str`: A compressed, hex-encoded, public key (33-byte long) if it can be compressed. Raises: :obj:`InvalidParameter`: if the value passed as public key is not a PublicKey object. :obj:`InvalidKey`: if the public key has not been properly created. """ if not isinstance(pk, PublicKey): raise InvalidParameter( "Wrong value passed as pk. Received {}, expected (PublicKey)". format(type(pk))) try: compressed_pk = pk.format(compressed=True) return hexlify(compressed_pk).decode("utf-8") except TypeError as e: raise InvalidKey("PublicKey has invalid initializer", error=str(e))
def get_appointment(plugin, tower_id, locator): """ Gets information about an appointment from the tower. Args: plugin (:obj:`Plugin`): this plugin. tower_id (:obj:`str`): the identifier of the tower to query. locator (:obj:`str`): the appointment locator. Returns: :obj:`dict`: a dictionary containing the appointment data. """ # FIXME: All responses from the tower should be signed. try: tower_id, locator = arg_parser.parse_get_appointment_arguments(tower_id, locator) if tower_id not in plugin.wt_client.towers: raise InvalidParameter("tower_id is not within the registered towers", tower_id=tower_id) message = f"get appointment {locator}" signature = Cryptographer.sign(message.encode("utf-8"), plugin.wt_client.sk) data = {"locator": locator, "signature": signature} # Send request to the server. tower_netaddr = plugin.wt_client.towers[tower_id].netaddr get_appointment_endpoint = f"{tower_netaddr}/get_appointment" plugin.log(f"Requesting appointment from {tower_id}") response = process_post_response(post_request(data, get_appointment_endpoint, tower_id)) return response except (InvalidParameter, TowerConnectionError, TowerResponseError) as e: plugin.log(str(e), level="warn") return e.to_json()
def register(user_id, teos_url): """ Registers the user to the tower. Args: user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user. teos_url (:obj:`str`): the teos base url. Returns: :obj:`dict`: a dictionary containing the tower response if the registration succeeded. Raises: :obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if `user_id` 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. """ if not is_compressed_pk(user_id): raise InvalidParameter("The cli public key is not valid") # Send request to the server. register_endpoint = "{}/register".format(teos_url) data = {"public_key": user_id} logger.info("Registering in the Eye of Satoshi") response = process_post_response(post_request(data, register_endpoint)) return response
def get_appointment(locator, user_sk, teos_id, teos_url): """ Gets information about an appointment from the tower. Args: locator (:obj:`str`): the appointment locator used to identify it. user_sk (:obj:`PrivateKey`): the user's private key. teos_id (:obj:`PublicKey`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`dict`: A dictionary containing the appointment data. Raises: :obj:`InvalidParameter`: if `appointment_data` or any of its fields is invalid. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`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_id atm. if not is_locator(locator): raise InvalidParameter("The provided locator is not valid", locator=locator) message = "get appointment {}".format(locator) signature = Cryptographer.sign(message.encode("utf-8"), user_sk) data = {"locator": locator, "signature": signature} # Send request to the server. get_appointment_endpoint = "{}/get_appointment".format(teos_url) logger.info("Requesting appointment from the Eye of Satoshi") response = process_post_response(post_request(data, get_appointment_endpoint)) return response
def check_data_key_format(data, secret): """ Checks that the data and secret that will be used to by ``encrypt`` / ``decrypt`` are properly formatted. Args: data(:obj:`str`): the data to be encrypted. secret(:obj:`str`): the secret used to derive the encryption key. Raises: :obj:`InvalidParameter`: if either the ``key`` and/or ``data`` are not properly formatted. """ if len(data) % 2: raise InvalidParameter("Incorrect (Odd-length) data", data=data) if not is_256b_hex_str(secret): raise InvalidParameter( "Secret must be a 32-byte hex value (64 hex chars)", secret=secret)
def get_request_data_json(request): """ Gets the content of a json POST request and makes sure it decodes to a dictionary. Args: request (:obj:`Request`): the request sent by the user. Returns: :obj:`dict`: the dictionary parsed from the json request. Raises: :obj:`InvalidParameter`: if the request is not json encoded or it does not decodes to a dictionary. """ if request.is_json: request_data = request.get_json() if isinstance(request_data, dict): return request_data else: raise InvalidParameter("Invalid request content") else: raise InvalidParameter("Request is not json encoded")
def sign(message, sk): """ Signs a given message with a given secret key using ECDSA over secp256k1. Args: message(:obj:`bytes`): the data to be signed. sk(:obj:`PrivateKey`): the ECDSA secret key to be used to sign the data. Returns: :obj:`str`: The zbase32 signature of the given message is it can be signed. Raises: :obj:`InvalidParameter`: if the message and/or secret key have a wrong value. :obj:`SignatureError`: if there is an error during the signing process. """ if not isinstance(message, bytes): raise InvalidParameter( "Wrong value passed as message. Received {}, expected (bytes)". format(type(message))) if not isinstance(sk, PrivateKey): raise InvalidParameter( "Wrong value passed as sk. Received {}, expected (PrivateKey)". format(type(message))) try: rsig_rid = sk.sign_recoverable(LN_MESSAGE_PREFIX + message, hasher=sha256d) sigrec = sigrec_encode(rsig_rid) zb32_sig = pyzbase32.encode_bytes(sigrec).decode() except ValueError as e: raise SignatureError("Couldn't sign the message. " + str(e)) return zb32_sig
def parse_get_appointment_arguments(tower_id, locator): """ Parses the arguments of the get_appointment command and checks that they are correct. Args: tower_id (:obj:`str`): the identifier of the tower to connect to (a compressed public key). locator (:obj:`str`): the locator of the appointment to query the tower about. Returns: :obj:`tuple`: the tower id and appointment locator. Raises: :obj:`common.exceptions.InvalidParameter`: if any of the parameters is wrong or missing. """ if not is_compressed_pk(tower_id): raise InvalidParameter( "tower id must be a compressed public key (33-byte hex value)") if not is_locator(locator): raise InvalidParameter("The provided locator is not valid", locator=locator) return tower_id, locator
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 register(user_id, teos_id, teos_url): """ Registers the user to the tower. Args: user_id (:obj:`str`): a 33-byte hex-encoded compressed public key representing the user. teos_id (:obj:`str`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`tuple`: A tuple containing the available slots count and the subscription expiry. Raises: :obj:`InvalidParameter`: if `user_id` is invalid. :obj:`ConnectionError`: if the client cannot connect to the tower. :obj:`TowerResponseError`: if the tower responded with an error, or the response was invalid. """ if not is_compressed_pk(user_id): raise InvalidParameter("The cli public key is not valid") # Send request to the server. register_endpoint = "{}/register".format(teos_url) data = {"public_key": user_id} logger.info("Registering in the Eye of Satoshi") response = process_post_response(post_request(data, register_endpoint)) available_slots = response.get("available_slots") subscription_expiry = response.get("subscription_expiry") tower_signature = response.get("subscription_signature") # Check that the server signed the response as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the subscription") # Check that the signature is correct. subscription_receipt = receipts.create_registration_receipt( user_id, available_slots, subscription_expiry) rpk = Cryptographer.recover_pk(subscription_receipt, tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") return available_slots, subscription_expiry
def raise_invalid_parameter(*args, **kwargs): # Message is passed in the API response raise InvalidParameter("Invalid parameter message")
def add_appointment(appointment_data, user_sk, teos_id, teos_url): """ Manages the add_appointment command. The life cycle of the function is as follows: - Check that the given commitment_txid is correct (proper format and not missing) - Check that the transaction is correct (not missing) - Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx - Sign the appointment - Send the appointment to the tower - Wait for the response - Check the tower's response and signature Args: appointment_data (:obj:`dict`): a dictionary containing the appointment data. user_sk (:obj:`PrivateKey`): the user's private key. teos_id (:obj:`str`): the tower's compressed public key. teos_url (:obj:`str`): the teos base url. Returns: :obj:`tuple`: A tuple (`:obj:Appointment <common.appointment.Appointment>`, :obj:`str`) containing the appointment and the tower's signature. Raises: :obj:`InvalidParameter <cli.exceptions.InvalidParameter>`: if `appointment_data` or any of its fields is invalid. :obj:`ValueError`: if the appointment cannot be signed. :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. """ if not appointment_data: raise InvalidParameter("The provided appointment JSON is empty") tx_id = appointment_data.get("tx_id") tx = appointment_data.get("tx") if not is_256b_hex_str(tx_id): raise InvalidParameter("The provided locator is wrong or missing") if not tx: raise InvalidParameter("The provided data is missing the transaction") appointment_data["locator"] = compute_locator(tx_id) appointment_data["encrypted_blob"] = Cryptographer.encrypt(tx, tx_id) appointment = Appointment.from_dict(appointment_data) signature = Cryptographer.sign(appointment.serialize(), user_sk) data = {"appointment": appointment.to_dict(), "signature": signature} # Send appointment to the server. logger.info("Sending appointment to the Eye of Satoshi") add_appointment_endpoint = "{}/add_appointment".format(teos_url) response = process_post_response( post_request(data, add_appointment_endpoint)) tower_signature = response.get("signature") # Check that the server signed the appointment as it should. if not tower_signature: raise TowerResponseError( "The response does not contain the signature of the appointment") rpk = Cryptographer.recover_pk(appointment.serialize(), tower_signature) if teos_id != Cryptographer.get_compressed_pk(rpk): raise TowerResponseError( "The returned appointment's signature is invalid") logger.info("Appointment accepted and signed by the Eye of Satoshi") logger.info("Remaining slots: {}".format(response.get("available_slots"))) logger.info("Start block: {}".format(response.get("start_block"))) return appointment, tower_signature
def main(command, args, config): setup_data_folder(config.get("DATA_DIR")) # Set the teos url teos_url = "{}:{}".format(config.get("API_CONNECT"), config.get("API_PORT")) # If an http or https prefix if found, leaves the server as is. Otherwise defaults to http. if not teos_url.startswith("http"): teos_url = "http://" + teos_url try: if os.path.exists(config.get("USER_PRIVATE_KEY")): logger.debug("Client id found. Loading keys") user_sk, user_id = load_keys(config.get("USER_PRIVATE_KEY")) else: logger.info("Client id not found. Generating new keys") user_sk = Cryptographer.generate_key() Cryptographer.save_key_file(user_sk.to_der(), "user_sk", config.get("DATA_DIR")) user_id = Cryptographer.get_compressed_pk(user_sk.public_key) if command == "register": if not args: raise InvalidParameter( "Cannot register. No tower id was given") else: teos_id = args.pop(0) if not is_compressed_pk(teos_id): raise InvalidParameter( "Cannot register. Tower id has invalid format") available_slots, subscription_expiry = register( user_id, teos_id, teos_url) logger.info( "Registration succeeded. Available slots: {}".format( available_slots)) logger.info("Subscription expires at block {}".format( subscription_expiry)) teos_id_file = os.path.join(config.get("DATA_DIR"), "teos_pk") Cryptographer.save_key_file(bytes.fromhex(teos_id), teos_id_file, config.get("DATA_DIR")) if command == "add_appointment": teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) appointment_data = parse_add_appointment_args(args) appointment = create_appointment(appointment_data) start_block, signature = add_appointment(appointment, user_sk, teos_id, teos_url) save_appointment_receipt(appointment.to_dict(), start_block, signature, config.get("APPOINTMENTS_FOLDER_NAME")) elif command == "get_appointment": if not args: logger.error("No arguments were given") else: arg_opt = args.pop(0) if arg_opt in ["-h", "--help"]: sys.exit(help_get_appointment()) teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) appointment_data = get_appointment(arg_opt, user_sk, teos_id, teos_url) if appointment_data: logger.info(json.dumps(appointment_data, indent=4)) elif command == "get_subscription_info": if args: arg_opt = args.pop(0) if arg_opt in ["-h", "--help"]: sys.exit(help_get_subscription_info()) teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) subscription_info = get_subscription_info(user_sk, teos_id, teos_url) if subscription_info: logger.info(json.dumps(subscription_info, indent=4)) elif command == "help": if args: command = args.pop(0) if command == "register": sys.exit(help_register()) if command == "add_appointment": sys.exit(help_add_appointment()) if command == "get_subscription_info": sys.exit(help_get_subscription_info()) elif command == "get_appointment": sys.exit(help_get_appointment()) else: logger.error( "Unknown command. Use help to check the list of available commands" ) else: sys.exit(show_usage()) except ( FileNotFoundError, IOError, ConnectionError, ValueError, BasicException, ) as e: logger.error(str(e)) except Exception as e: logger.error("Unknown error occurred", error=str(e))