├── .coveragerc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.rst
├── docs
├── api.rst
├── conf.py
├── index.rst
├── userguide.rst
└── versionhistory.rst
├── examples
├── apache_docker.conf
├── apache_docker.sh
├── asyncio-server.py
├── curio-server.py
├── nginx_docker.conf
├── nginx_docker.sh
└── twisted-server.py
├── fcgiproto
├── __init__.py
├── connection.py
├── connection.pyi
├── constants.py
├── events.py
├── exceptions.py
├── records.py
└── states.py
├── setup.cfg
├── setup.py
├── tests
├── test_connection.py
├── test_records.py
└── test_states.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = fcgiproto
3 | branch = 1
4 |
5 | [report]
6 | show_missing = true
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .project
2 | .pydevproject
3 | .idea/
4 | .coverage
5 | .cache/
6 | .tox/
7 | .eggs/
8 | *.egg-info/
9 | *.pyc
10 | dist/
11 | docs/_build/
12 | build/
13 | virtualenv/
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: python
4 |
5 | python:
6 | - "2.7"
7 | - "3.3"
8 | - "3.4"
9 | - "3.5"
10 | - "3.6"
11 |
12 | install: pip install tox-travis coveralls
13 |
14 | script: tox
15 |
16 | after_success: coveralls
17 |
18 | deploy:
19 | provider: pypi
20 | user: agronholm
21 | password:
22 | secure: irLJvcMUgiM0Q+0mXEze2JtIJEloFdehvICmYevGMmPm/01rR//iM+WFjya754EwLrJwNYULhxAQU71ysVTf47cNu7QGL/jmQcGIpXzBNm2KOfhFiWASN8srPXHkiUn98OpQypuU64zvUiAYPYz/73G6x1qHYd2lGGfAeunXIzliUlB4f9Vap7U9LiUmRVltIVbWqT0oVFZZItsVT5pBiGBWOhTBjlZ2P3aR71eJ7S3PFYs/wDAzsobeaY7AmG7D+2eOgTt+tb7zyhQXTMD+5o2UOCJfzxxUGEf804zPQzIjuKD8XJX0TBTuKsPlB8NNt3Kwqc2KL2vEglpMoO7MtlqT8DJeecxLr72zoos5Z0KEejwgPyQAUy2ICV36t73Cu+HPnDA9LJC847PmdkU/JDpRnIqpaALorNzBKiCB6H8bly3PcQjaoLiTqhfcXnvvuk3lqWBbt4eIThdshndD5TE2oYfZhK6t4p50CwOxLpxM4fTqh7H9KiGPIjPzjFyb6nuqSCEPcv8PFHtQ0Rn9QprVyny+iW/BRtDahM18+htoBnkjpiUAwIOuBmNZpPEmXQ7w6XU7LZ/fWzGvwbgBPGuvwSxh2AtmrNFSQPSDZK73lwX6rKtEmlSaWWUPAk4tIZbFk7ZGC6U9hv28OhV/bm7ZPc4sVCViDN1qKT9EIsQ=
23 | distributions: sdist bdist_wheel
24 | on:
25 | tags: true
26 | python: "3.6"
27 | repo: agronholm/fcgiproto
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is the MIT license: http://www.opensource.org/licenses/mit-license.php
2 |
3 | Copyright (c) Alex Grönholm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this
6 | software and associated documentation files (the "Software"), to deal in the Software
7 | without restriction, including without limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
9 | to whom the Software is furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or
12 | substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19 | DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://travis-ci.org/agronholm/fcgiproto.svg?branch=master
2 | :target: https://travis-ci.org/agronholm/fcgiproto
3 | :alt: Build Status
4 | .. image:: https://coveralls.io/repos/github/agronholm/fcgiproto/badge.svg?branch=master
5 | :target: https://coveralls.io/github/agronholm/fcgiproto?branch=master
6 | :alt: Code Coverage
7 |
8 | The FastCGI_ protocol is a protocol commonly used to relay HTTP requests and responses between a
9 | front-end web server (nginx, Apache, etc.) and a back-end web application.
10 |
11 | This library implements this protocol for the web application end as a pure state-machine which
12 | only takes in bytes and returns a list of parsed events. This leaves users free to use any I/O
13 | approach they see fit (asyncio_, curio_, Twisted_, etc.). Sample code is provided for implementing
14 | a FastCGI server using a variety of I/O frameworks.
15 |
16 | .. _FastCGI: https://htmlpreview.github.io/?https://github.com/FastCGI-Archives/FastCGI.com/blob/master/docs/FastCGI%20Specification.html
17 | .. _asyncio: https://docs.python.org/3/library/asyncio.html
18 | .. _curio: https://github.com/dabeaz/curio
19 | .. _Twisted: https://twistedmatrix.com/
20 |
21 | Project links
22 | -------------
23 |
24 | * `Documentation `_
25 | * `Source code `_
26 | * `Issue tracker `_
27 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API Reference
2 | =============
3 |
4 | Classes
5 | -------
6 |
7 | .. autoclass:: fcgiproto.FastCGIConnection
8 | :members:
9 |
10 | .. autoclass:: fcgiproto.RequestEvent
11 | :members:
12 |
13 | .. autoclass:: fcgiproto.RequestBeginEvent
14 | :members:
15 |
16 | .. autoclass:: fcgiproto.RequestAbortEvent
17 | :members:
18 |
19 | .. autoclass:: fcgiproto.RequestDataEvent
20 | :members:
21 |
22 | .. autoclass:: fcgiproto.RequestSecondaryDataEvent
23 | :members:
24 |
25 | .. autoexception:: fcgiproto.ProtocolError
26 |
27 | Constants
28 | ---------
29 |
30 | * ``fcgiproto.FCGI_RESPONDER``
31 | * ``fcgiproto.FCGI_AUTHORIZER``
32 | * ``fcgiproto.FCGI_FILTER``
33 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding: utf-8
3 | import pkg_resources
4 |
5 | extensions = [
6 | 'sphinx.ext.autodoc',
7 | ]
8 |
9 | templates_path = ['_templates']
10 | source_suffix = '.rst'
11 | master_doc = 'index'
12 | project = 'fcgiproto'
13 | author = u'Alex Grönholm'
14 | copyright = '2016, ' + author
15 |
16 | v = pkg_resources.get_distribution('fcgiproto').parsed_version
17 | version = v.base_version
18 | release = v.public
19 |
20 | language = None
21 |
22 | exclude_patterns = ['_build']
23 | pygments_style = 'sphinx'
24 | todo_include_todos = False
25 |
26 | html_theme = 'classic'
27 | html_static_path = ['_static']
28 | htmlhelp_basename = 'fcgiprotodoc'
29 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | FastCGI state-machine protocol (fcgiproto)
2 | ==========================================
3 |
4 | .. include:: ../README.rst
5 | :start-line: 7
6 | :end-before: Project links
7 |
8 |
9 | Table of Contents
10 | =================
11 |
12 | .. toctree::
13 | :maxdepth: 2
14 |
15 | userguide
16 | api
17 | versionhistory
18 |
--------------------------------------------------------------------------------
/docs/userguide.rst:
--------------------------------------------------------------------------------
1 | Protocol implementor's guide
2 | ============================
3 |
4 | Creating a real-world implementation of FastCGI using fcgiproto is quite straightforward.
5 | As with other sans-io protocols, you feed incoming data to fcgiproto and it vends events in return.
6 | To invoke actions on the connection, just call its methods, like
7 | :meth:`~fcgiproto.FastCGIConnection.send_headers` and so on.
8 | To get pending outgoing data, use the :meth:`~fcgiproto.FastCGIConnection.data_to_send` method.
9 |
10 | Connection configuration
11 | ------------------------
12 |
13 | The most common role is the responder role (``FCGI_RESPONDER``). The authorizer
14 | (``FCGI_AUTHORIZER``) and filter (``FCGI_FILTER``) roles are not commonly supported by web server
15 | software. As such, you will want to leave the default role setting alone, unless you really know
16 | what you're doing.
17 |
18 | It's also possible to set FCGI management values. The FastCGI specification defines names of three
19 | values:
20 |
21 | * ``FCGI_MAX_CONNS``: The maximum number of concurrent transport connections this application will
22 | accept, e.g. ``1`` or ``10``
23 | * ``FCGI_MAX_REQS``: The maximum number of concurrent requests this application will accept, e.g.
24 | ``1`` or ``50``.
25 | * ``FCGI_MPXS_CONNS``: ``0`` if this application does not multiplex connections (i.e. handle
26 | concurrent requests over each connection), ``1`` otherwise.
27 |
28 | The connection sets ``FCGI_MPXS_CONNS`` to ``1`` by default. It should be noted that the web server
29 | may never even query for these values, so leave this setting alone unless you know you need it.
30 | At least nginx does not attempt to multiplex FCGI connections, nor does it query for any management
31 | values.
32 |
33 | Implementor's responsibilities
34 | ------------------------------
35 |
36 | The logic in :class:`~fcgiproto.FastCGIConnection` will handle most complications of the protocol.
37 | That leaves just a handful of things for I/O implementors to keep in mind:
38 |
39 | * Always get any outgoing data from the connection (using
40 | :meth:`~fcgiproto.FastCGIConnection.data_to_send`) after calling either
41 | :meth:`~fcgiproto.FastCGIConnection.feed_data` or any of the other methods, and send it to the
42 | remote host
43 | * Remember to set ``Content-Length`` if your response contains a body
44 | * Respect the ``keep_connection`` flag in :class:`~fcgiproto.RequestBeginEvent`.
45 | Close the connection after calling :meth:`~fcgiproto.FastCGIConnection.end_request` if the flag
46 | is ``False``.
47 |
48 | Handling requests
49 | -----------------
50 |
51 | **RESPONDER**
52 |
53 | The sequence for handling responder requests (the most common case) is as follows:
54 |
55 | #. a :class:`~fcgiproto.RequestBeginEvent` is received
56 | #. one or more :class:`~fcgiproto.RequestDataEvent` are received, the last one having an empty
57 | bytestring as ``data`` attribute
58 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
59 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
60 | and the last call must have ``end_request`` set to ``True``
61 |
62 | The implementor can decide whether to wait until all of the request body has been received, or
63 | start running the request handler code right after :class:`~fcgiproto.RequestBeginEvent` has been
64 | received (to facilitate streaming uploads for example).
65 |
66 | In FastCGI responses, the HTTP status code is sent using the ``Status`` header. As a convenience,
67 | the :meth:`~fcgiproto.FastCGIConnection.send_headers` method provides the ``status`` parameter
68 | to add this header.
69 |
70 | **AUTHORIZER**
71 |
72 | Authorizer requests differ from responder requests in the way that the application never receives
73 | any request body. They also don't receive the ``CONTENT_LENGTH``, ``PATH_INFO``, ``SCRIPT_NAME`` or
74 | ``PATH_TRANSLATED`` parameters, which severely limits the usefulness of this role.
75 |
76 | The request-response sequence for authorizers goes as follows:
77 |
78 | #. a :class:`~fcgiproto.RequestBeginEvent` is received
79 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
80 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
81 | and the last call must have ``end_request`` set to ``True``
82 |
83 | A response code other than ``200`` will be interpreted as a negative response.
84 |
85 | **FILTER**
86 |
87 | Filter applications receive all the same information as responders, but they are also sent a
88 | secondary data stream which they're supposed to filter.
89 |
90 | The request-response sequence for filters goes as follows:
91 |
92 | #. a :class:`~fcgiproto.RequestBeginEvent` is received
93 | #. one or more :class:`~fcgiproto.RequestDataEvent` are received, the last one having an empty
94 | bytestring as ``data`` attribute
95 | #. one or more :class:`~fcgiproto.RequestSecondaryDataEvent` are received, the last one having an
96 | empty bytestring as ``data`` attribute
97 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
98 | #. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
99 | and the last call must have ``end_request`` set to ``True``
100 |
101 | The application is expected to send the (modified) secondary data stream as the response body.
102 | It must read in all of the request body before starting to send a response (thus somewhat deviating
103 | from the sequence above), but it does not need to wait for the secondary data stream to end (for
104 | example if the response comes from a cache).
105 |
106 | Handling request aborts
107 | -----------------------
108 |
109 | If the application receives a :class:`~fcgiproto.RequestAbortEvent`, it should cease processing of
110 | the request at once. No headers or data should be sent from this point on for this request, and
111 | :meth:`~fcgiproto.FastCGIConnection.end_request` should be called as soon as possible.
112 |
113 | Running the examples
114 | --------------------
115 |
116 | The ``examples`` directory in the project source tree contains example code for several popular
117 | I/O frameworks to get you started. Just run any of the server scripts and it will start a FastCGI
118 | server listening on port 9500.
119 |
120 | Since FastCGI requires a front-end server, a Docker script and configuration files for both nginx
121 | and Apache HTTPd have been provided as a convenience. Just run either ``nginx_docker.sh`` or
122 | ``apache_docker.sh`` from the ``examples`` directory and navigate to http://127.0.0.1/ to see the
123 | result. The example code displays a web page that shows the FastCGI parameters and the request body
124 | (if any).
125 |
126 | .. note:: You may have to make adjustments to the configuration if your Docker interface address or
127 | desired host HTTP port don't match the provided configuration.
128 |
--------------------------------------------------------------------------------
/docs/versionhistory.rst:
--------------------------------------------------------------------------------
1 | Version history
2 | ===============
3 |
4 | This library adheres to `Semantic Versioning `_.
5 |
6 | **1.0.2** (2016-10-25)
7 |
8 | - Fixed setup.py to include package data (``.pyi`` files) when installing
9 |
10 | **1.0.1** (2016-10-23)
11 |
12 | - Added an alternative, Apache based docker configuration in ``examples/``
13 | - Changed response headers to be emitted with a space after ``:`` and with
14 | lines ending with CRLF instead of plain LF
15 |
16 | **1.0.0** (2016-09-04)
17 |
18 | - Initial release
19 |
--------------------------------------------------------------------------------
/examples/apache_docker.conf:
--------------------------------------------------------------------------------
1 | #
2 | # This is the main Apache HTTP server configuration file. It contains the
3 | # configuration directives that give the server its instructions.
4 | # See for detailed information.
5 | # In particular, see
6 | #
7 | # for a discussion of each configuration directive.
8 | #
9 | # Do NOT simply read the instructions in here without understanding
10 | # what they do. They're here only as hints or reminders. If you are unsure
11 | # consult the online docs. You have been warned.
12 | #
13 | # Configuration and logfile names: If the filenames you specify for many
14 | # of the server's control files begin with "/" (or "drive:/" for Win32), the
15 | # server will use that explicit path. If the filenames do *not* begin
16 | # with "/", the value of ServerRoot is prepended -- so "logs/access_log"
17 | # with ServerRoot set to "/usr/local/apache2" will be interpreted by the
18 | # server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log"
19 | # will be interpreted as '/logs/access_log'.
20 |
21 | #
22 | # ServerRoot: The top of the directory tree under which the server's
23 | # configuration, error, and log files are kept.
24 | #
25 | # Do not add a slash at the end of the directory path. If you point
26 | # ServerRoot at a non-local disk, be sure to specify a local disk on the
27 | # Mutex directive, if file-based mutexes are used. If you wish to share the
28 | # same ServerRoot for multiple httpd daemons, you will need to change at
29 | # least PidFile.
30 | #
31 | ServerRoot "/usr/local/apache2"
32 |
33 | #
34 | # Mutex: Allows you to set the mutex mechanism and mutex file directory
35 | # for individual mutexes, or change the global defaults
36 | #
37 | # Uncomment and change the directory if mutexes are file-based and the default
38 | # mutex file directory is not on a local disk or is not appropriate for some
39 | # other reason.
40 | #
41 | # Mutex default:logs
42 |
43 | #
44 | # Listen: Allows you to bind Apache to specific IP addresses and/or
45 | # ports, instead of the default. See also the
46 | # directive.
47 | #
48 | # Change this to Listen on specific IP addresses as shown below to
49 | # prevent Apache from glomming onto all bound IP addresses.
50 | #
51 | #Listen 12.34.56.78:80
52 | Listen 80
53 |
54 | #
55 | # Dynamic Shared Object (DSO) Support
56 | #
57 | # To be able to use the functionality of a module which was built as a DSO you
58 | # have to place corresponding `LoadModule' lines at this location so the
59 | # directives contained in it are actually available _before_ they are used.
60 | # Statically compiled modules (those listed by `httpd -l') do not need
61 | # to be loaded here.
62 | #
63 | # Example:
64 | # LoadModule foo_module modules/mod_foo.so
65 | #
66 | LoadModule authn_file_module modules/mod_authn_file.so
67 | #LoadModule authn_dbm_module modules/mod_authn_dbm.so
68 | #LoadModule authn_anon_module modules/mod_authn_anon.so
69 | #LoadModule authn_dbd_module modules/mod_authn_dbd.so
70 | #LoadModule authn_socache_module modules/mod_authn_socache.so
71 | LoadModule authn_core_module modules/mod_authn_core.so
72 | LoadModule authz_host_module modules/mod_authz_host.so
73 | LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
74 | LoadModule authz_user_module modules/mod_authz_user.so
75 | #LoadModule authz_dbm_module modules/mod_authz_dbm.so
76 | #LoadModule authz_owner_module modules/mod_authz_owner.so
77 | #LoadModule authz_dbd_module modules/mod_authz_dbd.so
78 | LoadModule authz_core_module modules/mod_authz_core.so
79 | #LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
80 | #LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so
81 | LoadModule access_compat_module modules/mod_access_compat.so
82 | LoadModule auth_basic_module modules/mod_auth_basic.so
83 | #LoadModule auth_form_module modules/mod_auth_form.so
84 | #LoadModule auth_digest_module modules/mod_auth_digest.so
85 | #LoadModule allowmethods_module modules/mod_allowmethods.so
86 | #LoadModule isapi_module modules/mod_isapi.so
87 | #LoadModule file_cache_module modules/mod_file_cache.so
88 | #LoadModule cache_module modules/mod_cache.so
89 | #LoadModule cache_disk_module modules/mod_cache_disk.so
90 | #LoadModule cache_socache_module modules/mod_cache_socache.so
91 | #LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
92 | #LoadModule socache_dbm_module modules/mod_socache_dbm.so
93 | #LoadModule socache_memcache_module modules/mod_socache_memcache.so
94 | #LoadModule watchdog_module modules/mod_watchdog.so
95 | #LoadModule macro_module modules/mod_macro.so
96 | #LoadModule dbd_module modules/mod_dbd.so
97 | #LoadModule bucketeer_module modules/mod_bucketeer.so
98 | #LoadModule dumpio_module modules/mod_dumpio.so
99 | #LoadModule echo_module modules/mod_echo.so
100 | #LoadModule example_hooks_module modules/mod_example_hooks.so
101 | #LoadModule case_filter_module modules/mod_case_filter.so
102 | #LoadModule case_filter_in_module modules/mod_case_filter_in.so
103 | #LoadModule example_ipc_module modules/mod_example_ipc.so
104 | #LoadModule buffer_module modules/mod_buffer.so
105 | #LoadModule data_module modules/mod_data.so
106 | #LoadModule ratelimit_module modules/mod_ratelimit.so
107 | LoadModule reqtimeout_module modules/mod_reqtimeout.so
108 | #LoadModule ext_filter_module modules/mod_ext_filter.so
109 | #LoadModule request_module modules/mod_request.so
110 | #LoadModule include_module modules/mod_include.so
111 | LoadModule filter_module modules/mod_filter.so
112 | #LoadModule reflector_module modules/mod_reflector.so
113 | #LoadModule substitute_module modules/mod_substitute.so
114 | #LoadModule sed_module modules/mod_sed.so
115 | #LoadModule charset_lite_module modules/mod_charset_lite.so
116 | #LoadModule deflate_module modules/mod_deflate.so
117 | LoadModule mime_module modules/mod_mime.so
118 | #LoadModule ldap_module modules/mod_ldap.so
119 | LoadModule log_config_module modules/mod_log_config.so
120 | #LoadModule log_debug_module modules/mod_log_debug.so
121 | #LoadModule log_forensic_module modules/mod_log_forensic.so
122 | #LoadModule logio_module modules/mod_logio.so
123 | LoadModule env_module modules/mod_env.so
124 | #LoadModule mime_magic_module modules/mod_mime_magic.so
125 | #LoadModule cern_meta_module modules/mod_cern_meta.so
126 | #LoadModule expires_module modules/mod_expires.so
127 | LoadModule headers_module modules/mod_headers.so
128 | #LoadModule ident_module modules/mod_ident.so
129 | #LoadModule usertrack_module modules/mod_usertrack.so
130 | #LoadModule unique_id_module modules/mod_unique_id.so
131 | LoadModule setenvif_module modules/mod_setenvif.so
132 | LoadModule version_module modules/mod_version.so
133 | #LoadModule remoteip_module modules/mod_remoteip.so
134 | LoadModule proxy_module modules/mod_proxy.so
135 | #LoadModule proxy_connect_module modules/mod_proxy_connect.so
136 | #LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
137 | #LoadModule proxy_http_module modules/mod_proxy_http.so
138 | LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
139 | #LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
140 | #LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
141 | #LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
142 | #LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
143 | #LoadModule proxy_express_module modules/mod_proxy_express.so
144 | #LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so
145 | #LoadModule session_module modules/mod_session.so
146 | #LoadModule session_cookie_module modules/mod_session_cookie.so
147 | #LoadModule session_dbd_module modules/mod_session_dbd.so
148 | #LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
149 | #LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
150 | #LoadModule ssl_module modules/mod_ssl.so
151 | #LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
152 | #LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
153 | #LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
154 | #LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
155 | #LoadModule dialup_module modules/mod_dialup.so
156 | #LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
157 | #LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
158 | #LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
159 | #LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
160 | LoadModule unixd_module modules/mod_unixd.so
161 | #LoadModule heartbeat_module modules/mod_heartbeat.so
162 | #LoadModule heartmonitor_module modules/mod_heartmonitor.so
163 | #LoadModule dav_module modules/mod_dav.so
164 | LoadModule status_module modules/mod_status.so
165 | LoadModule autoindex_module modules/mod_autoindex.so
166 | #LoadModule asis_module modules/mod_asis.so
167 | #LoadModule info_module modules/mod_info.so
168 | #LoadModule suexec_module modules/mod_suexec.so
169 |
170 | #LoadModule cgid_module modules/mod_cgid.so
171 |
172 |
173 | #LoadModule cgi_module modules/mod_cgi.so
174 |
175 | #LoadModule dav_fs_module modules/mod_dav_fs.so
176 | #LoadModule dav_lock_module modules/mod_dav_lock.so
177 | #LoadModule vhost_alias_module modules/mod_vhost_alias.so
178 | #LoadModule negotiation_module modules/mod_negotiation.so
179 | LoadModule dir_module modules/mod_dir.so
180 | #LoadModule imagemap_module modules/mod_imagemap.so
181 | #LoadModule actions_module modules/mod_actions.so
182 | #LoadModule speling_module modules/mod_speling.so
183 | #LoadModule userdir_module modules/mod_userdir.so
184 | LoadModule alias_module modules/mod_alias.so
185 | #LoadModule rewrite_module modules/mod_rewrite.so
186 |
187 |
188 | #
189 | # If you wish httpd to run as a different user or group, you must run
190 | # httpd as root initially and it will switch.
191 | #
192 | # User/Group: The name (or #number) of the user/group to run httpd as.
193 | # It is usually good practice to create a dedicated user and group for
194 | # running httpd, as with most system services.
195 | #
196 | User daemon
197 | Group daemon
198 |
199 |
200 |
201 | # 'Main' server configuration
202 | #
203 | # The directives in this section set up the values used by the 'main'
204 | # server, which responds to any requests that aren't handled by a
205 | # definition. These values also provide defaults for
206 | # any containers you may define later in the file.
207 | #
208 | # All of these directives may appear inside containers,
209 | # in which case these default settings will be overridden for the
210 | # virtual host being defined.
211 | #
212 |
213 | #
214 | # ServerAdmin: Your address, where problems with the server should be
215 | # e-mailed. This address appears on some server-generated pages, such
216 | # as error documents. e.g. admin@your-domain.com
217 | #
218 | ServerAdmin you@example.com
219 |
220 | #
221 | # ServerName gives the name and port that the server uses to identify itself.
222 | # This can often be determined automatically, but we recommend you specify
223 | # it explicitly to prevent problems during startup.
224 | #
225 | # If your host doesn't have a registered DNS name, enter its IP address here.
226 | #
227 | #ServerName www.example.com:80
228 |
229 | #
230 | # Deny access to the entirety of your server's filesystem. You must
231 | # explicitly permit access to web content directories in other
232 | # blocks below.
233 | #
234 |
235 | AllowOverride none
236 | Require all denied
237 |
238 |
239 | #
240 | # Note that from this point forward you must specifically allow
241 | # particular features to be enabled - so if something's not working as
242 | # you might expect, make sure that you have specifically enabled it
243 | # below.
244 | #
245 |
246 | #
247 | # DocumentRoot: The directory out of which you will serve your
248 | # documents. By default, all requests are taken from this directory, but
249 | # symbolic links and aliases may be used to point to other locations.
250 | #
251 | DocumentRoot "/usr/local/apache2/htdocs"
252 |
253 | #
254 | # Possible values for the Options directive are "None", "All",
255 | # or any combination of:
256 | # Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
257 | #
258 | # Note that "MultiViews" must be named *explicitly* --- "Options All"
259 | # doesn't give it to you.
260 | #
261 | # The Options directive is both complicated and important. Please see
262 | # http://httpd.apache.org/docs/2.4/mod/core.html#options
263 | # for more information.
264 | #
265 | Options Indexes FollowSymLinks
266 |
267 | #
268 | # AllowOverride controls what directives may be placed in .htaccess files.
269 | # It can be "All", "None", or any combination of the keywords:
270 | # AllowOverride FileInfo AuthConfig Limit
271 | #
272 | AllowOverride None
273 |
274 | #
275 | # Controls who can get stuff from this server.
276 | #
277 | Require all granted
278 |
279 |
280 | #
281 | # DirectoryIndex: sets the file that Apache will serve if a directory
282 | # is requested.
283 | #
284 |
285 | DirectoryIndex index.html
286 |
287 |
288 | #
289 | # The following lines prevent .htaccess and .htpasswd files from being
290 | # viewed by Web clients.
291 | #
292 |
293 | Require all denied
294 |
295 |
296 | #
297 | # ErrorLog: The location of the error log file.
298 | # If you do not specify an ErrorLog directive within a
299 | # container, error messages relating to that virtual host will be
300 | # logged here. If you *do* define an error logfile for a
301 | # container, that host's errors will be logged there and not here.
302 | #
303 | ErrorLog /proc/self/fd/2
304 |
305 | #
306 | # LogLevel: Control the number of messages logged to the error_log.
307 | # Possible values include: debug, info, notice, warn, error, crit,
308 | # alert, emerg.
309 | #
310 | LogLevel warn
311 |
312 |
313 | #
314 | # The following directives define some format nicknames for use with
315 | # a CustomLog directive (see below).
316 | #
317 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
318 | LogFormat "%h %l %u %t \"%r\" %>s %b" common
319 |
320 |
321 | # You need to enable mod_logio.c to use %I and %O
322 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
323 |
324 |
325 | #
326 | # The location and format of the access logfile (Common Logfile Format).
327 | # If you do not define any access logfiles within a
328 | # container, they will be logged here. Contrariwise, if you *do*
329 | # define per- access logfiles, transactions will be
330 | # logged therein and *not* in this file.
331 | #
332 | CustomLog /proc/self/fd/1 common
333 |
334 | #
335 | # If you prefer a logfile with access, agent, and referer information
336 | # (Combined Logfile Format) you can use the following directive.
337 | #
338 | #CustomLog "logs/access_log" combined
339 |
340 |
341 |
342 | #
343 | # Redirect: Allows you to tell clients about documents that used to
344 | # exist in your server's namespace, but do not anymore. The client
345 | # will make a new request for the document at its new location.
346 | # Example:
347 | # Redirect permanent /foo http://www.example.com/bar
348 |
349 | #
350 | # Alias: Maps web paths into filesystem paths and is used to
351 | # access content that does not live under the DocumentRoot.
352 | # Example:
353 | # Alias /webpath /full/filesystem/path
354 | #
355 | # If you include a trailing / on /webpath then the server will
356 | # require it to be present in the URL. You will also likely
357 | # need to provide a section to allow access to
358 | # the filesystem path.
359 |
360 | #
361 | # ScriptAlias: This controls which directories contain server scripts.
362 | # ScriptAliases are essentially the same as Aliases, except that
363 | # documents in the target directory are treated as applications and
364 | # run by the server when requested rather than as documents sent to the
365 | # client. The same rules about trailing "/" apply to ScriptAlias
366 | # directives as to Alias.
367 | #
368 | ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
369 |
370 |
371 |
372 |
373 | #
374 | # ScriptSock: On threaded servers, designate the path to the UNIX
375 | # socket used to communicate with the CGI daemon of mod_cgid.
376 | #
377 | #Scriptsock cgisock
378 |
379 |
380 | #
381 | # "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased
382 | # CGI directory exists, if you have that configured.
383 | #
384 |
385 | AllowOverride None
386 | Options None
387 | Require all granted
388 |
389 |
390 |
391 | #
392 | # TypesConfig points to the file containing the list of mappings from
393 | # filename extension to MIME-type.
394 | #
395 | TypesConfig conf/mime.types
396 |
397 | #
398 | # AddType allows you to add to or override the MIME configuration
399 | # file specified in TypesConfig for specific file types.
400 | #
401 | #AddType application/x-gzip .tgz
402 | #
403 | # AddEncoding allows you to have certain browsers uncompress
404 | # information on the fly. Note: Not all browsers support this.
405 | #
406 | #AddEncoding x-compress .Z
407 | #AddEncoding x-gzip .gz .tgz
408 | #
409 | # If the AddEncoding directives above are commented-out, then you
410 | # probably should define those extensions to indicate media types:
411 | #
412 | AddType application/x-compress .Z
413 | AddType application/x-gzip .gz .tgz
414 |
415 | #
416 | # AddHandler allows you to map certain file extensions to "handlers":
417 | # actions unrelated to filetype. These can be either built into the server
418 | # or added with the Action directive (see below)
419 | #
420 | # To use CGI scripts outside of ScriptAliased directories:
421 | # (You will also need to add "ExecCGI" to the "Options" directive.)
422 | #
423 | #AddHandler cgi-script .cgi
424 |
425 | # For type maps (negotiated resources):
426 | #AddHandler type-map var
427 |
428 | #
429 | # Filters allow you to process content before it is sent to the client.
430 | #
431 | # To parse .shtml files for server-side includes (SSI):
432 | # (You will also need to add "Includes" to the "Options" directive.)
433 | #
434 | #AddType text/html .shtml
435 | #AddOutputFilter INCLUDES .shtml
436 |
437 |
438 | #
439 | # The mod_mime_magic module allows the server to use various hints from the
440 | # contents of the file itself to determine its type. The MIMEMagicFile
441 | # directive tells the module where the hint definitions are located.
442 | #
443 | #MIMEMagicFile conf/magic
444 |
445 | #
446 | # Customizable error responses come in three flavors:
447 | # 1) plain text 2) local redirects 3) external redirects
448 | #
449 | # Some examples:
450 | #ErrorDocument 500 "The server made a boo boo."
451 | #ErrorDocument 404 /missing.html
452 | #ErrorDocument 404 "/cgi-bin/missing_handler.pl"
453 | #ErrorDocument 402 http://www.example.com/subscription_info.html
454 | #
455 |
456 | #
457 | # MaxRanges: Maximum number of Ranges in a request before
458 | # returning the entire resource, or one of the special
459 | # values 'default', 'none' or 'unlimited'.
460 | # Default setting is to accept 200 Ranges.
461 | #MaxRanges unlimited
462 |
463 | #
464 | # EnableMMAP and EnableSendfile: On systems that support it,
465 | # memory-mapping or the sendfile syscall may be used to deliver
466 | # files. This usually improves server performance, but must
467 | # be turned off when serving from networked-mounted
468 | # filesystems or if support for these functions is otherwise
469 | # broken on your system.
470 | # Defaults: EnableMMAP On, EnableSendfile Off
471 | #
472 | #EnableMMAP off
473 | #EnableSendfile on
474 |
475 | # Supplemental configuration
476 | #
477 | # The configuration files in the conf/extra/ directory can be
478 | # included to add extra features or to modify the default configuration of
479 | # the server, or you may simply copy their contents here and change as
480 | # necessary.
481 |
482 | # Server-pool management (MPM specific)
483 | #Include conf/extra/httpd-mpm.conf
484 |
485 | # Multi-language error messages
486 | #Include conf/extra/httpd-multilang-errordoc.conf
487 |
488 | # Fancy directory listings
489 | #Include conf/extra/httpd-autoindex.conf
490 |
491 | # Language settings
492 | #Include conf/extra/httpd-languages.conf
493 |
494 | # User home directories
495 | #Include conf/extra/httpd-userdir.conf
496 |
497 | # Real-time info on requests and configuration
498 | #Include conf/extra/httpd-info.conf
499 |
500 | # Virtual hosts
501 | #Include conf/extra/httpd-vhosts.conf
502 |
503 | # Local access to the Apache HTTP Server Manual
504 | #Include conf/extra/httpd-manual.conf
505 |
506 | # Distributed authoring and versioning (WebDAV)
507 | #Include conf/extra/httpd-dav.conf
508 |
509 | # Various default settings
510 | #Include conf/extra/httpd-default.conf
511 |
512 | # Configure mod_proxy_html to understand HTML4/XHTML1
513 |
514 | Include conf/extra/proxy-html.conf
515 |
516 |
517 | # Secure (SSL/TLS) connections
518 | #Include conf/extra/httpd-ssl.conf
519 | #
520 | # Note: The following must must be present to support
521 | # starting without SSL on platforms with no /dev/random equivalent
522 | # but a statically compiled-in mod_ssl.
523 | #
524 |
525 | SSLRandomSeed startup builtin
526 | SSLRandomSeed connect builtin
527 |
528 |
529 | # Enable FCGI example application at /
530 | ProxyPass "/" "fcgi://172.17.0.1:9500/" enablereuse=on
531 |
--------------------------------------------------------------------------------
/examples/apache_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Run this in the same directory!
4 | docker run --rm -p 80:80 -v $PWD/apache_docker.conf:/usr/local/apache2/conf/httpd.conf:ro httpd:alpine
5 |
--------------------------------------------------------------------------------
/examples/asyncio-server.py:
--------------------------------------------------------------------------------
1 | from asyncio import get_event_loop, Protocol
2 |
3 | from fcgiproto import FastCGIConnection, RequestBeginEvent, RequestDataEvent
4 |
5 |
6 | class FastCGIProtocol(Protocol):
7 | def __init__(self):
8 | self.transport = None
9 | self.conn = FastCGIConnection()
10 | self.requests = {}
11 |
12 | def connection_made(self, transport):
13 | self.transport = transport
14 |
15 | def data_received(self, data):
16 | try:
17 | for event in self.conn.feed_data(data):
18 | if isinstance(event, RequestBeginEvent):
19 | self.requests[event.request_id] = (
20 | event.params, event.keep_connection, bytearray())
21 | elif isinstance(event, RequestDataEvent):
22 | request_data = self.requests[event.request_id][2]
23 | if event.data:
24 | request_data.extend(event.data)
25 | else:
26 | params, keep_connection, request_data = self.requests.pop(event.request_id)
27 | self.handle_request(event.request_id, params, request_data)
28 | if not keep_connection:
29 | self.transport.close()
30 |
31 | self.transport.write(self.conn.data_to_send())
32 | except Exception:
33 | self.transport.abort()
34 | raise
35 |
36 | def handle_request(self, request_id, params, content):
37 | fcgi_params = '\n'.join('%s | %s |
' % (key, value)
38 | for key, value in params.items())
39 | content = content.decode('utf-8', errors='replace')
40 | response = ("""\
41 |
42 |
43 |
44 | FCGI parameters
45 |
48 | Request body
49 | %s
50 |
51 |
52 | """ % (fcgi_params, content)).encode('utf-8')
53 | headers = [
54 | (b'Content-Length', str(len(response)).encode('ascii')),
55 | (b'Content-Type', b'text/html; charset=UTF-8')
56 | ]
57 | self.conn.send_headers(request_id, headers, 200)
58 | self.conn.send_data(request_id, response, end_request=True)
59 |
60 |
61 | loop = get_event_loop()
62 | coro = loop.create_server(FastCGIProtocol, port=9500, reuse_address=True)
63 | loop.run_until_complete(coro)
64 |
65 | try:
66 | loop.run_forever()
67 | except (KeyboardInterrupt, SystemExit):
68 | pass
69 |
--------------------------------------------------------------------------------
/examples/curio-server.py:
--------------------------------------------------------------------------------
1 | from curio import run, spawn
2 | from curio.socket import *
3 |
4 | from fcgiproto import FastCGIConnection, RequestBeginEvent, RequestDataEvent
5 |
6 |
7 | async def fcgi_server(address):
8 | sock = socket(AF_INET, SOCK_STREAM)
9 | sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
10 | sock.bind(address)
11 | sock.listen(5)
12 | async with sock:
13 | while True:
14 | client, addr = await sock.accept()
15 | await spawn(fcgi_client(client, addr))
16 |
17 |
18 | def handle_request(conn, request_id, params, content):
19 | fcgi_params = '\n'.join('%s | %s |
' % (key, value)
20 | for key, value in params.items())
21 | content = content.decode('utf-8', errors='replace')
22 | response = ("""\
23 |
24 |
25 |
26 | FCGI parameters
27 |
30 | Request body
31 | %s
32 |
33 |
34 | """ % (fcgi_params, content)).encode('utf-8')
35 | headers = [
36 | (b'Content-Length', str(len(response)).encode('ascii')),
37 | (b'Content-Type', b'text/html; charset=UTF-8')
38 | ]
39 | conn.send_headers(request_id, headers, 200)
40 | conn.send_data(request_id, response, end_request=True)
41 |
42 |
43 | async def fcgi_client(client, addr):
44 | conn = FastCGIConnection()
45 | requests = {}
46 | async with client:
47 | while True:
48 | data = await client.recv(100000)
49 | if not data:
50 | break
51 |
52 | for event in conn.feed_data(data):
53 | if isinstance(event, RequestBeginEvent):
54 | requests[event.request_id] = (
55 | event.params, event.keep_connection, bytearray())
56 | elif isinstance(event, RequestDataEvent):
57 | request_data = requests[event.request_id][2]
58 | if event.data:
59 | request_data.extend(event.data)
60 | else:
61 | params, keep_connection, request_data = requests.pop(event.request_id)
62 | handle_request(conn, event.request_id, params, request_data)
63 | if not keep_connection:
64 | break
65 |
66 | data = conn.data_to_send()
67 | if data:
68 | await client.sendall(data)
69 |
70 | if __name__ == '__main__':
71 | try:
72 | run(fcgi_server(('', 9500)))
73 | except (KeyboardInterrupt, SystemExit):
74 | pass
75 |
--------------------------------------------------------------------------------
/examples/nginx_docker.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | location / {
4 | include fastcgi_params;
5 | fastcgi_pass 172.17.0.1:9500;
6 | fastcgi_keep_conn on;
7 | }
8 | }
--------------------------------------------------------------------------------
/examples/nginx_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Run this in the same directory!
4 | docker run --rm -p 80:80 -v $PWD/nginx_docker.conf:/etc/nginx/conf.d/default.conf:ro nginx
5 |
--------------------------------------------------------------------------------
/examples/twisted-server.py:
--------------------------------------------------------------------------------
1 | from twisted.internet import reactor
2 | from twisted.internet.endpoints import TCP4ServerEndpoint
3 | from twisted.internet.protocol import Protocol, Factory
4 |
5 | from fcgiproto import FastCGIConnection, RequestBeginEvent, RequestDataEvent
6 |
7 |
8 | class FastCGIProtocol(Protocol):
9 | def __init__(self):
10 | self.conn = FastCGIConnection()
11 | self.requests = {}
12 |
13 | def dataReceived(self, data):
14 | try:
15 | for event in self.conn.feed_data(data):
16 | if isinstance(event, RequestBeginEvent):
17 | self.requests[event.request_id] = (
18 | event.params, event.keep_connection, bytearray())
19 | elif isinstance(event, RequestDataEvent):
20 | request_data = self.requests[event.request_id][2]
21 | if event.data:
22 | request_data.extend(event.data)
23 | else:
24 | params, keep_connection, request_data = self.requests.pop(event.request_id)
25 | self.handle_request(event.request_id, params, request_data)
26 | if not keep_connection:
27 | self.transport.loseConnection()
28 |
29 | self.transport.write(self.conn.data_to_send())
30 | except Exception:
31 | self.transport.abortConnection()
32 | raise
33 |
34 | def handle_request(self, request_id, params, content):
35 | fcgi_params = '\n'.join('%s | %s |
' % (key, value)
36 | for key, value in params.items())
37 | content = content.decode('utf-8', errors='replace')
38 | response = ("""\
39 |
40 |
41 |
42 | FCGI parameters
43 |
46 | Request body
47 | %s
48 |
49 |
50 | """ % (fcgi_params, content)).encode('utf-8')
51 | headers = [
52 | (b'Content-Length', str(len(response)).encode('ascii')),
53 | (b'Content-Type', b'text/html; charset=UTF-8')
54 | ]
55 | self.conn.send_headers(request_id, headers, 200)
56 | self.conn.send_data(request_id, response, end_request=True)
57 |
58 |
59 | class FastCGIFactory(Factory):
60 | def buildProtocol(self, addr):
61 | return FastCGIProtocol()
62 |
63 |
64 | endpoint = TCP4ServerEndpoint(reactor, 9500)
65 | endpoint.listen(FastCGIFactory())
66 |
67 | try:
68 | reactor.run()
69 | except (KeyboardInterrupt, SystemExit):
70 | pass
71 |
--------------------------------------------------------------------------------
/fcgiproto/__init__.py:
--------------------------------------------------------------------------------
1 | from .connection import FastCGIConnection # noqa
2 | from .constants import FCGI_RESPONDER, FCGI_AUTHORIZER, FCGI_FILTER # noqa
3 | from .events import ( # noqa
4 | RequestEvent, RequestBeginEvent, RequestAbortEvent, RequestDataEvent,
5 | RequestSecondaryDataEvent)
6 | from .exceptions import ProtocolError # noqa
7 |
--------------------------------------------------------------------------------
/fcgiproto/connection.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from fcgiproto.constants import (
4 | FCGI_REQUEST_COMPLETE, FCGI_GET_VALUES, FCGI_RESPONDER, FCGI_BEGIN_REQUEST, FCGI_UNKNOWN_ROLE)
5 | from fcgiproto.records import (
6 | FCGIStdout, FCGIEndRequest, FCGIGetValuesResult, FCGIUnknownType, decode_record)
7 | from fcgiproto.states import RequestState
8 |
9 |
10 | class FastCGIConnection(object):
11 | """
12 | FastCGIConnection(roles=(FCGI_RESPONDER,), fcgi_values=None)
13 |
14 | FastCGI connection state machine.
15 |
16 | :param roles: iterable of allowed application roles (``FCGI_RESPONDER``, ``FCGI_AUTHORIZER``,
17 | ``FCGI_FILTER``)
18 | :param dict fcgi_values: dictionary of FastCGI management values (see the
19 | `FastCGI specification`_ for a list); keys and values must be unicode strings
20 |
21 | .. _FastCGI specification: https://htmlpreview.github.io/?https://github.com/FastCGI-Archives/\
22 | FastCGI.com/blob/master/docs/FastCGI%20Specification.html
23 |
24 | """
25 |
26 | __slots__ = ('roles', 'fcgi_values', '_input_buffer', '_output_buffer', '_request_states')
27 |
28 | def __init__(self, roles=(FCGI_RESPONDER,), fcgi_values=None):
29 | self.roles = frozenset(roles)
30 | self.fcgi_values = fcgi_values or {}
31 | self.fcgi_values.setdefault(u'FCGI_MPXS_CONNS', u'1')
32 | self._input_buffer = bytearray()
33 | self._output_buffer = bytearray()
34 | self._request_states = defaultdict(RequestState)
35 |
36 | def feed_data(self, data):
37 | """
38 | Feed data to the internal buffer of the connection.
39 |
40 | If there is enough data to generate one or more events, they will be added to the list
41 | returned from this call.
42 |
43 | Sometimes this call generates outgoing data so it is important to call
44 | :meth:`.data_to_send` afterwards and write those bytes to the output.
45 |
46 | :param bytes data: incoming data
47 | :raise fcgiproto.ProtocolError: if the protocol is violated
48 | :return: the list of generated FastCGI events
49 | :rtype: list
50 |
51 | """
52 | self._input_buffer.extend(data)
53 | events = []
54 | while True:
55 | record = decode_record(self._input_buffer)
56 | if record is None:
57 | return events
58 |
59 | if record.request_id:
60 | request_state = self._request_states[record.request_id]
61 | event = request_state.receive_record(record)
62 | if record.record_type == FCGI_BEGIN_REQUEST and record.role not in self.roles:
63 | # Reject requests where the role isn't among our set of allowed roles
64 | self._send_record(FCGIEndRequest(record.request_id, 0, FCGI_UNKNOWN_ROLE))
65 | elif event is not None:
66 | events.append(event)
67 | else:
68 | if record.record_type == FCGI_GET_VALUES:
69 | pairs = [(key, self.fcgi_values[key]) for key in record.keys
70 | if key in self.fcgi_values]
71 | self._send_record(FCGIGetValuesResult(pairs))
72 | else:
73 | self._send_record(FCGIUnknownType(record.record_type))
74 |
75 | def data_to_send(self):
76 | """
77 | Return any data that is due to be sent to the other end.
78 |
79 | :rtype: bytes
80 |
81 | """
82 | data = bytes(self._output_buffer)
83 | del self._output_buffer[:]
84 | return data
85 |
86 | def send_headers(self, request_id, headers, status=None):
87 | """
88 | Send response headers for the given request.
89 |
90 | Header keys will be converted from unicode strings to bytestrings if necessary.
91 | Values will be converted from any type to bytestrings if necessary.
92 |
93 | :param int request_id: identifier of the request
94 | :param headers: an iterable of (key, value) tuples of bytestrings
95 | :param int status: the response status code, if not 200
96 | :raise fcgiproto.ProtocolError: if the protocol is violated
97 |
98 | """
99 | payload = bytearray()
100 |
101 | if status:
102 | payload.extend((u'Status: %d\r\n' % status).encode('ascii'))
103 |
104 | for key, value in headers:
105 | if not isinstance(key, bytes):
106 | raise TypeError('header keys must be bytestrings, not %s' % key.__class__.__name__)
107 | if not isinstance(value, bytes):
108 | raise TypeError('header values must be bytestrings, not %s' %
109 | value.__class__.__name__)
110 |
111 | payload.extend(key + b': ' + value + b'\r\n')
112 |
113 | payload.extend(b'\r\n')
114 | record = FCGIStdout(request_id, payload)
115 | self._send_record(record)
116 |
117 | def send_data(self, request_id, data, end_request=False):
118 | """
119 | Send response body data for the given request.
120 |
121 | This method may be called several times before :meth:`.end_request`.
122 |
123 | :param int request_id: identifier of the request
124 | :param bytes data: request body data
125 | :param bool end_request: ``True`` to finish the request
126 | :raise fcgiproto.ProtocolError: if the protocol is violated
127 |
128 | """
129 | self._send_record(FCGIStdout(request_id, data))
130 | if end_request:
131 | self._send_record(FCGIStdout(request_id, b''))
132 | self._send_record(FCGIEndRequest(request_id, 0, FCGI_REQUEST_COMPLETE))
133 |
134 | def end_request(self, request_id):
135 | """
136 | Mark the given request finished.
137 |
138 | :param int request_id: identifier of the request
139 | :raise fcgiproto.ProtocolError: if the protocol is violated
140 |
141 | """
142 | self._send_record(FCGIEndRequest(request_id, 0, FCGI_REQUEST_COMPLETE))
143 |
144 | def _send_record(self, record):
145 | if record.request_id:
146 | request_state = self._request_states[record.request_id]
147 | request_state.send_record(record)
148 | if request_state.state == RequestState.FINISHED:
149 | del self._request_states[record.request_id]
150 |
151 | self._output_buffer.extend(record.encode())
152 |
--------------------------------------------------------------------------------
/fcgiproto/connection.pyi:
--------------------------------------------------------------------------------
1 | from typing import Dict
2 | from typing import List, Iterable, Tuple, Any
3 | from typing import Set
4 |
5 | from fcgiproto.constants import FCGI_RESPONDER
6 | from fcgiproto.events import RequestEvent
7 | from fcgiproto.records import FCGIRecord
8 | from fcgiproto.states import RequestState
9 |
10 |
11 | class FastCGIConnection:
12 | def __init__(self, roles: Iterable[int] = (FCGI_RESPONDER,),
13 | fcgi_values: Dict[str, str] = None) -> None:
14 | self.roles = None # type: Set[int]
15 | self.fcgi_values = None # type: Dict[str, str]
16 | self._input_buffer = None # type: bytearray
17 | self._output_buffer = None # type: bytearray
18 | self._request_states = None # type: Dict[int, RequestState]
19 |
20 | def feed_data(self, data: bytes) -> List[RequestEvent]:
21 | ...
22 |
23 | def data_to_send(self) -> bytes:
24 | ...
25 |
26 | def send_headers(self, request_id: int, headers: Iterable[Tuple[bytes, bytes]],
27 | status: int = None) -> None:
28 | ...
29 |
30 | def send_data(self, request_id: int, data: bytes, end_request: bool = False) -> None:
31 | ...
32 |
33 | def end_request(self, request_id: int) -> None:
34 | ...
35 |
36 | def _send_record(self, record: FCGIRecord) -> None:
37 | ...
38 |
--------------------------------------------------------------------------------
/fcgiproto/constants.py:
--------------------------------------------------------------------------------
1 | # Values for type component of FCGIHeader
2 | FCGI_BEGIN_REQUEST = 1
3 | FCGI_ABORT_REQUEST = 2
4 | FCGI_END_REQUEST = 3
5 | FCGI_PARAMS = 4
6 | FCGI_STDIN = 5
7 | FCGI_STDOUT = 6
8 | FCGI_STDERR = 7
9 | FCGI_DATA = 8
10 | FCGI_GET_VALUES = 9
11 | FCGI_GET_VALUES_RESULT = 10
12 | FCGI_UNKNOWN_TYPE = 11
13 |
14 | # Mask for flags component of FCGIBeginRequestBody
15 | FCGI_KEEP_CONN = 1
16 |
17 | # Values for role component of FCGIBeginRequestBody
18 | FCGI_RESPONDER = 1
19 | FCGI_AUTHORIZER = 2
20 | FCGI_FILTER = 3
21 |
22 | # Values for protocol_status component of FCGIEndRequestBody
23 | FCGI_REQUEST_COMPLETE = 0
24 | FCGI_CANT_MPX_CONN = 1
25 | FCGI_OVERLOADED = 2
26 | FCGI_UNKNOWN_ROLE = 3
27 |
--------------------------------------------------------------------------------
/fcgiproto/events.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 |
3 | from fcgiproto.constants import FCGI_KEEP_CONN
4 |
5 |
6 | class RequestEvent(object):
7 | """
8 | Base class for events that target a specific request.
9 |
10 | :ivar int request_id: identifier of the associated request
11 | """
12 |
13 | __slots__ = ('request_id',)
14 |
15 | def __init__(self, request_id):
16 | self.request_id = request_id
17 |
18 |
19 | class RequestBeginEvent(RequestEvent):
20 | """
21 | Signals the application about a new incoming request.
22 |
23 | :ivar int request_id: identifier of the request
24 | :ivar int role: expected role of the application for the request
25 | one of (``FCGI_RESPONDER``, ``FCGI_AUTHORIZER``, ``FCGI_FILTER``)
26 | :ivar dict params: FCGI parameters for the request
27 | """
28 |
29 | __slots__ = ('role', 'keep_connection', 'params')
30 |
31 | def __init__(self, request_id, role, flags, params):
32 | super(RequestBeginEvent, self).__init__(request_id)
33 | self.role = role
34 | self.keep_connection = flags & FCGI_KEEP_CONN
35 | self.params = OrderedDict(params)
36 |
37 |
38 | class RequestDataEvent(RequestEvent):
39 | """
40 | Contains body data for the specified request.
41 |
42 | An empty ``data`` argument signifies the end of the data stream.
43 |
44 | :ivar int request_id: identifier of the request
45 | :ivar bytes data: bytestring containing raw request data
46 | """
47 |
48 | __slots__ = ('data',)
49 |
50 | def __init__(self, request_id, data):
51 | super(RequestDataEvent, self).__init__(request_id)
52 | self.data = data
53 |
54 |
55 | class RequestSecondaryDataEvent(RequestEvent):
56 | """
57 | Contains secondary data for the specified request.
58 |
59 | An empty ``data`` argument signifies the end of the data stream.
60 |
61 | These events are only received for the ``FCGI_FILTER`` role.
62 |
63 | :ivar int request_id: identifier of the request
64 | :ivar bytes data: bytestring containing raw secondary data
65 | """
66 |
67 | __slots__ = ('data',)
68 |
69 | def __init__(self, request_id, data):
70 | super(RequestSecondaryDataEvent, self).__init__(request_id)
71 | self.data = data
72 |
73 |
74 | class RequestAbortEvent(RequestEvent):
75 | """Signals the application that the server wants the specified request aborted."""
76 |
77 | __slots__ = ()
78 |
--------------------------------------------------------------------------------
/fcgiproto/exceptions.py:
--------------------------------------------------------------------------------
1 | class ProtocolError(Exception):
2 | """Raised by the state machine when the FastCGI protocol is being violated."""
3 |
4 | def __init__(self, message):
5 | super(ProtocolError, self).__init__('FastCGI protocol violation: %s' % message)
6 |
--------------------------------------------------------------------------------
/fcgiproto/records.py:
--------------------------------------------------------------------------------
1 | from struct import Struct
2 |
3 | from fcgiproto.constants import (
4 | FCGI_BEGIN_REQUEST, FCGI_PARAMS, FCGI_END_REQUEST, FCGI_UNKNOWN_TYPE, FCGI_ABORT_REQUEST,
5 | FCGI_STDOUT, FCGI_STDIN, FCGI_STDERR, FCGI_DATA, FCGI_GET_VALUES, FCGI_GET_VALUES_RESULT)
6 | from fcgiproto.exceptions import ProtocolError
7 |
8 | headers_struct = Struct('>BBHHBx')
9 | length4_struct = Struct('>I')
10 |
11 |
12 | class FCGIRecord(object):
13 | __slots__ = ('request_id',)
14 |
15 | struct = Struct('') # type: Struct
16 | record_type = None # type: int
17 |
18 | def __init__(self, request_id):
19 | self.request_id = request_id
20 |
21 | @classmethod
22 | def parse(cls, request_id, content):
23 | fields = cls.struct.unpack(content)
24 | return cls(request_id, *fields)
25 |
26 | def encode_header(self, content):
27 | return headers_struct.pack(1, self.record_type, self.request_id, len(content), 0)
28 |
29 | def encode(self): # pragma: no cover
30 | raise NotImplementedError
31 |
32 |
33 | class FCGIBytestreamRecord(FCGIRecord):
34 | __slots__ = ('content',)
35 |
36 | def __init__(self, request_id, content):
37 | super(FCGIBytestreamRecord, self).__init__(request_id)
38 | self.content = content
39 |
40 | @classmethod
41 | def parse(cls, request_id, content):
42 | return cls(request_id, bytes(content))
43 |
44 | def encode(self):
45 | return self.encode_header(self.content) + self.content
46 |
47 |
48 | class FCGIUnknownManagementRecord(FCGIRecord):
49 | def __init__(self, record_type):
50 | super(FCGIUnknownManagementRecord, self).__init__(0)
51 | self.record_type = record_type
52 |
53 |
54 | class FCGIGetValues(FCGIRecord):
55 | __slots__ = ('keys',)
56 |
57 | record_type = FCGI_GET_VALUES
58 |
59 | def __init__(self, keys):
60 | super(FCGIGetValues, self).__init__(0)
61 | self.keys = keys
62 |
63 | @classmethod
64 | def parse(cls, request_id, content):
65 | assert request_id == 0
66 | keys = [key for key, value in decode_name_value_pairs(content)]
67 | return cls(keys)
68 |
69 | def encode(self):
70 | pairs = [(key, '') for key in self.keys]
71 | content = encode_name_value_pairs(pairs)
72 | return self.encode_header(content) + content
73 |
74 |
75 | class FCGIGetValuesResult(FCGIRecord):
76 | __slots__ = ('values',)
77 |
78 | record_type = FCGI_GET_VALUES_RESULT
79 |
80 | def __init__(self, values):
81 | super(FCGIGetValuesResult, self).__init__(0)
82 | self.values = values
83 |
84 | @classmethod
85 | def parse(cls, request_id, content):
86 | assert request_id == 0
87 | values = decode_name_value_pairs(content)
88 | return cls(values)
89 |
90 | def encode(self):
91 | content = encode_name_value_pairs(self.values)
92 | return self.encode_header(content) + content
93 |
94 |
95 | class FCGIUnknownType(FCGIRecord):
96 | __slots__ = ('type',)
97 |
98 | struct = Struct('>B7x')
99 | record_type = FCGI_UNKNOWN_TYPE
100 |
101 | def __init__(self, type):
102 | assert type > FCGI_UNKNOWN_TYPE
103 | super(FCGIUnknownType, self).__init__(0)
104 | self.type = type
105 |
106 | def encode(self):
107 | content = self.struct.pack(self.type)
108 | return self.encode_header(content) + content
109 |
110 |
111 | class FCGIBeginRequest(FCGIRecord):
112 | __slots__ = ('role', 'flags')
113 |
114 | struct = Struct('>HB5x')
115 | record_type = FCGI_BEGIN_REQUEST
116 |
117 | def __init__(self, request_id, role, flags):
118 | super(FCGIBeginRequest, self).__init__(request_id)
119 | self.role = role
120 | self.flags = flags
121 |
122 | def encode(self):
123 | content = self.struct.pack(self.role, self.flags)
124 | return self.encode_header(content) + content
125 |
126 |
127 | class FCGIAbortRequest(FCGIRecord):
128 | __slots__ = ()
129 |
130 | record_type = FCGI_ABORT_REQUEST
131 |
132 | @classmethod
133 | def parse(cls, request_id, content):
134 | return cls(request_id)
135 |
136 | def encode(self):
137 | return self.encode_header(b'')
138 |
139 |
140 | class FCGIParams(FCGIBytestreamRecord):
141 | __slots__ = ()
142 |
143 | record_type = FCGI_PARAMS
144 |
145 |
146 | class FCGIStdin(FCGIBytestreamRecord):
147 | __slots__ = ()
148 |
149 | record_type = FCGI_STDIN
150 |
151 |
152 | class FCGIStdout(FCGIBytestreamRecord):
153 | __slots__ = ()
154 |
155 | record_type = FCGI_STDOUT
156 |
157 |
158 | class FCGIStderr(FCGIBytestreamRecord):
159 | __slots__ = ()
160 |
161 | record_type = FCGI_STDERR
162 |
163 |
164 | class FCGIData(FCGIBytestreamRecord):
165 | __slots__ = ()
166 |
167 | record_type = FCGI_DATA
168 |
169 |
170 | class FCGIEndRequest(FCGIRecord):
171 | __slots__ = ('app_status', 'protocol_status')
172 |
173 | struct = Struct('>IB3x')
174 | record_type = FCGI_END_REQUEST
175 |
176 | def __init__(self, request_id, app_status, protocol_status):
177 | super(FCGIEndRequest, self).__init__(request_id)
178 | self.app_status = app_status
179 | self.protocol_status = protocol_status
180 |
181 | def encode(self):
182 | content = self.struct.pack(self.app_status, self.protocol_status)
183 | return self.encode_header(content) + content
184 |
185 |
186 | record_classes = {cls.record_type: cls for cls in globals().values() # type: ignore
187 | if isinstance(cls, type) and issubclass(cls, FCGIRecord)
188 | and cls.record_type} # type: ignore
189 |
190 |
191 | def decode_name_value_pairs(buffer):
192 | """
193 | Decode a name-value pair list from a buffer.
194 |
195 | :param bytearray buffer: a buffer containing a FastCGI name-value pair list
196 | :raise ProtocolError: if the buffer contains incomplete data
197 | :return: a list of (name, value) tuples where both elements are unicode strings
198 | :rtype: list
199 |
200 | """
201 | index = 0
202 | pairs = []
203 | while index < len(buffer):
204 | if buffer[index] & 0x80 == 0:
205 | name_length = buffer[index]
206 | index += 1
207 | elif len(buffer) - index > 4:
208 | name_length = length4_struct.unpack_from(buffer, index)[0] & 0x7fffffff
209 | index += 4
210 | else:
211 | raise ProtocolError('not enough data to decode name length in name-value pair')
212 |
213 | if len(buffer) - index > 1 and buffer[index] & 0x80 == 0:
214 | value_length = buffer[index]
215 | index += 1
216 | elif len(buffer) - index > 4:
217 | value_length = length4_struct.unpack_from(buffer, index)[0] & 0x7fffffff
218 | index += 4
219 | else:
220 | raise ProtocolError('not enough data to decode value length in name-value pair')
221 |
222 | if len(buffer) - index >= name_length + value_length:
223 | name = buffer[index:index + name_length].decode('ascii')
224 | value = buffer[index + name_length:index + name_length + value_length].decode('utf-8')
225 | pairs.append((name, value))
226 | index += name_length + value_length
227 | else:
228 | raise ProtocolError('name/value data missing from buffer')
229 |
230 | return pairs
231 |
232 |
233 | def encode_name_value_pairs(pairs):
234 | """
235 | Encode a list of name-pair values into a binary form that FCGI understands.
236 |
237 | Both names and values can be either unicode strings or bytestrings and will be converted to
238 | bytestrings as necessary.
239 |
240 | :param list pairs: list of name-value pairs
241 | :return: the encoded bytestring
242 |
243 | """
244 | content = bytearray()
245 | for name, value in pairs:
246 | name = name if isinstance(name, bytes) else name.encode('ascii')
247 | value = value if isinstance(value, bytes) else value.encode('ascii')
248 | for item in (name, value):
249 | if len(item) < 128:
250 | content.append(len(item))
251 | else:
252 | length = len(item)
253 | content.extend(length4_struct.pack(length | 0x80000000))
254 |
255 | content.extend(name)
256 | content.extend(value)
257 |
258 | return bytes(content)
259 |
260 |
261 | def decode_record(buffer):
262 | """
263 | Create a new FCGI message from the bytes in the given buffer.
264 |
265 | If successful, the record's data is removed from the byte array.
266 |
267 | :param bytearray buffer: the byte array containing the data
268 | :return: an instance of this class, or ``None`` if there was not enough data
269 |
270 | """
271 | if len(buffer) >= headers_struct.size:
272 | version, record_type, request_id, content_length, padding_length = \
273 | headers_struct.unpack_from(buffer)
274 | if version != 1:
275 | raise ProtocolError('unexpected protocol version: %d' % buffer[0])
276 | elif len(buffer) >= headers_struct.size + content_length + padding_length:
277 | content = buffer[headers_struct.size:headers_struct.size + content_length]
278 | del buffer[:headers_struct.size + content_length + padding_length]
279 | try:
280 | record_class = record_classes[record_type]
281 | except KeyError:
282 | if request_id:
283 | raise ProtocolError('unknown record type: %d' % record_type)
284 | else:
285 | return FCGIUnknownManagementRecord(record_type)
286 |
287 | return record_class.parse(request_id, content)
288 |
289 | return None
290 |
--------------------------------------------------------------------------------
/fcgiproto/states.py:
--------------------------------------------------------------------------------
1 | from fcgiproto.constants import (
2 | FCGI_BEGIN_REQUEST, FCGI_PARAMS, FCGI_STDIN, FCGI_STDOUT, FCGI_END_REQUEST, FCGI_DATA,
3 | FCGI_FILTER, FCGI_AUTHORIZER, FCGI_ABORT_REQUEST, FCGI_REQUEST_COMPLETE)
4 | from fcgiproto.events import (
5 | RequestDataEvent, RequestSecondaryDataEvent, RequestAbortEvent, RequestBeginEvent)
6 | from fcgiproto.exceptions import ProtocolError
7 | from fcgiproto.records import decode_name_value_pairs
8 |
9 |
10 | class RequestState(object):
11 | __slots__ = ('state', 'role', 'flags', 'params_buffer')
12 |
13 | EXPECT_BEGIN_REQUEST = 1
14 | EXPECT_PARAMS = 2
15 | EXPECT_STDIN = 3
16 | EXPECT_DATA = 4
17 | EXPECT_STDOUT = 5
18 | EXPECT_END_REQUEST = 6
19 | FINISHED = 7
20 | state_names = {value: varname for varname, value in locals().items() if isinstance(value, int)}
21 |
22 | def __init__(self):
23 | self.state = RequestState.EXPECT_BEGIN_REQUEST
24 | self.role = self.flags = None
25 | self.params_buffer = bytearray()
26 |
27 | def receive_record(self, record):
28 | if record.record_type == FCGI_BEGIN_REQUEST:
29 | if self.state == RequestState.EXPECT_BEGIN_REQUEST:
30 | self.role = record.role
31 | self.flags = record.flags
32 | self.state = RequestState.EXPECT_PARAMS
33 | return None
34 | elif record.record_type == FCGI_PARAMS:
35 | if self.state == RequestState.EXPECT_PARAMS:
36 | if record.content:
37 | self.params_buffer.extend(record.content)
38 | return None
39 | else:
40 | params = decode_name_value_pairs(self.params_buffer)
41 | if self.role == FCGI_AUTHORIZER:
42 | self.state = RequestState.EXPECT_STDOUT
43 | else:
44 | self.state = RequestState.EXPECT_STDIN
45 |
46 | return RequestBeginEvent(record.request_id, self.role, self.flags, params)
47 | elif record.record_type == FCGI_STDIN:
48 | if self.state == RequestState.EXPECT_STDIN:
49 | if not record.content:
50 | if self.role == FCGI_FILTER:
51 | self.state = RequestState.EXPECT_DATA
52 | else:
53 | self.state = RequestState.EXPECT_STDOUT
54 |
55 | return RequestDataEvent(record.request_id, record.content)
56 | elif record.record_type == FCGI_DATA:
57 | if self.state == RequestState.EXPECT_DATA:
58 | if not record.content:
59 | self.state = RequestState.EXPECT_STDOUT
60 |
61 | return RequestSecondaryDataEvent(record.request_id, record.content)
62 | elif record.record_type == FCGI_ABORT_REQUEST:
63 | if RequestState.EXPECT_BEGIN_REQUEST < self.state < RequestState.FINISHED:
64 | self.state = RequestState.EXPECT_END_REQUEST
65 | return RequestAbortEvent(record.request_id)
66 |
67 | raise ProtocolError('received unexpected %s record in the %s state' % (
68 | record.__class__.__name__, self.state_names[self.state]))
69 |
70 | def send_record(self, record):
71 | if record.record_type == FCGI_STDOUT:
72 | if self.state == RequestState.EXPECT_STDOUT:
73 | if not record.content:
74 | self.state = RequestState.EXPECT_END_REQUEST
75 |
76 | return
77 | elif record.record_type == FCGI_END_REQUEST:
78 | # Only allow a normal request finish when it's expected
79 | if self.state == RequestState.EXPECT_END_REQUEST:
80 | if record.protocol_status == FCGI_REQUEST_COMPLETE:
81 | self.state = RequestState.FINISHED
82 | return
83 | elif self.state == RequestState.EXPECT_PARAMS:
84 | # Allow rejecting the request right after receiving it but not later
85 | if record.protocol_status != FCGI_REQUEST_COMPLETE:
86 | self.state = RequestState.FINISHED
87 | return
88 |
89 | raise ProtocolError('cannot send %s record in the %s state' % (
90 | record.__class__.__name__, self.state_names[self.state]))
91 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [build_sphinx]
2 | source-dir = docs
3 | build-dir = docs/_build
4 |
5 | [upload_docs]
6 | upload-dir = docs/_build/html
7 |
8 | [tool:pytest]
9 | addopts = -rsx --cov --tb=short
10 | norecursedirs = .git .tox .cache build docs examples virtualenv
11 |
12 | [flake8]
13 | max-line-length = 99
14 | exclude = .tox,build,docs
15 |
16 | [bdist_wheel]
17 | universal = 1
18 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import os.path
3 |
4 | from setuptools import setup, find_packages
5 |
6 |
7 | here = os.path.dirname(__file__)
8 | readme_path = os.path.join(here, 'README.rst')
9 | readme = open(readme_path).read()
10 |
11 | setup(
12 | name='fcgiproto',
13 | use_scm_version={
14 | 'version_scheme': 'post-release',
15 | 'local_scheme': 'dirty-tag'
16 | },
17 | description='FastCGI state-machine based protocol implementation',
18 | long_description=readme,
19 | author=u'Alex Grönholm',
20 | author_email='alex.gronholm@nextday.fi',
21 | url='https://github.com/agronholm/fcgiproto',
22 | classifiers=[
23 | 'Development Status :: 5 - Production/Stable',
24 | 'Intended Audience :: Developers',
25 | 'License :: OSI Approved :: MIT License',
26 | 'Programming Language :: Python',
27 | 'Programming Language :: Python :: 2.7',
28 | 'Programming Language :: Python :: 3',
29 | 'Programming Language :: Python :: 3.3',
30 | 'Programming Language :: Python :: 3.4',
31 | 'Programming Language :: Python :: 3.5',
32 | 'Programming Language :: Python :: 3.6'
33 | ],
34 | keywords='fastcgi http',
35 | license='MIT',
36 | packages=find_packages(exclude=['tests']),
37 | include_package_data=True,
38 | setup_requires=['setuptools_scm']
39 | )
40 |
--------------------------------------------------------------------------------
/tests/test_connection.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fcgiproto.connection import FastCGIConnection
4 | from fcgiproto.constants import (
5 | FCGI_RESPONDER, FCGI_AUTHORIZER, FCGI_FILTER, FCGI_REQUEST_COMPLETE, FCGI_UNKNOWN_ROLE)
6 | from fcgiproto.events import (
7 | RequestBeginEvent, RequestAbortEvent, RequestDataEvent, RequestSecondaryDataEvent)
8 | from fcgiproto.records import (
9 | FCGIBeginRequest, FCGIStdin, FCGIParams, FCGIStdout, FCGIEndRequest, encode_name_value_pairs,
10 | FCGIAbortRequest, FCGIGetValues, FCGIGetValuesResult, FCGIUnknownType, FCGIData)
11 |
12 |
13 | @pytest.fixture
14 | def conn():
15 | return FastCGIConnection()
16 |
17 |
18 | @pytest.mark.parametrize('send_status', [True, False])
19 | def test_responder_request(conn, send_status):
20 | events = conn.feed_data(FCGIBeginRequest(1, FCGI_RESPONDER, 0).encode())
21 | assert len(events) == 0
22 |
23 | content = encode_name_value_pairs([('REQUEST_METHOD', 'GET'), ('CONTENT_LENGTH', '')])
24 | events = conn.feed_data(FCGIParams(1, content).encode())
25 | assert len(events) == 0
26 |
27 | events = conn.feed_data(FCGIParams(1, b'').encode())
28 | assert isinstance(events[0], RequestBeginEvent)
29 |
30 | events = conn.feed_data(FCGIStdin(1, b'content').encode())
31 | assert len(events) == 1
32 | assert isinstance(events[0], RequestDataEvent)
33 |
34 | events = conn.feed_data(FCGIStdin(1, b'').encode())
35 | assert len(events) == 1
36 | assert isinstance(events[0], RequestDataEvent)
37 |
38 | headers = [(b'Content-Length', b'7'), (b'Content-Type', b'text/plain')]
39 | conn.send_headers(1, headers, status=200 if send_status else None)
40 | expected_body = b'Content-Length: 7\r\nContent-Type: text/plain\r\n\r\n'
41 | if send_status:
42 | expected_body = b'Status: 200\r\n' + expected_body
43 | assert conn.data_to_send() == \
44 | FCGIStdout(1, expected_body).encode()
45 |
46 | conn.send_data(1, b'Cont')
47 | assert conn.data_to_send() == FCGIStdout(1, b'Cont').encode()
48 |
49 | conn.send_data(1, b'ent', end_request=True)
50 | assert conn.data_to_send() == FCGIStdout(1, b'ent').encode() + \
51 | FCGIStdout(1, b'').encode() + FCGIEndRequest(1, 0, FCGI_REQUEST_COMPLETE).encode()
52 |
53 |
54 | def test_authorizer_request():
55 | conn = FastCGIConnection(roles=[FCGI_AUTHORIZER])
56 | events = conn.feed_data(FCGIBeginRequest(1, FCGI_AUTHORIZER, 0).encode())
57 | assert len(events) == 0
58 |
59 | content = encode_name_value_pairs([('REQUEST_METHOD', 'GET'), ('CONTENT_LENGTH', '')])
60 | events = conn.feed_data(FCGIParams(1, content).encode())
61 | assert len(events) == 0
62 |
63 | events = conn.feed_data(FCGIParams(1, b'').encode())
64 | assert len(events) == 1
65 | assert isinstance(events[0], RequestBeginEvent)
66 |
67 | headers = [(b'Content-Length', b'13'), (b'Content-Type', b'text/plain')]
68 | conn.send_headers(1, headers, status=403)
69 | assert conn.data_to_send() == \
70 | FCGIStdout(1, b'Status: 403\r\nContent-Length: 13\r\n'
71 | b'Content-Type: text/plain\r\n\r\n').encode()
72 |
73 | conn.send_data(1, b'Access denied', end_request=True)
74 | assert conn.data_to_send() == \
75 | FCGIStdout(1, b'Access denied').encode() + FCGIStdout(1, b'').encode() + \
76 | FCGIEndRequest(1, 0, FCGI_REQUEST_COMPLETE).encode()
77 |
78 |
79 | def test_filter_request():
80 | conn = FastCGIConnection(roles=[FCGI_FILTER])
81 | events = conn.feed_data(FCGIBeginRequest(1, FCGI_FILTER, 0).encode())
82 | assert len(events) == 0
83 |
84 | content = encode_name_value_pairs([('REQUEST_METHOD', 'GET'), ('CONTENT_LENGTH', '')])
85 | events = conn.feed_data(FCGIParams(1, content).encode())
86 | assert len(events) == 0
87 |
88 | events = conn.feed_data(FCGIParams(1, b'').encode())
89 | assert isinstance(events[0], RequestBeginEvent)
90 |
91 | events = conn.feed_data(FCGIStdin(1, b'content').encode())
92 | assert len(events) == 1
93 | assert isinstance(events[0], RequestDataEvent)
94 |
95 | events = conn.feed_data(FCGIStdin(1, b'').encode())
96 | assert len(events) == 1
97 | assert isinstance(events[0], RequestDataEvent)
98 |
99 | events = conn.feed_data(FCGIData(1, b'file data').encode())
100 | assert len(events) == 1
101 | assert isinstance(events[0], RequestSecondaryDataEvent)
102 |
103 | events = conn.feed_data(FCGIData(1, b'').encode())
104 | assert len(events) == 1
105 | assert isinstance(events[0], RequestSecondaryDataEvent)
106 |
107 | headers = [(b'Content-Length', b'9'), (b'Content-Type', b'application/octet-stream')]
108 | conn.send_headers(1, headers, status=404)
109 | assert conn.data_to_send() == \
110 | FCGIStdout(1, b'Status: 404\r\nContent-Length: 9\r\n'
111 | b'Content-Type: application/octet-stream\r\n\r\n').\
112 | encode()
113 |
114 | conn.send_data(1, b'file data', end_request=True)
115 | assert conn.data_to_send() == FCGIStdout(1, b'file data').encode() + \
116 | FCGIStdout(1, b'').encode() + FCGIEndRequest(1, 0, FCGI_REQUEST_COMPLETE).encode()
117 |
118 |
119 | def test_aborted_request(conn):
120 | events = conn.feed_data(FCGIBeginRequest(1, FCGI_RESPONDER, 0).encode())
121 | assert len(events) == 0
122 |
123 | content = encode_name_value_pairs([('REQUEST_METHOD', 'GET'), ('CONTENT_LENGTH', '')])
124 | events = conn.feed_data(FCGIParams(1, content).encode())
125 | assert len(events) == 0
126 |
127 | events = conn.feed_data(FCGIParams(1, b'').encode())
128 | assert len(events) == 1
129 | assert isinstance(events[0], RequestBeginEvent)
130 |
131 | events = conn.feed_data(FCGIStdin(1, b'').encode())
132 | assert len(events) == 1
133 | assert isinstance(events[0], RequestDataEvent)
134 |
135 | events = conn.feed_data(FCGIAbortRequest(1).encode())
136 | assert len(events) == 1
137 | assert isinstance(events[0], RequestAbortEvent)
138 |
139 | conn.end_request(1)
140 | assert conn.data_to_send() == FCGIEndRequest(1, 0, FCGI_REQUEST_COMPLETE).encode()
141 |
142 |
143 | def test_unknown_role(conn):
144 | events = conn.feed_data(FCGIBeginRequest(1, FCGI_AUTHORIZER, 0).encode())
145 | assert len(events) == 0
146 | assert conn.data_to_send() == FCGIEndRequest(1, 0, FCGI_UNKNOWN_ROLE).encode()
147 |
148 |
149 | def test_unknown_record_type(conn):
150 | events = conn.feed_data(b'\x01\x0c\x00\x00\x00\x00\x00\x00')
151 | assert len(events) == 0
152 | assert conn.data_to_send() == FCGIUnknownType(12).encode()
153 |
154 |
155 | def test_get_values(conn):
156 | keys = ['FCGI_MPXS_CONNS', 'FCGI_OTHER_KEY']
157 | values = [('FCGI_MPXS_CONNS', '1')]
158 | events = conn.feed_data(FCGIGetValues(keys).encode())
159 | assert len(events) == 0
160 | assert conn.data_to_send() == FCGIGetValuesResult(values).encode()
161 |
162 |
163 | def test_send_headers_invalid_key(conn):
164 | headers = [(1, b'value')]
165 | exc = pytest.raises(TypeError, conn.send_headers, 1, headers)
166 | assert str(exc.value) == 'header keys must be bytestrings, not int'
167 |
168 |
169 | def test_send_headers_invalid_value(conn):
170 | headers = [(b'Invalid', 1)]
171 | exc = pytest.raises(TypeError, conn.send_headers, 1, headers)
172 | assert str(exc.value) == 'header values must be bytestrings, not int'
173 |
--------------------------------------------------------------------------------
/tests/test_records.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fcgiproto.records import (
4 | encode_name_value_pairs, decode_name_value_pairs, decode_record, FCGIStdin, FCGIBeginRequest,
5 | FCGIEndRequest, FCGIUnknownType, FCGIStdout, FCGIGetValues, FCGIGetValuesResult,
6 | FCGIAbortRequest)
7 | from fcgiproto.exceptions import ProtocolError
8 |
9 |
10 | def test_encode_simple_record():
11 | record = FCGIStdout(5, b'data')
12 | assert record.encode() == b'\x01\x06\x00\x05\x00\x04\x00\x00data'
13 |
14 |
15 | def test_parse_get_values():
16 | buffer = bytearray(b'\x03\x00FOO\x03\x00BAR')
17 | record = FCGIGetValues.parse(0, buffer)
18 | assert record.keys == ['FOO', 'BAR']
19 |
20 |
21 | def test_encode_get_values():
22 | keys = ['FOO', 'BAR']
23 | record = FCGIGetValues(keys)
24 | assert record.encode() == b'\x01\x09\x00\x00\x00\x0a\x00\x00\x03\x00FOO\x03\x00BAR'
25 |
26 |
27 | def test_parse_get_values_result():
28 | buffer = bytearray(b'\x03\x03FOOabc\x03\x03BARxyz')
29 | record = FCGIGetValuesResult.parse(0, buffer)
30 | assert record.values == [('FOO', 'abc'), ('BAR', 'xyz')]
31 |
32 |
33 | def test_encode_get_values_result():
34 | values = [('FOO', 'abc'), ('BAR', 'xyz')]
35 | record = FCGIGetValuesResult(values)
36 | assert record.encode() == b'\x01\x0a\x00\x00\x00\x10\x00\x00\x03\x03FOOabc\x03\x03BARxyz'
37 |
38 |
39 | def test_parse_begin_request():
40 | buffer = bytearray(b'\x00\x01\x01\x00\x00\x00\x00\x00')
41 | record = FCGIBeginRequest.parse(5, buffer)
42 | assert record.request_id == 5
43 | assert record.role == 1
44 | assert record.flags == 1
45 |
46 |
47 | def test_encode_begin_request():
48 | record = FCGIBeginRequest(5, 1, 1)
49 | assert record.encode() == b'\x01\x01\x00\x05\x00\x08\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00'
50 |
51 |
52 | def test_encode_abort_request():
53 | record = FCGIAbortRequest(5)
54 | assert record.encode() == b'\x01\x02\x00\x05\x00\x00\x00\x00'
55 |
56 |
57 | def test_parse_abort_request():
58 | buffer = bytearray(b'')
59 | record = FCGIAbortRequest.parse(5, buffer)
60 | assert record.request_id == 5
61 |
62 |
63 | def test_parse_end_request():
64 | buffer = bytearray(b'\x00\x01\x00\x01\x02\x00\x00\x00')
65 | record = FCGIEndRequest.parse(5, buffer)
66 | assert record.request_id == 5
67 | assert record.app_status == 65537
68 | assert record.protocol_status == 2
69 |
70 |
71 | def test_encode_end_request():
72 | record = FCGIEndRequest(5, 65537, 2)
73 | assert record.encode() == b'\x01\x03\x00\x05\x00\x08\x00\x00\x00\x01\x00\x01\x02\x00\x00\x00'
74 |
75 |
76 | def test_encode_unknown_type():
77 | record = FCGIUnknownType(12)
78 | assert record.encode() == b'\x01\x0b\x00\x00\x00\x08\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00'
79 |
80 |
81 | @pytest.mark.parametrize('data, expected', [
82 | (b'\x03\x06foobarbar\x01\x03Xxyz', [(u'foo', u'barbar'), ('X', 'xyz')]),
83 | (b'\x03\x80\x01\x00\x00foo' + b'x' * 65536, [(u'foo', u'x' * 65536)]),
84 | (b'\x80\x01\x00\x00\x03' + b'x' * 65536 + b'foo', [(u'x' * 65536, 'foo')]),
85 | (b'\x80\x01\x00\x00\x80\x01\x00\x00' + b'x' * 65536 + b'y' * 65536,
86 | [('x' * 65536, 'y' * 65536)])
87 | ], ids=['short_both', 'long_value', 'long_name', 'long_both'])
88 | def test_decode_name_value_pairs(data, expected):
89 | buffer = bytearray(data)
90 | assert decode_name_value_pairs(buffer) == expected
91 |
92 |
93 | @pytest.mark.parametrize('data, message', [
94 | (b'\x80\x00\x00', 'not enough data to decode name length in name-value pair'),
95 | (b'\x03', 'not enough data to decode value length in name-value pair'),
96 | (b'\x03\x06foo', 'name/value data missing from buffer')
97 | ], ids=['name_missing', 'value_missing', 'content_missing'])
98 | def test_decode_name_value_pairs_incomplete(data, message):
99 | buffer = bytearray(data)
100 | exc = pytest.raises(ProtocolError, decode_name_value_pairs, buffer)
101 | assert str(exc.value).endswith(message)
102 |
103 |
104 | @pytest.mark.parametrize('pairs, expected', [
105 | ([(u'foo', u'barbar'), (u'X', u'xyz')], b'\x03\x06foobarbar\x01\x03Xxyz'),
106 | ([(u'foo', u'x' * 65536)], b'\x03\x80\x01\x00\x00foo' + b'x' * 65536),
107 | ([(u'x' * 65536, u'foo')], b'\x80\x01\x00\x00\x03' + b'x' * 65536 + b'foo'),
108 | ([(u'x' * 65536, u'y' * 65536)],
109 | b'\x80\x01\x00\x00\x80\x01\x00\x00' + b'x' * 65536 + b'y' * 65536)
110 | ], ids=['short_both', 'long_value', 'long_name', 'long_both'])
111 | def test_encode_name_value_pairs(pairs, expected):
112 | assert encode_name_value_pairs(pairs) == expected
113 |
114 |
115 | def test_decode_record():
116 | buffer = bytearray(b'\x01\x05\x00\x01\x00\x07\x00\x00content')
117 | record = decode_record(buffer)
118 | assert isinstance(record, FCGIStdin)
119 | assert record.request_id == 1
120 | assert record.content == b'content'
121 |
122 |
123 | def test_decode_record_incomplete():
124 | buffer = bytearray(b'\x01\x05\x00\x01\x00\x07\x00\x00conten')
125 | assert decode_record(buffer) is None
126 |
127 |
128 | def test_decode_record_wrong_version():
129 | buffer = bytearray(b'\x02\x01\x00\x01\x00\x00\x00\x00')
130 | exc = pytest.raises(ProtocolError, decode_record, buffer)
131 | assert str(exc.value).endswith('unexpected protocol version: 2')
132 |
133 |
134 | def test_decode_unknown_record_type():
135 | buffer = bytearray(b'\x01\x0c\x01\x00\x00\x00\x00\x00')
136 | exc = pytest.raises(ProtocolError, decode_record, buffer)
137 | assert str(exc.value).endswith('unknown record type: 12')
138 |
--------------------------------------------------------------------------------
/tests/test_states.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fcgiproto.constants import (
4 | FCGI_REQUEST_COMPLETE, FCGI_RESPONDER, FCGI_AUTHORIZER, FCGI_FILTER, FCGI_UNKNOWN_ROLE)
5 | from fcgiproto.events import (
6 | RequestDataEvent, RequestAbortEvent, RequestBeginEvent, RequestSecondaryDataEvent)
7 | from fcgiproto.exceptions import ProtocolError
8 | from fcgiproto.records import (
9 | FCGIStdin, FCGIData, FCGIAbortRequest, FCGIStdout, FCGIEndRequest, FCGIBeginRequest,
10 | FCGIParams, encode_name_value_pairs)
11 | from fcgiproto.states import RequestState
12 |
13 | begin_record = FCGIBeginRequest(1, FCGI_RESPONDER, 0)
14 | params_record = FCGIParams(1, encode_name_value_pairs([('NAME', 'VALUE')]))
15 | params_end_record = FCGIParams(1, b'')
16 | stdin_record = FCGIStdin(1, b'')
17 | data_record = FCGIData(1, b'')
18 | abort_record = FCGIAbortRequest(1)
19 | stdout_record = FCGIStdout(1, b'content')
20 | stdout_end_record = FCGIStdout(1, b'')
21 | end_record = FCGIEndRequest(1, 0, FCGI_REQUEST_COMPLETE)
22 | end_record_reject = FCGIEndRequest(1, 0, FCGI_UNKNOWN_ROLE)
23 |
24 |
25 | class BaseStateTests(object):
26 | possible_states = {RequestState.EXPECT_BEGIN_REQUEST,
27 | RequestState.EXPECT_PARAMS,
28 | RequestState.EXPECT_STDIN,
29 | RequestState.EXPECT_DATA,
30 | RequestState.EXPECT_STDOUT,
31 | RequestState.EXPECT_END_REQUEST,
32 | RequestState.FINISHED}
33 | role = None
34 |
35 | @pytest.mark.parametrize('allowed_states, record, expected_event_class', [
36 | ([RequestState.EXPECT_BEGIN_REQUEST], begin_record, type(None)),
37 | ([RequestState.EXPECT_PARAMS], params_record, type(None)),
38 | ([RequestState.EXPECT_PARAMS], params_end_record, RequestBeginEvent),
39 | ([RequestState.EXPECT_STDIN], stdin_record, RequestDataEvent),
40 | ([RequestState.EXPECT_DATA], data_record, RequestSecondaryDataEvent),
41 | ([RequestState.EXPECT_PARAMS,
42 | RequestState.EXPECT_STDIN,
43 | RequestState.EXPECT_DATA,
44 | RequestState.EXPECT_STDOUT,
45 | RequestState.EXPECT_END_REQUEST], abort_record, RequestAbortEvent),
46 | ([], stdout_record, type(None))
47 | ], ids=['begin', 'params', 'params_end', 'stdin', 'data', 'abort', 'stdout'])
48 | def test_receive_record(self, allowed_states, record, expected_event_class):
49 | for state_num in sorted(self.possible_states):
50 | state = RequestState()
51 | state.role = self.role
52 | state.state = state_num
53 | state.flags = 0
54 | if state_num in allowed_states:
55 | event = state.receive_record(record)
56 | assert isinstance(event, expected_event_class)
57 | else:
58 | pytest.raises(ProtocolError, state.receive_record, record)
59 |
60 | @pytest.mark.parametrize('allowed_states, record, expected_end_state', [
61 | ([RequestState.EXPECT_STDOUT], stdout_record, RequestState.EXPECT_STDOUT),
62 | ([RequestState.EXPECT_STDOUT], stdout_end_record, RequestState.EXPECT_END_REQUEST),
63 | ([RequestState.EXPECT_END_REQUEST], end_record, RequestState.FINISHED),
64 | ([RequestState.EXPECT_PARAMS], end_record_reject, RequestState.FINISHED),
65 | ([], stdin_record, RequestState.EXPECT_BEGIN_REQUEST),
66 | ], ids=['stdout', 'stdout_end', 'endrequest', 'rejectrequest', 'stdin'])
67 | def test_send_record(self, allowed_states, record, expected_end_state):
68 | for state_num in sorted(self.possible_states):
69 | state = RequestState()
70 | state.role = self.role
71 | state.state = state_num
72 | state.flags = 0
73 | if state_num in allowed_states:
74 | state.send_record(record)
75 | assert state.state == expected_end_state
76 | else:
77 | pytest.raises(ProtocolError, state.send_record, record)
78 |
79 |
80 | class TestResponder(BaseStateTests):
81 | role = FCGI_RESPONDER
82 | possible_states = BaseStateTests.possible_states - {RequestState.EXPECT_DATA}
83 |
84 |
85 | class TestAuthorizer(BaseStateTests):
86 | role = FCGI_AUTHORIZER
87 | possible_states = (BaseStateTests.possible_states -
88 | {RequestState.EXPECT_STDIN, RequestState.EXPECT_DATA})
89 |
90 |
91 | class TestFilter(BaseStateTests):
92 | role = FCGI_FILTER
93 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27, py33, py34, py35, py36, flake8, mypy
3 | skip_missing_interpreters = true
4 |
5 | [tox:travis]
6 | 2.7 = py27
7 | 3.3 = py33
8 | 3.4 = py34
9 | 3.5 = py35
10 | 3.6 = py36, flake8, mypy
11 | pypy = pypy
12 |
13 | [testenv]
14 | commands = python -m pytest {posargs}
15 | deps = pytest
16 | pytest-cov
17 |
18 | [testenv:flake8]
19 | basepython = python3.6
20 | deps = flake8
21 | commands = flake8 fcgiproto tests
22 | skip_install = true
23 |
24 | [testenv:mypy]
25 | basepython = python3.6
26 | deps = mypy-lang
27 | commands = mypy -p fcgiproto
28 | skip_install = true
29 |
--------------------------------------------------------------------------------