Home | History | Annotate | Download | only in testserver
      1 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Provides utility functions for TCP/UDP echo servers and clients.
      6 
      7 This program has classes and functions to encode, decode, calculate checksum
      8 and verify the "echo request" and "echo response" messages. "echo request"
      9 message is an echo message sent from the client to the server. "echo response"
     10 message is a response from the server to the "echo request" message from the
     11 client.
     12 
     13 The format of "echo request" message is
     14 <version><checksum><payload_size><payload>. <version> is the version number
     15 of the "echo request" protocol. <checksum> is the checksum of the <payload>.
     16 <payload_size> is the size of the <payload>. <payload> is the echo message.
     17 
     18 The format of "echo response" message is
     19 <version><checksum><payload_size><key><encoded_payload>.<version>,
     20 <checksum> and <payload_size> are same as what is in the "echo request" message.
     21 <encoded_payload> is encoded version of the <payload>. <key> is a randomly
     22 generated key that is used to encode/decode the <payload>.
     23 """
     24 
     25 __author__ = 'rtenneti (at] google.com (Raman Tenneti)'
     26 
     27 
     28 from itertools import cycle
     29 from itertools import izip
     30 import random
     31 
     32 
     33 class EchoHeader(object):
     34   """Class to keep header info of the EchoRequest and EchoResponse messages.
     35 
     36   This class knows how to parse the checksum, payload_size from the
     37   "echo request" and "echo response" messages. It holds the checksum,
     38   payload_size of the "echo request" and "echo response" messages.
     39   """
     40 
     41   # This specifies the version.
     42   VERSION_STRING = '01'
     43 
     44   # This specifies the starting position of the checksum and length of the
     45   # checksum. Maximum value for the checksum is less than (2 ** 31 - 1).
     46   CHECKSUM_START = 2
     47   CHECKSUM_LENGTH = 10
     48   CHECKSUM_FORMAT = '%010d'
     49   CHECKSUM_END = CHECKSUM_START + CHECKSUM_LENGTH
     50 
     51   # This specifies the starting position of the <payload_size> and length of the
     52   # <payload_size>. Maximum number of bytes that can be sent in the <payload> is
     53   # 9,999,999.
     54   PAYLOAD_SIZE_START = CHECKSUM_END
     55   PAYLOAD_SIZE_LENGTH = 7
     56   PAYLOAD_SIZE_FORMAT = '%07d'
     57   PAYLOAD_SIZE_END = PAYLOAD_SIZE_START + PAYLOAD_SIZE_LENGTH
     58 
     59   def __init__(self, checksum=0, payload_size=0):
     60     """Initializes the checksum and payload_size of self (EchoHeader).
     61 
     62     Args:
     63       checksum: (int)
     64         The checksum of the payload.
     65       payload_size: (int)
     66         The size of the payload.
     67     """
     68     self.checksum = checksum
     69     self.payload_size = payload_size
     70 
     71   def ParseAndInitialize(self, echo_message):
     72     """Parses the echo_message and initializes self with the parsed data.
     73 
     74     This method extracts checksum, and payload_size from the echo_message
     75     (echo_message could be either echo_request or echo_response messages) and
     76     initializes self (EchoHeader) with checksum and payload_size.
     77 
     78     Args:
     79       echo_message: (string)
     80         The string representation of EchoRequest or EchoResponse objects.
     81     Raises:
     82       ValueError: Invalid data
     83     """
     84     if not echo_message or len(echo_message) < EchoHeader.PAYLOAD_SIZE_END:
     85       raise ValueError('Invalid data:%s' % echo_message)
     86     self.checksum = int(echo_message[
     87         EchoHeader.CHECKSUM_START:EchoHeader.CHECKSUM_END])
     88     self.payload_size = int(echo_message[
     89         EchoHeader.PAYLOAD_SIZE_START:EchoHeader.PAYLOAD_SIZE_END])
     90 
     91   def InitializeFromPayload(self, payload):
     92     """Initializes the EchoHeader object with the payload.
     93 
     94     It calculates checksum for the payload and initializes self (EchoHeader)
     95     with the calculated checksum and size of the payload.
     96 
     97     This method is used by the client code during testing.
     98 
     99     Args:
    100       payload: (string)
    101         The payload is the echo string (like 'hello').
    102     Raises:
    103       ValueError: Invalid data
    104     """
    105     if not payload:
    106       raise ValueError('Invalid data:%s' % payload)
    107     self.payload_size = len(payload)
    108     self.checksum = Checksum(payload, self.payload_size)
    109 
    110   def __str__(self):
    111     """String representation of the self (EchoHeader).
    112 
    113     Returns:
    114       A string representation of self (EchoHeader).
    115     """
    116     checksum_string = EchoHeader.CHECKSUM_FORMAT % self.checksum
    117     payload_size_string = EchoHeader.PAYLOAD_SIZE_FORMAT % self.payload_size
    118     return EchoHeader.VERSION_STRING + checksum_string + payload_size_string
    119 
    120 
    121 class EchoRequest(EchoHeader):
    122   """Class holds data specific to the "echo request" message.
    123 
    124   This class holds the payload extracted from the "echo request" message.
    125   """
    126 
    127   # This specifies the starting position of the <payload>.
    128   PAYLOAD_START = EchoHeader.PAYLOAD_SIZE_END
    129 
    130   def __init__(self):
    131     """Initializes EchoRequest object."""
    132     EchoHeader.__init__(self)
    133     self.payload = ''
    134 
    135   def ParseAndInitialize(self, echo_request_data):
    136     """Parses and Initializes the EchoRequest object from the echo_request_data.
    137 
    138     This method extracts the header information (checksum and payload_size) and
    139     payload from echo_request_data.
    140 
    141     Args:
    142       echo_request_data: (string)
    143         The string representation of EchoRequest object.
    144     Raises:
    145       ValueError: Invalid data
    146     """
    147     EchoHeader.ParseAndInitialize(self, echo_request_data)
    148     if len(echo_request_data) <= EchoRequest.PAYLOAD_START:
    149       raise ValueError('Invalid data:%s' % echo_request_data)
    150     self.payload = echo_request_data[EchoRequest.PAYLOAD_START:]
    151 
    152   def InitializeFromPayload(self, payload):
    153     """Initializes the EchoRequest object with payload.
    154 
    155     It calculates checksum for the payload and initializes self (EchoRequest)
    156     object.
    157 
    158     Args:
    159       payload: (string)
    160         The payload string for which "echo request" needs to be constructed.
    161     """
    162     EchoHeader.InitializeFromPayload(self, payload)
    163     self.payload = payload
    164 
    165   def __str__(self):
    166     """String representation of the self (EchoRequest).
    167 
    168     Returns:
    169       A string representation of self (EchoRequest).
    170     """
    171     return EchoHeader.__str__(self) + self.payload
    172 
    173 
    174 class EchoResponse(EchoHeader):
    175   """Class holds data specific to the "echo response" message.
    176 
    177   This class knows how to parse the "echo response" message. This class holds
    178   key, encoded_payload and decoded_payload of the "echo response" message.
    179   """
    180 
    181   # This specifies the starting position of the |key_| and length of the |key_|.
    182   # Minimum and maximum values for the |key_| are 100,000 and 999,999.
    183   KEY_START = EchoHeader.PAYLOAD_SIZE_END
    184   KEY_LENGTH = 6
    185   KEY_FORMAT = '%06d'
    186   KEY_END = KEY_START + KEY_LENGTH
    187   KEY_MIN_VALUE = 0
    188   KEY_MAX_VALUE = 999999
    189 
    190   # This specifies the starting position of the <encoded_payload> and length
    191   # of the <encoded_payload>.
    192   ENCODED_PAYLOAD_START = KEY_END
    193 
    194   def __init__(self, key='', encoded_payload='', decoded_payload=''):
    195     """Initializes the EchoResponse object."""
    196     EchoHeader.__init__(self)
    197     self.key = key
    198     self.encoded_payload = encoded_payload
    199     self.decoded_payload = decoded_payload
    200 
    201   def ParseAndInitialize(self, echo_response_data=None):
    202     """Parses and Initializes the EchoResponse object from echo_response_data.
    203 
    204     This method calls EchoHeader to extract header information from the
    205     echo_response_data and it then extracts key and encoded_payload from the
    206     echo_response_data. It holds the decoded payload of the encoded_payload.
    207 
    208     Args:
    209       echo_response_data: (string)
    210         The string representation of EchoResponse object.
    211     Raises:
    212       ValueError: Invalid echo_request_data
    213     """
    214     EchoHeader.ParseAndInitialize(self, echo_response_data)
    215     if len(echo_response_data) <= EchoResponse.ENCODED_PAYLOAD_START:
    216       raise ValueError('Invalid echo_response_data:%s' % echo_response_data)
    217     self.key = echo_response_data[EchoResponse.KEY_START:EchoResponse.KEY_END]
    218     self.encoded_payload = echo_response_data[
    219         EchoResponse.ENCODED_PAYLOAD_START:]
    220     self.decoded_payload = Crypt(self.encoded_payload, self.key)
    221 
    222   def InitializeFromEchoRequest(self, echo_request):
    223     """Initializes EchoResponse with the data from the echo_request object.
    224 
    225     It gets the checksum, payload_size and payload from the echo_request object
    226     and then encodes the payload with a random key. It also saves the payload
    227     as decoded_payload.
    228 
    229     Args:
    230       echo_request: (EchoRequest)
    231         The EchoRequest object which has "echo request" message.
    232     """
    233     self.checksum = echo_request.checksum
    234     self.payload_size = echo_request.payload_size
    235     self.key = (EchoResponse.KEY_FORMAT %
    236                 random.randrange(EchoResponse.KEY_MIN_VALUE,
    237                                  EchoResponse.KEY_MAX_VALUE))
    238     self.encoded_payload = Crypt(echo_request.payload, self.key)
    239     self.decoded_payload = echo_request.payload
    240 
    241   def __str__(self):
    242     """String representation of the self (EchoResponse).
    243 
    244     Returns:
    245       A string representation of self (EchoResponse).
    246     """
    247     return EchoHeader.__str__(self) + self.key + self.encoded_payload
    248 
    249 
    250 def Crypt(payload, key):
    251   """Encodes/decodes the payload with the key and returns encoded payload.
    252 
    253   This method loops through the payload and XORs each byte with the key.
    254 
    255   Args:
    256     payload: (string)
    257       The string to be encoded/decoded.
    258     key: (string)
    259       The key used to encode/decode the payload.
    260 
    261   Returns:
    262     An encoded/decoded string.
    263   """
    264   return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in izip(payload, cycle(key)))
    265 
    266 
    267 def Checksum(payload, payload_size):
    268   """Calculates the checksum of the payload.
    269 
    270   Args:
    271     payload: (string)
    272       The payload string for which checksum needs to be calculated.
    273     payload_size: (int)
    274       The number of bytes in the payload.
    275 
    276   Returns:
    277     The checksum of the payload.
    278   """
    279   checksum = 0
    280   length = min(payload_size, len(payload))
    281   for i in range (0, length):
    282     checksum += ord(payload[i])
    283   return checksum
    284 
    285 
    286 def GetEchoRequestData(payload):
    287   """Constructs an "echo request" message from the payload.
    288 
    289   It builds an EchoRequest object from the payload and then returns a string
    290   representation of the EchoRequest object.
    291 
    292   This is used by the TCP/UDP echo clients to build the "echo request" message.
    293 
    294   Args:
    295     payload: (string)
    296       The payload string for which "echo request" needs to be constructed.
    297 
    298   Returns:
    299     A string representation of the EchoRequest object.
    300   Raises:
    301     ValueError: Invalid payload
    302   """
    303   try:
    304     echo_request = EchoRequest()
    305     echo_request.InitializeFromPayload(payload)
    306     return str(echo_request)
    307   except (IndexError, ValueError):
    308     raise ValueError('Invalid payload:%s' % payload)
    309 
    310 
    311 def GetEchoResponseData(echo_request_data):
    312   """Verifies the echo_request_data and returns "echo response" message.
    313 
    314   It builds the EchoRequest object from the echo_request_data and then verifies
    315   the checksum of the EchoRequest is same as the calculated checksum of the
    316   payload. If the checksums don't match then it returns None. It checksums
    317   match, it builds the echo_response object from echo_request object and returns
    318   string representation of the EchoResponse object.
    319 
    320   This is used by the TCP/UDP echo servers.
    321 
    322   Args:
    323     echo_request_data: (string)
    324       The string that echo servers send to the clients.
    325 
    326   Returns:
    327     A string representation of the EchoResponse object. It returns None if the
    328     echo_request_data is not valid.
    329   Raises:
    330     ValueError: Invalid echo_request_data
    331   """
    332   try:
    333     if not echo_request_data:
    334       raise ValueError('Invalid payload:%s' % echo_request_data)
    335 
    336     echo_request = EchoRequest()
    337     echo_request.ParseAndInitialize(echo_request_data)
    338 
    339     if Checksum(echo_request.payload,
    340                 echo_request.payload_size) != echo_request.checksum:
    341       return None
    342 
    343     echo_response = EchoResponse()
    344     echo_response.InitializeFromEchoRequest(echo_request)
    345 
    346     return str(echo_response)
    347   except (IndexError, ValueError):
    348     raise ValueError('Invalid payload:%s' % echo_request_data)
    349 
    350 
    351 def DecodeAndVerify(echo_request_data, echo_response_data):
    352   """Decodes and verifies the echo_response_data.
    353 
    354   It builds EchoRequest and EchoResponse objects from the echo_request_data and
    355   echo_response_data. It returns True if the EchoResponse's payload and
    356   checksum match EchoRequest's.
    357 
    358   This is used by the TCP/UDP echo clients for testing purposes.
    359 
    360   Args:
    361     echo_request_data: (string)
    362       The request clients sent to echo servers.
    363     echo_response_data: (string)
    364       The response clients received from the echo servers.
    365 
    366   Returns:
    367     True if echo_request_data and echo_response_data match.
    368   Raises:
    369     ValueError: Invalid echo_request_data or Invalid echo_response
    370   """
    371 
    372   try:
    373     echo_request = EchoRequest()
    374     echo_request.ParseAndInitialize(echo_request_data)
    375   except (IndexError, ValueError):
    376     raise ValueError('Invalid echo_request:%s' % echo_request_data)
    377 
    378   try:
    379     echo_response = EchoResponse()
    380     echo_response.ParseAndInitialize(echo_response_data)
    381   except (IndexError, ValueError):
    382     raise ValueError('Invalid echo_response:%s' % echo_response_data)
    383 
    384   return (echo_request.checksum == echo_response.checksum and
    385           echo_request.payload == echo_response.decoded_payload)
    386