├── nacho ├── renderers │ ├── __init__.py │ ├── quik.py │ └── jinja2.py ├── __init__.py ├── routing.py ├── http.py ├── app.py └── multithreading.py ├── MANIFEST.in ├── tests ├── __init__.py ├── runtests.py └── http_server_test.py ├── example ├── html │ └── home.html ├── favicon.ico └── main.py ├── docs └── _static │ ├── nacho.jpg │ └── nacho.png ├── requirements.txt ├── .travis.yml ├── .gitignore ├── setup.py ├── LICENSE └── README.rst /nacho/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /example/html/home.html: -------------------------------------------------------------------------------- 1 |

$title

2 | home page here -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avelino/nacho/HEAD/example/favicon.ico -------------------------------------------------------------------------------- /docs/_static/nacho.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avelino/nacho/HEAD/docs/_static/nacho.jpg -------------------------------------------------------------------------------- /docs/_static/nacho.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avelino/nacho/HEAD/docs/_static/nacho.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e hg+https://code.google.com/p/tulip/#egg=tulip 2 | quik 3 | jinja2 4 | blinker 5 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | from http_server_test import * 4 | 5 | if __name__ == '__main__': 6 | unittest.main() 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | install: 5 | - pip install -r requirements.txt 6 | - python setup.py install 7 | script: python tests/runtests.py 8 | notifications: 9 | irc: "irc.freenode.org#nacho" 10 | on_success: "never" 11 | -------------------------------------------------------------------------------- /nacho/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, division, print_function, with_statement 4 | 5 | 6 | VERSION = (0, 1) 7 | __version__ = ".".join(map(str, VERSION)) 8 | __status__ = "Development" 9 | __description__ = "Pythonic web micro-framework" 10 | __author__ = "Thiago Avelino" 11 | __license__ = "MIT License" -------------------------------------------------------------------------------- /nacho/renderers/quik.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from quik import FileLoader 3 | 4 | 5 | class QuikWorker(object): 6 | 7 | def __init__(self, template_dirs=['html']): 8 | self.template_dirs = template_dirs 9 | 10 | def render(self, template_name, *args, **kwargs): 11 | loader = FileLoader(self.template_dirs[0]) 12 | template = loader.load_template(template_name) 13 | return template.render(kwargs, loader=loader).encode('utf-8') 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Run application 38 | *.pid 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup, find_packages 4 | 5 | import nacho 6 | 7 | 8 | setup(name='nacho', 9 | version=nacho.__version__, 10 | description=nacho.__description__, 11 | long_description=nacho.__description__, 12 | author=nacho.__author__, 13 | license=nacho.__license__, 14 | packages=find_packages(exclude=('doc', 'docs', 'example')), 15 | package_dir={'nacho': 'nacho'}, 16 | #scripts=['nacho/bin/nacho'], 17 | include_package_data=True) 18 | -------------------------------------------------------------------------------- /nacho/renderers/jinja2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from jinja2 import Environment, FileSystemLoader, TemplateNotFound 3 | 4 | 5 | class Jinja2Worker(object): 6 | 7 | def __init__(self, template_dirs=['html']): 8 | self.template_dirs = template_dirs 9 | 10 | def render(self, template_name, *args, **kwargs): 11 | env = Environment(loader=FileSystemLoader(self.template_dirs)) 12 | try: 13 | template = env.get_template(template_name) 14 | except TemplateNotFound: 15 | raise TemplateNotFound(template_name) 16 | 17 | return template.render(kwargs).encode('utf-8') 18 | -------------------------------------------------------------------------------- /nacho/routing.py: -------------------------------------------------------------------------------- 1 | import re 2 | import collections 3 | 4 | class Router(object): 5 | def __init__(self, handlers=None): 6 | self.handlers = handlers or [] 7 | 8 | def add_handler(self, url_regex, handlers): 9 | self.handlers.append( 10 | (re.compile(url_regex), 11 | handlers if isinstance(handlers, collections.Iterable) 12 | else [handlers])) 13 | 14 | def get_handler(self, url): 15 | for matcher, handler in self.handlers: 16 | match = matcher.match(url) 17 | if match: 18 | return handler, match.groups() 19 | return None, None 20 | -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import sys 4 | 5 | assert sys.version >= '3.3', 'Please use Python 3.3 or higher.' 6 | 7 | from nacho.routing import Router 8 | from nacho.http import HttpServer 9 | from nacho.multithreading import Superviser 10 | from nacho.app import Application, StaticFile 11 | 12 | 13 | class Home(Application): 14 | def get(self, request_args=None): 15 | data = {'title': 'Nacho Application Server'} 16 | self.render('home.html', **data) 17 | 18 | 19 | def urls(): 20 | router = Router() 21 | router.add_handler('/static/', 22 | StaticFile('/Users/avelino/projects/nacho/example/')) 23 | router.add_handler('/(.*)', Home()) 24 | return HttpServer(router, debug=True, keep_alive=75) 25 | 26 | 27 | if __name__ == '__main__': 28 | logging.basicConfig(level=logging.DEBUG) 29 | superviser = Superviser() 30 | superviser.start(urls) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Thiago Avelino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /nacho/http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | 4 | import tulip 5 | from tulip.http import ServerHttpProtocol 6 | from tulip.http.errors import HttpErrorException 7 | 8 | class HttpServer(ServerHttpProtocol): 9 | def __init__(self, router, *args, **kwargs): 10 | super(HttpServer, self).__init__(*args, **kwargs) 11 | self.router = router 12 | 13 | @tulip.coroutine 14 | def handle_request(self, message, payload): 15 | response = None 16 | logging.debug('method = {!r}; path = {!r}; version = {!r}'.format( 17 | message.method, message.path, message.version)) 18 | 19 | handlers, args = self.router.get_handler(message.path) 20 | if handlers: 21 | for handler in handlers: 22 | logging.debug("handler: %s", handler) 23 | handler.initialize(self, message, payload, prev_response=response) 24 | result = handler(request_args=args) 25 | response = handler.response 26 | if not response: 27 | raise HttpErrorException(404, message="No Handler found") 28 | else: 29 | raise HttpErrorException(404) 30 | 31 | response.write_eof() 32 | if response.keep_alive(): 33 | self.keep_alive(True) 34 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | nacho 2 | ===== 3 | Python micro web-framework and asynchronous networking library tulip, support Python 3.x 4 | 5 | .. image:: https://raw.github.com/avelino/nacho/master/docs/_static/nacho.png 6 | 7 | .. image:: https://drone.io/github.com/avelino/nacho/status.png 8 | :target: https://drone.io/github.com/avelino/nacho/latest) 9 | :alt: Build Status - Drone 10 | 11 | .. image:: https://travis-ci.org/avelino/nacho.png?branch=master 12 | :target: https://travis-ci.org/avelino/nacho 13 | :alt: Build Status - Travis CI 14 | 15 | 16 | Our goals 17 | ========= 18 | 19 | - It was designed to work on Python >= 3.3 20 | - tulip is default http server 21 | - Templates are done by **Jinja2** 22 | - HTML5 as the big-main-thing 23 | - Work friendly with NoSQL (otherwise we should stop talking about them) 24 | - Handle asynchronous requests properly 25 | 26 | 27 | Parameters Server 28 | ================= 29 | 30 | - **host** - the hostname to listen on. Set this to '0.0.0.0' to have the server available externally as well. Defaults to *'127.0.0.1'*. 31 | - **port** - the port of the webserver. Defaults to *7000* or the port defined in the SERVER_NAME config variable if present. 32 | - **workers** - the workers number. Defaults to *1*. 33 | - **iocp** - the operacional sistem Windows IOCP event loop. Defaults to *False*. 34 | - **ssl** - the ssl mode. Defaults to *False* 35 | -------------------------------------------------------------------------------- /nacho/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cgi 3 | import os 4 | import tulip 5 | import tulip.http 6 | import email.message 7 | from urllib.parse import urlparse 8 | from tulip.http.errors import HttpErrorException 9 | 10 | from nacho.renderers.quik import QuikWorker 11 | 12 | 13 | class Application(object): 14 | 15 | template_dirs = ['html'] 16 | http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] 17 | 18 | def __init__(self, write_headers=True): 19 | self.response = None 20 | self.write_headers = write_headers 21 | self.renderer = QuikWorker(self.template_dirs) 22 | 23 | def initialize(self, server, message, payload, prev_response=None): 24 | self.server = server 25 | self.request = message 26 | self.payload = payload 27 | self.prev_response = prev_response 28 | if self.write_headers: 29 | self.response = self._write_headers() 30 | 31 | def __call__(self, request_args=None): 32 | if self.request.method.lower() in self.http_method_names: 33 | handler = getattr(self, self.request.method.lower(), None) 34 | if handler: 35 | return handler() 36 | self.response.write(b'nacho: base handler') 37 | return self.response 38 | 39 | @property 40 | def query(self): 41 | parsed = urlparse(self.request.path) 42 | querydict = cgi.parse_qs(parsed.query) 43 | for key, value in querydict.items(): 44 | if isinstance(value, list) and len(value) < 2: 45 | querydict[key] = value[0] if value else None 46 | return querydict 47 | 48 | def _write_headers(self): 49 | headers = email.message.Message() 50 | response = tulip.http.Response( 51 | self.server.transport, 200, close=True) 52 | response.add_header('Transfer-Encoding', 'chunked') 53 | 54 | # content encoding 55 | accept_encoding = headers.get('accept-encoding', '').lower() 56 | if 'deflate' in accept_encoding: 57 | response.add_header('Content-Encoding', 'deflate') 58 | response.add_compression_filter('deflate') 59 | elif 'gzip' in accept_encoding: 60 | response.add_header('Content-Encoding', 'gzip') 61 | response.add_compression_filter('gzip') 62 | 63 | response.add_chunking_filter(1025) 64 | 65 | response.add_header('Content-type', 'text/html') 66 | response.send_headers() 67 | return response 68 | 69 | def render(self, template_name, **kwargs): 70 | self.response.write(self.renderer.render(template_name, **kwargs)) 71 | 72 | 73 | class StaticFile(Application): 74 | def __init__(self, staticroot): 75 | super(StaticFile, self).__init__(write_headers=False) 76 | self.staticroot = staticroot 77 | 78 | def __call__(self, request_args=None): 79 | path = self.staticroot 80 | if not os.path.exists(path): 81 | print('no file', repr(path)) 82 | path = None 83 | else: 84 | isdir = os.path.isdir(path) 85 | 86 | if not path: 87 | raise HttpErrorException(404, message="Path not found") 88 | 89 | headers = email.message.Message() 90 | response = tulip.http.Response( 91 | self.server.transport, 200, close=True) 92 | response.add_header('Transfer-Encoding', 'chunked') 93 | 94 | if isdir: 95 | response.add_header('Content-type', 'text/html') 96 | response.send_headers() 97 | 98 | response.write(b'') 113 | else: 114 | response.add_header('Content-type', 'text/plain') 115 | response.send_headers() 116 | 117 | try: 118 | with open(path, 'rb') as fp: 119 | chunk = fp.read(8196) 120 | while chunk: 121 | response.write(chunk) 122 | chunk = fp.read(8196) 123 | except OSError: 124 | response.write(b'Cannot open') 125 | return response 126 | -------------------------------------------------------------------------------- /nacho/multithreading.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import socket 4 | import signal 5 | import time 6 | import tulip 7 | import argparse 8 | import tulip.http 9 | from tulip.http import websocket 10 | try: 11 | import ssl 12 | except ImportError: # pragma: no cover 13 | ssl = None 14 | 15 | 16 | ARGS = argparse.ArgumentParser(description="Run simple http server.") 17 | ARGS.add_argument( 18 | '--host', action="store", dest='host', 19 | default='127.0.0.1', help='Host name') 20 | ARGS.add_argument( 21 | '--port', action="store", dest='port', 22 | default=7000, type=int, help='Port number') 23 | ARGS.add_argument( 24 | '--iocp', action="store_true", dest='iocp', help='Windows IOCP event loop') 25 | ARGS.add_argument( 26 | '--ssl', action="store_true", dest='ssl', help='Run ssl mode.') 27 | ARGS.add_argument( 28 | '--sslcert', action="store", dest='certfile', help='SSL cert file.') 29 | ARGS.add_argument( 30 | '--sslkey', action="store", dest='keyfile', help='SSL key file.') 31 | ARGS.add_argument( 32 | '--workers', action="store", dest='workers', 33 | default=1, type=int, help='Number of workers.') 34 | ARGS.add_argument( 35 | '--staticroot', action="store", dest='staticroot', 36 | default='./static/', type=str, help='Static root.') 37 | 38 | 39 | class ChildProcess: 40 | 41 | def __init__(self, up_read, down_write, args, sock, protocol_factory, ssl): 42 | self.up_read = up_read 43 | self.down_write = down_write 44 | self.args = args 45 | self.sock = sock 46 | self.protocol_factory = protocol_factory 47 | self.ssl = ssl 48 | 49 | def start(self): 50 | # start server 51 | self.loop = loop = tulip.new_event_loop() 52 | tulip.set_event_loop(loop) 53 | 54 | def stop(): 55 | self.loop.stop() 56 | os._exit(0) 57 | loop.add_signal_handler(signal.SIGINT, stop) 58 | 59 | f = loop.start_serving( 60 | self.protocol_factory, sock=self.sock, ssl=self.ssl) 61 | x = loop.run_until_complete(f)[0] 62 | print('Starting srv worker process {} on {}'.format( 63 | os.getpid(), x.getsockname())) 64 | 65 | # heartbeat 66 | self.heartbeat() 67 | 68 | tulip.get_event_loop().run_forever() 69 | os._exit(0) 70 | 71 | @tulip.task 72 | def heartbeat(self): 73 | # setup pipes 74 | read_transport, read_proto = yield from self.loop.connect_read_pipe( 75 | tulip.StreamProtocol, os.fdopen(self.up_read, 'rb')) 76 | write_transport, _ = yield from self.loop.connect_write_pipe( 77 | tulip.StreamProtocol, os.fdopen(self.down_write, 'wb')) 78 | 79 | reader = read_proto.set_parser(websocket.WebSocketParser()) 80 | writer = websocket.WebSocketWriter(write_transport) 81 | 82 | while True: 83 | msg = yield from reader.read() 84 | if msg is None: 85 | print('Superviser is dead, {} stopping...'.format(os.getpid())) 86 | self.loop.stop() 87 | break 88 | elif msg.tp == websocket.MSG_PING: 89 | writer.pong() 90 | elif msg.tp == websocket.MSG_CLOSE: 91 | break 92 | 93 | read_transport.close() 94 | write_transport.close() 95 | 96 | 97 | class Worker: 98 | 99 | _started = False 100 | 101 | def __init__(self, loop, args, sock, protocol_factory, ssl): 102 | self.loop = loop 103 | self.args = args 104 | self.sock = sock 105 | self.protocol_factory = protocol_factory 106 | self.ssl = ssl 107 | self.start() 108 | 109 | def start(self): 110 | assert not self._started 111 | self._started = True 112 | 113 | up_read, up_write = os.pipe() 114 | down_read, down_write = os.pipe() 115 | args, sock = self.args, self.sock 116 | 117 | pid = os.fork() 118 | if pid: 119 | # parent 120 | os.close(up_read) 121 | os.close(down_write) 122 | self.connect(pid, up_write, down_read) 123 | else: 124 | # child 125 | os.close(up_write) 126 | os.close(down_read) 127 | 128 | # cleanup after fork 129 | tulip.set_event_loop(None) 130 | 131 | # setup process 132 | process = ChildProcess(up_read, down_write, args, sock, 133 | self.protocol_factory, self.ssl) 134 | process.start() 135 | 136 | @tulip.task 137 | def heartbeat(self, writer): 138 | while True: 139 | yield from tulip.sleep(15) 140 | 141 | if (time.monotonic() - self.ping) < 30: 142 | writer.ping() 143 | else: 144 | print('Restart unresponsive worker process: {}'.format( 145 | self.pid)) 146 | self.kill() 147 | self.start() 148 | return 149 | 150 | @tulip.task 151 | def chat(self, reader): 152 | while True: 153 | msg = yield from reader.read() 154 | if msg is None: 155 | print('Restart unresponsive worker process: {}'.format( 156 | self.pid)) 157 | self.kill() 158 | self.start() 159 | return 160 | elif msg.tp == websocket.MSG_PONG: 161 | self.ping = time.monotonic() 162 | 163 | @tulip.task 164 | def connect(self, pid, up_write, down_read): 165 | # setup pipes 166 | read_transport, proto = yield from self.loop.connect_read_pipe( 167 | tulip.StreamProtocol, os.fdopen(down_read, 'rb')) 168 | write_transport, _ = yield from self.loop.connect_write_pipe( 169 | tulip.StreamProtocol, os.fdopen(up_write, 'wb')) 170 | 171 | # websocket protocol 172 | reader = proto.set_parser(websocket.WebSocketParser()) 173 | writer = websocket.WebSocketWriter(write_transport) 174 | 175 | # store info 176 | self.pid = pid 177 | self.ping = time.monotonic() 178 | self.rtransport = read_transport 179 | self.wtransport = write_transport 180 | self.chat_task = self.chat(reader) 181 | self.heartbeat_task = self.heartbeat(writer) 182 | 183 | def kill(self): 184 | self._started = False 185 | self.chat_task.cancel() 186 | self.heartbeat_task.cancel() 187 | self.rtransport.close() 188 | self.wtransport.close() 189 | os.kill(self.pid, signal.SIGTERM) 190 | 191 | 192 | class Superviser: 193 | 194 | def __init__(self): 195 | self.loop = tulip.get_event_loop() 196 | args = ARGS.parse_args() 197 | if ':' in args.host: 198 | args.host, port = args.host.split(':', 1) 199 | args.port = int(port) 200 | 201 | if args.iocp: 202 | from tulip import windows_events 203 | sys.argv.remove('--iocp') 204 | logging.info('using iocp') 205 | el = windows_events.ProactorEventLoop() 206 | tulip.set_event_loop(el) 207 | 208 | if args.ssl: 209 | here = os.path.join(os.path.dirname(__file__), 'tests') 210 | 211 | if args.certfile: 212 | certfile = args.certfile or os.path.join(here, 'sample.crt') 213 | keyfile = args.keyfile or os.path.join(here, 'sample.key') 214 | else: 215 | certfile = os.path.join(here, 'sample.crt') 216 | keyfile = os.path.join(here, 'sample.key') 217 | 218 | sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 219 | sslcontext.load_cert_chain(certfile, keyfile) 220 | else: 221 | sslcontext = None 222 | self.ssl = sslcontext 223 | 224 | self.args = args 225 | self.workers = [] 226 | 227 | def start(self, protocol_factory): 228 | # bind socket 229 | sock = self.sock = socket.socket() 230 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 231 | sock.bind((self.args.host, self.args.port)) 232 | sock.listen(1024) 233 | sock.setblocking(False) 234 | 235 | # start processes 236 | for idx in range(self.args.workers): 237 | self.workers.append(Worker(self.loop, self.args, sock, protocol_factory, self.ssl)) 238 | 239 | self.loop.add_signal_handler(signal.SIGINT, lambda: self.loop.stop()) 240 | self.loop.run_forever() 241 | -------------------------------------------------------------------------------- /tests/http_server_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import unittest 3 | import unittest.mock 4 | import re 5 | 6 | import tulip 7 | from tulip.http import server, errors 8 | from tulip.test_utils import run_briefly 9 | from nacho.http import HttpServer 10 | from nacho.routing import Router 11 | 12 | 13 | class MockHttpServer(HttpServer): 14 | """Wrap server class with mocking support 15 | """ 16 | def handle_error(self, *args, **kwargs): 17 | if self.transport.__class__.__name__ == 'Mock': 18 | self.transport.reset_mock() 19 | super(HttpServer, self).handle_error(*args, **kwargs) 20 | 21 | 22 | class HttpServerTest(unittest.TestCase): 23 | def setUp(self): 24 | self.loop = tulip.new_event_loop() 25 | tulip.set_event_loop(self.loop) 26 | 27 | def tearDown(self): 28 | self.loop.close() 29 | 30 | def test_handle_request(self): 31 | transport = unittest.mock.Mock() 32 | srv = MockHttpServer(Router()) 33 | srv.connection_made(transport) 34 | rline = unittest.mock.Mock() 35 | rline.version = (1, 1) 36 | message = unittest.mock.Mock() 37 | srv.handle_request(rline, message) 38 | srv.stream.feed_data( 39 | b'GET / HTTP/1.0\r\n' 40 | b'Host: example.com\r\n\r\n') 41 | self.loop.run_until_complete(srv._request_handler) 42 | 43 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)]) 44 | self.assertTrue(content.startswith(b'HTTP/1.1 404 Not Found\r\n')) 45 | 46 | def _base_handler_test(self, urlregx, handler, testcontent, url=b'/', 47 | expected_code=200): 48 | transport = unittest.mock.Mock() 49 | router = Router() 50 | router.add_handler( 51 | urlregx, handler) 52 | srv = MockHttpServer(router) 53 | srv.connection_made(transport) 54 | rline = unittest.mock.Mock() 55 | rline.version = (1, 1) 56 | message = unittest.mock.Mock() 57 | srv.handle_request(rline, message) 58 | srv.stream.feed_data(b'GET ') 59 | srv.stream.feed_data(url) 60 | srv.stream.feed_data(b' HTTP/1.0\r\n' 61 | b'Host: example.com\r\n\r\n') 62 | self.loop.run_until_complete(srv._request_handler) 63 | 64 | header = transport.write.mock_calls[0][1][0].decode('utf-8') 65 | code = int(re.match("^HTTP/1.1 (\d+) ", header).groups()[0]) 66 | self.assertEquals(code, expected_code, ) 67 | headers = re.findall(r"(?P.*?): (?P.*?)\r\n", header) 68 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)[2:-2]]) 69 | self.assertEqual(content, testcontent) 70 | transport = None 71 | 72 | 73 | class HttpServerProtocolTests(unittest.TestCase): 74 | 75 | def setUp(self): 76 | self.loop = tulip.new_event_loop() 77 | tulip.set_event_loop(self.loop) 78 | 79 | def tearDown(self): 80 | self.loop.close() 81 | 82 | def test_http_status_exception(self): 83 | exc = errors.HttpErrorException(500, message='Internal error') 84 | self.assertEqual(exc.code, 500) 85 | self.assertEqual(exc.message, 'Internal error') 86 | 87 | def test_handle_request(self): 88 | transport = unittest.mock.Mock() 89 | 90 | srv = server.ServerHttpProtocol() 91 | srv.connection_made(transport) 92 | 93 | rline = unittest.mock.Mock() 94 | rline.version = (1, 1) 95 | message = unittest.mock.Mock() 96 | srv.handle_request(rline, message) 97 | 98 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)]) 99 | self.assertTrue(content.startswith(b'HTTP/1.1 404 Not Found\r\n')) 100 | 101 | def test_connection_made(self): 102 | srv = server.ServerHttpProtocol() 103 | self.assertIsNone(srv._request_handler) 104 | 105 | srv.connection_made(unittest.mock.Mock()) 106 | self.assertIsNotNone(srv._request_handler) 107 | 108 | def test_data_received(self): 109 | srv = server.ServerHttpProtocol() 110 | srv.connection_made(unittest.mock.Mock()) 111 | 112 | srv.data_received(b'123') 113 | self.assertEqual(b'123', bytes(srv.stream._buffer)) 114 | 115 | srv.data_received(b'456') 116 | self.assertEqual(b'123456', bytes(srv.stream._buffer)) 117 | 118 | def test_eof_received(self): 119 | srv = server.ServerHttpProtocol() 120 | srv.connection_made(unittest.mock.Mock()) 121 | srv.eof_received() 122 | self.assertTrue(srv.stream._eof) 123 | 124 | def test_connection_lost(self): 125 | srv = server.ServerHttpProtocol() 126 | srv.connection_made(unittest.mock.Mock()) 127 | srv.data_received(b'123') 128 | 129 | keep_alive_handle = srv._keep_alive_handle = unittest.mock.Mock() 130 | 131 | handle = srv._request_handler 132 | srv.connection_lost(None) 133 | 134 | self.assertIsNone(srv._request_handler) 135 | self.assertTrue(handle.cancelled()) 136 | 137 | self.assertIsNone(srv._keep_alive_handle) 138 | self.assertTrue(keep_alive_handle.cancel.called) 139 | 140 | srv.connection_lost(None) 141 | self.assertIsNone(srv._request_handler) 142 | self.assertIsNone(srv._keep_alive_handle) 143 | 144 | def test_srv_keep_alive(self): 145 | srv = server.ServerHttpProtocol() 146 | self.assertFalse(srv._keep_alive) 147 | 148 | srv.keep_alive(True) 149 | self.assertTrue(srv._keep_alive) 150 | 151 | srv.keep_alive(False) 152 | self.assertFalse(srv._keep_alive) 153 | 154 | def test_handle_error(self): 155 | transport = unittest.mock.Mock() 156 | srv = server.ServerHttpProtocol() 157 | srv.connection_made(transport) 158 | srv.keep_alive(True) 159 | 160 | srv.handle_error(404, headers=(('X-Server', 'Tulip'),)) 161 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)]) 162 | self.assertIn(b'HTTP/1.1 404 Not Found', content) 163 | self.assertIn(b'X-SERVER: Tulip', content) 164 | self.assertFalse(srv._keep_alive) 165 | 166 | @unittest.mock.patch('tulip.http.server.traceback') 167 | def test_handle_error_traceback_exc(self, m_trace): 168 | transport = unittest.mock.Mock() 169 | log = unittest.mock.Mock() 170 | srv = server.ServerHttpProtocol(debug=True, log=log) 171 | srv.connection_made(transport) 172 | 173 | m_trace.format_exc.side_effect = ValueError 174 | 175 | srv.handle_error(500, exc=object()) 176 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)]) 177 | self.assertTrue( 178 | content.startswith(b'HTTP/1.1 500 Internal Server Error')) 179 | self.assertTrue(log.exception.called) 180 | 181 | def test_handle_error_debug(self): 182 | transport = unittest.mock.Mock() 183 | srv = server.ServerHttpProtocol() 184 | srv.debug = True 185 | srv.connection_made(transport) 186 | 187 | try: 188 | raise ValueError() 189 | except Exception as exc: 190 | srv.handle_error(999, exc=exc) 191 | 192 | content = b''.join([c[1][0] for c in list(transport.write.mock_calls)]) 193 | 194 | self.assertIn(b'HTTP/1.1 500 Internal', content) 195 | self.assertIn(b'Traceback (most recent call last):', content) 196 | 197 | def test_handle_error_500(self): 198 | log = unittest.mock.Mock() 199 | transport = unittest.mock.Mock() 200 | 201 | srv = server.ServerHttpProtocol(log=log) 202 | srv.connection_made(transport) 203 | 204 | srv.handle_error(500) 205 | self.assertTrue(log.exception.called) 206 | 207 | def test_handle(self): 208 | transport = unittest.mock.Mock() 209 | srv = server.ServerHttpProtocol() 210 | srv.connection_made(transport) 211 | 212 | handle = srv.handle_request = unittest.mock.Mock() 213 | 214 | srv.stream.feed_data( 215 | b'GET / HTTP/1.0\r\n' 216 | b'Host: example.com\r\n\r\n') 217 | 218 | self.loop.run_until_complete(srv._request_handler) 219 | self.assertTrue(handle.called) 220 | self.assertTrue(transport.close.called) 221 | 222 | def test_handle_coro(self): 223 | transport = unittest.mock.Mock() 224 | srv = server.ServerHttpProtocol() 225 | 226 | called = False 227 | 228 | @tulip.coroutine 229 | def coro(message, payload): 230 | nonlocal called 231 | called = True 232 | srv.eof_received() 233 | 234 | srv.handle_request = coro 235 | srv.connection_made(transport) 236 | 237 | srv.stream.feed_data( 238 | b'GET / HTTP/1.0\r\n' 239 | b'Host: example.com\r\n\r\n') 240 | self.loop.run_until_complete(srv._request_handler) 241 | self.assertTrue(called) 242 | 243 | def test_handle_cancel(self): 244 | log = unittest.mock.Mock() 245 | transport = unittest.mock.Mock() 246 | 247 | srv = server.ServerHttpProtocol(log=log, debug=True) 248 | srv.connection_made(transport) 249 | 250 | srv.handle_request = unittest.mock.Mock() 251 | 252 | @tulip.task 253 | def cancel(): 254 | srv._request_handler.cancel() 255 | 256 | self.loop.run_until_complete( 257 | tulip.wait([srv._request_handler, cancel()])) 258 | self.assertTrue(log.debug.called) 259 | 260 | def test_handle_cancelled(self): 261 | log = unittest.mock.Mock() 262 | transport = unittest.mock.Mock() 263 | 264 | srv = server.ServerHttpProtocol(log=log, debug=True) 265 | srv.connection_made(transport) 266 | 267 | srv.handle_request = unittest.mock.Mock() 268 | run_briefly(self.loop) # start request_handler task 269 | 270 | srv.stream.feed_data( 271 | b'GET / HTTP/1.0\r\n' 272 | b'Host: example.com\r\n\r\n') 273 | 274 | r_handler = srv._request_handler 275 | srv._request_handler = None # emulate srv.connection_lost() 276 | 277 | self.assertIsNone(self.loop.run_until_complete(r_handler)) 278 | 279 | def test_handle_400(self): 280 | transport = unittest.mock.Mock() 281 | srv = server.ServerHttpProtocol() 282 | srv.connection_made(transport) 283 | srv.handle_error = unittest.mock.Mock() 284 | srv.keep_alive(True) 285 | srv.stream.feed_data(b'GET / HT/asd\r\n\r\n') 286 | 287 | self.loop.run_until_complete(srv._request_handler) 288 | self.assertTrue(srv.handle_error.called) 289 | self.assertTrue(400, srv.handle_error.call_args[0][0]) 290 | self.assertTrue(transport.close.called) 291 | 292 | def test_handle_500(self): 293 | transport = unittest.mock.Mock() 294 | srv = server.ServerHttpProtocol() 295 | srv.connection_made(transport) 296 | 297 | handle = srv.handle_request = unittest.mock.Mock() 298 | handle.side_effect = ValueError 299 | srv.handle_error = unittest.mock.Mock() 300 | 301 | srv.stream.feed_data( 302 | b'GET / HTTP/1.0\r\n' 303 | b'Host: example.com\r\n\r\n') 304 | self.loop.run_until_complete(srv._request_handler) 305 | 306 | self.assertTrue(srv.handle_error.called) 307 | self.assertTrue(500, srv.handle_error.call_args[0][0]) 308 | 309 | def test_handle_error_no_handle_task(self): 310 | transport = unittest.mock.Mock() 311 | srv = server.ServerHttpProtocol() 312 | srv.keep_alive(True) 313 | srv.connection_made(transport) 314 | srv.connection_lost(None) 315 | 316 | srv.handle_error(300) 317 | self.assertFalse(srv._keep_alive) 318 | 319 | def test_keep_alive(self): 320 | srv = server.ServerHttpProtocol(keep_alive=0.1) 321 | transport = unittest.mock.Mock() 322 | closed = False 323 | 324 | def close(): 325 | nonlocal closed 326 | closed = True 327 | srv.connection_lost(None) 328 | self.loop.stop() 329 | 330 | transport.close = close 331 | 332 | srv.connection_made(transport) 333 | 334 | handle = srv.handle_request = unittest.mock.Mock() 335 | 336 | srv.stream.feed_data( 337 | b'GET / HTTP/1.1\r\n' 338 | b'CONNECTION: keep-alive\r\n' 339 | b'HOST: example.com\r\n\r\n') 340 | 341 | self.loop.run_forever() 342 | self.assertTrue(handle.called) 343 | self.assertTrue(closed) 344 | 345 | def test_keep_alive_close_existing(self): 346 | transport = unittest.mock.Mock() 347 | srv = server.ServerHttpProtocol(keep_alive=15) 348 | srv.connection_made(transport) 349 | 350 | self.assertIsNone(srv._keep_alive_handle) 351 | keep_alive_handle = srv._keep_alive_handle = unittest.mock.Mock() 352 | srv.handle_request = unittest.mock.Mock() 353 | 354 | srv.stream.feed_data( 355 | b'GET / HTTP/1.0\r\n' 356 | b'HOST: example.com\r\n\r\n') 357 | 358 | self.loop.run_until_complete(srv._request_handler) 359 | self.assertTrue(keep_alive_handle.cancel.called) 360 | self.assertIsNone(srv._keep_alive_handle) 361 | self.assertTrue(transport.close.called) 362 | --------------------------------------------------------------------------------