# 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
<version><checksum><payload_size><payload>. <version> is the version number
of the "echo request" protocol. <checksum> is the checksum of the <payload>.
<payload_size> is the size of the <payload>. <payload> is the echo message.
The format of "echo response" message is
<version><checksum><payload_size><key><encoded_payload>.<version>,
<checksum> and <payload_size> are same as what is in the "echo request" message.
<encoded_payload> is encoded version of the <payload>. <key> is a randomly
generated key that is used to encode/decode the <payload>.
"""
__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 <payload_size> and length of the
# <payload_size>. Maximum number of bytes that can be sent in the <payload> 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>.
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 <encoded_payload> and length
# of the <encoded_payload>.
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)