# Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Provides utility functions for TCP/UDP echo servers and clients. This program has classes and functions to encode, decode, calculate checksum and verify the "echo request" and "echo response" messages. "echo request" message is an echo message sent from the client to the server. "echo response" message is a response from the server to the "echo request" message from the client. The format of "echo request" message is . is the version number of the "echo request" protocol. is the checksum of the . is the size of the . is the echo message. The format of "echo response" message is ., and are same as what is in the "echo request" message. is encoded version of the . is a randomly generated key that is used to encode/decode the . """ __author__ = 'rtenneti@google.com (Raman Tenneti)' from itertools import cycle from itertools import izip import random class EchoHeader(object): """Class to keep header info of the EchoRequest and EchoResponse messages. This class knows how to parse the checksum, payload_size from the "echo request" and "echo response" messages. It holds the checksum, payload_size of the "echo request" and "echo response" messages. """ # This specifies the version. VERSION_STRING = '01' # This specifies the starting position of the checksum and length of the # checksum. Maximum value for the checksum is less than (2 ** 31 - 1). CHECKSUM_START = 2 CHECKSUM_LENGTH = 10 CHECKSUM_FORMAT = '%010d' CHECKSUM_END = CHECKSUM_START + CHECKSUM_LENGTH # This specifies the starting position of the and length of the # . Maximum number of bytes that can be sent in the is # 9,999,999. PAYLOAD_SIZE_START = CHECKSUM_END PAYLOAD_SIZE_LENGTH = 7 PAYLOAD_SIZE_FORMAT = '%07d' PAYLOAD_SIZE_END = PAYLOAD_SIZE_START + PAYLOAD_SIZE_LENGTH def __init__(self, checksum=0, payload_size=0): """Initializes the checksum and payload_size of self (EchoHeader). Args: checksum: (int) The checksum of the payload. payload_size: (int) The size of the payload. """ self.checksum = checksum self.payload_size = payload_size def ParseAndInitialize(self, echo_message): """Parses the echo_message and initializes self with the parsed data. This method extracts checksum, and payload_size from the echo_message (echo_message could be either echo_request or echo_response messages) and initializes self (EchoHeader) with checksum and payload_size. Args: echo_message: (string) The string representation of EchoRequest or EchoResponse objects. Raises: ValueError: Invalid data """ if not echo_message or len(echo_message) < EchoHeader.PAYLOAD_SIZE_END: raise ValueError('Invalid data:%s' % echo_message) self.checksum = int(echo_message[ EchoHeader.CHECKSUM_START:EchoHeader.CHECKSUM_END]) self.payload_size = int(echo_message[ EchoHeader.PAYLOAD_SIZE_START:EchoHeader.PAYLOAD_SIZE_END]) def InitializeFromPayload(self, payload): """Initializes the EchoHeader object with the payload. It calculates checksum for the payload and initializes self (EchoHeader) with the calculated checksum and size of the payload. This method is used by the client code during testing. Args: payload: (string) The payload is the echo string (like 'hello'). Raises: ValueError: Invalid data """ if not payload: raise ValueError('Invalid data:%s' % payload) self.payload_size = len(payload) self.checksum = Checksum(payload, self.payload_size) def __str__(self): """String representation of the self (EchoHeader). Returns: A string representation of self (EchoHeader). """ checksum_string = EchoHeader.CHECKSUM_FORMAT % self.checksum payload_size_string = EchoHeader.PAYLOAD_SIZE_FORMAT % self.payload_size return EchoHeader.VERSION_STRING + checksum_string + payload_size_string class EchoRequest(EchoHeader): """Class holds data specific to the "echo request" message. This class holds the payload extracted from the "echo request" message. """ # This specifies the starting position of the . PAYLOAD_START = EchoHeader.PAYLOAD_SIZE_END def __init__(self): """Initializes EchoRequest object.""" EchoHeader.__init__(self) self.payload = '' def ParseAndInitialize(self, echo_request_data): """Parses and Initializes the EchoRequest object from the echo_request_data. This method extracts the header information (checksum and payload_size) and payload from echo_request_data. Args: echo_request_data: (string) The string representation of EchoRequest object. Raises: ValueError: Invalid data """ EchoHeader.ParseAndInitialize(self, echo_request_data) if len(echo_request_data) <= EchoRequest.PAYLOAD_START: raise ValueError('Invalid data:%s' % echo_request_data) self.payload = echo_request_data[EchoRequest.PAYLOAD_START:] def InitializeFromPayload(self, payload): """Initializes the EchoRequest object with payload. It calculates checksum for the payload and initializes self (EchoRequest) object. Args: payload: (string) The payload string for which "echo request" needs to be constructed. """ EchoHeader.InitializeFromPayload(self, payload) self.payload = payload def __str__(self): """String representation of the self (EchoRequest). Returns: A string representation of self (EchoRequest). """ return EchoHeader.__str__(self) + self.payload class EchoResponse(EchoHeader): """Class holds data specific to the "echo response" message. This class knows how to parse the "echo response" message. This class holds key, encoded_payload and decoded_payload of the "echo response" message. """ # This specifies the starting position of the |key_| and length of the |key_|. # Minimum and maximum values for the |key_| are 100,000 and 999,999. KEY_START = EchoHeader.PAYLOAD_SIZE_END KEY_LENGTH = 6 KEY_FORMAT = '%06d' KEY_END = KEY_START + KEY_LENGTH KEY_MIN_VALUE = 0 KEY_MAX_VALUE = 999999 # This specifies the starting position of the and length # of the . ENCODED_PAYLOAD_START = KEY_END def __init__(self, key='', encoded_payload='', decoded_payload=''): """Initializes the EchoResponse object.""" EchoHeader.__init__(self) self.key = key self.encoded_payload = encoded_payload self.decoded_payload = decoded_payload def ParseAndInitialize(self, echo_response_data=None): """Parses and Initializes the EchoResponse object from echo_response_data. This method calls EchoHeader to extract header information from the echo_response_data and it then extracts key and encoded_payload from the echo_response_data. It holds the decoded payload of the encoded_payload. Args: echo_response_data: (string) The string representation of EchoResponse object. Raises: ValueError: Invalid echo_request_data """ EchoHeader.ParseAndInitialize(self, echo_response_data) if len(echo_response_data) <= EchoResponse.ENCODED_PAYLOAD_START: raise ValueError('Invalid echo_response_data:%s' % echo_response_data) self.key = echo_response_data[EchoResponse.KEY_START:EchoResponse.KEY_END] self.encoded_payload = echo_response_data[ EchoResponse.ENCODED_PAYLOAD_START:] self.decoded_payload = Crypt(self.encoded_payload, self.key) def InitializeFromEchoRequest(self, echo_request): """Initializes EchoResponse with the data from the echo_request object. It gets the checksum, payload_size and payload from the echo_request object and then encodes the payload with a random key. It also saves the payload as decoded_payload. Args: echo_request: (EchoRequest) The EchoRequest object which has "echo request" message. """ self.checksum = echo_request.checksum self.payload_size = echo_request.payload_size self.key = (EchoResponse.KEY_FORMAT % random.randrange(EchoResponse.KEY_MIN_VALUE, EchoResponse.KEY_MAX_VALUE)) self.encoded_payload = Crypt(echo_request.payload, self.key) self.decoded_payload = echo_request.payload def __str__(self): """String representation of the self (EchoResponse). Returns: A string representation of self (EchoResponse). """ return EchoHeader.__str__(self) + self.key + self.encoded_payload def Crypt(payload, key): """Encodes/decodes the payload with the key and returns encoded payload. This method loops through the payload and XORs each byte with the key. Args: payload: (string) The string to be encoded/decoded. key: (string) The key used to encode/decode the payload. Returns: An encoded/decoded string. """ return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in izip(payload, cycle(key))) def Checksum(payload, payload_size): """Calculates the checksum of the payload. Args: payload: (string) The payload string for which checksum needs to be calculated. payload_size: (int) The number of bytes in the payload. Returns: The checksum of the payload. """ checksum = 0 length = min(payload_size, len(payload)) for i in range (0, length): checksum += ord(payload[i]) return checksum def GetEchoRequestData(payload): """Constructs an "echo request" message from the payload. It builds an EchoRequest object from the payload and then returns a string representation of the EchoRequest object. This is used by the TCP/UDP echo clients to build the "echo request" message. Args: payload: (string) The payload string for which "echo request" needs to be constructed. Returns: A string representation of the EchoRequest object. Raises: ValueError: Invalid payload """ try: echo_request = EchoRequest() echo_request.InitializeFromPayload(payload) return str(echo_request) except (IndexError, ValueError): raise ValueError('Invalid payload:%s' % payload) def GetEchoResponseData(echo_request_data): """Verifies the echo_request_data and returns "echo response" message. It builds the EchoRequest object from the echo_request_data and then verifies the checksum of the EchoRequest is same as the calculated checksum of the payload. If the checksums don't match then it returns None. It checksums match, it builds the echo_response object from echo_request object and returns string representation of the EchoResponse object. This is used by the TCP/UDP echo servers. Args: echo_request_data: (string) The string that echo servers send to the clients. Returns: A string representation of the EchoResponse object. It returns None if the echo_request_data is not valid. Raises: ValueError: Invalid echo_request_data """ try: if not echo_request_data: raise ValueError('Invalid payload:%s' % echo_request_data) echo_request = EchoRequest() echo_request.ParseAndInitialize(echo_request_data) if Checksum(echo_request.payload, echo_request.payload_size) != echo_request.checksum: return None echo_response = EchoResponse() echo_response.InitializeFromEchoRequest(echo_request) return str(echo_response) except (IndexError, ValueError): raise ValueError('Invalid payload:%s' % echo_request_data) def DecodeAndVerify(echo_request_data, echo_response_data): """Decodes and verifies the echo_response_data. It builds EchoRequest and EchoResponse objects from the echo_request_data and echo_response_data. It returns True if the EchoResponse's payload and checksum match EchoRequest's. This is used by the TCP/UDP echo clients for testing purposes. Args: echo_request_data: (string) The request clients sent to echo servers. echo_response_data: (string) The response clients received from the echo servers. Returns: True if echo_request_data and echo_response_data match. Raises: ValueError: Invalid echo_request_data or Invalid echo_response """ try: echo_request = EchoRequest() echo_request.ParseAndInitialize(echo_request_data) except (IndexError, ValueError): raise ValueError('Invalid echo_request:%s' % echo_request_data) try: echo_response = EchoResponse() echo_response.ParseAndInitialize(echo_response_data) except (IndexError, ValueError): raise ValueError('Invalid echo_response:%s' % echo_response_data) return (echo_request.checksum == echo_response.checksum and echo_request.payload == echo_response.decoded_payload)