Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Sep 4, 2016
0 parents commit bfd4adf
Show file tree
Hide file tree
Showing 29 changed files with 1,674 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
@@ -0,0 +1,6 @@
[run]
source = fcgiproto
branch = 1

[report]
show_missing = true
13 changes: 13 additions & 0 deletions .gitignore
@@ -0,0 +1,13 @@
.project
.pydevproject
.idea/
.coverage
.cache/
.tox/
.eggs/
*.egg-info/
*.pyc
dist/
docs/_build/
build/
virtualenv/
15 changes: 15 additions & 0 deletions .travis.yml
@@ -0,0 +1,15 @@
sudo: false

language: python

python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"

install: pip install tox-travis coveralls

script: tox

after_success: coveralls
19 changes: 19 additions & 0 deletions LICENSE
@@ -0,0 +1,19 @@
This is the MIT license: http://www.opensource.org/licenses/mit-license.php

Copyright (c) Alex Grönholm

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
26 changes: 26 additions & 0 deletions README.rst
@@ -0,0 +1,26 @@
.. image:: https://travis-ci.org/agronholm/fcgiproto.svg?branch=master
:target: https://travis-ci.org/agronholm/fcgiproto
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/fcgiproto/badge.svg?branch=master
:target: https://coveralls.io/github/agronholm/fcgiproto?branch=master
:alt: Code Coverage

The FastCGI_ protocol is a protocol commonly used to relay HTTP requests and responses between a
front-end web server (nginx, Apache, etc.) and a back-end web application.

This library implements this protocol for the web application end as a pure state-machine which
only takes in bytes and returns a list of parsed events. This leaves users free to use any I/O
approach they see fit (asyncio_, curio_, Twisted_, etc.). Sample code is provided for implementing
a FastCGI server using a variety of I/O frameworks.

.. _FastCGI: https://htmlpreview.github.io/?https://github.com/FastCGI-Archives/FastCGI.com/blob/master/docs/FastCGI%20Specification.html
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _curio: https://github.com/dabeaz/curio
.. _Twisted: https://twistedmatrix.com/

Project links
-------------

* `Documentation <http://fcgiproto.readthedocs.org/en/latest/>`_
* `Source code <https://github.com/agronholm/fcgiproto>`_
* `Issue tracker <https://github.com/agronholm/fcgiproto/issues>`_
32 changes: 32 additions & 0 deletions docs/api.rst
@@ -0,0 +1,32 @@
API Reference
=============

Classes
-------

.. autoclass:: fcgiproto.FastCGIConnection
:members:

.. autoclass:: fcgiproto.RequestEvent
:members:

.. autoclass:: fcgiproto.RequestBeginEvent
:members:

.. autoclass:: fcgiproto.RequestAbortEvent
:members:

.. autoclass:: fcgiproto.RequestDataEvent
:members:

.. autoclass:: fcgiproto.RequestSecondaryDataEvent
:members:

.. autoexception:: fcgiproto.ProtocolError

Constants
---------

* ``fcgiproto.FCGI_RESPONDER``
* ``fcgiproto.FCGI_AUTHORIZER``
* ``fcgiproto.FCGI_FILTER``
28 changes: 28 additions & 0 deletions docs/conf.py
@@ -0,0 +1,28 @@
#!/usr/bin/env python
# coding: utf-8
import pkg_resources

extensions = [
'sphinx.ext.autodoc',
]

templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = 'fcgiproto'
author = u'Alex Grönholm'
copyright = '2016, ' + author

v = pkg_resources.get_distribution('fcgiproto').parsed_version
version = v.base_version
release = v.public

language = None

exclude_patterns = ['_build']
pygments_style = 'sphinx'
todo_include_todos = False

html_theme = 'classic'
html_static_path = ['_static']
htmlhelp_basename = 'fcgiprotodoc'
17 changes: 17 additions & 0 deletions docs/index.rst
@@ -0,0 +1,17 @@
FastCGI state-machine protocol (fcgiproto)
==========================================

.. include:: ../README.rst
:start-line: 7
:end-before: Project links


Table of Contents
=================

.. toctree::
:maxdepth: 2

userguide
api
versionhistory
126 changes: 126 additions & 0 deletions docs/userguide.rst
@@ -0,0 +1,126 @@
Protocol implementor's guide
============================

Creating a real-world implementation of FastCGI using fcgiproto is quite straightforward.
As with other sans-io protocols, you feed incoming data to fcgiproto and it vends events in return.
To invoke actions on the connection, just call its methods, like
:meth:`~fcgiproto.FastCGIConnection.send_headers` and so on.
To get pending outgoing data, use the :meth:`~fcgiproto.FastCGIConnection.data_to_send` method.

Connection configuration
------------------------

The most common role is the responder role (``FCGI_RESPONDER``). The authorizer
(``FCGI_AUTHORIZER``) and filter (``FCGI_FILTER``) roles are not commonly supported by web server
software. As such, you will want to leave the default role setting alone, unless you really know
what you're doing.

It's also possible to set FCGI management values. The FastCGI specification defines names of three
values:

* ``FCGI_MAX_CONNS``: The maximum number of concurrent transport connections this application will
accept, e.g. ``1`` or ``10``
* ``FCGI_MAX_REQS``: The maximum number of concurrent requests this application will accept, e.g.
``1`` or ``50``.
* ``FCGI_MPXS_CONNS``: ``0`` if this application does not multiplex connections (i.e. handle
concurrent requests over each connection), ``1`` otherwise.

The connection sets ``FCGI_MPXS_CONNS`` to ``1`` by default. It should be noted that the web server
may never even query for these values, so leave this setting alone unless you know you need it.
At least nginx does not attempt to multiplex FCGI connections, nor does it query for any management
values.

Implementor's responsibilities
------------------------------

The logic in :class:`~fcgiproto.FastCGIConnection` will handle most complications of the protocol.
That leaves just a handful of things for I/O implementors to keep in mind:

* Always get any outgoing data from the connection (using
:meth:`~fcgiproto.FastCGIConnection.data_to_send`) after calling either
:meth:`~fcgiproto.FastCGIConnection.feed_data` or any of the other methods, and send it to the
remote host
* Remember to set ``Content-Length`` if your response contains a body
* Respect the ``keep_connection`` flag in :class:`~fcgiproto.RequestBeginEvent`.
Close the connection after calling :meth:`~fcgiproto.FastCGIConnection.end_request` if the flag
is ``False``.

Handling requests
-----------------

**RESPONDER**

The sequence for handling responder requests (the most common case) is as follows:

#. a :class:`~fcgiproto.RequestBeginEvent` is received
#. one or more :class:`~fcgiproto.RequestDataEvent` are received, the last one having an empty
bytestring as ``data`` attribute
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
and the last call must have ``end_request`` set to ``True``

The implementor can decide whether to wait until all of the request body has been received, or
start running the request handler code right after :class:`~fcgiproto.RequestBeginEvent` has been
received (to facilitate streaming uploads for example).

In FastCGI responses, the HTTP status code is sent using the ``Status`` header. As a convenience,
the :meth:`~fcgiproto.FastCGIConnection.send_headers` method provides the ``status`` parameter
to add this header.

**AUTHORIZER**

Authorizer requests differ from responder requests in the way that the application never receives
any request body. They also don't receive the ``CONTENT_LENGTH``, ``PATH_INFO``, ``SCRIPT_NAME`` or
``PATH_TRANSLATED`` parameters, which severely limits the usefulness of this role.

The request-response sequence for authorizers goes as follows:

#. a :class:`~fcgiproto.RequestBeginEvent` is received
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
and the last call must have ``end_request`` set to ``True``

A response code other than ``200`` will be interpreted as a negative response.

**FILTER**

Filter applications receive all the same information as responders, but they are also sent a
secondary data stream which they're supposed to filter.

The request-response sequence for filters goes as follows:

#. a :class:`~fcgiproto.RequestBeginEvent` is received
#. one or more :class:`~fcgiproto.RequestDataEvent` are received, the last one having an empty
bytestring as ``data`` attribute
#. one or more :class:`~fcgiproto.RequestSecondaryDataEvent` are received, the last one having an
empty bytestring as ``data`` attribute
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_headers` once
#. the application calls :meth:`~fcgiproto.FastCGIConnection.send_data` one or more times
and the last call must have ``end_request`` set to ``True``

The application is expected to send the (modified) secondary data stream as the response body.
It must read in all of the request body before starting to send a response (thus somewhat deviating
from the sequence above), but it does not need to wait for the secondary data stream to end (for
example if the response comes from a cache).

Handling request aborts
-----------------------

If the application receives a :class:`~fcgiproto.RequestAbortEvent`, it should cease processing of
the request at once. No headers or data should be sent from this point on for this request, and
:meth:`~fcgiproto.FastCGIConnection.end_request` should be called as soon as possible.

Running the examples
--------------------

The ``examples`` directory in the project source tree contains example code for several popular
I/O frameworks to get you started. Just run any of the server scripts and it will start a FastCGI
server listening on port 9500.

Since FastCGI requires a front-end server, a Docker script and nginx site configuration file have
been provided as a convenience. Just run ``nginx_docker.sh`` from the ``examples`` directory and
navigate to http://127.0.0.1/ to see the result. The example code displays a web page that shows
the FastCGI parameters and the request body (if any).

.. note:: You may have to make adjustments to the configuration if your Docker interface address or
desired host HTTP port don't match the provided configuration.
8 changes: 8 additions & 0 deletions docs/versionhistory.rst
@@ -0,0 +1,8 @@
Version history
===============

This library adheres to `Semantic Versioning <http://semver.org/>`_.

**1.0.0**

- Initial release
68 changes: 68 additions & 0 deletions examples/asyncio-server.py
@@ -0,0 +1,68 @@
from asyncio import get_event_loop, Protocol

from fcgiproto import FastCGIConnection, RequestBeginEvent, RequestDataEvent


class FastCGIProtocol(Protocol):
def __init__(self):
self.transport = None
self.conn = FastCGIConnection()
self.requests = {}

def connection_made(self, transport):
self.transport = transport

def data_received(self, data):
try:
for event in self.conn.feed_data(data):
if isinstance(event, RequestBeginEvent):
self.requests[event.request_id] = (
event.params, event.keep_connection, bytearray())
elif isinstance(event, RequestDataEvent):
request_data = self.requests[event.request_id][2]
if event.data:
request_data.extend(event.data)
else:
params, keep_connection, request_data = self.requests.pop(event.request_id)
self.handle_request(event.request_id, params, request_data)
if not keep_connection:
self.transport.close()

self.transport.write(self.conn.data_to_send())
except Exception:
self.transport.abort()
raise

def handle_request(self, request_id, params, content):
fcgi_params = '\n'.join('<tr><td>%s</td><td>%s</td></tr>' % (key, value)
for key, value in params.items())
content = content.decode('utf-8', errors='replace')
response = ("""\
<!DOCTYPE html>
<html>
<body>
<h2>FCGI parameters</h2>
<table>
%s
</table>
<h2>Request body</h2>
<pre>%s</pre>
</body>
</html>
""" % (fcgi_params, content)).encode('utf-8')
headers = [
(b'Content-Length', str(len(response)).encode('ascii')),
(b'Content-Type', b'text/html')
]
self.conn.send_headers(request_id, headers, 200)
self.conn.send_data(request_id, response, end_request=True)


loop = get_event_loop()
coro = loop.create_server(FastCGIProtocol, port=9500, reuse_address=True)
loop.run_until_complete(coro)

try:
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
pass

0 comments on commit bfd4adf

Please sign in to comment.