├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── gevent_fastcgi ├── __init__.py ├── adapters │ ├── __init__.py │ ├── django │ │ ├── __init__.py │ │ └── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── run_gevent_fastcgi.py │ └── paste_deploy.py ├── base.py ├── const.py ├── interfaces.py ├── server.py ├── speedups.c ├── utils.py └── wsgi.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── base ├── __init__.py ├── test_connection.py ├── test_input_stream.py ├── test_output_stream.py └── test_utils.py ├── server ├── __init__.py ├── test_connection_handler.py └── test_server.py ├── utils.py └── wsgi ├── __init__.py └── test_wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.so 3 | build 4 | dist 5 | gevent_fastcgi.egg-info 6 | *.log 7 | .coverage 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011-2012, Alexander Kulakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | gevent-fastcgi 2 | ============== 3 | 4 | `FastCGI `_ server implementation using `gevent `_ coroutine-based networking library. 5 | No need to monkeypatch and slow down your favourite FastCGI server in order to make it "green". 6 | 7 | Provides simple request handler API to allow for custom request handlers. 8 | Comes with two WSGI request hadler implementations -- one using standard *wsgiref.handlers.BasicCGIHandler* and another using original request handler. 9 | 10 | Full support for FastCGI protocol connection multiplexing 11 | feature. 12 | 13 | Can fork multiple processes to better utilize multi-core CPUs. 14 | 15 | Includes adapters for `Django `_ and frameworks that use 16 | `PasteDeploy `_ like `Pylons / Pyramid `_ and `TurboGears `_ to simplify depolyment. 17 | 18 | Contributors 19 | ------------ 20 | 21 | This project could not be where it is now without help of the following great people: 22 | 23 | * `David Galeano `_ 24 | * `Lucas Clemente Vella `_ 25 | * `Nicholas Kwan `_ 26 | * `Peter D. Gray `_ 27 | 28 | Thank you guys for all your help! 29 | 30 | Installation 31 | ------------ 32 | 33 | To install gevent-fastcgi using pip run the following command: 34 | 35 | .. code:: bash 36 | 37 | $ pip install gevent-fastcgi 38 | 39 | If you prefer easy_install here is how to use it: 40 | 41 | .. code:: bash 42 | 43 | $ easy_install gevent-fastcgi 44 | 45 | 46 | Usage 47 | ----- 48 | 49 | This is how to use gevent-fastcgi in stand-alone mode: 50 | 51 | .. code:: python 52 | 53 | from gevent_fastcgi.server import FastCGIServer 54 | from gevent_fastcgi.wsgi import WSGIRequestHandler 55 | 56 | 57 | def wsgi_app(environ, start_response): 58 | start_response('200 OK', [('Content-type', 'text/plain')]) 59 | yield 'Hello World!' 60 | 61 | 62 | request_handler = WSGIRequestHandler(wsgi_app) 63 | server = FastCGIServer(('127.0.0.1', 4000), request_handler, num_workers=4) 64 | server.serve_forever() 65 | 66 | 67 | Using with PasteDeploy_ and friends 68 | ----------------------------------- 69 | 70 | Gevent-fastcgi defines three *paste.server_runner* entry points. Each of them will run FastCGIServer with different request 71 | handler implementation: 72 | 73 | *wsgi* 74 | *gevent_fastcgi.wsgi.WSGIRequestHandler* will be used to handle requests. 75 | Application is expected to be a WSGI-application. 76 | 77 | *wsgiref* 78 | *gevent_fastcgi.wsgi.WSGIRefRequestHandler* which uses standard 79 | *wsgiref.handlers* will be used to handle requests. 80 | Application is expected to be a WSGI-application. 81 | 82 | *fastcgi* 83 | Application is expected to implement *gevent_fastcgi.interfaces.IRequestHandler* 84 | interface. It should use *request.stdin* to receive request body and 85 | *request.stdout* and/or *request.stderr* to send response back to Web-server. 86 | 87 | Use it as following: 88 | 89 | .. code:: ini 90 | 91 | [server:main] 92 | use = egg:gevent_fastcgi#wsgi 93 | host = 127.0.0.1 94 | port = 4000 95 | # UNIX domain socket can be used by specifying path instead of host and port 96 | # socket = /path/to/socket 97 | # socket_mode = 0660 98 | 99 | # The following values are used in reply to Web-server on `FCGI_GET_VALUES` request 100 | # 101 | # Maximum allowed simulteneous connections, i.e. the size of greenlet pool 102 | # used for connection handlers. 103 | max_conns = 1024 104 | max_reqs = 1024 105 | 106 | # Fork `num_workers` child processes after socket is bound. 107 | # Must be equal or greate than 1. No children will be forked 108 | # if set to 1 or not specified 109 | num_workers = 8 110 | 111 | # Call specified functions of gevent.monkey module before starting the server 112 | gevent.monkey.patch_thread = yes 113 | gevent.monkey.patch_time = no 114 | gevent.monkey.patch_socket = on 115 | gevent.monkey.patch_ssl = off 116 | # or 117 | gevent.monkey.patch_all = yes 118 | 119 | 120 | `Django `_ adapter 121 | --------------------------------------------- 122 | 123 | Add *gevent_fastcgi.adapters.django* to INSTALLED_APPS of settings.py then run 124 | the following command (replace
with : or ): 125 | 126 | .. code:: bash 127 | 128 | $ python manage.py run_gevent_fastcgi
129 | 130 | 131 | Custom request handlers 132 | ----------------------- 133 | 134 | Starting from version 0.1.16dev It is possible to use custom request handler with *gevent_fastcgi.server.FastCGIServer*. Such a handler should implement 135 | *gevent_fastcgi.interfaces.IRequestHandler* interface and basically is just a callable that accepts single positional argument *request*. *gevent_fastcgi.wsgi* module contains two implementations of *IRequestHandler*. 136 | 137 | Request handler is run in separate greenlet. Request argument passed to request 138 | handler callable has the following attributes: 139 | 140 | *environ* 141 | Dictionary containing request environment. 142 | NOTE: contains whatever was sent by Web-server via FCGI_PARAM stream 143 | 144 | *stdin* 145 | File-like object that represents request body, possibly empty 146 | 147 | *stdout* 148 | File-like object that should be used by request handler to send response (including response headers) 149 | 150 | *stderr* 151 | File-like object that can be used to send error information back to Web-server 152 | 153 | Following is sample of custom request handler implementation: 154 | 155 | .. code:: python 156 | 157 | import os 158 | from zope.interface import implements 159 | from gevent import spawn, joinall 160 | from gevent_subprocess import Popen, PIPE 161 | from gevent_fastcgi.interfaces import IRequestHandler 162 | 163 | 164 | # WARNING!!! 165 | # CGIRequestHandler is for demonstration purposes only!!! 166 | # IT MUST NOT BE USED IN PRODUCTION ENVIRONMENT!!! 167 | 168 | class CGIRequestHandler(object): 169 | 170 | implements(IRequestHandler) 171 | 172 | def __init__(self, root, buf_size=1024): 173 | self.root = os.path.abspath(root) 174 | self.buf_size = buf_size 175 | 176 | def __call__(self, request): 177 | script_name = request.environ['SCRIPT_NAME'] 178 | if script_name.startswith('/'): 179 | script_name = script_name[1:] 180 | script_filename = os.path.join(self.root, script_name) 181 | 182 | if script_filename.startswith(self.root) and 183 | os.path.isfile(script_filename) and 184 | os.access(script_filename, os.X_OK): 185 | proc = Popen(script_filename, stdin=PIPE, stdout=PIPE, stderr=PIPE) 186 | joinall((spawn(self.copy_stream, src, dest) for src, dest in [ 187 | (request.stdin, proc.stdin), 188 | (proc.stdout, request.stdout), 189 | (proc.stderr, request.stderr), 190 | ])) 191 | else: 192 | # report an error 193 | request.stderr.write('Cannot locate or execute CGI-script %s' % script_filename) 194 | 195 | # and send a reply 196 | request.stdout.write('\r\n'.join(( 197 | 'Status: 404 Not Found', 198 | 'Content-Type: text/plain', 199 | '', 200 | 'No resource can be found for URI %s' % request.environ['REQUEST_URI'], 201 | ))) 202 | 203 | def copy_stream(self, src, dest): 204 | buf_size = self.buf_size 205 | read = src.read 206 | write = dest.write 207 | 208 | while True: 209 | buf = read(buf_size) 210 | if not buf: 211 | break 212 | write(buf) 213 | 214 | 215 | if __name__ == '__main__': 216 | from gevent_fastcgi.server import FastCGIServer 217 | 218 | address = ('127.0.0.1', 8000) 219 | handler = CGIRequestHandler('/var/www/cgi-bin') 220 | server = FastCGIServer(address, handler) 221 | server.serve_forever() 222 | -------------------------------------------------------------------------------- /gevent_fastcgi/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | 22 | import logging 23 | 24 | logger = logging.getLogger(__name__) 25 | if not logger.handlers: 26 | logger.addHandler(logging.NullHandler()) 27 | -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/gevent_fastcgi/adapters/__init__.py -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/gevent_fastcgi/adapters/django/__init__.py -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/django/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/gevent_fastcgi/adapters/django/management/__init__.py -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/django/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/gevent_fastcgi/adapters/django/management/commands/__init__.py -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/django/management/commands/run_gevent_fastcgi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from optparse import make_option 22 | from django.core.management import BaseCommand, CommandError 23 | 24 | 25 | MONKEY_PATCH_NAMES = ('os', 'socket', 'thread', 'select', 'time', 'ssl', 'all') 26 | 27 | 28 | class Command(BaseCommand): 29 | args = ': | ' 30 | help = 'Start gevent-fastcgi server' 31 | option_list = BaseCommand.option_list + ( 32 | make_option('--max-conns', type='int', dest='max_conns', default=1024, 33 | metavar='MAX_CONNS', 34 | help='Maximum simulteneous connections (default %default)', 35 | ), 36 | make_option('--buffer-size', type='int', dest='buffer_size', 37 | default=4096, metavar='BUFFER_SIZE', 38 | help='Read buffer size (default %default)', 39 | ), 40 | make_option('--num-workers', type='int', dest='num_workers', default=1, 41 | metavar='NUM_WORKERS', 42 | help='Number of worker processes (default %default)', 43 | ), 44 | make_option('--monkey-patch', dest='monkey_patch', 45 | help='Comma separated list of function names from ' 46 | 'gevent.monkey module. Allowed names are: ' + ', '.join( 47 | map('"{0}"'.format, MONKEY_PATCH_NAMES))), 48 | make_option('--socket-mode', type='int', dest='socket_mode', 49 | metavar='SOCKET_MODE', 50 | help='Socket file mode', 51 | ), 52 | make_option('--daemon', action='store_true', dest='daemonize', 53 | default=False, help='Become a daemon'), 54 | make_option('--work-dir', dest='our_home_dir', default='.', 55 | metavar='WORKDIR', 56 | help='Chande dir in daemon mode (default %default)'), 57 | make_option('--stdout', dest='out_log', metavar='STDOUT', 58 | help='stdout in daemon mode (default sys.devnull)'), 59 | make_option('--stderr', dest='err_log', metavar='STDERR', 60 | help='stderr in daemon mode (default sys.devnull)'), 61 | ) 62 | 63 | def handle(self, *args, **options): 64 | from os.path import dirname, isdir 65 | from gevent_fastcgi.server import FastCGIServer 66 | from gevent_fastcgi.wsgi import WSGIRequestHandler 67 | from django.core.handlers.wsgi import WSGIHandler 68 | 69 | if not args: 70 | raise CommandError('Please specify binding address') 71 | 72 | if len(args) > 1: 73 | raise CommandError('Unexpected arguments: %s' % ' '.join(args[1:])) 74 | 75 | bind_address = args[0] 76 | 77 | try: 78 | host, port = bind_address.split(':', 1) 79 | port = int(port) 80 | except ValueError: 81 | socket_dir = dirname(bind_address) 82 | if not isdir(socket_dir): 83 | raise CommandError( 84 | 'Please create directory for socket file first %r' % 85 | dirname(socket_dir)) 86 | else: 87 | if options['socket_mode'] is not None: 88 | raise CommandError('--socket-mode option can only be used ' 89 | 'with Unix domain sockets. Either use ' 90 | 'socket file path as address or do not ' 91 | 'specify --socket-mode option') 92 | bind_address = (host, port) 93 | 94 | if options['monkey_patch']: 95 | names = filter( 96 | None, map(str.strip, options['monkey_patch'].split(','))) 97 | if names: 98 | module = __import__('gevent.monkey', fromlist=['*']) 99 | for name in names: 100 | if name not in MONKEY_PATCH_NAMES: 101 | raise CommandError( 102 | 'Unknown name "{0}" in --monkey-patch option' 103 | .format(name)) 104 | patch_func = getattr(module, 'patch_{0}'.format(name)) 105 | patch_func() 106 | 107 | if options['daemonize']: 108 | from django.utils.daemonize import become_daemon 109 | 110 | daemon_opts = dict( 111 | (key, value) for key, value in options.items() if key in ( 112 | 'our_home_dir', 'out_log', 'err_log', 'umask')) 113 | become_daemon(**daemon_opts) 114 | 115 | kwargs = dict(( 116 | (name, value) for name, value in options.iteritems() if name in ( 117 | 'num_workers', 'max_conns', 'buffer_size', 'socket_mode'))) 118 | 119 | app = WSGIHandler() 120 | request_handler = WSGIRequestHandler(app) 121 | server = FastCGIServer(bind_address, request_handler, **kwargs) 122 | server.serve_forever() 123 | -------------------------------------------------------------------------------- /gevent_fastcgi/adapters/paste_deploy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | from __future__ import absolute_import 21 | 22 | from functools import wraps 23 | 24 | import gevent.monkey 25 | from paste.deploy.converters import asbool 26 | 27 | from ..server import FastCGIServer 28 | 29 | 30 | def server_params(app, conf, host='127.0.0.1', port=5000, socket=None, 31 | **kwargs): 32 | address = (host, int(port)) if socket is None else socket 33 | for name in kwargs.keys(): 34 | if name in ('max_conns', 'num_workers', 'buffer_size', 'backlog', 35 | 'socket_mode'): 36 | kwargs[name] = int(kwargs[name]) 37 | elif name.startswith('gevent.monkey.') and asbool(kwargs.pop(name)): 38 | name = name[14:] 39 | if name in gevent.monkey.__all__: 40 | getattr(gevent.monkey, name)() 41 | return (app, address), kwargs 42 | 43 | 44 | @wraps(server_params) 45 | def fastcgi_server_runner(*args, **kwargs): 46 | (handler, address), kwargs = server_params(*args, **kwargs) 47 | FastCGIServer(address, handler, **kwargs).serve_forever() 48 | 49 | 50 | @wraps(server_params) 51 | def wsgiref_server_runner(*args, **kwargs): 52 | from ..wsgi import WSGIRefRequestHandler 53 | 54 | (app, address), kwargs = server_params(*args, **kwargs) 55 | handler = WSGIRefRequestHandler(app) 56 | FastCGIServer(address, handler, **kwargs).serve_forever() 57 | 58 | 59 | @wraps(server_params) 60 | def wsgi_server_runner(*args, **kwargs): 61 | from ..wsgi import WSGIRequestHandler 62 | 63 | (app, address), kwargs = server_params(*args, **kwargs) 64 | handler = WSGIRequestHandler(app) 65 | FastCGIServer(address, handler, **kwargs).serve_forever() 66 | -------------------------------------------------------------------------------- /gevent_fastcgi/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from __future__ import with_statement 22 | 23 | import six 24 | import sys 25 | import logging 26 | from collections import namedtuple 27 | from tempfile import SpooledTemporaryFile 28 | 29 | from zope.interface import implementer 30 | from gevent import socket 31 | from gevent.event import Event 32 | 33 | from .interfaces import IConnection 34 | from .const import ( 35 | FCGI_VERSION, 36 | FCGI_STDIN, 37 | FCGI_STDOUT, 38 | FCGI_STDERR, 39 | FCGI_DATA, 40 | FCGI_NULL_REQUEST_ID, 41 | FCGI_RECORD_HEADER_LEN, 42 | FCGI_RECORD_TYPES, 43 | FCGI_MAX_CONTENT_LEN, 44 | ) 45 | from .utils import pack_header, unpack_header 46 | 47 | if sys.version_info > (3,): 48 | buffer = memoryview 49 | 50 | 51 | __all__ = ( 52 | 'PartialRead', 53 | 'BufferedReader', 54 | 'Record', 55 | 'Connection', 56 | 'InputStream', 57 | 'StdoutStream', 58 | 'StderrStream', 59 | ) 60 | 61 | logger = logging.getLogger(__name__) 62 | 63 | 64 | class PartialRead(Exception): 65 | """ Raised by buffered_reader when it fails to read requested length 66 | of data 67 | """ 68 | def __init__(self, requested_size, partial_data): 69 | super(PartialRead, self).__init__( 70 | 'Expected {0} but received {1} bytes only'.format( 71 | requested_size, len(partial_data))) 72 | self.requested_size = requested_size 73 | self.partial_data = partial_data 74 | 75 | 76 | class BufferedReader(object): 77 | """ Allows to receive data in large chunks 78 | """ 79 | def __init__(self, read_callable, buffer_size): 80 | self._reader = self._reader_generator(read_callable, buffer_size) 81 | next(self._reader) # advance generator to first yield statement 82 | 83 | def read_bytes(self, max_len): 84 | return self._reader.send(max_len) 85 | 86 | @staticmethod 87 | def _reader_generator(read, buf_size): 88 | buf = b'' 89 | blen = 0 90 | chunks = [] 91 | size = (yield) 92 | 93 | while True: 94 | if blen >= size: 95 | data, buf = buf[:size], buf[size:] 96 | blen -= size 97 | else: 98 | while blen < size: 99 | chunks.append(buf) 100 | buf = read( 101 | (size - blen + buf_size - 1) // buf_size * buf_size) 102 | if not buf: 103 | raise PartialRead(size, b''.join(chunks)) 104 | blen += len(buf) 105 | 106 | blen -= size 107 | 108 | if blen: 109 | chunks.append(buf[:-blen]) 110 | buf = buf[-blen:] 111 | else: 112 | chunks.append(buf) 113 | buf = b'' 114 | 115 | data = b''.join(chunks) 116 | chunks = [] 117 | 118 | size = (yield data) 119 | 120 | 121 | class Record(namedtuple('Record', ('type', 'content', 'request_id'))): 122 | 123 | def __str__(self): 124 | return ''.format( 125 | FCGI_RECORD_TYPES.get(self.type, self.type), 126 | self.request_id, 127 | len(self.content)) 128 | 129 | 130 | @implementer(IConnection) 131 | class Connection(object): 132 | def __init__(self, sock, buffer_size=4096): 133 | self._sock = sock 134 | self.buffered_reader = BufferedReader(sock.recv, buffer_size) 135 | 136 | def write_record(self, record): 137 | send = self._sock.send 138 | content_len = len(record.content) 139 | if content_len > FCGI_MAX_CONTENT_LEN: 140 | raise ValueError('Record content length exceeds {0}'.format( 141 | FCGI_MAX_CONTENT_LEN)) 142 | 143 | header = pack_header( 144 | FCGI_VERSION, record.type, record.request_id, content_len, 0) 145 | 146 | for buf, length in ( 147 | (header, FCGI_RECORD_HEADER_LEN), 148 | (record.content, content_len), 149 | ): 150 | if isinstance(buf, six.text_type): 151 | buf = buf.encode("ISO-8859-1") 152 | sent = 0 153 | while sent < length: 154 | sent += send(buffer(buf[sent:])) 155 | 156 | def read_record(self): 157 | read_bytes = self.buffered_reader.read_bytes 158 | 159 | try: 160 | header = read_bytes(FCGI_RECORD_HEADER_LEN) 161 | except PartialRead as x: 162 | if x.partial_data: 163 | logger.exception('Partial header received: {0}'.format(x)) 164 | raise 165 | # Remote side closed connection after sending all records 166 | logger.debug('Connection closed by peer') 167 | return None 168 | except StopIteration: 169 | # Connection closed unexpectedly 170 | logger.debug('Connection closed by peer') 171 | return None 172 | 173 | version, record_type, request_id, content_len, padding = ( 174 | unpack_header(header)) 175 | 176 | if content_len: 177 | content = read_bytes(content_len) 178 | else: 179 | content = '' 180 | 181 | if padding: # pragma: no cover 182 | read_bytes(padding) 183 | 184 | if isinstance(content, six.text_type): 185 | content = content.encode("ISO-8859-1") 186 | 187 | return Record(record_type, content, request_id) 188 | 189 | def __iter__(self): 190 | return iter(self.read_record, None) 191 | 192 | def close(self): 193 | if self._sock: 194 | self._sock.close() 195 | self._sock = None 196 | 197 | def done_writing(self): 198 | self._sock.shutdown(socket.SHUT_WR) 199 | 200 | 201 | class InputStream(object): 202 | """ 203 | FCGI_STDIN or FCGI_DATA stream. 204 | Uses temporary file to store received data once max_mem bytes 205 | have been received. 206 | """ 207 | def __init__(self, max_mem=1024): 208 | self._file = SpooledTemporaryFile(max_mem) 209 | self._eof_received = Event() 210 | 211 | def __del__(self): 212 | self._file.close() 213 | 214 | def feed(self, data): 215 | if self._eof_received.is_set(): 216 | raise IOError('Feeding file beyond EOF mark') 217 | if not data: # EOF mark 218 | self._file.seek(0) 219 | self._eof_received.set() 220 | else: 221 | if isinstance(data, six.text_type): 222 | data = data.encode("ISO-8859-1") 223 | self._file.write(data) 224 | 225 | def __iter__(self): 226 | self._eof_received.wait() 227 | return iter(self._file) 228 | 229 | def read(self, size=-1): 230 | self._eof_received.wait() 231 | return self._file.read(size) 232 | 233 | def readline(self, size=-1): 234 | self._eof_received.wait() 235 | return self._file.readline(size) 236 | 237 | def readlines(self, sizehint=0): 238 | self._eof_received.wait() 239 | return self._file.readlines(sizehint) 240 | 241 | @property 242 | def eof_received(self): 243 | return self._eof_received.is_set() 244 | 245 | 246 | class OutputStream(object): 247 | """ 248 | FCGI_STDOUT or FCGI_STDERR stream. 249 | """ 250 | def __init__(self, conn, request_id): 251 | self.conn = conn 252 | self.request_id = request_id 253 | self.closed = False 254 | 255 | def write(self, data): 256 | if self.closed: 257 | raise IOError('Writing to closed stream {0}'.format(self)) 258 | 259 | if not data: 260 | return 261 | 262 | write_record = self.conn.write_record 263 | record_type = self.record_type 264 | request_id = self.request_id 265 | size = len(data) 266 | 267 | if size <= FCGI_MAX_CONTENT_LEN: 268 | record = Record(record_type, data, request_id) 269 | write_record(record) 270 | else: 271 | data = buffer(data) 272 | sent = 0 273 | while sent < size: 274 | record = Record(record_type, 275 | data[sent:sent + FCGI_MAX_CONTENT_LEN], 276 | request_id) 277 | write_record(record) 278 | sent += FCGI_MAX_CONTENT_LEN 279 | 280 | def writelines(self, lines): 281 | if self.closed: 282 | raise IOError('Writing to closed stream {0}'.format(self)) 283 | 284 | write_record = self.conn.write_record 285 | record_type = self.record_type 286 | request_id = self.request_id 287 | buf = [] 288 | remainder = FCGI_MAX_CONTENT_LEN 289 | 290 | for line in lines: 291 | if not line: 292 | # skip empty lines 293 | continue 294 | 295 | line_len = len(line) 296 | if isinstance(line, six.text_type): 297 | line = line.encode("ISO-8859-1") 298 | 299 | if line_len >= remainder: 300 | buf.append(line[:remainder]) 301 | record = Record(record_type, b''.join(buf), request_id) 302 | write_record(record) 303 | buf = [line[remainder:]] 304 | remainder = FCGI_MAX_CONTENT_LEN 305 | else: 306 | buf.append(line) 307 | remainder -= line_len 308 | 309 | if buf: 310 | record = Record(record_type, b''.join(buf), request_id) 311 | write_record(record) 312 | 313 | def flush(self): 314 | pass 315 | 316 | def close(self): 317 | if not self.closed: 318 | self.closed = True 319 | self.conn.write_record( 320 | Record(self.record_type, b'', self.request_id)) 321 | 322 | 323 | class StdoutStream(OutputStream): 324 | 325 | record_type = FCGI_STDOUT 326 | 327 | def writelines(self, lines): 328 | # WSGI server must not buffer application iterable 329 | if isinstance(lines, (list, tuple)): 330 | # ...unless we have all output readily available 331 | OutputStream.writelines(self, lines) 332 | else: 333 | if self.closed: 334 | raise IOError('Writing to closed stream {0}'.format(self)) 335 | write_record = self.conn.write_record 336 | record_type = self.record_type 337 | request_id = self.request_id 338 | for line in lines: 339 | if line: 340 | record = Record(record_type, line, request_id) 341 | write_record(record) 342 | 343 | 344 | class StderrStream(OutputStream): 345 | 346 | record_type = FCGI_STDERR 347 | -------------------------------------------------------------------------------- /gevent_fastcgi/const.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | 22 | FCGI_VERSION = 1 23 | FCGI_LISTENSOCK_FILENO = 0 24 | FCGI_HEADER_LEN = 8 25 | FCGI_BEGIN_REQUEST = 1 26 | FCGI_ABORT_REQUEST = 2 27 | FCGI_END_REQUEST = 3 28 | FCGI_PARAMS = 4 29 | FCGI_STDIN = 5 30 | FCGI_STDOUT = 6 31 | FCGI_STDERR = 7 32 | FCGI_DATA = 8 33 | FCGI_GET_VALUES = 9 34 | FCGI_GET_VALUES_RESULT = 10 35 | FCGI_UNKNOWN_TYPE = 11 36 | FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 37 | FCGI_NULL_REQUEST_ID = 0 38 | FCGI_RECORD_HEADER_LEN = 8 39 | FCGI_KEEP_CONN = 1 40 | FCGI_RESPONDER = 1 41 | FCGI_AUTHORIZER = 2 42 | FCGI_FILTER = 3 43 | FCGI_REQUEST_COMPLETE = 0 44 | FCGI_CANT_MPX_CONN = 1 45 | FCGI_OVERLOADED = 2 46 | FCGI_UNKNOWN_ROLE = 3 47 | 48 | FCGI_RECORD_TYPES = { 49 | FCGI_BEGIN_REQUEST: 'FCGI_BEGIN_REQUEST', 50 | FCGI_ABORT_REQUEST: 'FCGI_ABORT_REQUEST', 51 | FCGI_END_REQUEST: 'FCGI_END_REQUEST', 52 | FCGI_PARAMS: 'FCGI_PARAMS', 53 | FCGI_STDIN: 'FCGI_STDIN', 54 | FCGI_STDOUT: 'FCGI_STDOUT', 55 | FCGI_STDERR: 'FCGI_STDERR', 56 | FCGI_DATA: 'FCGI_DATA', 57 | FCGI_GET_VALUES: 'FCGI_GET_VALUES', 58 | FCGI_GET_VALUES_RESULT: 'FCGI_GET_VALUES_RESULT', 59 | } 60 | 61 | EXISTING_REQUEST_RECORD_TYPES = frozenset(( 62 | FCGI_STDIN, 63 | FCGI_DATA, 64 | FCGI_PARAMS, 65 | FCGI_ABORT_REQUEST, 66 | )) 67 | 68 | FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' 69 | FCGI_MAX_REQS = 'FCGI_MAX_REQS' 70 | FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' 71 | 72 | FCGI_MAX_CONTENT_LEN = 65535 73 | -------------------------------------------------------------------------------- /gevent_fastcgi/interfaces.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from zope.interface import Interface, Attribute 22 | 23 | 24 | class IConnection(Interface): 25 | 26 | def read_record(): 27 | """ 28 | Receive and deserialize next record from peer. 29 | Return None if no more records available 30 | """ 31 | 32 | def write_record(record): 33 | """ 34 | Serialize and send IRecord instance to peer 35 | """ 36 | 37 | def close(): 38 | """ 39 | Close connection 40 | """ 41 | 42 | 43 | class IRequest(Interface): 44 | 45 | id = Attribute('ID') 46 | environ = Attribute('Request environment dict') 47 | stdin = Attribute('Standard input stream') 48 | stout = Attribute('Standard output stream') 49 | stderr = Attribute('Standard error stream') 50 | 51 | 52 | class IRequestHandler(Interface): 53 | 54 | def __call__(request): 55 | """ Handle single request 56 | """ 57 | -------------------------------------------------------------------------------- /gevent_fastcgi/server.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | # DEALINGS IN THE SOFTWARE. 20 | 21 | from __future__ import with_statement, absolute_import 22 | 23 | from gevent.monkey import patch_os 24 | patch_os() 25 | 26 | import os 27 | import six 28 | import sys 29 | import errno 30 | import logging 31 | 32 | import atexit 33 | if os.name == "nt": 34 | from signal import SIGINT, SIGTERM 35 | else: 36 | from signal import SIGHUP, SIGKILL, SIGQUIT, SIGINT, SIGTERM 37 | 38 | from zope.interface import implementer 39 | 40 | from gevent import sleep, spawn, socket, signal, version_info 41 | from gevent.server import StreamServer 42 | from gevent.event import Event 43 | try: 44 | from gevent.lock import Semaphore 45 | except ImportError: 46 | from gevent.coros import Semaphore 47 | 48 | from .interfaces import IRequest 49 | from .const import ( 50 | FCGI_ABORT_REQUEST, 51 | FCGI_AUTHORIZER, 52 | FCGI_BEGIN_REQUEST, 53 | FCGI_END_REQUEST, 54 | FCGI_FILTER, 55 | FCGI_GET_VALUES, 56 | FCGI_GET_VALUES_RESULT, 57 | FCGI_KEEP_CONN, 58 | FCGI_NULL_REQUEST_ID, 59 | FCGI_PARAMS, 60 | FCGI_REQUEST_COMPLETE, 61 | FCGI_RESPONDER, 62 | FCGI_STDIN, 63 | FCGI_DATA, 64 | FCGI_UNKNOWN_ROLE, 65 | FCGI_UNKNOWN_TYPE, 66 | EXISTING_REQUEST_RECORD_TYPES, 67 | ) 68 | from .base import ( 69 | Connection, 70 | Record, 71 | InputStream, 72 | StdoutStream, 73 | StderrStream, 74 | ) 75 | from .utils import ( 76 | pack_pairs, 77 | unpack_pairs, 78 | unpack_begin_request, 79 | pack_end_request, 80 | pack_unknown_type, 81 | ) 82 | 83 | 84 | __all__ = ('Request', 'ServerConnection', 'FastCGIServer') 85 | 86 | logger = logging.getLogger(__name__) 87 | 88 | 89 | @implementer(IRequest) 90 | class Request(object): 91 | def __init__(self, conn, request_id, role): 92 | self.conn = conn 93 | self.id = request_id 94 | self.role = role 95 | self.environ = {} 96 | self.stdin = InputStream() 97 | self.stdout = StdoutStream(conn, request_id) 98 | self.stderr = StderrStream(conn, request_id) 99 | self.greenlet = None 100 | self._environ = InputStream() 101 | 102 | 103 | class ServerConnection(Connection): 104 | 105 | def __init__(self, *args, **kw): 106 | super(ServerConnection, self).__init__(*args, **kw) 107 | self.lock = Semaphore() 108 | 109 | def write_record(self, record): 110 | # We must serialize access for possible multiple request greenlets 111 | with self.lock: 112 | super(ServerConnection, self).write_record(record) 113 | 114 | 115 | HANDLE_RECORD_ATTR = '_handle_record_type' 116 | 117 | 118 | def record_handler(record_type): 119 | """ 120 | Mark method as a tecord handler of this record type 121 | """ 122 | def decorator(method): 123 | setattr(method, HANDLE_RECORD_ATTR, record_type) 124 | return method 125 | return decorator 126 | 127 | class ConnectionHandlerType(type): 128 | """ 129 | Collect record handlers during class construction 130 | """ 131 | 132 | def __new__(cls, name, bases, attrs): 133 | attrs['_record_handlers'] = dict( 134 | (getattr(method, HANDLE_RECORD_ATTR), method) 135 | for name, method in attrs.items() 136 | if hasattr(method, HANDLE_RECORD_ATTR)) 137 | return type(name, bases, attrs) 138 | 139 | class ConnectionHandler(six.with_metaclass(ConnectionHandlerType, object)): 140 | def __init__(self, conn, role, capabilities, request_handler): 141 | self.conn = conn 142 | self.role = role 143 | self.capabilities = capabilities 144 | self.request_handler = request_handler 145 | self.requests = {} 146 | self.keep_open = None 147 | self.closing = False 148 | self._job_is_done = Event() 149 | 150 | def run(self): 151 | reader = spawn(self.read_records) 152 | reader.link(self._report_finished_job) 153 | event = self._job_is_done 154 | 155 | while True: 156 | event.wait() 157 | event.clear() 158 | logger.debug('Checking if connection can be closed now') 159 | if self.requests: 160 | logger.debug('Connection left open due to active requests') 161 | elif self.keep_open and not reader.ready(): 162 | logger.debug('Connection left open due to KEEP_CONN flag') 163 | else: 164 | break 165 | 166 | reader.kill() 167 | reader.join() 168 | logger.debug('Closing connection') 169 | self.conn.close() 170 | 171 | def handle_request(self, request): 172 | try: 173 | logger.debug('Handling request {0}'.format(request.id)) 174 | self.request_handler(request) 175 | except: 176 | logger.exception('Request handler raised exception') 177 | raise 178 | finally: 179 | self.end_request(request) 180 | 181 | def end_request(self, request, request_status=FCGI_REQUEST_COMPLETE, 182 | app_status=0): 183 | try: 184 | request.stdout.close() 185 | request.stderr.close() 186 | self.send_record(FCGI_END_REQUEST, pack_end_request( 187 | app_status, request_status), request.id) 188 | finally: 189 | del self.requests[request.id] 190 | logger.debug('Request {0} ended'.format(request.id)) 191 | 192 | def read_records(self): 193 | record_handlers = self._record_handlers 194 | requests = self.requests 195 | for record in self.conn: 196 | handler = record_handlers.get(record.type) 197 | if handler is None: 198 | logger.error('{0}: Unknown record type'.format(record)) 199 | self.send_record(FCGI_UNKNOWN_TYPE, 200 | pack_unknown_type(record.type)) 201 | break 202 | 203 | if record.type in EXISTING_REQUEST_RECORD_TYPES: 204 | request = requests.get(record.request_id) 205 | if request is None: 206 | logger.error( 207 | 'Record {0} for non-existent request'.format(record)) 208 | break 209 | handler(self, record, request) 210 | else: 211 | handler(self, record) 212 | 213 | def send_record( 214 | self, record_type, content='', request_id=FCGI_NULL_REQUEST_ID): 215 | self.conn.write_record(Record(record_type, content, request_id)) 216 | 217 | @record_handler(FCGI_GET_VALUES) 218 | def handle_get_values_record(self, record): 219 | pairs = ((name, self.capabilities.get(name.decode("ISO-8859-1") if isinstance(name, six.binary_type) else name)) for name, _ in 220 | unpack_pairs(record.content)) 221 | content = pack_pairs( 222 | (name, str(value)) for name, value in pairs) 223 | self.send_record(FCGI_GET_VALUES_RESULT, content) 224 | self._report_finished_job() 225 | 226 | @record_handler(FCGI_BEGIN_REQUEST) 227 | def handle_begin_request_record(self, record): 228 | role, flags = unpack_begin_request(record.content) 229 | if role != self.role: 230 | self.send_record(FCGI_END_REQUEST, pack_end_request( 231 | 0, FCGI_UNKNOWN_ROLE), record.request_id) 232 | logger.error( 233 | 'Request role {0} does not match server role {1}'.format( 234 | role, self.role)) 235 | self._report_finished_job() 236 | else: 237 | # Should we check this for every request instead? 238 | if self.keep_open is None: 239 | self.keep_open = bool(FCGI_KEEP_CONN & flags) 240 | request = Request(self.conn, record.request_id, role) 241 | if role == FCGI_FILTER: 242 | request.data = InputStream() 243 | self.requests[request.id] = request 244 | 245 | @record_handler(FCGI_STDIN) 246 | def handle_stdin_record(self, record, request): 247 | request.stdin.feed(record.content) 248 | 249 | @record_handler(FCGI_DATA) 250 | def handle_data_record(self, record, request): 251 | request.data.feed(record.content) 252 | if not record.content and request.role == FCGI_FILTER: 253 | self.spawn_request_handler(request) 254 | 255 | @record_handler(FCGI_PARAMS) 256 | def handle_params_record(self, record, request): 257 | request._environ.feed(record.content) 258 | if not record.content: 259 | # EOF received 260 | request.environ = dict(unpack_pairs(request._environ.read())) 261 | del request._environ 262 | 263 | # Unicode compatibility 264 | for key in list(request.environ.keys()): 265 | request.environ[key.decode("ISO-8859-1")] = request.environ[key].decode("ISO-8859-1") 266 | if request.role in (FCGI_RESPONDER, FCGI_AUTHORIZER): 267 | self.spawn_request_handler(request) 268 | 269 | @record_handler(FCGI_ABORT_REQUEST) 270 | def handle_abort_request_record(self, record, request): 271 | logger.warn('Aborting request {0}'.format(request.id)) 272 | if request.id in self.requests: 273 | greenlet = request.greenlet 274 | if greenlet is None: 275 | self.end_request(request) 276 | self._report_finished_job() 277 | else: 278 | logger.warn('Killing greenlet {0} for request {1}'.format( 279 | greenlet, request.id)) 280 | greenlet.kill() 281 | greenlet.join() 282 | else: 283 | logger.debug('Request {0} not found'.format(request.id)) 284 | 285 | def spawn_request_handler(self, request): 286 | request.greenlet = g = spawn(self.handle_request, request) 287 | g.link(self._report_finished_job) 288 | 289 | def _report_finished_job(self, source=None): 290 | self._job_is_done.set() 291 | 292 | 293 | class FastCGIServer(StreamServer): 294 | """ 295 | Server that handles communication with Web-server via FastCGI protocol. 296 | It is request_handler's responsibility to choose protocol and deal with 297 | application invocation. gevent_fastcgi.wsgi module contains WSGI 298 | protocol implementation. 299 | """ 300 | 301 | def __init__(self, listener, request_handler, role=FCGI_RESPONDER, 302 | num_workers=1, buffer_size=1024, max_conns=1024, 303 | socket_mode=None, **kwargs): 304 | # StreamServer does not create UNIX-sockets 305 | if isinstance(listener, six.string_types): 306 | self._socket_file = listener 307 | self._socket_mode = socket_mode 308 | # StreamServer does not like "backlog" with pre-cooked socket 309 | self._backlog = kwargs.pop('backlog', None) 310 | if self._backlog is None: 311 | self._backlog = max_conns 312 | if os.name == "nt": 313 | raise NotImplemented("Windows do not support unix socket") 314 | listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 315 | 316 | super(FastCGIServer, self).__init__( 317 | listener, self.handle_connection, spawn=max_conns, **kwargs) 318 | 319 | if role not in (FCGI_RESPONDER, FCGI_FILTER, FCGI_AUTHORIZER): 320 | raise ValueError('Illegal FastCGI role {0}'.format(role)) 321 | 322 | self.max_conns = max_conns 323 | self.role = role 324 | self.request_handler = request_handler 325 | self.buffer_size = buffer_size 326 | self.capabilities = dict( 327 | FCGI_MAX_CONNS=str(max_conns), 328 | FCGI_MAX_REQS=str(max_conns * 1024), 329 | FCGI_MPXS_CONNS='1', 330 | ) 331 | 332 | self.num_workers = int(num_workers) 333 | assert self.num_workers > 0, 'num_workers must be positive number' 334 | self._workers = [] 335 | 336 | def start(self): 337 | logger.debug('Starting server') 338 | if not self.started: 339 | if hasattr(self, '_socket_file'): 340 | self._create_socket_file() 341 | super(FastCGIServer, self).start() 342 | if self.num_workers > 1: 343 | self._start_workers() 344 | self._supervisor = spawn(self._watch_workers) 345 | atexit.register(self._cleanup) 346 | sig_register = [SIGTERM, SIGINT] 347 | if os.name != "nt": 348 | sig_register.extend([SIGQUIT]) 349 | for signum in sig_register: 350 | signal(signum, sys.exit, 1) 351 | 352 | def start_accepting(self): 353 | # master proceess with workers should not start accepting 354 | if self._workers is None or self.num_workers == 1: 355 | super(FastCGIServer, self).start_accepting() 356 | 357 | def stop_accepting(self): 358 | # master proceess with workers did not start accepting 359 | if self._workers is None or self.num_workers == 1: 360 | super(FastCGIServer, self).stop_accepting() 361 | 362 | def handle_connection(self, sock, addr): 363 | if sock.family in (socket.AF_INET, socket.AF_INET6): 364 | sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 365 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 366 | self.buffer_size) 367 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 368 | self.buffer_size) 369 | conn = ServerConnection(sock, self.buffer_size) 370 | handler = ConnectionHandler( 371 | conn, self.role, self.capabilities, self.request_handler) 372 | handler.run() 373 | 374 | if version_info < (1,): 375 | # older version of gevent 376 | def kill(self): 377 | super(FastCGIServer, self).kill() 378 | self._cleanup() 379 | else: 380 | def close(self): 381 | super(FastCGIServer, self).close() 382 | self._cleanup() 383 | 384 | def _start_workers(self): 385 | while len(self._workers) < self.num_workers: 386 | self._start_worker() 387 | 388 | def _start_worker(self): 389 | if os.name == "nt": 390 | raise NotImplemented("Multiple workers not supported on Windows") 391 | pid = os.fork() 392 | if pid: 393 | # master process 394 | self._workers.append(pid) 395 | logger.debug('Started worker {0}'.format(pid)) 396 | return pid 397 | else: 398 | try: 399 | # this indicates current process is a worker 400 | self._workers = None 401 | devnull_fd = os.open(os.devnull, os.O_RDWR) 402 | try: 403 | for fd in (0,): 404 | os.dup2(devnull_fd, fd) 405 | finally: 406 | os.close(devnull_fd) 407 | if os.name != "nt": 408 | signal(SIGHUP, self.stop) 409 | self.start_accepting() 410 | super(FastCGIServer, self).serve_forever() 411 | finally: 412 | # worker must never return 413 | os._exit(0) 414 | 415 | def _watch_workers(self, check_interval=5): 416 | keep_running = True 417 | while keep_running: 418 | self._start_workers() 419 | 420 | try: 421 | try: 422 | sleep(check_interval) 423 | self._reap_workers() 424 | except self.Stop: 425 | logger.debug('Waiting for all workers to exit') 426 | keep_running = False 427 | self._reap_workers(True) 428 | except OSError as e: 429 | if e.errno != errno.ECHILD: 430 | logger.exception('Failed to wait for any worker to exit') 431 | else: 432 | logger.debug('No alive workers left') 433 | 434 | def _reap_workers(self, block=False): 435 | flags = 0 if block else os.WNOHANG 436 | while self._workers: 437 | pid, status = os.waitpid(-1, flags) 438 | if pid == 0: 439 | break 440 | elif pid in self._workers: 441 | logger.debug('Worker {0} exited'.format(pid)) 442 | self._workers.remove(pid) 443 | 444 | def _cleanup(self): 445 | if hasattr(self, '_workers'): 446 | # it was initialized 447 | if self._workers is not None: 448 | # master process 449 | try: 450 | self._kill_workers() 451 | finally: 452 | self._remove_socket_file() 453 | else: 454 | # _workers was not initialized but it's still master process 455 | self._remove_socket_file() 456 | 457 | def _kill_workers(self, kill_timeout=2): 458 | for pid, sig in self._killing_sequence(kill_timeout): 459 | try: 460 | logger.debug( 461 | 'Killing worker {0} with signal {1}'.format(pid, sig)) 462 | os.kill(pid, sig) 463 | except OSError as x: 464 | if x.errno == errno.ESRCH: 465 | logger.error('Worker with pid {0} not found'.format(pid)) 466 | if pid in self._workers: 467 | self._workers.remove(pid) 468 | elif x.errno == errno.ECHILD: 469 | logger.error('No alive workers left') 470 | self._workers = [] 471 | break 472 | else: 473 | logger.exception( 474 | 'Failed to kill worker {0} with signal {1}'.format( 475 | pid, sig)) 476 | 477 | def _killing_sequence(self, max_timeout): 478 | short_delay = max(0.1, max_timeout / 50) 479 | if os.name == "nt": 480 | return 481 | for sig in SIGHUP, SIGKILL: 482 | if not self._workers: 483 | raise StopIteration 484 | logger.debug('Killing workers {0} with signal {1}'. 485 | format(self._workers, sig)) 486 | for pid in self._workers[:]: 487 | yield pid, sig 488 | 489 | sleep(short_delay) 490 | self._supervisor.kill(self.Stop) 491 | sleep(short_delay) 492 | if self._workers: 493 | sleep(max_timeout) 494 | 495 | def _create_socket_file(self): 496 | if self._socket_mode is not None: 497 | umask = os.umask(0) 498 | try: 499 | self.socket.bind(self._socket_file) 500 | os.chmod(self._socket_file, self._socket_mode) 501 | finally: 502 | os.umask(umask) 503 | else: 504 | self.socket.bind(self._socket_file) 505 | 506 | self.socket.listen(self._backlog) 507 | 508 | def _remove_socket_file(self): 509 | socket_file = self.__dict__.pop('_socket_file', None) 510 | if socket_file: 511 | try: 512 | logger.debug('Removing socket-file {0}'.format(socket_file)) 513 | os.unlink(socket_file) 514 | except OSError: 515 | logger.exception( 516 | 'Failed to remove socket file {0}' 517 | .format(socket_file)) 518 | 519 | class Stop(BaseException): 520 | """ Used to signal watcher greenlet 521 | """ 522 | -------------------------------------------------------------------------------- /gevent_fastcgi/speedups.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011-2013, Alexander Kulakov 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | #define PY_SSIZE_T_CLEAN 24 | #include 25 | #ifdef _WIN32 26 | #include 27 | #else 28 | #include 29 | #endif 30 | 31 | #if PY_MAJOR_VERSION >= 3 32 | #define PyString_FromStringAndSize PyBytes_FromStringAndSize 33 | #endif 34 | 35 | #define ENSURE_LEN(req) if ((end - buf) < (req)) { \ 36 | Py_XDECREF(result); \ 37 | return PyErr_Format(PyExc_ValueError, "Buffer is %ld byte(s) short", (req) - (end - buf)); \ 38 | } 39 | 40 | #define PARSE_LEN(len) ENSURE_LEN(1); \ 41 | len = *buf++; \ 42 | if (len & 0x80) { \ 43 | ENSURE_LEN(3); \ 44 | len = ((len & 0x7f) << 24) + (buf[0] << 16) + (buf[1] << 8) + buf[2]; \ 45 | buf += 3; \ 46 | } 47 | 48 | static PyObject * 49 | py_unpack_pairs(PyObject *self, PyObject *args) { 50 | unsigned char *buf, *name, *value, *end; 51 | Py_ssize_t blen, nlen, vlen; 52 | PyObject *result, *tuple; 53 | 54 | if (!PyArg_ParseTuple(args, "s#:unpack_pairs", &buf, &blen)) { 55 | return PyErr_Format(PyExc_ValueError, "Single string argument expected"); 56 | } 57 | 58 | end = buf + blen; 59 | result = PyList_New(0); 60 | 61 | if (result) { 62 | while (buf < end) { 63 | PARSE_LEN(nlen); 64 | PARSE_LEN(vlen); 65 | ENSURE_LEN((nlen + vlen)); 66 | name = buf; 67 | buf += nlen; 68 | value = buf; 69 | buf += vlen; 70 | #if PY_MAJOR_VERSION >= 3 71 | tuple = Py_BuildValue("(y#y#)", name, nlen, value, vlen); 72 | #else 73 | tuple = Py_BuildValue("(s#s#)", name, nlen, value, vlen); 74 | #endif 75 | if (tuple) { 76 | PyList_Append(result, tuple); 77 | Py_DECREF(tuple); 78 | } else { 79 | Py_XDECREF(result); 80 | return PyErr_Format(PyExc_RuntimeError, "Failed to allocate memory for next name/value tuple"); 81 | } 82 | } 83 | } 84 | 85 | return result; 86 | } 87 | 88 | #define PACK_LEN(len) if (len > 127) { \ 89 | *ptr++ = 0x80 + ((len >> 24) & 0xff); \ 90 | *ptr++ = (len >> 16) & 0xff; \ 91 | *ptr++ = (len >> 8) & 0xff; \ 92 | *ptr++ = len & 0xff; \ 93 | } else { \ 94 | *ptr++ = len; \ 95 | } 96 | 97 | static PyObject * 98 | py_pack_pair(PyObject *self, PyObject *args) { 99 | PyObject *result, *name, *value; 100 | unsigned char *buf, *ptr; 101 | Py_ssize_t name_len, value_len, buf_len; 102 | 103 | if (!PyArg_ParseTuple(args, "s#s#:pack_pair", &name, &name_len, &value, &value_len)) { 104 | return NULL; 105 | } 106 | 107 | if (name_len > 0x7fffffff) { 108 | PyErr_SetString (PyExc_ValueError,"Pair name too long"); 109 | return NULL; 110 | } 111 | 112 | if (value_len > 0x7fffffff) { 113 | PyErr_SetString (PyExc_ValueError,"Pair value too long"); 114 | return NULL; 115 | } 116 | 117 | 118 | buf_len = name_len + value_len + (name_len > 127 ? 4 : 1) + (value_len > 127 ? 4 : 1); 119 | buf = ptr = (unsigned char*) PyMem_Malloc(buf_len); 120 | 121 | if (!buf) return PyErr_NoMemory(); 122 | 123 | PACK_LEN(name_len); 124 | PACK_LEN(value_len); 125 | memcpy(ptr, name, name_len); 126 | memcpy(ptr + name_len, value, value_len); 127 | 128 | result = PyString_FromStringAndSize(buf, buf_len); 129 | PyMem_Free(buf); 130 | 131 | return result; 132 | } 133 | 134 | typedef struct { 135 | unsigned char fcgi_version, record_type; 136 | unsigned short int request_id, content_len; 137 | unsigned char padding; 138 | char reserved; 139 | } record_header_t; 140 | 141 | 142 | static PyObject * 143 | py_pack_header(PyObject *self, PyObject *args) { 144 | PyObject *result; 145 | record_header_t *header; 146 | 147 | header = (record_header_t *) PyMem_Malloc(sizeof(record_header_t)); 148 | if (!header) return PyErr_NoMemory(); 149 | 150 | if (!PyArg_ParseTuple(args, "bbHHb:pack_header", 151 | &(header->fcgi_version), 152 | &(header->record_type), 153 | &(header->request_id), 154 | &(header->content_len), 155 | &(header->padding))) return NULL; 156 | 157 | header->request_id = htons(header->request_id); 158 | header->content_len = htons(header->content_len); 159 | 160 | result = PyString_FromStringAndSize((char *)header, sizeof(record_header_t)); 161 | PyMem_Free(header); 162 | return result; 163 | } 164 | 165 | 166 | static PyObject * 167 | py_unpack_header(PyObject *self, PyObject *args) { 168 | PyObject *result; 169 | record_header_t *header; 170 | Py_ssize_t len; 171 | 172 | if (!PyArg_ParseTuple(args, "s#:unpack_header", (char *)&header, &len)) return NULL; 173 | 174 | if (len < sizeof(record_header_t)) 175 | return PyErr_Format(PyExc_ValueError, 176 | "Data must be at least %ld bytes long (%ld passed)", 177 | sizeof(record_header_t), len); 178 | 179 | result = Py_BuildValue("(bbhhb)", 180 | header->fcgi_version, 181 | header->record_type, 182 | ntohs(header->request_id), 183 | ntohs(header->content_len), 184 | header->padding); 185 | if (!result) return PyErr_NoMemory(); 186 | PyMem_Free(header); 187 | return result; 188 | } 189 | 190 | 191 | static PyMethodDef _methods[] = { 192 | {"unpack_pairs", py_unpack_pairs, METH_VARARGS}, 193 | {"pack_pair", py_pack_pair, METH_VARARGS}, 194 | {"pack_header", py_pack_header, METH_VARARGS}, 195 | {"unpack_header", py_unpack_header, METH_VARARGS}, 196 | {NULL, NULL} 197 | }; 198 | 199 | struct module_state { 200 | PyObject *error; 201 | }; 202 | 203 | #if PY_MAJOR_VERSION >= 3 204 | #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) 205 | #else 206 | #define GETSTATE(m) (&_state) 207 | static struct module_state _state; 208 | #endif 209 | 210 | #if PY_MAJOR_VERSION >= 3 211 | 212 | static int _traverse(PyObject *m, visitproc visit, void *arg) { 213 | Py_VISIT(GETSTATE(m)->error); 214 | return 0; 215 | } 216 | 217 | static int _clear(PyObject *m) { 218 | Py_CLEAR(GETSTATE(m)->error); 219 | return 0; 220 | } 221 | 222 | static struct PyModuleDef moduledef = { 223 | PyModuleDef_HEAD_INIT, 224 | "speedups", 225 | NULL, 226 | sizeof(struct module_state), 227 | _methods, 228 | NULL, 229 | _traverse, 230 | _clear, 231 | NULL 232 | }; 233 | 234 | #define INITERROR return NULL 235 | PyMODINIT_FUNC 236 | PyInit_speedups(void) 237 | #else 238 | #define INITERROR return 239 | void 240 | initspeedups(void) 241 | #endif 242 | { 243 | #if PY_MAJOR_VERSION >= 3 244 | PyObject *module = PyModule_Create(&moduledef); 245 | return module; 246 | #else 247 | Py_InitModule("speedups", _methods); 248 | #endif 249 | } 250 | -------------------------------------------------------------------------------- /gevent_fastcgi/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from __future__ import absolute_import 22 | 23 | import struct 24 | import logging 25 | import six 26 | import sys 27 | 28 | 29 | __all__ = [ 30 | 'pack_pairs', 31 | 'unpack_pairs', 32 | ] 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | header_struct = struct.Struct('!BBHHBx') 37 | begin_request_struct = struct.Struct('!HB5x') 38 | end_request_struct = struct.Struct('!LB3x') 39 | unknown_type_struct = struct.Struct('!B7x') 40 | 41 | for name in 'header', 'begin_request', 'end_request', 'unknown_type': 42 | packer = globals().get('{}_struct'.format(name)) 43 | for prefix, attr in ( 44 | ('pack_', 'pack'), 45 | ('unpack_', 'unpack_from'), 46 | ): 47 | full_name = prefix + name 48 | globals()[full_name] = getattr(packer, attr) 49 | __all__.append(full_name) 50 | 51 | 52 | def pack_pairs(pairs): 53 | if isinstance(pairs, dict): 54 | pairs = six.iteritems(pairs) 55 | return b''.join(pack_pair(name, value) for name, value in pairs) 56 | 57 | 58 | try: 59 | from .speedups import pack_pair, unpack_pairs 60 | logger.debug('Using speedups module') 61 | except ImportError: 62 | logger.debug('Failed to load speedups module') 63 | 64 | length_struct = struct.Struct('!L') 65 | 66 | def pack_len(s): 67 | l = len(s) 68 | if l < 128: 69 | if sys.version_info < (3, 0): 70 | return chr(l) 71 | else: 72 | return bytes([l]) 73 | elif l > 0x7fffffff: 74 | raise ValueError('Maximum name or value length is {0}'.format( 75 | 0x7fffffff)) 76 | return length_struct.pack(l | 0x80000000) 77 | 78 | def pack_pair(name, value): 79 | if isinstance(name, six.text_type): 80 | name = name.encode("ISO-8859-1") 81 | if isinstance(value, six.text_type): 82 | value = value.encode("ISO-8859-1") 83 | return b''.join((pack_len(name), pack_len(value), name, value)) 84 | 85 | def unpack_len(buf, pos): 86 | if sys.version_info < (3, 0): 87 | _len = ord(buf[pos]) 88 | else: 89 | _len = buf[pos] 90 | if _len & 128: 91 | _len = length_struct.unpack_from(buf, pos)[0] & 0x7fffffff 92 | pos += 4 93 | else: 94 | pos += 1 95 | return _len, pos 96 | 97 | def unpack_pairs(data): 98 | end = len(data) 99 | pos = 0 100 | while pos < end: 101 | try: 102 | name_len, pos = unpack_len(data, pos) 103 | value_len, pos = unpack_len(data, pos) 104 | except (IndexError, struct.error): 105 | raise ValueError('Buffer is too short') 106 | 107 | if end - pos < name_len + value_len: 108 | raise ValueError('Buffer is {0} bytes short'.format( 109 | name_len + value_len - (end - pos))) 110 | name = data[pos:pos + name_len] 111 | pos += name_len 112 | value = data[pos:pos + value_len] 113 | pos += value_len 114 | yield name, value 115 | -------------------------------------------------------------------------------- /gevent_fastcgi/wsgi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2013, Alexander Kulakov 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from __future__ import absolute_import 22 | 23 | import six 24 | import sys 25 | import logging 26 | from traceback import format_exception 27 | import re 28 | from wsgiref.handlers import BaseCGIHandler 29 | 30 | from zope.interface import implementer 31 | 32 | from .interfaces import IRequestHandler 33 | from .server import Request, FastCGIServer 34 | 35 | 36 | __all__ = ('WSGIRequestHandler', 'WSGIRefRequestHandler', 'WSGIServer') 37 | 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | mandatory_environ = ( 42 | 'REQUEST_METHOD', 43 | 'SCRIPT_NAME', 44 | 'PATH_INFO', 45 | 'QUERY_STRING', 46 | 'CONTENT_TYPE', 47 | 'CONTENT_LENGTH', 48 | 'SERVER_NAME', 49 | 'SERVER_PORT', 50 | 'SERVER_PROTOCOL', 51 | ) 52 | 53 | 54 | @implementer(IRequestHandler) 55 | class WSGIRefRequestHandler(object): 56 | def __init__(self, app): 57 | self.app = app 58 | 59 | def __call__(self, request): 60 | handler = self.CGIHandler(request) 61 | handler.run(self.app) 62 | 63 | class CGIHandler(BaseCGIHandler): 64 | 65 | def __init__(self, request): 66 | BaseCGIHandler.__init__(self, request.stdin, request.stdout, 67 | request.stderr, request.environ) 68 | 69 | def log_exception(self, exc_info): 70 | try: 71 | logger.exception('WSGI application failed') 72 | finally: 73 | exc_info = None 74 | 75 | 76 | class WSGIRequest(object): 77 | 78 | status_pattern = re.compile(r'^[1-5]\d\d .+$') 79 | 80 | def __init__(self, fastcgi_request): 81 | self._environ = self.make_environ(fastcgi_request) 82 | self._stdout = fastcgi_request.stdout 83 | self._stderr = fastcgi_request.stderr 84 | self._status = None 85 | self._headers = [] 86 | self._headers_sent = False 87 | 88 | def make_environ(self, fastcgi_request): 89 | env = fastcgi_request.environ 90 | for name in mandatory_environ: 91 | env.setdefault(name, '') 92 | env['wsgi.version'] = (1, 0) 93 | env['wsgi.input'] = fastcgi_request.stdin 94 | env['wsgi.errors'] = fastcgi_request.stderr 95 | env['wsgi.multithread'] = True 96 | env['wsgi.multiprocess'] = False 97 | env['wsgi.run_once'] = False 98 | 99 | https = env.get('HTTPS', '').lower() 100 | if https in ('yes', 'on', '1'): 101 | env['wsgi.url_scheme'] = 'https' 102 | else: 103 | env['wsgi.url_scheme'] = 'http' 104 | 105 | return env 106 | 107 | def start_response(self, status, headers, exc_info=None): 108 | if exc_info is not None: 109 | try: 110 | if self._headers_sent: 111 | six.reraise(exc_info[0], exc_info[1], exc_info[2]) 112 | finally: 113 | exc_info = None 114 | 115 | self._status = status 116 | self._headers = headers 117 | 118 | return self._app_write 119 | 120 | def finish(self, app_iter): 121 | if self._headers_sent: 122 | # _app_write has been already called 123 | self._stdout.writelines(app_iter) 124 | else: 125 | app_iter = iter(app_iter) 126 | for chunk in app_iter: 127 | # do nothing until first non-empty chunk 128 | if chunk: 129 | self._send_headers() 130 | self._stdout.write(chunk) 131 | self._stdout.writelines(app_iter) 132 | break 133 | else: 134 | # app_iter had no data 135 | self._headers.append(('Content-length', '0')) 136 | self._send_headers() 137 | 138 | self._stdout.close() 139 | self._stderr.close() 140 | 141 | def _app_write(self, chunk): 142 | if not self._headers_sent: 143 | self._send_headers() 144 | self._stdout.write(chunk) 145 | 146 | def _send_headers(self): 147 | headers = ['Status: {0}\r\n'.format(self._status)] 148 | headers.extend(('{0}: {1}\r\n'.format(name, value) 149 | for name, value in self._headers)) 150 | headers.append('\r\n') 151 | self._stdout.writelines(headers) 152 | self._headers_sent = True 153 | 154 | @implementer(IRequestHandler) 155 | class WSGIRequestHandler(object): 156 | def __init__(self, app): 157 | self.app = app 158 | 159 | def __call__(self, fastcgi_request): 160 | request = WSGIRequest(fastcgi_request) 161 | try: 162 | app_iter = self.app(request._environ, request.start_response) 163 | request.finish(app_iter) 164 | if hasattr(app_iter, 'close'): 165 | app_iter.close() 166 | except Exception: 167 | exc_info = sys.exc_info() 168 | try: 169 | logger.exception('Application raised exception') 170 | request.start_response('500 Internal Server Error', [ 171 | ('Content-type', 'text/plain'), 172 | ]) 173 | request.finish(map(str, format_exception(*exc_info))) 174 | finally: 175 | exc_info = None 176 | 177 | 178 | class WSGIServer(FastCGIServer): 179 | 180 | def __init__(self, address, app, **kwargs): 181 | handler = WSGIRequestHandler(app) 182 | super(WSGIServer, self).__init__(address, handler, **kwargs) 183 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [sdist] 2 | formats = gztar,bztar,zip 3 | 4 | [nosetests] 5 | cover_package = gevent_fastcgi 6 | with_coverage = 1 7 | cover_erase = 1 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from setuptools import setup, Extension, find_packages 4 | 5 | 6 | ext_modules = [] 7 | # C speedups are no good for PyPy 8 | if '__pypy__' not in sys.builtin_module_names: 9 | if os.name == "nt": 10 | ext_modules.append( 11 | Extension('gevent_fastcgi.speedups', ['gevent_fastcgi/speedups.c'], libraries=["Ws2_32"])) 12 | else: 13 | ext_modules.append( 14 | Extension('gevent_fastcgi.speedups', ['gevent_fastcgi/speedups.c'])) 15 | 16 | setup( 17 | name='gevent-fastcgi', 18 | version='1.1.0.0', 19 | description='''FastCGI/WSGI client and server implemented using gevent 20 | library''', 21 | long_description=''' 22 | FastCGI/WSGI server implementation using gevent library. No need to 23 | monkeypatch and slow down your favourite FastCGI server in order to make 24 | it "green". 25 | 26 | Supports connection multiplexing. Out-of-the-box support for Django and 27 | frameworks that use PasteDeploy including Pylons and Pyramid. 28 | ''', 29 | keywords='fastcgi gevent wsgi', 30 | author='Alexander Kulakov', 31 | author_email='a.kulakov@mail.ru', 32 | url='http://github.com/momyc/gevent-fastcgi', 33 | packages=find_packages(exclude=('gevent_fastcgi.tests.*',)), 34 | zip_safe=True, 35 | license='MIT', 36 | install_requires=[ 37 | "zope.interface>=3.8.0", 38 | "gevent>=0.13.6", 39 | "six", 40 | ], 41 | entry_points={ 42 | 'paste.server_runner': [ 43 | 'fastcgi = gevent_fastcgi.adapters.paste_deploy:fastcgi_server_runner', 44 | 'wsgi = gevent_fastcgi.adapters.paste_deploy:wsgi_server_runner', 45 | 'wsgiref = gevent_fastcgi.adapters.paste_deploy:wsgiref_server_runner', 46 | ], 47 | }, 48 | classifiers=( 49 | 'Development Status :: 5 - Production/Stable', 50 | 'License :: OSI Approved :: MIT License', 51 | 'Programming Language :: Python :: 2', 52 | 'Programming Language :: Python :: 2.6', 53 | 'Programming Language :: Python :: 2.7', 54 | 'Programming Language :: Python :: 3', 55 | 'Programming Language :: Python :: 3.5', 56 | 'Programming Language :: Python :: 3.6', 57 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 58 | ), 59 | test_suite="tests", 60 | tests_require=['mock'], 61 | ext_modules=ext_modules 62 | ) 63 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/tests/__init__.py -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/tests/base/__init__.py -------------------------------------------------------------------------------- /tests/base/test_connection.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, with_statement 2 | 3 | import unittest 4 | from random import randint 5 | 6 | from gevent_fastcgi.const import ( 7 | FCGI_STDIN, 8 | FCGI_STDOUT, 9 | FCGI_STDERR, 10 | FCGI_DATA, 11 | FCGI_RECORD_TYPES, 12 | FCGI_RECORD_HEADER_LEN, 13 | FCGI_MAX_CONTENT_LEN, 14 | ) 15 | from gevent_fastcgi.base import Record, Connection, PartialRead 16 | from ..utils import binary_data, MockSocket 17 | 18 | 19 | class ConnectionTests(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.sock = MockSocket() 23 | 24 | def tearDown(self): 25 | del self.sock 26 | 27 | def test_read_write(self): 28 | record_type = FCGI_DATA 29 | request_id = randint(1, 65535) 30 | data = binary_data() 31 | record = Record(record_type, data, request_id) 32 | 33 | conn = Connection(self.sock) 34 | conn.write_record(record) 35 | 36 | self.sock.flip() 37 | 38 | record = conn.read_record() 39 | assert record.type == record_type 40 | assert record.content == data 41 | assert record.request_id == request_id 42 | 43 | assert conn.read_record() is None 44 | 45 | def test_read_write_long_content(self): 46 | data = binary_data(FCGI_MAX_CONTENT_LEN + 1) 47 | conn = Connection(self.sock) 48 | with self.assertRaises(ValueError): 49 | conn.write_record(Record(FCGI_STDERR, data, 1)) 50 | 51 | def test_partial_read(self): 52 | conn = Connection(self.sock) 53 | 54 | data = binary_data(FCGI_RECORD_HEADER_LEN - 1) 55 | 56 | self.sock.sendall(data) 57 | 58 | self.sock.flip() 59 | 60 | self.assertRaises(PartialRead, conn.read_record) 61 | -------------------------------------------------------------------------------- /tests/base/test_input_stream.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, with_statement 2 | 3 | import unittest 4 | from six.moves import xrange 5 | 6 | from gevent import Timeout 7 | 8 | from gevent_fastcgi.base import Connection, InputStream 9 | from ..utils import binary_data, text_data, MockSocket 10 | 11 | 12 | class InputStreamTests(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.sock = MockSocket() 16 | self.conn = Connection(self.sock) 17 | self.stream = InputStream() 18 | 19 | def tearDown(self): 20 | del self.stream 21 | del self.conn 22 | del self.sock 23 | 24 | def test_feed_stream(self): 25 | stream = self.stream 26 | 27 | data_in = binary_data() 28 | stream.feed(data_in) 29 | stream.feed('') 30 | 31 | self.assertRaises(IOError, stream.feed, binary_data(1)) 32 | self.assertRaises(IOError, stream.feed, '') 33 | 34 | data = stream.read() 35 | assert data == data_in 36 | 37 | def test_iter(self): 38 | stream = self.stream 39 | data_in = [text_data() + '\r\n' for _ in xrange(17)] 40 | data_in = [line.encode("ISO-8859-1") for line in data_in] 41 | 42 | list(map(stream.feed, data_in)) 43 | stream.feed('') 44 | 45 | for line_in, line_out in zip(data_in, stream): 46 | assert line_in == line_out 47 | 48 | def test_blocks_until_eof(self): 49 | stream = self.stream 50 | data = binary_data() 51 | stream.feed(data) 52 | 53 | # no EOF mark was fed 54 | with self.assertRaises(Timeout): 55 | with Timeout(2): 56 | stream.read() 57 | 58 | def test_readlines(self): 59 | stream = self.stream 60 | data_in = [text_data() + '\r\n' for _ in xrange(13)] 61 | list(map(stream.feed, data_in)) 62 | stream.feed('') 63 | 64 | data_out = stream.readlines() 65 | data_out = [line.decode("ISO-8859-1") for line in data_out] 66 | assert data_out == data_in, data_out 67 | -------------------------------------------------------------------------------- /tests/base/test_output_stream.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, with_statement 2 | 3 | import unittest 4 | from random import randint 5 | 6 | from gevent import sleep, Timeout 7 | 8 | from gevent_fastcgi.const import ( 9 | FCGI_STDOUT, 10 | FCGI_STDERR, 11 | FCGI_MAX_CONTENT_LEN, 12 | ) 13 | 14 | from gevent_fastcgi.base import Connection, StdoutStream, StderrStream 15 | from six.moves import xrange 16 | from ..utils import binary_data, text_data, MockSocket 17 | 18 | 19 | class StreamTestsBase(object): 20 | 21 | def setUp(self): 22 | self.sock = MockSocket() 23 | self.conn = Connection(self.sock) 24 | 25 | def tearDown(self): 26 | del self.conn 27 | del self.sock 28 | 29 | def stream(self, conn=None, request_id=None): 30 | if conn is None: 31 | conn = self.conn 32 | if request_id is None: 33 | request_id = randint(1, 65535) 34 | return self.stream_class(conn, request_id) 35 | 36 | def test_constructor(self): 37 | conn = self.conn 38 | 39 | self.assertRaises(TypeError, self.stream_class) 40 | self.assertRaises(TypeError, self.stream_class, conn) 41 | 42 | stream = self.stream_class(conn, 333) 43 | assert stream.conn is conn 44 | assert stream.request_id == 333 45 | assert stream.record_type == self.stream_class.record_type 46 | assert not stream.closed 47 | 48 | def test_write(self): 49 | stream = self.stream() 50 | data = [binary_data(1024, 1) for _ in range(13)] 51 | 52 | list(map(stream.write, data)) 53 | 54 | self.sock.flip() 55 | 56 | for chunk, record in zip(data, self.conn): 57 | assert record.type == stream.record_type 58 | assert record.request_id == stream.request_id 59 | assert record.content == chunk 60 | assert self.conn.read_record() is None 61 | 62 | def test_long_write(self): 63 | stream = self.stream() 64 | 65 | data = binary_data(FCGI_MAX_CONTENT_LEN * 3 + 13713) 66 | stream.write(data) 67 | 68 | self.sock.flip() 69 | 70 | received = [] 71 | for record in self.conn: 72 | assert record.type == stream.record_type 73 | assert record.request_id == stream.request_id 74 | received.append(record.content) 75 | assert b''.join(received) == b''.join([data]) 76 | 77 | def test_long_writelines(self): 78 | stream = self.stream() 79 | 80 | data = [binary_data(37137) for _ in range(3)] 81 | stream.writelines(data) 82 | 83 | self.sock.flip() 84 | 85 | received = [] 86 | for record in self.conn: 87 | assert record.type == stream.record_type 88 | assert record.request_id == stream.request_id 89 | received.append(record.content) 90 | assert b''.join(received) == b''.join(data) 91 | 92 | def test_empty_write(self): 93 | conn = self.conn 94 | # calling this would raise AttributeError 95 | conn.write_record = None 96 | 97 | stream = self.stream(conn=conn) 98 | stream.write('') 99 | stream.flush() 100 | stream.writelines('' for _ in range(13)) 101 | 102 | def test_close(self): 103 | stream = self.stream() 104 | 105 | # should send EOF record 106 | stream.close() 107 | # should fail since stream was closed 108 | self.assertRaises(IOError, stream.write, '') 109 | self.assertRaises(IOError, stream.writelines, (text_data(137) 110 | for _ in range(3))) 111 | 112 | self.sock.flip() 113 | 114 | # should receive EOF record 115 | record = self.conn.read_record() 116 | assert record.type == stream.record_type 117 | assert record.content == b'' 118 | assert record.request_id == stream.request_id 119 | 120 | assert self.conn.read_record() is None 121 | 122 | 123 | class StdoutStreamTests(StreamTestsBase, unittest.TestCase): 124 | 125 | stream_class = StdoutStream 126 | 127 | def test_writelines(self): 128 | stream = self.stream() 129 | 130 | def app_iter(delay): 131 | yield text_data(137) 132 | sleep(delay) 133 | yield text_data(137137) 134 | 135 | with self.assertRaises(Timeout): 136 | Timeout(1).start() 137 | stream.writelines(app_iter(3)) 138 | 139 | 140 | class StderrStreamTests(StreamTestsBase, unittest.TestCase): 141 | 142 | stream_class = StderrStream 143 | 144 | def test_writelines(self): 145 | stream = self.stream() 146 | data = [text_data(7) + '\r\n' for _ in xrange(3)] 147 | 148 | stream.writelines(data) 149 | 150 | self.sock.flip() 151 | 152 | data_in = ''.join(data) 153 | data_out = b''.join(record.content for record in self.conn 154 | if (record.type == stream.record_type 155 | and record.request_id == stream.request_id)) 156 | 157 | assert data_in == data_out.decode("ISO-8859-1") 158 | -------------------------------------------------------------------------------- /tests/base/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import sys 4 | import imp 5 | import unittest 6 | from itertools import product 7 | 8 | from gevent_fastcgi.utils import pack_pairs, unpack_pairs 9 | 10 | 11 | SHORT_STR = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 12 | MEDIUM_STR = SHORT_STR * 32 13 | LONG_STR = MEDIUM_STR * 32 14 | STRINGS = (b'', SHORT_STR, MEDIUM_STR, LONG_STR) 15 | 16 | 17 | class UtilsTests(unittest.TestCase): 18 | 19 | def test_pack_unpack_pairs(self): 20 | pairs = tuple(product(STRINGS, STRINGS)) 21 | 22 | assert pairs == tuple(unpack_pairs(pack_pairs(pairs))) 23 | 24 | def test_too_long(self): 25 | TOO_LONG_STR = LONG_STR * int(0x7fffffff / len(LONG_STR) + 1) 26 | pairs = product(STRINGS, (TOO_LONG_STR,)) 27 | 28 | for pair in pairs: 29 | with self.assertRaises(ValueError): 30 | pack_pairs((pair,)) 31 | 32 | 33 | class NoSpeedupsUtilsTests(UtilsTests): 34 | """ 35 | Makes importing gevent_fastcgi.speedups fail with ImportError to enforce 36 | usage of Python implementation of pack_pairs/unpack_pairs 37 | """ 38 | def setUp(self): 39 | sys.modules['gevent_fastcgi.speedups'] = None 40 | if 'gevent_fastcgi.utils' in sys.modules: 41 | sys.modules['gevent_fastcgi.utils'] = imp.reload( 42 | sys.modules['gevent_fastcgi.utils']) 43 | 44 | def tearDown(self): 45 | del sys.modules['gevent_fastcgi.speedups'] 46 | sys.modules['gevent_fastcgi.utils'] = imp.reload( 47 | sys.modules['gevent_fastcgi.utils']) 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /tests/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/tests/server/__init__.py -------------------------------------------------------------------------------- /tests/server/test_connection_handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import unittest 4 | import mock 5 | from itertools import count 6 | 7 | from gevent import sleep, spawn, event 8 | 9 | from gevent_fastcgi.const import ( 10 | FCGI_RESPONDER, 11 | FCGI_FILTER, 12 | FCGI_AUTHORIZER, 13 | FCGI_MAX_CONNS, 14 | FCGI_MAX_REQS, 15 | FCGI_MPXS_CONNS, 16 | FCGI_NULL_REQUEST_ID, 17 | FCGI_KEEP_CONN, 18 | FCGI_GET_VALUES, 19 | FCGI_GET_VALUES_RESULT, 20 | FCGI_BEGIN_REQUEST, 21 | FCGI_END_REQUEST, 22 | FCGI_ABORT_REQUEST, 23 | FCGI_UNKNOWN_ROLE, 24 | FCGI_UNKNOWN_TYPE, 25 | FCGI_PARAMS, 26 | FCGI_STDIN, 27 | FCGI_STDOUT, 28 | FCGI_STDERR, 29 | FCGI_DATA, 30 | ) 31 | from gevent_fastcgi.base import InputStream, Record 32 | from gevent_fastcgi.utils import ( 33 | pack_begin_request, 34 | pack_pairs, 35 | unpack_pairs, 36 | unpack_end_request, 37 | unpack_unknown_type, 38 | ) 39 | from gevent_fastcgi.server import ConnectionHandler, ServerConnection 40 | from ..utils import pack_env 41 | 42 | 43 | class ConnectionHandlerTests(unittest.TestCase): 44 | 45 | def test_unknown_request(self): 46 | records = ( 47 | (FCGI_STDIN, '', next_req_id()), 48 | (FCGI_ABORT_REQUEST, '', next_req_id()), 49 | (FCGI_PARAMS, pack_env(), next_req_id()), 50 | (FCGI_DATA, 'data', next_req_id()), 51 | ) 52 | 53 | for rec in records: 54 | handler = run_handler((rec,)) 55 | conn = handler.conn 56 | 57 | assert conn.close.called 58 | assert not read_records(conn) 59 | assert not handler.requests 60 | assert not handler.request_handler.called 61 | 62 | def test_unknown_record_type(self): 63 | rec_type = 123 64 | records = ( 65 | (rec_type, ), 66 | ) 67 | 68 | handler = run_handler(records) 69 | 70 | rec = find_rec(handler, FCGI_UNKNOWN_TYPE) 71 | assert rec and unpack_unknown_type(rec.content) 72 | 73 | def test_get_values(self): 74 | req_id = next_req_id 75 | records = ( 76 | (FCGI_GET_VALUES, pack_pairs( 77 | (name, '') for name in ( 78 | FCGI_MAX_CONNS, 79 | FCGI_MAX_REQS, 80 | FCGI_MPXS_CONNS, 81 | ) 82 | )), 83 | ) 84 | 85 | handler = run_handler(records) 86 | 87 | assert not handler.requests 88 | assert not handler.request_handler.called 89 | assert handler.conn.close.called 90 | 91 | rec = find_rec(handler, FCGI_GET_VALUES_RESULT) 92 | assert rec 93 | assert unpack_pairs(rec.content) 94 | 95 | def test_request(self): 96 | req_id = next_req_id() 97 | role = FCGI_RESPONDER 98 | flags = 0 99 | records = ( 100 | (FCGI_BEGIN_REQUEST, pack_begin_request(role, flags), req_id), 101 | (FCGI_PARAMS, pack_env(), req_id), 102 | (FCGI_PARAMS, '', req_id), 103 | ) 104 | 105 | handler = run_handler(records, role=role) 106 | 107 | assert not handler.requests 108 | assert handler.request_handler.call_count == 1 109 | assert handler.conn.close.called 110 | 111 | rec = find_rec(handler, FCGI_END_REQUEST, req_id) 112 | assert rec and unpack_end_request(rec.content) 113 | 114 | for stream in FCGI_STDOUT, FCGI_STDERR: 115 | assert b'' == read_stream(handler, stream, req_id) 116 | 117 | def test_abort_request(self): 118 | req_id = next_req_id() 119 | role = FCGI_RESPONDER 120 | flags = 0 121 | records = ( 122 | (FCGI_BEGIN_REQUEST, pack_begin_request(role, flags), req_id), 123 | (FCGI_PARAMS, pack_env(), req_id), 124 | (FCGI_PARAMS, '', req_id), 125 | # request_handler gets spawned after PARAMS is "closed" 126 | # lets give it a chance to run 127 | 0.1, 128 | # then abort it 129 | (FCGI_ABORT_REQUEST, '', req_id), 130 | ) 131 | 132 | # use request_handler that waits on STDIN so we can abort 133 | # it while it's running 134 | handler = run_handler(records, role=role, 135 | request_handler=copy_stdin_to_stdout) 136 | 137 | rec = find_rec(handler, FCGI_END_REQUEST, req_id) 138 | assert rec and unpack_end_request(rec.content) 139 | 140 | for stream in FCGI_STDOUT, FCGI_STDERR: 141 | assert b'' == read_stream(handler, stream, req_id) 142 | 143 | def test_request_multiplexing(self): 144 | req_id = next_req_id() 145 | req_id_2 = next_req_id() 146 | req_id_3 = next_req_id() 147 | role = FCGI_RESPONDER 148 | flags = 0 149 | records = ( 150 | (FCGI_BEGIN_REQUEST, pack_begin_request(role, flags), req_id), 151 | (FCGI_PARAMS, pack_env(), req_id), 152 | (FCGI_BEGIN_REQUEST, pack_begin_request(role, flags), req_id_2), 153 | (FCGI_BEGIN_REQUEST, pack_begin_request(role, flags), req_id_3), 154 | (FCGI_PARAMS, pack_env(), req_id_3), 155 | (FCGI_PARAMS, pack_env(), req_id_2), 156 | (FCGI_PARAMS, '', req_id_2), 157 | (FCGI_PARAMS, '', req_id), 158 | (FCGI_ABORT_REQUEST, '', req_id_3), 159 | ) 160 | 161 | handler = run_handler(records, role=role) 162 | 163 | assert not handler.requests 164 | assert handler.request_handler.call_count == 2 165 | assert handler.conn.close.called 166 | 167 | for r_id in req_id, req_id_2, req_id_3: 168 | rec = find_rec(handler, FCGI_END_REQUEST, r_id) 169 | assert rec and unpack_end_request(rec.content) 170 | 171 | for stream in FCGI_STDOUT, FCGI_STDERR: 172 | assert b'' == read_stream(handler, stream, r_id) 173 | 174 | 175 | # Helper functions 176 | 177 | def copy_stdin_to_stdout(request): 178 | """ 179 | Simple request handler 180 | """ 181 | request.stdout.write(request.stdin.read()) 182 | 183 | 184 | def make_record(record_type, content='', request_id=FCGI_NULL_REQUEST_ID): 185 | return Record(record_type, content, request_id) 186 | 187 | 188 | def iter_records(records, done=None): 189 | for rec in records: 190 | if isinstance(rec, tuple): 191 | rec = make_record(*rec) 192 | elif isinstance(rec, (int, float)): 193 | sleep(rec) 194 | continue 195 | elif isinstance(rec, event.Event): 196 | rec.set() 197 | continue 198 | yield rec 199 | sleep(0) 200 | 201 | 202 | def run_handler(records, role=FCGI_RESPONDER, request_handler=None, 203 | capabilities=None, timeout=None): 204 | conn = mock.MagicMock() 205 | conn.__iter__.return_value = iter_records(records) 206 | 207 | if capabilities is None: 208 | capabilities = { 209 | FCGI_MAX_CONNS: '1', 210 | FCGI_MAX_REQS: '1', 211 | FCGI_MPXS_CONNS: '0', 212 | } 213 | 214 | if request_handler is None: 215 | request_handler = mock.MagicMock() 216 | 217 | handler = ConnectionHandler(conn, role, capabilities, request_handler) 218 | g = spawn(handler.run) 219 | g.join(timeout) 220 | 221 | return handler 222 | 223 | 224 | def read_records(conn, req_id=None): 225 | return [args[0] for args, kw in conn.write_record.call_args_list 226 | if req_id is None or args[0].request_id == req_id] 227 | 228 | 229 | def find_rec(handler, rec_type, req_id=FCGI_NULL_REQUEST_ID): 230 | records = read_records(handler.conn, req_id) 231 | for rec in records: 232 | if rec.type == rec_type: 233 | return rec 234 | assert False, 'No %s record found in %r' % (rec_type, records) 235 | 236 | 237 | def read_stream(handler, rec_type, req_id): 238 | closed = False 239 | content = [] 240 | 241 | for rec in read_records(handler.conn, req_id): 242 | if rec.type == rec_type: 243 | assert not closed, 'Stream is already closed' 244 | if rec.content: 245 | content.append(rec.content) 246 | else: 247 | closed = True 248 | 249 | assert closed, 'Stream was not closed' 250 | 251 | return b''.join(content) 252 | 253 | c = count(1) 254 | def do_next(): 255 | return next(c) 256 | 257 | next_req_id = do_next 258 | 259 | 260 | if __name__ == '__main__': 261 | unittest.main() 262 | -------------------------------------------------------------------------------- /tests/server/test_server.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, with_statement 2 | 3 | import os 4 | import signal 5 | import unittest 6 | import logging 7 | import errno 8 | import six 9 | 10 | 11 | class Filter(logging.Filter): 12 | def filter(self, record): 13 | record.ppid = os.getppid() 14 | return 1 15 | 16 | logging.getLogger().addFilter(Filter()) 17 | 18 | 19 | from gevent_fastcgi.const import ( 20 | FCGI_ABORT_REQUEST, 21 | FCGI_AUTHORIZER, 22 | FCGI_BEGIN_REQUEST, 23 | FCGI_DATA, 24 | FCGI_END_REQUEST, 25 | FCGI_FILTER, 26 | FCGI_GET_VALUES, 27 | FCGI_GET_VALUES_RESULT, 28 | FCGI_KEEP_CONN, 29 | FCGI_MAX_CONNS, 30 | FCGI_MAX_REQS, 31 | FCGI_MPXS_CONNS, 32 | FCGI_PARAMS, 33 | FCGI_REQUEST_COMPLETE, 34 | FCGI_RESPONDER, 35 | FCGI_STDERR, 36 | FCGI_STDIN, 37 | FCGI_STDOUT, 38 | FCGI_UNKNOWN_ROLE, 39 | FCGI_UNKNOWN_TYPE, 40 | FCGI_NULL_REQUEST_ID, 41 | ) 42 | from gevent_fastcgi.base import Record 43 | from gevent_fastcgi.utils import ( 44 | pack_pairs, unpack_pairs, pack_begin_request, unpack_end_request) 45 | from ..utils import ( 46 | WSGIApplication as app, 47 | start_wsgi_server, 48 | make_connection, 49 | Response, 50 | pack_env, 51 | binary_data, 52 | ) 53 | 54 | 55 | class ServerTests(unittest.TestCase): 56 | 57 | def test_address(self): 58 | unix_address = 'socket.{0}'.format(os.getpid()) 59 | tcp_address = ('127.0.0.1', 47231) 60 | addresses = (tcp_address, unix_address) 61 | if os.name == "nt": 62 | addresses = (tcp_address,) # Windows do not support unix socket 63 | for address in addresses: 64 | num_workers = 2 65 | if os.name == "nt": 66 | num_workers = 1 # Windows do not support multiple processes 67 | with start_wsgi_server(address, num_workers=num_workers): 68 | with make_connection(address) as conn: 69 | self._run_get_values(conn) 70 | # check if socket file was removed 71 | if isinstance(address, six.string_types): 72 | assert not os.path.exists(address) 73 | 74 | def test_role(self): 75 | for role in (FCGI_RESPONDER, FCGI_FILTER, FCGI_AUTHORIZER): 76 | with start_wsgi_server(role=role) as server: 77 | with make_connection(server.address) as conn: 78 | self._run_get_values(conn) 79 | 80 | for bad_role in (979897, -1): 81 | with self.assertRaises(ValueError): 82 | with start_wsgi_server(role=bad_role): 83 | pass 84 | 85 | def test_unknown_request_id(self): 86 | with start_wsgi_server() as server: 87 | with make_connection(server.address) as conn: 88 | conn.write_record(Record(FCGI_ABORT_REQUEST, '', 1)) 89 | conn.done_writing() 90 | assert conn.read_record() is None 91 | 92 | def test_responder(self): 93 | request_id = 1 94 | request = ( 95 | Record(FCGI_BEGIN_REQUEST, 96 | pack_begin_request(FCGI_RESPONDER, 0), request_id), 97 | Record(FCGI_PARAMS, pack_env(REQUEST_METHOD='POST', HTTPS='yes'), 98 | request_id), 99 | Record(FCGI_PARAMS, '', request_id), 100 | Record(FCGI_STDIN, binary_data(), request_id), 101 | Record(FCGI_STDIN, '', request_id), 102 | ) 103 | response = self._handle_one_request(request_id, request) 104 | assert response.request_status == FCGI_REQUEST_COMPLETE 105 | 106 | def test_filter(self): 107 | request_id = 2 108 | request = [ 109 | Record(FCGI_BEGIN_REQUEST, 110 | pack_begin_request(FCGI_FILTER, 0), request_id), 111 | Record(FCGI_PARAMS, pack_env(), request_id), 112 | Record(FCGI_PARAMS, '', request_id), 113 | Record(FCGI_STDIN, '', request_id), 114 | Record(FCGI_DATA, '', request_id), 115 | ] 116 | response = self._handle_one_request( 117 | request_id, request, role=FCGI_FILTER) 118 | assert response.request_status == FCGI_REQUEST_COMPLETE 119 | 120 | def test_authorizer(self): 121 | request_id = 13 122 | request = [ 123 | Record(FCGI_BEGIN_REQUEST, 124 | pack_begin_request(FCGI_AUTHORIZER, 0), request_id), 125 | Record(FCGI_PARAMS, pack_env(), request_id), 126 | Record(FCGI_PARAMS, '', request_id), 127 | ] 128 | response = self._handle_one_request( 129 | request_id, request, role=FCGI_AUTHORIZER, app=app(response='')) 130 | assert response.request_status == FCGI_REQUEST_COMPLETE 131 | 132 | def test_keep_conn(self): 133 | data = binary_data() 134 | requests = [ 135 | # keep "connection" open 136 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 137 | FCGI_RESPONDER, FCGI_KEEP_CONN), 3), 138 | Record(FCGI_PARAMS, pack_env(REQUEST_METHOD='POST'), 3), 139 | Record(FCGI_PARAMS, '', 3), 140 | Record(FCGI_STDIN, data, 3), 141 | Record(FCGI_STDIN, '', 3), 142 | ] 143 | 144 | # following requests should be served too 145 | for request_id in (4, 44, 444): 146 | requests += [ 147 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 148 | FCGI_RESPONDER, 0), request_id), 149 | Record(FCGI_PARAMS, 150 | pack_env(REQUEST_METHOD='POST'), request_id), 151 | Record(FCGI_PARAMS, '', request_id), 152 | Record(FCGI_STDIN, data, request_id), 153 | Record(FCGI_STDIN, '', request_id), 154 | ] 155 | for response in self._handle_requests((3, 4, 44, 444), requests): 156 | assert response.request_status == FCGI_REQUEST_COMPLETE 157 | 158 | def test_wrong_role(self): 159 | request_id = 5 160 | request = [ 161 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 162 | FCGI_RESPONDER, 0), request_id), 163 | ] 164 | response = self._handle_one_request( 165 | request_id, request, role=FCGI_FILTER) 166 | assert response.request_status == FCGI_UNKNOWN_ROLE 167 | 168 | def test_abort_request(self): 169 | request_id = 6 170 | request = [ 171 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 172 | FCGI_RESPONDER, 0), request_id), 173 | Record(FCGI_ABORT_REQUEST, '', request_id), 174 | ] 175 | response = self._handle_one_request(request_id, request) 176 | assert response.request_status == FCGI_REQUEST_COMPLETE 177 | 178 | def test_multiplexer(self): 179 | data = binary_data() 180 | requests = [ 181 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 182 | FCGI_RESPONDER, 0), 8), 183 | Record(FCGI_PARAMS, pack_env(REQUEST_METHOD='POST'), 8), 184 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 185 | FCGI_RESPONDER, 0), 9), 186 | Record(FCGI_PARAMS, pack_env(REQUEST_METHOD='POST'), 9), 187 | Record(FCGI_PARAMS, '', 9), 188 | Record(FCGI_PARAMS, '', 8), 189 | Record(FCGI_STDIN, data, 9), 190 | Record(FCGI_STDIN, data, 8), 191 | Record(FCGI_STDIN, '', 9), 192 | Record(FCGI_STDIN, '', 8), 193 | ] 194 | for response in self._handle_requests((8, 9), requests): 195 | assert response.request_status == FCGI_REQUEST_COMPLETE 196 | assert response.stdout.eof_received 197 | headers, body = response.parse() 198 | assert headers.get(b'Status') == b'200 OK', repr(headers) 199 | assert body == data 200 | 201 | def test_failed_request(self): 202 | error = AssertionError('Mock application failure SIMULATION') 203 | request_id = 10 204 | request = [ 205 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 206 | FCGI_RESPONDER, 0), request_id), 207 | Record(FCGI_PARAMS, '', request_id), 208 | Record(FCGI_STDIN, '', request_id), 209 | ] 210 | response = self._handle_one_request(request_id, request, 211 | app=app(exception=error)) 212 | assert response.stdout.eof_received 213 | headers, body = response.parse() 214 | assert headers.get(b'Status', b'').startswith(b'500 ') 215 | 216 | request_id = 11 217 | request = [ 218 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 219 | FCGI_RESPONDER, 0), request_id), 220 | Record(FCGI_PARAMS, '', request_id), 221 | Record(FCGI_STDIN, '', request_id), 222 | ] 223 | response = self._handle_one_request(request_id, request, 224 | app=app(delay=1, exception=error)) 225 | 226 | def test_empty_response(self): 227 | request_id = 12 228 | request = [ 229 | Record(FCGI_BEGIN_REQUEST, pack_begin_request( 230 | FCGI_RESPONDER, 0), request_id), 231 | Record(FCGI_PARAMS, '', request_id), 232 | Record(FCGI_STDIN, '', request_id), 233 | ] 234 | response = self._handle_one_request(request_id, request, 235 | app=app(response='')) 236 | assert response.stdout.eof_received 237 | headers, body = response.parse() 238 | assert len(body) == 0, repr(body) 239 | 240 | @unittest.skipIf(os.name == "nt", "Test not supported on Windows") 241 | def test_restart_workers(self): 242 | from gevent import sleep 243 | 244 | with start_wsgi_server(num_workers=4) as server: 245 | assert server.num_workers == 4 246 | workers = server._workers 247 | assert len(workers) == server.num_workers 248 | worker = workers[2] 249 | os.kill(worker, signal.SIGKILL) 250 | sleep(0.1) 251 | try: 252 | os.kill(worker, 0) 253 | except OSError as e: 254 | assert e.errno == errno.ESRCH 255 | sleep(5) 256 | assert len(server._workers) == server.num_workers 257 | assert worker not in server._workers 258 | 259 | # Helpers 260 | 261 | def _run_get_values(self, conn): 262 | names = (FCGI_MAX_CONNS, FCGI_MAX_REQS, FCGI_MPXS_CONNS) 263 | get_values_record = Record(FCGI_GET_VALUES, 264 | pack_pairs(dict.fromkeys(names, '')), 265 | FCGI_NULL_REQUEST_ID) 266 | 267 | conn.write_record(get_values_record) 268 | conn.done_writing() 269 | done = False 270 | for record in conn: 271 | self.assertFalse(done) 272 | self.assertEquals(record.type, FCGI_GET_VALUES_RESULT) 273 | values = dict(unpack_pairs(record.content)) 274 | for name in names: 275 | self.assertIn(name.encode("ISO-8859-1") if isinstance(name, six.text_type) else name, values) 276 | done = True 277 | 278 | def _handle_one_request(self, request_id, records, **server_params): 279 | return self._handle_requests([request_id], records, **server_params)[0] 280 | 281 | def _handle_requests(self, request_ids, records, **server_params): 282 | responses = dict( 283 | (request_id, Response(request_id)) for request_id in request_ids) 284 | 285 | with start_wsgi_server(**server_params) as server: 286 | with make_connection(server.address) as conn: 287 | list(map(conn.write_record, records)) 288 | conn.done_writing() 289 | for record in conn: 290 | self.assertIn(record.request_id, responses) 291 | response = responses[record.request_id] 292 | self.assertIs(response.request_status, None, str(record)) 293 | if record.type == FCGI_STDOUT: 294 | response.stdout.feed(record.content) 295 | elif record.type == FCGI_STDERR: 296 | response.stderr.feed(record.content) 297 | elif record.type == FCGI_END_REQUEST: 298 | response.app_status, response.request_status = ( 299 | unpack_end_request(record.content)) 300 | else: 301 | self.fail('Unexpected record type %s' % record.type) 302 | 303 | return list(responses.values()) 304 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import errno 4 | import sys 5 | from random import random, randint, choice 6 | from string import digits, punctuation 7 | try: 8 | from string import ascii_letters as letters 9 | except ImportError: 10 | from string import letters 11 | from functools import wraps 12 | from contextlib import contextmanager 13 | import logging 14 | 15 | from gevent import socket, sleep 16 | 17 | from gevent_fastcgi.const import ( 18 | FCGI_RESPONDER, 19 | FCGI_MAX_CONNS, 20 | FCGI_MAX_REQS, 21 | FCGI_MPXS_CONNS, 22 | FCGI_MAX_CONTENT_LEN, 23 | ) 24 | from gevent_fastcgi.base import Connection, InputStream 25 | from gevent_fastcgi.utils import pack_pairs 26 | from gevent_fastcgi.wsgi import WSGIServer 27 | import six 28 | from six.moves import xrange 29 | 30 | 31 | __all__ = ( 32 | 'pack_env', 33 | 'binary_data', 34 | 'text_data', 35 | 'WSGIApplication', 36 | 'TestingConnection', 37 | 'start_wsgi_server', 38 | 'make_connection', 39 | 'MockSocket', 40 | 'MockServer', 41 | 'Response', 42 | ) 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | def pack_env(**vars): 48 | env = { 49 | 'SCRIPT_NAME': '', 50 | 'PATH_INFO': '/', 51 | 'REQUEST_METHOD': 'GET', 52 | 'QUERY_STRING': '', 53 | 'CONTENT_TYPE': 'text/plain', 54 | 'SERVER_NAME': '127.0.0.1', 55 | 'SERVER_PORT': '80', 56 | 'SERVER_PROTOCOL': 'HTTP/1.0', 57 | } 58 | if vars: 59 | env.update(vars) 60 | return pack_pairs(env) 61 | 62 | 63 | def some_delay(delay=None): 64 | if delay is None: 65 | delay = random * 3 66 | sleep(delay) 67 | 68 | 69 | if sys.version_info < (3, 0): 70 | _binary_source = map(chr, range(256)) 71 | else: 72 | _binary_source = list(map(lambda x: bytes([x]), range(256))) 73 | _text_source = letters + digits + punctuation 74 | 75 | 76 | def _random_data(source, max_len, min_len): 77 | if max_len is None: 78 | size = 137 79 | elif min_len is None: 80 | size = max_len 81 | else: 82 | if max_len < min_len: 83 | max_len, min_len = min_len, max_len 84 | size = randint(min_len, max_len) 85 | return (choice(source) for _ in xrange(size)) 86 | 87 | 88 | def binary_data(max_len=None, min_len=None): 89 | return b''.join(_random_data(_binary_source, max_len, min_len)) 90 | 91 | 92 | def text_data(max_len=None, min_len=None): 93 | return ''.join(_random_data(_text_source, max_len, min_len)) 94 | 95 | 96 | class WSGIApplication(object): 97 | 98 | def __init__(self, response=None, response_headers=None, exception=None, 99 | delay=None, slow=False): 100 | self.exception = exception 101 | self.response = response 102 | self.response_headers = response_headers 103 | self.delay = delay 104 | self.slow = slow 105 | 106 | def __call__(self, environ, start_response): 107 | stdin = environ['wsgi.input'] 108 | 109 | if not self.delay is None: 110 | some_delay(self.delay) 111 | 112 | if self.exception is not None: 113 | raise self.exception 114 | 115 | headers = ((self.response_headers is None) 116 | and [('Conent-Type', 'text/plain')] 117 | or self.response_headers) 118 | 119 | start_response('200 OK', headers) 120 | 121 | if self.response is None: 122 | response = [stdin.read()] or self.data 123 | elif isinstance(self.response, six.string_types): 124 | response = [self.response] 125 | else: 126 | response = self.response 127 | 128 | if self.slow: 129 | some_delay() 130 | 131 | return response 132 | 133 | data = map('\n'.__add__, [ 134 | 'Lorem ipsum dolor sit amet, consectetur adipisicing elit', 135 | 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', 136 | 't enim ad minim veniam, quis nostrud exercitation ullamco', 137 | 'laboris nisi ut aliquip ex ea commodo consequat', 138 | '', 139 | ]) 140 | 141 | 142 | class TestingConnection(Connection): 143 | 144 | def write_record(self, record): 145 | if isinstance(record, six.integer_types + (float,)): 146 | sleep(record) 147 | else: 148 | super(TestingConnection, self).write_record(record) 149 | 150 | 151 | @contextmanager 152 | def make_connection(address): 153 | af = isinstance(address, six.string_types) and getattr(socket, "AF_UNIX", 0) or socket.AF_INET 154 | sock = socket.socket(af, socket.SOCK_STREAM) 155 | try: 156 | sock.connect(address) 157 | conn = TestingConnection(sock) 158 | yield conn 159 | finally: 160 | sock.close() 161 | 162 | 163 | @contextmanager 164 | def start_wsgi_server(address=None, app=None, **kw): 165 | if address is None: 166 | address = ('127.0.0.1', randint(1024, 65535)) 167 | if app is None: 168 | app = WSGIApplication() 169 | 170 | server = WSGIServer(address, app, **kw) 171 | try: 172 | server.start() 173 | yield server 174 | finally: 175 | server.stop() 176 | 177 | 178 | def check_socket(callable): 179 | @wraps(callable) 180 | def wrapper(self, *args, **kw): 181 | return callable(self, *args, **kw) 182 | return wrapper 183 | 184 | 185 | class MockSocket(object): 186 | 187 | def __init__(self, data=b''): 188 | self.input = data 189 | self.output = b'' 190 | self.exception = False 191 | self.closed = False 192 | 193 | def send(self, data, flags=0, timeout=None): 194 | size = len(data) 195 | self.check_socket() 196 | self.output += data[:size] 197 | #self.some_delay() 198 | return size 199 | 200 | def sendall(self, data, flags=0): 201 | self.check_socket() 202 | self.output += data 203 | #self.some_delay() 204 | 205 | def recv(self, max_len=0): 206 | self.check_socket() 207 | if not self.input: 208 | return '' 209 | if max_len <= 0: 210 | max_len = self.read_size(len(self.input)) 211 | data = self.input[:max_len] 212 | self.input = self.input[max_len:] 213 | self.some_delay() 214 | return data 215 | 216 | def close(self): 217 | self.closed = True 218 | 219 | def setsockopt(self, *args): 220 | pass 221 | 222 | def flip(self): 223 | self.input, self.output = self.output, '' 224 | self.closed = False 225 | 226 | def check_socket(self): 227 | if self.closed: 228 | raise socket.error(errno.EBADF, 'Closed socket') 229 | if self.exception: 230 | raise socket.error(errno.EPIPE, 'Peer closed connection') 231 | 232 | @staticmethod 233 | def read_size(size): 234 | if bool(randint(0, 3)): 235 | size = randint(1, size) 236 | return size 237 | 238 | @staticmethod 239 | def some_delay(): 240 | sleep(random() / 27.31) 241 | 242 | 243 | class MockServer(object): 244 | 245 | def __init__(self, role=FCGI_RESPONDER, max_conns=1024, app=None, 246 | response='OK'): 247 | self.role = role 248 | self.max_conns = max_conns 249 | self.app = (app is None) and WSGIApplication() or app 250 | self.response = response 251 | 252 | def capability(self, name): 253 | if name == FCGI_MAX_CONNS: 254 | return str(self.max_conns) 255 | if name == FCGI_MAX_REQS: 256 | return str(self.max_conns ** 2) 257 | if name == FCGI_MPXS_CONNS: 258 | return '1' 259 | return '' 260 | 261 | 262 | class Response(object): 263 | 264 | def __init__(self, request_id): 265 | self.request_id = request_id 266 | self.stdout = InputStream() 267 | self.stderr = InputStream() 268 | self.request_status = None 269 | self.app_status = None 270 | 271 | def parse(self): 272 | headers, body = self.stdout.read().split(b'\r\n\r\n', 1) 273 | headers = dict( 274 | header.split(b': ', 1) for header in headers.split(b'\r\n')) 275 | return headers, body 276 | -------------------------------------------------------------------------------- /tests/wsgi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/momyc/gevent-fastcgi/4fef82c5a73a24b288d0d6c47bb63ff47921e8dc/tests/wsgi/__init__.py -------------------------------------------------------------------------------- /tests/wsgi/test_wsgi.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import sys 4 | import unittest 5 | import six 6 | from six.moves import xrange 7 | 8 | from gevent_fastcgi.const import FCGI_STDOUT, FCGI_RESPONDER 9 | from gevent_fastcgi.base import Connection 10 | from gevent_fastcgi.server import Request 11 | from gevent_fastcgi.wsgi import WSGIRequestHandler, WSGIRefRequestHandler 12 | from ..utils import text_data, MockSocket, text_data 13 | 14 | 15 | class WSGIRequestHandlerBase(object): 16 | 17 | def test_wsgi_handler(self): 18 | data = [text_data(1, 731).encode("ISO-8859-1") for _ in xrange(137)] 19 | 20 | def app(environ, start_response): 21 | start_response('222 NotOK', [('Content-type', 'text/plain')]) 22 | return data 23 | 24 | header, body = self._handle_request(app) 25 | 26 | assert header.startswith(b'Status: 222 NotOK\r\n') 27 | assert body == b''.join(data) 28 | 29 | def test_write(self): 30 | data = [text_data(1, 7).encode("ISO-8859-1") for _ in xrange(13)] 31 | 32 | def app(environ, start_response): 33 | write = start_response('500 Internal server error', 34 | [('Content-type', 'text/plain')]) 35 | list(map(write, data)) 36 | return [] 37 | 38 | header, body = self._handle_request(app) 39 | 40 | assert header.startswith(b'Status: 500 Internal server error\r\n') 41 | assert body == b''.join(data) 42 | 43 | def test_write_and_iterable(self): 44 | data = [text_data(1, 7).encode("ISO-8859-1") for _ in xrange(13)] 45 | cut = 5 46 | 47 | def app(environ, start_response): 48 | write = start_response('200 OK', 49 | [('Content-type', 'text/plain')]) 50 | # start using write 51 | list(map(write, data[:cut])) 52 | # and the rest is as iterator 53 | return iter(data[cut:]) 54 | 55 | header, body = self._handle_request(app) 56 | 57 | assert header.startswith(b'Status: 200 OK\r\n') 58 | assert body == b''.join(data) 59 | 60 | def test_iterable_with_close(self): 61 | 62 | class Result(object): 63 | def __init__(self, data): 64 | self.data = data 65 | self.closed = False 66 | 67 | def __iter__(self): 68 | return iter(self.data) 69 | 70 | def close(self): 71 | self.closed = True 72 | 73 | data = [text_data(1, 73) for _ in range(13)] 74 | data = [line.encode("ISO-8859-1") for line in data] 75 | result = Result(data) 76 | 77 | def app(environ, start_response): 78 | start_response('200 OK', [('Content-type', 'text/plain')]) 79 | return result 80 | 81 | header, body = self._handle_request(app) 82 | 83 | assert header.startswith(b'Status: 200 OK\r\n') 84 | assert body == b''.join(data) 85 | assert result.closed 86 | 87 | def test_app_exception(self): 88 | def app(environ, start_response): 89 | start_response('200 OK', [('Content-type', 'text/plain')]) 90 | raise NameError("LETS_MAKE_SOME_MESS") 91 | 92 | header, body = self._handle_request(app) 93 | 94 | assert header.startswith(b'Status: 500 ') 95 | 96 | def test_start_response_with_exc_info(self): 97 | error_message = b'Bad things happen' 98 | 99 | def app(environ, start_response): 100 | try: 101 | raise NameError("LETS_MAKE_SOME_MESS") 102 | except NameError: 103 | start_response('200 OK', [('Content-type', 'text/plain')], 104 | sys.exc_info()) 105 | return [error_message] 106 | 107 | header, body = self._handle_request(app) 108 | 109 | assert header.startswith(b'Status: 200 OK\r\n') 110 | assert body == error_message 111 | 112 | def test_start_response_with_exc_info_headers_sent(self): 113 | greetings = b'Hello World!\r\n' 114 | error_message = b'Bad things happen' 115 | 116 | def app(environ, start_response): 117 | start_response('200 OK', [('Content-type', 'text/plain')]) 118 | # force headers to be sent 119 | yield greetings 120 | try: 121 | raise NameError("LETS_MAKE_SOME_MESS") 122 | except NameError: 123 | start_response('500 ' + error_message.decode("utf-8"), 124 | [('Content-type', 'text/plain')], 125 | sys.exc_info()) 126 | yield error_message 127 | 128 | header, body = self._handle_request(app) 129 | 130 | assert header.startswith(b'Status: 200 OK\r\n'), header 131 | assert body.startswith(greetings) 132 | 133 | def _handle_request(self, app): 134 | sock = MockSocket() 135 | conn = Connection(sock) 136 | request = Request(conn, 1, FCGI_RESPONDER) 137 | 138 | handler = self.handler_class(app) 139 | handler(request) 140 | 141 | sock.flip() 142 | 143 | stdout = b''.join( 144 | record.content for record in conn 145 | if record.type == FCGI_STDOUT) 146 | 147 | return stdout.split(b'\r\n\r\n', 1) 148 | 149 | 150 | class WSGIRequestHandlerTests(WSGIRequestHandlerBase, unittest.TestCase): 151 | 152 | handler_class = WSGIRequestHandler 153 | 154 | 155 | class WSGIRefRequestHandlerTests(WSGIRequestHandlerBase, unittest.TestCase): 156 | 157 | handler_class = WSGIRefRequestHandler 158 | --------------------------------------------------------------------------------