#!/usr/bin/env python


class RFC7541:
   # define static table
   static_table = [
      [None, None],
      [":authority", None],
      [":method", "GET"],
      [":method", "POST"],
      [":path", "/"],
      [":path", "/index.html"],
      [":scheme", "http"],
      [":scheme", "https"],
      [":status", 200],
      [":status", 204],
      [":status", 206],
      [":status", 304],
      [":status", 400],
      [":status", 404],
      [":status", 500],
      ["accept-charset", None],
      ["accept-encoding", "gzip, deflate"],
      ["accept-language", None],
      ["accept-ranges", None],
      ["accept", None],
      ["access-control-allow-origin", None],
      ["age", None],
      ["allow", None],
      ["authorization", None],
      ["cache-control", None],
      ["content-disposition", None],
      ["content-encoding", None],
      ["content-language", None],
      ["content-length", None],
      ["content-location", None],
      ["content-range", None],
      ["content-type", None],
      ["cookie", None],
      ["date", None],
      ["etag", None],
      ["expect", None],
      ["expires", None],
      ["from", None],
      ["host", None],
      ["if-match", None],
      ["if-modified-since", None],
      ["if-none-match", None],
      ["if-range", None],
      ["if-unmodified-since", None],
      ["last-modified", None],
      ["link", None],
      ["location", None],
      ["max-forwards", None],
      ["proxy-authenticate", None],
      ["proxy-authorization", None],
      ["range", None],
      ["referer", None],
      ["refresh", None],
      ["retry-after", None],
      ["server", None],
      ["set-cookie", None],
      ["strict-transport-security", None],
      ["transfer-encoding", None],
      ["user-agent", None],
      ["vary", None],
      ["via", None],
      ["www-authenticate", None]
   ]

   def __init__(self, maxsize=256):    # default maximum defined in RFC7541 C.6
      self.statictable = RFC7541.static_table
      self.staticlen = len(self.statictable)
      self.dyntable = []
      self.dyntable_max = maxsize

   @property
   def len(self):
      length = 0
      for entry in self.dyntable:
         length += len(entry[0]) + len(entry[1]) + 32 # as defined in RFC7541 sect 4.1
      return length

   def insert(self, key, value):
      length = len(key) + len(value) + 32 # as defined in RFC7541 sect 4.1
      if (self.len + length > self.dyntable_max):
         self.evict(length)
      self.dyntable = [[key,value]] + self.dyntable

   def evict(self, length):
      while (self.len + length > self.dyntable_max):
         self.dyntable.pop()

   def lookup(self, idx):
      if (idx < self.staticlen): 
         return self.statictable[idx]
      else:
         idx -= self.staticlen
         return self.dyntable[idx]

   def update_max(self, newmax):
      if (newmax > 4096): # max table size for HTTP/2 defined in RFC7540 sect 11.3
         print("Error: New maximum exceeds the maximum of the protocol")
      self.dyntable_max = newmax
      self.evict(0)


huffman_code = [
#  (code,  bitmask-len,  character )
   (   0x14,  6, ' '),
   (  0x3f8, 10, '!'),
   (  0x3f9, 10, '"'),
   (  0xffa, 12, '#'),
   ( 0x1ff9, 13, '$'),
   (   0x15,  6, '%'),
   (   0xf8,  8, '&'),
   (  0x7fa, 11, '\''),
   (  0x3fa, 10, '('),
   (  0x3fb, 10, ')'),
   (   0xf9,  8, '*'),
   (  0x7fb, 11, '+'),
   (   0xfa,  8, ','),
   (   0x16,  6, '-'),
   (   0x17,  6, '.'),
   (   0x18,  6, '/'),
   (    0x0,  5, '0'),
   (    0x1,  5, '1'),
   (    0x2,  5, '2'),
   (   0x19,  6, '3'),
   (   0x1a,  6, '4'),
   (   0x1b,  6, '5'),
   (   0x1c,  6, '6'),
   (   0x1d,  6, '7'),
   (   0x1e,  6, '8'),
   (   0x1f,  6, '9'),
   (   0x5c,  7, ':'),
   (   0xfb,  8, ';'),
   ( 0x7ffc, 15, '<'),
   (   0x20,  6, '='),
   (  0xffb, 12, '>'),
   (  0x3fc, 10, '?'),
   ( 0x1ffa, 13, '@'),
   (   0x21,  6, 'A'),
   (   0x5d,  7, 'B'),
   (   0x5e,  7, 'C'),
   (   0x5f,  7, 'D'),
   (   0x60,  7, 'E'),
   (   0x61,  7, 'F'),
   (   0x62,  7, 'G'),
   (   0x63,  7, 'H'),
   (   0x64,  7, 'I'),
   (   0x65,  7, 'J'),
   (   0x66,  7, 'K'),
   (   0x67,  7, 'L'),
   (   0x68,  7, 'M'),
   (   0x69,  7, 'N'),
   (   0x6a,  7, 'O'),
   (   0x6b,  7, 'P'),
   (   0x6c,  7, 'Q'),
   (   0x6d,  7, 'R'),
   (   0x6e,  7, 'S'),
   (   0x6f,  7, 'T'),
   (   0x70,  7, 'U'),
   (   0x71,  7, 'V'),
   (   0x72,  7, 'W'),
   (   0xfc,  8, 'X'),
   (   0x73,  7, 'Y'),
   (   0xfd,  8, 'Z'),
   ( 0x1ffb, 13, '['),
   (0x7fff0, 19, '\\'),
   ( 0x1ffc, 13, ']'),
   ( 0x3ffc, 14, '^'),
   (   0x22,  6, '_'),
   ( 0x7ffd, 15, '`'),
   (    0x3,  5, 'a'),
   (   0x23,  6, 'b'),
   (    0x4,  5, 'c'),
   (   0x24,  6, 'd'),
   (    0x5,  5, 'e'),
   (   0x25,  6, 'f'),
   (   0x26,  6, 'g'),
   (   0x27,  6, 'h'),
   (    0x6,  5, 'i'),
   (   0x74,  7, 'j'),
   (   0x75,  7, 'k'),
   (   0x28,  6, 'l'),
   (   0x29,  6, 'm'),
   (   0x2a,  6, 'n'),
   (    0x7,  5, 'o'),
   (   0x2b,  6, 'p'),
   (   0x76,  7, 'q'),
   (   0x2c,  6, 'r'),
   (    0x8,  5, 's'),
   (    0x9,  5, 't'),
   (   0x2d,  6, 'u'),
   (   0x77,  7, 'v'),
   (   0x78,  7, 'w'),
   (   0x79,  7, 'x'),
   (   0x7a,  7, 'y'),
   (   0x7b,  7, 'z'),
   ( 0x7ffe, 15, '}'),
   (  0x7fc, 11, '|'),
   ( 0x3ffd, 14, '}'),
   ( 0x1ffd, 13, '~')
]


def parse_type_byte(octet):
   code , idx = 0, 0

#
# Header name and value indexed in static or dynamic table:
#
#      0   1   2   3   4   5   6   7
#      +---+---+---+---+---+---+---+---+
#      | 1 |        Index (7+)         |
#      +---+---------------------------+
   if (octet & 128 == 0x80):
      code, idx = octet & 128, octet & 127
   
#
# Header name indexed in static or dynamic table:
#
#      0   1   2   3   4   5   6   7
#      +---+---+---+---+---+---+---+---+
#      | 0 | 1 |      Index (6+)       |
#      +---+---+-----------------------+
#      | H |     Value Length (7+)     |
#      +---+---------------------------+
#      | Value String (Length octets)  |
#      +-------------------------------+
   elif (octet & 192 == 0x40):
      code, idx = octet & 192, octet & 191

# Header name indexed in static table but dynamic table remains unchanged
#
#      0   1   2   3   4   5   6   7      0   1   2   3   4   5   6   7
#      +---+---+---+---+---+---+---+---+  +---+---+---+---+---+---+---+---+
#      | 0 | 0 | 0 | 0 |  Index (4+)   |  | 0 | 0 | 0 | 1 |  Index (4+)   |
#      +---+---+-----------------------+  +---+---+-----------------------+
#      | H |     Value Length (7+)     |  | H |     Value Length (7+)     |
#      +---+---------------------------+  +---+---------------------------+
#      | Value String (Length octets)  |  | Value String (Length octets)  |
#      +-------------------------------+  +-------------------------------+
   elif (octet & 0xf0 in (0,0x10)):
      code, idx = octet & 0xf0, octet & 0x0f

# Dynamic RFC7541 Size Update
#
#      0   1   2   3   4   5   6   7
#      +---+---+---+---+---+---+---+---+
#      | 0 | 0 | 1 |   Max size (5+)   |
#      +---+---------------------------+
   elif (octet & 224 == 0x20):
      code, idx = octet & 224, octet & 223


   return code, idx


def parse_length_byte(octet):
   huffman_bit = (octet & 128) >> 7
   length = octet & 127
   return huffman_bit, length

def bits2bytes(bits):
   if (bits%8):
      return int((bits/8)+1)
   else:
      return int(bits/8)

def bits2mask(bits):
   return ((2**bits)-1)

def bytearray2int(array):
   integer = 0
   for i in array:
      integer <<= 8
      integer |= i & 0xff
   return integer

def int2binarystring(integer):
   return "{0:b}".format(integer)

def decode_plain(encoded):
   decoded = ""
   while (encoded):
      decoded += chr(encoded.pop(0))
   return decoded

def decode_huffman(encoded):
   decoded = ""
   carry = [0,0]
   # as long as we have data to decode or the EOS (padding) not set
   while (encoded or carry[0] != bits2mask(carry[1])):
      dlen = len(decoded)
      for he in huffman_code:
         bits_remain = he[1]-carry[1]
         if (bits_remain < 0):
            bits_remain = 0
         length = bits2bytes(bits_remain)
         # set leftover from last turn
         chunk = carry[0] << bits_remain
         # align carry if necessary
         if (carry[1] > he[1]):
            chunk >>= (carry[1]-he[1])
         # get required bits from stack for bits remaining
         chunk |= bytearray2int(encoded[0:length]) >> ((length*8)-bits_remain)
         if (chunk == he[0]):
            decoded += he[2]
            if (carry[1] > he[1]):
               # if we still have more bits in carry than we consumed
               bitmask = bits2mask(carry[1]-he[1])
               carry[0] &= bitmask
               carry[1] = carry[1]-he[1]
            else:
               # save remaining bits for next turn
               bitmask = bits2mask((length*8)-bits_remain)
               carry[0] = bytearray2int(encoded[0:length]) & bitmask
               carry[1] = (length*8)-bits_remain
            # delete decoded bytes
            encoded[0:length] = []
            break
      # check if decode was successful
      if (dlen==len(decoded)):
         print("Decoding error")
         return decoded

   if(carry[1]):
      print("%d bits padding" % carry[1])
   return decoded

def bin2hex(byte_array):
   return ''.join(format(x, '02x') for x in byte_array)

def parse_header_string(dyntable, header_string):
   header_list = []
   header_bytes = bytearray.fromhex(header_string)

   while (header_bytes):
      byte = header_bytes.pop(0)
      code, h_idx = parse_type_byte(byte)
      print("type byte (0x%02x): code: 0x%02x, idx: %d" % (byte, code, h_idx))

      if (code == 0x80):
         header_list.append(dyntable.lookup(h_idx))
         print("index lookup: '%s' => '%s'" % (dyntable.lookup(h_idx)[0], dyntable.lookup(h_idx)[1]))

      elif (code  in (0x40, 0x10, 0x00)):
         name_string = value_string = ""
         if (h_idx == 0):
            byte = header_bytes.pop(0)
            # name not indexed - decode name first
            huff, length = parse_length_byte(byte)
            print("length byte (0x%02x): huffman %d, length: %d" % (byte, huff, length))

            # slice encoded name bytes
            name_bytes = header_bytes[0:length]

            # forward byte array
            header_bytes = header_bytes[length:]

            print("decoding " + bin2hex(name_bytes))
            # decode
            if (huff):
               name_string = decode_huffman(name_bytes)
            else:
               name_string = decode_plain(name_bytes)

            print("name decoded to: %s" % name_string)

         else:
            name_string = dyntable.lookup(h_idx)[0]
            print("index lookup: '%s', value to be decoded..." % dyntable.lookup(h_idx)[0])

         byte = header_bytes.pop(0)
         huff, length = parse_length_byte(byte)
         print("length byte (0x%02x): huffman %d, length: %d" % (byte, huff, length))

         # slice encoded value bytes
         value_bytes = header_bytes[0:length]
         value_string = ""

         # forward byte array
         header_bytes = header_bytes[length:]
         
         print("decoding " + bin2hex(value_bytes))

         # decode
         if (huff):
            value_string = decode_huffman(value_bytes)
         else:
            value_string = decode_plain(value_bytes)

         print("value decoded to: %s" % value_string)
         # add to header list
         header_list.append([name_string, value_string])
         # add to index if allowed
         if (code == 0x40):
            dyntable.insert(name_string, value_string)


      elif (code == 0x20):
         print("update new dyntable maxsize to %d" % h_idx)
         dyntable.update_max(h_idx)

   return header_list
               




table = RFC7541()



# Example no Huffmann w/ dynamic indexing 3 consecutive requests
# 1. request
input = "828684410f7777772e6578616d706c652e636f6d"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: http, :path: /, :authority: www.example.com")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")


# 2. request
input = "828684be58086e6f2d6361636865"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: http, :path: /, :authority: www.example.com, cache-control: no-cache")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")



# 3. request
input = "828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: https, :path: /index.html, :authority: www.example.com, custom-key: custom-value")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# Reset dynamic table
table = RFC7541()



# 1. request with Huffman
input = "828684418cf1e3c2e5f23a6ba0ab90f4ff"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: http, :path: /, :authority: www.example.com")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# 2. request with Huffman
input = "828684be5886a8eb10649cbf"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: http, :path: /, :authority: www.example.com, cache-control: no-cache")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")


# 3. request with Huffman
input = "828785bf408825a849e95ba97d7f8925a849e95bb8e8b4bf"
print("input: " + input)
print(parse_header_string(table, input))
print("should be :method: GET, :scheme: http, :path: /, :authority: www.example.com, custom-key: custom-value")
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# Reset dynamic table
table = RFC7541()



# 1. response with Huffman
input = "488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3"
print("input: " + input)
print(parse_header_string(table, input))
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# 2. response with Huffman
input = "4883640effc1c0bf"
print("input: " + input)
print(parse_header_string(table, input))
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# 3. response with Huffman
input = "88c16196d07abe941054d444a8200595040b8166e084a62d1bffc05a839bd9ab77ad94e7821dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab270fb5291f9587316065c003ed4ee5b1063d5007"
print("input: " + input)
print(parse_header_string(table, input))
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# Reset dynamic table
table = RFC7541()
print "===================================="
print " R E A L   L I F E   E X A M P L E  "
print "      ( http2-get-2.pcapng )        "
print "===================================="

# 1. Request
input = "828486418dea72d75957b0e4ea8b8f01e07f7a8825b650c3abb625c353032a2f2a"
print("input: " + input)
print(parse_header_string(table, input))
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")

# 2. Response
input = "887689aa6355e580ae102ecf6196d07abe940bea693f750400bca05fb8d37702d298b46f5f87497ca589d34d1f5c0532373939356c96dd6d5f4a32053716b50400bca05eb8cbf700253168df00832a47378cfe5b8e3085e69c59c91b8ff9008919085ad2b583aa62a3848fd24a8f"
print("input: " + input)
print(parse_header_string(table, input))
print("dynamic table (size %d): " % table.len)
print(table.dyntable)
print("")


