mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 08:11:00 +00:00
http2: add basic protocol handling
This commit is contained in:
parent
e4c129026f
commit
5cecbdc168
@ -1 +1,170 @@
|
||||
from __future__ import (absolute_import, print_function, division)
|
||||
import itertools
|
||||
|
||||
from .. import utils
|
||||
from .frame import *
|
||||
|
||||
|
||||
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 = b'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):
|
||||
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.get_alpn_proto_negotiated()
|
||||
if alp != self.ALPN_PROTO_H2:
|
||||
raise NotImplementedError(
|
||||
"H2Client can not handle unknown ALP: %s" % alp)
|
||||
print("-> Successfully negotiated 'h2' application layer protocol.")
|
||||
|
||||
def send_connection_preface(self):
|
||||
self.wfile.write(bytes(self.CLIENT_CONNECTION_PREFACE.decode('hex')))
|
||||
self.send_frame(SettingsFrame(state=self))
|
||||
|
||||
frame = Frame.from_file(self.rfile, self)
|
||||
assert isinstance(frame, SettingsFrame)
|
||||
self._apply_settings(frame.settings)
|
||||
self.read_frame() # read setting ACK frame
|
||||
|
||||
print("-> 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.wfile.write(raw_bytes)
|
||||
self.wfile.flush()
|
||||
|
||||
def read_frame(self):
|
||||
frame = Frame.from_file(self.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
|
||||
print("-> Setting changed: %s to %d (was %s)" % (
|
||||
SettingsFrame.SETTINGS.get_name(setting),
|
||||
value,
|
||||
str(old_value)))
|
||||
|
||||
self.send_frame(SettingsFrame(state=self, flags=Frame.FLAG_ACK))
|
||||
print("-> 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
|
||||
|
||||
bytes = HeadersFrame(
|
||||
state=self,
|
||||
flags=flags,
|
||||
stream_id=stream_id,
|
||||
headers=headers).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
|
||||
else:
|
||||
print("Unexpected frame received:")
|
||||
print(frame.human_readable())
|
||||
|
||||
while True:
|
||||
frame = self.read_frame()
|
||||
if isinstance(frame, DataFrame):
|
||||
body += frame.payload
|
||||
if frame.flags | Frame.FLAG_END_STREAM:
|
||||
break
|
||||
else:
|
||||
print("Unexpected frame received:")
|
||||
print(frame.human_readable())
|
||||
|
||||
headers = {}
|
||||
for header, value in self.decoder.decode(header_block_fragment):
|
||||
headers[header] = value
|
||||
|
||||
return headers[':status'], headers, body
|
||||
|
@ -20,16 +20,24 @@ class Frame(object):
|
||||
FLAG_PADDED = 0x8
|
||||
FLAG_PRIORITY = 0x20
|
||||
|
||||
def __init__(self, state=None, length=0, flags=FLAG_NO_FLAGS, stream_id=0x0):
|
||||
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:
|
||||
from . import HTTP2Protocol
|
||||
|
||||
class State(object):
|
||||
pass
|
||||
|
||||
state = State()
|
||||
state.http2_settings = HTTP2Protocol.HTTP2_DEFAULT_SETTINGS.copy()
|
||||
state.encoder = Encoder()
|
||||
state.decoder = Decoder()
|
||||
|
||||
@ -40,6 +48,14 @@ class Frame(object):
|
||||
self.flags = flags
|
||||
self.stream_id = stream_id
|
||||
|
||||
def _check_frame_size(self, length):
|
||||
max_length = self.state.http2_settings[
|
||||
SettingsFrame.SETTINGS.SETTINGS_MAX_FRAME_SIZE]
|
||||
if length > max_length:
|
||||
raise NotImplementedError(
|
||||
"Frame size exceeded: %d, but only %d allowed." % (
|
||||
length, max_length))
|
||||
|
||||
@classmethod
|
||||
def from_file(self, fp, state=None):
|
||||
"""
|
||||
@ -54,8 +70,15 @@ class Frame(object):
|
||||
flags = fields[3]
|
||||
stream_id = fields[4]
|
||||
|
||||
# TODO: check frame size if <= current SETTINGS_MAX_FRAME_SIZE
|
||||
|
||||
payload = fp.safe_read(length)
|
||||
return FRAMES[fields[2]].from_bytes(state, length, flags, stream_id, payload)
|
||||
return FRAMES[fields[2]].from_bytes(
|
||||
state,
|
||||
length,
|
||||
flags,
|
||||
stream_id,
|
||||
payload)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(self, data, state=None):
|
||||
@ -64,12 +87,20 @@ class Frame(object):
|
||||
# type is already deducted from class
|
||||
flags = fields[3]
|
||||
stream_id = fields[4]
|
||||
return FRAMES[fields[2]].from_bytes(state, length, flags, stream_id, data[9:])
|
||||
|
||||
return FRAMES[fields[2]].from_bytes(
|
||||
state,
|
||||
length,
|
||||
flags,
|
||||
stream_id,
|
||||
data[9:])
|
||||
|
||||
def to_bytes(self):
|
||||
payload = self.payload_bytes()
|
||||
self.length = len(payload)
|
||||
|
||||
self._check_frame_size(self.length)
|
||||
|
||||
b = struct.pack('!HB', self.length & 0xFFFF00, self.length & 0x0000FF)
|
||||
b += struct.pack('!B', self.TYPE)
|
||||
b += struct.pack('!B', self.flags)
|
||||
@ -183,19 +214,20 @@ class HeadersFrame(Frame):
|
||||
|
||||
if f.flags & self.FLAG_PADDED:
|
||||
f.pad_length = struct.unpack('!B', payload[0])[0]
|
||||
header_block_fragment = payload[1:-f.pad_length]
|
||||
f.header_block_fragment = payload[1:-f.pad_length]
|
||||
else:
|
||||
header_block_fragment = payload[0:]
|
||||
f.header_block_fragment = payload[0:]
|
||||
|
||||
if f.flags & self.FLAG_PRIORITY:
|
||||
f.stream_dependency, f.weight = struct.unpack(
|
||||
'!LB', header_block_fragment[:5])
|
||||
f.exclusive = bool(f.stream_dependency >> 31)
|
||||
f.stream_dependency &= 0x7FFFFFFF
|
||||
header_block_fragment = header_block_fragment[5:]
|
||||
f.header_block_fragment = f.header_block_fragment[5:]
|
||||
|
||||
for header, value in f.state.decoder.decode(header_block_fragment):
|
||||
f.headers.append((header, value))
|
||||
# TODO only do this if END_HEADERS or something...
|
||||
# for header, value in f.state.decoder.decode(f.header_block_fragment):
|
||||
# f.headers.append((header, value))
|
||||
|
||||
return f
|
||||
|
||||
@ -217,6 +249,8 @@ class HeadersFrame(Frame):
|
||||
(int(self.exclusive) << 31) | self.stream_dependency,
|
||||
self.weight)
|
||||
|
||||
# TODO: maybe remove that and only deal with header_block_fragments
|
||||
# inside frames
|
||||
b += self.state.encoder.encode(self.headers)
|
||||
|
||||
if self.flags & self.FLAG_PADDED:
|
||||
|
@ -1,89 +0,0 @@
|
||||
from .. import utils, odict, tcp
|
||||
from frame import *
|
||||
|
||||
# "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
||||
CLIENT_CONNECTION_PREFACE = '505249202a20485454502f322e300d0a0d0a534d0d0a0d0a'
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
class H2Client(tcp.TCPClient):
|
||||
ALPN_PROTO_H2 = b'h2'
|
||||
|
||||
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, address, source_address=None):
|
||||
super(H2Client, self).__init__(address, source_address)
|
||||
self.settings = self.DEFAULT_SETTINGS.copy()
|
||||
|
||||
def connect(self, send_preface=True):
|
||||
super(H2Client, self).connect()
|
||||
self.convert_to_ssl(alpn_protos=[self.ALPN_PROTO_H2])
|
||||
|
||||
alp = self.get_alpn_proto_negotiated()
|
||||
if alp != b'h2':
|
||||
raise NotImplementedError(
|
||||
"H2Client can not handle unknown protocol: %s" %
|
||||
alp)
|
||||
print "-> Successfully negotiated 'h2' application layer protocol."
|
||||
|
||||
if send_preface:
|
||||
self.wfile.write(bytes(CLIENT_CONNECTION_PREFACE.decode('hex')))
|
||||
self.send_frame(SettingsFrame())
|
||||
|
||||
frame = Frame.from_file(self.rfile)
|
||||
print frame.human_readable()
|
||||
assert isinstance(frame, SettingsFrame)
|
||||
self.apply_settings(frame.settings)
|
||||
|
||||
print "-> Connection Preface completed."
|
||||
|
||||
print "-> H2Client is ready..."
|
||||
|
||||
def send_frame(self, frame):
|
||||
self.wfile.write(frame.to_bytes())
|
||||
self.wfile.flush()
|
||||
|
||||
def read_frame(self):
|
||||
frame = Frame.from_file(self.rfile)
|
||||
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.settings[setting]
|
||||
if not old_value:
|
||||
old_value = '-'
|
||||
|
||||
self.settings[setting] = value
|
||||
print "-> Setting changed: %s to %d (was %s)" %
|
||||
(SettingsFrame.SETTINGS.get_name(setting),
|
||||
value,
|
||||
str(old_value))
|
||||
|
||||
self.send_frame(SettingsFrame(flags=Frame.FLAG_ACK))
|
||||
print "-> New settings acknowledged."
|
@ -1,18 +0,0 @@
|
||||
from netlib.h2.frame import *
|
||||
from netlib.h2.h2 import *
|
||||
|
||||
c = H2Client(("127.0.0.1", 443))
|
||||
c.connect()
|
||||
|
||||
c.send_frame(HeadersFrame(
|
||||
flags=(Frame.FLAG_END_HEADERS | Frame.FLAG_END_STREAM),
|
||||
stream_id=0x1,
|
||||
headers=[
|
||||
(b':method', 'GET'),
|
||||
(b':path', b'/index.html'),
|
||||
(b':scheme', b'https'),
|
||||
(b':authority', b'localhost'),
|
||||
]))
|
||||
|
||||
while True:
|
||||
print c.read_frame().human_readable()
|
@ -3,6 +3,7 @@ import tutils
|
||||
|
||||
from nose.tools import assert_equal
|
||||
|
||||
|
||||
def test_invalid_flags():
|
||||
tutils.raises(
|
||||
ValueError,
|
||||
|
Loading…
Reference in New Issue
Block a user