[sans-io] http: emit error hook when killed

This commit is contained in:
Maximilian Hils 2020-12-07 14:19:24 +01:00
parent 7087539c4d
commit 4f0cbec308
3 changed files with 39 additions and 28 deletions

View File

@ -183,7 +183,7 @@ class HttpStream(layer.Layer):
self.flow.request.host_header = self.context.server.address[0] self.flow.request.host_header = self.context.server.address[0]
yield HttpRequestHeadersHook(self.flow) yield HttpRequestHeadersHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(True)):
return return
if self.flow.request.headers.get("expect", "").lower() == "100-continue": if self.flow.request.headers.get("expect", "").lower() == "100-continue":
@ -227,13 +227,13 @@ class HttpStream(layer.Layer):
self.flow.request.data.content = self.request_body_buf self.flow.request.data.content = self.request_body_buf
self.request_body_buf = b"" self.request_body_buf = b""
yield HttpRequestHook(self.flow) yield HttpRequestHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(True)):
return return
elif self.flow.response: elif self.flow.response:
# response was set by an inline script. # response was set by an inline script.
# we now need to emulate the responseheaders hook. # we now need to emulate the responseheaders hook.
yield HttpResponseHeadersHook(self.flow) yield HttpResponseHeadersHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(True)):
return return
yield from self.send_response() yield from self.send_response()
else: else:
@ -253,7 +253,7 @@ class HttpStream(layer.Layer):
def state_wait_for_response_headers(self, event: ResponseHeaders) -> layer.CommandGenerator[None]: def state_wait_for_response_headers(self, event: ResponseHeaders) -> layer.CommandGenerator[None]:
self.flow.response = event.response self.flow.response = event.response
yield HttpResponseHeadersHook(self.flow) yield HttpResponseHeadersHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(True)):
return return
elif self.flow.response.stream: elif self.flow.response.stream:
yield SendHttp(event, self.context.client) yield SendHttp(event, self.context.client)
@ -286,23 +286,9 @@ class HttpStream(layer.Layer):
yield from self.send_response() yield from self.send_response()
self.server_state = self.state_done self.server_state = self.state_done
def check_killed(self) -> layer.CommandGenerator[bool]:
killed_by_us = (
self.flow.error and self.flow.error.msg == flow.Error.KILLED_MESSAGE
)
killed_by_remote = (
self.context.client.state is not ConnectionState.OPEN
)
if killed_by_us or killed_by_remote:
if self.context.client.state & ConnectionState.CAN_WRITE:
yield commands.CloseConnection(self.context.client)
self._handle_event = self.state_errored
return True
return False
def send_response(self): def send_response(self):
yield HttpResponseHook(self.flow) yield HttpResponseHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(False)):
return return
has_content = bool(self.flow.response.raw_content) has_content = bool(self.flow.response.raw_content)
yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, not has_content), self.context.client) yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, not has_content), self.context.client)
@ -310,6 +296,25 @@ class HttpStream(layer.Layer):
yield SendHttp(ResponseData(self.stream_id, self.flow.response.raw_content), self.context.client) yield SendHttp(ResponseData(self.stream_id, self.flow.response.raw_content), self.context.client)
yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client)
def check_killed(self, emit_error_hook: bool) -> layer.CommandGenerator[bool]:
killed_by_us = (
self.flow.error and self.flow.error.msg == flow.Error.KILLED_MESSAGE
)
killed_by_remote = (
self.context.client.state is not ConnectionState.OPEN
)
if killed_by_remote:
if not self.flow.error:
self.flow.error = flow.Error("Client disconnected.")
if killed_by_us or killed_by_remote:
if emit_error_hook:
yield HttpErrorHook(self.flow)
if self.context.client.state & ConnectionState.CAN_WRITE:
yield commands.CloseConnection(self.context.client)
self._handle_event = self.state_errored
return True
return False
def handle_protocol_error( def handle_protocol_error(
self, self,
event: typing.Union[RequestProtocolError, ResponseProtocolError] event: typing.Union[RequestProtocolError, ResponseProtocolError]
@ -331,7 +336,7 @@ class HttpStream(layer.Layer):
self.flow.error = flow.Error(event.message) self.flow.error = flow.Error(event.message)
yield HttpErrorHook(self.flow) yield HttpErrorHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(False)):
return return
if isinstance(event, ResponseProtocolError): if isinstance(event, ResponseProtocolError):
@ -353,7 +358,7 @@ class HttpStream(layer.Layer):
def handle_connect(self) -> layer.CommandGenerator[None]: def handle_connect(self) -> layer.CommandGenerator[None]:
yield HttpConnectHook(self.flow) yield HttpConnectHook(self.flow)
if (yield from self.check_killed()): if (yield from self.check_killed(False)):
return return
self.context.server.address = (self.flow.request.host, self.flow.request.port) self.context.server.address = (self.flow.request.host, self.flow.request.port)

View File

@ -44,6 +44,8 @@ class HttpErrorHook(commands.Hook):
An HTTP error has occurred, e.g. invalid server responses, or An HTTP error has occurred, e.g. invalid server responses, or
interrupted connections. This is distinct from a valid server HTTP interrupted connections. This is distinct from a valid server HTTP
error response, which is simply a response with an HTTP error code. error response, which is simply a response with an HTTP error code.
Every flow will receive either an error or an response event, but not both.
""" """
name = "error" name = "error"
flow: http.HTTPFlow flow: http.HTTPFlow

View File

@ -806,19 +806,23 @@ def test_kill_flow(tctx, when):
flow = Placeholder(HTTPFlow) flow = Placeholder(HTTPFlow)
def kill(flow: HTTPFlow): def kill(flow: HTTPFlow):
# Can't use flow.kill() here because that currently still depends on a reply object.
flow.error = Error(Error.KILLED_MESSAGE) flow.error = Error(Error.KILLED_MESSAGE)
def assert_kill(): def assert_kill(err_hook: bool = True):
assert (playbook playbook >> reply(side_effect=kill)
>> reply(side_effect=kill) if err_hook:
<< CloseConnection(tctx.client)) playbook << http.HttpErrorHook(flow)
playbook >> reply()
playbook << CloseConnection(tctx.client)
assert playbook
playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular))
assert (playbook assert (playbook
>> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n")
<< http.HttpConnectHook(connect_flow)) << http.HttpConnectHook(connect_flow))
if when == "http_connect": if when == "http_connect":
return assert_kill() return assert_kill(False)
assert (playbook assert (playbook
>> reply() >> reply()
<< SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n')
@ -853,14 +857,14 @@ def test_kill_flow(tctx, when):
>> reply() >> reply()
>> DataReceived(server, b"!") >> DataReceived(server, b"!")
<< http.HttpResponseHook(flow)) << http.HttpResponseHook(flow))
return assert_kill() return assert_kill(False)
elif when == "error": elif when == "error":
assert (playbook assert (playbook
>> reply() >> reply()
>> ConnectionClosed(server) >> ConnectionClosed(server)
<< CloseConnection(server) << CloseConnection(server)
<< http.HttpErrorHook(flow)) << http.HttpErrorHook(flow))
return assert_kill() return assert_kill(False)
else: else:
raise AssertionError raise AssertionError