diff options
Diffstat (limited to 'sphinx/source/advanced.rst')
-rw-r--r-- | sphinx/source/advanced.rst | 664 |
1 files changed, 664 insertions, 0 deletions
diff --git a/sphinx/source/advanced.rst b/sphinx/source/advanced.rst new file mode 100644 index 00000000..44f54edb --- /dev/null +++ b/sphinx/source/advanced.rst @@ -0,0 +1,664 @@ +.. _advanced: + +Advanced +-------- + +CherryPy has support for more advanced features that these sections +will describe. + +.. contents:: + :depth: 4 + +.. _aliases: + +Set aliases to page handlers +############################ + +A fairly unknown, yet useful, feature provided by the :func:`cherrypy.expose` +decorator is to support aliases. + +Let's use the template provided by :ref:`tutorial 03 <tut03>`: + +.. code-block:: python + + import random + import string + + import cherrypy + + class StringGenerator(object): + @cherrypy.expose(['generer', 'generar']) + def generate(self, length=8): + return ''.join(random.sample(string.hexdigits, int(length))) + + if __name__ == '__main__': + cherrypy.quickstart(StringGenerator()) + +In this example, we create localized aliases for +the page handler. This means the page handler will be +accessible via: + +- /generate +- /generer (French) +- /generar (Spanish) + +Obviously, your aliases may be whatever suits your needs. + +.. note:: + + The alias may be a single string or a list of them. + +RESTful-style dispatching +######################### + +The term `RESTful URL` is sometimes used to talk about friendly URLs +that nicely map to the entities an application exposes. + +.. important:: + + We will not enter the debate around what is restful or not but we will + showcase two mechanisms to implement the usual idea in your + CherryPy application. + +Let's assume you wish to create an application that exposes +music bands and their records. Your application will probably have +the following URLs: + +- http://hostname/<bandname>/ +- http://hostname/<bandname>/albums/<recordname>/ + +It's quite clear you would not create a page handler named after +every possible band in the world. This means you will need a page handler +that acts as a proxy for all of them. + +The default dispatcher cannot deal with that scenario on its own +because it expects page handlers to be explicitely declared in your +source code. Luckily, CherryPy provides ways to support those use cases. + +.. seealso:: + + This section extends from this `stackoverflow response <http://stackoverflow.com/a/15789415/1363905>`_. + +The special _cp_dispatch method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``_cp_dispatch`` is a special method you declare in any of your :term:`controller` +to massage the remaining segments before CherryPy gets to process them. +This offers you the capacity to remove, add or otherwise handle any segment +you wish and, even, entirely change the remaining parts. + +.. code-block:: python + + import cherrypy + + class Band(object): + def __init__(self): + self.albums = Album() + + def _cp_dispatch(self, vpath): + if len(vpath) == 1: + cherrypy.request.params['name'] = vpath.pop() + return self + + if len(vpath) == 3: + cherrypy.request.params['artist'] = vpath.pop(0) # /band name/ + vpath.pop(0) # /albums/ + cherrypy.request.params['title'] = vpath.pop(0) # /album title/ + return self.albums + + return vpath + + @cherrypy.expose + def index(self, name): + return 'About %s...' % name + + class Album(object): + @cherrypy.expose + def index(self, artist, title): + return 'About %s by %s...' % (title, artist) + + if __name__ == '__main__': + cherrypy.quickstart(Band()) + +Notice how the controller defines `_cp_dispatch`, it takes +a single argument, the URL path info broken into its segments. + +The method can inspect and manipulate the list of segments, +removing any or adding new segments at any position. The new list of +segments is then sent to the dispatcher which will use it +to locate the appropriate resource. + +In the above example, you should be able to go to the following URLs: + +- http://localhost:8080/nirvana/ +- http://localhost:8080/nirvana/albums/nevermind/ + +The ``/nirvana/`` segment is associated to the band and +the ``/nevermind/`` segment relates to the album. + +To achieve this, our `_cp_dispatch` method works on the idea +that the default dispatcher matches URLs against page handler +signatures and their position in the tree of handlers. + +In this case, we take the dynamic segments in the URL (band and record names), +we inject them into the request parameters and we remove them +from the segment lists as if they had never been there in the first place. + +In other words, `_cp_dispatch` makes it as if we were +working on the following URLs: + +- http://localhost:8080/?artist=nirvana +- http://localhost:8080/albums/?artist=nirvana&title=nevermind + + +The popargs decorator +^^^^^^^^^^^^^^^^^^^^^ +:func:`cherrypy.popargs` is more straightforward as it gives a name to any segment +that CherryPy wouldn't be able to interpret otherwise. This makes the +matching of segments with page handler signatures easier and helps CherryPy +understand the structure of your URL. + +.. code-block:: python + + import cherrypy + + @cherrypy.popargs('name') + class Band(object): + def __init__(self): + self.albums = Album() + + @cherrypy.expose + def index(self, name): + return 'About %s...' % name + + @cherrypy.popargs('title') + class Album(object): + @cherrypy.expose + def index(self, name, title): + return 'About %s by %s...' % (title, name) + + if __name__ == '__main__': + cherrypy.quickstart(Band()) + +This works similarly to `_cp_dispatch` but, as said above, is more +explicit and localized. It says: + +- take the first segment and store it into a parameter name `band` +- take again the first segment (since we removed the previous first) + and store it into a parameter named `title` + +Note that the decorator accepts more than a single binding. For instance: + +.. code-block:: python + + @cherrypy.popargs('title') + class Album(object): + def __init__(self): + self.tracks = Track() + + @cherrypy.popargs('num', 'track') + class Track(object): + @cherrypy.expose + def index(self, name, title, num, track): + ... + +This would handle the following URL: + +- http://localhost:8080/nirvana/albums/nevermind/track/06/polly + +Notice finally how the whole stack of segments is passed to each +page handler so that you have the full context. + +Streaming the response body +########################### + +CherryPy handles HTTP requests, packing and unpacking the low-level details, +then passing control to your application's :term:`page handler`, which produce +the body of the response. CherryPy allows you to return body content in a +variety of types: a string, a list of strings, a file. CherryPy also allows you +to *yield* content, rather than *return* content. When you use "yield", you also +have the option of streaming the output. + +**In general, it is safer and easier to not stream output.** Therefore, +streaming output is off by default. Streaming output and also using sessions +requires a good understanding of :py:mod:`how session locks work +<cherrypy.lib.sessions>`. + +The "normal" CherryPy response process +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When you provide content from your page handler, CherryPy manages the +conversation between the HTTP server and your code like this: + +.. image:: _static/images/cpreturn.gif + +Notice that the HTTP server gathers all output first and then writes everything +to the client at once: status, headers, and body. This works well for static or +simple pages, since the entire response can be changed at any time, either in +your application code, or by the CherryPy framework. + +How "streaming output" works with CherryPy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When you set the config entry "response.stream" to True (and use "yield"), +CherryPy manages the conversation between the HTTP server and your code like this: + +.. image:: _static/images/cpyield.gif + +When you stream, your application doesn't immediately pass raw body content +back to CherryPy or to the HTTP server. Instead, it passes back a generator. +At that point, CherryPy finalizes the status and headers, **before** the +generator has been consumed, or has produced any output. This is necessary to +allow the HTTP server to send the headers and pieces of the body as they become +available. + +Once CherryPy has set the status and headers, it sends them to the HTTP server, +which then writes them out to the client. From that point on, the CherryPy +framework mostly steps out of the way, and the HTTP server essentially requests +content directly from your application code (your page handler method). + +Therefore, when streaming, if an error occurs within your page handler, +CherryPy will not catch it--the HTTP server will catch it. Because the headers +(and potentially some of the body) have already been written to the client, +the server *cannot* know a safe means of handling the error, and will therefore +simply close the connection (the current, builtin servers actually write out a +short error message in the body, but this may be changed, and is not guaranteed +behavior for all HTTP servers you might use with CherryPy). + +In addition, you cannot manually modify the status or headers within your page +handler if that handler method is a streaming generator, because the method will +not be iterated over until after the headers have been written to the client. +**This includes raising exceptions like HTTPError, NotFound, InternalRedirect +and HTTPRedirect.** To use a streaming generator while modifying headers, you +would have to return a generator that is separate from (or embedded in) your +page handler. For example: + +.. code-block:: python + + class Root: + @cherrypy.expose + def thing(self): + cherrypy.response.headers['Content-Type'] = 'text/plain' + if not authorized(): + raise cherrypy.NotFound() + def content(): + yield "Hello, " + yield "world" + return content() + thing._cp_config = {'response.stream': True} + +Streaming generators are sexy, but they play havoc with HTTP. CherryPy allows +you to stream output for specific situations: pages which take many minutes to +produce, or pages which need a portion of their content immediately output to +the client. Because of the issues outlined above, **it is usually better to +flatten (buffer) content rather than stream content**. Do otherwise only when +the benefits of streaming outweigh the risks. + +Response timeouts +################# + +CherryPy responses include 3 attributes related to time: + + * ``response.time``: the :func:`time.time` at which the response began + * ``response.timeout``: the number of seconds to allow responses to run + * ``response.timed_out``: a boolean indicating whether the response has + timed out (default False). + +The request processing logic inspects the value of ``response.timed_out`` at +various stages; if it is ever True, then :class:`TimeoutError` is raised. +You are free to do the same within your own code. + +Rather than calculate the difference by hand, you can call +``response.check_timeout`` to set ``timed_out`` for you. + +.. note:: + + The default response timeout is 300 seconds. + +.. _timeoutmonitor: + +Timeout Monitor +^^^^^^^^^^^^^^^ + +In addition, CherryPy includes a ``cherrypy.engine.timeout_monitor`` which +monitors all active requests in a separate thread; periodically, it calls +``check_timeout`` on them all. It is subscribed by default. To turn it off: + +.. code-block:: ini + + [global] + engine.timeout_monitor.on: False + +or: + +.. code-block:: python + + cherrypy.engine.timeout_monitor.unsubscribe() + +You can also change the interval (in seconds) at which the timeout monitor runs: + +.. code-block:: ini + + [global] + engine.timeout_monitor.frequency: 60 * 60 + +The default is once per minute. The above example changes that to once per hour. + +Deal with signals +################# + +This :ref:`engine plugin <busplugins>` is instantiated automatically as +`cherrypy.engine.signal_handler`. +However, it is only *subscribed* automatically by :func:`cherrypy.quickstart`. +So if you want signal handling and you're calling: + +.. code-block:: python + + tree.mount() + engine.start() + engine.block() + +on your own, be sure to add before you start the engine: + +.. code-block:: python + + engine.signals.subscribe() + +.. index:: Windows, Ctrl-C, shutdown +.. _windows-console: + +Windows Console Events +^^^^^^^^^^^^^^^^^^^^^^ + +Microsoft Windows uses console events to communicate some signals, like Ctrl-C. +When deploying CherryPy on Windows platforms, you should obtain the +`Python for Windows Extensions <http://sourceforge.net/projects/pywin32/>`_; +once you have them installed, CherryPy will handle Ctrl-C and other +console events (CTRL_C_EVENT, CTRL_LOGOFF_EVENT, CTRL_BREAK_EVENT, +CTRL_SHUTDOWN_EVENT, and CTRL_CLOSE_EVENT) automatically, shutting down the +bus in preparation for process exit. + + +Securing your server +#################### + +.. note:: + + This section is not meant as a complete guide to securing + a web application or ecosystem. Please review the various + guides provided at `OWASP <https://www.owasp.org/index.php/Main_Page>`_. + + +There are several settings that can be enabled to make CherryPy pages more secure. These include: + + Transmitting data: + + #. Use Secure Cookies + + Rendering pages: + + #. Set HttpOnly cookies + #. Set XFrame options + #. Enable XSS Protection + #. Set the Content Security Policy + +An easy way to accomplish this is to set headers with a tool +and wrap your entire CherryPy application with it: + +.. code-block:: python + + import cherrypy + + def secureheaders(): + headers = cherrypy.response.headers + headers['X-Frame-Options'] = 'DENY' + headers['X-XSS-Protection'] = '1; mode=block' + headers['Content-Security-Policy'] = "default-src='self'" + + # set the priority according to your needs if you are hooking something + # else on the 'before_finalize' hook point. + cherrypy.tools.secureheaders = cherrypy.Tool('before_finalize', secureheaders, priority=60) + +.. note:: + + Read more about `those headers <https://www.owasp.org/index.php/List_of_useful_HTTP_headers>`_. + +Then, in the :ref:`configuration file <config>` (or any other place that you want to enable the tool): + +.. code-block:: ini + + [/] + tools.secureheaders.on = True + + +If you use :ref:`sessions <basicsession>` you can also enable these settings: + +.. code-block:: ini + + [/] + tools.sessions.on = True + # increase security on sessions + tools.sessions.secure = True + tools.sessions.httponly = True + + +If you use SSL you can also enable Strict Transport Security: + +.. code-block:: python + + # add this to secureheaders(): + # only add Strict-Transport headers if we're actually using SSL; see the ietf spec + # "An HSTS Host MUST NOT include the STS header field in HTTP responses + # conveyed over non-secure transport" + # http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-7.2 + if (cherrypy.server.ssl_certificate != None and cherrypy.server.ssl_private_key != None): + headers['Strict-Transport-Security'] = 'max-age=31536000' # one year + +Next, you should probably use :ref:`SSL <ssl>`. + +Multiple HTTP servers support +############################# + +CherryPy starts its own HTTP server whenever you start the +engine. In some cases, you may wish to host your application +on more than a single port. This is easily achieved: + +.. code-block:: python + + from cherrypy._cpserver import Server + server = Server() + server.socket_port = 8090 + server.subscribe() + +You can create as many :class:`server <cherrypy._cpserver.Server>` +server instances as you need, once :ref:`subscribed <busplugins>`, +they will follow the CherryPy engine's life-cycle. + +WSGI support +############ + +CherryPy supports the WSGI interface defined in :pep:`333` +as well as its updates in :pep:`3333`. It means the following: + +- You can host a foreign WSGI application with the CherryPy server +- A CherryPy application can be hosted by another WSGI server + +Make your CherryPy application a WSGI application +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A WSGI application can be obtained from your application as follows: + +.. code-block:: python + + import cherrypy + wsgiapp = cherrypy.Application(StringGenerator(), '/', config=myconf) + +Simply use the `wsgiapp` instance in any WSGI-aware server. + +.. _hostwsgiapp: + +Host a foreign WSGI application in CherryPy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Assuming you have a WSGI-aware application, you can host it +in your CherryPy server using the :meth:`cherrypy.tree.graft <cherrypy._cptree.Tree.graft>` +facility. + +.. code-block:: python + + def raw_wsgi_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!'] + + cherrypy.tree.graft(raw_wsgi_app, '/') + +.. important:: + + You cannot use tools with a foreign WSGI application. + However, you can still benefit from the + :ref:`CherryPy bus <buspattern>`. + + +No need for the WSGI interface? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default CherryPy HTTP server supports the WSGI interfaces +defined in :pep:`333` and :pep:`3333`. However, if your application +is a pure CherryPy application, you can switch to a HTTP +server that by-passes the WSGI layer altogether. It will provide +a slight performance increase. + +.. code-block:: python + + import cherrypy + + class Root(object): + @cherrypy.expose + def index(self): + return "Hello World!" + + if __name__ == '__main__': + from cherrypy._cpnative_server import CPHTTPServer + cherrypy.server.httpserver = CPHTTPServer(cherrypy.server) + + cherrypy.quickstart(Root(), '/') + +.. important:: + + Using the native server, you will not be able to + graft a WSGI application as shown in the previous section. + Doing so will result in a server error at runtime. + +WebSocket support +################# + +`WebSocket <http://tools.ietf.org/html/rfc6455>`_ +is a recent application protocol that came to life +from the HTML5 working-group in response to the needs for +bi-directional communication. Various hacks had been proposed +such as Comet, polling, etc. + +WebSocket is a socket that starts its life from a HTTP upgrade request. +Once the upgrade is performed, the underlying socket is +kept opened but not used in a HTTP context any longer. +Instead, both connected endpoints may use the socket +to push data to the other end. + +CherryPy itself does not support WebSocket, but the feature +is provided by an external library called +`ws4py <https://github.com/Lawouach/WebSocket-for-Python>`_. + +Database support +################ + +CherryPy does not bundle any database access but its architecture +makes it easy to integrate common database interfaces such as +the DB-API specified in :pep:`249`. Alternatively, you can also +use an `ORM <en.wikipedia.org/wiki/Object-relational_mapping>`_ +such as `SQLAlchemy <http://sqlalchemy.readthedocs.org>`_ +or `SQLObject <https://pypi.python.org/pypi/SQLObject/>`_. + +You will find `here <https://bitbucket.org/Lawouach/cherrypy-recipes/src/tip/web/database/sql_alchemy/>`_ +a recipe on how integrating SQLAlchemy using a mix of +:ref:`plugins <busplugins>` and :ref:`tools <tools>`. + +HTML Templating support +####################### + +CherryPy does not provide any HTML template but its architecture +makes it easy to integrate one. Popular ones are `Mako <www.makotemplates.org>`_ +or `Jinja2 <jinja.pocoo.org/docs/>`_. + +You will find `here <https://bitbucket.org/Lawouach/cherrypy-recipes/src/tip/web/templating/>`_ +a recipe on how to integrate them using a mix +:ref:`plugins <busplugins>` and :ref:`tools <tools>`. + +Testing your application +######################## + +Web applications, like any other kind of code, must be tested. CherryPy provides +a :class:`helper class <cherrypy.test.helper.CPWebCase>` to ease writing +functional tests. + +Here is a simple example for a basic echo application: + +.. code-block:: python + + import cherrypy + from cherrypy.test import helper + + class SimpleCPTest(helper.CPWebCase): + def setup_server(): + class Root(object): + @cherrypy.expose + def echo(self, message): + return message + + cherrypy.tree.mount(Root()) + setup_server = staticmethod(setup_server) + + def test_message_should_be_returned_as_is(self): + self.getPage("/echo?message=Hello%20world") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('Hello world') + + def test_non_utf8_message_will_fail(self): + """ + CherryPy defaults to decode the query-string + using UTF-8, trying to send a query-string with + a different encoding will raise a 404 since + it considers it's a different URL. + """ + self.getPage("/echo?message=A+bient%F4t", + headers=[ + ('Accept-Charset', 'ISO-8859-1,utf-8'), + ('Content-Type', 'text/html;charset=ISO-8859-1') + ] + ) + self.assertStatus('404 Not Found') + +As you can see the, test inherits from that helper class. You should +setup your application and mount it as per-usual. Then, define your various +tests and call the helper :meth:`~cherrypy.test.helper.CPWebCase.getPage` +method to perform a request. Simply use the various specialized +assert* methods to validate your workflow and data. + +You can then run the test using `py.test <http://pytest.org/latest/>`_ as follows: + +.. code-block:: bash + + $ py.test -s test_echo_app.py + +The ``-s`` is necessary because the CherryPy class also wraps stdin and stdout. + +.. note:: + + Although they are written using the typical pattern the + :mod:`unittest` module supports, they are not bare unit tests. + Indeed, a whole CherryPy stack is started for you and runs your application. + If you want to really unit test your CherryPy application, meaning without + having to start a server, you may want to have a look at + this `recipe <https://bitbucket.org/Lawouach/cherrypy-recipes/src/tip/testing/unit/serverless/>`_. |