From 8cc028764d3f1eccc02f46ab6b602e435db22b4d Mon Sep 17 00:00:00 2001 From: Channel Cat Date: Sun, 2 Oct 2016 02:18:41 +0000 Subject: [PATCH] Fixed keep-alive header and broken connection handling --- .gitignore | 2 +- README.md | 44 +++--- requirements.txt | 3 + sanic/__init__.py | 3 +- sanic/config.py | 40 ++--- sanic/log.py | 6 +- sanic/response.py | 53 ++++++- sanic/router.py | 34 +++-- sanic/sanic.py | 50 +++---- sanic/server.py | 374 ++++++++++++++++++++++------------------------ test.go | 15 ++ test.py | 13 ++ 12 files changed, 347 insertions(+), 290 deletions(-) create mode 100644 requirements.txt create mode 100644 test.go create mode 100644 test.py diff --git a/.gitignore b/.gitignore index 5a72cfaa..2220d02d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -settings.py +settings.py *.pyc \ No newline at end of file diff --git a/README.md b/README.md index ff9b0fc8..6f761fec 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ -# Sanic - -Python 3.5+ web server that's written to go fast - - ▄▄▄▄▄ - ▀▀▀██████▄▄▄ _______________ - ▄▄▄▄▄ █████████▄ / \ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | - ▀▀█████▄▄ ▀██████▄██ | _________________/ - ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ - ▀▀▀▄ ▀▀███ ▀ ▄▄ - ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ - ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ -▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ -▌ ▐▀████▐███▒▒▒▒▒▐██▌ -▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ - ▀▀█████████▀ - ▄▄██▀██████▀█ - ▄██▀ ▀▀▀ █ - ▄█ ▐▌ - ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ -▌ ▐ ▀▀▄▄▄▀ +# Sanic + +Python 3.5+ web server that's written to go fast + + ▄▄▄▄▄ + ▀▀▀██████▄▄▄ _______________ + ▄▄▄▄▄ █████████▄ / \ + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀█████▄▄ ▀██████▄██ | _________________/ + ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ + ▀▀▀▄ ▀▀███ ▀ ▄▄ + ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ + ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ +▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ +▌ ▐▀████▐███▒▒▒▒▒▐██▌ +▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ + ▀▀█████████▀ + ▄▄██▀██████▀█ + ▄██▀ ▀▀▀ █ + ▄█ ▐▌ + ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ +▌ ▐ ▀▀▄▄▄▀ ▀▀▄▄▀ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..439111e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +uvloop +httptools +ujson \ No newline at end of file diff --git a/sanic/__init__.py b/sanic/__init__.py index ec55186e..240e97b3 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,2 +1 @@ -from .sanic import Sanic -from .server import Response \ No newline at end of file +from .sanic import Sanic \ No newline at end of file diff --git a/sanic/config.py b/sanic/config.py index 567bf9ae..94453e31 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,21 +1,21 @@ -LOGO = """ - ▄▄▄▄▄ - ▀▀▀██████▄▄▄ _______________ - ▄▄▄▄▄ █████████▄ / \\ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | - ▀▀█████▄▄ ▀██████▄██ | _________________/ - ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ - ▀▀▀▄ ▀▀███ ▀ ▄▄ - ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ - ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ -▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ -▌ ▐▀████▐███▒▒▒▒▒▐██▌ -▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ - ▀▀█████████▀ - ▄▄██▀██████▀█ - ▄██▀ ▀▀▀ █ - ▄█ ▐▌ - ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ -▌ ▐ ▀▀▄▄▄▀ - ▀▀▄▄▀ +LOGO = """ + ▄▄▄▄▄ + ▀▀▀██████▄▄▄ _______________ + ▄▄▄▄▄ █████████▄ / \\ + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀█████▄▄ ▀██████▄██ | _________________/ + ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ + ▀▀▀▄ ▀▀███ ▀ ▄▄ + ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ + ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ +▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ +▌ ▐▀████▐███▒▒▒▒▒▐██▌ +▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ + ▀▀█████████▀ + ▄▄██▀██████▀█ + ▄██▀ ▀▀▀ █ + ▄█ ▐▌ + ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ +▌ ▐ ▀▀▄▄▄▀ + ▀▀▄▄▀ """ \ No newline at end of file diff --git a/sanic/log.py b/sanic/log.py index 6e31e92a..6893c42c 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,4 +1,4 @@ -import logging - -logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") +import logging + +logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) \ No newline at end of file diff --git a/sanic/response.py b/sanic/response.py index 20a2ab89..c19fee6c 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,6 +1,47 @@ -import ujson - -from .server import Response - -def json(input): - return Response(ujson.dumps(input), content_type="application/json") \ No newline at end of file +import ujson + +STATUS_CODES = { + 200: 'OK', + 404: 'Not Found' +} +class HTTPResponse: + __slots__ = ('body', 'status', 'content_type') + + def __init__(self, body='', status=200, content_type='text/plain'): + self.content_type = 'text/plain' + self.body = body + self.status = status + + @property + def body_bytes(self): + body_type = type(self.body) + if body_type is str: + body = self.body.encode('utf-8') + elif body_type is bytes: + body = self.body + else: + body = b'Unable to interpret body' + + return body + + def output(self, version="1.1", keep_alive=False): + body = self.body_bytes + return b''.join([ + 'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'), + 'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'), + 'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'), + 'Connection: {}\r\n'.format('keep-alive' if keep_alive else 'close').encode('latin-1'), + b'\r\n', + body, + #b'\r\n' + ]) + + +def error_404(request, *args): + return HTTPResponse("404!", status=404) +error_404.is_async = False + +def json(input): + return HTTPResponse(ujson.dumps(input), content_type="application/json") +def text(input): + return HTTPResponse(input, content_type="text/plain") \ No newline at end of file diff --git a/sanic/router.py b/sanic/router.py index 16bf6ba1..93ea07bc 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,15 +1,19 @@ -from .log import log - -class Router(): - routes = None - default = None - - def __init__(self, default=None): - self.routes = {} - self.default=default - - def add(self, route, handler): - self.routes[route] = handler - - def get(self, uri): - return self.routes.get(uri.decode('utf-8'), self.default) \ No newline at end of file +from .log import log + +class Router(): + routes = None + default = None + + def __init__(self, default=None): + self.routes = {} + self.default=default + + def add(self, route, handler): + self.routes[route] = handler + + def get(self, uri): + handler = self.routes.get(uri.decode('utf-8'), self.default) + if handler: + return handler + else: + return self.default \ No newline at end of file diff --git a/sanic/sanic.py b/sanic/sanic.py index 14f37679..eada7657 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,26 +1,24 @@ -import inspect -from .router import Router -from .server import Response, serve -from .log import log - -class Sanic: - name = None - routes = [] - - def __init__(self, name): - self.name = name - self.router = Router(default=self.handler_default) - - def route(self, *args, **kwargs): - def response(handler): - handler.is_async = inspect.iscoroutinefunction(handler) - self.router.add(*args, **kwargs, handler=handler) - return handler - - return response - - def run(self, host="127.0.0.1", port=8000, debug=False): - return serve(router=self.router, host=host, port=port, debug=debug) - - def handler_default(self, request, *args): - return Response("404!", status=404) +import inspect +from .router import Router +from .response import HTTPResponse, error_404 +from .server import serve +from .log import log + +class Sanic: + name = None + routes = [] + + def __init__(self, name, router=None): + self.name = name + self.router = router or Router(default=error_404) + + def route(self, *args, **kwargs): + def response(handler): + handler.is_async = inspect.iscoroutinefunction(handler) + self.router.add(*args, **kwargs, handler=handler) + return handler + + return response + + def run(self, host="127.0.0.1", port=8000, debug=False): + return serve(router=self.router, host=host, port=port, debug=debug) \ No newline at end of file diff --git a/sanic/server.py b/sanic/server.py index 0d5df313..b6bb8dab 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,196 +1,180 @@ -import argparse -import sys -import asyncio -import signal -import functools -import httptools -import logging - -import httptools -try: - import uvloop as async_loop -except: - async_loop = asyncio - -from socket import * - -from .log import log -from .config import LOGO - -PRINT = 0 - -class Request: - __slots__ = ('protocol', 'url', 'headers', 'version', 'method') - - def __init__(self, protocol, url, headers, version, method): - self.protocol = protocol - self.url = url - self.headers = headers - self.version = version - self.method = method - -STATUS_CODES = { - 200: 'OK', - 404: 'Not Found' -} -class Response: - __slots__ = ('body', 'status', 'content_type') - - def __init__(self, body='', status=200, content_type='text/plain'): - self.content_type = 'text/plain' - self.body = body - self.status = status - - @property - def body_bytes(self): - body_type = type(self.body) - if body_type is str: - body = self.body.encode('utf-8') - elif body_type is bytes: - body = self.body - else: - body = b'Unable to interpret body' - - return body - - def output(self, version): - body = self.body_bytes - return b''.join([ - 'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'), - 'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'), - 'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'), - b'\r\n', - body - ]) - - -class HttpProtocol(asyncio.Protocol): - - __slots__ = ('loop', - 'transport', 'request', 'parser', - 'url', 'headers', 'router') - - def __init__(self, *, router, loop): - self.loop = loop - self.transport = None - self.request = None - self.parser = None - self.url = None - self.headers = None - self.router = router - - # -------------------------------------------- # - # Connection - # -------------------------------------------- # - - def connection_made(self, transport): - self.transport = transport - # TCP Nodelay - # I have no evidence to support this makes anything faster - # So I'll leave it commented out for now - - # sock = transport.get_extra_info('socket') - # try: - # sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) - # except (OSError, NameError): - # pass - - def connection_lost(self, exc): - self.request = self.parser = None - - # -------------------------------------------- # - # Parsing - # -------------------------------------------- # - - def data_received(self, data): - if self.parser is None: - assert self.request is None - self.headers = [] - self.parser = httptools.HttpRequestParser(self) - - self.parser.feed_data(data) - - def on_url(self, url): - self.url = url - - def on_header(self, name, value): - self.headers.append((name, value)) - - def on_headers_complete(self): - self.request = Request( - protocol=self, - url=self.url, - headers=dict(self.headers), - version=self.parser.get_http_version(), - method=self.parser.get_method() - ) - self.loop.call_soon(self.handle, self.request) - - # -------------------------------------------- # - # Responding - # -------------------------------------------- # - - def handle(self, request): - handler = self.router.get(request.url) - if handler.is_async: - future = asyncio.Future() - self.loop.create_task(self.handle_response(future, handler, request)) - future.add_done_callback(self.handle_result) - else: - response = handler(request) - self.write_response(response) - - def write_response(self, response): - self.transport.write(response.output(request.version)) - - if not self.parser.should_keep_alive(): - self.transport.close() - self.parser = None - self.request = None - - # -------------------------------------------- # - # Async - # -------------------------------------------- # - - async def handle_response(self, future, handler, request): - result = await handler(request) - future.set_result(result) - - def handle_result(self, future): - response = future.result() - self.write_response(response) - - -def abort(msg): - log.info(msg, file=sys.stderr) - sys.exit(1) - - -def serve(router, host, port, debug=False): - # Create Event Loop - loop = async_loop.new_event_loop() - asyncio.set_event_loop(loop) - loop.set_debug(debug) - - # Add signal handlers - def ask_exit(signame): - log.debug("Exiting, received signal %s" % signame) - loop.stop() - - for signame in ('SIGINT', 'SIGTERM'): - loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame)) - - if debug: - log.setLevel(logging.DEBUG) - log.debug(LOGO) - - # Serve - log.info('Goin\' Fast @ {}:{}'.format(host, port)) - - server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port) - server_loop = loop.run_until_complete(server_coroutine) - try: - loop.run_forever() - finally: - server_loop.close() +import argparse +import sys +import asyncio +import signal +import functools +import httptools +import logging + +import httptools +try: + import uvloop as async_loop +except: + async_loop = asyncio + +from socket import * + +from .log import log +from .config import LOGO +from .response import HTTPResponse + +PRINT = 0 + +class Request: + __slots__ = ('protocol', 'url', 'headers', 'version', 'method') + + def __init__(self, protocol, url, headers, version, method): + self.protocol = protocol + self.url = url + self.headers = headers + self.version = version + self.method = method + +class HttpProtocol(asyncio.Protocol): + + __slots__ = ('loop', + 'transport', 'request', 'parser', + 'url', 'headers', 'router') + + def __init__(self, *, router, loop): + self.loop = loop + self.transport = None + self.request = None + self.parser = None + self.url = None + self.headers = None + self.router = router + + # -------------------------------------------- # + # Connection + # -------------------------------------------- # + + def connection_made(self, transport): + self.transport = transport + #TODO: handle keep-alive/connection timeout + + # TCP Nodelay + # I have no evidence to support this makes anything faster + # So I'll leave it commented out for now + + # sock = transport.get_extra_info('socket') + # try: + # sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) + # except (OSError, NameError): + # pass + + def connection_lost(self, exc): + self.request = self.parser = None + + # -------------------------------------------- # + # Parsing + # -------------------------------------------- # + + def data_received(self, data): + if self.parser is None: + assert self.request is None + self.headers = [] + self.parser = httptools.HttpRequestParser(self) + + try: + #print(data) + self.parser.feed_data(data) + except httptools.parser.errors.HttpParserError: + #log.error("Invalid request data, connection closed") + self.transport.close() + + def on_url(self, url): + self.url = url + + def on_header(self, name, value): + self.headers.append((name, value)) + + def on_headers_complete(self): + self.request = Request( + protocol=self, + url=self.url, + headers=dict(self.headers), + version=self.parser.get_http_version(), + method=self.parser.get_method() + ) + global n + n += 1 + self.n = n + #print("res {} - {}".format(n, self.request)) + self.loop.call_soon(self.handle, self.request) + + # -------------------------------------------- # + # Responding + # -------------------------------------------- # + + def handle(self, request): + handler = self.router.get(request.url) + if handler.is_async: + future = asyncio.Future() + self.loop.create_task(self.handle_response(future, handler, request)) + future.add_done_callback(self.handle_result) + else: + response = handler(request) + self.write_response(request, response) + + def write_response(self, request, response): + #print("response - {} - {}".format(self.n, self.request)) + try: + keep_alive = self.parser.should_keep_alive() + self.transport.write(response.output(request.version, keep_alive)) + #print("KA - {}".format(self.parser.should_keep_alive())) + if not keep_alive: + self.transport.close() + except: + log.error("Writing request failed, connection closed") + self.transport.close() + + self.parser = None + self.request = None + + # -------------------------------------------- # + # Async + # -------------------------------------------- # + + async def handle_response(self, future, handler, request): + response = await handler(request) + future.set_result((request, response)) + + def handle_result(self, future): + request, response = future.result() + self.write_response(request, response) + + +def abort(msg): + log.info(msg, file=sys.stderr) + sys.exit(1) + + +def serve(router, host, port, debug=False): + # Create Event Loop + loop = async_loop.new_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(debug) + + # Add signal handlers + def ask_exit(signame): + log.debug("Exiting, received signal %s" % signame) + loop.stop() + + for signame in ('SIGINT', 'SIGTERM'): + loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame)) + + if debug: + log.setLevel(logging.DEBUG) + log.debug(LOGO) + + # Serve + log.info('Goin\' Fast @ {}:{}'.format(host, port)) + + server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port) + server_loop = loop.run_until_complete(server_coroutine) + try: + loop.run_forever() + finally: + server_loop.close() loop.close() \ No newline at end of file diff --git a/test.go b/test.go new file mode 100644 index 00000000..c022c00e --- /dev/null +++ b/test.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) +} + +func main() { + http.HandleFunc("/", handler) + http.ListenAndServe(":8000", nil) +} diff --git a/test.py b/test.py new file mode 100644 index 00000000..4d32cba6 --- /dev/null +++ b/test.py @@ -0,0 +1,13 @@ +from sanic import Sanic +from sanic.response import json, text + +app = Sanic("test") + +@app.route("/") +async def test(request): + return json({ "test": True }) +@app.route("/text") +def test(request): + return text('hi') + +app.run(host="0.0.0.0") \ No newline at end of file