├── 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'\r\n')
99 | for name in sorted(os.listdir(path)):
100 | if name.isprintable() and not name.startswith('.'):
101 | try:
102 | bname = name.encode('ascii')
103 | except UnicodeError:
104 | pass
105 | else:
106 | if os.path.isdir(os.path.join(path, name)):
107 | response.write(b'- ' + bname + b'/
\r\n')
109 | else:
110 | response.write(b'- ' + bname + b'
\r\n')
112 | 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 |
--------------------------------------------------------------------------------