def __init__(self): """ Initializes a user database for holding QChat contact information """ self.lock = threading.Lock() self.logger = QChatLogger(__name__) self.db = defaultdict(dict)
def __init__(self, name, cqc_connection, config): """ Initialize a connection to the CQC server and sets up a socket for classical communications. :param name: str Name of the host (Must be one available by SimulaQron CQC). :param cqc_connection: `cqc.pythonLib.CQCConnection` The Classical Quantum Combiner Connection over which quantum communication occurs. :param config: dict JSON configuration for the connection. """ # Lock on the connection self.lock = threading.Lock() # Logger self.logger = QChatLogger(__name__) # Host name the connection belongs to self.name = name # Listening configuration self.host = config['host'] self.port = config['port'] # Inbound message queue self.message_queue = [] # Daemon threads self.cqc = cqc_connection self.listening_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.listening_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.classical_thread = DaemonThread(target=self.listen_for_classical)
def __init__(self, name, cqc_connection, configFile=None, allow_invalid_signatures=False): """ Initializes a QChat Server that serves as the primary communication interface with other applications :param name: str Name of the host we want to be on the network :param cqc_connection: `~cqc.pythonLib.CQCConnection` Classical Quantum Combiner Connection used for quantum communications :param configFile: str Path to the configuration file that contains settings for the specified name :param allow_invalid_signatures: bool Process messages with faulty signatures """ self.name = name self.logger = QChatLogger(__name__) self.configFile = configFile # This is the server's personal config self.config = self._load_server_config(self.name) # This is information for the root registry server self.root_config = self._load_server_config(self.config.get("root")) # RSA Signer for handling unauthenticated classical channels self.signer = QChatSigner() self._allow_invalid_signatures = allow_invalid_signatures # Connection to other applications self.connection = QChatConnection(name=name, cqc_connection=cqc_connection, config=self.config) # Storage of user/network information self.userDB = UserDB() # Load ourselves into our DB self.userDB.addUser(user=self.name, pub=self.signer.get_pub(), **self.connection.get_connection_info()) # Start our inbound/outbound message handlers self.message_processor = DaemonThread(target=self.read_from_connection) # Register with the root registry self._register_with_root_server() # Storage of distributed qubit information self.qubit_history = defaultdict(list) # Inbound control messages for protocols self.control_message_queue = defaultdict(list)
def __init__(self, user, destination, server_url): """ Initializes the RPC client :param user: str The user we wish to send a message as :param destination: str The peer we wish to send a message to :param server_url: str The RPCServer url to connect to, eg. http://127.0.0.1:6666 """ self.client = xmlrpc.client.ServerProxy(server_url) self.user = user self.destination = destination self._running = False self._message_reader = threading.Thread(target=self._read_messages) self.lock = threading.Lock() self.logger = QChatLogger("QChatCLIRPCClient-{}".format(user))
def __init__(self, user, host, port, client): """ Initializes the RPC server :param user: str The name of the QChatServer :param host: str The host to receive RPC commands at :param port: int The port to receive RPC commands at :param client: `~qchat.client.QChatClient` The QChatClient to interact with """ self.user = user self.host = host self.port = port self.clients[user] = client self._ensure_client_for(user) self.logger = QChatLogger("QChatClientRPCServer-{}".format(user)) self.logger.debug("Starting server for {} at {}:{}".format( user, host, port))
def __init__(self, connection, relay_info): """ Used to implement trusted/untrusted measurement devices for use in receiving BB84 states from source and performing measurements on the qubits :param connection: `~qchat.connection.QChatConnection` Connection to be used for classical and quantum communications :param relay_info: dict Relay information about how messages travel over other nodes """ self.connection = connection self.logger = QChatLogger(__name__) # Connection information to the server providing the EPR pairs self.relay_host = relay_info["host"] self.relay_port = relay_info["port"]
class QChatMailbox: """ Implements a thread safe message storing mailbox """ def __init__(self): self.lock = threading.Lock() self.messages = [] self.logger = QChatLogger(__name__) def storeMessage(self, message): """ Stores a message into the mailbox :param message: obj The message to store :return: None """ self.logger.debug("New message in mailbox from {}".format( message.sender)) with self.lock: self.messages.append(message) def getMessages(self): """ Returns a list of the messages that are currently stored in the mailbox :return: list A list of the messages that are currently stored """ self.logger.debug("Retrieving messages") with self.lock: return list(self.messages) def popMessages(self): """ Pops all messages that are stored and returns them :return: list List of the stored messages that were removed """ self.logger.debug("Popping messages") with self.lock: messages = list(self.messages) self.messages = [] return messages
class QChatConnection: def __init__(self, name, cqc_connection, config): """ Initialize a connection to the CQC server and sets up a socket for classical communications. :param name: str Name of the host (Must be one available by SimulaQron CQC). :param cqc_connection: `cqc.pythonLib.CQCConnection` The Classical Quantum Combiner Connection over which quantum communication occurs. :param config: dict JSON configuration for the connection. """ # Lock on the connection self.lock = threading.Lock() # Logger self.logger = QChatLogger(__name__) # Host name the connection belongs to self.name = name # Listening configuration self.host = config['host'] self.port = config['port'] # Inbound message queue self.message_queue = [] # Daemon threads self.cqc = cqc_connection self.listening_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.listening_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.classical_thread = DaemonThread(target=self.listen_for_classical) def __del__(self): """ Makes sure to close the socket used for classical communications. :return: None """ if self.listening_socket: self.listening_socket.close() def get_connection_info(self): """ Returns a dictionary containing host/port information for the socket used in classical communication. :return: dict Dictionary containing info. """ info = { "connection": { "host": self.host, "port": self.port } } return info def listen_for_classical(self): """ A daemon for handling incoming connections. :return: None """ self.listening_socket.bind((self.host, self.port)) while True: self.logger.debug("Listening for incoming connection") self.listening_socket.listen(1) conn, addr = self.listening_socket.accept() self.logger.debug("Got connection from {}".format(addr)) self.start_handler(conn, addr) def start_handler(self, conn, addr): """ Simple connection handler that passes work to thread :param conn: Connection information from socket :param addr: Address information from socket :return: None """ t = threading.Thread(target=self._handle_connection, args=(conn, addr)) t.start() def _handle_connection(self, conn, addr): """ Receives incoming QChat Messages and verifies their structure before storing them so that they can be retrieved. :param conn: Connection information from sockets :param addr: Address information from sockets :return: None """ # Verify the header structure header = conn.recv(HEADER_LENGTH) if header not in MessageFactory().message_mapping.keys(): raise ConnectionError("Incorrect message header") # Verify the sender structe padded_sender = conn.recv(MAX_SENDER_LENGTH) if len(padded_sender) != MAX_SENDER_LENGTH: raise ConnectionError("Incorrect sender length") # Verify the sender info sender = str(padded_sender.replace(b'\x00', b''), 'utf-8') if len(sender) == 0: raise ConnectionError("Invalid sender") # Get the message size size = conn.recv(PAYLOAD_SIZE) if len(size) != PAYLOAD_SIZE: raise ConnectionError("Incorrect payload size") # Retrieve the message data data_length = int.from_bytes(size, 'big') message_data = b'' while len(message_data) < data_length: data = conn.recv(1024) if not data: raise ConnectionError("Message data too short") message_data += data # Verify the length of the sent data if len(message_data) > data_length or conn.recv(1): raise ConnectionError("Message data too long") # Pass the message up self.logger.debug("Inserting message into queue") m = MessageFactory().create_message(header, sender, message_data) self._append_message_to_queue(m) conn.close() def _append_message_to_queue(self, message): """ Appends message to the inbound message queue. :param message: bytes The message to be added to the queue. :return: None """ with self.lock: self.message_queue.append(message) def _pop_message_from_queue(self): """ Removes the oldest message from the head of the queue. :return: bytes The oldest message stored in the queue. """ with self.lock: return self.message_queue.pop(0) def recv_message(self): """ Method that receives a message if one exists in the queue. :return: bytes The message if one exists. Otherwise None. """ return self._pop_message_from_queue() if self.message_queue else None def send_message(self, host, port, message): """ Connects and sends a message to the specified host:port :param host: str Hostname to send to :param port: int Port to send to :param message: bytes Bytes object message :return: None """ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s.sendall(message) s.close() self.logger.debug("Sent message to {}:{}".format(host, port))
class QChatCore: def __init__(self, name, cqc_connection, configFile=None, allow_invalid_signatures=False): """ Initializes a QChat Server that serves as the primary communication interface with other applications :param name: str Name of the host we want to be on the network :param cqc_connection: `~cqc.pythonLib.CQCConnection` Classical Quantum Combiner Connection used for quantum communications :param configFile: str Path to the configuration file that contains settings for the specified name :param allow_invalid_signatures: bool Process messages with faulty signatures """ self.name = name self.logger = QChatLogger(__name__) self.configFile = configFile # This is the server's personal config self.config = self._load_server_config(self.name) # This is information for the root registry server self.root_config = self._load_server_config(self.config.get("root")) # RSA Signer for handling unauthenticated classical channels self.signer = QChatSigner() self._allow_invalid_signatures = allow_invalid_signatures # Connection to other applications self.connection = QChatConnection(name=name, cqc_connection=cqc_connection, config=self.config) # Storage of user/network information self.userDB = UserDB() # Load ourselves into our DB self.userDB.addUser(user=self.name, pub=self.signer.get_pub(), **self.connection.get_connection_info()) # Start our inbound/outbound message handlers self.message_processor = DaemonThread(target=self.read_from_connection) # Register with the root registry self._register_with_root_server() # Storage of distributed qubit information self.qubit_history = defaultdict(list) # Inbound control messages for protocols self.control_message_queue = defaultdict(list) def _load_server_config(self, name): """ Obtains the hosts server configuration from the config file :param name: str Key in configuration where settings should be loaded from :return: dict The configuration loaded from the file """ if self.configFile: config_path = self.configFile else: path = os.path.abspath(__file__) config_path = os.path.dirname(path) + "/config.json" self.logger.debug("Loading server config for {}".format(name)) with open(config_path) as f: base_config = json.load(f) config = base_config.get(name) self.logger.debug("Config: {}".format(config)) return config def _register_with_root_server(self): """ Registers our application server with the root registry server :return: None """ try: root_host = self.root_config["host"] root_port = self.root_config["port"] # No need to register with ourselves if we are the root registry if self.config["host"] == root_host and self.config[ "port"] == root_port: self.logger.debug("Am root server") else: self.sendRegistration(host=root_host, port=root_port) except Exception: self.logger.info( "Failed to register with root server, is it running?") def read_from_connection(self): """ Processes inbound messages from the application connection :return: None """ while not time.sleep(GLOBAL_SLEEP_TIME): message = self.connection.recv_message() if message: self.start_process_thread(message) def start_process_thread(self, message): """ Forks off a thread for handling messages so that they can be processed in parallel :param message: `~qchat.messages.Message` The message we obtained from the application connection :return: None """ t = threading.Thread(target=self.process_message, args=(message, )) t.start() def process_message(self, message): """ The primary message handling entrypoint, performs signature verification/stripping before passing the message to a specific handler :param message: `~qchat.messages.Message` The inbound message from the application connection :return: None """ self.logger.debug("Processing {} message from {}: {}".format( message.header, message.sender, message.data)) # Verify the signature on the message for key message types if message.verify: if not self.userDB.hasUser(message.sender): self.requestUserInfo(message.sender) message, signature = self._strip_signature(message) if not self._allow_invalid_signatures: self._verify_message(message, signature) else: self.logger.warning("Will not verify message signature") # Strip unnecessary signature information should it not be necessary for the message type elif message.strip: message, _ = self._strip_signature(message) handler = self.proc_map.get(message.header, self._store_control_message) handler(message) self.logger.debug("Completed processing message") def _sign_message(self, message): """ Internal method for signing outbound messages to assure authentication :param message: `~qchat.messages.Message` The message to be signed :return: `~qchat.messages.Message` The message with a signature attached """ sig = self.signer.sign(message.encode_message()) message.data["sig"] = sig.decode("ISO-8859-1") return message def _strip_signature(self, message): """ Internal method for stripping signature data from a message that is unecessary to message handlers :param message: `~qchat.messages.Message` The message we want to strip :return: tuple A tuple of the message without the signature data, signature """ signature = message.data.pop("sig").encode("ISO-8859-1") return message, signature def _verify_message(self, message, signature): """ Internal method for verifying the signature provided with a message :param message: `~qchat.messages.Message` The message we want to verify :param signature: bytes The signature we want to verify :return: None """ data = message.encode_message() # Use the stored public key for verification pub = self.userDB.getPublicKey(message.sender) if not QChatVerifier(pub).verify(data, signature): raise Exception("Obtained message with incorrect signature") self.logger.debug("Successfully verified signature") def _pass_message_data(self, message, handler): """ Internal method for passing the message data as arguments to the message handlers :param message: `~qchat.messages.Message` The message to unpack arguments from :param handler: func The handler that will process the message :return: None """ handler(**message.data) def _store_control_message(self, message): """ Internal method for handling messages that do not have specific handlers :param message: `~qchat.messages.Message` The message to store :return: None """ self.control_message_queue[message.sender].append(message) self.logger.debug("Stored message into control queue") def _get_registration_data(self): """ Internal method for constructing this server's registration data :return: dict The constructed registration data """ reg_data = { "user": self.name, "pub": self.getPublicKey().decode("ISO-8859-1") } reg_data.update(self.connection.get_connection_info()) self.logger.debug( "Constructing registration data: {}".format(reg_data)) return reg_data def hasUser(self, user): """ Interface to the user database for checking if a user exists :param user: str The name of the user to check in the database :return: bool Whether user exists or not """ return self.userDB.hasUser(user) def addUserInfo(self, user, **kwargs): """ Adds arbitrary information to the user database for a user :param user: str The user we want to add to the database :param kwargs: dict The key=value pairs we want to store in the database :return: None """ if user == "*": self.logger.debug("Get bulk user info!") for info in kwargs["info"]: user_name = info.pop("user") if not self.userDB.hasUser(user_name): self.logger.debug("Adding to user {} info {}".format( user_name, info)) self.userDB.addUser(user_name, **info) else: self.logger.debug("Adding to user {} info {}".format(user, kwargs)) self.userDB.addUser(user, **kwargs) def getPublicInfo(self, user): """ Returns the relevant public information for the application that is necessary for establishing RSA authenticated classical communication :param user: str The user we want the public information for :return: dict A dictionary containing the user's name, host/port info, and public key """ pub_info = dict(self.userDB.getPublicUserInfo(user)) return pub_info def getPublicKey(self): """ Returns the server's public key :return: bytes Public key of the server """ return self.userDB.getPublicKey(user=self.name) def getConnectionInfo(self, user): """ Returns the connection information for the specified user :param user: str User we want connection information for :return: dict A dictionary containing the host/port information of the user """ return self.userDB.getConnectionInfo(user) def sendRegistration(self, host, port): """ Sends this server's registration to the specified host/port :param host: str Host of the registry :param port: int Port of the registry :return: None """ message = RGSTMessage(sender=self.name, message_data=self._get_registration_data()) self.connection.send_message(host, port, message.encode_message()) self.logger.debug("Sent registration to {}:{}".format(host, port)) def requestUserInfo(self, user): """ Requests the specified user's information from the root registry in the network :param user: str User we want to obtain information for :return: None """ # Construct the request message request_message_data = { "user": user, } request_message_data.update(self.connection.get_connection_info()) # Create the messag eobject and sign it m = GETUMessage(sender=self.name, message_data=request_message_data) m = self._sign_message(m) # Send the request to the root registry self.connection.send_message(self.root_config["host"], self.root_config["port"], m.encode_message()) # Wait for a response from the root registry if user != "*": wait_start = time.time() while not self.userDB.hasUser(user): if time.time() - wait_start > 10: raise Exception( "Failed to get {} info from registry".format(user)) def sendUserInfo(self, user, connection): """ Sends the specified user's information to the server specified by connection :param user: str The user we want to provide information for :param connection: dict The host/port information of the receiving server :return: None """ self.logger.debug("Sending {} info to {}".format(user, connection)) # Construct and sign the message containing the requested information message = PUTUMessage(sender=self.name, message_data=self.getPublicInfo(user)) message = self._sign_message(message) self.connection.send_message(host=connection["host"], port=connection["port"], message=message.encode_message()) def sendMessage(self, user, message): """ Interface for sending a preconstructed message object to a user :param user: str The user to send the message to :param message: `~qchat.messages.Message` The Message object we want to send :return: None """ # Ensure we know how to contact the user, if not resolve the information if not self.userDB.hasUser(user): self.requestUserInfo(user) # Get the connection information connection_info = self.userDB.getConnectionInfo(user) host = connection_info['host'] port = connection_info['port'] # Sign the message and send it via the connection message = self._sign_message(message) self.connection.send_message(host, port, message.encode_message())
class UserDB: def __init__(self): """ Initializes a user database for holding QChat contact information """ self.lock = threading.Lock() self.logger = QChatLogger(__name__) self.db = defaultdict(dict) def _get_user(self, user): """ Retrieves a user's data in the database :param user: str Name of the user :return: dict Stored data """ return self.db.get(user) def hasUser(self, user): """ Checks if the database has the specified user :param user: str The name of the user :return: bool Whether user exists in database or not """ return self._get_user(user) is not None def getPublicKey(self, user): """ Returns the stored public key of the specified user :param user: str The name of the user :return: bytes The public key data """ info = self._get_user(user) if not info: raise DBException("User {} does not exist in the database!") return info.get('pub') def getMessageKey(self, user): """ Retrieves the key used for encrypting/decrypting messages :param user: str The name of the user :return: bytes The key associated with the specified user """ info = self._get_user(user) if not info: raise DBException("User {} does not exist in the database!") return info.get('message_key') def getConnectionInfo(self, user): """ Retrieves connection information for the specified user :param user: str The name of the user :return: dict Contains connection details of the user """ info = self._get_user(user) if not info: raise DBException("User {} does not exist in the database!") return info.get('connection') def deleteUserInfo(self, user, fields): """ Deletes all specified fields of data for the user :param user: str Name of the user :param fields: list List of strings of the names of the fields to delete :return: None """ self.logger.debug("Deleting user {} info {}".format(user, fields)) info = self._get_user(user) if not info: raise DBException("User {} does not exist in the database!") for field in fields: info.pop(field) def deleteUser(self, user): """ Deletes a user from the database :param user: str The name of the user :return: None """ self.logger.debug("Deleting user {}".format(user)) self.db.pop(user) def changeUserInfo(self, user, **kwargs): """ Updates a user entry in the database :param user: str The name of the user :param kwargs: dict A dictionary of updates to merge for the user :return: None """ self.logger.debug("Changing user {} with data {}".format(user, kwargs)) if self.hasUser(user): self.db[user].update(kwargs) def addUser(self, user, **kwargs): """ Adds a user into the database along with any initial data :param user: str The name of the user :param kwargs: dict The initial data to store for the user :return: None """ self.logger.debug("Adding user {} with data {}".format(user, kwargs)) self.db[user].update(kwargs) def getPublicUserInfo(self, user): """ Returns the public information of a user including the connection details and public key :param user: str The name of the user :return: dict Contains public information of the user """ if user == "*": public_info = [] for user in self.db: info = { "connection": self.getConnectionInfo(user), "pub": self.getPublicKey(user) } info["pub"] = info["pub"].decode("ISO-8859-1") info["user"] = user public_info.append(info) public_info = {"user": "******", "info": public_info} else: public_info = { "connection": self.getConnectionInfo(user), "pub": self.getPublicKey(user) } public_info["pub"] = public_info["pub"].decode("ISO-8859-1") public_info["user"] = user return public_info
def __init__(self): self.lock = threading.Lock() self.messages = [] self.logger = QChatLogger(__name__)
class QChatCLIRPCClient: """ Simple RPC client that sends messages to an RPCServer """ def __init__(self, user, destination, server_url): """ Initializes the RPC client :param user: str The user we wish to send a message as :param destination: str The peer we wish to send a message to :param server_url: str The RPCServer url to connect to, eg. http://127.0.0.1:6666 """ self.client = xmlrpc.client.ServerProxy(server_url) self.user = user self.destination = destination self._running = False self._message_reader = threading.Thread(target=self._read_messages) self.lock = threading.Lock() self.logger = QChatLogger("QChatCLIRPCClient-{}".format(user)) def start(self): """ Starts the RPC client and sends messages based on user input :return: None """ print("Hello, this is {}".format(self.user)) self._running = True self._message_reader.start() while self._running: input_text = input("\n[ {} ]: ".format(self.user)) the_message = "{} @ {}".format(input_text, time.time()) with self.lock: self.client.send_message(self.user, self.destination, the_message) def stop(self): """ Stops the RPC Client :return: None """ self.logger.info("Stopping the CLI RPC Client") self._running = False self._message_reader.join() def _read_messages(self): """ Polls the RPCServer for messages that belong to the user running the RPC client :return: None """ while self._running: try: with self.lock: user_messages = self.client.get_messages(self.user) if user_messages: print("\n") for sender, messages in user_messages.items(): for message in messages: print("[ {} ]: {}\n".format(sender, message)) print("[ {} ]: ".format(self.user), end="") except Exception: self.logger.exception("Failed getting messages for {}".format( self.user)) time.sleep(2) time.sleep(1)
class QChatRPCServer: """ An RPC Server that connects to a QChatServer """ clients = {} def __init__(self, user, host, port, client): """ Initializes the RPC server :param user: str The name of the QChatServer :param host: str The host to receive RPC commands at :param port: int The port to receive RPC commands at :param client: `~qchat.client.QChatClient` The QChatClient to interact with """ self.user = user self.host = host self.port = port self.clients[user] = client self._ensure_client_for(user) self.logger = QChatLogger("QChatClientRPCServer-{}".format(user)) self.logger.debug("Starting server for {} at {}:{}".format( user, host, port)) def send_message(self, user, destination, message): """ Listens for a send message RPC call and forwards the command to the underlying QChatClient :param user: str The name of the client to use :param destination: str The name of the peer to send the message to :param message: str The plaintext message to send :return: bool Whether message was sent or not """ self._ensure_client_for(user) try: self.clients[user].sendQChatMessage(destination, message) return True except Exception: return False def get_messages(self, user): """ RPC call for retrieving messages from the client :param user: str The client to retrieve messages for :return: dict A dictionary of message lists keyed by the sender """ self._ensure_client_for(user) messages = {} self.logger.info("Fetching messages from {}".format(user)) try: messages = dict(self.clients[user].getMessageHistory()) if messages: self.logger.info("Received messages {} for user {}".format( messages, user)) except Exception: self.logger.exception( "Failed getting messages from {}".format(user)) return messages def _ensure_client_for(self, user): """ Checks that the specified user has a client that is running, if not sets one up :param user: :return: """ if user not in self.clients: self.clients[user] = QChatClient(user) # wait for registration time.sleep(2)