diff options
Diffstat (limited to 'netlib')
-rw-r--r-- | netlib/http2/__init__.py | 183 | ||||
-rw-r--r-- | netlib/http2/frame.py | 18 | ||||
-rw-r--r-- | netlib/http2/protocol.py | 174 |
3 files changed, 188 insertions, 187 deletions
diff --git a/netlib/http2/__init__.py b/netlib/http2/__init__.py index 2803cccb..92897b5d 100644 --- a/netlib/http2/__init__.py +++ b/netlib/http2/__init__.py @@ -1,182 +1,3 @@ -from __future__ import (absolute_import, print_function, division) -import itertools -import logging -from .frame import * -from .. import utils - -log = logging.getLogger(__name__) - - -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' - - ALPN_PROTO_H2 = 'h2' - - HTTP2_DEFAULT_SETTINGS = { - SettingsFrame.SETTINGS.SETTINGS_HEADER_TABLE_SIZE: 4096, - SettingsFrame.SETTINGS.SETTINGS_ENABLE_PUSH: 1, - SettingsFrame.SETTINGS.SETTINGS_MAX_CONCURRENT_STREAMS: None, - SettingsFrame.SETTINGS.SETTINGS_INITIAL_WINDOW_SIZE: 2 ** 16 - 1, - SettingsFrame.SETTINGS.SETTINGS_MAX_FRAME_SIZE: 2 ** 14, - SettingsFrame.SETTINGS.SETTINGS_MAX_HEADER_LIST_SIZE: None, - } - - def __init__(self, tcp_client): - self.tcp_client = tcp_client - - self.http2_settings = self.HTTP2_DEFAULT_SETTINGS.copy() - self.current_stream_id = None - self.encoder = Encoder() - self.decoder = Decoder() - - def check_alpn(self): - alp = self.tcp_client.get_alpn_proto_negotiated() - if alp != self.ALPN_PROTO_H2: - raise NotImplementedError( - "HTTP2Protocol can not handle unknown ALP: %s" % alp) - log.debug("ALP 'h2' successfully negotiated.") - return True - - def perform_connection_preface(self): - self.tcp_client.wfile.write( - bytes(self.CLIENT_CONNECTION_PREFACE.decode('hex'))) - self.send_frame(SettingsFrame(state=self)) - - # read server settings frame - frame = Frame.from_file(self.tcp_client.rfile, self) - assert isinstance(frame, SettingsFrame) - self._apply_settings(frame.settings) - - # read setting ACK frame - settings_ack_frame = self.read_frame() - assert isinstance(settings_ack_frame, SettingsFrame) - assert settings_ack_frame.flags & Frame.FLAG_ACK - assert len(settings_ack_frame.settings) == 0 - - log.debug("Connection Preface completed.") - - def next_stream_id(self): - if self.current_stream_id is None: - self.current_stream_id = 1 - else: - self.current_stream_id += 2 - return self.current_stream_id - - def send_frame(self, frame): - raw_bytes = frame.to_bytes() - self.tcp_client.wfile.write(raw_bytes) - self.tcp_client.wfile.flush() - - def read_frame(self): - frame = Frame.from_file(self.tcp_client.rfile, self) - if isinstance(frame, SettingsFrame): - self._apply_settings(frame.settings) - - return frame - - def _apply_settings(self, settings): - for setting, value in settings.items(): - old_value = self.http2_settings[setting] - if not old_value: - old_value = '-' - - self.http2_settings[setting] = value - log.debug("Setting changed: %s to %s (was %s)" % ( - SettingsFrame.SETTINGS.get_name(setting), - str(value), - str(old_value))) - - self.send_frame(SettingsFrame(state=self, flags=Frame.FLAG_ACK)) - log.debug("New settings acknowledged.") - - def _create_headers(self, headers, stream_id, end_stream=True): - # TODO: implement max frame size checks and sending in chunks - - flags = Frame.FLAG_END_HEADERS - if end_stream: - flags |= Frame.FLAG_END_STREAM - - header_block_fragment = self.encoder.encode(headers) - - bytes = HeadersFrame( - state=self, - flags=flags, - stream_id=stream_id, - header_block_fragment=header_block_fragment).to_bytes() - return [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 - - bytes = DataFrame( - state=self, - flags=Frame.FLAG_END_STREAM, - stream_id=stream_id, - payload=body).to_bytes() - return [bytes] - - def create_request(self, method, path, headers=None, body=None): - if headers is None: - headers = [] - - headers = [ - (b':method', bytes(method)), - (b':path', bytes(path)), - (b':scheme', b'https')] + 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): - header_block_fragment = b'' - body = b'' - - while True: - frame = self.read_frame() - if isinstance(frame, HeadersFrame): - header_block_fragment += frame.header_block_fragment - if frame.flags | Frame.FLAG_END_HEADERS: - break - - while True: - frame = self.read_frame() - if isinstance(frame, DataFrame): - body += frame.payload - if frame.flags | Frame.FLAG_END_STREAM: - break - - headers = {} - for header, value in self.decoder.decode(header_block_fragment): - headers[header] = value - - for header, value in headers.items(): - log.debug("%s: %s" % (header, value)) - - return headers[':status'], headers, body +from frame import * +from protocol import * diff --git a/netlib/http2/frame.py b/netlib/http2/frame.py index 1497380a..fc86c228 100644 --- a/netlib/http2/frame.py +++ b/netlib/http2/frame.py @@ -38,13 +38,11 @@ class Frame(object): raise ValueError('invalid flags detected.') if state is None: - from . import HTTP2Protocol - class State(object): pass state = State() - state.http2_settings = HTTP2Protocol.HTTP2_DEFAULT_SETTINGS.copy() + state.http2_settings = HTTP2_DEFAULT_SETTINGS.copy() state.encoder = Encoder() state.decoder = Decoder() @@ -57,12 +55,10 @@ class Frame(object): @classmethod def _check_frame_size(self, length, state): - from . import HTTP2Protocol - if state: settings = state.http2_settings else: - settings = HTTP2Protocol.HTTP2_DEFAULT_SETTINGS + settings = HTTP2_DEFAULT_SETTINGS.copy() max_frame_size = settings[ SettingsFrame.SETTINGS.SETTINGS_MAX_FRAME_SIZE] @@ -623,3 +619,13 @@ _FRAME_CLASSES = [ ContinuationFrame ] FRAMES = {cls.TYPE: cls for cls in _FRAME_CLASSES} + + +HTTP2_DEFAULT_SETTINGS = { + SettingsFrame.SETTINGS.SETTINGS_HEADER_TABLE_SIZE: 4096, + SettingsFrame.SETTINGS.SETTINGS_ENABLE_PUSH: 1, + SettingsFrame.SETTINGS.SETTINGS_MAX_CONCURRENT_STREAMS: None, + SettingsFrame.SETTINGS.SETTINGS_INITIAL_WINDOW_SIZE: 2 ** 16 - 1, + SettingsFrame.SETTINGS.SETTINGS_MAX_FRAME_SIZE: 2 ** 14, + SettingsFrame.SETTINGS.SETTINGS_MAX_HEADER_LIST_SIZE: None, +} diff --git a/netlib/http2/protocol.py b/netlib/http2/protocol.py new file mode 100644 index 00000000..9bab431c --- /dev/null +++ b/netlib/http2/protocol.py @@ -0,0 +1,174 @@ +from __future__ import (absolute_import, print_function, division) +import itertools +import logging + +from hpack.hpack import Encoder, Decoder +from .. import utils +from . import frame + +log = logging.getLogger(__name__) + + +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' + + ALPN_PROTO_H2 = 'h2' + + def __init__(self, tcp_client): + self.tcp_client = tcp_client + + self.http2_settings = frame.HTTP2_DEFAULT_SETTINGS.copy() + self.current_stream_id = None + self.encoder = Encoder() + self.decoder = Decoder() + + def check_alpn(self): + alp = self.tcp_client.get_alpn_proto_negotiated() + if alp != self.ALPN_PROTO_H2: + raise NotImplementedError( + "HTTP2Protocol can not handle unknown ALP: %s" % alp) + log.debug("ALP 'h2' successfully negotiated.") + return True + + def perform_connection_preface(self): + self.tcp_client.wfile.write( + bytes(self.CLIENT_CONNECTION_PREFACE.decode('hex'))) + self.send_frame(frame.SettingsFrame(state=self)) + + # read server settings frame + frm = frame.Frame.from_file(self.tcp_client.rfile, self) + assert isinstance(frm, frame.SettingsFrame) + self._apply_settings(frm.settings) + + # read setting ACK frame + settings_ack_frame = self.read_frame() + assert isinstance(settings_ack_frame, frame.SettingsFrame) + assert settings_ack_frame.flags & frame.Frame.FLAG_ACK + assert len(settings_ack_frame.settings) == 0 + + log.debug("Connection Preface completed.") + + def next_stream_id(self): + if self.current_stream_id is None: + self.current_stream_id = 1 + else: + self.current_stream_id += 2 + return self.current_stream_id + + def send_frame(self, frame): + raw_bytes = frame.to_bytes() + self.tcp_client.wfile.write(raw_bytes) + self.tcp_client.wfile.flush() + + def read_frame(self): + frm = frame.Frame.from_file(self.tcp_client.rfile, self) + if isinstance(frm, frame.SettingsFrame): + self._apply_settings(frm.settings) + + return frm + + def _apply_settings(self, settings): + for setting, value in settings.items(): + old_value = self.http2_settings[setting] + if not old_value: + old_value = '-' + + self.http2_settings[setting] = value + log.debug("Setting changed: %s to %s (was %s)" % ( + frame.SettingsFrame.SETTINGS.get_name(setting), + str(value), + str(old_value))) + + self.send_frame(frame.SettingsFrame(state=self, flags=frame.Frame.FLAG_ACK)) + log.debug("New settings acknowledged.") + + 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) + + bytes = frame.HeadersFrame( + state=self, + flags=flags, + stream_id=stream_id, + header_block_fragment=header_block_fragment).to_bytes() + return [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 + + bytes = frame.DataFrame( + state=self, + flags=frame.Frame.FLAG_END_STREAM, + stream_id=stream_id, + payload=body).to_bytes() + return [bytes] + + def create_request(self, method, path, headers=None, body=None): + if headers is None: + headers = [] + + headers = [ + (b':method', bytes(method)), + (b':path', bytes(path)), + (b':scheme', b'https')] + 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): + header_block_fragment = b'' + body = b'' + + while True: + frm = self.read_frame() + if isinstance(frm, frame.HeadersFrame): + header_block_fragment += frm.header_block_fragment + if frm.flags | frame.Frame.FLAG_END_HEADERS: + break + + while True: + frm = self.read_frame() + if isinstance(frm, frame.DataFrame): + body += frm.payload + if frm.flags | frame.Frame.FLAG_END_STREAM: + break + + headers = {} + for header, value in self.decoder.decode(header_block_fragment): + headers[header] = value + + for header, value in headers.items(): + log.debug("%s: %s" % (header, value)) + + return headers[':status'], headers, body |