From eb3ed87100ff7c32e5bf040db7eb6ea3d0c06e12 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Fri, 17 Jun 2016 14:15:48 +0200 Subject: move custom HTTP/2 stack from netlib to pathod --- netlib/http/http2/__init__.py | 2 - netlib/http/http2/connections.py | 432 ----------------------- pathod/pathoc.py | 12 +- pathod/protocols/http2.py | 439 ++++++++++++++++++++++- test/netlib/http/http2/test_connections.py | 544 ---------------------------- test/netlib/http/http2/test_framereader.py | 1 + test/pathod/__init__.py | 1 + test/pathod/test_language_actions.py | 19 +- test/pathod/test_language_base.py | 3 +- test/pathod/test_language_generators.py | 2 +- test/pathod/test_language_http.py | 3 +- test/pathod/test_language_http2.py | 7 +- test/pathod/test_language_websocket.py | 3 +- test/pathod/test_pathoc.py | 10 +- test/pathod/test_pathoc_cmdline.py | 6 +- test/pathod/test_pathod.py | 3 +- test/pathod/test_pathod_cmdline.py | 6 +- test/pathod/test_protocols_http2.py | 545 +++++++++++++++++++++++++++++ test/pathod/test_test.py | 3 +- test/pathod/test_utils.py | 3 +- 20 files changed, 1029 insertions(+), 1015 deletions(-) delete mode 100644 netlib/http/http2/connections.py delete mode 100644 test/netlib/http/http2/test_connections.py create mode 100644 test/netlib/http/http2/test_framereader.py create mode 100644 test/pathod/__init__.py create mode 100644 test/pathod/test_protocols_http2.py diff --git a/netlib/http/http2/__init__.py b/netlib/http/http2/__init__.py index 633e6a20..6a979a0d 100644 --- a/netlib/http/http2/__init__.py +++ b/netlib/http/http2/__init__.py @@ -1,8 +1,6 @@ from __future__ import absolute_import, print_function, division -from .connections import HTTP2Protocol from netlib.http.http2 import framereader __all__ = [ - "HTTP2Protocol", "framereader", ] diff --git a/netlib/http/http2/connections.py b/netlib/http/http2/connections.py deleted file mode 100644 index 8f246feb..00000000 --- a/netlib/http/http2/connections.py +++ /dev/null @@ -1,432 +0,0 @@ -from __future__ import (absolute_import, print_function, division) -import itertools -import time - -import hyperframe.frame - -from hpack.hpack import Encoder, Decoder -from netlib import utils, strutils -from netlib.http import url -import netlib.http.headers -import netlib.http.response -import netlib.http.request -from netlib.http.http2 import framereader - - -class TCPHandler(object): - - def __init__(self, rfile, wfile=None): - self.rfile = rfile - self.wfile = wfile - - -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 - ) - - CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' - - HTTP2_DEFAULT_SETTINGS = { - hyperframe.frame.SettingsFrame.HEADER_TABLE_SIZE: 4096, - hyperframe.frame.SettingsFrame.ENABLE_PUSH: 1, - hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None, - hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1, - hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14, - hyperframe.frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None, - } - - 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 read_request( - self, - __rfile, - include_body=True, - body_size_limit=None, - allow_empty=False, - ): - if body_size_limit is not None: - raise NotImplementedError() - - self.perform_connection_preface() - - timestamp_start = time.time() - if hasattr(self.tcp_handler.rfile, "reset_timestamps"): - self.tcp_handler.rfile.reset_timestamps() - - stream_id, headers, body = self._receive_transmission( - include_body=include_body, - ) - - if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = self.tcp_handler.rfile.first_byte_timestamp - - timestamp_end = time.time() - - authority = headers.get(':authority', b'') - method = headers.get(':method', 'GET') - scheme = headers.get(':scheme', 'https') - path = headers.get(':path', '/') - - headers.clear(":method") - headers.clear(":scheme") - headers.clear(":path") - - host = None - port = None - - if path == '*' or path.startswith("/"): - first_line_format = "relative" - elif method == 'CONNECT': - first_line_format = "authority" - if ":" in authority: - host, port = authority.split(":", 1) - else: - host = authority - else: - first_line_format = "absolute" - # FIXME: verify if path or :host contains what we need - scheme, host, port, _ = url.parse(path) - scheme = scheme.decode('ascii') - host = host.decode('ascii') - - if host is None: - host = 'localhost' - if port is None: - port = 80 if scheme == 'http' else 443 - port = int(port) - - request = netlib.http.request.Request( - first_line_format, - method.encode('ascii'), - scheme.encode('ascii'), - host.encode('ascii'), - port, - path.encode('ascii'), - b"HTTP/2.0", - headers, - body, - timestamp_start, - timestamp_end, - ) - request.stream_id = stream_id - - return request - - def read_response( - self, - __rfile, - request_method=b'', - body_size_limit=None, - include_body=True, - stream_id=None, - ): - if body_size_limit is not None: - raise NotImplementedError() - - self.perform_connection_preface() - - timestamp_start = time.time() - if hasattr(self.tcp_handler.rfile, "reset_timestamps"): - self.tcp_handler.rfile.reset_timestamps() - - stream_id, headers, body = self._receive_transmission( - stream_id=stream_id, - include_body=include_body, - ) - - if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): - # more accurate timestamp_start - timestamp_start = self.tcp_handler.rfile.first_byte_timestamp - - if include_body: - timestamp_end = time.time() - else: - timestamp_end = None - - response = netlib.http.response.Response( - b"HTTP/2.0", - int(headers.get(':status', 502)), - b'', - headers, - body, - timestamp_start=timestamp_start, - timestamp_end=timestamp_end, - ) - response.stream_id = stream_id - - return response - - def assemble(self, message): - if isinstance(message, netlib.http.request.Request): - return self.assemble_request(message) - elif isinstance(message, netlib.http.response.Response): - return self.assemble_response(message) - else: - raise ValueError("HTTP message not supported.") - - def assemble_request(self, request): - assert isinstance(request, netlib.http.request.Request) - - 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 = request.headers.copy() - - if ':authority' not in headers: - headers.insert(0, b':authority', authority.encode('ascii')) - headers.insert(0, b':scheme', request.scheme.encode('ascii')) - headers.insert(0, b':path', request.path.encode('ascii')) - headers.insert(0, b':method', request.method.encode('ascii')) - - if hasattr(request, 'stream_id'): - stream_id = request.stream_id - else: - stream_id = self._next_stream_id() - - return list(itertools.chain( - self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)), - self._create_body(request.body, stream_id))) - - def assemble_response(self, response): - assert isinstance(response, netlib.http.response.Response) - - headers = response.headers.copy() - - if ':status' not in headers: - headers.insert(0, b':status', strutils.always_bytes(response.status_code)) - - if hasattr(response, 'stream_id'): - stream_id = response.stream_id - else: - stream_id = self._next_stream_id() - - return list(itertools.chain( - self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)), - self._create_body(response.body, stream_id), - )) - - def perform_connection_preface(self, force=False): - if force or not self.connection_preface_performed: - if self.is_server: - self.perform_server_connection_preface(force) - else: - self.perform_client_connection_preface(force) - - 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 - - frm = hyperframe.frame.SettingsFrame(settings={ - hyperframe.frame.SettingsFrame.ENABLE_PUSH: 0, - hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1, - }) - self.send_frame(frm, 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(hyperframe.frame.SettingsFrame(), hide=True) - self._receive_settings(hide=True) # server announces own settings - self._receive_settings(hide=True) # server acks my settings - - def send_frame(self, frm, hide=False): - raw_bytes = frm.serialize() - 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): - while True: - frm = framereader.http2_read_frame(self.tcp_handler.rfile) - if not hide and self.dump_frames: # pragma no cover - print(frm.human_readable("<<")) - - if isinstance(frm, hyperframe.frame.PingFrame): - raw_bytes = hyperframe.frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize() - self.tcp_handler.wfile.write(raw_bytes) - self.tcp_handler.wfile.flush() - continue - if isinstance(frm, hyperframe.frame.SettingsFrame) and 'ACK' not in frm.flags: - self._apply_settings(frm.settings, hide) - if isinstance(frm, hyperframe.frame.DataFrame) and frm.flow_controlled_length > 0: - self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length) - return frm - - def check_alpn(self): - alp = self.tcp_handler.get_alpn_proto_negotiated() - if alp != b'h2': - raise NotImplementedError( - "HTTP2Protocol can not handle unknown ALP: %s" % alp) - return True - - def _handle_unexpected_frame(self, frm): - if isinstance(frm, hyperframe.frame.SettingsFrame): - return - if self.unhandled_frame_cb: - self.unhandled_frame_cb(frm) - - def _receive_settings(self, hide=False): - while True: - frm = self.read_frame(hide) - if isinstance(frm, hyperframe.frame.SettingsFrame): - break - else: - self._handle_unexpected_frame(frm) - - 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 _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 = hyperframe.frame.SettingsFrame(flags=['ACK']) - self.send_frame(frm, hide) - - def _update_flow_control_window(self, stream_id, increment): - frm = hyperframe.frame.WindowUpdateFrame(stream_id=0, window_increment=increment) - self.send_frame(frm) - frm = hyperframe.frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment) - self.send_frame(frm) - - def _create_headers(self, headers, stream_id, end_stream=True): - def frame_cls(chunks): - for i in chunks: - if i == 0: - yield hyperframe.frame.HeadersFrame, i - else: - yield hyperframe.frame.ContinuationFrame, i - - header_block_fragment = self.encoder.encode(headers.fields) - - chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] - chunks = range(0, len(header_block_fragment), chunk_size) - frms = [frm_cls( - flags=[], - stream_id=stream_id, - data=header_block_fragment[i:i + chunk_size]) for frm_cls, i in frame_cls(chunks)] - - frms[-1].flags.add('END_HEADERS') - if end_stream: - frms[0].flags.add('END_STREAM') - - if self.dump_frames: # pragma no cover - for frm in frms: - print(frm.human_readable(">>")) - - return [frm.serialize() for frm in frms] - - def _create_body(self, body, stream_id): - if body is None or len(body) == 0: - return b'' - - chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] - chunks = range(0, len(body), chunk_size) - frms = [hyperframe.frame.DataFrame( - flags=[], - stream_id=stream_id, - data=body[i:i + chunk_size]) for i in chunks] - frms[-1].flags.add('END_STREAM') - - if self.dump_frames: # pragma no cover - for frm in frms: - print(frm.human_readable(">>")) - - return [frm.serialize() for frm in frms] - - def _receive_transmission(self, stream_id=None, include_body=True): - if not include_body: - raise NotImplementedError() - - body_expected = True - - header_blocks = b'' - body = b'' - - while True: - frm = self.read_frame() - if ( - (isinstance(frm, hyperframe.frame.HeadersFrame) or isinstance(frm, hyperframe.frame.ContinuationFrame)) and - (stream_id is None or frm.stream_id == stream_id) - ): - stream_id = frm.stream_id - header_blocks += frm.data - if 'END_STREAM' in frm.flags: - body_expected = False - if 'END_HEADERS' in frm.flags: - break - else: - self._handle_unexpected_frame(frm) - - while body_expected: - frm = self.read_frame() - if isinstance(frm, hyperframe.frame.DataFrame) and frm.stream_id == stream_id: - body += frm.data - if 'END_STREAM' in frm.flags: - break - else: - self._handle_unexpected_frame(frm) - - headers = netlib.http.headers.Headers( - (k.encode('ascii'), v.encode('ascii')) for k, v in self.decoder.decode(header_blocks) - ) - - return stream_id, headers, body diff --git a/pathod/pathoc.py b/pathod/pathoc.py index ea21b747..c6783878 100644 --- a/pathod/pathoc.py +++ b/pathod/pathoc.py @@ -11,18 +11,18 @@ import time import OpenSSL.crypto import six +import logging +from netlib.tutils import treq +from netlib import strutils from netlib import tcp, certutils, websockets, socks from netlib import exceptions from netlib.http import http1 -from netlib.http import http2 from netlib import basethread -from pathod import log, language +from . import log, language +from .protocols import http2 -import logging -from netlib.tutils import treq -from netlib import strutils logging.getLogger("hpack").setLevel(logging.WARNING) @@ -227,7 +227,7 @@ class Pathoc(tcp.TCPClient): "Pathoc might not be working as expected without ALPN.", timestamp=False ) - self.protocol = http2.HTTP2Protocol(self, dump_frames=self.http2_framedump) + self.protocol = http2.HTTP2StateProtocol(self, dump_frames=self.http2_framedump) else: self.protocol = http1 diff --git a/pathod/protocols/http2.py b/pathod/protocols/http2.py index 3f45ec80..c8728940 100644 --- a/pathod/protocols/http2.py +++ b/pathod/protocols/http2.py @@ -1,12 +1,445 @@ -from netlib.http import http2 +from __future__ import (absolute_import, print_function, division) + +import itertools +import time + +import hyperframe.frame +from hpack.hpack import Encoder, Decoder + +from netlib import utils, strutils +from netlib.http import url +from netlib.http.http2 import framereader +import netlib.http.headers +import netlib.http.response +import netlib.http.request + from .. import language -class HTTP2Protocol: +class TCPHandler(object): + + def __init__(self, rfile, wfile=None): + self.rfile = rfile + self.wfile = wfile + + +class HTTP2StateProtocol(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 + ) + + CLIENT_CONNECTION_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + + HTTP2_DEFAULT_SETTINGS = { + hyperframe.frame.SettingsFrame.HEADER_TABLE_SIZE: 4096, + hyperframe.frame.SettingsFrame.ENABLE_PUSH: 1, + hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: None, + hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 2 ** 16 - 1, + hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE: 2 ** 14, + hyperframe.frame.SettingsFrame.MAX_HEADER_LIST_SIZE: None, + } + + 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 read_request( + self, + __rfile, + include_body=True, + body_size_limit=None, + allow_empty=False, + ): + if body_size_limit is not None: + raise NotImplementedError() + + self.perform_connection_preface() + + timestamp_start = time.time() + if hasattr(self.tcp_handler.rfile, "reset_timestamps"): + self.tcp_handler.rfile.reset_timestamps() + + stream_id, headers, body = self._receive_transmission( + include_body=include_body, + ) + + if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = self.tcp_handler.rfile.first_byte_timestamp + + timestamp_end = time.time() + + authority = headers.get(':authority', b'') + method = headers.get(':method', 'GET') + scheme = headers.get(':scheme', 'https') + path = headers.get(':path', '/') + + headers.clear(":method") + headers.clear(":scheme") + headers.clear(":path") + + host = None + port = None + + if path == '*' or path.startswith("/"): + first_line_format = "relative" + elif method == 'CONNECT': + first_line_format = "authority" + if ":" in authority: + host, port = authority.split(":", 1) + else: + host = authority + else: + first_line_format = "absolute" + # FIXME: verify if path or :host contains what we need + scheme, host, port, _ = url.parse(path) + scheme = scheme.decode('ascii') + host = host.decode('ascii') + + if host is None: + host = 'localhost' + if port is None: + port = 80 if scheme == 'http' else 443 + port = int(port) + + request = netlib.http.request.Request( + first_line_format, + method.encode('ascii'), + scheme.encode('ascii'), + host.encode('ascii'), + port, + path.encode('ascii'), + b"HTTP/2.0", + headers, + body, + timestamp_start, + timestamp_end, + ) + request.stream_id = stream_id + + return request + + def read_response( + self, + __rfile, + request_method=b'', + body_size_limit=None, + include_body=True, + stream_id=None, + ): + if body_size_limit is not None: + raise NotImplementedError() + + self.perform_connection_preface() + + timestamp_start = time.time() + if hasattr(self.tcp_handler.rfile, "reset_timestamps"): + self.tcp_handler.rfile.reset_timestamps() + + stream_id, headers, body = self._receive_transmission( + stream_id=stream_id, + include_body=include_body, + ) + + if hasattr(self.tcp_handler.rfile, "first_byte_timestamp"): + # more accurate timestamp_start + timestamp_start = self.tcp_handler.rfile.first_byte_timestamp + + if include_body: + timestamp_end = time.time() + else: + timestamp_end = None + + response = netlib.http.response.Response( + b"HTTP/2.0", + int(headers.get(':status', 502)), + b'', + headers, + body, + timestamp_start=timestamp_start, + timestamp_end=timestamp_end, + ) + response.stream_id = stream_id + + return response + + def assemble(self, message): + if isinstance(message, netlib.http.request.Request): + return self.assemble_request(message) + elif isinstance(message, netlib.http.response.Response): + return self.assemble_response(message) + else: + raise ValueError("HTTP message not supported.") + + def assemble_request(self, request): + assert isinstance(request, netlib.http.request.Request) + + 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 = request.headers.copy() + + if ':authority' not in headers: + headers.insert(0, b':authority', authority.encode('ascii')) + headers.insert(0, b':scheme', request.scheme.encode('ascii')) + headers.insert(0, b':path', request.path.encode('ascii')) + headers.insert(0, b':method', request.method.encode('ascii')) + + if hasattr(request, 'stream_id'): + stream_id = request.stream_id + else: + stream_id = self._next_stream_id() + + return list(itertools.chain( + self._create_headers(headers, stream_id, end_stream=(request.body is None or len(request.body) == 0)), + self._create_body(request.body, stream_id))) + + def assemble_response(self, response): + assert isinstance(response, netlib.http.response.Response) + + headers = response.headers.copy() + + if ':status' not in headers: + headers.insert(0, b':status', strutils.always_bytes(response.status_code)) + + if hasattr(response, 'stream_id'): + stream_id = response.stream_id + else: + stream_id = self._next_stream_id() + + return list(itertools.chain( + self._create_headers(headers, stream_id, end_stream=(response.body is None or len(response.body) == 0)), + self._create_body(response.body, stream_id), + )) + + def perform_connection_preface(self, force=False): + if force or not self.connection_preface_performed: + if self.is_server: + self.perform_server_connection_preface(force) + else: + self.perform_client_connection_preface(force) + + 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 + + frm = hyperframe.frame.SettingsFrame(settings={ + hyperframe.frame.SettingsFrame.ENABLE_PUSH: 0, + hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 1, + }) + self.send_frame(frm, 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(hyperframe.frame.SettingsFrame(), hide=True) + self._receive_settings(hide=True) # server announces own settings + self._receive_settings(hide=True) # server acks my settings + + def send_frame(self, frm, hide=False): + raw_bytes = frm.serialize() + 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): + while True: + frm = framereader.http2_read_frame(self.tcp_handler.rfile) + if not hide and self.dump_frames: # pragma no cover + print(frm.human_readable("<<")) + + if isinstance(frm, hyperframe.frame.PingFrame): + raw_bytes = hyperframe.frame.PingFrame(flags=['ACK'], payload=frm.payload).serialize() + self.tcp_handler.wfile.write(raw_bytes) + self.tcp_handler.wfile.flush() + continue + if isinstance(frm, hyperframe.frame.SettingsFrame) and 'ACK' not in frm.flags: + self._apply_settings(frm.settings, hide) + if isinstance(frm, hyperframe.frame.DataFrame) and frm.flow_controlled_length > 0: + self._update_flow_control_window(frm.stream_id, frm.flow_controlled_length) + return frm + + def check_alpn(self): + alp = self.tcp_handler.get_alpn_proto_negotiated() + if alp != b'h2': + raise NotImplementedError( + "HTTP2Protocol can not handle unknown ALPN value: %s" % alp) + return True + + def _handle_unexpected_frame(self, frm): + if isinstance(frm, hyperframe.frame.SettingsFrame): + return + if self.unhandled_frame_cb: + self.unhandled_frame_cb(frm) + + def _receive_settings(self, hide=False): + while True: + frm = self.read_frame(hide) + if isinstance(frm, hyperframe.frame.SettingsFrame): + break + else: + self._handle_unexpected_frame(frm) + + 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 _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 = hyperframe.frame.SettingsFrame(flags=['ACK']) + self.send_frame(frm, hide) + + def _update_flow_control_window(self, stream_id, increment): + frm = hyperframe.frame.WindowUpdateFrame(stream_id=0, window_increment=increment) + self.send_frame(frm) + frm = hyperframe.frame.WindowUpdateFrame(stream_id=stream_id, window_increment=increment) + self.send_frame(frm) + + def _create_headers(self, headers, stream_id, end_stream=True): + def frame_cls(chunks): + for i in chunks: + if i == 0: + yield hyperframe.frame.HeadersFrame, i + else: + yield hyperframe.frame.ContinuationFrame, i + + header_block_fragment = self.encoder.encode(headers.fields) + + chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] + chunks = range(0, len(header_block_fragment), chunk_size) + frms = [frm_cls( + flags=[], + stream_id=stream_id, + data=header_block_fragment[i:i + chunk_size]) for frm_cls, i in frame_cls(chunks)] + + frms[-1].flags.add('END_HEADERS') + if end_stream: + frms[0].flags.add('END_STREAM') + + if self.dump_frames: # pragma no cover + for frm in frms: + print(frm.human_readable(">>")) + + return [frm.serialize() for frm in frms] + + def _create_body(self, body, stream_id): + if body is None or len(body) == 0: + return b'' + + chunk_size = self.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] + chunks = range(0, len(body), chunk_size) + frms = [hyperframe.frame.DataFrame( + flags=[], + stream_id=stream_id, + data=body[i:i + chunk_size]) for i in chunks] + frms[-1].flags.add('END_STREAM') + + if self.dump_frames: # pragma no cover + for frm in frms: + print(frm.human_readable(">>")) + + return [frm.serialize() for frm in frms] + + def _receive_transmission(self, stream_id=None, include_body=True): + if not include_body: + raise NotImplementedError() + + body_expected = True + + header_blocks = b'' + body = b'' + + while True: + frm = self.read_frame() + if ( + (isinstance(frm, hyperframe.frame.HeadersFrame) or isinstance(frm, hyperframe.frame.ContinuationFrame)) and + (stream_id is None or frm.stream_id == stream_id) + ): + stream_id = frm.stream_id + header_blocks += frm.data + if 'END_STREAM' in frm.flags: + body_expected = False + if 'END_HEADERS' in frm.flags: + break + else: + self._handle_unexpected_frame(frm) + + while body_expected: + frm = self.read_frame() + if isinstance(frm, hyperframe.frame.DataFrame) and frm.stream_id == stream_id: + body += frm.data + if 'END_STREAM' in frm.flags: + break + else: + self._handle_unexpected_frame(frm) + + headers = netlib.http.headers.Headers( + (k.encode('ascii'), v.encode('ascii')) for k, v in self.decoder.decode(header_blocks) + ) + + return stream_id, headers, body + + +class HTTP2Protocol(object): def __init__(self, pathod_handler): self.pathod_handler = pathod_handler - self.wire_protocol = http2.HTTP2Protocol( + self.wire_protocol = HTTP2StateProtocol( self.pathod_handler, is_server=True, dump_frames=self.pathod_handler.http2_framedump ) diff --git a/test/netlib/http/http2/test_connections.py b/test/netlib/http/http2/test_connections.py deleted file mode 100644 index 2a43627a..00000000 --- a/test/netlib/http/http2/test_connections.py +++ /dev/null @@ -1,544 +0,0 @@ -import mock -import codecs - -import hyperframe -from netlib import tcp, http -from netlib.tutils import raises -from netlib.exceptions import TcpDisconnect -from netlib.http.http2.connections import HTTP2Protocol, TCPHandler -from netlib.http.http2 import framereader - -from ... import tservers - - -class TestTCPHandlerWrapper: - def test_wrapped(self): - h = TCPHandler(rfile='foo', wfile='bar') - p = HTTP2Protocol(h) - assert p.tcp_handler.rfile == 'foo' - assert p.tcp_handler.wfile == 'bar' - - def test_direct(self): - p = HTTP2Protocol(rfile='foo', wfile='bar') - assert isinstance(p.tcp_handler, TCPHandler) - assert p.tcp_handler.rfile == 'foo' - assert p.tcp_handler.wfile == 'bar' - - -class EchoHandler(tcp.BaseHandler): - sni = None - - def handle(self): - while True: - v = self.rfile.safe_read(1) - self.wfile.write(v) - self.wfile.flush() - - -class TestProtocol: - @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface") - @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface") - def test_perform_connection_preface(self, mock_client_method, mock_server_method): - protocol = HTTP2Protocol(is_server=False) - protocol.connection_preface_performed = True - - protocol.perform_connection_preface() - assert not mock_client_method.called - assert not mock_server_method.called - - protocol.perform_connection_preface(force=True) - assert mock_client_method.called - assert not mock_server_method.called - - @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface") - @mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface") - def test_perform_connection_preface_server(self, mock_client_method, mock_server_method): - protocol = HTTP2Protocol(is_server=True) - protocol.connection_preface_performed = True - - protocol.perform_connection_preface() - assert not mock_client_method.called - assert not mock_server_method.called - - protocol.perform_connection_preface(force=True) - assert not mock_client_method.called - assert mock_server_method.called - - -class TestCheckALPNMatch(tservers.ServerTestBase): - handler = EchoHandler - ssl = dict( - alpn_select=b'h2', - ) - - if tcp.HAS_ALPN: - - def test_check_alpn(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl(alpn_protos=[b'h2']) - protocol = HTTP2Protocol(c) - assert protocol.check_alpn() - - -class TestCheckALPNMismatch(tservers.ServerTestBase): - handler = EchoHandler - ssl = dict( - alpn_select=None, - ) - - if tcp.HAS_ALPN: - - def test_check_alpn(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl(alpn_protos=[b'h2']) - protocol = HTTP2Protocol(c) - with raises(NotImplementedError): - protocol.check_alpn() - - -class TestPerformServerConnectionPreface(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - - def handle(self): - # send magic - self.wfile.write(codecs.decode('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a', 'hex_codec')) - self.wfile.flush() - - # send empty settings frame - self.wfile.write(codecs.decode('000000040000000000', 'hex_codec')) - self.wfile.flush() - - # check empty settings frame - raw = framereader.http2_read_raw_frame(self.rfile) - assert raw == codecs.decode('00000c040000000000000200000000000300000001', 'hex_codec') - - # check settings acknowledgement - raw = framereader.http2_read_raw_frame(self.rfile) - assert raw == codecs.decode('000000040100000000', 'hex_codec') - - # send settings acknowledgement - self.wfile.write(codecs.decode('000000040100000000', 'hex_codec')) - self.wfile.flush() - - def test_perform_server_connection_preface(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - protocol = HTTP2Protocol(c) - - assert not protocol.connection_preface_performed - protocol.perform_server_connection_preface() - assert protocol.connection_preface_performed - - with raises(TcpDisconnect): - protocol.perform_server_connection_preface(force=True) - - -class TestPerformClientConnectionPreface(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - - def handle(self): - # check magic - assert self.rfile.read(24) == HTTP2Protocol.CLIENT_CONNECTION_PREFACE - - # check empty settings frame - assert self.rfile.read(9) ==\ - codecs.decode('000000040000000000', 'hex_codec') - - # send empty settings frame - self.wfile.write(codecs.decode('000000040000000000', 'hex_codec')) - self.wfile.flush() - - # check settings acknowledgement - assert self.rfile.read(9) == \ - codecs.decode('000000040100000000', 'hex_codec') - - # send settings acknowledgement - self.wfile.write(codecs.decode('000000040100000000', 'hex_codec')) - self.wfile.flush() - - def test_perform_client_connection_preface(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - protocol = HTTP2Protocol(c) - - assert not protocol.connection_preface_performed - protocol.perform_client_connection_preface() - assert protocol.connection_preface_performed - - -class TestClientStreamIds(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - protocol = HTTP2Protocol(c) - - def test_client_stream_ids(self): - assert self.protocol.current_stream_id is None - assert self.protocol._next_stream_id() == 1 - assert self.protocol.current_stream_id == 1 - assert self.protocol._next_stream_id() == 3 - assert self.protocol.current_stream_id == 3 - assert self.protocol._next_stream_id() == 5 - assert self.protocol.current_stream_id == 5 - - -class TestServerStreamIds(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - protocol = HTTP2Protocol(c, is_server=True) - - def test_server_stream_ids(self): - assert self.protocol.current_stream_id is None - assert self.protocol._next_stream_id() == 2 - assert self.protocol.current_stream_id == 2 - assert self.protocol._next_stream_id() == 4 - assert self.protocol.current_stream_id == 4 - assert self.protocol._next_stream_id() == 6 - assert self.protocol.current_stream_id == 6 - - -class TestApplySettings(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - # check settings acknowledgement - assert self.rfile.read(9) == codecs.decode('000000040100000000', 'hex_codec') - self.wfile.write("OK") - self.wfile.flush() - self.rfile.safe_read(9) # just to keep the connection alive a bit longer - - ssl = True - - def test_apply_settings(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c) - - protocol._apply_settings({ - hyperframe.frame.SettingsFrame.ENABLE_PUSH: 'foo', - hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 'bar', - hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 'deadbeef', - }) - - assert c.rfile.safe_read(2) == b"OK" - - assert protocol.http2_settings[ - hyperframe.frame.SettingsFrame.ENABLE_PUSH] == 'foo' - assert protocol.http2_settings[ - hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS] == 'bar' - assert protocol.http2_settings[ - hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE] == 'deadbeef' - - -class TestCreateHeaders(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - - def test_create_headers(self): - headers = http.Headers([ - (b':method', b'GET'), - (b':path', b'index.html'), - (b':scheme', b'https'), - (b'foo', b'bar')]) - - bytes = HTTP2Protocol(self.c)._create_headers( - headers, 1, end_stream=True) - assert b''.join(bytes) ==\ - codecs.decode('000014010500000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec') - - bytes = HTTP2Protocol(self.c)._create_headers( - headers, 1, end_stream=False) - assert b''.join(bytes) ==\ - codecs.decode('000014010400000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec') - - def test_create_headers_multiple_frames(self): - headers = http.Headers([ - (b':method', b'GET'), - (b':path', b'/'), - (b':scheme', b'https'), - (b'foo', b'bar'), - (b'server', b'version')]) - - protocol = HTTP2Protocol(self.c) - protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 8 - bytes = protocol._create_headers(headers, 1, end_stream=True) - assert len(bytes) == 3 - assert bytes[0] == codecs.decode('000008010100000001828487408294e783', 'hex_codec') - assert bytes[1] == codecs.decode('0000080900000000018c767f7685ee5b10', 'hex_codec') - assert bytes[2] == codecs.decode('00000209040000000163d5', 'hex_codec') - - -class TestCreateBody(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - - def test_create_body_empty(self): - protocol = HTTP2Protocol(self.c) - bytes = protocol._create_body(b'', 1) - assert b''.join(bytes) == b'' - - def test_create_body_single_frame(self): - protocol = HTTP2Protocol(self.c) - bytes = protocol._create_body(b'foobar', 1) - assert b''.join(bytes) == codecs.decode('000006000100000001666f6f626172', 'hex_codec') - - def test_create_body_multiple_frames(self): - protocol = HTTP2Protocol(self.c) - protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 5 - bytes = protocol._create_body(b'foobarmehm42', 1) - assert len(bytes) == 3 - assert bytes[0] == codecs.decode('000005000000000001666f6f6261', 'hex_codec') - assert bytes[1] == codecs.decode('000005000000000001726d65686d', 'hex_codec') - assert bytes[2] == codecs.decode('0000020001000000013432', 'hex_codec') - - -class TestReadRequest(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - - def handle(self): - self.wfile.write( - codecs.decode('000003010400000001828487', 'hex_codec')) - self.wfile.write( - codecs.decode('000006000100000001666f6f626172', 'hex_codec')) - self.wfile.flush() - self.rfile.safe_read(9) # just to keep the connection alive a bit longer - - ssl = True - - def test_read_request(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c, is_server=True) - protocol.connection_preface_performed = True - - req = protocol.read_request(NotImplemented) - - assert req.stream_id - assert req.headers.fields == () - assert req.method == "GET" - assert req.path == "/" - assert req.scheme == "https" - assert req.content == b'foobar' - - -class TestReadRequestRelative(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - self.wfile.write( - codecs.decode('00000c0105000000014287d5af7e4d5a777f4481f9', 'hex_codec')) - self.wfile.flush() - - ssl = True - - def test_asterisk_form(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c, is_server=True) - protocol.connection_preface_performed = True - - req = protocol.read_request(NotImplemented) - - assert req.first_line_format == "relative" - assert req.method == "OPTIONS" - assert req.path == "*" - - -class TestReadRequestAbsolute(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - self.wfile.write( - codecs.decode('00001901050000000182448d9d29aee30c0e492c2a1170426366871c92585422e085', 'hex_codec')) - self.wfile.flush() - - ssl = True - - def test_absolute_form(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c, is_server=True) - protocol.connection_preface_performed = True - - req = protocol.read_request(NotImplemented) - - assert req.first_line_format == "absolute" - assert req.scheme == "http" - assert req.host == "address" - assert req.port == 22 - - -class TestReadRequestConnect(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - self.wfile.write( - codecs.decode('00001b0105000000014287bdab4e9c17b7ff44871c92585422e08541871c92585422e085', 'hex_codec')) - self.wfile.write( - codecs.decode('00001d0105000000014287bdab4e9c17b7ff44882f91d35d055c87a741882f91d35d055c87a7', 'hex_codec')) - self.wfile.flush() - - ssl = True - - def test_connect(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c, is_server=True) - protocol.connection_preface_performed = True - - req = protocol.read_request(NotImplemented) - assert req.first_line_format == "authority" - assert req.method == "CONNECT" - assert req.host == "address" - assert req.port == 22 - - req = protocol.read_request(NotImplemented) - assert req.first_line_format == "authority" - assert req.method == "CONNECT" - assert req.host == "example.com" - assert req.port == 443 - - -class TestReadResponse(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - self.wfile.write( - codecs.decode('00000801040000002a88628594e78c767f', 'hex_codec')) - self.wfile.write( - codecs.decode('00000600010000002a666f6f626172', 'hex_codec')) - self.wfile.flush() - self.rfile.safe_read(9) # just to keep the connection alive a bit longer - - ssl = True - - def test_read_response(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c) - protocol.connection_preface_performed = True - - resp = protocol.read_response(NotImplemented, stream_id=42) - - assert resp.http_version == "HTTP/2.0" - assert resp.status_code == 200 - assert resp.reason == '' - assert resp.headers.fields == ((b':status', b'200'), (b'etag', b'foobar')) - assert resp.content == b'foobar' - assert resp.timestamp_end - - -class TestReadEmptyResponse(tservers.ServerTestBase): - class handler(tcp.BaseHandler): - def handle(self): - self.wfile.write( - codecs.decode('00000801050000002a88628594e78c767f', 'hex_codec')) - self.wfile.flush() - - ssl = True - - def test_read_empty_response(self): - c = tcp.TCPClient(("127.0.0.1", self.port)) - with c.connect(): - c.convert_to_ssl() - protocol = HTTP2Protocol(c) - protocol.connection_preface_performed = True - - resp = protocol.read_response(NotImplemented, stream_id=42) - - assert resp.stream_id == 42 - assert resp.http_version == "HTTP/2.0" - assert resp.status_code == 200 - assert resp.reason == '' - assert resp.headers.fields == ((b':status', b'200'), (b'etag', b'foobar')) - assert resp.content == b'' - - -class TestAssembleRequest(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - - def test_request_simple(self): - bytes = HTTP2Protocol(self.c).assemble_request(http.Request( - b'', - b'GET', - b'https', - b'', - b'', - b'/', - b"HTTP/2.0", - (), - None, - )) - assert len(bytes) == 1 - assert bytes[0] == codecs.decode('00000d0105000000018284874188089d5c0b8170dc07', 'hex_codec') - - def test_request_with_stream_id(self): - req = http.Request( - b'', - b'GET', - b'https', - b'', - b'', - b'/', - b"HTTP/2.0", - (), - None, - ) - req.stream_id = 0x42 - bytes = HTTP2Protocol(self.c).assemble_request(req) - assert len(bytes) == 1 - assert bytes[0] == codecs.decode('00000d0105000000428284874188089d5c0b8170dc07', 'hex_codec') - - def test_request_with_body(self): - bytes = HTTP2Protocol(self.c).assemble_request(http.Request( - b'', - b'GET', - b'https', - b'', - b'', - b'/', - b"HTTP/2.0", - http.Headers([(b'foo', b'bar')]), - b'foobar', - )) - assert len(bytes) == 2 - assert bytes[0] ==\ - codecs.decode('0000150104000000018284874188089d5c0b8170dc07408294e7838c767f', 'hex_codec') - assert bytes[1] ==\ - codecs.decode('000006000100000001666f6f626172', 'hex_codec') - - -class TestAssembleResponse(object): - c = tcp.TCPClient(("127.0.0.1", 0)) - - def test_simple(self): - bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response( - b"HTTP/2.0", - 200, - )) - assert len(bytes) == 1 - assert bytes[0] ==\ - codecs.decode('00000101050000000288', 'hex_codec') - - def test_with_stream_id(self): - resp = http.Response( - b"HTTP/2.0", - 200, - ) - resp.stream_id = 0x42 - bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(resp) - assert len(bytes) == 1 - assert bytes[0] ==\ - codecs.decode('00000101050000004288', 'hex_codec') - - def test_with_body(self): - bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response( - b"HTTP/2.0", - 200, - b'', - http.Headers(foo=b"bar"), - b'foobar' - )) - assert len(bytes) == 2 - assert bytes[0] ==\ - codecs.decode('00000901040000000288408294e7838c767f', 'hex_codec') - assert bytes[1] ==\ - codecs.decode('000006000100000002666f6f626172', 'hex_codec') diff --git a/test/netlib/http/http2/test_framereader.py b/test/netlib/http/http2/test_framereader.py new file mode 100644 index 00000000..41b73189 --- /dev/null +++ b/test/netlib/http/http2/test_framereader.py @@ -0,0 +1 @@ +# foobar diff --git a/test/pathod/__init__.py b/test/pathod/__init__.py new file mode 100644 index 00000000..3f5dc124 --- /dev/null +++ b/test/pathod/__init__.py @@ -0,0 +1 @@ +from __future__ import (print_function, absolute_import, division) diff --git a/test/pathod/test_language_actions.py b/test/pathod/test_language_actions.py index f12d8105..2b1b6915 100644 --- a/test/pathod/test_language_actions.py +++ b/test/pathod/test_language_actions.py @@ -1,11 +1,10 @@ from six import BytesIO -from pathod.language import actions -from pathod import language +from pathod.language import actions, parse_pathoc, parse_pathod, serve def parse_request(s): - return next(language.parse_pathoc(s)) + return next(parse_pathoc(s)) def test_unique_name(): @@ -16,9 +15,9 @@ def test_unique_name(): class TestDisconnects: def test_parse_pathod(self): - a = next(language.parse_pathod("400:d0")).actions[0] + a = next(parse_pathod("400:d0")).actions[0] assert a.spec() == "d0" - a = next(language.parse_pathod("400:dr")).actions[0] + a = next(parse_pathod("400:dr")).actions[0] assert a.spec() == "dr" def test_at(self): @@ -42,12 +41,12 @@ class TestDisconnects: class TestInject: def test_parse_pathod(self): - a = next(language.parse_pathod("400:ir,@100")).actions[0] + a = next(parse_pathod("400:ir,@100")).actions[0] assert a.offset == "r" assert a.value.datatype == "bytes" assert a.value.usize == 100 - a = next(language.parse_pathod("400:ia,@100")).actions[0] + a = next(parse_pathod("400:ia,@100")).actions[0] assert a.offset == "a" def test_at(self): @@ -62,8 +61,8 @@ class TestInject: def test_serve(self): s = BytesIO() - r = next(language.parse_pathod("400:i0,'foo'")) - assert language.serve(r, s, {}) + r = next(parse_pathod("400:i0,'foo'")) + assert serve(r, s, {}) def test_spec(self): e = actions.InjectAt.expr() @@ -96,7 +95,7 @@ class TestPauses: assert v.offset == "a" def test_request(self): - r = next(language.parse_pathod('400:p10,10')) + r = next(parse_pathod('400:p10,10')) assert r.actions[0].spec() == "p10,10" def test_spec(self): diff --git a/test/pathod/test_language_base.py b/test/pathod/test_language_base.py index 7c7d8cf9..12a235e4 100644 --- a/test/pathod/test_language_base.py +++ b/test/pathod/test_language_base.py @@ -1,7 +1,8 @@ import os from pathod import language from pathod.language import base, exceptions -import tutils + +from . import tutils def parse_request(s): diff --git a/test/pathod/test_language_generators.py b/test/pathod/test_language_generators.py index 51f55991..4ec6ec3f 100644 --- a/test/pathod/test_language_generators.py +++ b/test/pathod/test_language_generators.py @@ -1,7 +1,7 @@ import os from pathod.language import generators -import tutils +from . import tutils def test_randomgenerator(): diff --git a/test/pathod/test_language_http.py b/test/pathod/test_language_http.py index 18059e3a..dd0b8d02 100644 --- a/test/pathod/test_language_http.py +++ b/test/pathod/test_language_http.py @@ -1,7 +1,8 @@ from six import BytesIO from pathod import language from pathod.language import http, base -import tutils + +from . import tutils def parse_request(s): diff --git a/test/pathod/test_language_http2.py b/test/pathod/test_language_http2.py index a2bffe63..f4b34047 100644 --- a/test/pathod/test_language_http2.py +++ b/test/pathod/test_language_http2.py @@ -1,12 +1,13 @@ from six import BytesIO -import netlib from netlib import tcp from netlib.http import user_agents from pathod import language from pathod.language import http2 -import tutils +from pathod.protocols.http2 import HTTP2StateProtocol + +from . import tutils def parse_request(s): @@ -20,7 +21,7 @@ def parse_response(s): def default_settings(): return language.Settings( request_host="foo.com", - protocol=netlib.http.http2.HTTP2Protocol(tcp.TCPClient(('localhost', 1234))) + protocol=HTTP2StateProtocol(tcp.TCPClient(('localhost', 1234))) ) diff --git a/test/pathod/test_language_websocket.py b/test/pathod/test_language_websocket.py index 58297141..89cbb772 100644 --- a/test/pathod/test_language_websocket.py +++ b/test/pathod/test_language_websocket.py @@ -2,7 +2,8 @@ from pathod import language from pathod.language import websockets import netlib.websockets -import tutils + +from . import tutils def parse_request(s): diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py index 7f26c247..28f9f0f8 100644 --- a/test/pathod/test_pathoc.py +++ b/test/pathod/test_pathoc.py @@ -5,11 +5,13 @@ from mock import Mock from netlib import http from netlib import tcp from netlib.exceptions import NetlibException -from netlib.http import http1, http2 +from netlib.http import http1 +from netlib.tutils import raises from pathod import pathoc, language -from netlib.tutils import raises -import tutils +from pathod.protocols.http2 import HTTP2StateProtocol + +from . import tutils def test_response(): @@ -219,7 +221,7 @@ class TestDaemonHTTP2(PathocTestDaemon): ssl=True, use_http2=True, ) - assert isinstance(c.protocol, http2.HTTP2Protocol) + assert isinstance(c.protocol, HTTP2StateProtocol) c = pathoc.Pathoc( ("127.0.0.1", self.d.port), diff --git a/test/pathod/test_pathoc_cmdline.py b/test/pathod/test_pathoc_cmdline.py index 35909325..922cf3a9 100644 --- a/test/pathod/test_pathoc_cmdline.py +++ b/test/pathod/test_pathoc_cmdline.py @@ -1,8 +1,10 @@ -from pathod import pathoc_cmdline as cmdline -import tutils from six.moves import cStringIO as StringIO import mock +from pathod import pathoc_cmdline as cmdline + +from . import tutils + @mock.patch("argparse.ArgumentParser.error") def test_pathoc(perror): diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py index dc02fffb..0b34f924 100644 --- a/test/pathod/test_pathod.py +++ b/test/pathod/test_pathod.py @@ -3,7 +3,8 @@ from six.moves import cStringIO as StringIO from pathod import pathod from netlib import tcp from netlib.exceptions import HttpException, TlsException -import tutils + +from . import tutils class TestPathod(object): diff --git a/test/pathod/test_pathod_cmdline.py b/test/pathod/test_pathod_cmdline.py index 18d54c82..58123b37 100644 --- a/test/pathod/test_pathod_cmdline.py +++ b/test/pathod/test_pathod_cmdline.py @@ -1,7 +1,9 @@ -from pathod import pathod_cmdline as cmdline -import tutils import mock +from pathod import pathod_cmdline as cmdline + +from . import tutils + def test_parse_anchor_spec(): assert cmdline.parse_anchor_spec("foo=200") == ("foo", "200") diff --git a/test/pathod/test_protocols_http2.py b/test/pathod/test_protocols_http2.py new file mode 100644 index 00000000..e42c2858 --- /dev/null +++ b/test/pathod/test_protocols_http2.py @@ -0,0 +1,545 @@ +import mock +import codecs + +import hyperframe +from netlib import tcp, http +from netlib.tutils import raises +from netlib.exceptions import TcpDisconnect +from netlib.http.http2 import framereader + +from ..netlib import tservers as netlib_tservers + +from pathod.protocols.http2 import HTTP2StateProtocol, TCPHandler + + +class TestTCPHandlerWrapper: + def test_wrapped(self): + h = TCPHandler(rfile='foo', wfile='bar') + p = HTTP2StateProtocol(h) + assert p.tcp_handler.rfile == 'foo' + assert p.tcp_handler.wfile == 'bar' + + def test_direct(self): + p = HTTP2StateProtocol(rfile='foo', wfile='bar') + assert isinstance(p.tcp_handler, TCPHandler) + assert p.tcp_handler.rfile == 'foo' + assert p.tcp_handler.wfile == 'bar' + + +class EchoHandler(tcp.BaseHandler): + sni = None + + def handle(self): + while True: + v = self.rfile.safe_read(1) + self.wfile.write(v) + self.wfile.flush() + + +class TestProtocol: + @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_server_connection_preface") + @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_client_connection_preface") + def test_perform_connection_preface(self, mock_client_method, mock_server_method): + protocol = HTTP2StateProtocol(is_server=False) + protocol.connection_preface_performed = True + + protocol.perform_connection_preface() + assert not mock_client_method.called + assert not mock_server_method.called + + protocol.perform_connection_preface(force=True) + assert mock_client_method.called + assert not mock_server_method.called + + @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_server_connection_preface") + @mock.patch("pathod.protocols.http2.HTTP2StateProtocol.perform_client_connection_preface") + def test_perform_connection_preface_server(self, mock_client_method, mock_server_method): + protocol = HTTP2StateProtocol(is_server=True) + protocol.connection_preface_performed = True + + protocol.perform_connection_preface() + assert not mock_client_method.called + assert not mock_server_method.called + + protocol.perform_connection_preface(force=True) + assert not mock_client_method.called + assert mock_server_method.called + + +class TestCheckALPNMatch(netlib_tservers.ServerTestBase): + handler = EchoHandler + ssl = dict( + alpn_select=b'h2', + ) + + if tcp.HAS_ALPN: + + def test_check_alpn(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl(alpn_protos=[b'h2']) + protocol = HTTP2StateProtocol(c) + assert protocol.check_alpn() + + +class TestCheckALPNMismatch(netlib_tservers.ServerTestBase): + handler = EchoHandler + ssl = dict( + alpn_select=None, + ) + + if tcp.HAS_ALPN: + + def test_check_alpn(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl(alpn_protos=[b'h2']) + protocol = HTTP2StateProtocol(c) + with raises(NotImplementedError): + protocol.check_alpn() + + +class TestPerformServerConnectionPreface(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + + def handle(self): + # send magic + self.wfile.write(codecs.decode('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a', 'hex_codec')) + self.wfile.flush() + + # send empty settings frame + self.wfile.write(codecs.decode('000000040000000000', 'hex_codec')) + self.wfile.flush() + + # check empty settings frame + raw = framereader.http2_read_raw_frame(self.rfile) + assert raw == codecs.decode('00000c040000000000000200000000000300000001', 'hex_codec') + + # check settings acknowledgement + raw = framereader.http2_read_raw_frame(self.rfile) + assert raw == codecs.decode('000000040100000000', 'hex_codec') + + # send settings acknowledgement + self.wfile.write(codecs.decode('000000040100000000', 'hex_codec')) + self.wfile.flush() + + def test_perform_server_connection_preface(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + protocol = HTTP2StateProtocol(c) + + assert not protocol.connection_preface_performed + protocol.perform_server_connection_preface() + assert protocol.connection_preface_performed + + with raises(TcpDisconnect): + protocol.perform_server_connection_preface(force=True) + + +class TestPerformClientConnectionPreface(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + + def handle(self): + # check magic + assert self.rfile.read(24) == HTTP2StateProtocol.CLIENT_CONNECTION_PREFACE + + # check empty settings frame + assert self.rfile.read(9) ==\ + codecs.decode('000000040000000000', 'hex_codec') + + # send empty settings frame + self.wfile.write(codecs.decode('000000040000000000', 'hex_codec')) + self.wfile.flush() + + # check settings acknowledgement + assert self.rfile.read(9) == \ + codecs.decode('000000040100000000', 'hex_codec') + + # send settings acknowledgement + self.wfile.write(codecs.decode('000000040100000000', 'hex_codec')) + self.wfile.flush() + + def test_perform_client_connection_preface(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + protocol = HTTP2StateProtocol(c) + + assert not protocol.connection_preface_performed + protocol.perform_client_connection_preface() + assert protocol.connection_preface_performed + + +class TestClientStreamIds(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + protocol = HTTP2StateProtocol(c) + + def test_client_stream_ids(self): + assert self.protocol.current_stream_id is None + assert self.protocol._next_stream_id() == 1 + assert self.protocol.current_stream_id == 1 + assert self.protocol._next_stream_id() == 3 + assert self.protocol.current_stream_id == 3 + assert self.protocol._next_stream_id() == 5 + assert self.protocol.current_stream_id == 5 + + +class TestserverstreamIds(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + protocol = HTTP2StateProtocol(c, is_server=True) + + def test_server_stream_ids(self): + assert self.protocol.current_stream_id is None + assert self.protocol._next_stream_id() == 2 + assert self.protocol.current_stream_id == 2 + assert self.protocol._next_stream_id() == 4 + assert self.protocol.current_stream_id == 4 + assert self.protocol._next_stream_id() == 6 + assert self.protocol.current_stream_id == 6 + + +class TestApplySettings(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + # check settings acknowledgement + assert self.rfile.read(9) == codecs.decode('000000040100000000', 'hex_codec') + self.wfile.write("OK") + self.wfile.flush() + self.rfile.safe_read(9) # just to keep the connection alive a bit longer + + ssl = True + + def test_apply_settings(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c) + + protocol._apply_settings({ + hyperframe.frame.SettingsFrame.ENABLE_PUSH: 'foo', + hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS: 'bar', + hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE: 'deadbeef', + }) + + assert c.rfile.safe_read(2) == b"OK" + + assert protocol.http2_settings[ + hyperframe.frame.SettingsFrame.ENABLE_PUSH] == 'foo' + assert protocol.http2_settings[ + hyperframe.frame.SettingsFrame.MAX_CONCURRENT_STREAMS] == 'bar' + assert protocol.http2_settings[ + hyperframe.frame.SettingsFrame.INITIAL_WINDOW_SIZE] == 'deadbeef' + + +class TestCreateHeaders(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + + def test_create_headers(self): + headers = http.Headers([ + (b':method', b'GET'), + (b':path', b'index.html'), + (b':scheme', b'https'), + (b'foo', b'bar')]) + + bytes = HTTP2StateProtocol(self.c)._create_headers( + headers, 1, end_stream=True) + assert b''.join(bytes) ==\ + codecs.decode('000014010500000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec') + + bytes = HTTP2StateProtocol(self.c)._create_headers( + headers, 1, end_stream=False) + assert b''.join(bytes) ==\ + codecs.decode('000014010400000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec') + + def test_create_headers_multiple_frames(self): + headers = http.Headers([ + (b':method', b'GET'), + (b':path', b'/'), + (b':scheme', b'https'), + (b'foo', b'bar'), + (b'server', b'version')]) + + protocol = HTTP2StateProtocol(self.c) + protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 8 + bytes = protocol._create_headers(headers, 1, end_stream=True) + assert len(bytes) == 3 + assert bytes[0] == codecs.decode('000008010100000001828487408294e783', 'hex_codec') + assert bytes[1] == codecs.decode('0000080900000000018c767f7685ee5b10', 'hex_codec') + assert bytes[2] == codecs.decode('00000209040000000163d5', 'hex_codec') + + +class TestCreateBody(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + + def test_create_body_empty(self): + protocol = HTTP2StateProtocol(self.c) + bytes = protocol._create_body(b'', 1) + assert b''.join(bytes) == b'' + + def test_create_body_single_frame(self): + protocol = HTTP2StateProtocol(self.c) + bytes = protocol._create_body(b'foobar', 1) + assert b''.join(bytes) == codecs.decode('000006000100000001666f6f626172', 'hex_codec') + + def test_create_body_multiple_frames(self): + protocol = HTTP2StateProtocol(self.c) + protocol.http2_settings[hyperframe.frame.SettingsFrame.MAX_FRAME_SIZE] = 5 + bytes = protocol._create_body(b'foobarmehm42', 1) + assert len(bytes) == 3 + assert bytes[0] == codecs.decode('000005000000000001666f6f6261', 'hex_codec') + assert bytes[1] == codecs.decode('000005000000000001726d65686d', 'hex_codec') + assert bytes[2] == codecs.decode('0000020001000000013432', 'hex_codec') + + +class TestReadRequest(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + + def handle(self): + self.wfile.write( + codecs.decode('000003010400000001828487', 'hex_codec')) + self.wfile.write( + codecs.decode('000006000100000001666f6f626172', 'hex_codec')) + self.wfile.flush() + self.rfile.safe_read(9) # just to keep the connection alive a bit longer + + ssl = True + + def test_read_request(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c, is_server=True) + protocol.connection_preface_performed = True + + req = protocol.read_request(NotImplemented) + + assert req.stream_id + assert req.headers.fields == () + assert req.method == "GET" + assert req.path == "/" + assert req.scheme == "https" + assert req.content == b'foobar' + + +class TestReadRequestRelative(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + self.wfile.write( + codecs.decode('00000c0105000000014287d5af7e4d5a777f4481f9', 'hex_codec')) + self.wfile.flush() + + ssl = True + + def test_asterisk_form(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c, is_server=True) + protocol.connection_preface_performed = True + + req = protocol.read_request(NotImplemented) + + assert req.first_line_format == "relative" + assert req.method == "OPTIONS" + assert req.path == "*" + + +class TestReadRequestAbsolute(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + self.wfile.write( + codecs.decode('00001901050000000182448d9d29aee30c0e492c2a1170426366871c92585422e085', 'hex_codec')) + self.wfile.flush() + + ssl = True + + def test_absolute_form(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c, is_server=True) + protocol.connection_preface_performed = True + + req = protocol.read_request(NotImplemented) + + assert req.first_line_format == "absolute" + assert req.scheme == "http" + assert req.host == "address" + assert req.port == 22 + + +class TestReadRequestConnect(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + self.wfile.write( + codecs.decode('00001b0105000000014287bdab4e9c17b7ff44871c92585422e08541871c92585422e085', 'hex_codec')) + self.wfile.write( + codecs.decode('00001d0105000000014287bdab4e9c17b7ff44882f91d35d055c87a741882f91d35d055c87a7', 'hex_codec')) + self.wfile.flush() + + ssl = True + + def test_connect(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c, is_server=True) + protocol.connection_preface_performed = True + + req = protocol.read_request(NotImplemented) + assert req.first_line_format == "authority" + assert req.method == "CONNECT" + assert req.host == "address" + assert req.port == 22 + + req = protocol.read_request(NotImplemented) + assert req.first_line_format == "authority" + assert req.method == "CONNECT" + assert req.host == "example.com" + assert req.port == 443 + + +class TestReadResponse(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + self.wfile.write( + codecs.decode('00000801040000002a88628594e78c767f', 'hex_codec')) + self.wfile.write( + codecs.decode('00000600010000002a666f6f626172', 'hex_codec')) + self.wfile.flush() + self.rfile.safe_read(9) # just to keep the connection alive a bit longer + + ssl = True + + def test_read_response(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c) + protocol.connection_preface_performed = True + + resp = protocol.read_response(NotImplemented, stream_id=42) + + assert resp.http_version == "HTTP/2.0" + assert resp.status_code == 200 + assert resp.reason == '' + assert resp.headers.fields == ((b':status', b'200'), (b'etag', b'foobar')) + assert resp.content == b'foobar' + assert resp.timestamp_end + + +class TestReadEmptyResponse(netlib_tservers.ServerTestBase): + class handler(tcp.BaseHandler): + def handle(self): + self.wfile.write( + codecs.decode('00000801050000002a88628594e78c767f', 'hex_codec')) + self.wfile.flush() + + ssl = True + + def test_read_empty_response(self): + c = tcp.TCPClient(("127.0.0.1", self.port)) + with c.connect(): + c.convert_to_ssl() + protocol = HTTP2StateProtocol(c) + protocol.connection_preface_performed = True + + resp = protocol.read_response(NotImplemented, stream_id=42) + + assert resp.stream_id == 42 + assert resp.http_version == "HTTP/2.0" + assert resp.status_code == 200 + assert resp.reason == '' + assert resp.headers.fields == ((b':status', b'200'), (b'etag', b'foobar')) + assert resp.content == b'' + + +class TestAssembleRequest(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + + def test_request_simple(self): + bytes = HTTP2StateProtocol(self.c).assemble_request(http.Request( + b'', + b'GET', + b'https', + b'', + b'', + b'/', + b"HTTP/2.0", + (), + None, + )) + assert len(bytes) == 1 + assert bytes[0] == codecs.decode('00000d0105000000018284874188089d5c0b8170dc07', 'hex_codec') + + def test_request_with_stream_id(self): + req = http.Request( + b'', + b'GET', + b'https', + b'', + b'', + b'/', + b"HTTP/2.0", + (), + None, + ) + req.stream_id = 0x42 + bytes = HTTP2StateProtocol(self.c).assemble_request(req) + assert len(bytes) == 1 + assert bytes[0] == codecs.decode('00000d0105000000428284874188089d5c0b8170dc07', 'hex_codec') + + def test_request_with_body(self): + bytes = HTTP2StateProtocol(self.c).assemble_request(http.Request( + b'', + b'GET', + b'https', + b'', + b'', + b'/', + b"HTTP/2.0", + http.Headers([(b'foo', b'bar')]), + b'foobar', + )) + assert len(bytes) == 2 + assert bytes[0] ==\ + codecs.decode('0000150104000000018284874188089d5c0b8170dc07408294e7838c767f', 'hex_codec') + assert bytes[1] ==\ + codecs.decode('000006000100000001666f6f626172', 'hex_codec') + + +class TestAssembleResponse(object): + c = tcp.TCPClient(("127.0.0.1", 0)) + + def test_simple(self): + bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(http.Response( + b"HTTP/2.0", + 200, + )) + assert len(bytes) == 1 + assert bytes[0] ==\ + codecs.decode('00000101050000000288', 'hex_codec') + + def test_with_stream_id(self): + resp = http.Response( + b"HTTP/2.0", + 200, + ) + resp.stream_id = 0x42 + bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(resp) + assert len(bytes) == 1 + assert bytes[0] ==\ + codecs.decode('00000101050000004288', 'hex_codec') + + def test_with_body(self): + bytes = HTTP2StateProtocol(self.c, is_server=True).assemble_response(http.Response( + b"HTTP/2.0", + 200, + b'', + http.Headers(foo=b"bar"), + b'foobar' + )) + assert len(bytes) == 2 + assert bytes[0] ==\ + codecs.decode('00000901040000000288408294e7838c767f', 'hex_codec') + assert bytes[1] ==\ + codecs.decode('000006000100000002666f6f626172', 'hex_codec') diff --git a/test/pathod/test_test.py b/test/pathod/test_test.py index 6399894e..d69e72f3 100644 --- a/test/pathod/test_test.py +++ b/test/pathod/test_test.py @@ -1,7 +1,8 @@ import logging import requests from pathod import test -import tutils + +from . import tutils import requests.packages.urllib3 diff --git a/test/pathod/test_utils.py b/test/pathod/test_utils.py index 2bb82fe7..2bdfe501 100644 --- a/test/pathod/test_utils.py +++ b/test/pathod/test_utils.py @@ -1,5 +1,6 @@ from pathod import utils -import tutils + +from . import tutils def test_membool(): -- cgit v1.2.3