Skip to content

Commit

Permalink
[WIP] Changed the way Python handlers work for H2, it now returns a F…
Browse files Browse the repository at this point in the history
…unctionHandler that has methods for specific frame handling. No longer requires a thread to parse the request. Refactored `finish_handling` to be cleaner split between h1 and h2.
  • Loading branch information
David Heiberg committed Aug 13, 2018
1 parent 00ff580 commit 38993c9
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 211 deletions.
2 changes: 2 additions & 0 deletions tools/wptserve/tests/functional/docroot/test_h2_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def handle_data(frame, request, response):
response.content = frame.data[::-1]
3 changes: 3 additions & 0 deletions tools/wptserve/tests/functional/docroot/test_h2_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def handle_headers(frame, request, response):
response.status = 203
response.headers.update([('test', 'passed')])
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def handle_headers(frame, request, response):
response.status = 203
response.headers.update([('test', 'passed')])

def handle_data(frame, request, response):
response.content = frame.data[::-1]
110 changes: 53 additions & 57 deletions tools/wptserve/tests/functional/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,82 +324,78 @@ def test_as_is(self):
#Add a check that the response is actually sane


class TestH2ResponseHandler(TestUsingH2Server):

class TestH2Handler(TestUsingH2Server):
def test_handle_headers(self):

class Handler(wptserve.handlers.H2ResponseHandler):
def handle_headers(self, frame, request, response):
if request.headers['path'] == "/h2test/test_handler":
response.headers.update([('test', 'passed')])
response.status = 203
else:
response.headers.update([('test', 'failed')])

route = ("GET", "/h2test/test_handler", Handler())
self.server.router.register(*route)
self.conn.request(route[0], route[1])
self.conn.request("GET", '/test_h2_headers.py')
resp = self.conn.get_response()

assert resp.status == 203
assert resp.headers['test'][0] == 'passed'
assert resp.read() == ''

def test_only_main(self):
self.conn.request("GET", '/test_tuple_3.py')
resp = self.conn.get_response()

assert resp.status == 202
assert resp.headers['Content-Type'][0] == 'text/html'
assert resp.headers['X-Test'][0] == 'PASS'
assert resp.read() == b'PASS'

def test_handle_data(self):
self.conn.request("POST", '/test_h2_data.py', body="hello world!")
resp = self.conn.get_response()

class Handler(wptserve.handlers.H2ResponseHandler):
def handle_data(self, frame, request, response):
data = frame.data
response.content = ''.join(reversed(data))
assert resp.status == 200
assert resp.read() == b'!dlrow olleh'

route = ("POST", "/h2test/test_handler", Handler())
self.server.router.register(*route)
self.conn.request(route[0], route[1], body="hello world")
def test_handle_headers_data(self):
self.conn.request("POST", '/test_h2_headers_data.py', body="hello world!")
resp = self.conn.get_response()

assert resp.status == 203
assert resp.headers['test'][0] == 'passed'
assert resp.read() == b'!dlrow olleh'

def test_no_main_or_handlers(self):
self.conn.request("GET", '/no_main.py')
resp = self.conn.get_response()
assert resp.status == 200
assert resp.read() == 'dlrow olleh'

def test_handle_data_no_headers(self):
# The HTTP/2.0 protocol does allow for just DATA frames to be written when a stream is
# in the OPEN state.
class Handler(wptserve.handlers.H2ResponseHandler):
def handle_data(self, frame, request, response):
data = frame.data
response.content = ''.join(reversed(data))
response.write_content()

route = ("POST", "/h2test/test_handler", Handler())
self.server.router.register(*route)
sid = self.conn.request(route[0], route[1], body="hello world")
assert self.conn.streams[sid]._read() == 'dlrow olleh'

assert resp.status == 500
assert "No main function or handlers in script " in json.loads(resp.read())["error"]["message"]

def test_handle_headers_data(self):
def test_not_found(self):
self.conn.request("GET", '/no_exist.py')
resp = self.conn.get_response()

class Handler(wptserve.handlers.H2ResponseHandler):
def handle_headers(self, frame, request, response):
if request.headers['path'] == "/h2test/test_handler":
response.headers.update([('test', 'passed')])
response.status = 203
else:
response.headers.update([('test', 'failed')])
response.status = 404
response.write_status_headers()

def handle_data(self, frame, request, response):
data = frame.data
response.content = ''.join(reversed(data))
response.write_content()

route = ("POST", "/h2test/test_handler", Handler())
self.server.router.register(*route)
self.conn.request(route[0], route[1], body="hello world")
assert resp.status == 404

def test_requesting_multiple_resources(self):
# 1st .py resource
self.conn.request("GET", '/test_h2_headers.py')
resp = self.conn.get_response()

assert resp.status == 203
assert resp.headers['test'][0] == 'passed'
assert resp.read() == ''

# 2nd .py resource
self.conn.request("GET", '/test_tuple_3.py')
resp = self.conn.get_response()

assert resp.status == 202
assert resp.headers['Content-Type'][0] == 'text/html'
assert resp.headers['X-Test'][0] == 'PASS'
assert resp.read() == b'PASS'

# 3rd .py resource
self.conn.request("GET", '/test_h2_headers.py')
resp = self.conn.get_response()

assert resp.status == 203
assert resp.headers['test'][0] == 'passed'
assert resp.read() == 'dlrow olleh'
assert resp.read() == ''


if __name__ == '__main__':
unittest.main()
10 changes: 0 additions & 10 deletions tools/wptserve/tests/functional/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ def handler(request, response):


class TestH2Response(TestUsingH2Server):

def test_write_without_ending_stream(self):
data = b"TEST"

Expand Down Expand Up @@ -259,7 +258,6 @@ def handler(request, response):
assert push.read() == push_data

def test_set_error(self):

@wptserve.handlers.handler
def handler(request, response):
response.set_error(503, message="Test error")
Expand All @@ -273,7 +271,6 @@ def handler(request, response):
assert json.loads(resp.read()) == json.loads("{\"error\": {\"message\": \"Test error\", \"code\": 503}}")

def test_file_like_response(self):

@wptserve.handlers.handler
def handler(request, response):
content = BytesIO("Hello, world!")
Expand All @@ -288,7 +285,6 @@ def handler(request, response):
assert resp.read() == "Hello, world!"

def test_list_response(self):

@wptserve.handlers.handler
def handler(request, response):
response.content = ['hello', 'world']
Expand All @@ -302,7 +298,6 @@ def handler(request, response):
assert resp.read() == "helloworld"

def test_content_longer_than_frame_size(self):

@wptserve.handlers.handler
def handler(request, response):
size = response.writer.get_max_payload_size()
Expand All @@ -320,7 +315,6 @@ def handler(request, response):
assert resp.read() == "a" * (payload_size + 5)

def test_encode(self):

@wptserve.handlers.handler
def handler(request, response):
response.encoding = "utf8"
Expand All @@ -336,7 +330,6 @@ def handler(request, response):
self.conn.get_response()

def test_raw_header_frame(self):

@wptserve.handlers.handler
def handler(request, response):
response.writer.write_raw_header_frame([
Expand All @@ -354,7 +347,6 @@ def handler(request, response):
assert resp.read() == ''

def test_raw_header_frame_invalid(self):

@wptserve.handlers.handler
def handler(request, response):
response.writer.write_raw_header_frame([
Expand All @@ -370,7 +362,6 @@ def handler(request, response):
self.conn.get_response()

def test_raw_data_frame(self):

@wptserve.handlers.handler
def handler(request, response):
response.writer.write_raw_data_frame(data=b'Hello world', end_stream=True)
Expand All @@ -382,7 +373,6 @@ def handler(request, response):
assert self.conn.streams[sid]._read() == 'Hello world'

def test_raw_header_continuation_frame(self):

@wptserve.handlers.handler
def handler(request, response):
response.writer.write_raw_header_frame([
Expand Down
90 changes: 52 additions & 38 deletions tools/wptserve/wptserve/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,17 @@ def __init__(self, base_path=None, url_base="/"):
def __repr__(self):
return "<%s base_path:%s url_base:%s>" % (self.__class__.__name__, self.base_path, self.url_base)

def __call__(self, request, response):
def _set_path_and_load_file(self, request, response, func):
"""
This modifies the `sys.path` and loads the requested python file as an environ variable.
Once the environ is loaded, the passed `func` is run with this loaded environ.
:param request: The request object
:param response: The response object
:param func: The function to be run with the loaded environ with the modified filepath. Signature: (request, response, environ, path)
:return: The return of func
"""
path = filesystem_path(self.base_path, request, self.url_base)

sys_path = sys.path[:]
Expand All @@ -242,22 +252,54 @@ def __call__(self, request, response):
sys.path.insert(0, os.path.dirname(path))
with open(path, 'rb') as f:
exec(compile(f.read(), path, 'exec'), environ, environ)

if func is not None:
return func(request, response, environ, path)

except IOError:
raise HTTPException(404)
finally:
sys.path = sys_path
sys.modules = sys_modules

def __call__(self, request, response):
def func(request, response, environ, path):
if "main" in environ:
handler = FunctionHandler(environ["main"])
handler(request, response)
wrap_pipeline(path, request, response)
elif "h2main" in environ:
handler = environ["h2main"](H2ResponseHandler)
handler(request, response)
wrap_pipeline(path, request, response)
else:
raise HTTPException(500, "No main function in script %s" % path)
except IOError:
raise HTTPException(404)
finally:
sys.path = sys_path
sys.modules = sys_modules

self._set_path_and_load_file(request, response, func)


def convert_to_frame_handler(self, request):
"""
This creates a FunctionHandler with one or more of the handling functions.
Used by the H2 server.
:param request: The request object used to generate the handler.
:return: A FunctionHandler object with one or more of these functions: `handle_headers`, `handle_data` or `main`
"""
def func(request, response, environ, path):
def _main(req, resp):
pass

handler = FunctionHandler(_main)
if "main" in environ:
handler.func = environ["main"]
if "handle_headers" in environ:
handler.handle_headers = environ["handle_headers"]
if "handle_data" in environ:
handler.handle_data = environ["handle_data"]

if handler.func is _main and not hasattr(handler, "handle_headers") and not hasattr(handler, "handle_data"):
raise HTTPException(500, "No main function or handlers in script %s" % path)

return handler
return self._set_path_and_load_file(request, None, func)

python_script_handler = PythonScriptHandler()

Expand Down Expand Up @@ -293,34 +335,6 @@ def __call__(self, request, response):
def handler(func):
return FunctionHandler(func)


class H2ResponseHandler(object):

def __init__(self):
self.func_map = {
'headers': None,
'data': None,
}

def __call__(self, request, response):
while True:
frame = request.frames.get(True, None)
if isinstance(frame, RequestReceived):
self.handle_headers(frame, request, response)
elif isinstance(frame, DataReceived):
self.handle_data(frame, request, response)
else:
raise ValueError('Frame type not recognized: ' + str(frame))

if frame.stream_ended:
break

def handle_headers(self, frame, request, response):
pass

def handle_data(self, frame, request, response):
pass

class JsonHandler(object):
def __init__(self, func):
self.func = func
Expand Down
3 changes: 1 addition & 2 deletions tools/wptserve/wptserve/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import tempfile

from six.moves.urllib.parse import parse_qsl, urlsplit
from six.moves.queue import Queue

from . import stash
from .utils import HTTPException
Expand Down Expand Up @@ -352,7 +351,7 @@ def auth(self):
class H2Request(Request):
def __init__(self, request_handler):
self.h2_stream_id = request_handler.h2_stream_id
self.frames = Queue()
self.frames = []
super(H2Request, self).__init__(request_handler)


Expand Down
Loading

0 comments on commit 38993c9

Please sign in to comment.