Thu, 25 Jan 2024 14:13:36 +0100
JSON server and client
- Changed the code code to secure the data with an Adler32 checksum.
# -*- coding: utf-8 -*- # Copyright (c) 2017 - 2024 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the JSON based client base class. """ import contextlib import io import json import select import socket import struct import sys import traceback import zlib class EricJsonClient: """ Class implementing a JSON based client base class. """ def __init__(self, host, port, idString=""): """ Constructor @param host IP address the background service is listening @type str @param port port of the background service @type int @param idString assigned client id to be sent back to the server in order to identify the connection @type str """ self.__connection = socket.create_connection((host, port)) if idString: reply = idString + "\n" self.__connection.sendall(reply.encode("utf8", "backslashreplace")) def sendJson(self, command, params): """ Public method to send a single refactoring command to the server. @param command command name to be sent @type str @param params dictionary of named parameters for the command @type dict """ commandDict = { "jsonrpc": "2.0", "method": command, "params": params, } data = json.dumps(commandDict).encode("utf8", "backslashreplace") header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF) self.__connection.sendall(header) self.__connection.sendall(data) def __receiveBytes(self, length): """ Private method to receive the given length of bytes. @param length bytes to receive @type int @return received bytes or None if connection closed @rtype bytes """ data = bytearray() while len(data) < length: newData = self.__connection.recv(length - len(data)) if not newData: return None data += newData return data def __receiveJson(self): """ Private method to receive a JSON encoded command and data from the server. @return tuple containing the received command and a dictionary containing the associated data @rtype tuple of (str, dict) """ # step 1: receive the data header = self.__receiveBytes(struct.calcsize(b"!II")) if not header: return None, None length, datahash = struct.unpack(b"!II", header) length = int(length) data = self.__receiveBytes(length) if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash: self.sendJson( "ClientException", { "ExceptionType": "ProtocolError", "ExceptionValue": "The checksum of the data does not match.", "ProtocolData": data.decode("utf8", "backslashreplace"), }, ) return None, None # step 2: decode and convert the data jsonString = data.decode("utf8", "backslashreplace") try: commandDict = json.loads(jsonString.strip()) except (TypeError, ValueError) as err: self.sendJson( "ClientException", { "ExceptionType": "ProtocolError", "ExceptionValue": str(err), "ProtocolData": jsonString.strip(), }, ) return None, None method = commandDict["method"] params = commandDict["params"] return method, params def handleCall(self, method, params): """ Public method to handle a method call from the server. Note: This is an empty implementation that must be overridden in derived classes. @param method requested method name @type str @param params dictionary with method specific parameters @type dict """ pass def run(self): """ Public method implementing the main loop of the client. """ try: selectErrors = 0 while selectErrors <= 10: # selected arbitrarily try: rrdy, wrdy, xrdy = select.select([self.__connection], [], []) # Just waiting for self.__connection. Therefor no check # needed. method, params = self.__receiveJson() if method is None: selectErrors += 1 elif method == "Exit": break else: self.handleCall(method, params) # reset select errors selectErrors = 0 except (KeyboardInterrupt, select.error, socket.error): selectErrors += 1 except Exception: exctype, excval, exctb = sys.exc_info() tbinfofile = io.StringIO() traceback.print_tb(exctb, None, tbinfofile) tbinfofile.seek(0) tbinfo = tbinfofile.read() del exctb self.sendJson( "ClientException", { "ExceptionType": str(exctype), "ExceptionValue": str(excval), "Traceback": tbinfo, }, ) # Give time to process latest response on server side with contextlib.suppress(socket.error, OSError): self.__connection.shutdown(socket.SHUT_RDWR) self.__connection.close() def poll(self, waitMethod=""): """ Public method to check and receive one message (if available). @param waitMethod name of a method to wait for @type str @return dictionary containing the data of the waited for method @rtype dict """ try: if waitMethod: rrdy, wrdy, xrdy = select.select([self.__connection], [], []) else: rrdy, wrdy, xrdy = select.select([self.__connection], [], [], 0) if self.__connection in rrdy: method, params = self.__receiveJson() if method is not None: if method == "Exit": self.__exitClient = True elif method == waitMethod: return params else: self.handleCall(method, params) except (KeyboardInterrupt, select.error, socket.error): # just ignore these pass except Exception: exctype, excval, exctb = sys.exc_info() tbinfofile = io.StringIO() traceback.print_tb(exctb, None, tbinfofile) tbinfofile.seek(0) tbinfo = tbinfofile.read() del exctb self.sendJson( "ClientException", { "ExceptionType": str(exctype), "ExceptionValue": str(excval), "Traceback": tbinfo, }, ) return None