mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 00:01:36 +00:00
[sans-io] add http fuzzing tests
This commit is contained in:
parent
a568721e86
commit
b9e3b46fd8
@ -58,7 +58,6 @@ class Client(Connection):
|
||||
|
||||
class Server(Connection):
|
||||
state = ConnectionState.CLOSED
|
||||
|
||||
peername = None
|
||||
sockname = None
|
||||
address: Optional[tuple]
|
||||
|
@ -368,7 +368,8 @@ class HttpStream(layer.Layer):
|
||||
yield from self.child_layer.handle_event(events.Start())
|
||||
self._handle_event = self.passthrough
|
||||
else:
|
||||
return (yield from self.send_response())
|
||||
yield from self.send_response()
|
||||
return SendHttp(ResponseProtocolError(self.stream_id, "EOF"), self.context.client)
|
||||
|
||||
@expect(RequestData, RequestEndOfMessage, events.Event)
|
||||
def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]:
|
||||
|
@ -5,7 +5,7 @@ import h11
|
||||
from h11._readers import ChunkedReader, ContentLengthReader, Http10Reader
|
||||
from h11._receivebuffer import ReceiveBuffer
|
||||
|
||||
from mitmproxy import http
|
||||
from mitmproxy import exceptions, http
|
||||
from mitmproxy.net.http import http1
|
||||
from mitmproxy.net.http.http1 import read_sansio as http1_sansio
|
||||
from mitmproxy.proxy2 import commands, events, layer
|
||||
@ -167,12 +167,14 @@ class Http1Server(Http1Connection):
|
||||
if request_head:
|
||||
request_head = [bytes(x) for x in request_head] # TODO: Make url.parse compatible with bytearrays
|
||||
try:
|
||||
self.request = http.HTTPRequest.wrap(http1_sansio.read_request_head(request_head))
|
||||
except ValueError as e:
|
||||
yield commands.Log(f"{human.format_address(self.conn.address)}: {e}")
|
||||
req = http1_sansio.read_request_head(request_head)
|
||||
expected_body_size = http1.expected_http_body_size(req, expect_continue_as_0=False)
|
||||
except (ValueError, exceptions.HttpSyntaxException) as e:
|
||||
yield commands.Log(f"{human.format_address(self.conn.peername)}: {e}")
|
||||
yield commands.CloseConnection(self.conn)
|
||||
self.state = self.wait
|
||||
return
|
||||
self.request = http.HTTPRequest.wrap(req)
|
||||
yield ReceiveHttp(RequestHeaders(self.stream_id, self.request))
|
||||
|
||||
if self.request.first_line_format == "authority":
|
||||
@ -182,8 +184,7 @@ class Http1Server(Http1Connection):
|
||||
# https://http2.github.io/http2-spec/#CONNECT
|
||||
self.state = self.wait
|
||||
else:
|
||||
expected_size = http1.expected_http_body_size(self.request, expect_continue_as_0=False)
|
||||
self.body_reader = self.make_body_reader(expected_size)
|
||||
self.body_reader = self.make_body_reader(expected_body_size)
|
||||
self.state = self.read_request_body
|
||||
yield from self.state(event)
|
||||
else:
|
||||
@ -271,14 +272,24 @@ class Http1Client(Http1Connection):
|
||||
@expect(events.ConnectionEvent)
|
||||
def read_response_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]:
|
||||
if isinstance(event, events.DataReceived):
|
||||
if not self.request:
|
||||
# we just received some data for an unknown request.
|
||||
yield commands.Log(f"Unexpected data from server: {bytes(self.buf)!r}")
|
||||
yield commands.CloseConnection(self.conn)
|
||||
return
|
||||
|
||||
response_head = self.buf.maybe_extract_lines()
|
||||
|
||||
if response_head:
|
||||
response_head = [bytes(x) for x in response_head]
|
||||
self.response = http.HTTPResponse.wrap(http1_sansio.read_response_head(response_head))
|
||||
response_head = [bytes(x) for x in response_head] # TODO: Make url.parse compatible with bytearrays
|
||||
try:
|
||||
resp = http1_sansio.read_response_head(response_head)
|
||||
expected_size = http1.expected_http_body_size(self.request, resp)
|
||||
except (ValueError, exceptions.HttpSyntaxException) as e:
|
||||
yield ReceiveHttp(ResponseProtocolError(self.stream_id, f"Cannot parse HTTP response: {e}"))
|
||||
yield commands.CloseConnection(self.conn)
|
||||
return
|
||||
self.response = http.HTTPResponse.wrap(resp)
|
||||
yield ReceiveHttp(ResponseHeaders(self.stream_id, self.response))
|
||||
|
||||
expected_size = http1.expected_http_body_size(self.request, self.response)
|
||||
self.body_reader = self.make_body_reader(expected_size)
|
||||
|
||||
self.state = self.read_response_body
|
||||
@ -288,8 +299,8 @@ class Http1Client(Http1Connection):
|
||||
elif isinstance(event, events.ConnectionClosed):
|
||||
if self.stream_id:
|
||||
if self.buf:
|
||||
yield ReceiveHttp(
|
||||
ResponseProtocolError(self.stream_id, f"unexpected server response: {bytes(self.buf)}"))
|
||||
yield ReceiveHttp(ResponseProtocolError(self.stream_id,
|
||||
f"unexpected server response: {bytes(self.buf)!r}"))
|
||||
else:
|
||||
# The server has closed the connection to prevent us from continuing.
|
||||
# We need to signal that to the stream.
|
||||
|
125
test/mitmproxy/proxy2/layers/test_http_fuzz.py
Normal file
125
test/mitmproxy/proxy2/layers/test_http_fuzz.py
Normal file
@ -0,0 +1,125 @@
|
||||
import os
|
||||
from typing import Iterable
|
||||
|
||||
import pytest
|
||||
from hypothesis import example, given, seed, settings
|
||||
from hypothesis.strategies import binary, composite, integers, lists, sampled_from
|
||||
|
||||
from mitmproxy import options
|
||||
from mitmproxy.addons.proxyserver import Proxyserver
|
||||
from mitmproxy.proxy.protocol.http import HTTPMode
|
||||
from mitmproxy.proxy2 import context, events
|
||||
from mitmproxy.proxy2.commands import OpenConnection, SendData
|
||||
from mitmproxy.proxy2.events import DataReceived, Start
|
||||
from mitmproxy.proxy2.layers import http
|
||||
from test.mitmproxy.proxy2.tutils import Placeholder, Playbook, reply
|
||||
|
||||
settings.register_profile("full", max_examples=100_000, deadline=None)
|
||||
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def opts():
|
||||
opts = options.Options()
|
||||
Proxyserver().load(opts)
|
||||
return opts
|
||||
|
||||
|
||||
request_lines = sampled_from([
|
||||
b"GET / HTTP/1.1",
|
||||
b"GET http://example.com/ HTTP/1.1",
|
||||
b"CONNECT example.com:443 HTTP/1.1",
|
||||
b"HEAD /foo HTTP/0.9",
|
||||
])
|
||||
response_lines = sampled_from([
|
||||
b"HTTP/1.1 200 OK",
|
||||
b"HTTP/1.1 100 Continue",
|
||||
b"HTTP/0.9 204 No Content",
|
||||
b"HEAD /foo HTTP/0.9",
|
||||
])
|
||||
headers = lists(sampled_from([
|
||||
b"Host: example.com",
|
||||
b"Content-Length: 5",
|
||||
b"Expect: 100-continue",
|
||||
b"Transfer-Encoding: chunked",
|
||||
b"Connection: close",
|
||||
b"",
|
||||
]))
|
||||
bodies = sampled_from([
|
||||
b"",
|
||||
b"12345",
|
||||
b"5\r\n12345\r\n0\r\n\r\n"
|
||||
])
|
||||
|
||||
|
||||
def mutate(draw, data: bytes) -> bytes:
|
||||
cut_start = draw(integers(0, len(data)))
|
||||
cut_end = draw(integers(cut_start, len(data)))
|
||||
data = data[:cut_start] + data[cut_end:]
|
||||
|
||||
replace_start = draw(integers(0, len(data)))
|
||||
replace_end = draw(integers(replace_start, len(data)))
|
||||
return data[:replace_start] + draw(binary()) + data[replace_end:]
|
||||
|
||||
|
||||
def split(draw, data: bytes) -> Iterable[bytes]:
|
||||
a, b = sorted([
|
||||
draw(integers(0, len(data))),
|
||||
draw(integers(0, len(data)))
|
||||
])
|
||||
if a > 0:
|
||||
yield data[:a]
|
||||
if a != b:
|
||||
yield data[a:b]
|
||||
if b < len(data):
|
||||
yield data[b:]
|
||||
|
||||
|
||||
@composite
|
||||
def fuzz_request(draw):
|
||||
request = draw(request_lines) + b"\r\n"
|
||||
request += b"\r\n".join(draw(headers))
|
||||
request += b"\r\n\r\n" + draw(bodies)
|
||||
request = mutate(draw, request)
|
||||
request = list(split(draw, request))
|
||||
return request
|
||||
|
||||
|
||||
@composite
|
||||
def fuzz_response(draw):
|
||||
response = draw(response_lines) + b"\r\n"
|
||||
response += b"\r\n".join(draw(headers))
|
||||
response += b"\r\n\r\n" + draw(bodies)
|
||||
response = mutate(draw, response)
|
||||
response = list(split(draw, response))
|
||||
return response
|
||||
|
||||
|
||||
@given(fuzz_request())
|
||||
def test_fuzz_request(opts, data):
|
||||
tctx = context.Context(context.Client(("client", 1234), ("127.0.0.1", 8080)), opts)
|
||||
|
||||
layer = http.HttpLayer(tctx, HTTPMode.regular)
|
||||
for _ in layer.handle_event(Start()):
|
||||
pass
|
||||
for chunk in data:
|
||||
for _ in layer.handle_event(DataReceived(tctx.client, chunk)):
|
||||
pass
|
||||
|
||||
|
||||
@given(fuzz_response())
|
||||
@example([b'0 OK\r\n\r\n', b'\r\n', b'5\r\n12345\r\n0\r\n\r\n'])
|
||||
def test_fuzz_response(opts, data):
|
||||
tctx = context.Context(context.Client(("client", 1234), ("127.0.0.1", 8080)), opts)
|
||||
server = Placeholder()
|
||||
playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False)
|
||||
assert (
|
||||
playbook
|
||||
>> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||
<< OpenConnection(server)
|
||||
>> reply(None)
|
||||
<< SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
||||
)
|
||||
for chunk in data:
|
||||
for _ in playbook.layer.handle_event(events.DataReceived(server(), chunk)):
|
||||
pass
|
Loading…
Reference in New Issue
Block a user