def get_frame_bin(self): # Encode header encoder = Encoder() headers_frame_field = bytearray() if self._data is None: # if user didn't touch data self._data = encoder.encode(self._header_list) # encode header list self._flag = self.flag # get flag by method if self._is_padded: headers_frame_field.append(self._pad_len) # append pad length self._data += bytearray(self._pad_len) # append pad byte in pad length if self._is_priority: headers_frame_field += int_to_bytes(self._dependency_id, 4) # append dependency stream id headers_frame_field.append(self._weight) self._data = headers_frame_field + self._data # append HEADERS field return Frame.get_frame_bin(self)
def test_resizing_header_table(self): # We need to encode a substantial number of headers, to populate the # header table. e = Encoder() header_set = [ (':method', 'GET'), (':scheme', 'https'), (':path', '/some/path'), (':authority', 'www.example.com'), ('custom-key', 'custom-value'), ( "user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) " "Gecko/20100101 Firefox/16.0", ), ( "accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;" "q=0.8", ), ('X-Lukasa-Test', '88989'), ] e.encode(header_set, huffman=True) # Resize the header table to a size so small that nothing can be in it. e.header_table_size = 40 assert len(e.header_table.dynamic_entries) == 0
def test_ordering_applies_to_encoding(self, special_keys, boring_keys): """ When encoding a dictionary the special keys all appear first. """ def _prepend_colon(k): if isinstance(k, unicode): return u':' + k else: return b':' + k special_keys = set(map(_prepend_colon, special_keys)) input_dict = { k: b'testval' for k in itertools.chain(special_keys, boring_keys) } e = Encoder() d = Decoder() encoded = e.encode(input_dict) decoded = iter(d.decode(encoded, raw=True)) received_special = set() received_boring = set() expected_special = set(map(_to_bytes, special_keys)) expected_boring = set(map(_to_bytes, boring_keys)) for _ in special_keys: k, _ = next(decoded) received_special.add(k) for _ in boring_keys: k, _ = next(decoded) received_boring.add(k) assert expected_special == received_special assert expected_boring == received_boring
def get_encoder(self): """ Returns a HPACK encoder set up for responses. """ e = Encoder() e.huffman_coder = HuffmanEncoder(REQUEST_CODES, REQUEST_CODES_LENGTH) return e
def build_headers_frame(headers, encoder=None): f = HeadersFrame(1) e = encoder if e is None: e = Encoder() e.huffman_coder = HuffmanEncoder(REQUEST_CODES, REQUEST_CODES_LENGTH) f.data = e.encode(headers) f.flags.add('END_HEADERS') return f
def test_evicting_header_table_objects(self): e = Encoder() # Set the header table size large enough to include one header. e.header_table_size = 66 header_set = [('a', 'b'), ('long-custom-header', 'longish value')] e.encode(header_set) assert len(e.header_table.dynamic_entries) == 1
def test_request_examples_with_huffman(self): """ This section shows the same examples as the previous section, but using Huffman encoding for the literal values. """ e = Encoder() first_header_set = [ (':method', 'GET',), (':scheme', 'http',), (':path', '/',), (':authority', 'www.example.com'), ] first_header_table = [(':authority', 'www.example.com')] first_result = ( b'\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff' ) assert e.encode(first_header_set, huffman=True) == first_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in first_header_table ] second_header_table = [ ('cache-control', 'no-cache'), (':authority', 'www.example.com') ] second_header_set = [ (':method', 'GET',), (':scheme', 'http',), (':path', '/',), (':authority', 'www.example.com',), ('cache-control', 'no-cache'), ] second_result = b'\x82\x86\x84\xbeX\x86\xa8\xeb\x10d\x9c\xbf' assert e.encode(second_header_set, huffman=True) == second_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in second_header_table ] third_header_set = [ (':method', 'GET',), (':scheme', 'https',), (':path', '/index.html',), (':authority', 'www.example.com',), ('custom-key', 'custom-value'), ] third_result = ( b'\x82\x87\x85\xbf' b'@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf' ) assert e.encode(third_header_set, huffman=True) == third_result assert len(e.header_table.dynamic_entries) == 3
def test_request_examples_without_huffman(self): """ This section shows several consecutive header sets, corresponding to HTTP requests, on the same connection. """ e = Encoder() first_header_set = [ (':method', 'GET',), (':scheme', 'http',), (':path', '/',), (':authority', 'www.example.com'), ] # We should have :authority in first_header_table since we index it first_header_table = [(':authority', 'www.example.com')] first_result = b'\x82\x86\x84\x41\x0fwww.example.com' assert e.encode(first_header_set, huffman=False) == first_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in first_header_table ] second_header_set = [ (':method', 'GET',), (':scheme', 'http',), (':path', '/',), (':authority', 'www.example.com',), ('cache-control', 'no-cache'), ] second_header_table = [ ('cache-control', 'no-cache'), (':authority', 'www.example.com') ] second_result = b'\x82\x86\x84\xbeX\x08no-cache' assert e.encode(second_header_set, huffman=False) == second_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in second_header_table ] third_header_set = [ (':method', 'GET',), (':scheme', 'https',), (':path', '/index.html',), (':authority', 'www.example.com',), ('custom-key', 'custom-value'), ] third_result = ( b'\x82\x87\x85\xbf@\ncustom-key\x0ccustom-value' ) assert e.encode(third_header_set, huffman=False) == third_result # Don't check the header table here, it's just too complex to be # reliable. Check its length though. assert len(e.header_table.dynamic_entries) == 3
def test_resizing_header_table_sends_multiple_updates(self): e = Encoder() e.header_table_size = 40 e.header_table_size = 100 e.header_table_size = 40 header_set = [(':method', 'GET')] out = e.encode(header_set, huffman=True) assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
def test_indexed_header_field_from_static_table(self): e = Encoder() e.header_table_size = 0 header_set = {':method': 'GET'} result = b'\x82' # Make sure we don't emit an encoding context update. e.header_table.resized = False assert e.encode(header_set, huffman=False) == result assert list(e.header_table.dynamic_entries) == []
def test_literal_header_field_without_indexing(self): """ The header field representation uses an indexed name and a literal value. """ e = Encoder() header_set = {':path': '/sample/path'} result = b'\x04\x0c/sample/path' assert e.encode(header_set, huffman=False) == result assert list(e.header_table) == []
def test_indexed_header_field(self): """ The header field representation uses an indexed header field, from the static table. """ e = Encoder() header_set = {':method': 'GET'} result = b'\x82' assert e.encode(header_set, huffman=False) == result assert list(e.header_table.dynamic_entries) == []
def test_setting_table_size_to_the_same_does_nothing(self): e = Encoder() # Set the header table size to the default. e.header_table_size = 4096 # Now encode a header set. Just a small one, with a well-defined # output. header_set = [(':method', 'GET')] out = e.encode(header_set, huffman=True) assert out == b'\x82'
def test_literal_header_field_with_indexing(self): """ The header field representation uses a literal name and a literal value. """ e = Encoder() header_set = {'custom-key': 'custom-header'} result = b'\x40\x0acustom-key\x0dcustom-header' assert e.encode(header_set, huffman=False) == result assert list(e.header_table) == [(n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set.items()]
def test_resizing_header_table_sends_context_update(self): e = Encoder() # Resize the header table to a size so small that nothing can be in it. e.header_table_size = 40 # Now, encode a header set. Just a small one, with a well-defined # output. header_set = [(':method', 'GET')] out = e.encode(header_set, huffman=True) assert out == b'?\t\x82'
def test_can_encode_a_story_with_huffman(self, raw_story): d = Decoder() e = Encoder() for case in raw_story['cases']: # The input headers are a list of dicts, which is annoying. input_headers = [(item[0], item[1]) for header in case['headers'] for item in header.items()] encoded = e.encode(input_headers, huffman=True) decoded_headers = d.decode(encoded) assert input_headers == decoded_headers
def test_indexed_literal_header_field_with_indexing(self): """ The header field representation uses an indexed name and a literal value and performs incremental indexing. """ e = Encoder() header_set = {':path': '/sample/path'} result = b'\x44\x0c/sample/path' assert e.encode(header_set, huffman=False) == result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set.items() ]
def test_literal_header_field_with_indexing(self): """ The header field representation uses a literal name and a literal value. """ e = Encoder() header_set = {'custom-key': 'custom-header'} result = b'\x40\x0acustom-key\x0dcustom-header' assert e.encode(header_set, huffman=False) == result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in header_set.items() ]
def test_sensitive_headers(self): """ Test encoding header values """ e = Encoder() result = (b'\x82\x14\x88\x63\xa1\xa9' + b'\x32\x08\x73\xd0\xc7\x10' + b'\x87\x25\xa8\x49\xe9\xea' + b'\x5f\x5f\x89\x41\x6a\x41' + b'\x92\x6e\xe5\x35\x52\x9f') header_set = [ (':method', 'GET', True), (':path', '/jimiscool/', True), ('customkey', 'sensitiveinfo', True), ] assert e.encode(header_set, huffman=True) == result
def test_sensitive_headers_with_header_tuples(self): """ A header field stored in a NeverIndexedHeaderTuple emits a representation that forbids indexing. """ e = Encoder() result = (b'\x82\x14\x88\x63\xa1\xa9' + b'\x32\x08\x73\xd0\xc7\x10' + b'\x87\x25\xa8\x49\xe9\xea' + b'\x5f\x5f\x89\x41\x6a\x41' + b'\x92\x6e\xe5\x35\x52\x9f') header_set = [ NeverIndexedHeaderTuple(':method', 'GET'), NeverIndexedHeaderTuple(':path', '/jimiscool/'), NeverIndexedHeaderTuple('customkey', 'sensitiveinfo'), ] assert e.encode(header_set, huffman=True) == result
def socket_handler(listener): sock = listener.accept()[0] e = Encoder() # We get two messages for the connection open and then a HEADERS # frame. receive_preamble(sock) sock.recv(65535) # Now, send the headers for the response. f = build_headers_frame([(':status', '200'), ('content-length', '14')], e) f.stream_id = 1 sock.send(f.serialize()) # Also send a data frame. f = DataFrame(1) f.data = b'have some data' sock.send(f.serialize()) # Now, send a headers frame again, containing trailing headers. f = build_headers_frame([('trialing', 'no'), ('trailing', 'sure')], e) f.flags.add('END_STREAM') f.stream_id = 1 sock.send(f.serialize()) # Wait for the message from the main thread. recv_event.set() sock.close()
def test_sensitive_headers(self): """ Test encoding header values """ e = Encoder() result = (b'\x82\x14\x88\x63\xa1\xa9' + b'\x32\x08\x73\xd0\xc7\x10' + b'\x87\x25\xa8\x49\xe9\xea' + b'\x5f\x5f\x89\x41\x6a\x41' + b'\x92\x6e\xe5\x35\x52\x9f') header_set = [ (':method', 'GET', True), (':path', '/jimiscool/', True), ('customkey','sensitiveinfo',True) ] assert e.encode(header_set, huffman=True) == result
def __init__(self, local_settings=None): super(H2Connection, self).__init__() self.encoder = Encoder() self.decoder = Decoder() self.streams = {} self.local_settings = Settings(local_settings) self.remote_settings = Settings() # 接收数据窗口 self.inbound_flow_control_window = self.local_settings.initial_window_size self.inbound_window_manager = WindowManager(self.local_settings.initial_window_size) # 发送数据窗口(根据对端接受能力,取决于对端设置) self.outbound_flow_control_window = self.remote_settings.initial_window_size self.__set_encoder() self.__set_decoder() # 接收数据缓冲区 self.inbound_buffer = FrameBuffer(self.local_settings.max_frame_size) # 待发送的数据缓冲区 self._data_to_send = b'' self.__dispatch_table = { SettingsFrame: self._receive_settings_frame, HeadersFrame: self._receive_headers_frame, DataFrame: self._receive_data_frame, WindowUpdateFrame: self.receive_window_update_frame, GoAwayFrame: self.receive_goaway_frame, RstStreamFrame: self._receive_rst_stream_frame, PingFrame: self._receive_ping_frame }
def __init__(self, state=None, length=0, flags=FLAG_NO_FLAGS, stream_id=0x0): valid_flags = reduce(lambda x, y: x | y, self.VALID_FLAGS, 0x0) if flags | valid_flags != valid_flags: raise ValueError('invalid flags detected.') if state is None: class State(object): pass state = State() state.http2_settings = HTTP2_DEFAULT_SETTINGS.copy() state.encoder = Encoder() state.decoder = Decoder() self.state = state self.length = length self.type = self.TYPE self.flags = flags self.stream_id = stream_id
def test_can_encode_a_story_no_huffman(self, raw_story): d = Decoder() e = Encoder() for case in raw_story['cases']: # The input headers are a list of dicts, which is annoying. input_headers = [ (item[0], item[1]) for header in case['headers'] for item in header.items() ] encoded = e.encode(input_headers, huffman=False) decoded_headers = d.decode(encoded) assert input_headers == decoded_headers assert all( isinstance(header, HeaderTuple) for header in decoded_headers )
def __init__(self, tcp_handler, is_server=False, dump_frames=False): self.tcp_handler = tcp_handler self.is_server = is_server self.http2_settings = frame.HTTP2_DEFAULT_SETTINGS.copy() self.current_stream_id = None self.encoder = Encoder() self.decoder = Decoder() self.connection_preface_performed = False self.dump_frames = dump_frames
def __init__(self, client_side=True): self.state_machine = H2ConnectionStateMachine() self.streams = {} self.highest_inbound_stream_id = 0 self.highest_outbound_stream_id = 0 self.encoder = Encoder() self.decoder = Decoder() self.client_side = client_side # Objects that store settings, including defaults. self.local_settings = Settings(client=client_side) self.remote_settings = Settings(client=not client_side) # The curent value of the connection flow control windows on the # connection. self.outbound_flow_control_window = ( self.remote_settings.initial_window_size) self.inbound_flow_control_window = ( self.local_settings.initial_window_size) #: The maximum size of a frame that can be emitted by this peer, in #: bytes. self.max_outbound_frame_size = self.remote_settings.max_frame_size #: The maximum size of a frame that can be received by this peer, in #: bytes. self.max_inbound_frame_size = self.local_settings.max_frame_size # Buffer for incoming data. self.incoming_buffer = FrameBuffer(server=not client_side) # A private variable to store a sequence of received header frames # until completion. self._header_frames = [] # Data that needs to be sent. self._data_to_send = b'' # When in doubt use dict-dispatch. self._frame_dispatch_table = { HeadersFrame: self._receive_headers_frame, PushPromiseFrame: self._receive_push_promise_frame, SettingsFrame: self._receive_settings_frame, DataFrame: self._receive_data_frame, WindowUpdateFrame: self._receive_window_update_frame, PingFrame: self._receive_ping_frame, RstStreamFrame: self._receive_rst_stream_frame, PriorityFrame: self._receive_priority_frame, GoAwayFrame: self._receive_goaway_frame, ContinuationFrame: self._receive_naked_continuation, }
def test_ordering_applies_to_encoding(self, special_keys, boring_keys): """ When encoding a dictionary the special keys all appear first. """ def _prepend_colon(k): if isinstance(k, unicode): return u':' + k else: return b':' + k special_keys = set(map(_prepend_colon, special_keys)) input_dict = { k: b'testval' for k in itertools.chain( special_keys, boring_keys ) } e = Encoder() d = Decoder() encoded = e.encode(input_dict) decoded = iter(d.decode(encoded, raw=True)) received_special = set() received_boring = set() expected_special = set(map(_to_bytes, special_keys)) expected_boring = set(map(_to_bytes, boring_keys)) for _ in special_keys: k, _ = next(decoded) received_special.add(k) for _ in boring_keys: k, _ = next(decoded) received_boring.add(k) assert expected_special == received_special assert expected_boring == received_boring
def test_resizing_header_table_to_same_size_ignored(self): e = Encoder() # These size changes should be ignored e.header_table_size = 4096 e.header_table_size = 4096 e.header_table_size = 4096 # These size changes should be encoded e.header_table_size = 40 e.header_table_size = 100 e.header_table_size = 40 header_set = [(':method', 'GET')] out = e.encode(header_set, huffman=True) assert out == b'\x3F\x09\x3F\x45\x3F\x09\x82'
def __init__( self, tcp_handler=None, rfile=None, wfile=None, is_server=False, dump_frames=False, encoder=None, decoder=None, unhandled_frame_cb=None, ): self.tcp_handler = tcp_handler or TCPHandler(rfile, wfile) self.is_server = is_server self.dump_frames = dump_frames self.encoder = encoder or Encoder() self.decoder = decoder or Decoder() self.unhandled_frame_cb = unhandled_frame_cb self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy() self.current_stream_id = None self.connection_preface_performed = False
def test_case19(self): """ http://httpwg.org/specs/rfc7540.html#StreamStates """ print("[ TEST CASE 19 ]----------------------------------") data = self.__read_from_socket() print("< {}".format(self.__parse_client_request(data))) print("> [SETTINGS]") self.send_data([http2.setting()]) data = self.__read_from_socket() print("< {}".format(self.__parse_client_request(data))) print("> [HEADERS]") small_get_index_html_header = { ":method" : "GET", ":scheme" : "http", ":path" : "/index.html", ":authority": "localhost", "host" : "localhost" } header_data = Encoder().encode(small_get_index_html_header, huffman=False) self.send_data([http2.headers(END_HEADERS = True, header_block_fragment = header_data)]) data = self.__read_from_socket() print("< {}".format(self.__parse_client_request(data))) print("> [PUSH_PROMISE]") self.send_data([http2.push_promise(END_HEADERS = False)]) data = self.__read_from_socket() print("< {}".format(self.__parse_client_request(data)))
def refresh_encoder(self): self.encoder = Encoder()
class FrameFactory(object): """ A class containing lots of helper methods and state to build frames. This allows test cases to easily build correct HTTP/2 frames to feed to hyper-h2. """ def __init__(self): self.encoder = Encoder() def refresh_encoder(self): self.encoder = Encoder() def preamble(self): return b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' def build_headers_frame(self, headers, flags=[], stream_id=1): """ Builds a single valid headers frame out of the contained headers. """ f = HeadersFrame(stream_id) f.data = self.encoder.encode(headers) f.flags.add('END_HEADERS') for flag in flags: f.flags.add(flag) return f def build_data_frame(self, data, flags=None, stream_id=1): """ Builds a single data frame out of a chunk of data. """ flags = set(flags) if flags is not None else set() f = DataFrame(stream_id) f.data = data f.flags = flags return f def build_settings_frame(self, settings, ack=False): """ Builds a single settings frame. """ f = SettingsFrame(0) if ack: f.flags.add('ACK') f.settings = settings return f def build_window_update_frame(self, stream_id, increment): """ Builds a single WindowUpdate frame. """ f = WindowUpdateFrame(stream_id) f.window_increment = increment return f def build_ping_frame(self, ping_data, flags=None): """ Builds a single Ping frame. """ f = PingFrame(0) f.opaque_data = ping_data if flags: f.flags = set(flags) return f def build_goaway_frame(self, last_stream_id, error_code=0): """ Builds a single GOAWAY frame. """ f = GoAwayFrame(0) f.error_code = error_code f.last_stream_id = last_stream_id return f def build_rst_stream_frame(self, stream_id, error_code=0): """ Builds a single RST_STREAM frame. """ f = RstStreamFrame(stream_id) f.error_code = error_code return f def build_push_promise_frame(self, stream_id, promised_stream_id, headers, flags=[]): """ Builds a single PUSH_PROMISE frame. """ f = PushPromiseFrame(stream_id) f.promised_stream_id = promised_stream_id f.data = self.encoder.encode(headers) f.flags = set(flags) return f def build_priority_frame(self, stream_id, weight, depends_on=0, exclusive=False): """ Builds a single priority frame. """ f = PriorityFrame(stream_id) f.depends_on = depends_on f.stream_weight = weight f.exclusive = exclusive return f
class HTTP2Protocol(object): ERROR_CODES = utils.BiDi( NO_ERROR=0x0, PROTOCOL_ERROR=0x1, INTERNAL_ERROR=0x2, FLOW_CONTROL_ERROR=0x3, SETTINGS_TIMEOUT=0x4, STREAM_CLOSED=0x5, FRAME_SIZE_ERROR=0x6, REFUSED_STREAM=0x7, CANCEL=0x8, COMPRESSION_ERROR=0x9, CONNECT_ERROR=0xa, ENHANCE_YOUR_CALM=0xb, INADEQUATE_SECURITY=0xc, HTTP_1_1_REQUIRED=0xd ) # "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" CLIENT_CONNECTION_PREFACE =\ '505249202a20485454502f322e300d0a0d0a534d0d0a0d0a'.decode('hex') ALPN_PROTO_H2 = 'h2' def __init__(self, tcp_handler, is_server=False, dump_frames=False): self.tcp_handler = tcp_handler self.is_server = is_server self.http2_settings = frame.HTTP2_DEFAULT_SETTINGS.copy() self.current_stream_id = None self.encoder = Encoder() self.decoder = Decoder() self.connection_preface_performed = False self.dump_frames = dump_frames def check_alpn(self): alp = self.tcp_handler.get_alpn_proto_negotiated() if alp != self.ALPN_PROTO_H2: raise NotImplementedError( "HTTP2Protocol can not handle unknown ALP: %s" % alp) return True def _receive_settings(self, hide=False): while True: frm = self.read_frame(hide) if isinstance(frm, frame.SettingsFrame): break def _read_settings_ack(self, hide=False): # pragma no cover while True: frm = self.read_frame(hide) if isinstance(frm, frame.SettingsFrame): assert frm.flags & frame.Frame.FLAG_ACK assert len(frm.settings) == 0 break def perform_server_connection_preface(self, force=False): if force or not self.connection_preface_performed: self.connection_preface_performed = True magic_length = len(self.CLIENT_CONNECTION_PREFACE) magic = self.tcp_handler.rfile.safe_read(magic_length) assert magic == self.CLIENT_CONNECTION_PREFACE self.send_frame(frame.SettingsFrame(state=self), hide=True) self._receive_settings(hide=True) def perform_client_connection_preface(self, force=False): if force or not self.connection_preface_performed: self.connection_preface_performed = True self.tcp_handler.wfile.write(self.CLIENT_CONNECTION_PREFACE) self.send_frame(frame.SettingsFrame(state=self), hide=True) self._receive_settings(hide=True) def next_stream_id(self): if self.current_stream_id is None: if self.is_server: # servers must use even stream ids self.current_stream_id = 2 else: # clients must use odd stream ids self.current_stream_id = 1 else: self.current_stream_id += 2 return self.current_stream_id def send_frame(self, frm, hide=False): raw_bytes = frm.to_bytes() self.tcp_handler.wfile.write(raw_bytes) self.tcp_handler.wfile.flush() if not hide and self.dump_frames: # pragma no cover print(frm.human_readable(">>")) def read_frame(self, hide=False): frm = frame.Frame.from_file(self.tcp_handler.rfile, self) if not hide and self.dump_frames: # pragma no cover print(frm.human_readable("<<")) if isinstance(frm, frame.SettingsFrame) and not frm.flags & frame.Frame.FLAG_ACK: self._apply_settings(frm.settings, hide) return frm def _apply_settings(self, settings, hide=False): for setting, value in settings.items(): old_value = self.http2_settings[setting] if not old_value: old_value = '-' self.http2_settings[setting] = value frm = frame.SettingsFrame( state=self, flags=frame.Frame.FLAG_ACK) self.send_frame(frm, hide) # be liberal in what we expect from the other end # to be more strict use: self._read_settings_ack(hide) def _create_headers(self, headers, stream_id, end_stream=True): # TODO: implement max frame size checks and sending in chunks flags = frame.Frame.FLAG_END_HEADERS if end_stream: flags |= frame.Frame.FLAG_END_STREAM header_block_fragment = self.encoder.encode(headers) frm = frame.HeadersFrame( state=self, flags=flags, stream_id=stream_id, header_block_fragment=header_block_fragment) if self.dump_frames: # pragma no cover print(frm.human_readable(">>")) return [frm.to_bytes()] def _create_body(self, body, stream_id): if body is None or len(body) == 0: return b'' # TODO: implement max frame size checks and sending in chunks # TODO: implement flow-control window frm = frame.DataFrame( state=self, flags=frame.Frame.FLAG_END_STREAM, stream_id=stream_id, payload=body) if self.dump_frames: # pragma no cover print(frm.human_readable(">>")) return [frm.to_bytes()] def create_request(self, method, path, headers=None, body=None): if headers is None: headers = [] authority = self.tcp_handler.sni if self.tcp_handler.sni else self.tcp_handler.address.host if self.tcp_handler.address.port != 443: authority += ":%d" % self.tcp_handler.address.port headers = [ (b':method', bytes(method)), (b':path', bytes(path)), (b':scheme', b'https'), (b':authority', authority), ] + headers stream_id = self.next_stream_id() return list(itertools.chain( self._create_headers(headers, stream_id, end_stream=(body is None)), self._create_body(body, stream_id))) def read_response(self): stream_id_, headers, body = self._receive_transmission() return headers[':status'], headers, body def read_request(self): return self._receive_transmission() def _receive_transmission(self): body_expected = True stream_id = 0 header_block_fragment = b'' body = b'' while True: frm = self.read_frame() if isinstance(frm, frame.HeadersFrame)\ or isinstance(frm, frame.ContinuationFrame): stream_id = frm.stream_id header_block_fragment += frm.header_block_fragment if frm.flags & frame.Frame.FLAG_END_STREAM: body_expected = False if frm.flags & frame.Frame.FLAG_END_HEADERS: break while body_expected: frm = self.read_frame() if isinstance(frm, frame.DataFrame): body += frm.payload if frm.flags & frame.Frame.FLAG_END_STREAM: break # TODO: implement window update & flow headers = {} for header, value in self.decoder.decode(header_block_fragment): headers[header] = value return stream_id, headers, body def create_response(self, code, stream_id=None, headers=None, body=None): if headers is None: headers = [] headers = [(b':status', bytes(str(code)))] + headers if not stream_id: stream_id = self.next_stream_id() return list(itertools.chain( self._create_headers(headers, stream_id, end_stream=(body is None)), self._create_body(body, stream_id), ))
def test_header_table_size_getter(self): e = Encoder() assert e.header_table_size == 4096
class FrameFactory(object): """ A class containing lots of helper methods and state to build frames. This allows test cases to easily build correct HTTP/2 frames to feed to hyper-h2. """ def __init__(self): self.encoder = Encoder() def refresh_encoder(self): self.encoder = Encoder() def preamble(self): return b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' def build_headers_frame(self, headers, flags=[], stream_id=1, **priority_kwargs): """ Builds a single valid headers frame out of the contained headers. """ f = HeadersFrame(stream_id) f.data = self.encoder.encode(headers) f.flags.add('END_HEADERS') for flag in flags: f.flags.add(flag) for k, v in priority_kwargs.items(): setattr(f, k, v) return f def build_continuation_frame(self, header_block, flags=[], stream_id=1): """ Builds a single continuation frame out of the binary header block. """ f = ContinuationFrame(stream_id) f.data = header_block f.flags = set(flags) return f def build_data_frame(self, data, flags=None, stream_id=1, padding_len=0): """ Builds a single data frame out of a chunk of data. """ flags = set(flags) if flags is not None else set() f = DataFrame(stream_id) f.data = data f.flags = flags if padding_len: flags.add('PADDED') f.pad_length = padding_len return f def build_settings_frame(self, settings, ack=False): """ Builds a single settings frame. """ f = SettingsFrame(0) if ack: f.flags.add('ACK') f.settings = settings return f def build_window_update_frame(self, stream_id, increment): """ Builds a single WindowUpdate frame. """ f = WindowUpdateFrame(stream_id) f.window_increment = increment return f def build_ping_frame(self, ping_data, flags=None): """ Builds a single Ping frame. """ f = PingFrame(0) f.opaque_data = ping_data if flags: f.flags = set(flags) return f def build_goaway_frame(self, last_stream_id, error_code=0, additional_data=b''): """ Builds a single GOAWAY frame. """ f = GoAwayFrame(0) f.error_code = error_code f.last_stream_id = last_stream_id f.additional_data = additional_data return f def build_rst_stream_frame(self, stream_id, error_code=0): """ Builds a single RST_STREAM frame. """ f = RstStreamFrame(stream_id) f.error_code = error_code return f def build_push_promise_frame(self, stream_id, promised_stream_id, headers, flags=[]): """ Builds a single PUSH_PROMISE frame. """ f = PushPromiseFrame(stream_id) f.promised_stream_id = promised_stream_id f.data = self.encoder.encode(headers) f.flags = set(flags) f.flags.add('END_HEADERS') return f def build_priority_frame(self, stream_id, weight, depends_on=0, exclusive=False): """ Builds a single priority frame. """ f = PriorityFrame(stream_id) f.depends_on = depends_on f.stream_weight = weight f.exclusive = exclusive return f def build_alt_svc_frame(self, stream_id, origin, field): """ Builds a single ALTSVC frame. """ f = AltSvcFrame(stream_id) f.origin = origin f.field = field return f
def __init__(self): self.encoder = Encoder()
class FrameFactory(object): """ A class containing lots of helper methods and state to build frames. This allows test cases to easily build correct HTTP/2 frames to feed to hyper-h2. """ def __init__(self): self.encoder = Encoder() def refresh_encoder(self): self.encoder = Encoder() def preamble(self): return b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' def build_headers_frame(self, headers, flags=[], stream_id=1, **priority_kwargs): """ Builds a single valid headers frame out of the contained headers. """ f = HeadersFrame(stream_id) f.data = self.encoder.encode(headers) f.flags.add('END_HEADERS') for flag in flags: f.flags.add(flag) for k, v in priority_kwargs.items(): setattr(f, k, v) return f def build_continuation_frame(self, header_block, flags=[], stream_id=1): """ Builds a single continuation frame out of the binary header block. """ f = ContinuationFrame(stream_id) f.data = header_block f.flags = set(flags) return f def build_data_frame(self, data, flags=None, stream_id=1, padding_len=0): """ Builds a single data frame out of a chunk of data. """ flags = set(flags) if flags is not None else set() f = DataFrame(stream_id) f.data = data f.flags = flags if padding_len: flags.add('PADDED') f.pad_length = padding_len return f def build_settings_frame(self, settings, ack=False): """ Builds a single settings frame. """ f = SettingsFrame(0) if ack: f.flags.add('ACK') f.settings = settings return f def build_window_update_frame(self, stream_id, increment): """ Builds a single WindowUpdate frame. """ f = WindowUpdateFrame(stream_id) f.window_increment = increment return f def build_ping_frame(self, ping_data, flags=None): """ Builds a single Ping frame. """ f = PingFrame(0) f.opaque_data = ping_data if flags: f.flags = set(flags) return f def build_goaway_frame(self, last_stream_id, error_code=0, additional_data=b''): """ Builds a single GOAWAY frame. """ f = GoAwayFrame(0) f.error_code = error_code f.last_stream_id = last_stream_id f.additional_data = additional_data return f def build_rst_stream_frame(self, stream_id, error_code=0): """ Builds a single RST_STREAM frame. """ f = RstStreamFrame(stream_id) f.error_code = error_code return f def build_push_promise_frame(self, stream_id, promised_stream_id, headers, flags=[]): """ Builds a single PUSH_PROMISE frame. """ f = PushPromiseFrame(stream_id) f.promised_stream_id = promised_stream_id f.data = self.encoder.encode(headers) f.flags = set(flags) f.flags.add('END_HEADERS') return f def build_priority_frame(self, stream_id, weight, depends_on=0, exclusive=False): """ Builds a single priority frame. """ f = PriorityFrame(stream_id) f.depends_on = depends_on f.stream_weight = weight f.exclusive = exclusive return f def build_alt_svc_frame(self, stream_id, origin, field): """ Builds a single ALTSVC frame. """ f = AltSvcFrame(stream_id) f.origin = origin f.field = field return f def change_table_size(self, new_size): """ Causes the encoder to send a dynamic size update in the next header block it sends. """ self.encoder.header_table_size = new_size
def test_request_examples_without_huffman(self): """ This section shows several consecutive header sets, corresponding to HTTP requests, on the same connection. """ e = Encoder() first_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'http', ), ( ':path', '/', ), (':authority', 'www.example.com'), ] # We should have :authority in first_header_table since we index it first_header_table = [(':authority', 'www.example.com')] first_result = b'\x82\x86\x84\x41\x0fwww.example.com' assert e.encode(first_header_set, huffman=False) == first_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in first_header_table ] second_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'http', ), ( ':path', '/', ), ( ':authority', 'www.example.com', ), ('cache-control', 'no-cache'), ] second_header_table = [('cache-control', 'no-cache'), (':authority', 'www.example.com')] second_result = b'\x82\x86\x84\xbeX\x08no-cache' assert e.encode(second_header_set, huffman=False) == second_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in second_header_table ] third_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'https', ), ( ':path', '/index.html', ), ( ':authority', 'www.example.com', ), ('custom-key', 'custom-value'), ] third_result = (b'\x82\x87\x85\xbf@\ncustom-key\x0ccustom-value') assert e.encode(third_header_set, huffman=False) == third_result # Don't check the header table here, it's just too complex to be # reliable. Check its length though. assert len(e.header_table.dynamic_entries) == 3
def test_request_examples_with_huffman(self): """ This section shows the same examples as the previous section, but using Huffman encoding for the literal values. """ e = Encoder() first_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'http', ), ( ':path', '/', ), (':authority', 'www.example.com'), ] first_header_table = [(':authority', 'www.example.com')] first_result = ( b'\x82\x86\x84\x41\x8c\xf1\xe3\xc2\xe5\xf2:k\xa0\xab\x90\xf4\xff') assert e.encode(first_header_set, huffman=True) == first_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in first_header_table ] second_header_table = [('cache-control', 'no-cache'), (':authority', 'www.example.com')] second_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'http', ), ( ':path', '/', ), ( ':authority', 'www.example.com', ), ('cache-control', 'no-cache'), ] second_result = b'\x82\x86\x84\xbeX\x86\xa8\xeb\x10d\x9c\xbf' assert e.encode(second_header_set, huffman=True) == second_result assert list(e.header_table.dynamic_entries) == [ (n.encode('utf-8'), v.encode('utf-8')) for n, v in second_header_table ] third_header_set = [ ( ':method', 'GET', ), ( ':scheme', 'https', ), ( ':path', '/index.html', ), ( ':authority', 'www.example.com', ), ('custom-key', 'custom-value'), ] third_result = ( b'\x82\x87\x85\xbf' b'@\x88%\xa8I\xe9[\xa9}\x7f\x89%\xa8I\xe9[\xb8\xe8\xb4\xbf') assert e.encode(third_header_set, huffman=True) == third_result assert len(e.header_table.dynamic_entries) == 3