├── .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 |
--------------------------------------------------------------------------------