PK#HWWneteria/client.py#!/usr/bin/python # # Asteria # Copyright (C) 2014, William Edwards , # # This file is part of Asteria. # # Asteria is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Asteria is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Asteria. If not, see . # # Contributor(s): # # William Edwards , # """This module is used to create client objects that allows sending and receiving messages to a Neteria server. You can run an example by running the following from the command line: `python -m neteria.client`""" import logging import random import uuid from . import core from .core import serialize_data from .core import unserialize_data from datetime import datetime from neteria.encryption import Encryption from pprint import pformat try: from rsa import PublicKey except: PublicKey = None # Create a logger for optional handling of debug messages. logger = logging.getLogger(__name__) class NeteriaClient(object): """The primary Neteria client class handles all client functions. The Neteria client allows you to register and send messages to a Neteria server. Messages are usually dictionary objects that are serialized and sent over the network. Attributes: server_ip (str): When the client successfully registers with the server, the server's IP will be stored by the client. server_port (int): When the client successfully registers with the server, the server's port will be stored by the client. event_uuids (dict): A list of all events that are awaiting a response from the server. event_rollbacks (dict): A list of all events that were declared "ILLEGAL" by the server. event_notifies (dict): A list of event notifications received from the server. event_confirmations (dict): A list of events that were confirmed as "LEGAL" by the server. cuuid (str): The universally unique identifier of the client. Args: version (str): A version number of your client that will be sent to the server upon registration. This can be used to enable backward compatability of your server application. Defaults to "1.0" client_address (str): The address for the client to listen on. Defaults to all addresses. client_port (int): The port that the client will listen on and send messages from. Defaults to a random port between 50000 and 60000. server_port (int): The server port that the client will attempt to register with and communicate on. Defaults to port 40850. compression (boolean): Whether or not compression should be enabled. Compression is done on all messages with zlib compression. Defaults to "False". encryption (boolean): Whether or not encryption should be enabled. Encryption is done on all messages with RSA encryption. Defaults to "False". timeout (float): The amount of time to wait in seconds for a confirmation before retrying to send the message. Defaults to 2.0 seconds. max_retries (int): The maximum number of retry attempts the server should try before considering the message has failed. Defaults to 4. stats (boolean): Whether or not to keep track of network statistics such as total bytes sent/recieved. Defaults to False. Examples: >>> import neteria.client >>> >>> myclient = neteria.client.NeteriaClient() >>> myclient.listen() Listening on port 51280 ... >>> myclient.autodiscover() """ def __init__(self, version="1.0.2", client_address='', client_port=None, server_port=40080, compression=False, encryption=False, timeout=2.0, max_retries=4, stats=False): self.version = version self.client_port = client_port self.server = None self.server_key = None # Used to hold the public key of the server self.server_ip = None self.server_port = server_port self.autoregistering = False # Whether or not to auto-register. self.discovered_servers = {} # List of discovered servers. self.event_uuids = {} self.event_rollbacks = {} self.event_notifies = {} # Notify events buffer received from server self.event_confirmations = {} # Legal high priority events buffer # Enable packet compression self.compression = compression # Generate a keypair if encryption is enabled if encryption: self.encryption = Encryption() else: self.encryption = False # Handle client registration self.registered = False self.register_retries = 0 # This is the current register retry # If no client port was specified, choose a random high level port. if not client_port: self.client_port = random.randrange(50000, 60000) # Check to see if we have generated a uuid or not. If not, generate # one. self.cuuid = uuid.uuid1() # Create a listener object that we can use to send and receive # messages. self.listener = core.ListenerUDP(self, listen_address=client_address, listen_port=self.client_port, stats=stats) # Set a timeout and maximum number of retries for responses from the # server. self.timeout = timeout self.max_retries = max_retries def listen(self): """Starts the client listener to listen for server responses. Args: None Returns: None """ logger.info("Listening on port " + str(self.listener.listen_port)) self.listener.listen() def retransmit(self, data): """Processes messages that have been delivered from the transport protocol. Args: data (dict): A dictionary containing the packet data to resend. Returns: None Examples: >>> data {'method': 'REGISTER', 'address': ('192.168.0.20', 40080)} """ # Handle retransmitting REGISTER requests if we don't hear back from # the server. if data["method"] == "REGISTER": if not self.registered and self.register_retries < self.max_retries: logger.debug("<%s> Timeout exceeded. " % str(self.cuuid) + \ "Retransmitting REGISTER request.") self.register_retries += 1 self.register(data["address"], retry=False) else: logger.debug("<%s> No need to retransmit." % str(self.cuuid)) if data["method"] == "EVENT": if data["euuid"] in self.event_uuids: # Increment the current retry count of the euuid self.event_uuids[data["euuid"]]["retry"] += 1 if self.event_uuids[data["euuid"]]["retry"] > self.max_retries: logger.debug("<%s> Max retries exceeded. Timed out waiting " "for server for event: %s" % (data["cuuid"], data["euuid"])) logger.debug("<%s> Deleting event from currently " "processing event uuids" % (data["cuuid"], str(data["euuid"]))) del self.event_uuids[data["euuid"]] else: # Retransmit that shit self.listener.send_datagram( serialize_data(data, self.compression, self.encryption, self.server_key), self.server) # Then we set another schedule to check again logger.debug("<%s> Scheduling to retry in %s " "seconds" % (data["cuuid"], str(data["euuid"]), str(self.timeout))) self.listener.call_later( self.timeout, self.retransmit, data) else: logger.debug("<%s> No need to " "retransmit." % (str(self.cuuid), str(data["euuid"]))) def handle_message(self, msg, host): """Processes messages that have been delivered from the transport protocol Args: msg (string): The raw packet data delivered from the transport protocol. host (tuple): A tuple containing the (address, port) combination of the message's origin. Returns: A formatted response to the client with the results of the processed message. Examples: >>> msg {"method": "OHAI Client", "version": "1.0"} >>> host ('192.168.0.20', 36545) """ logger.debug("Executing handle_message method.") response = None # Unserialize the data packet # If encryption is enabled, and we've receive the server's public key # already, try to decrypt if self.encryption and self.server_key: msg_data = unserialize_data(msg, self.compression, self.encryption) else: msg_data = unserialize_data(msg, self.compression) # Log the packet logger.debug("Packet received: " + pformat(msg_data)) # If the message data is blank, return none if not msg_data: return response if "method" in msg_data: if msg_data["method"] == "OHAI Client": logger.debug("<%s> Autodiscover response from server received " "from: %s" % (self.cuuid, host[0])) self.discovered_servers[host]= [msg_data["version"], msg_data["server_name"]] # Try to register with the discovered server if self.autoregistering: self.register(host) self.autoregistering = False elif msg_data["method"] == "NOTIFY": self.event_notifies[msg_data["euuid"]] = msg_data["event_data"] logger.debug("<%s> Notify received" % self.cuuid) logger.debug("<%s> Notify event buffer: %s" % (self.cuuid, pformat(self.event_notifies))) # Send an OK NOTIFY to the server confirming we got the message response = serialize_data( {"cuuid": str(self.cuuid), "method": "OK NOTIFY", "euuid": msg_data["euuid"]}, self.compression, self.encryption, self.server_key) elif msg_data["method"] == "OK REGISTER": logger.debug("<%s> Ok register received" % self.cuuid) self.registered = True self.server = host # If the server sent us their public key, store it if "encryption" in msg_data and self.encryption: self.server_key = PublicKey( msg_data["encryption"][0], msg_data["encryption"][1]) elif (msg_data["method"] == "LEGAL" or msg_data["method"] == "ILLEGAL"): logger.debug("<%s> Legality message received" % str(self.cuuid)) self.legal_check(msg_data) # Send an OK EVENT response to the server confirming we # received the message response = serialize_data( {"cuuid": str(self.cuuid), "method": "OK EVENT", "euuid": msg_data["euuid"]}, self.compression, self.encryption, self.server_key) logger.debug("Packet processing completed") return response def autodiscover(self, autoregister=True): """This function will send out an autodiscover broadcast to find a Neteria server. Any servers that respond with an "OHAI CLIENT" packet are servers that we can connect to. Servers that respond are stored in the "discovered_servers" list. Args: autoregister (boolean): Whether or not to automatically register with any responding servers. Defaults to True. Returns: None Examples: >>> myclient = neteria.client.NeteriaClient() >>> myclient.listen() >>> myclient.autodiscover() >>> myclient.discovered_servers {('192.168.0.20', 40080): u'1.0', ('192.168.0.82', 40080): '2.0'} """ logger.debug("<%s> Sending autodiscover message to broadcast " "address" % str(self.cuuid)) if not self.listener.listening: logger.warning("Neteria client is not listening. The client " "will not be able to process responses from the server") message = serialize_data( {"method": "OHAI", "version": self.version, "cuuid": str(self.cuuid)}, self.compression, encryption=False) if autoregister: self.autoregistering = True self.listener.send_datagram( message, ("", self.server_port), message_type="broadcast") def register(self, address, retry=True): """This function will send a register packet to the discovered Neteria server. Args: address (tuple): A tuple of the (address, port) to send the register request to. retry (boolean): Whether or not we want to reset the current number of registration retries to 0. Returns: None Examples: >>> address ('192.168.0.20', 40080) """ logger.debug("<%s> Sending REGISTER request to: %s" % (str(self.cuuid), str(address))) if not self.listener.listening: logger.warning("Neteria client is not listening.") # Construct the message to send message = {"method": "REGISTER", "cuuid": str(self.cuuid)} # If we have encryption enabled, send our public key with our REGISTER # request if self.encryption: message["encryption"] = [self.encryption.n, self.encryption.e] # Send a REGISTER to the server self.listener.send_datagram( serialize_data(message, self.compression, encryption=False), address) if retry: # Reset the current number of REGISTER retries self.register_retries = 0 # Schedule a task to run in x seconds to check to see if we've timed # out in receiving a response from the server self.listener.call_later( self.timeout, self.retransmit, {"method": "REGISTER", "address": address}) def event(self, event_data, priority="normal"): """This function will send event packets to the server. This is the main method you would use to send data from your application to the server. Whenever an event is sent to the server, a universally unique event id (euuid) is created for each event and stored in the "event_uuids" dictionary. This dictionary contains a list of all events that are currently waiting for a response from the server. The event will only be removed from this dictionary if the server responds with LEGAL or ILLEGAL or if the request times out. Args: event_data (dict): The event data to send to the server. This data will be passed through the server's middleware to determine if the event is legal or not, and then processed by the server it is legal priority (string): The event's priority informs the server of whether or not the client is going to wait for a confirmation message from the server indicating whether its event was LEGAL or ILLEGAL. Setting this to "normal" informs the server that the client will wait for a response from the server before processing the event. Setting this to "high" informs the server that the client will NOT wait for a response. Defaults to "normal". Returns: A universally unique identifier (uuid) of the event. Examples: >>> event_data >>> priority """ logger.debug("event: " + str(event_data)) # Generate an event UUID for this event euuid = uuid.uuid1() logger.debug("<%s> Sending event data to server: " "%s" % (str(self.cuuid), str(euuid), str(self.server))) if not self.listener.listening: logger.warning("Neteria client is not listening.") # If we're not even registered, don't even bother. if not self.registered: logger.warning("<%s> Client is currently not registered. " "Event not sent." % (str(self.cuuid), str(euuid))) return False # Send the event data to the server packet = {"method": "EVENT", "cuuid": str(self.cuuid), "euuid": str(euuid), "event_data": event_data, "timestamp": str(datetime.now()), "retry": 0, "priority": priority} self.listener.send_datagram( serialize_data(packet, self.compression, self.encryption, self.server_key), self.server) logger.debug("<%s> Sending EVENT Packet: %s" % (str(self.cuuid), pformat(packet))) # Set the sent event to our event buffer to see if we need to roll back # or anything self.event_uuids[str(euuid)] = packet # Now we need to reschedule a timeout/retransmit check logger.debug("<%s> Scheduling retry in %s seconds" % (str(self.cuuid), str(self.timeout))) self.listener.call_later(self.timeout, self.retransmit, packet) return euuid def legal_check(self, message): """This method handles event legality check messages from the server. Args: message (dict): The unserialized legality dictionary received from the server. Returns: None Examples: >>> message """ # If the event was legal, remove it from our event buffer if message["method"] == "LEGAL": logger.debug("<%s> Event LEGAL" % (str(self.cuuid), message["euuid"])) logger.debug("<%s> Removing event from event " "buffer." % (str(self.cuuid), message["euuid"])) # If the message was a high priority, then we keep track of legal # events too if message["priority"] == "high": self.event_confirmations[ message["euuid"]] = self.event_uuids[message["euuid"]] logger.debug("<%s> Event was high priority. Adding " "to confirmations buffer." % (str(self.cuuid), message["euuid"])) logger.debug("<%s> Current event confirmation " "buffer: %s" % (str(self.cuuid), message["euuid"], pformat(self.event_confirmations))) # Try and remove the event from the currently processing events try: del self.event_uuids[message["euuid"]] except KeyError: logger.warning("<%s> Euuid does not exist in event " "buffer. Key was removed before we could process " "it." % (str(self.cuuid), message["euuid"])) # If the event was illegal, remove it from our event buffer and add it # to our rollback list elif message["method"] == "ILLEGAL": logger.debug("<%s> Event ILLEGAL" % (str(self.cuuid), message["euuid"])) logger.debug("<%s> Removing event from event buffer and " "adding to rollback buffer." % (str(self.cuuid), message["euuid"])) self.event_rollbacks[ message["euuid"]] = self.event_uuids[message["euuid"]] del self.event_uuids[message["euuid"]] # For testing you can run: # sudo sendip -p ipv4 -is 127.0.0.1 -p udp -us 5070 -ud 10858 -d # '{"method": "OHAI"}' -v 127.0.0.1 if __name__ == '__main__': # Enable logging for our client. import sys logger = logging.getLogger('neteria.client') logger.setLevel(logging.DEBUG) log_hdlr = logging.StreamHandler(sys.stdout) log_hdlr.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') log_hdlr.setFormatter(formatter) logger.addHandler(log_hdlr) logger.info("Client Started") # Create an instance of our client. client = NeteriaClient() client.listen() client.autodiscover() raw_input("enter to exit...") PK#H95b5bneteria/server.py#!/usr/bin/python # # Neteria # Copyright (C) 2014, William Edwards , # # This file is part of Neteria. # # Neteria is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Neteria is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Neteria. If not, see . # # Contributor(s): # # William Edwards # """The server module provides a way to send and receive messages to clients and process incoming messages. Since the Neteria server inherently does not trust the clients, each message received will go through a "middleware" layer which decides whether or not a message received from a client is legal or not. If the message is LEGAL, it will send the message to your application's "middleware" which will carry out legal actions in your application. If the message was deamed ILLEGAL, it will respond to the client indicating that the action was illegal, and discard the message without further processing. You can run an example by running the following from the command line: `python -m neteria.server`""" import logging import threading import uuid from datetime import datetime from pprint import pformat from rsa import PublicKey from .core import serialize_data from .core import unserialize_data from .core import ListenerUDP from .encryption import Encryption # Create a logger for optional handling of debug messages. logger = logging.getLogger(__name__) class NeteriaServer(object): """The primary Neteria Server class handles all server functions. Args: middleware (tools._Middleware): The middleware object used to determine if an event recieved by the client is LEGAL or not. It will also execute the event in your application if the event was LEGAL. version (string): The version of your application. This can be used to handle clients of different versions differently. Defaults to "1.0". app (object): The instance of your application. Setting this will automatically set the app.server attribute to an instance of this class. Defaults to None. port (int): The UDP port for the server to listen on. Defaults to 40080. compression (boolean): Whether or not to enable zlib compression on all network traffic. Defaults to False. encryption (boolean): Whether or not to enable RSA encryption on traffic to the client. Defaults to False. timeout (float): The amount of time to wait in seconds for a confirmation before retrying to send the message. Defaults to 2.0 seconds. max_retries (int): The maximum number of retry attempts the server should try before considering the message has failed. Defaults to 4. registration_limit (int): The maximum number of clients that can be connected and registered with the server. Defaults to 50. stats (boolean): Whether or not to keep track of network statistics such as total bytes sent/recieved. Defaults to False. Examples: >>> from neteria.tools import _Middleware >>> from neteria.server import NeteriaServer >>> >>> middleware = _Middleware() >>> myserver = NeteriaServer(middleware) >>> myserver.listen() """ def __init__(self, middleware, version="1.0.2", app=None, server_address='', server_port=40080, server_name=None, compression=False, encryption=False, timeout=2.0, max_retries=4, registration_limit=50, stats=False): self.version = version self.allowed_versions = [ version] # Client versions allowed to register. self.event_uuids = {} self.middleware = middleware self.port = server_port self.server_name = server_name self.compression = compression # Generate a keypair if encryption is enabled if encryption: self.encryption = Encryption() else: self.encryption = False # Keep a separate key pair that matches hosts to cuuid to find their # public key. self.encrypted_hosts = {} # Provide access to the app the created the server class. self.app = app # Provide the middleware with this instance of Neteria Server self.middleware.server = self # Create a listener to allow us to send and recieve messages. self.listener = ListenerUDP(self, listen_address=server_address, listen_port=server_port, stats=stats) # Set a timeout and maximum number of retries for responses from # clients. self.timeout = timeout self.max_retries = max_retries # Set a limit on the number of registrations to prevent registration # attacks. self.registration_limit = registration_limit self.registry = {} def listen(self): """Starts the server listener to listen for client messages. Args: None Returns: None """ logger.info("Listening on port " + str(self.listener.listen_port)) self.listener.listen() def retransmit(self, data): """Processes messages that have been delivered from the listener. Args: data (dict): A dictionary containing the uuid, euuid, and message response. E.g. {"cuuid": x, "euuid": y, "response": z}. Returns: None """ # If that shit is still in self.event_uuids, then that means we STILL # haven't gotten a response from the client. Then we resend that shit # and WAIT if data["euuid"] in self.event_uuids: # Increment the current retry count of the euuid self.event_uuids[data["euuid"]] += 1 # If we've tried more than the maximum, just log an error # and stahap. if (self.event_uuids[data["euuid"]] > self.max_retries or data["cuuid"] not in self.registry): logger.warning("<%s> Retry limit exceeded. " "Timed out waiting for client for " "event: %s" % (data["cuuid"], data["euuid"])) logger.warning("<%s> Deleting event from currently processing " "event uuids" % data["cuuid"]) del self.event_uuids[data["euuid"]] else: # Retransmit that shit logger.debug("<%s> Timed out waiting for response. Retry %s. " "Retransmitting message: " "%s" % (data["cuuid"], pformat(self.event_uuids[data["euuid"]]), data["response"])) # Look up the host and port based on cuuid host = self.registry[data["cuuid"]]["host"] port = self.registry[data["cuuid"]]["port"] # Send the packet to the client self.listener.send_datagram(data["response"], (host, port)) # Then we set another schedule to check again logger.debug("<%s> Scheduling to retry in %s " "seconds" % (data["cuuid"], str(self.timeout))) self.listener.call_later(self.timeout, self.retransmit, data) def handle_message(self, msg, host): """Processes messages that have been delivered from the listener. Args: msg (string): The raw packet data delivered from the listener. This data will be unserialized and then processed based on the packet's method. host (tuple): The (address, host) tuple of the source message. Returns: A response that will be sent back to the client via the listener. """ response = None # Unserialize the packet, and decrypt if the host has encryption enabled if host in self.encrypted_hosts: msg_data = unserialize_data(msg, self.compression, self.encryption) else: msg_data = unserialize_data(msg, self.compression) logger.debug("Packet received: " + pformat(msg_data)) # If the message data is blank, return none if not msg_data: return response # For debug purposes, check if the client is registered or not if self.is_registered(msg_data["cuuid"], host[0]): logger.debug("<%s> Client is currently registered" % msg_data["cuuid"]) else: logger.debug("<%s> Client is not registered" % msg_data["cuuid"]) if "method" in msg_data: if msg_data["method"] == "REGISTER": logger.debug("<%s> Register packet received" % msg_data["cuuid"]) response = self.register(msg_data, host) elif msg_data["method"] == "OHAI": logger.debug("<%s> Autodiscover packet received" % msg_data["cuuid"]) response = self.autodiscover(msg_data) elif msg_data["method"] == "EVENT": logger.debug("<%s> Event message " "received" % (msg_data["cuuid"], msg_data["euuid"])) response = self.event(msg_data["cuuid"], host, msg_data["euuid"], msg_data["event_data"], msg_data["timestamp"], msg_data["priority"]) elif msg_data["method"] == "OK EVENT": logger.debug("<%s> Event confirmation message " "received" % (msg_data["cuuid"], msg_data["euuid"])) try: del self.event_uuids[msg_data["euuid"]] except KeyError: logger.warning("<%s> Euuid does not exist in event " "buffer. Key was removed before we could process " "it." % (msg_data["cuuid"], msg_data["euuid"])) elif msg_data["method"] == "OK NOTIFY": logger.debug("<%s> Ok notify " "received" % (msg_data["cuuid"], msg_data["euuid"])) try: del self.event_uuids[msg_data["euuid"]] except KeyError: logger.warning("<%s> Euuid does not exist in event " "buffer. Key was removed before we could process " "it." % (msg_data["cuuid"], msg_data["euuid"])) logger.debug("Packet processing completed") return response def autodiscover(self, message): """This function simply returns the server version number as a response to the client. Args: message (dict): A dictionary of the autodiscover message from the client. Returns: A JSON string of the "OHAI Client" server response with the server's version number. Examples: >>> response '{"method": "OHAI Client", "version": "1.0"}' """ # Check to see if the client's version is the same as our own. if message["version"] in self.allowed_versions: logger.debug("<%s> Client version matches server " "version." % message["cuuid"]) response = serialize_data({"method": "OHAI Client", "version": self.version, "server_name": self.server_name}, self.compression, encryption=False) else: logger.warning("<%s> Client version %s does not match allowed server " "versions %s" % (message["cuuid"], message["version"], self.version)) response = serialize_data({"method": "BYE REGISTER"}, self.compression, encryption=False) return response def register(self, message, host): """This function will register a particular client in the server's registry dictionary. Any clients that are registered will be able to send and recieve events to and from the server. Args: message (dict): The client message from the client who wants to register. host (tuple): The (address, port) tuple of the client that is registering. Returns: A server response with an "OK REGISTER" if the registration was successful or a "BYE REGISTER" if unsuccessful. """ # Get the client generated cuuid from the register message cuuid = message["cuuid"] # Check to see if we've hit the maximum number of registrations # If we've reached the maximum limit, return a failure response to the # client. if len(self.registry) > self.registration_limit: logger.warning("<%s> Registration limit exceeded" % cuuid) response = serialize_data({"method": "BYE REGISTER"}, self.compression, encryption=False) return response # Insert a new record in the database with the client's information data = {"host": host[0], "port": host[1], "time": datetime.now()} # Prepare an OK REGISTER response to the client to let it know that it # has registered return_msg = {"method": "OK REGISTER"} # If the register request has a public key included in it, then include # it in the registry. if "encryption" in message and self.encryption: data["encryption"] = PublicKey(message["encryption"][0], message["encryption"][1]) # Add the host to the encrypted_hosts dictionary so we know to # decrypt messages from this host self.encrypted_hosts[host] = cuuid # If the client requested encryption and we have it enabled, send # our public key to the client return_msg["encryption"] = [self.encryption.n, self.encryption.e] # Add the entry to the registry if cuuid in self.registry: for key in data: self.registry[cuuid][key]=data[key] else: self.registry[cuuid] = data # Serialize our response to the client response = serialize_data(return_msg, self.compression, encryption=False) # For debugging, print all the current rows in the registry logger.debug("<%s> Registry entries:" % cuuid) for (key, value) in self.registry.items(): logger.debug("<%s> %s %s" % (str(cuuid), str(key), pformat(value))) return response def is_registered(self, cuuid, host): """This function will check to see if a given host with client uuid is currently registered. Args: cuuid (string): The client uuid that wishes to register. host (tuple): The (address, port) tuple of the client that is registering. Returns: Will return True if the client is registered and will return False if it is not. """ # Check to see if the host with the client uuid exists in the registry # table. if (cuuid in self.registry) and (self.registry[cuuid]["host"] == host): return True else: return False def event(self, cuuid, host, euuid, event_data, timestamp, priority): """This function will process event packets and send them to legal checks. Args: cuuid (string): The client uuid that the event came from. host (tuple): The (address, port) tuple of the client. euuid (string): The event uuid of the specific event. event_data (any): The event data that we will be sending to the middleware to be judged and executed. timestamp (string): The client provided timestamp of when the event was created. priority (string): The priority of the event. This is normally set to either "normal" or "high". If an event was sent with a high priority, then the client will not wait for a response from the server before executing the event locally. Returns: A LEGAL/ILLEGAL response to be sent to the client. """ # Set the initial response to none response = None # If the host we're sending to is using encryption, get their key to # encrypt. if host in self.encrypted_hosts: logger.debug("Encrypted!") client_key = self.registry[cuuid]["encryption"] else: logger.debug("Not encrypted :<") client_key = None # Get the port and host port = host[1] host = host[0] # First, we need to check if the request is coming from a registered # client. If it's not coming from a registered client, we tell them to # fuck off and register first. if not self.is_registered(cuuid, host): logger.warning("<%s> Sending BYE EVENT: Client not registered." % cuuid) response = serialize_data({"method": "BYE EVENT", "data": "Not registered"}, self.compression, self.encryption, client_key) return response # Check our stored event uuid's to see if we're already processing # this event. if euuid in self.event_uuids: logger.warning("<%s> Event ID is already being processed: %s" % (cuuid, euuid)) # If we're already working on this event, return none so we do not # reply to the client return response # If we're not already processing this event, store the event uuid # until we receive a confirmation from the client that it received our # judgement. self.event_uuids[euuid] = 0 logger.debug("<%s> Currently processing events: " "%s" % (cuuid, euuid, str(self.event_uuids))) logger.debug("<%s> New event being processed" % (cuuid, euuid)) logger.debug("<%s> Event Data: %s" % (cuuid, euuid, pformat(event_data))) # Send the event to the game middleware to determine if the event is # legal or not and to process the event in the Game Server if it is # legal. if self.middleware.event_legal(cuuid, euuid, event_data): logger.debug("<%s> Event LEGAL. Sending judgement " "to client." % (cuuid, euuid)) response = serialize_data({"method": "LEGAL", "euuid": euuid, "priority": priority}, self.compression, self.encryption, client_key) # Execute the event thread = threading.Thread(target=self.middleware.event_execute, args=(cuuid, euuid, event_data) ) thread.start() else: logger.debug("<%s> Event ILLEGAL. Sending judgement " "to client." % (cuuid, euuid)) response = serialize_data({"method": "ILLEGAL", "euuid": euuid, "priority": priority}, self.compression, self.encryption, client_key) # Schedule a task to run in x seconds to check to see if we've timed # out in receiving a response from the client. self.listener.call_later(self.timeout, self.retransmit, {"euuid": euuid, "response": response, "cuuid": cuuid}) return response def notify(self, cuuid, event_data): """This function will send a NOTIFY event to a registered client. NOTIFY messages are nearly identical to EVENT messages, except that NOTIFY messages are always sent from server -> client. EVENT messages are always sent from client -> server. In addition to this difference, NOTIFY messages are not processed by a middleware to determine if they are legal or not, since all messages from the server should be considered LEGAL. Args: cuuid (string): The client uuid to send the event data to. event_data (any): The event data that we will be sending to the client. Returns: None """ # Generate an event uuid for the notify event euuid = str(uuid.uuid1()) # If the client uses encryption, get their key to encrypt if "encryption" in self.registry[cuuid]: client_key = self.registry[cuuid]["encryption"] else: client_key = None logger.debug("<%s> <%s> Sending NOTIFY event to client with event data: " "%s" % (str(cuuid), str(euuid), pformat(event_data))) # Look up the host details based on cuuid try: ip_address = self.registry[cuuid]["host"] except KeyError: logger.warning("<%s> <%s> Host not found in registry! Transmit " "Canceled" % (str(cuuid), str(euuid))) return False try: port = self.registry[cuuid]["port"] except KeyError: logger.warning("<%s> <%s> Port not found! Transmit " "Canceled" % (str(cuuid), str(euuid))) return False # Set up the packet and address to send to packet = serialize_data({"method": "NOTIFY", "event_data": event_data, "euuid": euuid}, self.compression, self.encryption, client_key) address = (ip_address, port) # If we're not already processing this event, store the event uuid # until we receive a confirmation from the client that it received our # notification. self.event_uuids[euuid] = 0 # This is the current retry attempt logger.debug("<%s> Currently processing events: " "%s" % (cuuid, pformat(self.event_uuids))) logger.debug("<%s> New NOTIFY event being processed:" % cuuid) logger.debug("<%s> EUUID: %s" % (cuuid, euuid)) logger.debug("<%s> Event Data: %s" % (cuuid, pformat(event_data))) # Send the packet to the client self.listener.send_datagram(packet, address) # Schedule a task to run in x seconds to check to see if we've timed # out in receiving a response from the client/ self.listener.call_later(self.timeout, self.retransmit, {"euuid": euuid, "response": packet, "cuuid": cuuid}) # For testing you can run: # sudo sendip -p ipv4 -is 127.0.0.1 -p udp -us 5070 -ud 10858 -d # '{"method": "OHAI"}' -v 127.0.0.1 if __name__ == '__main__': # Enable logging for our server. import sys logger = logging.getLogger('neteria.server') logger.setLevel(logging.DEBUG) log_hdlr = logging.StreamHandler(sys.stdout) log_hdlr.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') log_hdlr.setFormatter(formatter) logger.addHandler(log_hdlr) logger.info("Server Started") # Import our middleware, which will process incoming events from our # clients. from tools import _Middleware middleware = _Middleware() myserver = NeteriaServer(middleware) myserver.listen() raw_input("Press enter to exit...") PK&G&W0neteria/encryption.py#!/usr/bin/python """The encryption module is used to encrypt and decrypt messages using the python-rsa package. You can run an example by running the following from the command line: `python -m neteria.encryption`""" import json import binascii import textwrap try: import rsa except: rsa = False class Encryption(): """Handles encrypting and decrypting messages using RSA public key/private key authentication. Args: key_length (int): The length of the encryption key in bytes. All messages will be this size, so larger keys means larger packets. Defaults to 512 """ def __init__(self, key_length=512): # Set the key length self.key_length = key_length # Generate a public key/private key pair self.public_key, self.private_key = rsa.newkeys(self.key_length) # To send the public key, just send the values of "n" and "e", then we # can construct a new PublicKey object on the other side. self.n = self.public_key["n"] self.e = self.public_key["e"] def encrypt(self, message, public_key): """Encrypts a string using a given rsa.PublicKey object. If the message is larger than the key, it will split it up into a list and encrypt each line in the list. Args: message (string): The string to encrypt. public_key (rsa.PublicKey): The key object used to encrypt the message. Only the paired private key can decrypt it. Returns: A json string of the list of encrypted lines of the message. """ # Get the maximum message length based on the key max_str_len = rsa.common.byte_size(public_key.n) - 11 # If the message is longer than the key size, split it into a list to # be encrypted if len(message) > max_str_len: message = textwrap.wrap(message, width=max_str_len) else: message = [message] # Create a list for the encrypted message to send enc_msg = [] # If we have a long message, loop through and encrypt each part of the # string for line in message: # Encrypt the line in the message into a bytestring enc_line = rsa.encrypt(line, public_key) # Convert the encrypted bytestring into ASCII, so we can send it # over the network enc_line_converted = binascii.b2a_base64(enc_line) enc_msg.append(enc_line_converted) # Serialize the encrypted message again with json enc_msg = json.dumps(enc_msg) # Return the list of encrypted strings return enc_msg def decrypt(self, message): """Decrypts a string using our own private key object. Args: message (string): The string of the message to decrypt. Returns: The unencrypted string. """ # Unserialize the encrypted message message = json.loads(message) # Set up a list for the unencrypted lines of the message unencrypted_msg = [] for line in message: # Convert from ascii back to bytestring enc_line = binascii.a2b_base64(line) # Decrypt the line using our private key unencrypted_line = rsa.decrypt(enc_line, self.private_key) unencrypted_msg.append(unencrypted_line) # Convert the message from a list back into a string unencrypted_msg = "".join(unencrypted_msg) return unencrypted_msg # Run an example if we execute standalone if __name__ == '__main__': import zlib message = "hello asdmkasd" * 50 # message = "hi" print("Plain text:", message) message = zlib.compress(message) message = binascii.b2a_base64(message) print("") print("Compressed message:", message) print("") enc = Encryption() msg = enc.encrypt(message, enc.public_key) decrypted_msg = enc.decrypt(msg) print("Encryped message:", msg) print("") print("Decrypted message:", decrypted_msg) decrypted_msg = binascii.a2b_base64(decrypted_msg) uncompressed_msg = zlib.decompress(decrypted_msg) print("") print("Decompressed message:", uncompressed_msg) PK4DG\  neteria/tools.py#!/usr/bin/python """This module contains prototype middleware classes that you can use with your Neteria server. Every Neteria server instance requires a middleware that will process and handle events from the client. The middleware is also responsible for determining if an event recieved from a client is legal or not.""" class _Middleware(object): """This is a prototype class for your server's middleware. Your middleware should inherit from it. No direct instances of this class should be created. The "event_legal" and "event_execute" methods MUST be overridden in the child class. The middleware is used to handle and process all events that come in from the clients. Since Neteria inheritely does not trust the client, ALL events that are recieved are passed through the "event_legal" method. If "event_legal" returns True, then it will pass to the "event_execute" method to process the event. If "event_legal" returns False, then it will not be executed and the client will recieve a response indicating that the event was ILLEGAL. Args: game_server (object): An instance of the application that is running the Neteria server. """ def __init__(self, game_server=None): self.game_server = game_server self.server = None def event_legal(self, cuuid, euuid, event_data): """Determines whether or not the event is LEGAL or ILLEGAL. If the event is LEGAL, the Neteria server will execute the "event_execute" method. This method should be overridden in the child class, otherwise ALL events from the client will be considered LEGAL. Args: cuuid (string): The client's universally unique identifier (uuid). euuid (string): The event's universally unique identifier (uuid). event_data (any): Arbitrary data sent from the client. Returns: True if the event is LEGAL. False if the event was ILLEGAL. """ return True def event_execute(self, cuuid, euuid, event_data): """If the event was LEGAL, execute the event on the server. This method MUST be overridden in the child class. Otherwise this method does nothing. Args: cuuid (string): The client's universally unique identifier (uuid). euuid (string): The event's universally unique identifier (uuid). event_data (any): Arbitrary data sent from the client. Returns: None """ pass class _ControllerMiddleware(_Middleware): """This middleware will allow you to use the NeteriaServer as a basic controller. When it receives KEYDOWN/KEYUP events, it will set the corresponding dictionary key in "network_events" to true or false. In your main game loop, you can then iterate through this dictionary and change the game accordingly. """ def __init__(self, game_server=None): _Middleware.__init__(self, game_server) def event_execute(self, cuuid, euuid, event_data): if event_data == "KEYDOWN:down": self.game_server.network_events["down"] = True elif event_data == "KEYUP:down": self.game_server.network_events["down"] = False elif event_data == "KEYDOWN:up": self.game_server.network_events["up"] = True elif event_data == "KEYUP:up": self.game_server.network_events["up"] = False elif event_data == "KEYDOWN:left": self.game_server.network_events["left"] = True elif event_data == "KEYUP:left": self.game_server.network_events["left"] = False elif event_data == "KEYDOWN:right": self.game_server.network_events["right"] = True elif event_data == "KEYUP:right": self.game_server.network_events["right"] = False PK4DGJ/neteria/__init__.py"""Neteria is an open source game networking framework used to send and recieve messages in a client/server architecture. With Neteria, you can autodiscover servers on your local network and register to send messages to and from your server. """ PK#HT4949neteria/core.py#!/usr/bin/python # # Asteria # Copyright (C) 2013-2014, William Edwards , # Derek J. Clark # # This file is part of Asteria. # # Asteria is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Asteria is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Asteria. If not, see . # # Contributor(s): # # William Edwards , # """This module is used for the core network functionality for neteria such as sending and receiving UDP datagrams.""" import binascii import json import logging import socket import errno import struct import time import traceback import zlib # Create a logger for optional handling of debug messages. logger = logging.getLogger(__name__) def serialize_data(data, compression=False, encryption=False, public_key=None): """Serializes normal Python datatypes into plaintext using json. You may also choose to enable compression and encryption when serializing data to send over the network. Enabling one or both of these options will incur additional overhead. Args: data (dict): The data to convert into plain text using json. compression (boolean): True or False value on whether or not to compress the serialized data. encryption (rsa.encryption): An encryption instance used to encrypt the message if encryption is desired. public_key (str): The public key to use to encrypt if encryption is enabled. Returns: The string message serialized using json. """ message = json.dumps(data) if compression: message = zlib.compress(message) message = binascii.b2a_base64(message) if encryption and public_key: message = encryption.encrypt(message, public_key) encoded_message = str.encode(message) return encoded_message def unserialize_data(data, compression=False, encryption=False): """Unserializes the packet data and converts it from json format to normal Python datatypes. If you choose to enable encryption and/or compression when serializing data, you MUST enable the same options when unserializing data. Args: data (str): The raw, serialized packet data delivered from the transport protocol. compression (boolean): True or False value on whether or not to uncompress the serialized data. encryption (rsa.encryption): An encryption instance used to decrypt the message if encryption is desired. Returns: The message unserialized in normal Python datatypes. """ try: if encryption: data = encryption.decrypt(data) except Exception as err: logger.error("Decryption Error: " + str(err)) message = False try: if compression: data = binascii.a2b_base64(data) data = zlib.decompress(data) message = json.loads(data) except Exception as err: logger.error("Decompression Error: " + str(err)) message = False decoded_message = data.decode() if not encryption and not compression: message = json.loads(decoded_message) return message class ListenerUDP(object): """A class used to send and recieve UDP datagrams over the network. Args: app (object): A class instance with a "handle_message" method that will process received messages. threading (boolean): Whether or not to set up the main listener loop in its own thread. Defaults to True. stats (boolean): Whether or not to keep track of network statistics such as total bytes sent/recieved. Defaults to False. listen_address (str): The address for the listener to listen on, defaults to all addresses. listen_port (int): The port for the listener to listen on, defaults to port 40080. listen_type (str): The type of listener. This can be either "unicast" or "multicast". Unicast is designed to send messages to a single host. Multicast is designed to send messages to multiple hosts at once. Defaults to "unicast". bufsize (int): The size of our buffer used for receiving messages in bytes. If a message received is larger than this buffer size, the message will be truncated. Defaults to 10240. """ def __init__(self, app, threading=True, stats=False, listen_address='', listen_port=40080, listen_type="unicast", bufsize=10240): self.app = app self.threading = threading self.scheduler_thread = None self.listen_thread = None self.listen_address = listen_address self.listen_port = listen_port self.listen_type = listen_type self.bufsize = bufsize self.listening = False self.stats_enabled = stats self.stats = {} self.stats['bytes_sent'] = 0 self.stats['bytes_recieved'] = 0 self.stats['mbps_sent'] = 0.0 self.stats['mbps_recieved'] = 0.0 self.stats['last_bytes_sent'] = 0 self.stats['last_bytes_recieved'] = 0 self.stats['check_interval'] = 2.0 # Set up our socket and bind to our listen address and port. self.sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((listen_address, listen_port)) # If this is a multicast receiver, set it up as such. if listen_type == "multicast": mreq = struct.pack( "4sl", socket.inet_aton(listen_address), socket.INADDR_ANY) self.sock.setsockopt( socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) # Create a list of callbacks that we can schedule to run while we're # listening. self.scheduled_calls = [] # If stats are enabled, start a recurring task that will calculate our # network stats every x seconds. if stats: self.call_later(self.stats['check_interval'], self.calculate_stats, None) def listen(self): """Starts the listen loop. If threading is enabled, then the loop will be started in its own thread. Args: None Returns: None """ self.listening = True if self.threading: from threading import Thread self.listen_thread = Thread(target=self.listen_loop) self.listen_thread.daemon = True self.listen_thread.start() self.scheduler_thread = Thread(target=self.scheduler) self.scheduler_thread.daemon = True self.scheduler_thread.start() else: self.listen_loop() def listen_loop(self): """Starts the listen loop and executes the receieve_datagram method whenever a packet is receieved. Args: None Returns: None """ while self.listening: try: data, address = self.sock.recvfrom(self.bufsize) self.receive_datagram(data, address) if self.stats_enabled: self.stats['bytes_recieved'] += len(data) except socket.error as error: if error.errno == errno.WSAECONNRESET: logger.info("connection reset") else: raise logger.info("Shutting down the listener...") def scheduler(self, sleep_time=0.2): """Starts the scheduler to check for scheduled calls and execute them at the correct time. Args: sleep_time (float): The amount of time to wait in seconds between each loop iteration. This prevents the scheduler from consuming 100% of the host's CPU. Defaults to 0.2 seconds. Returns: None """ while self.listening: # If we have any scheduled calls, execute them and remove them from # our list of scheduled calls. if self.scheduled_calls: timestamp = time.time() self.scheduled_calls[:] = [item for item in self.scheduled_calls if not self.time_reached(timestamp, item)] time.sleep(sleep_time) logger.info("Shutting down the call scheduler...") def call_later(self, time_seconds, callback, arguments): """Schedules a function to be run x number of seconds from now. The call_later method is primarily used to resend messages if we haven't received a confirmation message from the receiving host. We can wait x number of seconds for a response and then try sending the message again. Args: time_seconds (float): The number of seconds from now we should call the provided function. callback (function): The method to execute when our time has been reached. E.g. self.retransmit arguments (dict): A dictionary of arguments to send to the callback. Returns: None """ scheduled_call = {'ts': time.time() + time_seconds, 'callback': callback, 'args': arguments} self.scheduled_calls.append(scheduled_call) def time_reached(self, current_time, scheduled_call): """Checks to see if it's time to run a scheduled call or not. If it IS time to run a scheduled call, this function will execute the method associated with that call. Args: current_time (float): Current timestamp from time.time(). scheduled_call (dict): A scheduled call dictionary that contains the timestamp to execute the call, the method to execute, and the arguments used to call the method. Returns: None Examples: >>> scheduled_call {'callback': , 'args': {'k': 'v'}, 'ts': 1415066599.769509} """ if current_time >= scheduled_call['ts']: scheduled_call['callback'](scheduled_call['args']) return True else: return False def send_datagram(self, message, address, message_type="unicast"): """Sends a UDP datagram packet to the requested address. Datagrams can be sent as a "unicast", "multicast", or "broadcast" message. Unicast messages are messages that will be sent to a single host, multicast messages will be delivered to all hosts listening for multicast messages, and broadcast messages will be delivered to ALL hosts on the network. Args: message (str): The raw serialized packet data to send. address (tuple): The address and port of the destination to send the packet. E.g. (address, port) message_type -- The type of packet to send. Can be "unicast", "multicast", or "broadcast". Defaults to "unicast". Returns: None """ if self.bufsize is not 0 and len(message) > self.bufsize: raise Exception("Datagram is too large. Messages should be " + "under " + str(self.bufsize) + " bytes in size.") if message_type == "broadcast": self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) elif message_type == "multicast": self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) try: logger.debug("Sending packet") self.sock.sendto(message, address) if self.stats_enabled: self.stats['bytes_sent'] += len(message) except socket.error: logger.error("Failed to send, [Errno 101]: Network is unreachable.") def receive_datagram(self, data, address): """Executes when UDP data has been received and sends the packet data to our app to process the request. Args: data (str): The raw serialized packet data received. address (tuple): The address and port of the origin of the received packet. E.g. (address, port). Returns: None """ # If we do not specify an application, just print the data. if not self.app: logger.debug("Packet received", address, data) return False # Send the data we've recieved from the network and send it # to our application for processing. try: response = self.app.handle_message(data, address) except Exception as err: logger.error("Error processing message from " + str(address) + ":" + str(data)) logger.error(traceback.format_exc()) return False # If our application generated a response to this message, # send it to the original sender. if response: self.send_datagram(response, address) def calculate_stats(self, arguments): # Get our previous number of bytes sent. bytes_sent = self.stats['bytes_sent'] - self.stats['last_bytes_sent'] bytes_recieved = self.stats['bytes_recieved'] - \ self.stats['last_bytes_recieved'] # Set our last number of bytes to the current one in preparation for # the next time this method is called. self.stats['last_bytes_sent'] = self.stats['bytes_sent'] self.stats['last_bytes_recieved'] = self.stats['bytes_recieved'] # Calculate the traffic in KiB/s. self.stats['kbps_sent'] = (float(bytes_sent) / 1024) / \ self.stats['check_interval'] self.stats['kbps_recieved'] = (float(bytes_recieved) / 1024) / \ self.stats['check_interval'] # Schedule this calculation to run again in x number of seconds. self.call_later(self.stats['check_interval'], self.calculate_stats, None) PKܣ#H^- 'neteria-1.0.2.dist-info/DESCRIPTION.rstUNKNOWN PKܣ#Hڮ%neteria-1.0.2.dist-info/metadata.json{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Topic :: System :: Networking", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "shadowapex@gmail.com", "name": "William Edwards", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://www.neteria.org"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["gaming", "networking", "network", "game"], "license": "GPLv3", "metadata_version": "2.0", "name": "neteria", "run_requires": [{"requires": ["rsa"]}], "summary": "A simple game networking library.", "version": "1.0.2"}PKܣ#H%neteria-1.0.2.dist-info/top_level.txtneteria PKܣ#H''\\neteria-1.0.2.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PKܣ#Hh  neteria-1.0.2.dist-info/METADATAMetadata-Version: 2.0 Name: neteria Version: 1.0.2 Summary: A simple game networking library. Home-page: http://www.neteria.org Author: William Edwards Author-email: shadowapex@gmail.com License: GPLv3 Keywords: gaming networking network game Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Topic :: System :: Networking Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.5 Requires-Dist: rsa UNKNOWN PKܣ#HҰneteria-1.0.2.dist-info/RECORDneteria/__init__.py,sha256=4FLD0bwCSZMCz1u62Fw1J1kESuMgsKqXgkpGGFEC1S4,246 neteria/client.py,sha256=M7HNwXG_PUG_Ib4q1bt9vjv_AujZB1kGm2iZXwnhVx8,22431 neteria/core.py,sha256=wWzHC8ArtEAd69e-1Ps4Lqjp70aYsGHEGszMdyjK3Bo,14644 neteria/encryption.py,sha256=6_XBjm6H86CWRiGZ8TQBxSV6JABm5ahc5DZDaVrqAPc,4275 neteria/server.py,sha256=V7DbUMUOOf5SwFTr2vJ5EhxAC3SVRmbBFrIFMMwFTYw,25141 neteria/tools.py,sha256=K6y771ahmQ_6cH1vFtFRMp-JK9mAbhAFVa3ybRuXS1A,3872 neteria-1.0.2.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 neteria-1.0.2.dist-info/METADATA,sha256=snk1GX-m1HN-1Mnq9SUipD3FUsuOkIGUzfHQrdj_Ueo,741 neteria-1.0.2.dist-info/RECORD,, neteria-1.0.2.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 neteria-1.0.2.dist-info/metadata.json,sha256=-E79nJE69rhTZZ7mcQHhcRtHK7PKh8U4toTb_lEWXeQ,914 neteria-1.0.2.dist-info/top_level.txt,sha256=USoawdcEX-9P17BMiEne13K0Ye17jDVzkvPwJSUUXhU,8 PK#HWWneteria/client.pyPK#H95b5bWneteria/server.pyPK&G&W02neteria/encryption.pyPK4DG\  neteria/tools.pyPK4DGJ/fneteria/__init__.pyPK#HT4949neteria/core.pyPKܣ#H^- 'neteria-1.0.2.dist-info/DESCRIPTION.rstPKܣ#Hڮ%=neteria-1.0.2.dist-info/metadata.jsonPKܣ#H%neteria-1.0.2.dist-info/top_level.txtPKܣ#H''\\]neteria-1.0.2.dist-info/WHEELPKܣ#Hh  neteria-1.0.2.dist-info/METADATAPKܣ#HҰneteria-1.0.2.dist-info/RECORDPK ]!