From 21f36855d4599b2a913c7d27e285a5ecf70c1af1 Mon Sep 17 00:00:00 2001 From: Michael Dlesk Date: Fri, 30 Dec 2011 20:03:25 -0500 Subject: [PATCH] Added cherrypy module --- Gamez.db | Bin 819200 -> 819200 bytes cherrypy/LICENSE.txt | 25 + cherrypy/__init__.py | 624 +++++ cherrypy/__init__.pyc | Bin 0 -> 20741 bytes cherrypy/_cpchecker.py | 327 +++ cherrypy/_cpchecker.pyc | Bin 0 -> 12016 bytes cherrypy/_cpcompat.py | 318 +++ cherrypy/_cpcompat.pyc | Bin 0 -> 11107 bytes cherrypy/_cpconfig.py | 295 +++ cherrypy/_cpconfig.pyc | Bin 0 -> 9510 bytes cherrypy/_cpdispatch.py | 636 +++++ cherrypy/_cpdispatch.pyc | Bin 0 -> 19243 bytes cherrypy/_cperror.py | 556 ++++ cherrypy/_cperror.pyc | Bin 0 -> 18499 bytes cherrypy/_cplogging.py | 440 ++++ cherrypy/_cplogging.pyc | Bin 0 -> 16178 bytes cherrypy/_cpmodpy.py | 344 +++ cherrypy/_cpnative_server.py | 149 ++ cherrypy/_cpreqbody.py | 965 +++++++ cherrypy/_cpreqbody.pyc | Bin 0 -> 27032 bytes cherrypy/_cprequest.py | 956 +++++++ cherrypy/_cprequest.pyc | Bin 0 -> 19745 bytes cherrypy/_cpserver.py | 205 ++ cherrypy/_cpserver.pyc | Bin 0 -> 4995 bytes cherrypy/_cpthreadinglocal.py | 239 ++ cherrypy/_cptools.py | 510 ++++ cherrypy/_cptools.pyc | Bin 0 -> 20911 bytes cherrypy/_cptree.py | 290 ++ cherrypy/_cptree.pyc | Bin 0 -> 9761 bytes cherrypy/_cpwsgi.py | 408 +++ cherrypy/_cpwsgi.pyc | Bin 0 -> 12815 bytes cherrypy/_cpwsgi_server.py | 63 + cherrypy/_cpwsgi_server.pyc | Bin 0 -> 2560 bytes cherrypy/cherryd | 109 + cherrypy/favicon.ico | Bin 0 -> 1406 bytes cherrypy/lib/__init__.py | 45 + cherrypy/lib/__init__.pyc | Bin 0 -> 2130 bytes cherrypy/lib/auth.py | 87 + cherrypy/lib/auth.pyc | Bin 0 -> 2753 bytes cherrypy/lib/auth_basic.py | 87 + cherrypy/lib/auth_basic.pyc | Bin 0 -> 3531 bytes cherrypy/lib/auth_digest.py | 365 +++ cherrypy/lib/auth_digest.pyc | Bin 0 -> 13019 bytes cherrypy/lib/caching.py | 465 ++++ cherrypy/lib/caching.pyc | Bin 0 -> 14466 bytes cherrypy/lib/covercp.py | 365 +++ cherrypy/lib/cpstats.py | 662 +++++ cherrypy/lib/cptools.py | 617 +++++ cherrypy/lib/cptools.pyc | Bin 0 -> 21963 bytes cherrypy/lib/encoding.py | 388 +++ cherrypy/lib/encoding.pyc | Bin 0 -> 10134 bytes cherrypy/lib/gctools.py | 214 ++ cherrypy/lib/http.py | 7 + cherrypy/lib/httpauth.py | 354 +++ cherrypy/lib/httpauth.pyc | Bin 0 -> 11664 bytes cherrypy/lib/httputil.py | 506 ++++ cherrypy/lib/httputil.pyc | Bin 0 -> 17803 bytes cherrypy/lib/jsontools.py | 87 + cherrypy/lib/jsontools.pyc | Bin 0 -> 4068 bytes cherrypy/lib/profiler.py | 208 ++ cherrypy/lib/reprconf.py | 485 ++++ cherrypy/lib/reprconf.pyc | Bin 0 -> 16783 bytes cherrypy/lib/sessions.py | 871 +++++++ cherrypy/lib/sessions.pyc | Bin 0 -> 30551 bytes cherrypy/lib/static.py | 363 +++ cherrypy/lib/static.pyc | Bin 0 -> 10849 bytes cherrypy/lib/xmlrpcutil.py | 55 + cherrypy/lib/xmlrpcutil.pyc | Bin 0 -> 2174 bytes cherrypy/process/__init__.py | 14 + cherrypy/process/__init__.pyc | Bin 0 -> 742 bytes cherrypy/process/plugins.py | 683 +++++ cherrypy/process/plugins.pyc | Bin 0 -> 24183 bytes cherrypy/process/servers.py | 427 +++ cherrypy/process/servers.pyc | Bin 0 -> 13911 bytes cherrypy/process/win32.py | 174 ++ cherrypy/process/win32.pyc | Bin 0 -> 6980 bytes cherrypy/process/wspbus.py | 432 +++ cherrypy/process/wspbus.pyc | Bin 0 -> 15599 bytes cherrypy/scaffold/__init__.py | 61 + cherrypy/scaffold/apache-fcgi.conf | 22 + cherrypy/scaffold/example.conf | 3 + cherrypy/scaffold/site.conf | 14 + .../static/made_with_cherrypy_small.png | Bin 0 -> 7455 bytes cherrypy/test/__init__.py | 27 + cherrypy/test/_test_decorators.py | 41 + cherrypy/test/_test_states_demo.py | 66 + cherrypy/test/benchmark.py | 409 +++ cherrypy/test/checkerdemo.py | 47 + cherrypy/test/helper.py | 493 ++++ cherrypy/test/logtest.py | 188 ++ cherrypy/test/modfastcgi.py | 135 + cherrypy/test/modfcgid.py | 125 + cherrypy/test/modpy.py | 163 ++ cherrypy/test/modwsgi.py | 148 ++ cherrypy/test/sessiondemo.py | 153 ++ cherrypy/test/static/dirback.jpg | Bin 0 -> 18238 bytes cherrypy/test/static/index.html | 1 + cherrypy/test/style.css | 1 + cherrypy/test/test.pem | 38 + cherrypy/test/test_auth_basic.py | 79 + cherrypy/test/test_auth_digest.py | 115 + cherrypy/test/test_bus.py | 263 ++ cherrypy/test/test_caching.py | 328 +++ cherrypy/test/test_config.py | 256 ++ cherrypy/test/test_config_server.py | 121 + cherrypy/test/test_conn.py | 734 ++++++ cherrypy/test/test_core.py | 688 +++++ cherrypy/test/test_dynamicobjectmapping.py | 404 +++ cherrypy/test/test_encoding.py | 363 +++ cherrypy/test/test_etags.py | 83 + cherrypy/test/test_http.py | 212 ++ cherrypy/test/test_httpauth.py | 151 ++ cherrypy/test/test_httplib.py | 29 + cherrypy/test/test_json.py | 79 + cherrypy/test/test_logging.py | 157 ++ cherrypy/test/test_mime.py | 128 + cherrypy/test/test_misc_tools.py | 207 ++ cherrypy/test/test_objectmapping.py | 404 +++ cherrypy/test/test_proxy.py | 129 + cherrypy/test/test_refleaks.py | 59 + cherrypy/test/test_request_obj.py | 737 ++++++ cherrypy/test/test_routes.py | 69 + cherrypy/test/test_session.py | 464 ++++ cherrypy/test/test_sessionauthenticate.py | 62 + cherrypy/test/test_states.py | 439 ++++ cherrypy/test/test_static.py | 300 +++ cherrypy/test/test_tools.py | 399 +++ cherrypy/test/test_tutorials.py | 201 ++ cherrypy/test/test_virtualhost.py | 107 + cherrypy/test/test_wsgi_ns.py | 91 + cherrypy/test/test_wsgi_vhost.py | 36 + cherrypy/test/test_wsgiapps.py | 118 + cherrypy/test/test_xmlrpc.py | 179 ++ cherrypy/test/webtest.py | 575 ++++ cherrypy/tutorial/README.txt | 16 + cherrypy/tutorial/__init__.py | 3 + cherrypy/tutorial/bonus-sqlobject.py | 168 ++ cherrypy/tutorial/custom_error.html | 14 + cherrypy/tutorial/pdf_file.pdf | Bin 0 -> 85698 bytes cherrypy/tutorial/tut01_helloworld.py | 35 + cherrypy/tutorial/tut02_expose_methods.py | 32 + cherrypy/tutorial/tut03_get_and_post.py | 53 + cherrypy/tutorial/tut04_complex_site.py | 98 + cherrypy/tutorial/tut05_derived_objects.py | 83 + cherrypy/tutorial/tut06_default_method.py | 64 + cherrypy/tutorial/tut07_sessions.py | 44 + .../tutorial/tut08_generators_and_yield.py | 47 + cherrypy/tutorial/tut09_files.py | 107 + cherrypy/tutorial/tut10_http_errors.py | 81 + cherrypy/tutorial/tutorial.conf | 4 + cherrypy/wsgiserver/__init__.py | 14 + cherrypy/wsgiserver/__init__.pyc | Bin 0 -> 734 bytes cherrypy/wsgiserver/ssl_builtin.py | 91 + cherrypy/wsgiserver/ssl_pyopenssl.py | 256 ++ cherrypy/wsgiserver/wsgiserver2.py | 2322 +++++++++++++++++ cherrypy/wsgiserver/wsgiserver2.pyc | Bin 0 -> 65433 bytes cherrypy/wsgiserver/wsgiserver3.py | 2040 +++++++++++++++ lib/ConfigFunctions.pyc | Bin 2500 -> 2500 bytes lib/Constants.pyc | Bin 249 -> 249 bytes lib/DBFunctions.pyc | Bin 8896 -> 8896 bytes lib/GameTasks.pyc | Bin 5245 -> 5245 bytes lib/Logger.pyc | Bin 453 -> 453 bytes lib/UpgradeFunctions.pyc | Bin 4350 -> 4350 bytes lib/WebRoot.pyc | Bin 23689 -> 23689 bytes lib/__init__.pyc | Bin 111 -> 111 bytes 165 files changed, 31893 insertions(+) create mode 100644 cherrypy/LICENSE.txt create mode 100644 cherrypy/__init__.py create mode 100644 cherrypy/__init__.pyc create mode 100644 cherrypy/_cpchecker.py create mode 100644 cherrypy/_cpchecker.pyc create mode 100644 cherrypy/_cpcompat.py create mode 100644 cherrypy/_cpcompat.pyc create mode 100644 cherrypy/_cpconfig.py create mode 100644 cherrypy/_cpconfig.pyc create mode 100644 cherrypy/_cpdispatch.py create mode 100644 cherrypy/_cpdispatch.pyc create mode 100644 cherrypy/_cperror.py create mode 100644 cherrypy/_cperror.pyc create mode 100644 cherrypy/_cplogging.py create mode 100644 cherrypy/_cplogging.pyc create mode 100644 cherrypy/_cpmodpy.py create mode 100644 cherrypy/_cpnative_server.py create mode 100644 cherrypy/_cpreqbody.py create mode 100644 cherrypy/_cpreqbody.pyc create mode 100644 cherrypy/_cprequest.py create mode 100644 cherrypy/_cprequest.pyc create mode 100644 cherrypy/_cpserver.py create mode 100644 cherrypy/_cpserver.pyc create mode 100644 cherrypy/_cpthreadinglocal.py create mode 100644 cherrypy/_cptools.py create mode 100644 cherrypy/_cptools.pyc create mode 100644 cherrypy/_cptree.py create mode 100644 cherrypy/_cptree.pyc create mode 100644 cherrypy/_cpwsgi.py create mode 100644 cherrypy/_cpwsgi.pyc create mode 100644 cherrypy/_cpwsgi_server.py create mode 100644 cherrypy/_cpwsgi_server.pyc create mode 100644 cherrypy/cherryd create mode 100644 cherrypy/favicon.ico create mode 100644 cherrypy/lib/__init__.py create mode 100644 cherrypy/lib/__init__.pyc create mode 100644 cherrypy/lib/auth.py create mode 100644 cherrypy/lib/auth.pyc create mode 100644 cherrypy/lib/auth_basic.py create mode 100644 cherrypy/lib/auth_basic.pyc create mode 100644 cherrypy/lib/auth_digest.py create mode 100644 cherrypy/lib/auth_digest.pyc create mode 100644 cherrypy/lib/caching.py create mode 100644 cherrypy/lib/caching.pyc create mode 100644 cherrypy/lib/covercp.py create mode 100644 cherrypy/lib/cpstats.py create mode 100644 cherrypy/lib/cptools.py create mode 100644 cherrypy/lib/cptools.pyc create mode 100644 cherrypy/lib/encoding.py create mode 100644 cherrypy/lib/encoding.pyc create mode 100644 cherrypy/lib/gctools.py create mode 100644 cherrypy/lib/http.py create mode 100644 cherrypy/lib/httpauth.py create mode 100644 cherrypy/lib/httpauth.pyc create mode 100644 cherrypy/lib/httputil.py create mode 100644 cherrypy/lib/httputil.pyc create mode 100644 cherrypy/lib/jsontools.py create mode 100644 cherrypy/lib/jsontools.pyc create mode 100644 cherrypy/lib/profiler.py create mode 100644 cherrypy/lib/reprconf.py create mode 100644 cherrypy/lib/reprconf.pyc create mode 100644 cherrypy/lib/sessions.py create mode 100644 cherrypy/lib/sessions.pyc create mode 100644 cherrypy/lib/static.py create mode 100644 cherrypy/lib/static.pyc create mode 100644 cherrypy/lib/xmlrpcutil.py create mode 100644 cherrypy/lib/xmlrpcutil.pyc create mode 100644 cherrypy/process/__init__.py create mode 100644 cherrypy/process/__init__.pyc create mode 100644 cherrypy/process/plugins.py create mode 100644 cherrypy/process/plugins.pyc create mode 100644 cherrypy/process/servers.py create mode 100644 cherrypy/process/servers.pyc create mode 100644 cherrypy/process/win32.py create mode 100644 cherrypy/process/win32.pyc create mode 100644 cherrypy/process/wspbus.py create mode 100644 cherrypy/process/wspbus.pyc create mode 100644 cherrypy/scaffold/__init__.py create mode 100644 cherrypy/scaffold/apache-fcgi.conf create mode 100644 cherrypy/scaffold/example.conf create mode 100644 cherrypy/scaffold/site.conf create mode 100644 cherrypy/scaffold/static/made_with_cherrypy_small.png create mode 100644 cherrypy/test/__init__.py create mode 100644 cherrypy/test/_test_decorators.py create mode 100644 cherrypy/test/_test_states_demo.py create mode 100644 cherrypy/test/benchmark.py create mode 100644 cherrypy/test/checkerdemo.py create mode 100644 cherrypy/test/helper.py create mode 100644 cherrypy/test/logtest.py create mode 100644 cherrypy/test/modfastcgi.py create mode 100644 cherrypy/test/modfcgid.py create mode 100644 cherrypy/test/modpy.py create mode 100644 cherrypy/test/modwsgi.py create mode 100644 cherrypy/test/sessiondemo.py create mode 100644 cherrypy/test/static/dirback.jpg create mode 100644 cherrypy/test/static/index.html create mode 100644 cherrypy/test/style.css create mode 100644 cherrypy/test/test.pem create mode 100644 cherrypy/test/test_auth_basic.py create mode 100644 cherrypy/test/test_auth_digest.py create mode 100644 cherrypy/test/test_bus.py create mode 100644 cherrypy/test/test_caching.py create mode 100644 cherrypy/test/test_config.py create mode 100644 cherrypy/test/test_config_server.py create mode 100644 cherrypy/test/test_conn.py create mode 100644 cherrypy/test/test_core.py create mode 100644 cherrypy/test/test_dynamicobjectmapping.py create mode 100644 cherrypy/test/test_encoding.py create mode 100644 cherrypy/test/test_etags.py create mode 100644 cherrypy/test/test_http.py create mode 100644 cherrypy/test/test_httpauth.py create mode 100644 cherrypy/test/test_httplib.py create mode 100644 cherrypy/test/test_json.py create mode 100644 cherrypy/test/test_logging.py create mode 100644 cherrypy/test/test_mime.py create mode 100644 cherrypy/test/test_misc_tools.py create mode 100644 cherrypy/test/test_objectmapping.py create mode 100644 cherrypy/test/test_proxy.py create mode 100644 cherrypy/test/test_refleaks.py create mode 100644 cherrypy/test/test_request_obj.py create mode 100644 cherrypy/test/test_routes.py create mode 100644 cherrypy/test/test_session.py create mode 100644 cherrypy/test/test_sessionauthenticate.py create mode 100644 cherrypy/test/test_states.py create mode 100644 cherrypy/test/test_static.py create mode 100644 cherrypy/test/test_tools.py create mode 100644 cherrypy/test/test_tutorials.py create mode 100644 cherrypy/test/test_virtualhost.py create mode 100644 cherrypy/test/test_wsgi_ns.py create mode 100644 cherrypy/test/test_wsgi_vhost.py create mode 100644 cherrypy/test/test_wsgiapps.py create mode 100644 cherrypy/test/test_xmlrpc.py create mode 100644 cherrypy/test/webtest.py create mode 100644 cherrypy/tutorial/README.txt create mode 100644 cherrypy/tutorial/__init__.py create mode 100644 cherrypy/tutorial/bonus-sqlobject.py create mode 100644 cherrypy/tutorial/custom_error.html create mode 100644 cherrypy/tutorial/pdf_file.pdf create mode 100644 cherrypy/tutorial/tut01_helloworld.py create mode 100644 cherrypy/tutorial/tut02_expose_methods.py create mode 100644 cherrypy/tutorial/tut03_get_and_post.py create mode 100644 cherrypy/tutorial/tut04_complex_site.py create mode 100644 cherrypy/tutorial/tut05_derived_objects.py create mode 100644 cherrypy/tutorial/tut06_default_method.py create mode 100644 cherrypy/tutorial/tut07_sessions.py create mode 100644 cherrypy/tutorial/tut08_generators_and_yield.py create mode 100644 cherrypy/tutorial/tut09_files.py create mode 100644 cherrypy/tutorial/tut10_http_errors.py create mode 100644 cherrypy/tutorial/tutorial.conf create mode 100644 cherrypy/wsgiserver/__init__.py create mode 100644 cherrypy/wsgiserver/__init__.pyc create mode 100644 cherrypy/wsgiserver/ssl_builtin.py create mode 100644 cherrypy/wsgiserver/ssl_pyopenssl.py create mode 100644 cherrypy/wsgiserver/wsgiserver2.py create mode 100644 cherrypy/wsgiserver/wsgiserver2.pyc create mode 100644 cherrypy/wsgiserver/wsgiserver3.py diff --git a/Gamez.db b/Gamez.db index fac98ea74c52b276f57e743dded2903008847f6b..c73df905f8b062deb75bc403aeb0b84ea5c1d3fd 100644 GIT binary patch delta 1042 zcmc&zO=uHQ5T1FHm+Wpfu`Nhb+DZ@)X;9eRrkEB@(nLIo&DE1=59VZvJw+@{R=SFp z#tM1ClP9GYK}FXHBKF`(P!L2&1d9qDJQRO07ol$#A%WsKGjC=d-@NY)!<$#FTGgsm zACGJTz-ItV1F*?jVl9Dd3HCTaopoi5mSvKagkC3u(XyMV(+mYAv}EVp$}PGah-|~Q zB%kDjxU%9j=}9vXI>D3j%sE%@1iDAHD;NWe;Q>4(^k4j^z~?mXruOsKOaxz2tfBC{ zn~(9W(byt<2f2Pg7XcL}5Pm zLsurg!6*12+mI3>#xRFDKV~uO#|&otSipiG^O$$v%KK%N1zr-(zf^LH+$jc~V#p~f zcZ=$&XhdE{tPOOLMp#?qTB<1)TI#|7xv^CR2CA!rEf0h4|Lnj1{W0*I<_&D6B6qs? z>d=pm#LSzOwu50+T-u%V@zLcmW$C{#3ohlC~JW xp~QQItl1emD{kJSWIvX+Goto8e54;cW*-+grJ23j*6g)%OO>X%_}HjauKUZME8{)l%_bMMwpW z9s~=Pj)xw07ui$U!?L@hpa(_vw0O~jkO+bb9y}?B-*ee*ft~odCw~X7Q_4?k2 zXZfSI?8U&`oXW}44##ReI-0$0|GXUtwmTKFDD4;I1E1;w7qqLq!gEwwbu~@s1g+9l z)YUF+*3Gm@H_=AjNE>tmt=ILmPS?>|T}zWX8T5a8VSA0|b~)*6%E_i?WmCayD)dV# z+&P)I`3xe6%-oD9qI3Zk(1ln?i%>*kh|xt@M2k^O7h^FkK?z-gCA1W!^xOMq48HdP zORDnl7vhGld)meUnjLF0(il~_s3Q697z`Q{a<;Dt(5(p6YRS7S9@gEiCz$AKzv zJV=1!LNz!()S!mI3%lrU?55r5rhBl5?!{iZ5BunT?578CfF4BVAWshA5bZ$^J&eQj2#(NR z^wK``(SG#Pqc}>B;TRpj06mW5^aM`OK@8F~()1)w(o^L)#go%GO@}Z<&)^I_i?ehX z!;{y;^6&mJ{3sF&=@Dh0+E(+%{Avb`Ypfo*^KZ^fOJ2#{@VQLRO#PQLccf|Qcj>owFm!$L3kTf9mNZnGW)GjqkHBzNiCdH(AQh^kZK9_%# jO{I6zYbh^1lT?rN_2K<66|TKDaxW~+_DAl`x%t-@dzlN2 diff --git a/cherrypy/LICENSE.txt b/cherrypy/LICENSE.txt new file mode 100644 index 0000000..3816f93 --- /dev/null +++ b/cherrypy/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the CherryPy Team nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cherrypy/__init__.py b/cherrypy/__init__.py new file mode 100644 index 0000000..41e3898 --- /dev/null +++ b/cherrypy/__init__.py @@ -0,0 +1,624 @@ +"""CherryPy is a pythonic, object-oriented HTTP framework. + + +CherryPy consists of not one, but four separate API layers. + +The APPLICATION LAYER is the simplest. CherryPy applications are written as +a tree of classes and methods, where each branch in the tree corresponds to +a branch in the URL path. Each method is a 'page handler', which receives +GET and POST params as keyword arguments, and returns or yields the (HTML) +body of the response. The special method name 'index' is used for paths +that end in a slash, and the special method name 'default' is used to +handle multiple paths via a single handler. This layer also includes: + + * the 'exposed' attribute (and cherrypy.expose) + * cherrypy.quickstart() + * _cp_config attributes + * cherrypy.tools (including cherrypy.session) + * cherrypy.url() + +The ENVIRONMENT LAYER is used by developers at all levels. It provides +information about the current request and response, plus the application +and server environment, via a (default) set of top-level objects: + + * cherrypy.request + * cherrypy.response + * cherrypy.engine + * cherrypy.server + * cherrypy.tree + * cherrypy.config + * cherrypy.thread_data + * cherrypy.log + * cherrypy.HTTPError, NotFound, and HTTPRedirect + * cherrypy.lib + +The EXTENSION LAYER allows advanced users to construct and share their own +plugins. It consists of: + + * Hook API + * Tool API + * Toolbox API + * Dispatch API + * Config Namespace API + +Finally, there is the CORE LAYER, which uses the core API's to construct +the default components which are available at higher layers. You can think +of the default components as the 'reference implementation' for CherryPy. +Megaframeworks (and advanced users) may replace the default components +with customized or extended components. The core API's are: + + * Application API + * Engine API + * Request API + * Server API + * WSGI API + +These API's are described in the CherryPy specification: +http://www.cherrypy.org/wiki/CherryPySpec +""" + +__version__ = "3.2.2" + +from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode +from cherrypy._cpcompat import basestring, unicodestr, set + +from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect +from cherrypy._cperror import NotFound, CherryPyException, TimeoutError + +from cherrypy import _cpdispatch as dispatch + +from cherrypy import _cptools +tools = _cptools.default_toolbox +Tool = _cptools.Tool + +from cherrypy import _cprequest +from cherrypy.lib import httputil as _httputil + +from cherrypy import _cptree +tree = _cptree.Tree() +from cherrypy._cptree import Application +from cherrypy import _cpwsgi as wsgi + +from cherrypy import process +try: + from cherrypy.process import win32 + engine = win32.Win32Bus() + engine.console_control_handler = win32.ConsoleCtrlHandler(engine) + del win32 +except ImportError: + engine = process.bus + + +# Timeout monitor. We add two channels to the engine +# to which cherrypy.Application will publish. +engine.listeners['before_request'] = set() +engine.listeners['after_request'] = set() + +class _TimeoutMonitor(process.plugins.Monitor): + + def __init__(self, bus): + self.servings = [] + process.plugins.Monitor.__init__(self, bus, self.run) + + def before_request(self): + self.servings.append((serving.request, serving.response)) + + def after_request(self): + try: + self.servings.remove((serving.request, serving.response)) + except ValueError: + pass + + def run(self): + """Check timeout on all responses. (Internal)""" + for req, resp in self.servings: + resp.check_timeout() +engine.timeout_monitor = _TimeoutMonitor(engine) +engine.timeout_monitor.subscribe() + +engine.autoreload = process.plugins.Autoreloader(engine) +engine.autoreload.subscribe() + +engine.thread_manager = process.plugins.ThreadManager(engine) +engine.thread_manager.subscribe() + +engine.signal_handler = process.plugins.SignalHandler(engine) + + +from cherrypy import _cpserver +server = _cpserver.Server() +server.subscribe() + + +def quickstart(root=None, script_name="", config=None): + """Mount the given root, start the builtin server (and engine), then block. + + root: an instance of a "controller class" (a collection of page handler + methods) which represents the root of the application. + script_name: a string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the URL + at which to mount the given root. For example, if root.index() will + handle requests to "http://www.example.com:8080/dept/app1/", then + the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the root + of the URI, it MUST be an empty string (not "/"). + config: a file or dict containing application config. If this contains + a [global] section, those entries will be used in the global + (site-wide) config. + """ + if config: + _global_conf_alias.update(config) + + tree.mount(root, script_name, config) + + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + + engine.start() + engine.block() + + +from cherrypy._cpcompat import threadlocal as _local + +class _Serving(_local): + """An interface for registering request and response objects. + + Rather than have a separate "thread local" object for the request and + the response, this class works as a single threadlocal container for + both objects (and any others which developers wish to define). In this + way, we can easily dump those objects when we stop/start a new HTTP + conversation, yet still refer to them as module-level globals in a + thread-safe way. + """ + + request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), + _httputil.Host("127.0.0.1", 1111)) + """ + The request object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + response = _cprequest.Response() + """ + The response object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + def load(self, request, response): + self.request = request + self.response = response + + def clear(self): + """Remove all attributes of self.""" + self.__dict__.clear() + +serving = _Serving() + + +class _ThreadLocalProxy(object): + + __slots__ = ['__attrname__', '__dict__'] + + def __init__(self, attrname): + self.__attrname__ = attrname + + def __getattr__(self, name): + child = getattr(serving, self.__attrname__) + return getattr(child, name) + + def __setattr__(self, name, value): + if name in ("__attrname__", ): + object.__setattr__(self, name, value) + else: + child = getattr(serving, self.__attrname__) + setattr(child, name, value) + + def __delattr__(self, name): + child = getattr(serving, self.__attrname__) + delattr(child, name) + + def _get_dict(self): + child = getattr(serving, self.__attrname__) + d = child.__class__.__dict__.copy() + d.update(child.__dict__) + return d + __dict__ = property(_get_dict) + + def __getitem__(self, key): + child = getattr(serving, self.__attrname__) + return child[key] + + def __setitem__(self, key, value): + child = getattr(serving, self.__attrname__) + child[key] = value + + def __delitem__(self, key): + child = getattr(serving, self.__attrname__) + del child[key] + + def __contains__(self, key): + child = getattr(serving, self.__attrname__) + return key in child + + def __len__(self): + child = getattr(serving, self.__attrname__) + return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) + # Python 3 + __bool__ = __nonzero__ + +# Create request and response object (the same objects will be used +# throughout the entire life of the webserver, but will redirect +# to the "serving" object) +request = _ThreadLocalProxy('request') +response = _ThreadLocalProxy('response') + +# Create thread_data object as a thread-specific all-purpose storage +class _ThreadData(_local): + """A container for thread-specific data.""" +thread_data = _ThreadData() + + +# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. +# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. +# The only other way would be to change what is returned from type(request) +# and that's not possible in pure Python (you'd have to fake ob_type). +def _cherrypy_pydoc_resolve(thing, forceload=0): + """Given an object or a path to an object, get the object and its name.""" + if isinstance(thing, _ThreadLocalProxy): + thing = getattr(serving, thing.__attrname__) + return _pydoc._builtin_resolve(thing, forceload) + +try: + import pydoc as _pydoc + _pydoc._builtin_resolve = _pydoc.resolve + _pydoc.resolve = _cherrypy_pydoc_resolve +except ImportError: + pass + + +from cherrypy import _cplogging + +class _GlobalLogManager(_cplogging.LogManager): + """A site-wide LogManager; routes to app.log or global log as appropriate. + + This :class:`LogManager` implements + cherrypy.log() and cherrypy.log.access(). If either + function is called during a request, the message will be sent to the + logger for the current Application. If they are called outside of a + request, the message will be sent to the site-wide logger. + """ + + def __call__(self, *args, **kwargs): + """Log the given message to the app.log or global log as appropriate.""" + # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945 + if hasattr(request, 'app') and hasattr(request.app, 'log'): + log = request.app.log + else: + log = self + return log.error(*args, **kwargs) + + def access(self): + """Log an access message to the app.log or global log as appropriate.""" + try: + return request.app.log.access() + except AttributeError: + return _cplogging.LogManager.access(self) + + +log = _GlobalLogManager() +# Set a default screen handler on the global log. +log.screen = True +log.error_file = '' +# Using an access file makes CP about 10% slower. Leave off by default. +log.access_file = '' + +def _buslog(msg, level): + log.error(msg, 'ENGINE', severity=level) +engine.subscribe('log', _buslog) + +# Helper functions for CP apps # + + +def expose(func=None, alias=None): + """Expose the function, optionally providing an alias or set of aliases.""" + def expose_(func): + func.exposed = True + if alias is not None: + if isinstance(alias, basestring): + parents[alias.replace(".", "_")] = func + else: + for a in alias: + parents[a.replace(".", "_")] = func + return func + + import sys, types + if isinstance(func, (types.FunctionType, types.MethodType)): + if alias is None: + # @expose + func.exposed = True + return func + else: + # func = expose(func, alias) + parents = sys._getframe(1).f_locals + return expose_(func) + elif func is None: + if alias is None: + # @expose() + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose(alias="alias") or + # @expose(alias=["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose("alias") or + # @expose(["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + alias = func + return expose_ + +def popargs(*args, **kwargs): + """A decorator for _cp_dispatch + (cherrypy.dispatch.Dispatcher.dispatch_method_name). + + Optional keyword argument: handler=(Object or Function) + + Provides a _cp_dispatch function that pops off path segments into + cherrypy.request.params under the names specified. The dispatch + is then forwarded on to the next vpath element. + + Note that any existing (and exposed) member function of the class that + popargs is applied to will override that value of the argument. For + instance, if you have a method named "list" on the class decorated with + popargs, then accessing "/list" will call that function instead of popping + it off as the requested parameter. This restriction applies to all + _cp_dispatch functions. The only way around this restriction is to create + a "blank class" whose only function is to provide _cp_dispatch. + + If there are path elements after the arguments, or more arguments + are requested than are available in the vpath, then the 'handler' + keyword argument specifies the next object to handle the parameterized + request. If handler is not specified or is None, then self is used. + If handler is a function rather than an instance, then that function + will be called with the args specified and the return value from that + function used as the next object INSTEAD of adding the parameters to + cherrypy.request.args. + + This decorator may be used in one of two ways: + + As a class decorator: + @cherrypy.popargs('year', 'month', 'day') + class Blog: + def index(self, year=None, month=None, day=None): + #Process the parameters here; any url like + #/, /2009, /2009/12, or /2009/12/31 + #will fill in the appropriate parameters. + + def create(self): + #This link will still be available at /create. Defined functions + #take precedence over arguments. + + Or as a member of a class: + class Blog: + _cp_dispatch = cherrypy.popargs('year', 'month', 'day') + #... + + The handler argument may be used to mix arguments with built in functions. + For instance, the following setup allows different activities at the + day, month, and year level: + + class DayHandler: + def index(self, year, month, day): + #Do something with this day; probably list entries + + def delete(self, year, month, day): + #Delete all entries for this day + + @cherrypy.popargs('day', handler=DayHandler()) + class MonthHandler: + def index(self, year, month): + #Do something with this month; probably list entries + + def delete(self, year, month): + #Delete all entries for this month + + @cherrypy.popargs('month', handler=MonthHandler()) + class YearHandler: + def index(self, year): + #Do something with this year + + #... + + @cherrypy.popargs('year', handler=YearHandler()) + class Root: + def index(self): + #... + + """ + + #Since keyword arg comes after *args, we have to process it ourselves + #for lower versions of python. + + handler = None + handler_call = False + for k,v in kwargs.items(): + if k == 'handler': + handler = v + else: + raise TypeError( + "cherrypy.popargs() got an unexpected keyword argument '{0}'" \ + .format(k) + ) + + import inspect + + if handler is not None \ + and (hasattr(handler, '__call__') or inspect.isclass(handler)): + handler_call = True + + def decorated(cls_or_self=None, vpath=None): + if inspect.isclass(cls_or_self): + #cherrypy.popargs is a class decorator + cls = cls_or_self + setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated) + return cls + + #We're in the actual function + self = cls_or_self + parms = {} + for arg in args: + if not vpath: + break + parms[arg] = vpath.pop(0) + + if handler is not None: + if handler_call: + return handler(**parms) + else: + request.params.update(parms) + return handler + + request.params.update(parms) + + #If we are the ultimate handler, then to prevent our _cp_dispatch + #from being called again, we will resolve remaining elements through + #getattr() directly. + if vpath: + return getattr(self, vpath.pop(0), None) + else: + return self + + return decorated + +def url(path="", qs="", script_name=None, base=None, relative=None): + """Create an absolute URL for the given path. + + If 'path' starts with a slash ('/'), this will return + (base + script_name + path + qs). + If it does not start with a slash, this returns + (base + script_name [+ request.path_info] + path + qs). + + If script_name is None, cherrypy.request will be used + to find a script_name, if available. + + If base is None, cherrypy.request.base will be used (if available). + Note that you can use cherrypy.tools.proxy to change this. + + Finally, note that this function can be used to obtain an absolute URL + for the current request path (minus the querystring) by passing no args. + If you call url(qs=cherrypy.request.query_string), you should get the + original browser URL (assuming no internal redirections). + + If relative is None or not provided, request.app.relative_urls will + be used (if available, else False). If False, the output will be an + absolute URL (including the scheme, host, vhost, and script_name). + If True, the output will instead be a URL that is relative to the + current request path, perhaps including '..' atoms. If relative is + the string 'server', the output will instead be a URL that is + relative to the server root; i.e., it will start with a slash. + """ + if isinstance(qs, (tuple, list, dict)): + qs = _urlencode(qs) + if qs: + qs = '?' + qs + + if request.app: + if not path.startswith("/"): + # Append/remove trailing slash from path_info as needed + # (this is to support mistyped URL's without redirecting; + # if you want to redirect, use tools.trailing_slash). + pi = request.path_info + if request.is_index is True: + if not pi.endswith('/'): + pi = pi + '/' + elif request.is_index is False: + if pi.endswith('/') and pi != '/': + pi = pi[:-1] + + if path == "": + path = pi + else: + path = _urljoin(pi, path) + + if script_name is None: + script_name = request.script_name + if base is None: + base = request.base + + newurl = base + script_name + path + qs + else: + # No request.app (we're being called outside a request). + # We'll have to guess the base from server.* attributes. + # This will produce very different results from the above + # if you're using vhosts or tools.proxy. + if base is None: + base = server.base() + + path = (script_name or "") + path + newurl = base + path + qs + + if './' in newurl: + # Normalize the URL by removing ./ and ../ + atoms = [] + for atom in newurl.split('/'): + if atom == '.': + pass + elif atom == '..': + atoms.pop() + else: + atoms.append(atom) + newurl = '/'.join(atoms) + + # At this point, we should have a fully-qualified absolute URL. + + if relative is None: + relative = getattr(request.app, "relative_urls", False) + + # See http://www.ietf.org/rfc/rfc2396.txt + if relative == 'server': + # "A relative reference beginning with a single slash character is + # termed an absolute-path reference, as defined by ..." + # This is also sometimes called "server-relative". + newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) + elif relative: + # "A relative reference that does not begin with a scheme name + # or a slash character is termed a relative-path reference." + old = url(relative=False).split('/')[:-1] + new = newurl.split('/') + while old and new: + a, b = old[0], new[0] + if a != b: + break + old.pop(0) + new.pop(0) + new = (['..'] * len(old)) + new + newurl = '/'.join(new) + + return newurl + + +# import _cpconfig last so it can reference other top-level objects +from cherrypy import _cpconfig +# Use _global_conf_alias so quickstart can use 'config' as an arg +# without shadowing cherrypy.config. +config = _global_conf_alias = _cpconfig.Config() +config.defaults = { + 'tools.log_tracebacks.on': True, + 'tools.log_headers.on': True, + 'tools.trailing_slash.on': True, + 'tools.encode.on': True + } +config.namespaces["log"] = lambda k, v: setattr(log, k, v) +config.namespaces["checker"] = lambda k, v: setattr(checker, k, v) +# Must reset to get our defaults applied. +config.reset() + +from cherrypy import _cpchecker +checker = _cpchecker.Checker() +engine.subscribe('start', checker) diff --git a/cherrypy/__init__.pyc b/cherrypy/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3ffd34ec3e6fbfec69fabec9889322d7da22322 GIT binary patch literal 20741 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 1: + if atoms[0] not in ns: + # Spit out a special warning if a known + # namespace is preceded by "cherrypy." + if (atoms[0] == "cherrypy" and atoms[1] in ns): + msg = ("The config entry %r is invalid; " + "try %r instead.\nsection: [%s]" + % (k, ".".join(atoms[1:]), section)) + else: + msg = ("The config entry %r is invalid, because " + "the %r config namespace is unknown.\n" + "section: [%s]" % (k, atoms[0], section)) + warnings.warn(msg) + elif atoms[0] == "tools": + if atoms[1] not in dir(cherrypy.tools): + msg = ("The config entry %r may be invalid, " + "because the %r tool was not found.\n" + "section: [%s]" % (k, atoms[1], section)) + warnings.warn(msg) + + def check_config_namespaces(self): + """Process config and warn on each unknown config namespace.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_ns(app) + + + + + # -------------------------- Config Types -------------------------- # + + known_config_types = {} + + def _populate_known_types(self): + b = [x for x in vars(builtins).values() + if type(x) is type(str)] + + def traverse(obj, namespace): + for name in dir(obj): + # Hack for 3.2's warning about body_params + if name == 'body_params': + continue + vtype = type(getattr(obj, name, None)) + if vtype in b: + self.known_config_types[namespace + "." + name] = vtype + + traverse(cherrypy.request, "request") + traverse(cherrypy.response, "response") + traverse(cherrypy.server, "server") + traverse(cherrypy.engine, "engine") + traverse(cherrypy.log, "log") + + def _known_types(self, config): + msg = ("The config entry %r in section %r is of type %r, " + "which does not match the expected type %r.") + + for section, conf in config.items(): + if isinstance(conf, dict): + for k, v in conf.items(): + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + else: + k, v = section, conf + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + + def check_config_types(self): + """Assert that config values are of the same type as default values.""" + self._known_types(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_types(app.config) + + + # -------------------- Specific config warnings -------------------- # + + def check_localhost(self): + """Warn if any socket_host is 'localhost'. See #711.""" + for k, v in cherrypy.config.items(): + if k == 'server.socket_host' and v == 'localhost': + warnings.warn("The use of 'localhost' as a socket host can " + "cause problems on newer systems, since 'localhost' can " + "map to either an IPv4 or an IPv6 address. You should " + "use '127.0.0.1' or '[::1]' instead.") diff --git a/cherrypy/_cpchecker.pyc b/cherrypy/_cpchecker.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1103c7f2265e62c486f192f76a055c145fe3bf8 GIT binary patch literal 12016 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 xqy-ynB944*RKhI;VF3ILhhj{{ly+vYh|` literal 0 HcmV?d00001 diff --git a/cherrypy/_cpcompat.py b/cherrypy/_cpcompat.py new file mode 100644 index 0000000..ed24c1a --- /dev/null +++ b/cherrypy/_cpcompat.py @@ -0,0 +1,318 @@ +"""Compatibility code for using CherryPy with various versions of Python. + +CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a +useful abstraction over the differences between Python versions, sometimes by +preferring a newer idiom, sometimes an older one, and sometimes a custom one. + +In particular, Python 2 uses str and '' for byte strings, while Python 3 +uses str and '' for unicode strings. We will call each of these the 'native +string' type for each version. Because of this major difference, this module +provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as +two functions: 'ntob', which translates native strings (of type 'str') into +byte strings regardless of Python version, and 'ntou', which translates native +strings to unicode strings. This also provides a 'BytesIO' name for dealing +specifically with bytes, and a 'StringIO' name for dealing with native strings. +It also provides a 'base64_decode' function with native strings as input and +output. +""" +import os +import re +import sys + +if sys.version_info >= (3, 0): + py3k = True + bytestr = bytes + unicodestr = str + nativestr = unicodestr + basestring = (bytes, str) + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 3, the native string type is unicode + return n.encode(encoding) + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given encoding.""" + # In Python 3, the native string type is unicode + return n + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n + # type("") + from io import StringIO + # bytes: + from io import BytesIO as BytesIO +else: + # Python 2 + py3k = False + bytestr = str + unicodestr = unicode + nativestr = bytestr + basestring = basestring + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given encoding.""" + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses this + # to signal that it wants to pass a string with embedded \uXXXX escapes, + # but without having to prefix it with u'' for Python 2, but no prefix + # for Python 3. + if encoding == 'escape': + return unicode( + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 is almost + # always what was intended. + return n.decode(encoding) + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 2, the native string type is bytes. + if isinstance(n, unicode): + return n.encode(encoding) + return n + try: + # type("") + from cStringIO import StringIO + except ImportError: + # type("") + from StringIO import StringIO + # bytes: + BytesIO = StringIO + +try: + set = set +except NameError: + from sets import Set as set + +try: + # Python 3.1+ + from base64 import decodebytes as _base64_decodebytes +except ImportError: + # Python 3.0- + # since CherryPy claims compability with Python 2.3, we must use + # the legacy API of base64 + from base64 import decodestring as _base64_decodebytes + +def base64_decode(n, encoding='ISO-8859-1'): + """Return the native string base64-decoded (as a native string).""" + if isinstance(n, unicodestr): + b = n.encode(encoding) + else: + b = n + b = _base64_decodebytes(b) + if nativestr is unicodestr: + return b.decode(encoding) + else: + return b + +try: + # Python 2.5+ + from hashlib import md5 +except ImportError: + from md5 import new as md5 + +try: + # Python 2.5+ + from hashlib import sha1 as sha +except ImportError: + from sha import new as sha + +try: + sorted = sorted +except NameError: + def sorted(i): + i = i[:] + i.sort() + return i + +try: + reversed = reversed +except NameError: + def reversed(x): + i = len(x) + while i > 0: + i -= 1 + yield x[i] + +try: + # Python 3 + from urllib.parse import urljoin, urlencode + from urllib.parse import quote, quote_plus + from urllib.request import unquote, urlopen + from urllib.request import parse_http_list, parse_keqv_list +except ImportError: + # Python 2 + from urlparse import urljoin + from urllib import urlencode, urlopen + from urllib import quote, quote_plus + from urllib import unquote + from urllib2 import parse_http_list, parse_keqv_list + +try: + from threading import local as threadlocal +except ImportError: + from cherrypy._cpthreadinglocal import local as threadlocal + +try: + dict.iteritems + # Python 2 + iteritems = lambda d: d.iteritems() + copyitems = lambda d: d.items() +except AttributeError: + # Python 3 + iteritems = lambda d: d.items() + copyitems = lambda d: list(d.items()) + +try: + dict.iterkeys + # Python 2 + iterkeys = lambda d: d.iterkeys() + copykeys = lambda d: d.keys() +except AttributeError: + # Python 3 + iterkeys = lambda d: d.keys() + copykeys = lambda d: list(d.keys()) + +try: + dict.itervalues + # Python 2 + itervalues = lambda d: d.itervalues() + copyvalues = lambda d: d.values() +except AttributeError: + # Python 3 + itervalues = lambda d: d.values() + copyvalues = lambda d: list(d.values()) + +try: + # Python 3 + import builtins +except ImportError: + # Python 2 + import __builtin__ as builtins + +try: + # Python 2. We have to do it in this order so Python 2 builds + # don't try to import the 'http' module from cherrypy.lib + from Cookie import SimpleCookie, CookieError + from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected + from BaseHTTPServer import BaseHTTPRequestHandler +except ImportError: + # Python 3 + from http.cookies import SimpleCookie, CookieError + from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected + from http.server import BaseHTTPRequestHandler + +try: + # Python 2. We have to do it in this order so Python 2 builds + # don't try to import the 'http' module from cherrypy.lib + from httplib import HTTPSConnection +except ImportError: + try: + # Python 3 + from http.client import HTTPSConnection + except ImportError: + # Some platforms which don't have SSL don't expose HTTPSConnection + HTTPSConnection = None + +try: + # Python 2 + xrange = xrange +except NameError: + # Python 3 + xrange = range + +import threading +if hasattr(threading.Thread, "daemon"): + # Python 2.6+ + def get_daemon(t): + return t.daemon + def set_daemon(t, val): + t.daemon = val +else: + def get_daemon(t): + return t.isDaemon() + def set_daemon(t, val): + t.setDaemon(val) + +try: + from email.utils import formatdate + def HTTPDate(timeval=None): + return formatdate(timeval, usegmt=True) +except ImportError: + from rfc822 import formatdate as HTTPDate + +try: + # Python 3 + from urllib.parse import unquote as parse_unquote + def unquote_qs(atom, encoding, errors='strict'): + return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors) +except ImportError: + # Python 2 + from urllib import unquote as parse_unquote + def unquote_qs(atom, encoding, errors='strict'): + return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors) + +try: + # Prefer simplejson, which is usually more advanced than the builtin module. + import simplejson as json + json_decode = json.JSONDecoder().decode + json_encode = json.JSONEncoder().iterencode +except ImportError: + if py3k: + # Python 3.0: json is part of the standard library, + # but outputs unicode. We need bytes. + import json + json_decode = json.JSONDecoder().decode + _json_encode = json.JSONEncoder().iterencode + def json_encode(value): + for chunk in _json_encode(value): + yield chunk.encode('utf8') + elif sys.version_info >= (2, 6): + # Python 2.6: json is part of the standard library + import json + json_decode = json.JSONDecoder().decode + json_encode = json.JSONEncoder().iterencode + else: + json = None + def json_decode(s): + raise ValueError('No JSON library is available') + def json_encode(s): + raise ValueError('No JSON library is available') + +try: + import cPickle as pickle +except ImportError: + # In Python 2, pickle is a Python version. + # In Python 3, pickle is the sped-up C version. + import pickle + +try: + os.urandom(20) + import binascii + def random20(): + return binascii.hexlify(os.urandom(20)).decode('ascii') +except (AttributeError, NotImplementedError): + import random + # os.urandom not available until Python 2.4. Fall back to random.random. + def random20(): + return sha('%s' % random.random()).hexdigest() + +try: + from _thread import get_ident as get_thread_ident +except ImportError: + from thread import get_ident as get_thread_ident + +try: + # Python 3 + next = next +except NameError: + # Python 2 + def next(i): + return i.next() diff --git a/cherrypy/_cpcompat.pyc b/cherrypy/_cpcompat.pyc new file mode 100644 index 0000000000000000000000000000000000000000..855f6108f2e9bde20840310a7a6eda1ae4c00c2f GIT binary patch literal 11107 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 literal 0 HcmV?d00001 diff --git a/cherrypy/_cpconfig.py b/cherrypy/_cpconfig.py new file mode 100644 index 0000000..7b4c6a4 --- /dev/null +++ b/cherrypy/_cpconfig.py @@ -0,0 +1,295 @@ +""" +Configuration system for CherryPy. + +Configuration in CherryPy is implemented via dictionaries. Keys are strings +which name the mapped value, which may be of any type. + + +Architecture +------------ + +CherryPy Requests are part of an Application, which runs in a global context, +and configuration data may apply to any of those three scopes: + +Global + Configuration entries which apply everywhere are stored in + cherrypy.config. + +Application + Entries which apply to each mounted application are stored + on the Application object itself, as 'app.config'. This is a two-level + dict where each key is a path, or "relative URL" (for example, "/" or + "/path/to/my/page"), and each value is a config dict. Usually, this + data is provided in the call to tree.mount(root(), config=conf), + although you may also use app.merge(conf). + +Request + Each Request object possesses a single 'Request.config' dict. + Early in the request process, this dict is populated by merging global + config entries, Application entries (whose path equals or is a parent + of Request.path_info), and any config acquired while looking up the + page handler (see next). + + +Declaration +----------- + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, CherryPy +uses Python's builtin ConfigParser; you declare Application config by +writing each path as a section header:: + + [/path/to/my/page] + request.stream = True + +To declare global configuration entries, place them in a [global] section. + +You may also declare config entries directly on the classes and methods +(page handlers) that make up your CherryPy application via the ``_cp_config`` +attribute. For example:: + + class Demo: + _cp_config = {'tools.gzip.on': True} + + def index(self): + return "Hello world" + index.exposed = True + index._cp_config = {'request.show_tracebacks': False} + +.. note:: + + This behavior is only guaranteed for the default dispatcher. + Other dispatchers may have different restrictions on where + you can attach _cp_config attributes. + + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. +Current namespaces: + +engine + Controls the 'application engine', including autoreload. + These can only be declared in the global config. + +tree + Grafts cherrypy.Application objects onto cherrypy.tree. + These can only be declared in the global config. + +hooks + Declares additional request-processing functions. + +log + Configures the logging for each application. + These can only be declared in the global or / config. + +request + Adds attributes to each Request. + +response + Adds attributes to each Response. + +server + Controls the default HTTP server via cherrypy.server. + These can only be declared in the global config. + +tools + Runs and configures additional request-processing packages. + +wsgi + Adds WSGI middleware to an Application's "pipeline". + These can only be declared in the app's root config ("/"). + +checker + Controls the 'checker', which looks for common errors in + app state (including config) when the engine starts. + Global config only. + +The only key that does not exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +cherrypy._cpconfig.environments[environment]. It only applies to the global +config, and only when you use cherrypy.config.update. + +You can define your own namespaces to be called at the Global, Application, +or Request level, by adding a named handler to cherrypy.config.namespaces, +app.namespaces, or app.request_class.namespaces. The name can +be any string, and the handler must be either a callable or a (Python 2.5 +style) context manager. +""" + +import cherrypy +from cherrypy._cpcompat import set, basestring +from cherrypy.lib import reprconf + +# Deprecated in CherryPy 3.2--remove in 3.3 +NamespaceSet = reprconf.NamespaceSet + +def merge(base, other): + """Merge one app config (from a dict, file, or filename) into another. + + If the given config is a filename, it will be appended to + the list of files to monitor for "autoreload" changes. + """ + if isinstance(other, basestring): + cherrypy.engine.autoreload.files.add(other) + + # Load other into base + for section, value_map in reprconf.as_dict(other).items(): + if not isinstance(value_map, dict): + raise ValueError( + "Application config must include section headers, but the " + "config you tried to merge doesn't have any sections. " + "Wrap your config in another dict with paths as section " + "headers, for example: {'/': config}.") + base.setdefault(section, {}).update(value_map) + + +class Config(reprconf.Config): + """The 'global' configuration data for the entire CherryPy process.""" + + def update(self, config): + """Update self from a dict, file or filename.""" + if isinstance(config, basestring): + # Filename + cherrypy.engine.autoreload.files.add(config) + reprconf.Config.update(self, config) + + def _apply(self, config): + """Update self from a dict.""" + if isinstance(config.get("global", None), dict): + if len(config) > 1: + cherrypy.checker.global_config_contained_paths = True + config = config["global"] + if 'tools.staticdir.dir' in config: + config['tools.staticdir.section'] = "global" + reprconf.Config._apply(self, config) + + def __call__(self, *args, **kwargs): + """Decorator for page handlers to set _cp_config.""" + if args: + raise TypeError( + "The cherrypy.config decorator does not accept positional " + "arguments; you must use keyword arguments.") + def tool_decorator(f): + if not hasattr(f, "_cp_config"): + f._cp_config = {} + for k, v in kwargs.items(): + f._cp_config[k] = v + return f + return tool_decorator + + +Config.environments = environments = { + "staging": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + }, + "production": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + }, + "embedded": { + # For use with CherryPy embedded in another deployment stack. + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }, + "test_suite": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': True, + 'request.show_mismatched_params': True, + 'log.screen': False, + }, + } + + +def _server_namespace_handler(k, v): + """Config handler for the "server" namespace.""" + atoms = k.split(".", 1) + if len(atoms) > 1: + # Special-case config keys of the form 'server.servername.socket_port' + # to configure additional HTTP servers. + if not hasattr(cherrypy, "servers"): + cherrypy.servers = {} + + servername, k = atoms + if servername not in cherrypy.servers: + from cherrypy import _cpserver + cherrypy.servers[servername] = _cpserver.Server() + # On by default, but 'on = False' can unsubscribe it (see below). + cherrypy.servers[servername].subscribe() + + if k == 'on': + if v: + cherrypy.servers[servername].subscribe() + else: + cherrypy.servers[servername].unsubscribe() + else: + setattr(cherrypy.servers[servername], k, v) + else: + setattr(cherrypy.server, k, v) +Config.namespaces["server"] = _server_namespace_handler + +def _engine_namespace_handler(k, v): + """Backward compatibility handler for the "engine" namespace.""" + engine = cherrypy.engine + if k == 'autoreload_on': + if v: + engine.autoreload.subscribe() + else: + engine.autoreload.unsubscribe() + elif k == 'autoreload_frequency': + engine.autoreload.frequency = v + elif k == 'autoreload_match': + engine.autoreload.match = v + elif k == 'reload_files': + engine.autoreload.files = set(v) + elif k == 'deadlock_poll_freq': + engine.timeout_monitor.frequency = v + elif k == 'SIGHUP': + engine.listeners['SIGHUP'] = set([v]) + elif k == 'SIGTERM': + engine.listeners['SIGTERM'] = set([v]) + elif "." in k: + plugin, attrname = k.split(".", 1) + plugin = getattr(engine, plugin) + if attrname == 'on': + if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'): + plugin.subscribe() + return + elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'): + plugin.unsubscribe() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) +Config.namespaces["engine"] = _engine_namespace_handler + + +def _tree_namespace_handler(k, v): + """Namespace handler for the 'tree' config namespace.""" + if isinstance(v, dict): + for script_name, app in v.items(): + cherrypy.tree.graft(app, script_name) + cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/")) + else: + cherrypy.tree.graft(v, v.script_name) + cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/")) +Config.namespaces["tree"] = _tree_namespace_handler + + diff --git a/cherrypy/_cpconfig.pyc b/cherrypy/_cpconfig.pyc new file mode 100644 index 0000000000000000000000000000000000000000..393c29bcd3b58c23814cea16a7a71993e2d84a55 GIT binary patch literal 9510 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 ClJ+?O literal 0 HcmV?d00001 diff --git a/cherrypy/_cpdispatch.py b/cherrypy/_cpdispatch.py new file mode 100644 index 0000000..d614e08 --- /dev/null +++ b/cherrypy/_cpdispatch.py @@ -0,0 +1,636 @@ +"""CherryPy dispatchers. + +A 'dispatcher' is the object which looks up the 'page handler' callable +and collects config for the current request based on the path_info, other +request attributes, and the application architecture. The core calls the +dispatcher as early as possible, passing it a 'path_info' argument. + +The default dispatcher discovers the page handler by matching path_info +to a hierarchical arrangement of objects, starting at request.app.root. +""" + +import string +import sys +import types +try: + classtype = (type, types.ClassType) +except AttributeError: + classtype = type + +import cherrypy +from cherrypy._cpcompat import set + + +class PageHandler(object): + """Callable which sets response.body.""" + + def __init__(self, callable, *args, **kwargs): + self.callable = callable + self.args = args + self.kwargs = kwargs + + def __call__(self): + try: + return self.callable(*self.args, **self.kwargs) + except TypeError: + x = sys.exc_info()[1] + try: + test_callable_spec(self.callable, self.args, self.kwargs) + except cherrypy.HTTPError: + raise sys.exc_info()[1] + except: + raise x + raise + + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + show_mismatched_params = getattr( + cherrypy.serving.request, 'show_mismatched_params', False) + try: + (args, varargs, varkw, defaults) = inspect.getargspec(callable) + except TypeError: + if isinstance(callable, object) and hasattr(callable, '__call__'): + (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__) + else: + # If it wasn't one of our own types, re-raise + # the original error + raise + + if args and args[0] == 'self': + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + # figure out which args have defaults. + args_with_defaults = args[-len(defaults or []):] + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args_with_defaults[i]] == 0: + arg_usage[args_with_defaults[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.items(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + message = None + if show_mismatched_params: + message="Missing parameters: %s" % ",".join(missing_args) + raise cherrypy.HTTPError(404, message=message) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.serving.request.body.params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + message = None + if show_mismatched_params: + message="Multiple values for parameters: "\ + "%s" % ",".join(multiple_args) + raise cherrypy.HTTPError(error, message=message) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + message = None + if show_mismatched_params: + message="Unexpected query string "\ + "parameters: %s" % ", ".join(extra_qs_params) + raise cherrypy.HTTPError(404, message=message) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + message = None + if show_mismatched_params: + message="Unexpected body parameters: "\ + "%s" % ", ".join(extra_body_params) + raise cherrypy.HTTPError(400, message=message) + + +try: + import inspect +except ImportError: + test_callable_spec = lambda callable, args, kwargs: None + + + +class LateParamPageHandler(PageHandler): + """When passing cherrypy.request.params to the page handler, we do not + want to capture that dict too early; we want to give tools like the + decoding tool a chance to modify the params dict in-between the lookup + of the handler and the actual calling of the handler. This subclass + takes that into account, and allows request.params to be 'bound late' + (it's more complicated than that, but that's the effect). + """ + + def _get_kwargs(self): + kwargs = cherrypy.serving.request.params.copy() + if self._kwargs: + kwargs.update(self._kwargs) + return kwargs + + def _set_kwargs(self, kwargs): + self._kwargs = kwargs + + kwargs = property(_get_kwargs, _set_kwargs, + doc='page handler kwargs (with ' + 'cherrypy.request.params copied in)') + + +if sys.version_info < (3, 0): + punctuation_to_underscores = string.maketrans( + string.punctuation, '_' * len(string.punctuation)) + def validate_translator(t): + if not isinstance(t, str) or len(t) != 256: + raise ValueError("The translate argument must be a str of len 256.") +else: + punctuation_to_underscores = str.maketrans( + string.punctuation, '_' * len(string.punctuation)) + def validate_translator(t): + if not isinstance(t, dict): + raise ValueError("The translate argument must be a dict.") + +class Dispatcher(object): + """CherryPy Dispatcher which walks a tree of objects to find a handler. + + The tree is rooted at cherrypy.request.app.root, and each hierarchical + component in the path_info argument is matched to a corresponding nested + attribute of the root object. Matching handlers must have an 'exposed' + attribute which evaluates to True. The special method name "index" + matches a URI which ends in a slash ("/"). The special method name + "default" may match a portion of the path_info (but only when no longer + substring of the path_info matches some other object). + + This is the default, built-in dispatcher for CherryPy. + """ + + dispatch_method_name = '_cp_dispatch' + """ + The name of the dispatch method that nodes may optionally implement + to provide their own dynamic dispatch algorithm. + """ + + def __init__(self, dispatch_method_name=None, + translate=punctuation_to_underscores): + validate_translator(translate) + self.translate = translate + if dispatch_method_name: + self.dispatch_method_name = dispatch_method_name + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + func, vpath = self.find_handler(path_info) + + if func: + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace("%2F", "/") for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.NotFound() + + def find_handler(self, path): + """Return the appropriate page handler, plus any virtual path. + + This will return two objects. The first will be a callable, + which can be used to generate page output. Any parameters from + the query string or request body will be sent to that callable + as keyword arguments. + + The callable is found by traversing the application's tree, + starting from cherrypy.request.app.root, and matching path + components to successive objects in the tree. For example, the + URL "/path/to/handler" might return root.path.to.handler. + + The second object returned will be a list of names which are + 'virtual path' components: parts of the URL which are dynamic, + and were not used when looking up the handler. + These virtual path components are passed to the handler as + positional arguments. + """ + request = cherrypy.serving.request + app = request.app + root = app.root + dispatch_name = self.dispatch_method_name + + # Get config for the root object/path. + fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] + fullpath_len = len(fullpath) + segleft = fullpath_len + nodeconf = {} + if hasattr(root, "_cp_config"): + nodeconf.update(root._cp_config) + if "/" in app.config: + nodeconf.update(app.config["/"]) + object_trail = [['root', root, nodeconf, segleft]] + + node = root + iternames = fullpath[:] + while iternames: + name = iternames[0] + # map to legal Python identifiers (e.g. replace '.' with '_') + objname = name.translate(self.translate) + + nodeconf = {} + subnode = getattr(node, objname, None) + pre_len = len(iternames) + if subnode is None: + dispatch = getattr(node, dispatch_name, None) + if dispatch and hasattr(dispatch, '__call__') and not \ + getattr(dispatch, 'exposed', False) and \ + pre_len > 1: + #Don't expose the hidden 'index' token to _cp_dispatch + #We skip this if pre_len == 1 since it makes no sense + #to call a dispatcher when we have no tokens left. + index_name = iternames.pop() + subnode = dispatch(vpath=iternames) + iternames.append(index_name) + else: + #We didn't find a path, but keep processing in case there + #is a default() handler. + iternames.pop(0) + else: + #We found the path, remove the vpath entry + iternames.pop(0) + segleft = len(iternames) + if segleft > pre_len: + #No path segment was removed. Raise an error. + raise cherrypy.CherryPyException( + "A vpath segment was added. Custom dispatchers may only " + + "remove elements. While trying to process " + + "{0} in {1}".format(name, fullpath) + ) + elif segleft == pre_len: + #Assume that the handler used the current path segment, but + #did not pop it. This allows things like + #return getattr(self, vpath[0], None) + iternames.pop(0) + segleft -= 1 + node = subnode + + if node is not None: + # Get _cp_config attached to this node. + if hasattr(node, "_cp_config"): + nodeconf.update(node._cp_config) + + # Mix in values from app.config for this path. + existing_len = fullpath_len - pre_len + if existing_len != 0: + curpath = '/' + '/'.join(fullpath[0:existing_len]) + else: + curpath = '' + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] + for seg in new_segs: + curpath += '/' + seg + if curpath in app.config: + nodeconf.update(app.config[curpath]) + + object_trail.append([name, node, nodeconf, segleft]) + + def set_conf(): + """Collapse all object_trail config into cherrypy.request.config.""" + base = cherrypy.config.copy() + # Note that we merge the config from each node + # even if that node was None. + for name, obj, conf, segleft in object_trail: + base.update(conf) + if 'tools.staticdir.dir' in conf: + base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft]) + return base + + # Try successive objects (reverse order) + num_candidates = len(object_trail) - 1 + for i in range(num_candidates, -1, -1): + + name, candidate, nodeconf, segleft = object_trail[i] + if candidate is None: + continue + + # Try a "default" method on the current leaf. + if hasattr(candidate, "default"): + defhandler = candidate.default + if getattr(defhandler, 'exposed', False): + # Insert any extra _cp_config from the default handler. + conf = getattr(defhandler, "_cp_config", {}) + object_trail.insert(i+1, ["default", defhandler, conf, segleft]) + request.config = set_conf() + # See http://www.cherrypy.org/ticket/613 + request.is_index = path.endswith("/") + return defhandler, fullpath[fullpath_len - segleft:-1] + + # Uncomment the next line to restrict positional params to "default". + # if i < num_candidates - 2: continue + + # Try the current leaf. + if getattr(candidate, 'exposed', False): + request.config = set_conf() + if i == num_candidates: + # We found the extra ".index". Mark request so tools + # can redirect if path_info has no trailing slash. + request.is_index = True + else: + # We're not at an 'index' handler. Mark request so tools + # can redirect if path_info has NO trailing slash. + # Note that this also includes handlers which take + # positional parameters (virtual paths). + request.is_index = False + return candidate, fullpath[fullpath_len - segleft:-1] + + # We didn't find anything + request.config = set_conf() + return None, [] + + +class MethodDispatcher(Dispatcher): + """Additional dispatch based on cherrypy.request.method.upper(). + + Methods named GET, POST, etc will be called on an exposed class. + The method names must be all caps; the appropriate Allow header + will be output showing all capitalized method names as allowable + HTTP verbs. + + Note that the containing class must be exposed, not the methods. + """ + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + resource, vpath = self.find_handler(path_info) + + if resource: + # Set Allow header + avail = [m for m in dir(resource) if m.isupper()] + if "GET" in avail and "HEAD" not in avail: + avail.append("HEAD") + avail.sort() + cherrypy.serving.response.headers['Allow'] = ", ".join(avail) + + # Find the subhandler + meth = request.method.upper() + func = getattr(resource, meth, None) + if func is None and meth == "HEAD": + func = getattr(resource, "GET", None) + if func: + # Grab any _cp_config on the subhandler. + if hasattr(func, "_cp_config"): + request.config.update(func._cp_config) + + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace("%2F", "/") for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.HTTPError(405) + else: + request.handler = cherrypy.NotFound() + + +class RoutesDispatcher(object): + """A Routes based dispatcher for CherryPy.""" + + def __init__(self, full_result=False): + """ + Routes dispatcher + + Set full_result to True if you wish the controller + and the action to be passed on to the page handler + parameters. By default they won't be. + """ + import routes + self.full_result = full_result + self.controllers = {} + self.mapper = routes.Mapper() + self.mapper.controller_scan = self.controllers.keys + + def connect(self, name, route, controller, **kwargs): + self.controllers[name] = controller + self.mapper.connect(name, route, controller=name, **kwargs) + + def redirect(self, url): + raise cherrypy.HTTPRedirect(url) + + def __call__(self, path_info): + """Set handler and config for the current request.""" + func = self.find_handler(path_info) + if func: + cherrypy.serving.request.handler = LateParamPageHandler(func) + else: + cherrypy.serving.request.handler = cherrypy.NotFound() + + def find_handler(self, path_info): + """Find the right page handler, and set request.config.""" + import routes + + request = cherrypy.serving.request + + config = routes.request_config() + config.mapper = self.mapper + if hasattr(request, 'wsgi_environ'): + config.environ = request.wsgi_environ + config.host = request.headers.get('Host', None) + config.protocol = request.scheme + config.redirect = self.redirect + + result = self.mapper.match(path_info) + + config.mapper_dict = result + params = {} + if result: + params = result.copy() + if not self.full_result: + params.pop('controller', None) + params.pop('action', None) + request.params.update(params) + + # Get config for the root object/path. + request.config = base = cherrypy.config.copy() + curpath = "" + + def merge(nodeconf): + if 'tools.staticdir.dir' in nodeconf: + nodeconf['tools.staticdir.section'] = curpath or "/" + base.update(nodeconf) + + app = request.app + root = app.root + if hasattr(root, "_cp_config"): + merge(root._cp_config) + if "/" in app.config: + merge(app.config["/"]) + + # Mix in values from app.config. + atoms = [x for x in path_info.split("/") if x] + if atoms: + last = atoms.pop() + else: + last = None + for atom in atoms: + curpath = "/".join((curpath, atom)) + if curpath in app.config: + merge(app.config[curpath]) + + handler = None + if result: + controller = result.get('controller') + controller = self.controllers.get(controller, controller) + if controller: + if isinstance(controller, classtype): + controller = controller() + # Get config from the controller. + if hasattr(controller, "_cp_config"): + merge(controller._cp_config) + + action = result.get('action') + if action is not None: + handler = getattr(controller, action, None) + # Get config from the handler + if hasattr(handler, "_cp_config"): + merge(handler._cp_config) + else: + handler = controller + + # Do the last path atom here so it can + # override the controller's _cp_config. + if last: + curpath = "/".join((curpath, last)) + if curpath in app.config: + merge(app.config[curpath]) + + return handler + + +def XMLRPCDispatcher(next_dispatcher=Dispatcher()): + from cherrypy.lib import xmlrpcutil + def xmlrpc_dispatch(path_info): + path_info = xmlrpcutil.patched_path(path_info) + return next_dispatcher(path_info) + return xmlrpc_dispatch + + +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains): + """ + Select a different handler based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different parts of a single + website structure. For example:: + + http://www.domain.example -> root + http://www.domain2.example -> root/domain2/ + http://www.domain2.example:443 -> root/secure + + can be accomplished via the following config:: + + [/] + request.dispatch = cherrypy.dispatch.VirtualHost( + **{'www.domain2.example': '/domain2', + 'www.domain2.example:443': '/secure', + }) + + next_dispatcher + The next dispatcher object in the dispatch chain. + The VirtualHost dispatcher adds a prefix to the URL and calls + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). + + use_x_forwarded_host + If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying. + + ``**domains`` + A dict of {host header value: virtual prefix} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding "virtual prefix" + value will be prepended to the URL path before calling the + next dispatcher. Note that you often need separate entries + for "example.com" and "www.example.com". In addition, "Host" + headers may contain the port number. + """ + from cherrypy.lib import httputil + def vhost_dispatch(path_info): + request = cherrypy.serving.request + header = request.headers.get + + domain = header('Host', '') + if use_x_forwarded_host: + domain = header("X-Forwarded-Host", domain) + + prefix = domains.get(domain, "") + if prefix: + path_info = httputil.urljoin(prefix, path_info) + + result = next_dispatcher(path_info) + + # Touch up staticdir config. See http://www.cherrypy.org/ticket/614. + section = request.config.get('tools.staticdir.section') + if section: + section = section[len(prefix):] + request.config['tools.staticdir.section'] = section + + return result + return vhost_dispatch + diff --git a/cherrypy/_cpdispatch.pyc b/cherrypy/_cpdispatch.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e884cfec0bc74255823079abbfb1c51de4ebe16 GIT binary patch literal 19243 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 +or :class:`HTTPRedirect` as the +:attr:`request.handler`. + +.. _redirectingpost: + +Redirecting POST +================ + +When you GET a resource and are redirected by the server to another Location, +there's generally no problem since GET is both a "safe method" (there should +be no side-effects) and an "idempotent method" (multiple calls are no different +than a single call). + +POST, however, is neither safe nor idempotent--if you +charge a credit card, you don't want to be charged twice by a redirect! + +For this reason, *none* of the 3xx responses permit a user-agent (browser) to +resubmit a POST on redirection without first confirming the action with the user: + +===== ================================= =========== +300 Multiple Choices Confirm with the user +301 Moved Permanently Confirm with the user +302 Found (Object moved temporarily) Confirm with the user +303 See Other GET the new URI--no confirmation +304 Not modified (for conditional GET only--POST should not raise this error) +305 Use Proxy Confirm with the user +307 Temporary Redirect Confirm with the user +===== ================================= =========== + +However, browsers have historically implemented these restrictions poorly; +in particular, many browsers do not force the user to confirm 301, 302 +or 307 when redirecting POST. For this reason, CherryPy defaults to 303, +which most user-agents appear to have implemented correctly. Therefore, if +you raise HTTPRedirect for a POST request, the user-agent will most likely +attempt to GET the new URI (without asking for confirmation from the user). +We realize this is confusing for developers, but it's the safest thing we +could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` +or any other 3xx status if you know what you're doing, but given the +environment, we couldn't let any of those be the default. + +Custom Error Handling +===================== + +.. image:: /refman/cperrors.gif + +Anticipated HTTP responses +-------------------------- + +The 'error_page' config namespace can be used to provide custom HTML output for +expected responses (like 404 Not Found). Supply a filename from which the output +will be read. The contents will be interpolated with the values %(status)s, +%(message)s, %(traceback)s, and %(version)s using plain old Python +`string formatting `_. + +:: + + _cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")} + + +Beginning in version 3.1, you may also provide a function or other callable as +an error_page entry. It will be passed the same status, message, traceback and +version arguments that are interpolated into templates:: + + def error_page_402(status, message, traceback, version): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + cherrypy.config.update({'error_page.402': error_page_402}) + +Also in 3.1, in addition to the numbered error codes, you may also supply +"error_page.default" to handle all codes which do not have their own error_page entry. + + + +Unanticipated errors +-------------------- + +CherryPy also has a generic error handling mechanism: whenever an unanticipated +error occurs in your code, it will call +:func:`Request.error_response` to set +the response status, headers, and body. By default, this is the same output as +:class:`HTTPError(500) `. If you want to provide +some other behavior, you generally replace "request.error_response". + +Here is some sample code that shows how to display a custom error message and +send an e-mail containing the error:: + + from cherrypy import _cperror + + def handle_error(): + cherrypy.response.status = 500 + cherrypy.response.body = ["Sorry, an error occured"] + sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc()) + + class Root: + _cp_config = {'request.error_response': handle_error} + + +Note that you have to explicitly set :attr:`response.body ` +and not simply return an error message as a result. +""" + +from cgi import escape as _escape +from sys import exc_info as _exc_info +from traceback import format_exception as _format_exception +from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob, tonative, urljoin as _urljoin +from cherrypy.lib import httputil as _httputil + + +class CherryPyException(Exception): + """A base class for CherryPy exceptions.""" + pass + + +class TimeoutError(CherryPyException): + """Exception raised when Response.timed_out is detected.""" + pass + + +class InternalRedirect(CherryPyException): + """Exception raised to switch to the handler for a different URL. + + This exception will redirect processing to another path within the site + (without informing the client). Provide the new path as an argument when + raising the exception. Provide any params in the querystring for the new URL. + """ + + def __init__(self, path, query_string=""): + import cherrypy + self.request = cherrypy.serving.request + + self.query_string = query_string + if "?" in path: + # Separate any params included in the path + path, self.query_string = path.split("?", 1) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a URL relative to root (e.g. "/dummy") + # 2. a URL relative to the current path + # Note that any query string will be discarded. + path = _urljoin(self.request.path_info, path) + + # Set a 'path' member attribute so that code which traps this + # error can have access to it. + self.path = path + + CherryPyException.__init__(self, path, self.query_string) + + +class HTTPRedirect(CherryPyException): + """Exception raised when the request should be redirected. + + This exception will force a HTTP redirect to the URL or URL's you give it. + The new URL must be passed as the first argument to the Exception, + e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. + If a URL is absolute, it will be used as-is. If it is relative, it is + assumed to be relative to the current cherrypy.request.path_info. + + If one of the provided URL is a unicode object, it will be encoded + using the default encoding or the one passed in parameter. + + There are multiple types of redirect, from which you can select via the + ``status`` argument. If you do not provide a ``status`` arg, it defaults to + 303 (or 302 if responding with HTTP/1.0). + + Examples:: + + raise cherrypy.HTTPRedirect("") + raise cherrypy.HTTPRedirect("/abs/path", 307) + raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) + + See :ref:`redirectingpost` for additional caveats. + """ + + status = None + """The integer HTTP status code to emit.""" + + urls = None + """The list of URL's to emit.""" + + encoding = 'utf-8' + """The encoding when passed urls are not native strings""" + + def __init__(self, urls, status=None, encoding=None): + import cherrypy + request = cherrypy.serving.request + + if isinstance(urls, basestring): + urls = [urls] + + abs_urls = [] + for url in urls: + url = tonative(url, encoding or self.encoding) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a complete URL with host (e.g. "http://www.example.com/test") + # 2. a URL relative to root (e.g. "/dummy") + # 3. a URL relative to the current path + # Note that any query string in cherrypy.request is discarded. + url = _urljoin(cherrypy.url(), url) + abs_urls.append(url) + self.urls = abs_urls + + # RFC 2616 indicates a 301 response code fits our goal; however, + # browser support for 301 is quite messy. Do 302/303 instead. See + # http://www.alanflavell.org.uk/www/post-redirect.html + if status is None: + if request.protocol >= (1, 1): + status = 303 + else: + status = 302 + else: + status = int(status) + if status < 300 or status > 399: + raise ValueError("status must be between 300 and 399.") + + self.status = status + CherryPyException.__init__(self, abs_urls, status) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent self. + + CherryPy uses this internally, but you can also use it to create an + HTTPRedirect object and set its output without *raising* the exception. + """ + import cherrypy + response = cherrypy.serving.response + response.status = status = self.status + + if status in (300, 301, 302, 303, 307): + response.headers['Content-Type'] = "text/html;charset=utf-8" + # "The ... URI SHOULD be given by the Location field + # in the response." + response.headers['Location'] = self.urls[0] + + # "Unless the request method was HEAD, the entity of the response + # SHOULD contain a short hypertext note with a hyperlink to the + # new URI(s)." + msg = {300: "This resource can be found at %s.", + 301: "This resource has permanently moved to %s.", + 302: "This resource resides temporarily at %s.", + 303: "This resource can be found at %s.", + 307: "This resource has moved temporarily to %s.", + }[status] + msgs = [msg % (u, u) for u in self.urls] + response.body = ntob("
\n".join(msgs), 'utf-8') + # Previous code may have set C-L, so we have to reset it + # (allow finalize to set it). + response.headers.pop('Content-Length', None) + elif status == 304: + # Not Modified. + # "The response MUST include the following header fields: + # Date, unless its omission is required by section 14.18.1" + # The "Date" header should have been set in Response.__init__ + + # "...the response SHOULD NOT include other entity-headers." + for key in ('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Location', 'Content-MD5', + 'Content-Range', 'Content-Type', 'Expires', + 'Last-Modified'): + if key in response.headers: + del response.headers[key] + + # "The 304 response MUST NOT contain a message-body." + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + elif status == 305: + # Use Proxy. + # self.urls[0] should be the URI of the proxy. + response.headers['Location'] = self.urls[0] + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + else: + raise ValueError("The %s status code is unknown." % status) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +def clean_headers(status): + """Remove any headers which should not apply to an error response.""" + import cherrypy + + response = cherrypy.serving.response + + # Remove headers which applied to the original content, + # but do not apply to the error page. + respheaders = response.headers + for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After", + "Vary", "Content-Encoding", "Content-Length", "Expires", + "Content-Location", "Content-MD5", "Last-Modified"]: + if key in respheaders: + del respheaders[key] + + if status != 416: + # A server sending a response with status code 416 (Requested + # range not satisfiable) SHOULD include a Content-Range field + # with a byte-range-resp-spec of "*". The instance-length + # specifies the current length of the selected resource. + # A response with status code 206 (Partial Content) MUST NOT + # include a Content-Range field with a byte-range- resp-spec of "*". + if "Content-Range" in respheaders: + del respheaders["Content-Range"] + + +class HTTPError(CherryPyException): + """Exception used to return an HTTP error code (4xx-5xx) to the client. + + This exception can be used to automatically send a response using a http status + code, with an appropriate error page. It takes an optional + ``status`` argument (which must be between 400 and 599); it defaults to 500 + ("Internal Server Error"). It also takes an optional ``message`` argument, + which will be returned in the response body. See + `RFC 2616 `_ + for a complete list of available error codes and when to use them. + + Examples:: + + raise cherrypy.HTTPError(403) + raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.") + """ + + status = None + """The HTTP status code. May be of type int or str (with a Reason-Phrase).""" + + code = None + """The integer HTTP status code.""" + + reason = None + """The HTTP Reason-Phrase string.""" + + def __init__(self, status=500, message=None): + self.status = status + try: + self.code, self.reason, defaultmsg = _httputil.valid_status(status) + except ValueError: + raise self.__class__(500, _exc_info()[1].args[0]) + + if self.code < 400 or self.code > 599: + raise ValueError("status must be between 400 and 599.") + + # See http://www.python.org/dev/peps/pep-0352/ + # self.message = message + self._message = message or defaultmsg + CherryPyException.__init__(self, status, message) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent self. + + CherryPy uses this internally, but you can also use it to create an + HTTPError object and set its output without *raising* the exception. + """ + import cherrypy + + response = cherrypy.serving.response + + clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + tb = None + if cherrypy.serving.request.show_tracebacks: + tb = format_exc() + response.headers['Content-Type'] = "text/html;charset=utf-8" + response.headers.pop('Content-Length', None) + + content = ntob(self.get_error_page(self.status, traceback=tb, + message=self._message), 'utf-8') + response.body = content + + _be_ie_unfriendly(self.code) + + def get_error_page(self, *args, **kwargs): + return get_error_page(*args, **kwargs) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +class NotFound(HTTPError): + """Exception raised when a URL could not be mapped to any handler (404). + + This is equivalent to raising + :class:`HTTPError("404 Not Found") `. + """ + + def __init__(self, path=None): + if path is None: + import cherrypy + request = cherrypy.serving.request + path = request.script_name + request.path_info + self.args = (path,) + HTTPError.__init__(self, 404, "The path '%s' was not found." % path) + + +_HTTPErrorTemplate = ''' + + + + %(status)s + + + +

%(status)s

+

%(message)s

+
%(traceback)s
+
+ Powered by CherryPy %(version)s +
+ + +''' + +def get_error_page(status, **kwargs): + """Return an HTML page, containing a pretty error response. + + status should be an int or a str. + kwargs will be interpolated into the page template. + """ + import cherrypy + + try: + code, reason, message = _httputil.valid_status(status) + except ValueError: + raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) + + # We can't use setdefault here, because some + # callers send None for kwarg values. + if kwargs.get('status') is None: + kwargs['status'] = "%s %s" % (code, reason) + if kwargs.get('message') is None: + kwargs['message'] = message + if kwargs.get('traceback') is None: + kwargs['traceback'] = '' + if kwargs.get('version') is None: + kwargs['version'] = cherrypy.__version__ + + for k, v in iteritems(kwargs): + if v is None: + kwargs[k] = "" + else: + kwargs[k] = _escape(kwargs[k]) + + # Use a custom template or callable for the error page? + pages = cherrypy.serving.request.error_page + error_page = pages.get(code) or pages.get('default') + if error_page: + try: + if hasattr(error_page, '__call__'): + return error_page(**kwargs) + else: + data = open(error_page, 'rb').read() + return tonative(data) % kwargs + except: + e = _format_exception(*_exc_info())[-1] + m = kwargs['message'] + if m: + m += "
" + m += "In addition, the custom error page failed:\n
%s" % e + kwargs['message'] = m + + return _HTTPErrorTemplate % kwargs + + +_ie_friendly_error_sizes = { + 400: 512, 403: 256, 404: 512, 405: 256, + 406: 512, 408: 512, 409: 512, 410: 256, + 500: 512, 501: 512, 505: 512, + } + + +def _be_ie_unfriendly(status): + import cherrypy + response = cherrypy.serving.response + + # For some statuses, Internet Explorer 5+ shows "friendly error + # messages" instead of our response.body if the body is smaller + # than a given size. Fix this by returning a body over that size + # (by adding whitespace). + # See http://support.microsoft.com/kb/q218155/ + s = _ie_friendly_error_sizes.get(status, 0) + if s: + s += 1 + # Since we are issuing an HTTP error status, we assume that + # the entity is short, and we should just collapse it. + content = response.collapse_body() + l = len(content) + if l and l < s: + # IN ADDITION: the response must be written to IE + # in one chunk or it will still get replaced! Bah. + content = content + (ntob(" ") * (s - l)) + response.body = content + response.headers['Content-Length'] = str(len(content)) + + +def format_exc(exc=None): + """Return exc (or sys.exc_info if None), formatted.""" + try: + if exc is None: + exc = _exc_info() + if exc == (None, None, None): + return "" + import traceback + return "".join(traceback.format_exception(*exc)) + finally: + del exc + +def bare_error(extrabody=None): + """Produce status, headers, body for a critical error. + + Returns a triple without calling any other questionable functions, + so it should be as error-free as possible. Call it from an HTTP server + if you get errors outside of the request. + + If extrabody is None, a friendly but rather unhelpful error message + is set in the body. If extrabody is a string, it will be appended + as-is to the body. + """ + + # The whole point of this function is to be a last line-of-defense + # in handling errors. That is, it must not raise any errors itself; + # it cannot be allowed to fail. Therefore, don't add to it! + # In particular, don't call any other CP functions. + + body = ntob("Unrecoverable error in the server.") + if extrabody is not None: + if not isinstance(extrabody, bytestr): + extrabody = extrabody.encode('utf-8') + body += ntob("\n") + extrabody + + return (ntob("500 Internal Server Error"), + [(ntob('Content-Type'), ntob('text/plain')), + (ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))], + [body]) + + diff --git a/cherrypy/_cperror.pyc b/cherrypy/_cperror.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c8706f2a11e05b83ab1802a3f067ec18b353427 GIT binary patch literal 18499 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 O7$*zIhDMK#jr|WcGtvqG literal 0 HcmV?d00001 diff --git a/cherrypy/_cplogging.py b/cherrypy/_cplogging.py new file mode 100644 index 0000000..e10c942 --- /dev/null +++ b/cherrypy/_cplogging.py @@ -0,0 +1,440 @@ +""" +Simple config +============= + +Although CherryPy uses the :mod:`Python logging module `, it does so +behind the scenes so that simple logging is simple, but complicated logging +is still possible. "Simple" logging means that you can log to the screen +(i.e. console/stdout) or to a file, and that you can easily have separate +error and access log files. + +Here are the simplified logging settings. You use these by adding lines to +your config file or dict. You should set these at either the global level or +per application (see next), but generally not both. + + * ``log.screen``: Set this to True to have both "error" and "access" messages + printed to stdout. + * ``log.access_file``: Set this to an absolute filename where you want + "access" messages written. + * ``log.error_file``: Set this to an absolute filename where you want "error" + messages written. + +Many events are automatically logged; to log your own application events, call +:func:`cherrypy.log`. + +Architecture +============ + +Separate scopes +--------------- + +CherryPy provides log managers at both the global and application layers. +This means you can have one set of logging rules for your entire site, +and another set of rules specific to each application. The global log +manager is found at :func:`cherrypy.log`, and the log manager for each +application is found at :attr:`app.log`. +If you're inside a request, the latter is reachable from +``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain +a reference to the ``app``: either the return value of +:func:`tree.mount()` or, if you used +:func:`quickstart()` instead, via ``cherrypy.tree.apps['/']``. + +By default, the global logs are named "cherrypy.error" and "cherrypy.access", +and the application logs are named "cherrypy.error.2378745" and +"cherrypy.access.2378745" (the number is the id of the Application object). +This means that the application logs "bubble up" to the site logs, so if your +application has no log handlers, the site-level handlers will still log the +messages. + +Errors vs. Access +----------------- + +Each log manager handles both "access" messages (one per HTTP request) and +"error" messages (everything else). Note that the "error" log is not just for +errors! The format of access messages is highly formalized, but the error log +isn't--it receives messages from a variety of sources (including full error +tracebacks, if enabled). + + +Custom Handlers +=============== + +The simple settings above work by manipulating Python's standard :mod:`logging` +module. So when you need something more complex, the full power of the standard +module is yours to exploit. You can borrow or create custom handlers, formats, +filters, and much more. Here's an example that skips the standard FileHandler +and uses a RotatingFileHandler instead: + +:: + + #python + log = app.log + + # Remove the default FileHandlers if present. + log.error_file = "" + log.access_file = "" + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + fname = getattr(log, "rot_error_file", "error.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + fname = getattr(log, "rot_access_file", "access.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.access_log.addHandler(h) + + +The ``rot_*`` attributes are pulled straight from the application log object. +Since "log.*" config entries simply set attributes on the log object, you can +add custom attributes to your heart's content. Note that these handlers are +used ''instead'' of the default, simple handlers outlined above (so don't set +the "log.error_file" config entry, for example). +""" + +import datetime +import logging +# Silence the no-handlers "warning" (stderr write!) in stdlib logging +logging.Logger.manager.emittedNoHandlerWarning = 1 +logfmt = logging.Formatter("%(message)s") +import os +import sys + +import cherrypy +from cherrypy import _cperror +from cherrypy._cpcompat import ntob, py3k + + +class NullHandler(logging.Handler): + """A no-op logging handler to silence the logging.lastResort handler.""" + + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + +class LogManager(object): + """An object to assist both simple and advanced logging. + + ``cherrypy.log`` is an instance of this class. + """ + + appid = None + """The id() of the Application object which owns this log manager. If this + is a global log manager, appid is None.""" + + error_log = None + """The actual :class:`logging.Logger` instance for error messages.""" + + access_log = None + """The actual :class:`logging.Logger` instance for access messages.""" + + if py3k: + access_log_format = \ + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' + else: + access_log_format = \ + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + + logger_root = None + """The "top-level" logger name. + + This string will be used as the first segment in the Logger names. + The default is "cherrypy", for example, in which case the Logger names + will be of the form:: + + cherrypy.error. + cherrypy.access. + """ + + def __init__(self, appid=None, logger_root="cherrypy"): + self.logger_root = logger_root + self.appid = appid + if appid is None: + self.error_log = logging.getLogger("%s.error" % logger_root) + self.access_log = logging.getLogger("%s.access" % logger_root) + else: + self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid)) + self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid)) + self.error_log.setLevel(logging.INFO) + self.access_log.setLevel(logging.INFO) + + # Silence the no-handlers "warning" (stderr write!) in stdlib logging + self.error_log.addHandler(NullHandler()) + self.access_log.addHandler(NullHandler()) + + cherrypy.engine.subscribe('graceful', self.reopen_files) + + def reopen_files(self): + """Close and reopen all file handlers.""" + for log in (self.error_log, self.access_log): + for h in log.handlers: + if isinstance(h, logging.FileHandler): + h.acquire() + h.stream.close() + h.stream = open(h.baseFilename, h.mode) + h.release() + + def error(self, msg='', context='', severity=logging.INFO, traceback=False): + """Write the given ``msg`` to the error log. + + This is not just for errors! Applications may call this at any time + to log application-specific information. + + If ``traceback`` is True, the traceback of the current exception + (if any) will be appended to ``msg``. + """ + if traceback: + msg += _cperror.format_exc() + self.error_log.log(severity, ' '.join((self.time(), context, msg))) + + def __call__(self, *args, **kwargs): + """An alias for ``error``.""" + return self.error(*args, **kwargs) + + def access(self): + """Write to the access log (in Apache/NCSA Combined Log format). + + See http://httpd.apache.org/docs/2.0/logs.html#combined for format + details. + + CherryPy calls this automatically for you. Note there are no arguments; + it collects the data itself from + :class:`cherrypy.request`. + + Like Apache started doing in 2.0.46, non-printable and other special + characters in %r (and we expand that to all parts) are escaped using + \\xhh sequences, where hh stands for the hexadecimal representation + of the raw byte. Exceptions from this rule are " and \\, which are + escaped by prepending a backslash, and all whitespace characters, + which are written in their C-style notation (\\n, \\t, etc). + """ + request = cherrypy.serving.request + remote = request.remote + response = cherrypy.serving.response + outheaders = response.headers + inheaders = request.headers + if response.output_status is None: + status = "-" + else: + status = response.output_status.split(ntob(" "), 1)[0] + if py3k: + status = status.decode('ISO-8859-1') + + atoms = {'h': remote.name or remote.ip, + 'l': '-', + 'u': getattr(request, "login", None) or "-", + 't': self.time(), + 'r': request.request_line, + 's': status, + 'b': dict.get(outheaders, 'Content-Length', '') or "-", + 'f': dict.get(inheaders, 'Referer', ''), + 'a': dict.get(inheaders, 'User-Agent', ''), + } + if py3k: + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v + + try: + self.access_log.log(logging.INFO, self.access_log_format.format(**atoms)) + except: + self(traceback=True) + else: + for k, v in atoms.items(): + if isinstance(v, unicode): + v = v.encode('utf8') + elif not isinstance(v, str): + v = str(v) + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[1:-1] + # Escape double-quote. + atoms[k] = v.replace('"', '\\"') + + try: + self.access_log.log(logging.INFO, self.access_log_format % atoms) + except: + self(traceback=True) + + def time(self): + """Return now() in Apache Common Log Format (no timezone).""" + now = datetime.datetime.now() + monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + month = monthnames[now.month - 1].capitalize() + return ('[%02d/%s/%04d:%02d:%02d:%02d]' % + (now.day, month, now.year, now.hour, now.minute, now.second)) + + def _get_builtin_handler(self, log, key): + for h in log.handlers: + if getattr(h, "_cpbuiltin", None) == key: + return h + + + # ------------------------- Screen handlers ------------------------- # + + def _set_screen_handler(self, log, enable, stream=None): + h = self._get_builtin_handler(log, "screen") + if enable: + if not h: + if stream is None: + stream=sys.stderr + h = logging.StreamHandler(stream) + h.setFormatter(logfmt) + h._cpbuiltin = "screen" + log.addHandler(h) + elif h: + log.handlers.remove(h) + + def _get_screen(self): + h = self._get_builtin_handler + has_h = h(self.error_log, "screen") or h(self.access_log, "screen") + return bool(has_h) + + def _set_screen(self, newvalue): + self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) + self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) + screen = property(_get_screen, _set_screen, + doc="""Turn stderr/stdout logging on or off. + + If you set this to True, it'll add the appropriate StreamHandler for + you. If you set it to False, it will remove the handler. + """) + + # -------------------------- File handlers -------------------------- # + + def _add_builtin_file_handler(self, log, fname): + h = logging.FileHandler(fname) + h.setFormatter(logfmt) + h._cpbuiltin = "file" + log.addHandler(h) + + def _set_file_handler(self, log, filename): + h = self._get_builtin_handler(log, "file") + if filename: + if h: + if h.baseFilename != os.path.abspath(filename): + h.close() + log.handlers.remove(h) + self._add_builtin_file_handler(log, filename) + else: + self._add_builtin_file_handler(log, filename) + else: + if h: + h.close() + log.handlers.remove(h) + + def _get_error_file(self): + h = self._get_builtin_handler(self.error_log, "file") + if h: + return h.baseFilename + return '' + def _set_error_file(self, newvalue): + self._set_file_handler(self.error_log, newvalue) + error_file = property(_get_error_file, _set_error_file, + doc="""The filename for self.error_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """) + + def _get_access_file(self): + h = self._get_builtin_handler(self.access_log, "file") + if h: + return h.baseFilename + return '' + def _set_access_file(self, newvalue): + self._set_file_handler(self.access_log, newvalue) + access_file = property(_get_access_file, _set_access_file, + doc="""The filename for self.access_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """) + + # ------------------------- WSGI handlers ------------------------- # + + def _set_wsgi_handler(self, log, enable): + h = self._get_builtin_handler(log, "wsgi") + if enable: + if not h: + h = WSGIErrorHandler() + h.setFormatter(logfmt) + h._cpbuiltin = "wsgi" + log.addHandler(h) + elif h: + log.handlers.remove(h) + + def _get_wsgi(self): + return bool(self._get_builtin_handler(self.error_log, "wsgi")) + + def _set_wsgi(self, newvalue): + self._set_wsgi_handler(self.error_log, newvalue) + wsgi = property(_get_wsgi, _set_wsgi, + doc="""Write errors to wsgi.errors. + + If you set this to True, it'll add the appropriate + :class:`WSGIErrorHandler` for you + (which writes errors to ``wsgi.errors``). + If you set it to False, it will remove the handler. + """) + + +class WSGIErrorHandler(logging.Handler): + "A handler class which writes logging records to environ['wsgi.errors']." + + def flush(self): + """Flushes the stream.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + stream.flush() + + def emit(self, record): + """Emit a record.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + try: + msg = self.format(record) + fs = "%s\n" + import types + if not hasattr(types, "UnicodeType"): #if no unicode support... + stream.write(fs % msg) + else: + try: + stream.write(fs % msg) + except UnicodeError: + stream.write(fs % msg.encode("UTF-8")) + self.flush() + except: + self.handleError(record) diff --git a/cherrypy/_cplogging.pyc b/cherrypy/_cplogging.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e9be62908b60e0767c60852e54b0add5117c567 GIT binary patch literal 16178 zcmdU0UvwPTS--O@$=bDKCwAi4iIYs$RkTW_b(*G*QPO~;*hx+kH61y}sp_yh+L_gk zv^%q&J1c1u3)(t|(gXAXS}62zfEFHj=v!ZbC!TnK!#hu$!wVcOd|212^_^*Gu+E(dL8NYuGkN#&Ao>J>5E#;L|&r<8Cn0iUA zmy}mleo1YW)z*Y+PD*`Bc@yd$dY;I7POJ6ltX@&;m8@P>>s6^ws$Ny~Ppb8k%Hr%( zsyCzhvub@-^-rnwQ}}v9=02@DXr5LdD|Jt)udhF(yo&N4mao

lx*pEOejA8_seQ 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 K%%1zXy6``1s)Qo| literal 0 HcmV?d00001 diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py new file mode 100644 index 0000000..76ef6ea --- /dev/null +++ b/cherrypy/_cpmodpy.py @@ -0,0 +1,344 @@ +"""Native adapter for serving CherryPy via mod_python + +Basic usage: + +########################################## +# Application in a module called myapp.py +########################################## + +import cherrypy + +class Root: + @cherrypy.expose + def index(self): + return 'Hi there, Ho there, Hey there' + + +# We will use this method from the mod_python configuration +# as the entry point to our application +def setup_server(): + cherrypy.tree.mount(Root()) + cherrypy.config.update({'environment': 'production', + 'log.screen': False, + 'show_tracebacks': False}) + +########################################## +# mod_python settings for apache2 +# This should reside in your httpd.conf +# or a file that will be loaded at +# apache startup +########################################## + +# Start +DocumentRoot "/" +Listen 8080 +LoadModule python_module /usr/lib/apache2/modules/mod_python.so + + + PythonPath "sys.path+['/path/to/my/application']" + SetHandler python-program + PythonHandler cherrypy._cpmodpy::handler + PythonOption cherrypy.setup myapp::setup_server + PythonDebug On + +# End + +The actual path to your mod_python.so is dependent on your +environment. In this case we suppose a global mod_python +installation on a Linux distribution such as Ubuntu. + +We do set the PythonPath configuration setting so that +your application can be found by from the user running +the apache2 instance. Of course if your application +resides in the global site-package this won't be needed. + +Then restart apache2 and access http://127.0.0.1:8080 +""" + +import logging +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO, copyitems, ntob +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +# ------------------------------ Request-handling + + + +def setup(req): + from mod_python import apache + + # Run any setup functions defined by a "PythonOption cherrypy.setup" directive. + options = req.get_options() + if 'cherrypy.setup' in options: + for function in options['cherrypy.setup'].split(): + atoms = function.split('::', 1) + if len(atoms) == 1: + mod = __import__(atoms[0], globals(), locals()) + else: + modname, fname = atoms + mod = __import__(modname, globals(), locals(), [fname]) + func = getattr(mod, fname) + func() + + cherrypy.config.update({'log.screen': False, + "tools.ignore_headers.on": True, + "tools.ignore_headers.headers": ['Range'], + }) + + engine = cherrypy.engine + if hasattr(engine, "signal_handler"): + engine.signal_handler.unsubscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.unsubscribe() + engine.autoreload.unsubscribe() + cherrypy.server.unsubscribe() + + def _log(msg, level): + newlevel = apache.APLOG_ERR + if logging.DEBUG >= level: + newlevel = apache.APLOG_DEBUG + elif logging.INFO >= level: + newlevel = apache.APLOG_INFO + elif logging.WARNING >= level: + newlevel = apache.APLOG_WARNING + # On Windows, req.server is required or the msg will vanish. See + # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html. + # Also, "When server is not specified...LogLevel does not apply..." + apache.log_error(msg, newlevel, req.server) + engine.subscribe('log', _log) + + engine.start() + + def cherrypy_cleanup(data): + engine.exit() + try: + # apache.register_cleanup wasn't available until 3.1.4. + apache.register_cleanup(cherrypy_cleanup) + except AttributeError: + req.server.register_cleanup(req, cherrypy_cleanup) + + +class _ReadOnlyRequest: + expose = ('read', 'readline', 'readlines') + def __init__(self, req): + for method in self.expose: + self.__dict__[method] = getattr(req, method) + + +recursive = False + +_isSetUp = False +def handler(req): + from mod_python import apache + try: + global _isSetUp + if not _isSetUp: + setup(req) + _isSetUp = True + + # Obtain a Request object from CherryPy + local = req.connection.local_addr + local = httputil.Host(local[0], local[1], req.connection.local_host or "") + remote = req.connection.remote_addr + remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "") + + scheme = req.parsed_uri[0] or 'http' + req.get_basic_auth_pw() + + try: + # apache.mpm_query only became available in mod_python 3.1 + q = apache.mpm_query + threaded = q(apache.AP_MPMQ_IS_THREADED) + forked = q(apache.AP_MPMQ_IS_FORKED) + except AttributeError: + bad_value = ("You must provide a PythonOption '%s', " + "either 'on' or 'off', when running a version " + "of mod_python < 3.1") + + threaded = options.get('multithread', '').lower() + if threaded == 'on': + threaded = True + elif threaded == 'off': + threaded = False + else: + raise ValueError(bad_value % "multithread") + + forked = options.get('multiprocess', '').lower() + if forked == 'on': + forked = True + elif forked == 'off': + forked = False + else: + raise ValueError(bad_value % "multiprocess") + + sn = cherrypy.tree.script_name(req.uri or "/") + if sn is None: + send_response(req, '404 Not Found', [], '') + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.uri + qs = req.args or "" + reqproto = req.protocol + headers = copyitems(req.headers_in) + rfile = _ReadOnlyRequest(req) + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving(local, remote, scheme, + "HTTP/1.1") + request.login = req.user + request.multithread = bool(threaded) + request.multiprocess = bool(forked) + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, reqproto, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not recursive: + if ir.path in redirections: + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % ir.path) + else: + # Add the *previous* path_info + qs to redirections. + if qs: + qs = "?" + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = "GET" + path = ir.path + qs = ir.query_string + rfile = BytesIO() + + send_response(req, response.output_status, response.header_list, + response.body, response.stream) + finally: + app.release_serving() + except: + tb = format_exc() + cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) + s, h, b = bare_error() + send_response(req, s, h, b) + return apache.OK + + +def send_response(req, status, headers, body, stream=False): + # Set response status + req.status = int(status[:3]) + + # Set response headers + req.content_type = "text/plain" + for header, value in headers: + if header.lower() == 'content-type': + req.content_type = value + continue + req.headers_out.add(header, value) + + if stream: + # Flush now so the status and headers are sent immediately. + req.flush() + + # Set response body + if isinstance(body, basestring): + req.write(body) + else: + for seg in body: + req.write(seg) + + + +# --------------- Startup tools for CherryPy + mod_python --------------- # + + +import os +import re +try: + import subprocess + def popen(fullcmd): + p = subprocess.Popen(fullcmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=True) + return p.stdout +except ImportError: + def popen(fullcmd): + pipein, pipeout = os.popen4(fullcmd) + return pipeout + + +def read_process(cmd, args=""): + fullcmd = "%s %s" % (cmd, args) + pipeout = popen(fullcmd) + try: + firstline = pipeout.readline() + if (re.search(ntob("(not recognized|No such file|not found)"), firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +class ModPythonServer(object): + + template = """ +# Apache2 server configuration file for running CherryPy with mod_python. + +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + + + SetHandler python-program + PythonHandler %(handler)s + PythonDebug On +%(opts)s + +""" + + def __init__(self, loc="/", port=80, opts=None, apache_path="apache", + handler="cherrypy._cpmodpy::handler"): + self.loc = loc + self.port = port + self.opts = opts + self.apache_path = apache_path + self.handler = handler + + def start(self): + opts = "".join([" PythonOption %s %s\n" % (k, v) + for k, v in self.opts]) + conf_data = self.template % {"port": self.port, + "loc": self.loc, + "opts": opts, + "handler": self.handler, + } + + mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf") + f = open(mpconf, 'wb') + try: + f.write(conf_data) + finally: + f.close() + + response = read_process(self.apache_path, "-k start -f %s" % mpconf) + self.ready = True + return response + + def stop(self): + os.popen("apache -k stop") + self.ready = False + diff --git a/cherrypy/_cpnative_server.py b/cherrypy/_cpnative_server.py new file mode 100644 index 0000000..57f715a --- /dev/null +++ b/cherrypy/_cpnative_server.py @@ -0,0 +1,149 @@ +"""Native adapter for serving CherryPy via its builtin server.""" + +import logging +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil +from cherrypy import wsgiserver + + +class NativeGateway(wsgiserver.Gateway): + + recursive = False + + def respond(self): + req = self.req + try: + # Obtain a Request object from CherryPy + local = req.server.bind_addr + local = httputil.Host(local[0], local[1], "") + remote = req.conn.remote_addr, req.conn.remote_port + remote = httputil.Host(remote[0], remote[1], "") + + scheme = req.scheme + sn = cherrypy.tree.script_name(req.uri or "/") + if sn is None: + self.send_response('404 Not Found', [], ['']) + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.path + qs = req.qs or "" + headers = req.inheaders.items() + rfile = req.rfile + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving( + local, remote, scheme, "HTTP/1.1") + request.multithread = True + request.multiprocess = False + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, req.request_protocol, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not self.recursive: + if ir.path in redirections: + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % ir.path) + else: + # Add the *previous* path_info + qs to redirections. + if qs: + qs = "?" + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = "GET" + path = ir.path + qs = ir.query_string + rfile = BytesIO() + + self.send_response( + response.output_status, response.header_list, + response.body) + finally: + app.release_serving() + except: + tb = format_exc() + #print tb + cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) + s, h, b = bare_error() + self.send_response(s, h, b) + + def send_response(self, status, headers, body): + req = self.req + + # Set response status + req.status = str(status or "500 Server Error") + + # Set response headers + for header, value in headers: + req.outheaders.append((header, value)) + if (req.ready and not req.sent_headers): + req.sent_headers = True + req.send_headers() + + # Set response body + for seg in body: + req.write(seg) + + +class CPHTTPServer(wsgiserver.HTTPServer): + """Wrapper for wsgiserver.HTTPServer. + + wsgiserver has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. + Therefore, we wrap it here, so we can apply some attributes + from config -> cherrypy.server -> HTTPServer. + """ + + def __init__(self, server_adapter=cherrypy.server): + self.server_adapter = server_adapter + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + wsgiserver.HTTPServer.__init__( + self, server_adapter.bind_addr, NativeGateway, + minthreads=server_adapter.thread_pool, + maxthreads=server_adapter.thread_pool_max, + server_name=server_name) + + self.max_request_header_size = self.server_adapter.max_request_header_size or 0 + self.max_request_body_size = self.server_adapter.max_request_body_size or 0 + self.request_queue_size = self.server_adapter.socket_queue_size + self.timeout = self.server_adapter.socket_timeout + self.shutdown_timeout = self.server_adapter.shutdown_timeout + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + + diff --git a/cherrypy/_cpreqbody.py b/cherrypy/_cpreqbody.py new file mode 100644 index 0000000..5d72c85 --- /dev/null +++ b/cherrypy/_cpreqbody.py @@ -0,0 +1,965 @@ +"""Request body processing for CherryPy. + +.. versionadded:: 3.2 + +Application authors have complete control over the parsing of HTTP request +entities. In short, :attr:`cherrypy.request.body` +is now always set to an instance of :class:`RequestBody`, +and *that* class is a subclass of :class:`Entity`. + +When an HTTP request includes an entity body, it is often desirable to +provide that information to applications in a form other than the raw bytes. +Different content types demand different approaches. Examples: + + * For a GIF file, we want the raw bytes in a stream. + * An HTML form is better parsed into its component fields, and each text field + decoded from bytes to unicode. + * A JSON body should be deserialized into a Python dict or list. + +When the request contains a Content-Type header, the media type is used as a +key to look up a value in the +:attr:`request.body.processors` dict. +If the full media +type is not found, then the major type is tried; for example, if no processor +is found for the 'image/jpeg' type, then we look for a processor for the 'image' +types altogether. If neither the full type nor the major type has a matching +processor, then a default processor is used +(:func:`default_proc`). For most +types, this means no processing is done, and the body is left unread as a +raw byte stream. Processors are configurable in an 'on_start_resource' hook. + +Some processors, especially those for the 'text' types, attempt to decode bytes +to unicode. If the Content-Type request header includes a 'charset' parameter, +this is used to decode the entity. Otherwise, one or more default charsets may +be attempted, although this decision is up to each processor. If a processor +successfully decodes an Entity or Part, it should set the +:attr:`charset` attribute +on the Entity or Part to the name of the successful charset, so that +applications can easily re-encode or transcode the value if they wish. + +If the Content-Type of the request entity is of major type 'multipart', then +the above parsing process, and possibly a decoding process, is performed for +each part. + +For both the full entity and multipart parts, a Content-Disposition header may +be used to fill :attr:`name` and +:attr:`filename` attributes on the +request.body or the Part. + +.. _custombodyprocessors: + +Custom Processors +================= + +You can add your own processors for any specific or major MIME type. Simply add +it to the :attr:`processors` dict in a +hook/tool that runs at ``on_start_resource`` or ``before_request_body``. +Here's the built-in JSON tool for an example:: + + def json_in(force=True, debug=False): + request = cherrypy.serving.request + def json_processor(entity): + \"""Read application/json data into request.json.\""" + if not entity.headers.get("Content-Length", ""): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + request.json = json_decode(body) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + if force: + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an application/json content type') + request.body.processors['application/json'] = json_processor + +We begin by defining a new ``json_processor`` function to stick in the ``processors`` +dictionary. All processor functions take a single argument, the ``Entity`` instance +they are to process. It will be called whenever a request is received (for those +URI's where the tool is turned on) which has a ``Content-Type`` of +"application/json". + +First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then +reads the remaining bytes on the socket. The ``fp`` object knows its own length, so +it won't hang waiting for data that never arrives. It will return when all data +has been read. Then, we decode those bytes using Python's built-in ``json`` module, +and stick the decoded result onto ``request.json`` . If it cannot be decoded, we +raise 400. + +If the "force" argument is True (the default), the ``Tool`` clears the ``processors`` +dict so that request entities of other ``Content-Types`` aren't parsed at all. Since +there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body`` +is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.) +But in our case, we want to raise 415, so we replace ``request.body.default_proc`` +with the error (``HTTPError`` instances, when called, raise themselves). + +If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry:: + + request.body.processors = {'application/json': json_processor} + +Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one. +""" + +try: + from io import DEFAULT_BUFFER_SIZE +except ImportError: + DEFAULT_BUFFER_SIZE = 8192 +import re +import sys +import tempfile +try: + from urllib import unquote_plus +except ImportError: + def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(ntob('+'), ntob(' ')) + atoms = bs.split(ntob('%')) + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return ntob('').join(atoms) + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, ntou +from cherrypy.lib import httputil + + +# -------------------------------- Processors -------------------------------- # + +def process_urlencoded(entity): + """Read application/x-www-form-urlencoded data into entity.params.""" + qs = entity.fp.read() + for charset in entity.attempt_charsets: + try: + params = {} + for aparam in qs.split(ntob('&')): + for pair in aparam.split(ntob(';')): + if not pair: + continue + + atoms = pair.split(ntob('='), 1) + if len(atoms) == 1: + atoms.append(ntob('')) + + key = unquote_plus(atoms[0]).decode(charset) + value = unquote_plus(atoms[1]).decode(charset) + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + except UnicodeDecodeError: + pass + else: + entity.charset = charset + break + else: + raise cherrypy.HTTPError( + 400, "The request entity could not be decoded. The following " + "charsets were attempted: %s" % repr(entity.attempt_charsets)) + + # Now that all values have been successfully parsed and decoded, + # apply them to the entity.params dict. + for key, value in params.items(): + if key in entity.params: + if not isinstance(entity.params[key], list): + entity.params[key] = [entity.params[key]] + entity.params[key].append(value) + else: + entity.params[key] = value + + +def process_multipart(entity): + """Read all multipart parts into entity.parts.""" + ib = "" + if 'boundary' in entity.content_type.params: + # http://tools.ietf.org/html/rfc2046#section-5.1.1 + # "The grammar for parameters on the Content-type field is such that it + # is often necessary to enclose the boundary parameter values in quotes + # on the Content-type line" + ib = entity.content_type.params['boundary'].strip('"') + + if not re.match("^[ -~]{0,200}[!-~]$", ib): + raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) + + ib = ('--' + ib).encode('ascii') + + # Find the first marker + while True: + b = entity.readline() + if not b: + return + + b = b.strip() + if b == ib: + break + + # Read all parts + while True: + part = entity.part_class.from_fp(entity.fp, ib) + entity.parts.append(part) + part.process() + if part.fp.done: + break + +def process_multipart_form_data(entity): + """Read all multipart/form-data parts into entity.parts or entity.params.""" + process_multipart(entity) + + kept_parts = [] + for part in entity.parts: + if part.name is None: + kept_parts.append(part) + else: + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if part.name in entity.params: + if not isinstance(entity.params[part.name], list): + entity.params[part.name] = [entity.params[part.name]] + entity.params[part.name].append(value) + else: + entity.params[part.name] = value + + entity.parts = kept_parts + +def _old_process_multipart(entity): + """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" + process_multipart(entity) + + params = entity.params + + for part in entity.parts: + if part.name is None: + key = ntou('parts') + else: + key = part.name + + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + + + +# --------------------------------- Entities --------------------------------- # + + +class Entity(object): + """An HTTP request body, or MIME multipart body. + + This class collects information about the HTTP request entity. When a + given entity is of MIME type "multipart", each part is parsed into its own + Entity instance, and the set of parts stored in + :attr:`entity.parts`. + + Between the ``before_request_body`` and ``before_handler`` tools, CherryPy + tries to process the request body (if any) by calling + :func:`request.body.process`, a dict. + If a matching processor cannot be found for the complete Content-Type, + it tries again using the major type. For example, if a request with an + entity of type "image/jpeg" arrives, but no processor can be found for + that complete type, then one is sought for the major type "image". If a + processor is still not found, then the + :func:`default_proc` method of the + Entity is called (which does nothing by default; you can override this too). + + CherryPy includes processors for the "application/x-www-form-urlencoded" + type, the "multipart/form-data" type, and the "multipart" major type. + CherryPy 3.2 processes these types almost exactly as older versions. + Parts are passed as arguments to the page handler using their + ``Content-Disposition.name`` if given, otherwise in a generic "parts" + argument. Each such part is either a string, or the + :class:`Part` itself if it's a file. (In this + case it will have ``file`` and ``filename`` attributes, or possibly a + ``value`` attribute). Each Part is itself a subclass of + Entity, and has its own ``process`` method and ``processors`` dict. + + There is a separate processor for the "multipart" major type which is more + flexible, and simply stores all multipart parts in + :attr:`request.body.parts`. You can + enable it with:: + + cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart + + in an ``on_start_resource`` tool. + """ + + # http://tools.ietf.org/html/rfc2046#section-4.1.2: + # "The default character set, which must be assumed in the + # absence of a charset parameter, is US-ASCII." + # However, many browsers send data in utf-8 with no charset. + attempt_charsets = ['utf-8'] + """A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + charset = None + """The successful decoding; see "attempt_charsets" above.""" + + content_type = None + """The value of the Content-Type request header. + + If the Entity is part of a multipart payload, this will be the Content-Type + given in the MIME headers for this part. + """ + + default_content_type = 'application/x-www-form-urlencoded' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + filename = None + """The ``Content-Disposition.filename`` header, if available.""" + + fp = None + """The readable socket file object.""" + + headers = None + """A dict of request/multipart header names and values. + + This is a copy of the ``request.headers`` for the ``request.body``; + for multipart parts, it is the set of headers for that part. + """ + + length = None + """The value of the ``Content-Length`` header, if provided.""" + + name = None + """The "name" parameter of the ``Content-Disposition`` header, if any.""" + + params = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True).""" + + processors = {'application/x-www-form-urlencoded': process_urlencoded, + 'multipart/form-data': process_multipart_form_data, + 'multipart': process_multipart, + } + """A dict of Content-Type names to processor methods.""" + + parts = None + """A list of Part instances if ``Content-Type`` is of major type "multipart".""" + + part_class = None + """The class used for multipart parts. + + You can replace this with custom subclasses to alter the processing of + multipart parts. + """ + + def __init__(self, fp, headers, params=None, parts=None): + # Make an instance-specific copy of the class processors + # so Tools, etc. can replace them per-request. + self.processors = self.processors.copy() + + self.fp = fp + self.headers = headers + + if params is None: + params = {} + self.params = params + + if parts is None: + parts = [] + self.parts = parts + + # Content-Type + self.content_type = headers.elements('Content-Type') + if self.content_type: + self.content_type = self.content_type[0] + else: + self.content_type = httputil.HeaderElement.from_str( + self.default_content_type) + + # Copy the class 'attempt_charsets', prepending any Content-Type charset + dec = self.content_type.params.get("charset", None) + if dec: + self.attempt_charsets = [dec] + [c for c in self.attempt_charsets + if c != dec] + else: + self.attempt_charsets = self.attempt_charsets[:] + + # Length + self.length = None + clen = headers.get('Content-Length', None) + # If Transfer-Encoding is 'chunked', ignore any Content-Length. + if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''): + try: + self.length = int(clen) + except ValueError: + pass + + # Content-Disposition + self.name = None + self.filename = None + disp = headers.elements('Content-Disposition') + if disp: + disp = disp[0] + if 'name' in disp.params: + self.name = disp.params['name'] + if self.name.startswith('"') and self.name.endswith('"'): + self.name = self.name[1:-1] + if 'filename' in disp.params: + self.filename = disp.params['filename'] + if self.filename.startswith('"') and self.filename.endswith('"'): + self.filename = self.filename[1:-1] + + # The 'type' attribute is deprecated in 3.2; remove it in 3.3. + type = property(lambda self: self.content_type, + doc="""A deprecated alias for :attr:`content_type`.""") + + def read(self, size=None, fp_out=None): + return self.fp.read(size, fp_out) + + def readline(self, size=None): + return self.fp.readline(size) + + def readlines(self, sizehint=None): + return self.fp.readlines(sizehint) + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def next(self): + return self.__next__() + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). Return fp_out.""" + if fp_out is None: + fp_out = self.make_file() + self.read(fp_out=fp_out) + return fp_out + + def make_file(self): + """Return a file-like object into which the request body will be read. + + By default, this will return a TemporaryFile. Override as needed. + See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" + return tempfile.TemporaryFile() + + def fullvalue(self): + """Return this entity as a string, whether stored in a file or not.""" + if self.file: + # It was stored in a tempfile. Read it. + self.file.seek(0) + value = self.file.read() + self.file.seek(0) + else: + value = self.value + return value + + def process(self): + """Execute the best-match processor for the given media type.""" + proc = None + ct = self.content_type.value + try: + proc = self.processors[ct] + except KeyError: + toptype = ct.split('/', 1)[0] + try: + proc = self.processors[toptype] + except KeyError: + pass + if proc is None: + self.default_proc() + else: + proc(self) + + def default_proc(self): + """Called if a more-specific processor is not found for the ``Content-Type``.""" + # Leave the fp alone for someone else to read. This works fine + # for request.body, but the Part subclasses need to override this + # so they can move on to the next part. + pass + + +class Part(Entity): + """A MIME part entity, part of a multipart entity.""" + + # "The default character set, which must be assumed in the absence of a + # charset parameter, is US-ASCII." + attempt_charsets = ['us-ascii', 'utf-8'] + """A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + boundary = None + """The MIME multipart boundary.""" + + default_content_type = 'text/plain' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however (this class), + the MIME spec declares that a part with no Content-Type defaults to + "text/plain". + """ + + # This is the default in stdlib cgi. We may want to increase it. + maxrambytes = 1000 + """The threshold of bytes after which point the ``Part`` will store its data + in a file (generated by :func:`make_file`) + instead of a string. Defaults to 1000, just like the :mod:`cgi` module in + Python's standard library. + """ + + def __init__(self, fp, headers, boundary): + Entity.__init__(self, fp, headers) + self.boundary = boundary + self.file = None + self.value = None + + def from_fp(cls, fp, boundary): + headers = cls.read_headers(fp) + return cls(fp, headers, boundary) + from_fp = classmethod(from_fp) + + def read_headers(cls, fp): + headers = httputil.HeaderMap() + while True: + line = fp.readline() + if not line: + # No more data--illegal end of headers + raise EOFError("Illegal end of headers.") + + if line == ntob('\r\n'): + # Normal end of headers + break + if not line.endswith(ntob('\r\n')): + raise ValueError("MIME requires CRLF terminators: %r" % line) + + if line[0] in ntob(' \t'): + # It's a continuation line. + v = line.strip().decode('ISO-8859-1') + else: + k, v = line.split(ntob(":"), 1) + k = k.strip().decode('ISO-8859-1') + v = v.strip().decode('ISO-8859-1') + + existing = headers.get(k) + if existing: + v = ", ".join((existing, v)) + headers[k] = v + + return headers + read_headers = classmethod(read_headers) + + def read_lines_to_boundary(self, fp_out=None): + """Read bytes from self.fp and return or write them to a file. + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like object that + supports the 'write' method; all bytes read will be written to the fp, + and that fp is returned. + """ + endmarker = self.boundary + ntob("--") + delim = ntob("") + prev_lf = True + lines = [] + seen = 0 + while True: + line = self.fp.readline(1<<16) + if not line: + raise EOFError("Illegal end of multipart body.") + if line.startswith(ntob("--")) and prev_lf: + strippedline = line.strip() + if strippedline == self.boundary: + break + if strippedline == endmarker: + self.fp.finish() + break + + line = delim + line + + if line.endswith(ntob("\r\n")): + delim = ntob("\r\n") + line = line[:-2] + prev_lf = True + elif line.endswith(ntob("\n")): + delim = ntob("\n") + line = line[:-1] + prev_lf = True + else: + delim = ntob("") + prev_lf = False + + if fp_out is None: + lines.append(line) + seen += len(line) + if seen > self.maxrambytes: + fp_out = self.make_file() + for line in lines: + fp_out.write(line) + else: + fp_out.write(line) + + if fp_out is None: + result = ntob('').join(lines) + for charset in self.attempt_charsets: + try: + result = result.decode(charset) + except UnicodeDecodeError: + pass + else: + self.charset = charset + return result + else: + raise cherrypy.HTTPError( + 400, "The request entity could not be decoded. The following " + "charsets were attempted: %s" % repr(self.attempt_charsets)) + else: + fp_out.seek(0) + return fp_out + + def default_proc(self): + """Called if a more-specific processor is not found for the ``Content-Type``.""" + if self.filename: + # Always read into a file if a .filename was given. + self.file = self.read_into_file() + else: + result = self.read_lines_to_boundary() + if isinstance(result, basestring): + self.value = result + else: + self.file = result + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). Return fp_out.""" + if fp_out is None: + fp_out = self.make_file() + self.read_lines_to_boundary(fp_out=fp_out) + return fp_out + +Entity.part_class = Part + +try: + inf = float('inf') +except ValueError: + # Python 2.4 and lower + class Infinity(object): + def __cmp__(self, other): + return 1 + def __sub__(self, other): + return self + inf = Infinity() + + +comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding', + 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection', + 'Content-Encoding', 'Content-Language', 'Expect', 'If-Match', + 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer', + 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate'] + + +class SizedReader: + + def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False): + # Wrap our fp in a buffer so peek() works + self.fp = fp + self.length = length + self.maxbytes = maxbytes + self.buffer = ntob('') + self.bufsize = bufsize + self.bytes_read = 0 + self.done = False + self.has_trailers = has_trailers + + def read(self, size=None, fp_out=None): + """Read bytes from the request body and return or write them to a file. + + A number of bytes less than or equal to the 'size' argument are read + off the socket. The actual number of bytes read are tracked in + self.bytes_read. The number may be smaller than 'size' when 1) the + client sends fewer bytes, 2) the 'Content-Length' request header + specifies fewer bytes than requested, or 3) the number of bytes read + exceeds self.maxbytes (in which case, 413 is raised). + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like object that + supports the 'write' method; all bytes read will be written to the fp, + and None is returned. + """ + + if self.length is None: + if size is None: + remaining = inf + else: + remaining = size + else: + remaining = self.length - self.bytes_read + if size and size < remaining: + remaining = size + if remaining == 0: + self.finish() + if fp_out is None: + return ntob('') + else: + return None + + chunks = [] + + # Read bytes from the buffer. + if self.buffer: + if remaining is inf: + data = self.buffer + self.buffer = ntob('') + else: + data = self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + # Read bytes from the socket. + while remaining > 0: + chunksize = min(remaining, self.bufsize) + try: + data = self.fp.read(chunksize) + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, "Maximum request length: %r" % e.args[1]) + else: + raise + if not data: + self.finish() + break + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + if fp_out is None: + return ntob('').join(chunks) + + def readline(self, size=None): + """Read a line from the request body and return it.""" + chunks = [] + while size is None or size > 0: + chunksize = self.bufsize + if size is not None and size < self.bufsize: + chunksize = size + data = self.read(chunksize) + if not data: + break + pos = data.find(ntob('\n')) + 1 + if pos: + chunks.append(data[:pos]) + remainder = data[pos:] + self.buffer += remainder + self.bytes_read -= len(remainder) + break + else: + chunks.append(data) + return ntob('').join(chunks) + + def readlines(self, sizehint=None): + """Read lines from the request body and return them.""" + if self.length is not None: + if sizehint is None: + sizehint = self.length - self.bytes_read + else: + sizehint = min(sizehint, self.length - self.bytes_read) + + lines = [] + seen = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + seen += len(line) + if seen >= sizehint: + break + return lines + + def finish(self): + self.done = True + if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): + self.trailers = {} + + try: + for line in self.fp.read_trailer_lines(): + if line[0] in ntob(' \t'): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(ntob(":"), 1) + except ValueError: + raise ValueError("Illegal header line.") + k = k.strip().title() + v = v.strip() + + if k in comma_separated_headers: + existing = self.trailers.get(envname) + if existing: + v = ntob(", ").join((existing, v)) + self.trailers[k] = v + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, "Maximum request length: %r" % e.args[1]) + else: + raise + + +class RequestBody(Entity): + """The entity of the HTTP request.""" + + bufsize = 8 * 1024 + """The buffer size used when reading the socket.""" + + # Don't parse the request body at all if the client didn't provide + # a Content-Type header. See http://www.cherrypy.org/ticket/790 + default_content_type = '' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + maxbytes = None + """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket.""" + + def __init__(self, fp, headers, params=None, request_params=None): + Entity.__init__(self, fp, headers, params) + + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 + # When no explicit charset parameter is provided by the + # sender, media subtypes of the "text" type are defined + # to have a default charset value of "ISO-8859-1" when + # received via HTTP. + if self.content_type.value.startswith('text/'): + for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): + if c in self.attempt_charsets: + break + else: + self.attempt_charsets.append('ISO-8859-1') + + # Temporary fix while deprecating passing .parts as .params. + self.processors['multipart'] = _old_process_multipart + + if request_params is None: + request_params = {} + self.request_params = request_params + + def process(self): + """Process the request entity based on its Content-Type.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # It is possible to send a POST request with no body, for example; + # however, app developers are responsible in that case to set + # cherrypy.request.process_body to False so this method isn't called. + h = cherrypy.serving.request.headers + if 'Content-Length' not in h and 'Transfer-Encoding' not in h: + raise cherrypy.HTTPError(411) + + self.fp = SizedReader(self.fp, self.length, + self.maxbytes, bufsize=self.bufsize, + has_trailers='Trailer' in h) + super(RequestBody, self).process() + + # Body params should also be a part of the request_params + # add them in here. + request_params = self.request_params + for key, value in self.params.items(): + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if sys.version_info < (3, 0): + if isinstance(key, unicode): + key = key.encode('ISO-8859-1') + + if key in request_params: + if not isinstance(request_params[key], list): + request_params[key] = [request_params[key]] + request_params[key].append(value) + else: + request_params[key] = value diff --git a/cherrypy/_cpreqbody.pyc b/cherrypy/_cpreqbody.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c827cc4ea64c5b7f8478e48be6606c1a345f66db GIT binary patch literal 27032 zcmeHwZH!#kdEU9R-@{!lx%^y-maf*acWLjAs1MsRMKR*1MTJ@sLrN>JsLamJ+})k! zeC@q=#9dod(@-^+1Pud`+aN_nh;7zwfyk_@5KS6Tkb5i#3=0Ifj2+vtD$6@zAh9B+kbj&S}siP6sEV|Y{x4h4_ z#@+I`YfZT23FmR}QPT9a;h64ztuI}Wx%z|)4!bqHxu19L zI`{eIBd$K_f+Mo3>IdCj&n-{8rS>U6|A^M(CYHmiPo*{l^e$v%H;nt72WwigKTf=V6XG7urg%ti$b{Aa^y6baKaeG^boht`h zy&#JH)lPlK?}nXP5Jiply1&*5{R>%KjZPT(8`XCLzt(AWn?cM zLj#}~wBttH2%?gIrR_)PDW3OFSK~N5y;2h#-JMd>wnSjhWOpmIZu%gZjT)R?DK;X% z-MQ^oo42bwksk%IA9ws}+i$d^xZ17-1mbk9S&gF8E6J4;yE0#_w(I^U z;*Dzj311!f7^~_>y;ZyF>-Z8Ub`QAf(;l5&!2&OB1X#lMt~CLiwPvp#py#%)_1V!r z&HIg*<8{_zyoBeCu)5j|Fu5Xj@SR2-RX~QTQ2@C&oq;FE-ZJf)E317x{0z(*-p3pvCFev&ZN;>{PKK*u6{JRc?KBx>)o-;a>(C zRsGLhx$Lhsn!&t(JMeE;*-`&U222!(LA6!lTj#hAuU<9yFwxZ@jsYW4C#a(}Cew%` zAz7zQxYim$vmVW3o$7u7K>Rq^PVN?c9{|@nz*>JT?6hohM1)?u!P_>X|H}2(7L5vk ze7$BJgK}wuuu*L`zK{-E^{?#!&9Ul@TI^$T%|;Zb+owq-^hCU`0?#n}3#bCfg&SBr ze!OEus?A=&Rl*3xgeX#!7a}Eu z3-a4b7JYbD%3GaX(F97xD{F#et=DY&0;k9=COGX57O>N6*9EKLMytAsDW@&suo2W> z5T^)i(}9?4croQ8osbl$PSlF)GuvoY*Mp}vyTSUbdP=$nVrahPJJq~{J@3s5JgiYO z?yLvsF9f^5kU@j>d@2I5O;|BxAJiLMU)+dm8{k2t7V^7}?FT8M9;{V+&A1PMWM_(V zr`LMz+Ub>~MTPHv&|>$uJG*kSBvNQ~qS&UuX`|~_Pz8?UOGeI!+jXF|VKXPdJ>=bH zuoeSrK_&^Jq_pZMwBuh(CxmH+V!msQ^`3DDQheK=?X)Xkeqme*gQ(LBYr(9)0Yn21 zU+=WQp0gh2{UGWFHK2bJ=(y2|f_yVbUbcNCIuK{8E8c2^W+YbZBQb6gXR?bF5)4jA z0_dGGJAbyeK~l#MAo*$wawMEDYQ2)(AbBKXv0G3VSZv9E4Hx00L6h&p!#(%4(`OeI?zAzdGq7NcH`h(t2lNdUtR zwEas0jDTFjl8bLA+(p7Dr{)B-4??J<2}qYxv9a2VgCaoW{C5vSczL&tm7|1XJqOTF z`kD75HmkrYsDo(96Xi)(yn5x;OJegS|9S&@ z0XD2&FG77ua5NESIY@gKjml}s@cUl*CB>mg_VrZUfs!e$C+tz`#{SC6J*;kJg^;bR ztOkHM00)%rTj8Rttd#uX%g}aaBVopBuhEPbz%!*6sCS#CpUAq?RB)tQtofS}hHA9u z(4rQ+cq8nA0o8-m-ujD|tIa4ld0H*mIwgb`L55VWq0~afcn9d4Dz{lva4qXNTbwz= z)V}W6<;=`XSrQ>v@17z+e!Uu3O<77&gSSg)pN+xyC9>m`hZ<@}B}mt~nUq~$4chDR z#>_m%+A}yi2&>?@`FyC=UkbxcIQQJsPoLa1)UFFjwHMQI*Sc7MYJG0^3vsxUuN}wY zpZ?kSFWMTJFqmVXyL$+>YXM}_8`Pi#)qVQ=f$*O?HSf<}X@lD}>V^pQPOaC%Hq7Ru zVEwf3yE{&SBt(6PTdFmKYPe^XyV~~a4VYbqCi$}NT176%b5DN?a9-Mmw2rAmf>8J3 z0@YD~EmG`*eb>kto<(oX?(Jaq?R2HHR|O!@|M zfF@LYy?h0X0`1u+7_Art+Qz-Gjh3DENjz-85HXE&WhEsg8UbWOYsHzpYdk~g)CeQf z6R`2MTM28Wyafj4y%<5FpE(jAmmRd0QnDJKOynqu$~amGtIf7Z=3OE-pas-!1#!v0 zfmy7qtaS<8>LxJBzXjVFh9V3tvU^A;@BnHJWZ$! z{3u;K6P`5mjEGA9`5x_i+^BZ|HC7>T+Q{O9zc|l3u;Up7S#0CVIV^%YLW!o zey7`o8wh54tfuw^2w6H=id~vp_9lXF-54jcn}8NW)JqQkYLmyi-1uSJhv^mj; zT2&qV5O8An4rbM=-r{mqGmI(uuRuqW?#0~!Cx=aEo0;k${d>_TY{0U=crPbEy+^CK zQ!Fla;HeYYLc0@8owf`BTQ@_Bj1Lp#w5(Sf6PR z40pJ^8*$w2#f_%$hKrKn&Hu(YhBuqoNGF_F_jreR@S+Di*qQbkcrf6$$35)t<^V@9 z6vIYdeEW~xok6$#heU?tnPDBDeb?Oq;taW{>9&?#8)FTnjf923JYwgt@C%3q^cZxv z3NHM0_wKOU#C;4kl;Ep}_pzIJ034i7kVs?Q8A%@u?s)(-!y5?A47>1SUOdJ+?o4Ch zFmQCxORj;&Ry#u=tqq2Mca&p$fE~4@0WOxd&bp|w?cM~%VDT#B z?i#L^;t998pBRrYOgyQD7Q9C>_D_NO?y%3z16=rvZ+F~HEX9bsV=!Y24hrnw7udS& zgZy{2Ne49<0-rt?{OQGX-xR*&aSG{^Gh1KsmJkoRK1bY+ehkGurqlMq?c28(AlX|B zphRFJn?j3hrU*CbsEpbTFz;PbeFwapj^^FU zhG}AJoo2Ihn~XDaT1lQ{F00f2$D+l#DfBB+TfmB% z)9nWBx@b<qMBUQK9doSL&1HU3{&bmBVl>GpF$YO08(8ODCP5SEx}i7* zIYgu}Z5fSa5hhz*O=&AC&)`mZ7DWOt*IYeytBtTMjz@j~f4RDa;@b)oEYOr302BkJ zVDbtie3QHbJOZo97?-0WE0^x)ITU2Tn$5`kDnufUPldM zWq0v5IZ+5w4juk3ZC};v-`DG7ZnLP$J_#~VE|>U{isx$}$HfgijXa^!N;C)X5O4IU-e=SS}zMt6GcIgf_y56ua@ke27Z66aNr5wTKRQJvDTqK)gRR zFy@T|%Ljq|SeqH?|&OZ4-EIkKfiM8B)3Bto>W_hL8Qk~y|h zuq;YMmCv!m7r2m8;-EXJvIrb7>>$fc-NItIbZLtb3R}z6y2?I^K9r#ZxCO6R#jxxp zT$UHu1iuGY*5XIeI%S<1)>Jsx3g`MAJdRGV%R&(tbO2oOKw+{lIXDbVn)ZC}k-}l) zkVNMld{g<)fhm}W!QoFhg zB@^{gl}HOhy{K}Z%5jwwt});OXru-A>0pv#_0s{efd<{DmyrvESEf`V3kcab_&em@ zM~2g3=FUKzz7L@)RrapuEhACK+90P0ZIHBsOQaP6He~Ohj?5w@10|V7NamSf56ejD zQDzYnb>gV<(h|q3AtTXiG z!^wzH5o6>v;E#$MG|w$Dr3S?k&OIi8kW!P>9mq%eYyzJO+a4{o1EblMxdDfwy0iTUET(K)Y>#j-Jc_=JT_SK5iVXBl5;Dr! zW&FWO50s=#vB_5Bd=THJw~^!gfLM@-Gkcb}q*hks=7k67f?_w*C4g_s5-)Nm z7@bXt{j)nyBugOccbv}~Y(Q15d37BQZRE|+Oyd0VfU;#<^k=2y9)0=ws)m1tL|PxF zz0BmvDfH^fcRdd$bKIAq0b--m4&LDmU_%3DTxC#GT{I6oJzpdNBwMU+Fs0_6q!b%< zM(*dT`pWh~FT#b!hjea&zSTxM(A$8d5LYbMDmqIgxWIaBIK z5W$v#MR}Y+WHf|;1BXE%xyhUz27PNWqt{@0O(bxo4m)JYagx`_WE5cPBAJk4oL3f= z5^o?RyWHCu);2=*kbCv}5)DdB2mt+pe~9sd)uSV2xKJsm`s;{Sg^ij&BSvY}SNmZFJ1geqfK#zhTO@!oJ?n~o*a2CMC@N};crbdhq46+E3xlr=w zu8^ZPBK1X|JRX}Sql8d|me34y;l9{KGPx|91&kHMk!N-!>mp^AFQ!>9$feTE2oEQf z0E9{9*_V3PaM^?kV@zo%D~r3J*Ce)KP~}L8CvXTtOayjLLIPAaf|YUt_QINzu)80l%mMla2O;OpLp#S5QK= zhhi%i2`e!CpmxGwf`R&3`t)dFpqUK93Oq1y%h=N;M;jN01)jndz`HV0H=#XRxBz#| zGDTNEr=g*^!q>y5$ZM(ad&J98TEg(MKz+q{^dHN|9R3Nt%TSIn zHk<8-fpna{WjKuS;MRZ(zr*@Z6x`N#+%fjI-?ibaQQODt8Xd9%UoFff452A;2-v%T z9@86hVaKDt$r_k@304&xOC4|Lkb~0^Efatz;VSNUgK#VGY``sfi96n#bWhnpP`Isf zD6z=k6~epp0b^<2Dxl=L;2O?aTwvxj&%RG*F8u*xwq{eB#gyBW5{?eo0`tNhsP6D} z_uYYJ4xRi7PIJm_eV<-+sOEX!8kBucSl@sQJss-n`^`dfC+j=C)7SSQxAkUW>#c&# z_4?d?AlhO?M^e8Y11y5gHG?8zgY=O_^%IY3uYD`1NA%3xVCo$bu@^2e8xL$-t}tPJ z`evWZFkA2x-m~BkLvAt%=V-8*^C`>mlry2$>F${4X#_2j1R%2}GYeB5te_b%_a!Pn zgLY+gM~~ObaBKzPC3~oba%Vxr#0=x4Vb+?ZpvaS^@DPGeF`QT9Kj}}9(FfcQ8Q96aU_FmU z^8E>{G6cv&?nIeAeEUa*L$WUQFh@B+q86?baxfltfvN!P-}M28>TbLw-C=>!MZuN( zk?;|Og-q{C<=^ial)J*gwJUUJ+OFJ%v=z!kk21n<0T5tN3J+h?>LMTsDC17|3c8oa zqi=yY3A*JU4tun&l}a0V@RiDcAiU%ZPQLLm^M$9&C8){6GN@Ak*`mCL;zO?*fhP?A zi4btUeRw>8D~-*wURV}LRO&E~3XU}F-8Qh~VMCj{NL$bf^5y6i6!K=%dfDaCPXsP+|A%UcT-G`zKeSa>}K68G#kh_Ow!^scT?!^S(nsvOE65C z-KABZSDqwupy(TbIiU7QPF&T$fnyY%5Rt9RvZ!85EoyMyb`T&DE${Mr5Wq}8+EB`l z<&i=*ai1bi`>52aZX;-_gpQSx)>o*@6C2zwk;blZKP)bmH?V5|EyKehE%h#b`Z5|A z4TLWw6)d~y#?src4Hk)%-V1S;Kc-I~*YN3!;*)U8OZi4=IU(1XD@Nlxvv85Eg>4FP zoj4Kkj5DXL3C~kWx|o6^k||mgQz(L4LQ3FoH6j8p4LWug~!H)6_l)w;k z{Zf6B#>gnHiJ`^08Qe&P7@SO%iqz~%#qwuy$z7>bpbS%=?SLhhH59rdDel|_vOCk+FSa2L(JiE`ik_thNQSeiD^E!7NbeI^st#<#B!;bc>VoFsX4~ z9@aL(PY4*n^tHq9V=gGV01<4&l@Q??(bIjpxlcD?*y8V~B1#B&*=cte@wjP`sr?Kg zk#Ztx0_QAX0}obu=40O6g9s7;Nz-#!X*gPXO)pxgGB2XfGb6bP&EqMgh{7ED{Q;y( z5dYKP1&&90HsSiNR1oU+1Ii1*HC!xp351H=4ZMSuNQLm?9Q`!fA7M(Fup16GStFOo zslN{g*)GR~2SS3Ic;#7aSE9omq8aDOQrR_|T=S%+1b2m8K;)39XlDJ2op_bhk;I}l z8H06lwPs`hYfiKv9}-}4;Lj3Z!sj4MWW11K*}azjB#=7OYal_kk9y#Y;gd-+<6Zf& z0C@CiWgBWji3W{SM{EQpWT`+`-8E{7$m&tTqCLQ~C}wU7P#2Gm;yQa9cs$J>l)*^9 z&DZp{Qja!)!peB0pX_DbFKReBkLU&}C?Rl&jx?rC?s-f8w!Jse65&71OSwOjrSuFQTJMeWjSmrW3Io6B#o(2LKq_81GcDqm+oP1wZh7kQa8 zXs{ugO3uBb@|O5-nxp-YNcaVGbCyW>aa3seC^HCA{;9$s@^%jujuwt9gZBt>=!TIe zw7(!UrHhBOEi`=-9f0zo>j5BB1&;w+d0{&SWP}EUjB6mQ7$vrM`+ec^KbF#HiE&6c z13}7N8C8W)!m

M})iS0B9M0i%7u|k1QcXzDEOv32?t)SdY6fn{bV$Qos!NylY$E zO1;1%?wAb>+$P+@WRCGC`6XEeXl2y-XlfH`U)JFEj@UcjS@76}E|YhP{qIceUYVk( z1TRcTcl)|WiUG`54}z;k)v*{PYNxi6-z`*8=nG z&cdz;g=2ZKAn6SAnTYExEc9halOO5$LPR#UhXLmbiBcdEKHT^}@M=eV(ax0onPypo zn|p3fOjH6}3|P!r_oS!32T;oNnwH6r%_#}BDB2LGR!XOi0-FZIyiUu*CNu0a(WTa^ zwKhVG+_Z0T_$T-tMNs*pES_ZXF%~TpmKa7`NX`?vS0Ke&)$kUyYZ{pKpxJ0iv4>yx zol0{}cj^3!q`pSTiYZG(NLyr%Yd5f*RWfWlJg4y=PE>$?6VHsss=zywCgk3v09nOj zS3D%=8B?g?Cr3HyJ&HS%-XN_lZv;t%M^G!`Y4R_8I>Y!)D7BFP9xEKg^$}c~kDW=` zqp;uLe=kg!t&QPw9qBb(7{?DERYgu9@*aG~>Xhz)qliIbeZXU|UeHg%^NCrE_#XWd zGLfmx{+XG6!nlio)@&asz<-&SR7uN+SuCLFbD#YzFY&YDfwW+nh+pM)aP#S;n3B_M z+@$udg(DeY+7QwIPk9v0DCENQt;7m1@q37sp!)U=uEZ$c(<&VB79~!~(5YRy$Ko9p zCs!)zl`_(Df=Eo1>CM6 zad8e`km<&X2U~UFf_(%hnnbhYRvH2BzjqZ$!abavjVAJTWhM^W>AZ71NTg>M_=T%p=NRBT{qKV1gw2)K=g5CZQ)g>^=SxPPyw+!YI!ZJ zuD7bu6uQ3_cD8pG&fzn0^wTokS-!d(fp!~FZg^$G`$xRh;`Q!&h)Aa($FW?t_4Nb8Cuoz)!QT!}O6j}XA8CL^H!r)D2 z>U=V3%vT27aT--Hv-B8#i(Al3vZm-)yFhc#+~COV2fM>=B}2WRdq%F~)^+$r)}bC~ zv)~;GZ}P|!keXoq3k^I>s}*X4S+m?Y=nOb30BGQs7=Uf3?X-)>?K!tP^JDGP1Mx3bY_?G?>`R2--QS!6$Pg5p!5-zX3ze5`O+W6Mk^84u&3_M6i z^Ky{Og9ZM*988{BLtYPZ>(Ml^>9+pR`zeGr8JfU3TBp<-+YLIS9%hBgdN*A#PO4n^ zpcQHtcCS!n_X=%QJi3e$g!rXGwzsfQ@ggW#m|^F;7Y9HRH2TbjlI=-@X;&=uLm8px z{C2Okim(>Ew$?dL;?sshejQ9%IH%ylO3braQh1-83JVkvRO^{d*;zA_w*ONgNOxry zdq?4WG-3HYFnl+JXH&9nWL4)(#16719pH;=G)AKq10u|T2Am08`kcbkCuvD$UDcWm z$Ot4_BG1QP3vfurW;yRaqlW%$Dyyz4Z)i4KQghg49VPJ=`04t3vEh?V;O9NCV$WK? z_nU9l8NVfjoGgF^_d&{5k=@VlnJ~YzrY})E_w=)}eEC(OdXfZ9%Q)-igG~7U??50q zXcg}3J)mmtOy2z>ltAg6IAac{Y;~Iw{*tx`n?&J+dyQ7FmEnfvn9)JIICq@=P)3^e z{#RK0WfovN$wBF_vPQvZ^6jInJ;_4m`(Hp!8VDS}Ci^50nN=jyn~-#=TB%XA6D7XZ z8qyQiI%-m}6QBsNQL~g@gXHbV+_w~Zc}`Jo(r2IXk$P3zK%Ssk?>uuSna&DR+}MLX z$HKrt2NmZbMKdS!A1RulYdAMG3Hy6Y(Q9V6(H6(AFj5#sE*ssyQ?RlR@ZJDz_o;z# z9QZqc`V{PO9+EzQ7Q;C9Hv)^iD7(JDC2e=v7!>tXQ!d~a{SOqBv4~*&js!i#$EZVM zFzrthQvX@_(!rixrCn=%9^~;&lf5qH~4Kkcu46X1uHs*X-jfc@r7CV6o07bz*liZ8;MhlyDc(F&XIC04NHZfgSdn#Shp_% z2%m-Rq5z@%ezdRf11V5uz;=euxff0uUrQBa?o(7`>ae`hL zU%bT)wZIJcM0>^U%;*kcD`=)o0aw5XH?hx1Au2#!{Rq_6qT&v}&O{j0EiOrgnOY~p zN)~!VkR3Y@NU`6#gV*sCh7pH8ApKj3`LV^I#8GgcLn{umiL`p$ETl0=B;p^SqK!M8 zJy^;BB9SlQ(<)o&WZqwwhj>w+yr3SJ+H@76p>Yo|d*_dp0hq~FeTXM@$(wE^hE$SAX5KIV z3R_YX{}7350#`^%yHbXtq!o-yp6pSOQX(?7G|Rumc0?ih)GfS8ABViwNc2MX+A&<> z%w4NmNzdfeD@n*!kx-2gwBO-TBXg#Io&Eh9i@(Ey?8AI|A7xGA{qLdHXXcVt%72c< zPqO&)EWU~2Avqnkg3dH2*6xhU6(|>vB4j$?&6s@j;I)(Y`w4KoTw*3%Ja<5$GiO%% zP1Y{3AZIrYKw6a|lf$Um1KHz$3om~gc%Z=baA9)ajsriys@r~!uw8t6Iyq0n zMt0id8F21r9R4oZJ+L9O;?plOp{25P&Van@b_D2s9T4h20tCK7KMkQ?RQe1*o&D6F zqySwuqE3<{5E1FGB2l@$@U-eptCwHFeYqk(-1`DNR$IGl; zWx*u{V`KQnzLm?9Z>^}0Jf!t5vh6I3-)8Z76m$4l@jzI26Rp1tRI)FBc}GWx{*wsb z92%GaNhGO~+~p#E+UZj!RIt+!?v@$~L4qyksieVJ;Cc5pJuLHDmy= zE;k-4V5jid;E}}3-2r+K=z>G03X)88_==~q=o&)n2`Q&vRTumDwHrLR`1Y4mVKd0H zQHY8od`(64dk*}U@&nEhh`jHy&htxY$baj5XeU2;L-(lPM1#C|45-k5sjEJu=kaY295Be@DzK}njBU@L9Q~W#mK(guy z8d|!cxHZ|8Ww_j6?JF$4hC)&T!R;U{e~b6du=s5j!qbnl_G>8Q;Y6l2zxOWFOhGjb zmi!MQ%AaOip9QHS*Q#VwZCI!Cd`wz;NW3Rlzlw!11$cB4c`{Q_uBPzcp}{fl1nN+J zK>_xysfP2JGCRi6a%rGY6enG#o7tqnS6I8rfg-v$#OLQ&e2&G-C^Se%pRcvlK}N~SOg9xetTj96T(&+H2}NSc z|AaMR5hwg17D`&n8B3f!yM{iZ^C*TP)DMG)(MaZhb~Q0Ej_-&a7#dp`dt&U7G5_eF a9{u_lS{^_`@zEh35u;)aEEqWS5B@JXZH?pr literal 0 HcmV?d00001 diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py new file mode 100644 index 0000000..5890c72 --- /dev/null +++ b/cherrypy/_cprequest.py @@ -0,0 +1,956 @@ + +import os +import sys +import time +import warnings + +import cherrypy +from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr +from cherrypy._cpcompat import SimpleCookie, CookieError, py3k +from cherrypy import _cpreqbody, _cpconfig +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil, file_generator + + +class Hook(object): + """A callback and its metadata: failsafe, priority, and kwargs.""" + + callback = None + """ + The bare callable that this Hook object is wrapping, which will + be called when the Hook is called.""" + + failsafe = False + """ + If True, the callback is guaranteed to run even if other callbacks + from the same call point raise exceptions.""" + + priority = 50 + """ + Defines the order of execution for a list of Hooks. Priority numbers + should be limited to the closed interval [0, 100], but values outside + this range are acceptable, as are fractional values.""" + + kwargs = {} + """ + A set of keyword arguments that will be passed to the + callable on each call.""" + + def __init__(self, callback, failsafe=None, priority=None, **kwargs): + self.callback = callback + + if failsafe is None: + failsafe = getattr(callback, "failsafe", False) + self.failsafe = failsafe + + if priority is None: + priority = getattr(callback, "priority", 50) + self.priority = priority + + self.kwargs = kwargs + + def __lt__(self, other): + # Python 3 + return self.priority < other.priority + + def __cmp__(self, other): + # Python 2 + return cmp(self.priority, other.priority) + + def __call__(self): + """Run self.callback(**self.kwargs).""" + return self.callback(**self.kwargs) + + def __repr__(self): + cls = self.__class__ + return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)" + % (cls.__module__, cls.__name__, self.callback, + self.failsafe, self.priority, + ", ".join(['%s=%r' % (k, v) + for k, v in self.kwargs.items()]))) + + +class HookMap(dict): + """A map of call points to lists of callbacks (Hook objects).""" + + def __new__(cls, points=None): + d = dict.__new__(cls) + for p in points or []: + d[p] = [] + return d + + def __init__(self, *a, **kw): + pass + + def attach(self, point, callback, failsafe=None, priority=None, **kwargs): + """Append a new Hook made from the supplied arguments.""" + self[point].append(Hook(callback, failsafe, priority, **kwargs)) + + def run(self, point): + """Execute all registered Hooks (callbacks) for the given point.""" + exc = None + hooks = self[point] + hooks.sort() + for hook in hooks: + # Some hooks are guaranteed to run even if others at + # the same hookpoint fail. We will still log the failure, + # but proceed on to the next hook. The only way + # to stop all processing from one of these hooks is + # to raise SystemExit and stop the whole server. + if exc is None or hook.failsafe: + try: + hook() + except (KeyboardInterrupt, SystemExit): + raise + except (cherrypy.HTTPError, cherrypy.HTTPRedirect, + cherrypy.InternalRedirect): + exc = sys.exc_info()[1] + except: + exc = sys.exc_info()[1] + cherrypy.log(traceback=True, severity=40) + if exc: + raise exc + + def __copy__(self): + newmap = self.__class__() + # We can't just use 'update' because we want copies of the + # mutable values (each is a list) as well. + for k, v in self.items(): + newmap[k] = v[:] + return newmap + copy = __copy__ + + def __repr__(self): + cls = self.__class__ + return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self)) + + +# Config namespace handlers + +def hooks_namespace(k, v): + """Attach bare hooks declared in config.""" + # Use split again to allow multiple hooks for a single + # hookpoint per path (e.g. "hooks.before_handler.1"). + # Little-known fact you only get from reading source ;) + hookpoint = k.split(".", 1)[0] + if isinstance(v, basestring): + v = cherrypy.lib.attributes(v) + if not isinstance(v, Hook): + v = Hook(v) + cherrypy.serving.request.hooks[hookpoint].append(v) + +def request_namespace(k, v): + """Attach request attributes declared in config.""" + # Provides config entries to set request.body attrs (like attempt_charsets). + if k[:5] == 'body.': + setattr(cherrypy.serving.request.body, k[5:], v) + else: + setattr(cherrypy.serving.request, k, v) + +def response_namespace(k, v): + """Attach response attributes declared in config.""" + # Provides config entries to set default response headers + # http://cherrypy.org/ticket/889 + if k[:8] == 'headers.': + cherrypy.serving.response.headers[k.split('.', 1)[1]] = v + else: + setattr(cherrypy.serving.response, k, v) + +def error_page_namespace(k, v): + """Attach error pages declared in config.""" + if k != 'default': + k = int(k) + cherrypy.serving.request.error_page[k] = v + + +hookpoints = ['on_start_resource', 'before_request_body', + 'before_handler', 'before_finalize', + 'on_end_resource', 'on_end_request', + 'before_error_response', 'after_error_response'] + + +class Request(object): + """An HTTP request. + + This object represents the metadata of an HTTP request message; + that is, it contains attributes which describe the environment + in which the request URL, headers, and body were sent (if you + want tools to interpret the headers and body, those are elsewhere, + mostly in Tools). This 'metadata' consists of socket data, + transport characteristics, and the Request-Line. This object + also contains data regarding the configuration in effect for + the given URL, and the execution plan for generating a response. + """ + + prev = None + """ + The previous Request object (if any). This should be None + unless we are processing an InternalRedirect.""" + + # Conversation/connection attributes + local = httputil.Host("127.0.0.1", 80) + "An httputil.Host(ip, port, hostname) object for the server socket." + + remote = httputil.Host("127.0.0.1", 1111) + "An httputil.Host(ip, port, hostname) object for the client socket." + + scheme = "http" + """ + The protocol used between client and server. In most cases, + this will be either 'http' or 'https'.""" + + server_protocol = "HTTP/1.1" + """ + The HTTP version for which the HTTP server is at least + conditionally compliant.""" + + base = "" + """The (scheme://host) portion of the requested URL. + In some cases (e.g. when proxying via mod_rewrite), this may contain + path segments which cherrypy.url uses when constructing url's, but + which otherwise are ignored by CherryPy. Regardless, this value + MUST NOT end in a slash.""" + + # Request-Line attributes + request_line = "" + """ + The complete Request-Line received from the client. This is a + single string consisting of the request method, URI, and protocol + version (joined by spaces). Any final CRLF is removed.""" + + method = "GET" + """ + Indicates the HTTP method to be performed on the resource identified + by the Request-URI. Common methods include GET, HEAD, POST, PUT, and + DELETE. CherryPy allows any extension method; however, various HTTP + servers and gateways may restrict the set of allowable methods. + CherryPy applications SHOULD restrict the set (on a per-URI basis).""" + + query_string = "" + """ + The query component of the Request-URI, a string of information to be + interpreted by the resource. The query portion of a URI follows the + path component, and is separated by a '?'. For example, the URI + 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component, + 'a=3&b=4'.""" + + query_string_encoding = 'utf8' + """ + The encoding expected for query string arguments after % HEX HEX decoding). + If a query string is provided that cannot be decoded with this encoding, + 404 is raised (since technically it's a different URI). If you want + arbitrary encodings to not error, set this to 'Latin-1'; you can then + encode back to bytes and re-decode to whatever encoding you like later. + """ + + protocol = (1, 1) + """The HTTP protocol version corresponding to the set + of features which should be allowed in the response. If BOTH + the client's request message AND the server's level of HTTP + compliance is HTTP/1.1, this attribute will be the tuple (1, 1). + If either is 1.0, this attribute will be the tuple (1, 0). + Lower HTTP protocol versions are not explicitly supported.""" + + params = {} + """ + A dict which combines query string (GET) and request entity (POST) + variables. This is populated in two stages: GET params are added + before the 'on_start_resource' hook, and POST params are added + between the 'before_request_body' and 'before_handler' hooks.""" + + # Message attributes + header_list = [] + """ + A list of the HTTP request headers as (name, value) tuples. + In general, you should use request.headers (a dict) instead.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the request headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). See also: + httputil.HeaderMap, httputil.HeaderElement.""" + + cookie = SimpleCookie() + """See help(Cookie).""" + + rfile = None + """ + If the request included an entity (body), it will be available + as a stream in this attribute. However, the rfile will normally + be read for you between the 'before_request_body' hook and the + 'before_handler' hook, and the resulting string is placed into + either request.params or the request.body attribute. + + You may disable the automatic consumption of the rfile by setting + request.process_request_body to False, either in config for the desired + path, or in an 'on_start_resource' or 'before_request_body' hook. + + WARNING: In almost every case, you should not attempt to read from the + rfile stream after CherryPy's automatic mechanism has read it. If you + turn off the automatic parsing of rfile, you should read exactly the + number of bytes specified in request.headers['Content-Length']. + Ignoring either of these warnings may result in a hung request thread + or in corruption of the next (pipelined) request. + """ + + process_request_body = True + """ + If True, the rfile (if any) is automatically read and parsed, + and the result placed into request.params or request.body.""" + + methods_with_bodies = ("POST", "PUT") + """ + A sequence of HTTP methods for which CherryPy will automatically + attempt to read a body from the rfile.""" + + body = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' + or multipart, this will be None. Otherwise, this will be an instance + of :class:`RequestBody` (which you + can .read()); this value is set between the 'before_request_body' and + 'before_handler' hooks (assuming that process_request_body is True).""" + + # Dispatch attributes + dispatch = cherrypy.dispatch.Dispatcher() + """ + The object which looks up the 'page handler' callable and collects + config for the current request based on the path_info, other + request attributes, and the application architecture. The core + calls the dispatcher as early as possible, passing it a 'path_info' + argument. + + The default dispatcher discovers the page handler by matching path_info + to a hierarchical arrangement of objects, starting at request.app.root. + See help(cherrypy.dispatch) for more information.""" + + script_name = "" + """ + The 'mount point' of the application which is handling this request. + + This attribute MUST NOT end in a slash. If the script_name refers to + the root of the URI, it MUST be an empty string (not "/"). + """ + + path_info = "/" + """ + The 'relative path' portion of the Request-URI. This is relative + to the script_name ('mount point') of the application which is + handling this request.""" + + login = None + """ + When authentication is used during the request processing this is + set to 'False' if it failed and to the 'username' value if it succeeded. + The default 'None' implies that no authentication happened.""" + + # Note that cherrypy.url uses "if request.app:" to determine whether + # the call is during a real HTTP request or not. So leave this None. + app = None + """The cherrypy.Application object which is handling this request.""" + + handler = None + """ + The function, method, or other callable which CherryPy will call to + produce the response. The discovery of the handler and the arguments + it will receive are determined by the request.dispatch object. + By default, the handler is discovered by walking a tree of objects + starting at request.app.root, and is then passed all HTTP params + (from the query string and POST body) as keyword arguments.""" + + toolmaps = {} + """ + A nested dict of all Toolboxes and Tools in effect for this request, + of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" + + config = None + """ + A flat dict of all configuration entries which apply to the + current request. These entries are collected from global config, + application config (based on request.path_info), and from handler + config (exactly how is governed by the request.dispatch object in + effect for this request; by default, handler config can be attached + anywhere in the tree between request.app.root and the final handler, + and inherits downward).""" + + is_index = None + """ + This will be True if the current request is mapped to an 'index' + resource handler (also, a 'default' handler if path_info ends with + a slash). The value may be used to automatically redirect the + user-agent to a 'more canonical' URL which either adds or removes + the trailing slash. See cherrypy.tools.trailing_slash.""" + + hooks = HookMap(hookpoints) + """ + A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. + Each key is a str naming the hook point, and each value is a list + of hooks which will be called at that hook point during this request. + The list of hooks is generally populated as early as possible (mostly + from Tools specified in config), but may be extended at any time. + See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" + + error_response = cherrypy.HTTPError(500).set_response + """ + The no-arg callable which will handle unexpected, untrapped errors + during request processing. This is not used for expected exceptions + (like NotFound, HTTPError, or HTTPRedirect) which are raised in + response to expected conditions (those should be customized either + via request.error_page or by overriding HTTPError.set_response). + By default, error_response uses HTTPError(500) to return a generic + error response to the user-agent.""" + + error_page = {} + """ + A dict of {error code: response filename or callable} pairs. + + The error code must be an int representing a given HTTP error code, + or the string 'default', which will be used if no matching entry + is found for a given numeric code. + + If a filename is provided, the file should contain a Python string- + formatting template, and can expect by default to receive format + values with the mapping keys %(status)s, %(message)s, %(traceback)s, + and %(version)s. The set of format mappings can be extended by + overriding HTTPError.set_response. + + If a callable is provided, it will be called by default with keyword + arguments 'status', 'message', 'traceback', and 'version', as for a + string-formatting template. The callable must return a string or iterable of + strings which will be set to response.body. It may also override headers or + perform any other processing. + + If no entry is given for an error code, and no 'default' entry exists, + a default template will be used. + """ + + show_tracebacks = True + """ + If True, unexpected errors encountered during request processing will + include a traceback in the response body.""" + + show_mismatched_params = True + """ + If True, mismatched parameters encountered during PageHandler invocation + processing will be included in the response body.""" + + throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) + """The sequence of exceptions which Request.run does not trap.""" + + throw_errors = False + """ + If True, Request.run will not trap any errors (except HTTPRedirect and + HTTPError, which are more properly called 'exceptions', not errors).""" + + closed = False + """True once the close method has been called, False otherwise.""" + + stage = None + """ + A string containing the stage reached in the request-handling process. + This is useful when debugging a live server with hung requests.""" + + namespaces = _cpconfig.NamespaceSet( + **{"hooks": hooks_namespace, + "request": request_namespace, + "response": response_namespace, + "error_page": error_page_namespace, + "tools": cherrypy.tools, + }) + + def __init__(self, local_host, remote_host, scheme="http", + server_protocol="HTTP/1.1"): + """Populate a new Request object. + + local_host should be an httputil.Host object with the server info. + remote_host should be an httputil.Host object with the client info. + scheme should be a string, either "http" or "https". + """ + self.local = local_host + self.remote = remote_host + self.scheme = scheme + self.server_protocol = server_protocol + + self.closed = False + + # Put a *copy* of the class error_page into self. + self.error_page = self.error_page.copy() + + # Put a *copy* of the class namespaces into self. + self.namespaces = self.namespaces.copy() + + self.stage = None + + def close(self): + """Run cleanup code. (Core)""" + if not self.closed: + self.closed = True + self.stage = 'on_end_request' + self.hooks.run('on_end_request') + self.stage = 'close' + + def run(self, method, path, query_string, req_protocol, headers, rfile): + r"""Process the Request. (Core) + + method, path, query_string, and req_protocol should be pulled directly + from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). + + path + This should be %XX-unquoted, but query_string should not be. + + When using Python 2, they both MUST be byte strings, + not unicode strings. + + When using Python 3, they both MUST be unicode strings, + not byte strings, and preferably not bytes \x00-\xFF + disguised as unicode. + + headers + A list of (name, value) tuples. + + rfile + A file-like object containing the HTTP request entity. + + When run() is done, the returned object should have 3 attributes: + + * status, e.g. "200 OK" + * header_list, a list of (name, value) tuples + * body, an iterable yielding strings + + Consumer code (HTTP servers) should then access these response + attributes to build the outbound stream. + + """ + response = cherrypy.serving.response + self.stage = 'run' + try: + self.error_response = cherrypy.HTTPError(500).set_response + + self.method = method + path = path or "/" + self.query_string = query_string or '' + self.params = {} + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(self.server_protocol[5]), int(self.server_protocol[7]) + self.protocol = min(rp, sp) + response.headers.protocol = self.protocol + + # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). + url = path + if query_string: + url += '?' + query_string + self.request_line = '%s %s %s' % (method, url, req_protocol) + + self.header_list = list(headers) + self.headers = httputil.HeaderMap() + + self.rfile = rfile + self.body = None + + self.cookie = SimpleCookie() + self.handler = None + + # path_info should be the path from the + # app root (script_name) to the handler. + self.script_name = self.app.script_name + self.path_info = pi = path[len(self.script_name):] + + self.stage = 'respond' + self.respond(pi) + + except self.throws: + raise + except: + if self.throw_errors: + raise + else: + # Failure in setup, error handler or finalize. Bypass them. + # Can't use handle_error because we may not have hooks yet. + cherrypy.log(traceback=True, severity=40) + if self.show_tracebacks: + body = format_exc() + else: + body = "" + r = bare_error(body) + response.output_status, response.header_list, response.body = r + + if self.method == "HEAD": + # HEAD requests MUST NOT return a message-body in the response. + response.body = [] + + try: + cherrypy.log.access() + except: + cherrypy.log.error(traceback=True) + + if response.timed_out: + raise cherrypy.TimeoutError() + + return response + + # Uncomment for stage debugging + # stage = property(lambda self: self._stage, lambda self, v: print(v)) + + def respond(self, path_info): + """Generate a response for the resource at self.path_info. (Core)""" + response = cherrypy.serving.response + try: + try: + try: + if self.app is None: + raise cherrypy.NotFound() + + # Get the 'Host' header, so we can HTTPRedirect properly. + self.stage = 'process_headers' + self.process_headers() + + # Make a copy of the class hooks + self.hooks = self.__class__.hooks.copy() + self.toolmaps = {} + + self.stage = 'get_resource' + self.get_resource(path_info) + + self.body = _cpreqbody.RequestBody( + self.rfile, self.headers, request_params=self.params) + + self.namespaces(self.config) + + self.stage = 'on_start_resource' + self.hooks.run('on_start_resource') + + # Parse the querystring + self.stage = 'process_query_string' + self.process_query_string() + + # Process the body + if self.process_request_body: + if self.method not in self.methods_with_bodies: + self.process_request_body = False + self.stage = 'before_request_body' + self.hooks.run('before_request_body') + if self.process_request_body: + self.body.process() + + # Run the handler + self.stage = 'before_handler' + self.hooks.run('before_handler') + if self.handler: + self.stage = 'handler' + response.body = self.handler() + + # Finalize + self.stage = 'before_finalize' + self.hooks.run('before_finalize') + response.finalize() + except (cherrypy.HTTPRedirect, cherrypy.HTTPError): + inst = sys.exc_info()[1] + inst.set_response() + self.stage = 'before_finalize (HTTPError)' + self.hooks.run('before_finalize') + response.finalize() + finally: + self.stage = 'on_end_resource' + self.hooks.run('on_end_resource') + except self.throws: + raise + except: + if self.throw_errors: + raise + self.handle_error() + + def process_query_string(self): + """Parse the query string into Python structures. (Core)""" + try: + p = httputil.parse_query_string( + self.query_string, encoding=self.query_string_encoding) + except UnicodeDecodeError: + raise cherrypy.HTTPError( + 404, "The given query string could not be processed. Query " + "strings for this resource must be encoded with %r." % + self.query_string_encoding) + + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if not py3k: + for key, value in p.items(): + if isinstance(key, unicode): + del p[key] + p[key.encode(self.query_string_encoding)] = value + self.params.update(p) + + def process_headers(self): + """Parse HTTP header data into Python structures. (Core)""" + # Process the headers into self.headers + headers = self.headers + for name, value in self.header_list: + # Call title() now (and use dict.__method__(headers)) + # so title doesn't have to be called twice. + name = name.title() + value = value.strip() + + # Warning: if there is more than one header entry for cookies (AFAIK, + # only Konqueror does that), only the last one will remain in headers + # (but they will be correctly stored in request.cookie). + if "=?" in value: + dict.__setitem__(headers, name, httputil.decode_TEXT(value)) + else: + dict.__setitem__(headers, name, value) + + # Handle cookies differently because on Konqueror, multiple + # cookies come on different lines with the same key + if name == 'Cookie': + try: + self.cookie.load(value) + except CookieError: + msg = "Illegal cookie name %s" % value.split('=')[0] + raise cherrypy.HTTPError(400, msg) + + if not dict.__contains__(headers, 'Host'): + # All Internet-based HTTP/1.1 servers MUST respond with a 400 + # (Bad Request) status code to any HTTP/1.1 request message + # which lacks a Host header field. + if self.protocol >= (1, 1): + msg = "HTTP/1.1 requires a 'Host' request header." + raise cherrypy.HTTPError(400, msg) + host = dict.get(headers, 'Host') + if not host: + host = self.local.name or self.local.ip + self.base = "%s://%s" % (self.scheme, host) + + def get_resource(self, path): + """Call a dispatcher (which sets self.handler and .config). (Core)""" + # First, see if there is a custom dispatch at this URI. Custom + # dispatchers can only be specified in app.config, not in _cp_config + # (since custom dispatchers may not even have an app.root). + dispatch = self.app.find_config(path, "request.dispatch", self.dispatch) + + # dispatch() should set self.handler and self.config + dispatch(path) + + def handle_error(self): + """Handle the last unanticipated exception. (Core)""" + try: + self.hooks.run("before_error_response") + if self.error_response: + self.error_response() + self.hooks.run("after_error_response") + cherrypy.serving.response.finalize() + except cherrypy.HTTPRedirect: + inst = sys.exc_info()[1] + inst.set_response() + cherrypy.serving.response.finalize() + + # ------------------------- Properties ------------------------- # + + def _get_body_params(self): + warnings.warn( + "body_params is deprecated in CherryPy 3.2, will be removed in " + "CherryPy 3.3.", + DeprecationWarning + ) + return self.body.params + body_params = property(_get_body_params, + doc= """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True). + + Deprecated in 3.2, will be removed for 3.3 in favor of + :attr:`request.body.params`.""") + + +class ResponseBody(object): + """The body of the HTTP response (the response entity).""" + + if py3k: + unicode_err = ("Page handlers MUST return bytes. Use tools.encode " + "if you wish to return unicode.") + + def __get__(self, obj, objclass=None): + if obj is None: + # When calling on the class instead of an instance... + return self + else: + return obj._body + + def __set__(self, obj, value): + # Convert the given value to an iterable object. + if py3k and isinstance(value, str): + raise ValueError(self.unicode_err) + + if isinstance(value, basestring): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if value: + value = [value] + else: + # [''] doesn't evaluate to False, so replace it with []. + value = [] + elif py3k and isinstance(value, list): + # every item in a list must be bytes... + for i, item in enumerate(value): + if isinstance(item, str): + raise ValueError(self.unicode_err) + # Don't use isinstance here; io.IOBase which has an ABC takes + # 1000 times as long as, say, isinstance(value, str) + elif hasattr(value, 'read'): + value = file_generator(value) + elif value is None: + value = [] + obj._body = value + + +class Response(object): + """An HTTP Response, including status, headers, and body.""" + + status = "" + """The HTTP Status-Code and Reason-Phrase.""" + + header_list = [] + """ + A list of the HTTP response headers as (name, value) tuples. + In general, you should use response.headers (a dict) instead. This + attribute is generated from response.headers and is not valid until + after the finalize phase.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the response headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). + + .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` + """ + + cookie = SimpleCookie() + """See help(Cookie).""" + + body = ResponseBody() + """The body (entity) of the HTTP response.""" + + time = None + """The value of time.time() when created. Use in HTTP dates.""" + + timeout = 300 + """Seconds after which the response will be aborted.""" + + timed_out = False + """ + Flag to indicate the response should be aborted, because it has + exceeded its timeout.""" + + stream = False + """If False, buffer the response body.""" + + def __init__(self): + self.status = None + self.header_list = None + self._body = [] + self.time = time.time() + + self.headers = httputil.HeaderMap() + # Since we know all our keys are titled strings, we can + # bypass HeaderMap.update and get a big speed boost. + dict.update(self.headers, { + "Content-Type": 'text/html', + "Server": "CherryPy/" + cherrypy.__version__, + "Date": httputil.HTTPDate(self.time), + }) + self.cookie = SimpleCookie() + + def collapse_body(self): + """Collapse self.body to a single string; replace it and return it.""" + if isinstance(self.body, basestring): + return self.body + + newbody = [] + for chunk in self.body: + if py3k and not isinstance(chunk, bytes): + raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk)) + newbody.append(chunk) + newbody = ntob('').join(newbody) + + self.body = newbody + return newbody + + def finalize(self): + """Transform headers (and cookies) into self.header_list. (Core)""" + try: + code, reason, _ = httputil.valid_status(self.status) + except ValueError: + raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) + + headers = self.headers + + self.status = "%s %s" % (code, reason) + self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason) + + if self.stream: + # The upshot: wsgiserver will chunk the response if + # you pop Content-Length (or set it explicitly to None). + # Note that lib.static sets C-L to the file's st_size. + if dict.get(headers, 'Content-Length') is None: + dict.pop(headers, 'Content-Length', None) + elif code < 200 or code in (204, 205, 304): + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." + dict.pop(headers, 'Content-Length', None) + self.body = ntob("") + else: + # Responses which are not streamed should have a Content-Length, + # but allow user code to set Content-Length if desired. + if dict.get(headers, 'Content-Length') is None: + content = self.collapse_body() + dict.__setitem__(headers, 'Content-Length', len(content)) + + # Transform our header dict into a list of tuples. + self.header_list = h = headers.output() + + cookie = self.cookie.output() + if cookie: + for line in cookie.split("\n"): + if line.endswith("\r"): + # Python 2.4 emits cookies joined by LF but 2.5+ by CRLF. + line = line[:-1] + name, value = line.split(": ", 1) + if isinstance(name, unicodestr): + name = name.encode("ISO-8859-1") + if isinstance(value, unicodestr): + value = headers.encode(value) + h.append((name, value)) + + def check_timeout(self): + """If now > self.time + self.timeout, set self.timed_out. + + This purposefully sets a flag, rather than raising an error, + so that a monitor thread can interrupt the Response thread. + """ + if time.time() > self.time + self.timeout: + self.timed_out = True + + + diff --git a/cherrypy/_cprequest.pyc b/cherrypy/_cprequest.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdc0c22e0cb4bb28f87fcc64f666d642743fc654 GIT binary patch literal 19745 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 literal 0 HcmV?d00001 diff --git a/cherrypy/_cpthreadinglocal.py b/cherrypy/_cpthreadinglocal.py new file mode 100644 index 0000000..34c17ac --- /dev/null +++ b/cherrypy/_cpthreadinglocal.py @@ -0,0 +1,239 @@ +# This is a backport of Python-2.4's threading.local() implementation + +"""Thread-local objects + +(Note that this module provides a Python version of thread + threading.local class. Depending on the version of Python you're + using, there may be a faster one available. You should always import + the local class from threading.) + +Thread-local objects support the management of thread-local data. +If you have data that you want to be local to a thread, simply create +a thread-local object and use its attributes: + + >>> mydata = local() + >>> mydata.number = 42 + >>> mydata.number + 42 + +You can also access the local-object's dictionary: + + >>> mydata.__dict__ + {'number': 42} + >>> mydata.__dict__.setdefault('widgets', []) + [] + >>> mydata.widgets + [] + +What's important about thread-local objects is that their data are +local to a thread. If we access the data in a different thread: + + >>> log = [] + >>> def f(): + ... items = mydata.__dict__.items() + ... items.sort() + ... log.append(items) + ... mydata.number = 11 + ... log.append(mydata.number) + + >>> import threading + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + >>> log + [[], 11] + +we get different data. Furthermore, changes made in the other thread +don't affect data seen in this thread: + + >>> mydata.number + 42 + +Of course, values you get from a local object, including a __dict__ +attribute, are for whatever thread was current at the time the +attribute was read. For that reason, you generally don't want to save +these values across threads, as they apply only to the thread they +came from. + +You can create custom local objects by subclassing the local class: + + >>> class MyLocal(local): + ... number = 2 + ... initialized = False + ... def __init__(self, **kw): + ... if self.initialized: + ... raise SystemError('__init__ called too many times') + ... self.initialized = True + ... self.__dict__.update(kw) + ... def squared(self): + ... return self.number ** 2 + +This can be useful to support default values, methods and +initialization. Note that if you define an __init__ method, it will be +called each time the local object is used in a separate thread. This +is necessary to initialize each thread's dictionary. + +Now if we create a local object: + + >>> mydata = MyLocal(color='red') + +Now we have a default number: + + >>> mydata.number + 2 + +an initial color: + + >>> mydata.color + 'red' + >>> del mydata.color + +And a method that operates on the data: + + >>> mydata.squared() + 4 + +As before, we can access the data in a separate thread: + + >>> log = [] + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + >>> log + [[('color', 'red'), ('initialized', True)], 11] + +without affecting this thread's data: + + >>> mydata.number + 2 + >>> mydata.color + Traceback (most recent call last): + ... + AttributeError: 'MyLocal' object has no attribute 'color' + +Note that subclasses can define slots, but they are not thread +local. They are shared across threads: + + >>> class MyLocal(local): + ... __slots__ = 'number' + + >>> mydata = MyLocal() + >>> mydata.number = 42 + >>> mydata.color = 'red' + +So, the separate thread: + + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + +affects what we see: + + >>> mydata.number + 11 + +>>> del mydata +""" + +# Threading import is at end + +class _localbase(object): + __slots__ = '_local__key', '_local__args', '_local__lock' + + def __new__(cls, *args, **kw): + self = object.__new__(cls) + key = 'thread.local.' + str(id(self)) + object.__setattr__(self, '_local__key', key) + object.__setattr__(self, '_local__args', (args, kw)) + object.__setattr__(self, '_local__lock', RLock()) + + if args or kw and (cls.__init__ is object.__init__): + raise TypeError("Initialization arguments are not supported") + + # We need to create the thread dict in anticipation of + # __init__ being called, to make sure we don't call it + # again ourselves. + dict = object.__getattribute__(self, '__dict__') + currentThread().__dict__[key] = dict + + return self + +def _patch(self): + key = object.__getattribute__(self, '_local__key') + d = currentThread().__dict__.get(key) + if d is None: + d = {} + currentThread().__dict__[key] = d + object.__setattr__(self, '__dict__', d) + + # we have a new instance dict, so call out __init__ if we have + # one + cls = type(self) + if cls.__init__ is not object.__init__: + args, kw = object.__getattribute__(self, '_local__args') + cls.__init__(self, *args, **kw) + else: + object.__setattr__(self, '__dict__', d) + +class local(_localbase): + + def __getattribute__(self, name): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__getattribute__(self, name) + finally: + lock.release() + + def __setattr__(self, name, value): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__setattr__(self, name, value) + finally: + lock.release() + + def __delattr__(self, name): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__delattr__(self, name) + finally: + lock.release() + + + def __del__(): + threading_enumerate = enumerate + __getattribute__ = object.__getattribute__ + + def __del__(self): + key = __getattribute__(self, '_local__key') + + try: + threads = list(threading_enumerate()) + except: + # if enumerate fails, as it seems to do during + # shutdown, we'll skip cleanup under the assumption + # that there is nothing to clean up + return + + for thread in threads: + try: + __dict__ = thread.__dict__ + except AttributeError: + # Thread is dying, rest in peace + continue + + if key in __dict__: + try: + del __dict__[key] + except KeyError: + pass # didn't have anything in this thread + + return __del__ + __del__ = __del__() + +from threading import currentThread, enumerate, RLock diff --git a/cherrypy/_cptools.py b/cherrypy/_cptools.py new file mode 100644 index 0000000..22316b3 --- /dev/null +++ b/cherrypy/_cptools.py @@ -0,0 +1,510 @@ +"""CherryPy tools. A "tool" is any helper, adapted to CP. + +Tools are usually designed to be used in a variety of ways (although some +may only offer one if they choose): + + Library calls + All tools are callables that can be used wherever needed. + The arguments are straightforward and should be detailed within the + docstring. + + Function decorators + All tools, when called, may be used as decorators which configure + individual CherryPy page handlers (methods on the CherryPy tree). + That is, "@tools.anytool()" should "turn on" the tool via the + decorated function's _cp_config attribute. + + CherryPy config + If a tool exposes a "_setup" callable, it will be called + once per Request (if the feature is "turned on" via config). + +Tools may be implemented as any object with a namespace. The builtins +are generally either modules or instances of the tools.Tool class. +""" + +import sys +import warnings + +import cherrypy + + +def _getargs(func): + """Return the names of all static arguments to the given function.""" + # Use this instead of importing inspect for less mem overhead. + import types + if sys.version_info >= (3, 0): + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ + else: + if isinstance(func, types.MethodType): + func = func.im_func + co = func.func_code + return co.co_varnames[:co.co_argcount] + + +_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them " + "on via config, or use them as decorators on your page handlers.") + +class Tool(object): + """A registered function for use with CherryPy request-processing hooks. + + help(tool.callable) should give you more information about this Tool. + """ + + namespace = "tools" + + def __init__(self, point, callable, name=None, priority=50): + self._point = point + self.callable = callable + self._name = name + self._priority = priority + self.__doc__ = self.callable.__doc__ + self._setargs() + + def _get_on(self): + raise AttributeError(_attr_error) + def _set_on(self, value): + raise AttributeError(_attr_error) + on = property(_get_on, _set_on) + + def _setargs(self): + """Copy func parameter names to obj attributes.""" + try: + for arg in _getargs(self.callable): + setattr(self, arg, None) + except (TypeError, AttributeError): + if hasattr(self.callable, "__call__"): + for arg in _getargs(self.callable.__call__): + setattr(self, arg, None) + # IronPython 1.0 raises NotImplementedError because + # inspect.getargspec tries to access Python bytecode + # in co_code attribute. + except NotImplementedError: + pass + # IronPython 1B1 may raise IndexError in some cases, + # but if we trap it here it doesn't prevent CP from working. + except IndexError: + pass + + def _merged_args(self, d=None): + """Return a dict of configuration entries for this Tool.""" + if d: + conf = d.copy() + else: + conf = {} + + tm = cherrypy.serving.request.toolmaps[self.namespace] + if self._name in tm: + conf.update(tm[self._name]) + + if "on" in conf: + del conf["on"] + + return conf + + def __call__(self, *args, **kwargs): + """Compile-time decorator (turn on the tool in config). + + For example:: + + @tools.proxy() + def whats_my_base(self): + return cherrypy.request.base + whats_my_base.exposed = True + """ + if args: + raise TypeError("The %r Tool does not accept positional " + "arguments; you must use keyword arguments." + % self._name) + def tool_decorator(f): + if not hasattr(f, "_cp_config"): + f._cp_config = {} + subspace = self.namespace + "." + self._name + "." + f._cp_config[subspace + "on"] = True + for k, v in kwargs.items(): + f._cp_config[subspace + k] = v + return f + return tool_decorator + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + cherrypy.serving.request.hooks.attach(self._point, self.callable, + priority=p, **conf) + + +class HandlerTool(Tool): + """Tool which is called 'before main', that may skip normal handlers. + + If the tool successfully handles the request (by setting response.body), + if should return True. This will cause CherryPy to skip any 'normal' page + handler. If the tool did not handle the request, it should return False + to tell CherryPy to continue on and call the normal page handler. If the + tool is declared AS a page handler (see the 'handler' method), returning + False will raise NotFound. + """ + + def __init__(self, callable, name=None): + Tool.__init__(self, 'before_handler', callable, name) + + def handler(self, *args, **kwargs): + """Use this tool as a CherryPy page handler. + + For example:: + + class Root: + nav = tools.staticdir.handler(section="/nav", dir="nav", + root=absDir) + """ + def handle_func(*a, **kw): + handled = self.callable(*args, **self._merged_args(kwargs)) + if not handled: + raise cherrypy.NotFound() + return cherrypy.serving.response.body + handle_func.exposed = True + return handle_func + + def _wrapper(self, **kwargs): + if self.callable(**kwargs): + cherrypy.serving.request.handler = None + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + cherrypy.serving.request.hooks.attach(self._point, self._wrapper, + priority=p, **conf) + + +class HandlerWrapperTool(Tool): + """Tool which wraps request.handler in a provided wrapper function. + + The 'newhandler' arg must be a handler wrapper function that takes a + 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all + page handler + functions, it must return an iterable for use as cherrypy.response.body. + + For example, to allow your 'inner' page handlers to return dicts + which then get interpolated into a template:: + + def interpolator(next_handler, *args, **kwargs): + filename = cherrypy.request.config.get('template') + cherrypy.response.template = env.get_template(filename) + response_dict = next_handler(*args, **kwargs) + return cherrypy.response.template.render(**response_dict) + cherrypy.tools.jinja = HandlerWrapperTool(interpolator) + """ + + def __init__(self, newhandler, point='before_handler', name=None, priority=50): + self.newhandler = newhandler + self._point = point + self._name = name + self._priority = priority + + def callable(self, debug=False): + innerfunc = cherrypy.serving.request.handler + def wrap(*args, **kwargs): + return self.newhandler(innerfunc, *args, **kwargs) + cherrypy.serving.request.handler = wrap + + +class ErrorTool(Tool): + """Tool which is used to replace the default request.error_response.""" + + def __init__(self, callable, name=None): + Tool.__init__(self, None, callable, name) + + def _wrapper(self): + self.callable(**self._merged_args()) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + cherrypy.serving.request.error_response = self._wrapper + + +# Builtin tools # + +from cherrypy.lib import cptools, encoding, auth, static, jsontools +from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc +from cherrypy.lib import caching as _caching +from cherrypy.lib import auth_basic, auth_digest + + +class SessionTool(Tool): + """Session Tool for CherryPy. + + sessions.locking + When 'implicit' (the default), the session will be locked for you, + just before running the page handler. + + When 'early', the session will be locked before reading the request + body. This is off by default for safety reasons; for example, + a large upload would block the session, denying an AJAX + progress meter (see http://www.cherrypy.org/ticket/630). + + When 'explicit' (or any other value), you need to call + cherrypy.session.acquire_lock() yourself before using + session data. + """ + + def __init__(self): + # _sessions.init must be bound after headers are read + Tool.__init__(self, 'before_request_body', _sessions.init) + + def _lock_session(self): + cherrypy.serving.session.acquire_lock() + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + hooks = cherrypy.serving.request.hooks + + conf = self._merged_args() + + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + + hooks.attach(self._point, self.callable, priority=p, **conf) + + locking = conf.pop('locking', 'implicit') + if locking == 'implicit': + hooks.attach('before_handler', self._lock_session) + elif locking == 'early': + # Lock before the request body (but after _sessions.init runs!) + hooks.attach('before_request_body', self._lock_session, + priority=60) + else: + # Don't lock + pass + + hooks.attach('before_finalize', _sessions.save) + hooks.attach('on_end_request', _sessions.close) + + def regenerate(self): + """Drop the current session and make a new one (with a new id).""" + sess = cherrypy.serving.session + sess.regenerate() + + # Grab cookie-relevant tool args + conf = dict([(k, v) for k, v in self._merged_args().items() + if k in ('path', 'path_header', 'name', 'timeout', + 'domain', 'secure')]) + _sessions.set_response_cookie(**conf) + + + + +class XMLRPCController(object): + """A Controller (page handler collection) for XML-RPC. + + To use it, have your controllers subclass this base class (it will + turn on the tool for you). + + You can also supply the following optional config entries:: + + tools.xmlrpc.encoding: 'utf-8' + tools.xmlrpc.allow_none: 0 + + XML-RPC is a rather discontinuous layer over HTTP; dispatching to the + appropriate handler must first be performed according to the URL, and + then a second dispatch step must take place according to the RPC method + specified in the request body. It also allows a superfluous "/RPC2" + prefix in the URL, supplies its own handler args in the body, and + requires a 200 OK "Fault" response instead of 404 when the desired + method is not found. + + Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. + This Controller acts as the dispatch target for the first half (based + on the URL); it then reads the RPC method from the request body and + does its own second dispatch step based on that method. It also reads + body params, and returns a Fault on error. + + The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 + in your URL's, you can safely skip turning on the XMLRPCDispatcher. + Otherwise, you need to use declare it in config:: + + request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() + """ + + # Note we're hard-coding this into the 'tools' namespace. We could do + # a huge amount of work to make it relocatable, but the only reason why + # would be if someone actually disabled the default_toolbox. Meh. + _cp_config = {'tools.xmlrpc.on': True} + + def default(self, *vpath, **params): + rpcparams, rpcmethod = _xmlrpc.process_body() + + subhandler = self + for attr in str(rpcmethod).split('.'): + subhandler = getattr(subhandler, attr, None) + + if subhandler and getattr(subhandler, "exposed", False): + body = subhandler(*(vpath + rpcparams), **params) + + else: + # http://www.cherrypy.org/ticket/533 + # if a method is not found, an xmlrpclib.Fault should be returned + # raising an exception here will do that; see + # cherrypy.lib.xmlrpcutil.on_error + raise Exception('method "%s" is not supported' % attr) + + conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {}) + _xmlrpc.respond(body, + conf.get('encoding', 'utf-8'), + conf.get('allow_none', 0)) + return cherrypy.serving.response.body + default.exposed = True + + +class SessionAuthTool(HandlerTool): + + def _setargs(self): + for name in dir(cptools.SessionAuth): + if not name.startswith("__"): + setattr(self, name, None) + + +class CachingTool(Tool): + """Caching Tool for CherryPy.""" + + def _wrapper(self, **kwargs): + request = cherrypy.serving.request + if _caching.get(**kwargs): + request.handler = None + else: + if request.cacheable: + # Note the devious technique here of adding hooks on the fly + request.hooks.attach('before_finalize', _caching.tee_output, + priority = 90) + _wrapper.priority = 20 + + def _setup(self): + """Hook caching into cherrypy.request.""" + conf = self._merged_args() + + p = conf.pop("priority", None) + cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, + priority=p, **conf) + + + +class Toolbox(object): + """A collection of Tools. + + This object also functions as a config namespace handler for itself. + Custom toolboxes should be added to each Application's toolboxes dict. + """ + + def __init__(self, namespace): + self.namespace = namespace + + def __setattr__(self, name, value): + # If the Tool._name is None, supply it from the attribute name. + if isinstance(value, Tool): + if value._name is None: + value._name = name + value.namespace = self.namespace + object.__setattr__(self, name, value) + + def __enter__(self): + """Populate request.toolmaps from tools specified in config.""" + cherrypy.serving.request.toolmaps[self.namespace] = map = {} + def populate(k, v): + toolname, arg = k.split(".", 1) + bucket = map.setdefault(toolname, {}) + bucket[arg] = v + return populate + + def __exit__(self, exc_type, exc_val, exc_tb): + """Run tool._setup() for each tool in our toolmap.""" + map = cherrypy.serving.request.toolmaps.get(self.namespace) + if map: + for name, settings in map.items(): + if settings.get("on", False): + tool = getattr(self, name) + tool._setup() + + +class DeprecatedTool(Tool): + + _name = None + warnmsg = "This Tool is deprecated." + + def __init__(self, point, warnmsg=None): + self.point = point + if warnmsg is not None: + self.warnmsg = warnmsg + + def __call__(self, *args, **kwargs): + warnings.warn(self.warnmsg) + def tool_decorator(f): + return f + return tool_decorator + + def _setup(self): + warnings.warn(self.warnmsg) + + +default_toolbox = _d = Toolbox("tools") +_d.session_auth = SessionAuthTool(cptools.session_auth) +_d.allow = Tool('on_start_resource', cptools.allow) +_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) +_d.response_headers = Tool('on_start_resource', cptools.response_headers) +_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) +_d.log_headers = Tool('before_error_response', cptools.log_request_headers) +_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) +_d.err_redirect = ErrorTool(cptools.redirect) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) +_d.decode = Tool('before_request_body', encoding.decode) +# the order of encoding, gzip, caching is important +_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) +_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) +_d.staticdir = HandlerTool(static.staticdir) +_d.staticfile = HandlerTool(static.staticfile) +_d.sessions = SessionTool() +_d.xmlrpc = ErrorTool(_xmlrpc.on_error) +_d.caching = CachingTool('before_handler', _caching.get, 'caching') +_d.expires = Tool('before_finalize', _caching.expires) +_d.tidy = DeprecatedTool('before_finalize', + "The tidy tool has been removed from the standard distribution of CherryPy. " + "The most recent version can be found at http://tools.cherrypy.org/browser.") +_d.nsgmls = DeprecatedTool('before_finalize', + "The nsgmls tool has been removed from the standard distribution of CherryPy. " + "The most recent version can be found at http://tools.cherrypy.org/browser.") +_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) +_d.referer = Tool('before_request_body', cptools.referer) +_d.basic_auth = Tool('on_start_resource', auth.basic_auth) +_d.digest_auth = Tool('on_start_resource', auth.digest_auth) +_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) +_d.flatten = Tool('before_finalize', cptools.flatten) +_d.accept = Tool('on_start_resource', cptools.accept) +_d.redirect = Tool('on_start_resource', cptools.redirect) +_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) +_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) +_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) +_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) +_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) + +del _d, cptools, encoding, auth, static diff --git a/cherrypy/_cptools.pyc b/cherrypy/_cptools.pyc new file mode 100644 index 0000000000000000000000000000000000000000..024b1d27e032297316094a79b2db5e12ee27c90f GIT binary patch literal 20911 zcmds9TWlQHc|N;Ku1J~^b)_WBmoaUJE16o!mpHL3*`_HwcH_{_NU0Lq&U&~zB!}GH zS!`i>@op|UUez4d!>CY&Bzl2Y`HDXN1m?fkY)2W(n z#VjFX^HsA{&GI#~R5P8L`4E*yOlQO_jhfD=@#|&I`?IzlX>ZchTLnq_nF?LS(-Gxy=G~z>FqO1`%G`jEKQl-ezUaSR5;Cj zs&~LF9W=c|X6djkol@x$vovkL@7MREX6dNu9WzVEP|!GdcWx$ zG5(lVG@X~8FrA}W56AM-2TbR9*2DdI>4TiMMcCsckao7)qr{J09|RQWSm<;Ve z^=C~7^RKBUjud-~`J?=E*(5(_{D)ZBc_J(Md{#1-l{}}0cK^?1h386z&u4|ts}RSx z&#a%%3eT4cFJy%mRESAp=U>ncoW>*V{0n+y_xZ)F{)<^BsQ*${_)@75%e2&Ts#G|y zo_Qkbd0vmOko{);Wj)FX>192_eDG*t=c9$}(NQjn@RMyPTyZwN z?bvB}-DE8suC6(8*z+fP9v=14LAbKwN67k4u;L_Z{&NKCOHj4%Wb@g?YW-^%rSJP4zthZnxV+}0?dq`S_Y=FU zIElPqbuC#5qfIaBU^6;SjA?f}?5*P`UeLt~H-lsitHgBkzB*wWjf4KG?Ox@t4*TsS z2>WQ*4kIrKqub1W&cXcpTASaQbGR|t#J#xG64iqCn$r&ZE5Yh8Dn<_ao#0x~!Ff3O zxemNl-&w=#yMBbd@A=sKPK<5loQrBn1*D`={H+0~ z0;lB6v|>LQ4ra7vJn}gwNN^+|2W%Ya!0+T8g#EVffR-KCzdH2e#A#SEb5?v0O99oz zpna?f)8(qT7&`~-OG@wQkp#U#*C(lcQ!9_EQJVgXCl&CA@}cGrvJ=0xzH_*raM zz`(Wivp{BkA4v)xjJaMhNyV%KLX4V?Q4_sllA8Izm=91mLID*$$FJ+7=DIO`lp|j^ z*Q=&KdrQr>p6)5t9AnK7s%Cv$>4b4EHR_zhi%49Y2urCs1kuv!NTOM-TZ$skEs zX*IY8n$PH?X_pspX#qZ_QDv_9dLOhM{@k=tPl(oE~hzC&uX!(l&Z za4N}2+Qh|glW^|su)(~N_(9|r&b{*mkjMM)v?Y26+wXuQNZ4_6ux%%^3h5H9#t)-* ze2}ecla;B;!OCQ1vdVP45`Tj|WwM*}5mV6h!-SNdQ?b(tB9LWwyXm|R((*lsmF#$W zlmP{i&yoHhw0S5!QgtDzSaCZXM$Qgd($r!(bemJ;54YfDeBuoxKHw@)B2pn)Vp!ly z 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 literal 0 HcmV?d00001 diff --git a/cherrypy/_cptree.py b/cherrypy/_cptree.py new file mode 100644 index 0000000..3aa4b9e --- /dev/null +++ b/cherrypy/_cptree.py @@ -0,0 +1,290 @@ +"""CherryPy Application and Tree objects.""" + +import os +import sys + +import cherrypy +from cherrypy._cpcompat import ntou, py3k +from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools +from cherrypy.lib import httputil + + +class Application(object): + """A CherryPy Application. + + Servers and gateways should not instantiate Request objects directly. + Instead, they should ask an Application object for a request object. + + An instance of this class may also be used as a WSGI callable + (WSGI application object) for itself. + """ + + root = None + """The top-most container of page handlers for this app. Handlers should + be arranged in a hierarchy of attributes, matching the expected URI + hierarchy; the default dispatcher then searches this hierarchy for a + matching handler. When using a dispatcher other than the default, + this value may be None.""" + + config = {} + """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict + of {key: value} pairs.""" + + namespaces = _cpconfig.NamespaceSet() + toolboxes = {'tools': cherrypy.tools} + + log = None + """A LogManager instance. See _cplogging.""" + + wsgiapp = None + """A CPWSGIApp instance. See _cpwsgi.""" + + request_class = _cprequest.Request + response_class = _cprequest.Response + + relative_urls = False + + def __init__(self, root, script_name="", config=None): + self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) + self.root = root + self.script_name = script_name + self.wsgiapp = _cpwsgi.CPWSGIApp(self) + + self.namespaces = self.namespaces.copy() + self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) + self.namespaces["wsgi"] = self.wsgiapp.namespace_handler + + self.config = self.__class__.config.copy() + if config: + self.merge(config) + + def __repr__(self): + return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, + self.root, self.script_name) + + script_name_doc = """The URI "mount point" for this app. A mount point is that portion of + the URI which is constant for all URIs that are serviced by this + application; it does not include scheme, host, or proxy ("virtual host") + portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + def _get_script_name(self): + if self._script_name is None: + # None signals that the script name should be pulled from WSGI environ. + return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/") + return self._script_name + def _set_script_name(self, value): + if value: + value = value.rstrip("/") + self._script_name = value + script_name = property(fget=_get_script_name, fset=_set_script_name, + doc=script_name_doc) + + def merge(self, config): + """Merge the given config into self.config.""" + _cpconfig.merge(self.config, config) + + # Handle namespaces specified in config. + self.namespaces(self.config.get("/", {})) + + def find_config(self, path, key, default=None): + """Return the most-specific value for key along path, or default.""" + trail = path or "/" + while trail: + nodeconf = self.config.get(trail, {}) + + if key in nodeconf: + return nodeconf[key] + + lastslash = trail.rfind("/") + if lastslash == -1: + break + elif lastslash == 0 and trail != "/": + trail = "/" + else: + trail = trail[:lastslash] + + return default + + def get_serving(self, local, remote, scheme, sproto): + """Create and return a Request and Response object.""" + req = self.request_class(local, remote, scheme, sproto) + req.app = self + + for name, toolbox in self.toolboxes.items(): + req.namespaces[name] = toolbox + + resp = self.response_class() + cherrypy.serving.load(req, resp) + cherrypy.engine.publish('acquire_thread') + cherrypy.engine.publish('before_request') + + return req, resp + + def release_serving(self): + """Release the current serving (request and response).""" + req = cherrypy.serving.request + + cherrypy.engine.publish('after_request') + + try: + req.close() + except: + cherrypy.log(traceback=True, severity=40) + + cherrypy.serving.clear() + + def __call__(self, environ, start_response): + return self.wsgiapp(environ, start_response) + + +class Tree(object): + """A registry of CherryPy applications, mounted at diverse points. + + An instance of this class may also be used as a WSGI callable + (WSGI application object), in which case it dispatches to all + mounted apps. + """ + + apps = {} + """ + A dict of the form {script name: application}, where "script name" + is a string declaring the URI mount point (no trailing slash), and + "application" is an instance of cherrypy.Application (or an arbitrary + WSGI callable if you happen to be using a WSGI server).""" + + def __init__(self): + self.apps = {} + + def mount(self, root, script_name="", config=None): + """Mount a new app from a root object, script_name, and config. + + root + An instance of a "controller class" (a collection of page + handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. + + script_name + A string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the + URL at which to mount the given root. For example, if root.index() + will handle requests to "http://www.example.com:8080/dept/app1/", + then the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the + root of the URI, it MUST be an empty string (not "/"). + + config + A file or dict containing application config. + """ + if script_name is None: + raise TypeError( + "The 'script_name' argument may not be None. Application " + "objects may, however, possess a script_name of None (in " + "order to inpect the WSGI environ for SCRIPT_NAME upon each " + "request). You cannot mount such Applications on this Tree; " + "you must pass them to a WSGI server interface directly.") + + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip("/") + + if isinstance(root, Application): + app = root + if script_name != "" and script_name != app.script_name: + raise ValueError("Cannot specify a different script name and " + "pass an Application instance to cherrypy.mount") + script_name = app.script_name + else: + app = Application(root, script_name) + + # If mounted at "", add favicon.ico + if (script_name == "" and root is not None + and not hasattr(root, "favicon_ico")): + favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), + "favicon.ico") + root.favicon_ico = tools.staticfile.handler(favicon) + + if config: + app.merge(config) + + self.apps[script_name] = app + + return app + + def graft(self, wsgi_callable, script_name=""): + """Mount a wsgi callable at the given script_name.""" + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip("/") + self.apps[script_name] = wsgi_callable + + def script_name(self, path=None): + """The script_name of the app at the given path, or None. + + If path is None, cherrypy.request is used. + """ + if path is None: + try: + request = cherrypy.serving.request + path = httputil.urljoin(request.script_name, + request.path_info) + except AttributeError: + return None + + while True: + if path in self.apps: + return path + + if path == "": + return None + + # Move one node up the tree and try again. + path = path[:path.rfind("/")] + + def __call__(self, environ, start_response): + # If you're calling this, then you're probably setting SCRIPT_NAME + # to '' (some WSGI servers always set SCRIPT_NAME to ''). + # Try to look up the app using the full path. + env1x = environ + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) + path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), + env1x.get('PATH_INFO', '')) + sn = self.script_name(path or "/") + if sn is None: + start_response('404 Not Found', []) + return [] + + app = self.apps[sn] + + # Correct the SCRIPT_NAME and PATH_INFO environ entries. + environ = environ.copy() + if not py3k: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 2/WSGI u.0: all strings MUST be of type unicode + enc = environ[ntou('wsgi.url_encoding')] + environ[ntou('SCRIPT_NAME')] = sn.decode(enc) + environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc) + else: + # Python 2/WSGI 1.x: all strings MUST be of type str + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip("/")):] + else: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 3/WSGI u.0: all strings MUST be full unicode + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip("/")):] + else: + # Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str + environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1') + environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1') + return app(environ, start_response) diff --git a/cherrypy/_cptree.pyc b/cherrypy/_cptree.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43264b30274d2e698a74e682639c5af5bd8d0ab0 GIT binary patch literal 9761 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 T>zZ(cZT!wU9cQh6_Rs$h%PpW8 literal 0 HcmV?d00001 diff --git a/cherrypy/_cpwsgi.py b/cherrypy/_cpwsgi.py new file mode 100644 index 0000000..91cd044 --- /dev/null +++ b/cherrypy/_cpwsgi.py @@ -0,0 +1,408 @@ +"""WSGI interface (see PEP 333 and 3333). + +Note that WSGI environ keys and values are 'native strings'; that is, +whatever the type of "" is. For Python 2, that's a byte string; for Python 3, +it's a unicode string. But PEP 3333 says: "even if Python's str type is +actually Unicode "under the hood", the content of native strings must +still be translatable to bytes via the Latin-1 encoding!" +""" + +import sys as _sys + +import cherrypy as _cherrypy +from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr +from cherrypy import _cperror +from cherrypy.lib import httputil + + +def downgrade_wsgi_ux_to_1x(environ): + """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.""" + env1x = {} + + url_encoding = environ[ntou('wsgi.url_encoding')] + for k, v in list(environ.items()): + if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: + v = v.encode(url_encoding) + elif isinstance(v, unicodestr): + v = v.encode('ISO-8859-1') + env1x[k.encode('ISO-8859-1')] = v + + return env1x + + +class VirtualHost(object): + """Select a different WSGI application based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different applications. For example:: + + root = Root() + RootApp = cherrypy.Application(root) + Domain2App = cherrypy.Application(root) + SecureApp = cherrypy.Application(Secure()) + + vhost = cherrypy._cpwsgi.VirtualHost(RootApp, + domains={'www.domain2.example': Domain2App, + 'www.domain2.example:443': SecureApp, + }) + + cherrypy.tree.graft(vhost) + """ + default = None + """Required. The default WSGI application.""" + + use_x_forwarded_host = True + """If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying.""" + + domains = {} + """A dict of {host header value: application} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding WSGI application + will be called instead of the default. Note that you often need + separate entries for "example.com" and "www.example.com". + In addition, "Host" headers may contain the port number. + """ + + def __init__(self, default, domains=None, use_x_forwarded_host=True): + self.default = default + self.domains = domains or {} + self.use_x_forwarded_host = use_x_forwarded_host + + def __call__(self, environ, start_response): + domain = environ.get('HTTP_HOST', '') + if self.use_x_forwarded_host: + domain = environ.get("HTTP_X_FORWARDED_HOST", domain) + + nextapp = self.domains.get(domain) + if nextapp is None: + nextapp = self.default + return nextapp(environ, start_response) + + +class InternalRedirector(object): + """WSGI middleware that handles raised cherrypy.InternalRedirect.""" + + def __init__(self, nextapp, recursive=False): + self.nextapp = nextapp + self.recursive = recursive + + def __call__(self, environ, start_response): + redirections = [] + while True: + environ = environ.copy() + try: + return self.nextapp(environ, start_response) + except _cherrypy.InternalRedirect: + ir = _sys.exc_info()[1] + sn = environ.get('SCRIPT_NAME', '') + path = environ.get('PATH_INFO', '') + qs = environ.get('QUERY_STRING', '') + + # Add the *previous* path_info + qs to redirections. + old_uri = sn + path + if qs: + old_uri += "?" + qs + redirections.append(old_uri) + + if not self.recursive: + # Check to see if the new URI has been redirected to already + new_uri = sn + ir.path + if ir.query_string: + new_uri += "?" + ir.query_string + if new_uri in redirections: + ir.request.close() + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % new_uri) + + # Munge the environment and try again. + environ['REQUEST_METHOD'] = "GET" + environ['PATH_INFO'] = ir.path + environ['QUERY_STRING'] = ir.query_string + environ['wsgi.input'] = BytesIO() + environ['CONTENT_LENGTH'] = "0" + environ['cherrypy.previous_request'] = ir.request + + +class ExceptionTrapper(object): + """WSGI middleware that traps exceptions.""" + + def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): + self.nextapp = nextapp + self.throws = throws + + def __call__(self, environ, start_response): + return _TrappedResponse(self.nextapp, environ, start_response, self.throws) + + +class _TrappedResponse(object): + + response = iter([]) + + def __init__(self, nextapp, environ, start_response, throws): + self.nextapp = nextapp + self.environ = environ + self.start_response = start_response + self.throws = throws + self.started_response = False + self.response = self.trap(self.nextapp, self.environ, self.start_response) + self.iter_response = iter(self.response) + + def __iter__(self): + self.started_response = True + return self + + if py3k: + def __next__(self): + return self.trap(next, self.iter_response) + else: + def next(self): + return self.trap(self.iter_response.next) + + def close(self): + if hasattr(self.response, 'close'): + self.response.close() + + def trap(self, func, *args, **kwargs): + try: + return func(*args, **kwargs) + except self.throws: + raise + except StopIteration: + raise + except: + tb = _cperror.format_exc() + #print('trapped (started %s):' % self.started_response, tb) + _cherrypy.log(tb, severity=40) + if not _cherrypy.request.show_tracebacks: + tb = "" + s, h, b = _cperror.bare_error(tb) + if py3k: + # What fun. + s = s.decode('ISO-8859-1') + h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in h] + if self.started_response: + # Empty our iterable (so future calls raise StopIteration) + self.iter_response = iter([]) + else: + self.iter_response = iter(b) + + try: + self.start_response(s, h, _sys.exc_info()) + except: + # "The application must not trap any exceptions raised by + # start_response, if it called start_response with exc_info. + # Instead, it should allow such exceptions to propagate + # back to the server or gateway." + # But we still log and call close() to clean up ourselves. + _cherrypy.log(traceback=True, severity=40) + raise + + if self.started_response: + return ntob("").join(b) + else: + return b + + +# WSGI-to-CP Adapter # + + +class AppResponse(object): + """WSGI response iterable for CherryPy applications.""" + + def __init__(self, environ, start_response, cpapp): + self.cpapp = cpapp + try: + if not py3k: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + environ = downgrade_wsgi_ux_to_1x(environ) + self.environ = environ + self.run() + + r = _cherrypy.serving.response + + outstatus = r.output_status + if not isinstance(outstatus, bytestr): + raise TypeError("response.output_status is not a byte string.") + + outheaders = [] + for k, v in r.header_list: + if not isinstance(k, bytestr): + raise TypeError("response.header_list key %r is not a byte string." % k) + if not isinstance(v, bytestr): + raise TypeError("response.header_list value %r is not a byte string." % v) + outheaders.append((k, v)) + + if py3k: + # According to PEP 3333, when using Python 3, the response status + # and headers must be bytes masquerading as unicode; that is, they + # must be of type "str" but are restricted to code points in the + # "latin-1" set. + outstatus = outstatus.decode('ISO-8859-1') + outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in outheaders] + + self.iter_response = iter(r.body) + self.write = start_response(outstatus, outheaders) + except: + self.close() + raise + + def __iter__(self): + return self + + if py3k: + def __next__(self): + return next(self.iter_response) + else: + def next(self): + return self.iter_response.next() + + def close(self): + """Close and de-reference the current request and response. (Core)""" + self.cpapp.release_serving() + + def run(self): + """Create a Request object using environ.""" + env = self.environ.get + + local = httputil.Host('', int(env('SERVER_PORT', 80)), + env('SERVER_NAME', '')) + remote = httputil.Host(env('REMOTE_ADDR', ''), + int(env('REMOTE_PORT', -1) or -1), + env('REMOTE_HOST', '')) + scheme = env('wsgi.url_scheme') + sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") + request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) + + # LOGON_USER is served by IIS, and is the name of the + # user after having been mapped to a local account. + # Both IIS and Apache set REMOTE_USER, when possible. + request.login = env('LOGON_USER') or env('REMOTE_USER') or None + request.multithread = self.environ['wsgi.multithread'] + request.multiprocess = self.environ['wsgi.multiprocess'] + request.wsgi_environ = self.environ + request.prev = env('cherrypy.previous_request', None) + + meth = self.environ['REQUEST_METHOD'] + + path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), + self.environ.get('PATH_INFO', '')) + qs = self.environ.get('QUERY_STRING', '') + + if py3k: + # This isn't perfect; if the given PATH_INFO is in the wrong encoding, + # it may fail to match the appropriate config section URI. But meh. + old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') + new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''), + "request.uri_encoding", 'utf-8') + if new_enc.lower() != old_enc.lower(): + # Even though the path and qs are unicode, the WSGI server is + # required by PEP 3333 to coerce them to ISO-8859-1 masquerading + # as unicode. So we have to encode back to bytes and then decode + # again using the "correct" encoding. + try: + u_path = path.encode(old_enc).decode(new_enc) + u_qs = qs.encode(old_enc).decode(new_enc) + except (UnicodeEncodeError, UnicodeDecodeError): + # Just pass them through without transcoding and hope. + pass + else: + # Only set transcoded values if they both succeed. + path = u_path + qs = u_qs + + rproto = self.environ.get('SERVER_PROTOCOL') + headers = self.translate_headers(self.environ) + rfile = self.environ['wsgi.input'] + request.run(meth, path, qs, rproto, headers, rfile) + + headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', + 'CONTENT_LENGTH': 'Content-Length', + 'CONTENT_TYPE': 'Content-Type', + 'REMOTE_HOST': 'Remote-Host', + 'REMOTE_ADDR': 'Remote-Addr', + } + + def translate_headers(self, environ): + """Translate CGI-environ header names to HTTP header names.""" + for cgiName in environ: + # We assume all incoming header keys are uppercase already. + if cgiName in self.headerNames: + yield self.headerNames[cgiName], environ[cgiName] + elif cgiName[:5] == "HTTP_": + # Hackish attempt at recovering original header names. + translatedHeader = cgiName[5:].replace("_", "-") + yield translatedHeader, environ[cgiName] + + +class CPWSGIApp(object): + """A WSGI application object for a CherryPy Application.""" + + pipeline = [('ExceptionTrapper', ExceptionTrapper), + ('InternalRedirector', InternalRedirector), + ] + """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a + constructor that takes an initial, positional 'nextapp' argument, + plus optional keyword arguments, and returns a WSGI application + (that takes environ and start_response arguments). The 'name' can + be any you choose, and will correspond to keys in self.config.""" + + head = None + """Rather than nest all apps in the pipeline on each call, it's only + done the first time, and the result is memoized into self.head. Set + this to None again if you change self.pipeline after calling self.""" + + config = {} + """A dict whose keys match names listed in the pipeline. Each + value is a further dict which will be passed to the corresponding + named WSGI callable (from the pipeline) as keyword arguments.""" + + response_class = AppResponse + """The class to instantiate and return as the next app in the WSGI chain.""" + + def __init__(self, cpapp, pipeline=None): + self.cpapp = cpapp + self.pipeline = self.pipeline[:] + if pipeline: + self.pipeline.extend(pipeline) + self.config = self.config.copy() + + def tail(self, environ, start_response): + """WSGI application callable for the actual CherryPy application. + + You probably shouldn't call this; call self.__call__ instead, + so that any WSGI middleware in self.pipeline can run first. + """ + return self.response_class(environ, start_response, self.cpapp) + + def __call__(self, environ, start_response): + head = self.head + if head is None: + # Create and nest the WSGI apps in our pipeline (in reverse order). + # Then memoize the result in self.head. + head = self.tail + for name, callable in self.pipeline[::-1]: + conf = self.config.get(name, {}) + head = callable(head, **conf) + self.head = head + return head(environ, start_response) + + def namespace_handler(self, k, v): + """Config handler for the 'wsgi' namespace.""" + if k == "pipeline": + # Note this allows multiple 'wsgi.pipeline' config entries + # (but each entry will be processed in a 'random' order). + # It should also allow developers to set default middleware + # in code (passed to self.__init__) that deployers can add to + # (but not remove) via config. + self.pipeline.extend(v) + elif k == "response_class": + self.response_class = v + else: + name, arg = k.split(".", 1) + bucket = self.config.setdefault(name, {}) + bucket[arg] = v + diff --git a/cherrypy/_cpwsgi.pyc b/cherrypy/_cpwsgi.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90b7b08737d54633c789a8eb2aa52ad06ef9eef3 GIT binary patch literal 12815 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 cherrypy.server -> wsgiserver. + """ + + def __init__(self, server_adapter=cherrypy.server): + self.server_adapter = server_adapter + self.max_request_header_size = self.server_adapter.max_request_header_size or 0 + self.max_request_body_size = self.server_adapter.max_request_body_size or 0 + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + self.wsgi_version = self.server_adapter.wsgi_version + s = wsgiserver.CherryPyWSGIServer + s.__init__(self, server_adapter.bind_addr, cherrypy.tree, + self.server_adapter.thread_pool, + server_name, + max = self.server_adapter.thread_pool_max, + request_queue_size = self.server_adapter.socket_queue_size, + timeout = self.server_adapter.socket_timeout, + shutdown_timeout = self.server_adapter.shutdown_timeout, + ) + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + if sys.version_info >= (3, 0): + ssl_module = self.server_adapter.ssl_module or 'builtin' + else: + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + + self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False) + + def error_log(self, msg="", level=20, traceback=False): + cherrypy.engine.log(msg, level, traceback) + diff --git a/cherrypy/_cpwsgi_server.pyc b/cherrypy/_cpwsgi_server.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86f6897e99937300a5df9f10336a6d85a6b99f97 GIT binary patch literal 2560 zcmbVO-EI>{6h3P^4lxOVKnkS=rtOW3NP3YuMYQ5MlL7S*~%$E~n#Q`Vwsn~vKQsSfEjO*%Bhp%r>VbV>B{@e1EQ>UUYG z19@@u>__2rd8$ica<9u_l4{X+T8qP{hhlGU@AF;pd~BRZlybt^sTTW(JFd{F8JbiK zjCaD@o*!#5ER(4|v*pQ7GRm#<5TCh`skIkLdXkLvZqNLK-$6fub>9P7p7HD0?jN#G zN5Q295FH4-uM znLi3@oE3hYcwxLqlN@Tq%s~N;SwIJu8tedct_1wdOqzIObI15YJ)G8+t}`vp5DDvI z!6RfvfX8~B_QK9eVb5|gwX@ud!Xm8>`t4MtA<$yJ>r1VJTaB%GmD$Am(hO!^Hy3O^ zG$XO|WO=pDCRj^kD!9@XaOT3=!ne{i&`BELxhuTJ^cm!u8-t z|5H|U4Wz0~nW&_|B=fhyf`hVgZmbif%gVd$cVq*r=9T0sE3)Z{_Hv9NhB{j8hbGgW zN6~@Jbr@FM0}`{-p;2Q9D4@|yf#pE)S0Izp&GQr zmW8#M-(=;>y=3HXfLFWv7VrUyDu5d5m?vOJLQz_8Q=2tD2jdMhV4Pc}vt%B!>*g** zSqBLBhqlU#%jDFm8Q=);V4GUJ92tK<`Qe{@)~w|f_V=2ha|%=45J@R7hb%8uG85t=69!37v33} zmyTv@_zPO3ceGfv> zTC^Q)x7MTesN32MyS1o`9sk(?XA|FcYYV&0s15mAw1peX-FC~f9&-yMY)vZNK!rhR_zJdtfiw$m?BBmX0|pFW;J|@s zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO` z$*x_y*u8rke3QwOtB{embw6rwR)6;qO>=_vu z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQT(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz? zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg Kbp8+jKgeIP@z;_7 literal 0 HcmV?d00001 diff --git a/cherrypy/lib/__init__.py b/cherrypy/lib/__init__.py new file mode 100644 index 0000000..3fc0ec5 --- /dev/null +++ b/cherrypy/lib/__init__.py @@ -0,0 +1,45 @@ +"""CherryPy Library""" + +# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3 +from cherrypy.lib.reprconf import unrepr, modules, attributes + +class file_generator(object): + """Yield the given input (a file object) in chunks (default 64k). (Core)""" + + def __init__(self, input, chunkSize=65536): + self.input = input + self.chunkSize = chunkSize + + def __iter__(self): + return self + + def __next__(self): + chunk = self.input.read(self.chunkSize) + if chunk: + return chunk + else: + if hasattr(self.input, 'close'): + self.input.close() + raise StopIteration() + next = __next__ + +def file_generator_limited(fileobj, count, chunk_size=65536): + """Yield the given file object in chunks, stopping after `count` + bytes has been emitted. Default chunk size is 64kB. (Core) + """ + remaining = count + while remaining > 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + +def set_vary_header(response, header_name): + "Add a Vary header to a response" + varies = response.headers.get("Vary", "") + varies = [x.strip() for x in varies.split(",") if x.strip()] + if header_name not in varies: + varies.append(header_name) + response.headers['Vary'] = ", ".join(varies) diff --git a/cherrypy/lib/__init__.pyc b/cherrypy/lib/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99607dbfe667c394a8be5e7fd82e391e7d2374a0 GIT binary patch literal 2130 zcmbVN;cnbS5T3Ptmt06_K%_zlwabqRM~dzTst`gIqD>L1gihd-ih2YbpS^eS#b>+T zy((8FLTdUNJROg~1Hd=qyBzSVtrO3UcV~BI=bLZV|7&mO&Yyn_Qd&Jee!svS%6u{%n!qBU*F@Ju&qiI!I+S(kwW84$Wm`Nk+NNxq zhGCDl-iJ6oGflJnajAdE$4$~K^S|+g?A>)C7G-1V#%)2JSJ|R4*6lz~oNMy&!r}9M zd`|Mh#8Xq6CUI4Ba|RDnkMPRCGxcL-9nBIESIa)F5O_$DxQ<$C(p1!~CUb6BS^{nqm^M60vEB zw-xp?Xvbfle4otC%ac^HT`y0He0&ned6~O7?$=AV4XgFHV8dIH>Zz22Sicf5&mh`$ zcx)$;MvT|HI(DXs<2`I(`MA`&WtDR!ZU<1{c7}h+*36@8MOV-jHFsGKoxid`rCxi~ zJX|jbxFA(2d4%?Am0#X}AgN7@%9@Z5AUSk=#>OO>BvY#IEU{d45hurSphH*HPY`zE z@~RY&BmwPqTn6u$iqc#F6Q+jUg&=jHx+?HKRQoDq8myoyfoB{G8REF520qy!je{@<>|%R_EnZClkP?Pt$5qy3-wvXS_rw(STTw4d`v=xpOA#YyEh|VFa=$8OMR?%@fWBal`=0FOFqRhTm24k z!##k5`~$ixkER&+2vkHD1$vKvy~Cgbu^GNWp?e$sH;^c^F}AKsYoyS5*1?ugYD7oU zRF3pugG0O03P4li1P@ymz+IX?ZkJl_pQNr$naQ5BDlbD$ivV)t?mdjOyT!~&S@j}m za$HU9`{Jz;yMr+k3uYCw3d6>woIq{%CjaAH)K^IsgCw literal 0 HcmV?d00001 diff --git a/cherrypy/lib/auth.py b/cherrypy/lib/auth.py new file mode 100644 index 0000000..7d2f6dc --- /dev/null +++ b/cherrypy/lib/auth.py @@ -0,0 +1,87 @@ +import cherrypy +from cherrypy.lib import httpauth + + +def check_auth(users, encrypt=None, realm=None): + """If an authorization header contains credentials, return True, else False.""" + request = cherrypy.serving.request + if 'authorization' in request.headers: + # make sure the provided credentials are correctly set + ah = httpauth.parseAuthorization(request.headers['authorization']) + if ah is None: + raise cherrypy.HTTPError(400, 'Bad Request') + + if not encrypt: + encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5] + + if hasattr(users, '__call__'): + try: + # backward compatibility + users = users() # expect it to return a dictionary + + if not isinstance(users, dict): + raise ValueError("Authentication users must be a dictionary") + + # fetch the user password + password = users.get(ah["username"], None) + except TypeError: + # returns a password (encrypted or clear text) + password = users(ah["username"]) + else: + if not isinstance(users, dict): + raise ValueError("Authentication users must be a dictionary") + + # fetch the user password + password = users.get(ah["username"], None) + + # validate the authorization by re-computing it here + # and compare it with what the user-agent provided + if httpauth.checkResponse(ah, password, method=request.method, + encrypt=encrypt, realm=realm): + request.login = ah["username"] + return True + + request.login = False + return False + +def basic_auth(realm, users, encrypt=None, debug=False): + """If auth fails, raise 401 with a basic authentication header. + + realm + A string containing the authentication realm. + + users + A dict of the form: {username: password} or a callable returning a dict. + + encrypt + callable used to encrypt the password returned from the user-agent. + if None it defaults to a md5 encryption. + + """ + if check_auth(users, encrypt): + if debug: + cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH') + return + + # inform the user-agent this path is protected + cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm) + + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + +def digest_auth(realm, users, debug=False): + """If auth fails, raise 401 with a digest authentication header. + + realm + A string containing the authentication realm. + users + A dict of the form: {username: password} or a callable returning a dict. + """ + if check_auth(users, realm=realm): + if debug: + cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH') + return + + # inform the user-agent this path is protected + cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm) + + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") diff --git a/cherrypy/lib/auth.pyc b/cherrypy/lib/auth.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37ad37655e734930afd873afa479db375494b7ab GIT binary patch literal 2753 zcmcIlTW=dh6h5=QG)a@X7h0MMssZs}piPU^zEl+=O+y<5E?S#g6xEvSj+1rl-E?M5 zl#;yA@(w?MH+~ln{j*VlhVKh!cDg(T|F9q60;KhuC;V7%0!r>1MINMOWiZtU*^vzM2E=Y zBz=4p8kAQiC&;&hXySSvrW2LdCVzI2?-BJxk#nwZj{{79Xes^mg+k z*Le+Mt*1?{xetZQ9-ELAx!$%WwN58R9!!>doj9A?JY;6zuV`n($mP1@M)r!f18?=G z7`IEvpXfP1+0W>s=SX)qDo4qGx@R7I9>*2@;y=k@4 zX_jzO#_fwKPv>T610x21+eI2qK(%?|_IE?5L3h>+hOOp#sA7|BTZhSgSqS^@vwTZ* zH_nM2l$-W3R0?Myc8&9vlbaR>)hB8dxeW}}`<>30tIib;1tq$>c5k)a=`MfOx!+xF ztvtB98n*)@`-{6bCDyj_CIkntfeIdF4JJ>lxa2L$_}6AIvZYTp-LgR=kKY|I6Uxp*;eB18~oTgtR+yx%Ka+Qblv z@NYv~xwUZ*1%JFTDdWZ<+uYzzFQKHo9m%N6BkbH?hoER)HKGe@UY(EX>Ri-Nm1q{S zQMsfRqBljmP`#)cYC%=8HsO8n%llrz>mNek5CF0T*5s}*gk6(@919>m_zlJ>KpHgX zV)wxdFjvWv8ujbc$0<;y+iWt!)CCD)0`{^+liTip0!Wzz$X@qMCJ;9nV148IM|v;A zsL`9oXNfF12GkNTOLH2J2!WkwJhZHRZ~)qbMyvwpFM1K9(#h13H0qcm#)mF?V%{s< z@Rt5zLW5g+(zQcfIHbzNGMfWC<|#)lIrKt|F<+)prgre0>QLzM0tq^aF?NatJy#56 ziW9kNwosR&09j8nOLP`=YI|lh2%bF}Jxp&-{DRU)29kwupdHLN?MF#sz3+_%{xrrq z4<3BpUb?f~URx1;A*G7Vw|ddzqb&VozjIlyz$e9kN8TT7S%+md~Z{+LPGWbBr-y2Kh2K}eU{&C%8? zGfH9#qprb`pNF7R4Fx8tYUQm+uFML?IO+;sZy-vx@MXy)Q0^Kt$9*^<%;Yu{!c_l- zFtsUR>U_zJ2?L8J7r;M*_%Fhw*%r?F7qR}oGv+lCMSA&1;d!TJuK4Ez +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +__doc__ = """This module provides a CherryPy 3.x tool which implements +the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`. + +Example usage, using the built-in checkpassword_dict function which uses a dict +as the credentials store:: + + userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} + checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) + basic_auth = {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'earth', + 'tools.auth_basic.checkpassword': checkpassword, + } + app_config = { '/' : basic_auth } + +""" + +__author__ = 'visteya' +__date__ = 'April 2009' + +import binascii +from cherrypy._cpcompat import base64_decode +import cherrypy + + +def checkpassword_dict(user_password_dict): + """Returns a checkpassword function which checks credentials + against a dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, use + checkpassword_dict(my_credentials_dict) as the value for the + checkpassword argument to basic_auth(). + """ + def checkpassword(realm, user, password): + p = user_password_dict.get(user) + return p and p == password or False + + return checkpassword + + +def basic_auth(realm, checkpassword, debug=False): + """A CherryPy tool which hooks at before_handler to perform + HTTP Basic Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Basic' scheme, this + tool attempts to authenticate the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not 'Basic', or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Basic header. + + realm + A string containing the authentication realm. + + checkpassword + A callable which checks the authentication credentials. + Its signature is checkpassword(realm, username, password). where + username and password are the values obtained from the request's + 'authorization' header. If authentication succeeds, checkpassword + returns True, else it returns False. + + """ + + if '"' in realm: + raise ValueError('Realm cannot contain the " (quote) character.') + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + if auth_header is not None: + try: + scheme, params = auth_header.split(' ', 1) + if scheme.lower() == 'basic': + username, password = base64_decode(params).split(':', 1) + if checkpassword(realm, username, password): + if debug: + cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') + request.login = username + return # successful authentication + except (ValueError, binascii.Error): # split() error, base64.decodestring() error + raise cherrypy.HTTPError(400, 'Bad Request') + + # Respond with 401 status and a WWW-Authenticate header + cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + diff --git a/cherrypy/lib/auth_basic.pyc b/cherrypy/lib/auth_basic.pyc new file mode 100644 index 0000000000000000000000000000000000000000..989a48b86c22652c4e986fe45ceb6bb6646a2ab1 GIT binary patch literal 3531 zcma)9?`|7K5TCOXCvnrJ6si0ZXlX%qL}E&*Kv)!!R^^X`0&)U{DAKXMTictQZ?C(1 zF{aKJihz&s06Yng(8u5r_{IldX6~HWX^~>(_U`s(W@l%9GrvvsuZ#6p{``GAg7R0v z`-gbB9UNkSJsbnr3m~q*UIpSR#0!vAVGkehZxNCO7%sxzA_V2P8tl~|uECF}yaZ|) z;wAVIO{|dSPIH-ze}&8J4zv?P6X%H%nKef`R!&H9cc82te=!zsx4skJm_&>QIvNN) z%n~(JsdsfhP{JvDr0k|cEn#}%lil4f#2x8$B(|bRIVZMq6idC1q}L|h5Yh=;AhNov zVxd#fw!NtR?ag=IdiUE_y?*aIN%lqVWM6IIO{aZ9>vVIS_)V0L1}Zwtq;n%<<1p5d z7ri`<$av{t?sxU+z@Ti=&G*C z8XKZvY^uChJUNnMll#Uo>&|&1Zj)yiv25H*bhjmO+hLai(VCNEkDIg0uCp-~4M__v zJ#0|G-K-IlHris>=4yk*$@kOWlj2s%WQZCKC9NMc&ZjizC@Rmr7bc3G%Pb2cllF9< zoE42XDL6AbIIh?I65{JfJFmvl)$w^NvpNwsZ`^oK|BJuvW`L9aA`Ymi-nkXVDl&M{ zG=ZP4Dqikc9DWAqA@l?I3E((@Nq_(*0oOwDj`F!p3A1w!pM?c~bhA+4NXov{sq?5! z7(f(BJ0>I(YDq_ zTlYE+zW^Kn#r07D$5ohA!M;l4qymQ($X)|aA*bAIbKpH`_pvS(@%|B(m1>erer4== zdZoa5YBbZZp%r{7Qh%is2&W&5o#QOH>p0wP?|+Oz|9&6C!*#Wfb+tcJSJ{~1GZ#JD zi=(9>SPPnTXLmc~L}!Iwd5+xRxpS{X-p;MW-oA{p#d6DQ3lYVH)~Mlim%hA#>mX<+ z0EacOFM-F`3WfNG08)%r00$K~sFo2wSb&2?>gJS#1(+;AT!+IIu>ZjLXL>zYqSlSQ z{!s|`TRN+9T zaiT2YJX4m6l@Wd}4sw}_bWcBxgUY(5QnTtouCO@_u=Hdq8iYP$_4mwW4KYwM#`Q-U zyS5aKJJjwQC*|#tSlJ=ofAYp_%TaXr0_9Sc3w-g}iKu?2%VIcH}AVpK*ni%HBIyjx&ZL znIy7{hlt|9!eT||JY#I8!xp`fGmj~9x}QoU6@@XHCzU2UKnIh85;jg4VZDXxDO)sT zGBg!I;KV0V*qn=ua}#&MbZN7Q!k#rlj&KRNMgbi%^mNoQ9%hk52OyW@p-aV>tfvRZ zmQ=|H38AoU6F8=`+{cSZ6hNakdChI&*rD@;h-6A=DO1GmUK7m+x$$ZpSCm#pUfCA6 zWs3DlsbIYW2u&kX1kxgsExoqgB^(JXQ|HnB%Q&_B`RAYQw6?z7{Up4zwe#uSZT&0C zy9@Z~j*La8s7w|uz{Q!leeHF3jVDK=(dJCPmHv&?Td4jG5;moQwleh+GeC7&99g;Q z5ZETSk!q4No=z}d5%1o!))btE5oTKAlMce+2qDYQWd`}Vs9o2g!wOU4w!zaRk*32U zFX%HdBV{`r8*F`$m_7^OU8l^RU7bo7Y0cAx14J{_=T6<37Gje=b%+9)?^q{R-Mrr+ zviB4}+H_cW=83C{e!!0z=5XkE)hT{EBvQ~OKAfMMDP){yKGklJ>T6`+>MBz6YUN6> z8dQVlgDb%+c-4YugO%Vya5Z?L`drW?U5;NE#wH3wj(^C8jKx=?bNnQ|3d7EeBwX@y zaR3jaEHcAPdX8#`Xr)6YI~3o}bsSFj!%ycv2GGLq7ym4Mz=tP7Z>>@bYL(UCa +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +__doc__ = """An implementation of the server-side of HTTP Digest Access +Authentication, which is described in :rfc:`2617`. + +Example usage, using the built-in get_ha1_dict_plain function which uses a dict +of plaintext passwords as the credentials store:: + + userpassdict = {'alice' : '4x5istwelve'} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) + digest_auth = {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'wonderland', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + } + app_config = { '/' : digest_auth } +""" + +__author__ = 'visteya' +__date__ = 'April 2009' + + +import time +from cherrypy._cpcompat import parse_http_list, parse_keqv_list + +import cherrypy +from cherrypy._cpcompat import md5, ntob +md5_hex = lambda s: md5(ntob(s)).hexdigest() + +qop_auth = 'auth' +qop_auth_int = 'auth-int' +valid_qops = (qop_auth, qop_auth_int) + +valid_algorithms = ('MD5', 'MD5-sess') + + +def TRACE(msg): + cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') + +# Three helper functions for users of the tool, providing three variants +# of get_ha1() functions for three different kinds of credential stores. +def get_ha1_dict_plain(user_password_dict): + """Returns a get_ha1 function which obtains a plaintext password from a + dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, with plaintext + passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the + get_ha1 argument to digest_auth(). + """ + def get_ha1(realm, username): + password = user_password_dict.get(username) + if password: + return md5_hex('%s:%s:%s' % (username, realm, password)) + return None + + return get_ha1 + +def get_ha1_dict(user_ha1_dict): + """Returns a get_ha1 function which obtains a HA1 password hash from a + dictionary of the form: {username : HA1}. + + If you want a dictionary-based authentication scheme, but with + pre-computed HA1 hashes instead of plain-text passwords, use + get_ha1_dict(my_userha1_dict) as the value for the get_ha1 + argument to digest_auth(). + """ + def get_ha1(realm, username): + return user_ha1_dict.get(user) + + return get_ha1 + +def get_ha1_file_htdigest(filename): + """Returns a get_ha1 function which obtains a HA1 password hash from a + flat file with lines of the same format as that produced by the Apache + htdigest utility. For example, for realm 'wonderland', username 'alice', + and password '4x5istwelve', the htdigest line would be:: + + alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c + + If you want to use an Apache htdigest file as the credentials store, + then use get_ha1_file_htdigest(my_htdigest_file) as the value for the + get_ha1 argument to digest_auth(). It is recommended that the filename + argument be an absolute path, to avoid problems. + """ + def get_ha1(realm, username): + result = None + f = open(filename, 'r') + for line in f: + u, r, ha1 = line.rstrip().split(':') + if u == username and r == realm: + result = ha1 + break + f.close() + return result + + return get_ha1 + + +def synthesize_nonce(s, key, timestamp=None): + """Synthesize a nonce value which resists spoofing and can be checked for staleness. + Returns a string suitable as the value for 'nonce' in the www-authenticate header. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + timestamp + An integer seconds-since-the-epoch timestamp + + """ + if timestamp is None: + timestamp = int(time.time()) + h = md5_hex('%s:%s:%s' % (timestamp, s, key)) + nonce = '%s:%s' % (timestamp, h) + return nonce + + +def H(s): + """The hash function H""" + return md5_hex(s) + + +class HttpDigestAuthorization (object): + """Class to parse a Digest Authorization header and perform re-calculation + of the digest. + """ + + def errmsg(self, s): + return 'Digest Authorization header: %s' % s + + def __init__(self, auth_header, http_method, debug=False): + self.http_method = http_method + self.debug = debug + scheme, params = auth_header.split(" ", 1) + self.scheme = scheme.lower() + if self.scheme != 'digest': + raise ValueError('Authorization scheme is not "Digest"') + + self.auth_header = auth_header + + # make a dict of the params + items = parse_http_list(params) + paramsd = parse_keqv_list(items) + + self.realm = paramsd.get('realm') + self.username = paramsd.get('username') + self.nonce = paramsd.get('nonce') + self.uri = paramsd.get('uri') + self.method = paramsd.get('method') + self.response = paramsd.get('response') # the response digest + self.algorithm = paramsd.get('algorithm', 'MD5') + self.cnonce = paramsd.get('cnonce') + self.opaque = paramsd.get('opaque') + self.qop = paramsd.get('qop') # qop + self.nc = paramsd.get('nc') # nonce count + + # perform some correctness checks + if self.algorithm not in valid_algorithms: + raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm)) + + has_reqd = self.username and \ + self.realm and \ + self.nonce and \ + self.uri and \ + self.response + if not has_reqd: + raise ValueError(self.errmsg("Not all required parameters are present.")) + + if self.qop: + if self.qop not in valid_qops: + raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop)) + if not (self.cnonce and self.nc): + raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present")) + else: + if self.cnonce or self.nc: + raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present")) + + + def __str__(self): + return 'authorization : %s' % self.auth_header + + def validate_nonce(self, s, key): + """Validate the nonce. + Returns True if nonce was generated by synthesize_nonce() and the timestamp + is not spoofed, else returns False. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + Both s and key must be the same values which were used to synthesize the nonce + we are trying to validate. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + if self.debug: + TRACE('validate_nonce: %s' % is_valid) + return is_valid + except ValueError: # split() error + pass + return False + + + def is_nonce_stale(self, max_age_seconds=600): + """Returns True if a validated nonce is stale. The nonce contains a + timestamp in plaintext and also a secure hash of the timestamp. You should + first validate the nonce to ensure the plaintext timestamp is not spoofed. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + if int(timestamp) + max_age_seconds > int(time.time()): + return False + except ValueError: # int() error + pass + if self.debug: + TRACE("nonce is stale") + return True + + + def HA2(self, entity_body=''): + """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" + # RFC 2617 3.2.2.3 + # If the "qop" directive's value is "auth" or is unspecified, then A2 is: + # A2 = method ":" digest-uri-value + # + # If the "qop" value is "auth-int", then A2 is: + # A2 = method ":" digest-uri-value ":" H(entity-body) + if self.qop is None or self.qop == "auth": + a2 = '%s:%s' % (self.http_method, self.uri) + elif self.qop == "auth-int": + a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body)) + else: + # in theory, this should never happen, since I validate qop in __init__() + raise ValueError(self.errmsg("Unrecognized value for qop!")) + return H(a2) + + + def request_digest(self, ha1, entity_body=''): + """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. + + ha1 + The HA1 string obtained from the credentials store. + + entity_body + If 'qop' is set to 'auth-int', then A2 includes a hash + of the "entity body". The entity body is the part of the + message which follows the HTTP headers. See :rfc:`2617` section + 4.3. This refers to the entity the user agent sent in the request which + has the Authorization header. Typically GET requests don't have an entity, + and POST requests do. + + """ + ha2 = self.HA2(entity_body) + # Request-Digest -- RFC 2617 3.2.2.1 + if self.qop: + req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2) + else: + req = "%s:%s" % (self.nonce, ha2) + + # RFC 2617 3.2.2.2 + # + # If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + # + # If the "algorithm" directive's value is "MD5-sess", then A1 is + # calculated only once - on the first request by the client following + # receipt of a WWW-Authenticate challenge from the server. + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + if self.algorithm == 'MD5-sess': + ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) + + digest = H('%s:%s' % (ha1, req)) + return digest + + + +def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False): + """Constructs a WWW-Authenticate header for Digest authentication.""" + if qop not in valid_qops: + raise ValueError("Unsupported value for qop: '%s'" % qop) + if algorithm not in valid_algorithms: + raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) + + if nonce is None: + nonce = synthesize_nonce(realm, key) + s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( + realm, nonce, algorithm, qop) + if stale: + s += ', stale="true"' + return s + + +def digest_auth(realm, get_ha1, key, debug=False): + """A CherryPy tool which hooks at before_handler to perform + HTTP Digest Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Digest' scheme, this + tool authenticates the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not "Digest", or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Digest header. + + realm + A string containing the authentication realm. + + get_ha1 + A callable which looks up a username in a credentials store + and returns the HA1 string, which is defined in the RFC to be + MD5(username : realm : password). The function's signature is: + ``get_ha1(realm, username)`` + where username is obtained from the request's 'authorization' header. + If username is not found in the credentials store, get_ha1() returns + None. + + key + A secret string known only to the server, used in the synthesis of nonces. + + """ + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + nonce_is_stale = False + if auth_header is not None: + try: + auth = HttpDigestAuthorization(auth_header, request.method, debug=debug) + except ValueError: + raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.") + + if debug: + TRACE(str(auth)) + + if auth.validate_nonce(realm, key): + ha1 = get_ha1(realm, auth.username) + if ha1 is not None: + # note that for request.body to be available we need to hook in at + # before_handler, not on_start_resource like 3.1.x digest_auth does. + digest = auth.request_digest(ha1, entity_body=request.body) + if digest == auth.response: # authenticated + if debug: + TRACE("digest matches auth.response") + # Now check if nonce is stale. + # The choice of ten minutes' lifetime for nonce is somewhat arbitrary + nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600) + if not nonce_is_stale: + request.login = auth.username + if debug: + TRACE("authentication of %s successful" % auth.username) + return + + # Respond with 401 status and a WWW-Authenticate header + header = www_authenticate(realm, key, stale=nonce_is_stale) + if debug: + TRACE(header) + cherrypy.serving.response.headers['WWW-Authenticate'] = header + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + diff --git a/cherrypy/lib/auth_digest.pyc b/cherrypy/lib/auth_digest.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62264530cc462083194e452541e8269494d4a4af GIT binary patch literal 13019 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 Ir}^3c1w+hlvH$=8 literal 0 HcmV?d00001 diff --git a/cherrypy/lib/caching.py b/cherrypy/lib/caching.py new file mode 100644 index 0000000..435b9dc --- /dev/null +++ b/cherrypy/lib/caching.py @@ -0,0 +1,465 @@ +""" +CherryPy implements a simple caching system as a pluggable Tool. This tool tries +to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but +it's probably good enough for most sites. + +In general, GET responses are cached (along with selecting headers) and, if +another request arrives for the same resource, the caching Tool will return 304 +Not Modified if possible, or serve the cached response otherwise. It also sets +request.cached to True if serving a cached representation, and sets +request.cacheable to False (so it doesn't get cached again). + +If POST, PUT, or DELETE requests are made for a cached resource, they invalidate +(delete) any cached response. + +Usage +===== + +Configuration file example:: + + [/] + tools.caching.on = True + tools.caching.delay = 3600 + +You may use a class other than the default +:class:`MemoryCache` by supplying the config +entry ``cache_class``; supply the full dotted name of the replacement class +as the config value. It must implement the basic methods ``get``, ``put``, +``delete``, and ``clear``. + +You may set any attribute, including overriding methods, on the cache +instance by providing them in config. The above sets the +:attr:`delay` attribute, for example. +""" + +import datetime +import sys +import threading +import time + +import cherrypy +from cherrypy.lib import cptools, httputil +from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted + + +class Cache(object): + """Base class for Cache implementations.""" + + def get(self): + """Return the current variant if in the cache, else None.""" + raise NotImplemented + + def put(self, obj, size): + """Store the current variant in the cache.""" + raise NotImplemented + + def delete(self): + """Remove ALL cached variants of the current resource.""" + raise NotImplemented + + def clear(self): + """Reset the cache to its initial, empty state.""" + raise NotImplemented + + + +# ------------------------------- Memory Cache ------------------------------- # + + +class AntiStampedeCache(dict): + """A storage system for cached items which reduces stampede collisions.""" + + def wait(self, key, timeout=5, debug=False): + """Return the cached value for the given key, or None. + + If timeout is not None, and the value is already + being calculated by another thread, wait until the given timeout has + elapsed. If the value is available before the timeout expires, it is + returned. If not, None is returned, and a sentinel placed in the cache + to signal other threads to wait. + + If timeout is None, no waiting is performed nor sentinels used. + """ + value = self.get(key) + if isinstance(value, threading._Event): + if timeout is None: + # Ignore the other thread and recalc it ourselves. + if debug: + cherrypy.log('No timeout', 'TOOLS.CACHING') + return None + + # Wait until it's done or times out. + if debug: + cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING') + value.wait(timeout) + if value.result is not None: + # The other thread finished its calculation. Use it. + if debug: + cherrypy.log('Result!', 'TOOLS.CACHING') + return value.result + # Timed out. Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + + return None + elif value is None: + # Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + return value + + def __setitem__(self, key, value): + """Set the cached value for the given key.""" + existing = self.get(key) + dict.__setitem__(self, key, value) + if isinstance(existing, threading._Event): + # Set Event.result so other threads waiting on it have + # immediate access without needing to poll the cache again. + existing.result = value + existing.set() + + +class MemoryCache(Cache): + """An in-memory cache for varying response content. + + Each key in self.store is a URI, and each value is an AntiStampedeCache. + The response for any given URI may vary based on the values of + "selecting request headers"; that is, those named in the Vary + response header. We assume the list of header names to be constant + for each URI throughout the lifetime of the application, and store + that list in ``self.store[uri].selecting_headers``. + + The items contained in ``self.store[uri]`` have keys which are tuples of + request header values (in the same order as the names in its + selecting_headers), and values which are the actual responses. + """ + + maxobjects = 1000 + """The maximum number of cached objects; defaults to 1000.""" + + maxobj_size = 100000 + """The maximum size of each cached object in bytes; defaults to 100 KB.""" + + maxsize = 10000000 + """The maximum size of the entire cache in bytes; defaults to 10 MB.""" + + delay = 600 + """Seconds until the cached content expires; defaults to 600 (10 minutes).""" + + antistampede_timeout = 5 + """Seconds to wait for other threads to release a cache lock.""" + + expire_freq = 0.1 + """Seconds to sleep between cache expiration sweeps.""" + + debug = False + + def __init__(self): + self.clear() + + # Run self.expire_cache in a separate daemon thread. + t = threading.Thread(target=self.expire_cache, name='expire_cache') + self.expiration_thread = t + set_daemon(t, True) + t.start() + + def clear(self): + """Reset the cache to its initial, empty state.""" + self.store = {} + self.expirations = {} + self.tot_puts = 0 + self.tot_gets = 0 + self.tot_hist = 0 + self.tot_expires = 0 + self.tot_non_modified = 0 + self.cursize = 0 + + def expire_cache(self): + """Continuously examine cached objects, expiring stale ones. + + This function is designed to be run in its own daemon thread, + referenced at ``self.expiration_thread``. + """ + # It's possible that "time" will be set to None + # arbitrarily, so we check "while time" to avoid exceptions. + # See tickets #99 and #180 for more information. + while time: + now = time.time() + # Must make a copy of expirations so it doesn't change size + # during iteration + for expiration_time, objects in copyitems(self.expirations): + if expiration_time <= now: + for obj_size, uri, sel_header_values in objects: + try: + del self.store[uri][tuple(sel_header_values)] + self.tot_expires += 1 + self.cursize -= obj_size + except KeyError: + # the key may have been deleted elsewhere + pass + del self.expirations[expiration_time] + time.sleep(self.expire_freq) + + def get(self): + """Return the current variant if in the cache, else None.""" + request = cherrypy.serving.request + self.tot_gets += 1 + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + return None + + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + variant = uricache.wait(key=tuple(sorted(header_values)), + timeout=self.antistampede_timeout, + debug=self.debug) + if variant is not None: + self.tot_hist += 1 + return variant + + def put(self, variant, size): + """Store the current variant in the cache.""" + request = cherrypy.serving.request + response = cherrypy.serving.response + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + uricache = AntiStampedeCache() + uricache.selecting_headers = [ + e.value for e in response.headers.elements('Vary')] + self.store[uri] = uricache + + if len(self.store) < self.maxobjects: + total_size = self.cursize + size + + # checks if there's space for the object + if (size < self.maxobj_size and total_size < self.maxsize): + # add to the expirations list + expiration_time = response.time + self.delay + bucket = self.expirations.setdefault(expiration_time, []) + bucket.append((size, uri, uricache.selecting_headers)) + + # add to the cache + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + uricache[tuple(sorted(header_values))] = variant + self.tot_puts += 1 + self.cursize = total_size + + def delete(self): + """Remove ALL cached variants of the current resource.""" + uri = cherrypy.url(qs=cherrypy.serving.request.query_string) + self.store.pop(uri, None) + + +def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs): + """Try to obtain cached output. If fresh enough, raise HTTPError(304). + + If POST, PUT, or DELETE: + * invalidates (deletes) any cached response for this resource + * sets request.cached = False + * sets request.cacheable = False + + else if a cached copy exists: + * sets request.cached = True + * sets request.cacheable = False + * sets response.headers to the cached values + * checks the cached Last-Modified response header against the + current If-(Un)Modified-Since request headers; raises 304 + if necessary. + * sets response.status and response.body to the cached values + * returns True + + otherwise: + * sets request.cached = False + * sets request.cacheable = True + * returns False + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + if not hasattr(cherrypy, "_cache"): + # Make a process-wide Cache object. + cherrypy._cache = kwargs.pop("cache_class", MemoryCache)() + + # Take all remaining kwargs and set them on the Cache object. + for k, v in kwargs.items(): + setattr(cherrypy._cache, k, v) + cherrypy._cache.debug = debug + + # POST, PUT, DELETE should invalidate (delete) the cached copy. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. + if request.method in invalid_methods: + if debug: + cherrypy.log('request.method %r in invalid_methods %r' % + (request.method, invalid_methods), 'TOOLS.CACHING') + cherrypy._cache.delete() + request.cached = False + request.cacheable = False + return False + + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: + request.cached = False + request.cacheable = True + return False + + cache_data = cherrypy._cache.get() + request.cached = bool(cache_data) + request.cacheable = not request.cached + if request.cached: + # Serve the cached copy. + max_age = cherrypy._cache.delay + for v in [e.value for e in request.headers.elements('Cache-Control')]: + atoms = v.split('=', 1) + directive = atoms.pop(0) + if directive == 'max-age': + if len(atoms) != 1 or not atoms[0].isdigit(): + raise cherrypy.HTTPError(400, "Invalid Cache-Control header") + max_age = int(atoms[0]) + break + elif directive == 'no-cache': + if debug: + cherrypy.log('Ignoring cache due to Cache-Control: no-cache', + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + if debug: + cherrypy.log('Reading response from cache', 'TOOLS.CACHING') + s, h, b, create_time = cache_data + age = int(response.time - create_time) + if (age > max_age): + if debug: + cherrypy.log('Ignoring cache due to age > %d' % max_age, + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + # Copy the response headers. See http://www.cherrypy.org/ticket/721. + response.headers = rh = httputil.HeaderMap() + for k in h: + dict.__setitem__(rh, k, dict.__getitem__(h, k)) + + # Add the required Age header + response.headers["Age"] = str(age) + + try: + # Note that validate_since depends on a Last-Modified header; + # this was put into the cached copy, and should have been + # resurrected just above (response.headers = cache_data[1]). + cptools.validate_since() + except cherrypy.HTTPRedirect: + x = sys.exc_info()[1] + if x.status == 304: + cherrypy._cache.tot_non_modified += 1 + raise + + # serve it & get out from the request + response.status = s + response.body = b + else: + if debug: + cherrypy.log('request is not cached', 'TOOLS.CACHING') + return request.cached + + +def tee_output(): + """Tee response output to cache storage. Internal.""" + # Used by CachingTool by attaching to request.hooks + + request = cherrypy.serving.request + if 'no-store' in request.headers.values('Cache-Control'): + return + + def tee(body): + """Tee response.body into a list.""" + if ('no-cache' in response.headers.values('Pragma') or + 'no-store' in response.headers.values('Cache-Control')): + for chunk in body: + yield chunk + return + + output = [] + for chunk in body: + output.append(chunk) + yield chunk + + # save the cache data + body = ntob('').join(output) + cherrypy._cache.put((response.status, response.headers or {}, + body, response.time), len(body)) + + response = cherrypy.serving.response + response.body = tee(response.body) + + +def expires(secs=0, force=False, debug=False): + """Tool for influencing cache mechanisms using the 'Expires' header. + + secs + Must be either an int or a datetime.timedelta, and indicates the + number of seconds between response.time and when the response should + expire. The 'Expires' header will be set to response.time + secs. + If secs is zero, the 'Expires' header is set one year in the past, and + the following "cache prevention" headers are also set: + + * Pragma: no-cache + * Cache-Control': no-cache, must-revalidate + + force + If False, the following headers are checked: + + * Etag + * Last-Modified + * Age + * Expires + + If any are already present, none of the above response headers are set. + + """ + + response = cherrypy.serving.response + headers = response.headers + + cacheable = False + if not force: + # some header names that indicate that the response can be cached + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): + if indicator in headers: + cacheable = True + break + + if not cacheable and not force: + if debug: + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') + else: + if debug: + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') + if isinstance(secs, datetime.timedelta): + secs = (86400 * secs.days) + secs.seconds + + if secs == 0: + if force or ("Pragma" not in headers): + headers["Pragma"] = "no-cache" + if cherrypy.serving.request.protocol >= (1, 1): + if force or "Cache-Control" not in headers: + headers["Cache-Control"] = "no-cache, must-revalidate" + # Set an explicit Expires date in the past. + expiry = httputil.HTTPDate(1169942400.0) + else: + expiry = httputil.HTTPDate(response.time + secs) + if force or "Expires" not in headers: + headers["Expires"] = expiry diff --git a/cherrypy/lib/caching.pyc b/cherrypy/lib/caching.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27648d989fa187977e53ff6af8ac96b3d39b7429 GIT binary patch literal 14466 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 +or Ned Batchelder's `enhanced version: +`_ + +To turn on coverage tracing, use the following code:: + + cherrypy.engine.subscribe('start', covercp.start) + +DO NOT subscribe anything on the 'start_thread' channel, as previously +recommended. Calling start once in the main thread should be sufficient +to start coverage on all threads. Calling start again in each thread +effectively clears any coverage data gathered up to that point. + +Run your code, then use the ``covercp.serve()`` function to browse the +results in a web browser. If you run this module from the command line, +it will call ``serve()`` for you. +""" + +import re +import sys +import cgi +from cherrypy._cpcompat import quote_plus +import os, os.path +localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") + +the_coverage = None +try: + from coverage import coverage + the_coverage = coverage(data_file=localFile) + def start(): + the_coverage.start() +except ImportError: + # Setting the_coverage to None will raise errors + # that need to be trapped downstream. + the_coverage = None + + import warnings + warnings.warn("No code coverage will be performed; coverage.py could not be imported.") + + def start(): + pass +start.priority = 20 + +TEMPLATE_MENU = """ + + CherryPy Coverage Menu + + + +

CherryPy Coverage

""" + +TEMPLATE_FORM = """ +
+
+ + Show percentages
+ Hide files over %%
+ Exclude files matching
+ +
+ + +
+
""" + +TEMPLATE_FRAMESET = """ +CherryPy coverage data + + + + + +""" + +TEMPLATE_COVERAGE = """ + + Coverage for %(name)s + + + +

%(name)s

+

%(fullpath)s

+

Coverage: %(pc)s%%

""" + +TEMPLATE_LOC_COVERED = """ + %s  + %s +\n""" +TEMPLATE_LOC_NOT_COVERED = """ + %s  + %s +\n""" +TEMPLATE_LOC_EXCLUDED = """ + %s  + %s +\n""" + +TEMPLATE_ITEM = "%s%s%s\n" + +def _percent(statements, missing): + s = len(statements) + e = s - len(missing) + if s > 0: + return int(round(100.0 * e / s)) + return 0 + +def _show_branch(root, base, path, pct=0, showpct=False, exclude="", + coverage=the_coverage): + + # Show the directory name and any of our children + dirs = [k for k, v in root.items() if v] + dirs.sort() + for name in dirs: + newpath = os.path.join(path, name) + + if newpath.lower().startswith(base): + relpath = newpath[len(base):] + yield "| " * relpath.count(os.sep) + yield "%s\n" % \ + (newpath, quote_plus(exclude), name) + + for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage): + yield chunk + + # Now list the files + if path.lower().startswith(base): + relpath = path[len(base):] + files = [k for k, v in root.items() if not v] + files.sort() + for name in files: + newpath = os.path.join(path, name) + + pc_str = "" + if showpct: + try: + _, statements, _, missing, _ = coverage.analysis2(newpath) + except: + # Yes, we really want to pass on all errors. + pass + else: + pc = _percent(statements, missing) + pc_str = ("%3d%% " % pc).replace(' ',' ') + if pc < float(pct) or pc == -1: + pc_str = "%s" % pc_str + else: + pc_str = "%s" % pc_str + + yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), + pc_str, newpath, name) + +def _skip_file(path, exclude): + if exclude: + return bool(re.search(exclude, path)) + +def _graft(path, tree): + d = tree + + p = path + atoms = [] + while True: + p, tail = os.path.split(p) + if not tail: + break + atoms.append(tail) + atoms.append(p) + if p != "/": + atoms.append("/") + + atoms.reverse() + for node in atoms: + if node: + d = d.setdefault(node, {}) + +def get_tree(base, exclude, coverage=the_coverage): + """Return covered module names as a nested dict.""" + tree = {} + runs = coverage.data.executed_files() + for path in runs: + if not _skip_file(path, exclude) and not os.path.isdir(path): + _graft(path, tree) + return tree + +class CoverStats(object): + + def __init__(self, coverage, root=None): + self.coverage = coverage + if root is None: + # Guess initial depth. Files outside this path will not be + # reachable from the web interface. + import cherrypy + root = os.path.dirname(cherrypy.__file__) + self.root = root + + def index(self): + return TEMPLATE_FRAMESET % self.root.lower() + index.exposed = True + + def menu(self, base="/", pct="50", showpct="", + exclude=r'python\d\.\d|test|tut\d|tutorial'): + + # The coverage module uses all-lower-case names. + base = base.lower().rstrip(os.sep) + + yield TEMPLATE_MENU + yield TEMPLATE_FORM % locals() + + # Start by showing links for parent paths + yield "
" + path = "" + atoms = base.split(os.sep) + atoms.pop() + for atom in atoms: + path += atom + os.sep + yield ("%s %s" + % (path, quote_plus(exclude), atom, os.sep)) + yield "
" + + yield "
" + + # Then display the tree + tree = get_tree(base, exclude, self.coverage) + if not tree: + yield "

No modules covered.

" + else: + for chunk in _show_branch(tree, base, "/", pct, + showpct=='checked', exclude, coverage=self.coverage): + yield chunk + + yield "
" + yield "" + menu.exposed = True + + def annotated_file(self, filename, statements, excluded, missing): + source = open(filename, 'r') + buffer = [] + for lineno, line in enumerate(source.readlines()): + lineno += 1 + line = line.strip("\n\r") + empty_the_buffer = True + if lineno in excluded: + template = TEMPLATE_LOC_EXCLUDED + elif lineno in missing: + template = TEMPLATE_LOC_NOT_COVERED + elif lineno in statements: + template = TEMPLATE_LOC_COVERED + else: + empty_the_buffer = False + buffer.append((lineno, line)) + if empty_the_buffer: + for lno, pastline in buffer: + yield template % (lno, cgi.escape(pastline)) + buffer = [] + yield template % (lineno, cgi.escape(line)) + + def report(self, name): + filename, statements, excluded, missing, _ = self.coverage.analysis2(name) + pc = _percent(statements, missing) + yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), + fullpath=name, + pc=pc) + yield '\n' + for line in self.annotated_file(filename, statements, excluded, + missing): + yield line + yield '
' + yield '' + yield '' + report.exposed = True + + +def serve(path=localFile, port=8080, root=None): + if coverage is None: + raise ImportError("The coverage module could not be imported.") + from coverage import coverage + cov = coverage(data_file = path) + cov.load() + + import cherrypy + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': "production", + }) + cherrypy.quickstart(CoverStats(cov, root)) + +if __name__ == "__main__": + serve(*tuple(sys.argv[1:])) + diff --git a/cherrypy/lib/cpstats.py b/cherrypy/lib/cpstats.py new file mode 100644 index 0000000..9be947f --- /dev/null +++ b/cherrypy/lib/cpstats.py @@ -0,0 +1,662 @@ +"""CPStats, a package for collecting and reporting on program statistics. + +Overview +======== + +Statistics about program operation are an invaluable monitoring and debugging +tool. Unfortunately, the gathering and reporting of these critical values is +usually ad-hoc. This package aims to add a centralized place for gathering +statistical performance data, a structure for recording that data which +provides for extrapolation of that data into more useful information, +and a method of serving that data to both human investigators and +monitoring software. Let's examine each of those in more detail. + +Data Gathering +-------------- + +Just as Python's `logging` module provides a common importable for gathering +and sending messages, performance statistics would benefit from a similar +common mechanism, and one that does *not* require each package which wishes +to collect stats to import a third-party module. Therefore, we choose to +re-use the `logging` module by adding a `statistics` object to it. + +That `logging.statistics` object is a nested dict. It is not a custom class, +because that would 1) require libraries and applications to import a third- +party module in order to participate, 2) inhibit innovation in extrapolation +approaches and in reporting tools, and 3) be slow. There are, however, some +specifications regarding the structure of the dict. + + { + +----"SQLAlchemy": { + | "Inserts": 4389745, + | "Inserts per Second": + | lambda s: s["Inserts"] / (time() - s["Start"]), + | C +---"Table Statistics": { + | o | "widgets": {-----------+ + N | l | "Rows": 1.3M, | Record + a | l | "Inserts": 400, | + m | e | },---------------------+ + e | c | "froobles": { + s | t | "Rows": 7845, + p | i | "Inserts": 0, + a | o | }, + c | n +---}, + e | "Slow Queries": + | [{"Query": "SELECT * FROM widgets;", + | "Processing Time": 47.840923343, + | }, + | ], + +----}, + } + +The `logging.statistics` dict has four levels. The topmost level is nothing +more than a set of names to introduce modularity, usually along the lines of +package names. If the SQLAlchemy project wanted to participate, for example, +it might populate the item `logging.statistics['SQLAlchemy']`, whose value +would be a second-layer dict we call a "namespace". Namespaces help multiple +packages to avoid collisions over key names, and make reports easier to read, +to boot. The maintainers of SQLAlchemy should feel free to use more than one +namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case +or other syntax constraints on the namespace names; they should be chosen +to be maximally readable by humans (neither too short nor too long). + +Each namespace, then, is a dict of named statistical values, such as +'Requests/sec' or 'Uptime'. You should choose names which will look +good on a report: spaces and capitalization are just fine. + +In addition to scalars, values in a namespace MAY be a (third-layer) +dict, or a list, called a "collection". For example, the CherryPy StatsTool +keeps track of what each request is doing (or has most recently done) +in a 'Requests' collection, where each key is a thread ID; each +value in the subdict MUST be a fourth dict (whew!) of statistical data about +each thread. We call each subdict in the collection a "record". Similarly, +the StatsTool also keeps a list of slow queries, where each record contains +data about each slow query, in order. + +Values in a namespace or record may also be functions, which brings us to: + +Extrapolation +------------- + +The collection of statistical data needs to be fast, as close to unnoticeable +as possible to the host program. That requires us to minimize I/O, for example, +but in Python it also means we need to minimize function calls. So when you +are designing your namespace and record values, try to insert the most basic +scalar values you already have on hand. + +When it comes time to report on the gathered data, however, we usually have +much more freedom in what we can calculate. Therefore, whenever reporting +tools (like the provided StatsPage CherryPy class) fetch the contents of +`logging.statistics` for reporting, they first call `extrapolate_statistics` +(passing the whole `statistics` dict as the only argument). This makes a +deep copy of the statistics dict so that the reporting tool can both iterate +over it and even change it without harming the original. But it also expands +any functions in the dict by calling them. For example, you might have a +'Current Time' entry in the namespace with the value "lambda scope: time.time()". +The "scope" parameter is the current namespace dict (or record, if we're +currently expanding one of those instead), allowing you access to existing +static entries. If you're truly evil, you can even modify more than one entry +at a time. + +However, don't try to calculate an entry and then use its value in further +extrapolations; the order in which the functions are called is not guaranteed. +This can lead to a certain amount of duplicated work (or a redesign of your +schema), but that's better than complicating the spec. + +After the whole thing has been extrapolated, it's time for: + +Reporting +--------- + +The StatsPage class grabs the `logging.statistics` dict, extrapolates it all, +and then transforms it to HTML for easy viewing. Each namespace gets its own +header and attribute table, plus an extra table for each collection. This is +NOT part of the statistics specification; other tools can format how they like. + +You can control which columns are output and how they are formatted by updating +StatsPage.formatting, which is a dict that mirrors the keys and nesting of +`logging.statistics`. The difference is that, instead of data values, it has +formatting values. Use None for a given key to indicate to the StatsPage that a +given column should not be output. Use a string with formatting (such as '%.3f') +to interpolate the value(s), or use a callable (such as lambda v: v.isoformat()) +for more advanced formatting. Any entry which is not mentioned in the formatting +dict is output unchanged. + +Monitoring +---------- + +Although the HTML output takes pains to assign unique id's to each with +statistical data, you're probably better off fetching /cpstats/data, which +outputs the whole (extrapolated) `logging.statistics` dict in JSON format. +That is probably easier to parse, and doesn't have any formatting controls, +so you get the "original" data in a consistently-serialized format. +Note: there's no treatment yet for datetime objects. Try time.time() instead +for now if you can. Nagios will probably thank you. + +Turning Collection Off +---------------------- + +It is recommended each namespace have an "Enabled" item which, if False, +stops collection (but not reporting) of statistical data. Applications +SHOULD provide controls to pause and resume collection by setting these +entries to False or True, if present. + + +Usage +===== + +To collect statistics on CherryPy applications: + + from cherrypy.lib import cpstats + appconfig['/']['tools.cpstats.on'] = True + +To collect statistics on your own code: + + import logging + # Initialize the repository + if not hasattr(logging, 'statistics'): logging.statistics = {} + # Initialize my namespace + mystats = logging.statistics.setdefault('My Stuff', {}) + # Initialize my namespace's scalars and collections + mystats.update({ + 'Enabled': True, + 'Start Time': time.time(), + 'Important Events': 0, + 'Events/Second': lambda s: ( + (s['Important Events'] / (time.time() - s['Start Time']))), + }) + ... + for event in events: + ... + # Collect stats + if mystats.get('Enabled', False): + mystats['Important Events'] += 1 + +To report statistics: + + root.cpstats = cpstats.StatsPage() + +To format statistics reports: + + See 'Reporting', above. + +""" + +# -------------------------------- Statistics -------------------------------- # + +import logging +if not hasattr(logging, 'statistics'): logging.statistics = {} + +def extrapolate_statistics(scope): + """Return an extrapolated copy of the given scope.""" + c = {} + for k, v in list(scope.items()): + if isinstance(v, dict): + v = extrapolate_statistics(v) + elif isinstance(v, (list, tuple)): + v = [extrapolate_statistics(record) for record in v] + elif hasattr(v, '__call__'): + v = v(scope) + c[k] = v + return c + + +# --------------------- CherryPy Applications Statistics --------------------- # + +import threading +import time + +import cherrypy + +appstats = logging.statistics.setdefault('CherryPy Applications', {}) +appstats.update({ + 'Enabled': True, + 'Bytes Read/Request': lambda s: (s['Total Requests'] and + (s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0), + 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), + 'Bytes Written/Request': lambda s: (s['Total Requests'] and + (s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0), + 'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s), + 'Current Time': lambda s: time.time(), + 'Current Requests': 0, + 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), + 'Server Version': cherrypy.__version__, + 'Start Time': time.time(), + 'Total Bytes Read': 0, + 'Total Bytes Written': 0, + 'Total Requests': 0, + 'Total Time': 0, + 'Uptime': lambda s: time.time() - s['Start Time'], + 'Requests': {}, + }) + +proc_time = lambda s: time.time() - s['Start Time'] + + +class ByteCountWrapper(object): + """Wraps a file-like object, counting the number of bytes read.""" + + def __init__(self, rfile): + self.rfile = rfile + self.bytes_read = 0 + + def read(self, size=-1): + data = self.rfile.read(size) + self.bytes_read += len(data) + return data + + def readline(self, size=-1): + data = self.rfile.readline(size) + self.bytes_read += len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + return data + + +average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0 + + +class StatsTool(cherrypy.Tool): + """Record various information about the current request.""" + + def __init__(self): + cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + if appstats.get('Enabled', False): + cherrypy.Tool._setup(self) + self.record_start() + + def record_start(self): + """Record the beginning of a request.""" + request = cherrypy.serving.request + if not hasattr(request.rfile, 'bytes_read'): + request.rfile = ByteCountWrapper(request.rfile) + request.body.fp = request.rfile + + r = request.remote + + appstats['Current Requests'] += 1 + appstats['Total Requests'] += 1 + appstats['Requests'][threading._get_ident()] = { + 'Bytes Read': None, + 'Bytes Written': None, + # Use a lambda so the ip gets updated by tools.proxy later + 'Client': lambda s: '%s:%s' % (r.ip, r.port), + 'End Time': None, + 'Processing Time': proc_time, + 'Request-Line': request.request_line, + 'Response Status': None, + 'Start Time': time.time(), + } + + def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100, + debug=False, **kwargs): + """Record the end of a request.""" + resp = cherrypy.serving.response + w = appstats['Requests'][threading._get_ident()] + + r = cherrypy.request.rfile.bytes_read + w['Bytes Read'] = r + appstats['Total Bytes Read'] += r + + if resp.stream: + w['Bytes Written'] = 'chunked' + else: + cl = int(resp.headers.get('Content-Length', 0)) + w['Bytes Written'] = cl + appstats['Total Bytes Written'] += cl + + w['Response Status'] = getattr(resp, 'output_status', None) or resp.status + + w['End Time'] = time.time() + p = w['End Time'] - w['Start Time'] + w['Processing Time'] = p + appstats['Total Time'] += p + + appstats['Current Requests'] -= 1 + + if debug: + cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') + + if uriset: + rs = appstats.setdefault('URI Set Tracking', {}) + r = rs.setdefault(uriset, { + 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, + 'Avg': average_uriset_time}) + if r['Min'] is None or p < r['Min']: + r['Min'] = p + if r['Max'] is None or p > r['Max']: + r['Max'] = p + r['Count'] += 1 + r['Sum'] += p + + if slow_queries and p > slow_queries: + sq = appstats.setdefault('Slow Queries', []) + sq.append(w.copy()) + if len(sq) > slow_queries_count: + sq.pop(0) + + +import cherrypy +cherrypy.tools.cpstats = StatsTool() + + +# ---------------------- CherryPy Statistics Reporting ---------------------- # + +import os +thisdir = os.path.abspath(os.path.dirname(__file__)) + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + json = None + + +missing = object() + +locale_date = lambda v: time.strftime('%c', time.gmtime(v)) +iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) + +def pause_resume(ns): + def _pause_resume(enabled): + pause_disabled = '' + resume_disabled = '' + if enabled: + resume_disabled = 'disabled="disabled" ' + else: + pause_disabled = 'disabled="disabled" ' + return """ +
+ + +
+
+ + +
+ """ % (ns, pause_disabled, ns, resume_disabled) + return _pause_resume + + +class StatsPage(object): + + formatting = { + 'CherryPy Applications': { + 'Enabled': pause_resume('CherryPy Applications'), + 'Bytes Read/Request': '%.3f', + 'Bytes Read/Second': '%.3f', + 'Bytes Written/Request': '%.3f', + 'Bytes Written/Second': '%.3f', + 'Current Time': iso_format, + 'Requests/Second': '%.3f', + 'Start Time': iso_format, + 'Total Time': '%.3f', + 'Uptime': '%.3f', + 'Slow Queries': { + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': iso_format, + }, + 'URI Set Tracking': { + 'Avg': '%.3f', + 'Max': '%.3f', + 'Min': '%.3f', + 'Sum': '%.3f', + }, + 'Requests': { + 'Bytes Read': '%s', + 'Bytes Written': '%s', + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': None, + }, + }, + 'CherryPy WSGIServer': { + 'Enabled': pause_resume('CherryPy WSGIServer'), + 'Connections/second': '%.3f', + 'Start time': iso_format, + }, + } + + + def index(self): + # Transform the raw data into pretty output for HTML + yield """ + + + Statistics + + + +""" + for title, scalars, collections in self.get_namespaces(): + yield """ +

%s

+ + + +""" % title + for i, (key, value) in enumerate(scalars): + colnum = i % 3 + if colnum == 0: yield """ + """ + yield """ + """ % vars() + if colnum == 2: yield """ + """ + + if colnum == 0: yield """ + + + """ + elif colnum == 1: yield """ + + """ + yield """ + +
%(key)s%(value)s
""" + + for subtitle, headers, subrows in collections: + yield """ +

%s

+ + + """ % subtitle + for key in headers: + yield """ + """ % key + yield """ + + + """ + for subrow in subrows: + yield """ + """ + for value in subrow: + yield """ + """ % value + yield """ + """ + yield """ + +
%s
%s
""" + yield """ + + +""" + index.exposed = True + + def get_namespaces(self): + """Yield (title, scalars, collections) for each namespace.""" + s = extrapolate_statistics(logging.statistics) + for title, ns in sorted(s.items()): + scalars = [] + collections = [] + ns_fmt = self.formatting.get(title, {}) + for k, v in sorted(ns.items()): + fmt = ns_fmt.get(k, {}) + if isinstance(v, dict): + headers, subrows = self.get_dict_collection(v, fmt) + collections.append((k, ['ID'] + headers, subrows)) + elif isinstance(v, (list, tuple)): + headers, subrows = self.get_list_collection(v, fmt) + collections.append((k, headers, subrows)) + else: + format = ns_fmt.get(k, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v = format(v) + elif format is not missing: + v = format % v + scalars.append((k, v)) + yield title, scalars, collections + + def get_dict_collection(self, v, formatting): + """Return ([headers], [rows]) for the given collection.""" + # E.g., the 'Requests' dict. + headers = [] + for record in v.itervalues(): + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for k2, record in sorted(v.items()): + subrow = [k2] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + def get_list_collection(self, v, formatting): + """Return ([headers], [subrows]) for the given collection.""" + # E.g., the 'Slow Queries' list. + headers = [] + for record in v: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for record in v: + subrow = [] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + if json is not None: + def data(self): + s = extrapolate_statistics(logging.statistics) + cherrypy.response.headers['Content-Type'] = 'application/json' + return json.dumps(s, sort_keys=True, indent=4) + data.exposed = True + + def pause(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = False + raise cherrypy.HTTPRedirect('./') + pause.exposed = True + pause.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + + def resume(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = True + raise cherrypy.HTTPRedirect('./') + resume.exposed = True + resume.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + diff --git a/cherrypy/lib/cptools.py b/cherrypy/lib/cptools.py new file mode 100644 index 0000000..b426a3e --- /dev/null +++ b/cherrypy/lib/cptools.py @@ -0,0 +1,617 @@ +"""Functions for builtin CherryPy tools.""" + +import logging +import re + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, md5, set +from cherrypy.lib import httputil as _httputil + + +# Conditional HTTP request support # + +def validate_etags(autotags=False, debug=False): + """Validate the current ETag against If-Match, If-None-Match headers. + + If autotags is True, an ETag response-header value will be provided + from an MD5 hash of the response body (unless some other code has + already provided an ETag header). If False (the default), the ETag + will not be automatic. + + WARNING: the autotags feature is not designed for URL's which allow + methods other than GET. For example, if a POST to the same URL returns + no content, the automatic ETag will be incorrect, breaking a fundamental + use for entity tags in a possibly destructive fashion. Likewise, if you + raise 304 Not Modified, the response body will be empty, the ETag hash + will be incorrect, and your application will break. + See :rfc:`2616` Section 14.24. + """ + response = cherrypy.serving.response + + # Guard against being run twice. + if hasattr(response, "ETag"): + return + + status, reason, msg = _httputil.valid_status(response.status) + + etag = response.headers.get('ETag') + + # Automatic ETag generation. See warning in docstring. + if etag: + if debug: + cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') + elif not autotags: + if debug: + cherrypy.log('Autotags off', 'TOOLS.ETAGS') + elif status != 200: + if debug: + cherrypy.log('Status not 200', 'TOOLS.ETAGS') + else: + etag = response.collapse_body() + etag = '"%s"' % md5(etag).hexdigest() + if debug: + cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') + response.headers['ETag'] = etag + + response.ETag = etag + + # "If the request would, without the If-Match header field, result in + # anything other than a 2xx or 412 status, then the If-Match header + # MUST be ignored." + if debug: + cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') + if status >= 200 and status <= 299: + request = cherrypy.serving.request + + conditions = request.headers.elements('If-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions and not (conditions == ["*"] or etag in conditions): + raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " + "not match %r" % (etag, conditions)) + + conditions = request.headers.elements('If-None-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-None-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions == ["*"] or etag in conditions: + if debug: + cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS') + if request.method in ("GET", "HEAD"): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r " + "matched %r" % (etag, conditions)) + +def validate_since(): + """Validate the current Last-Modified against If-Modified-Since headers. + + If no code has set the Last-Modified response header, then no validation + will be performed. + """ + response = cherrypy.serving.response + lastmod = response.headers.get('Last-Modified') + if lastmod: + status, reason, msg = _httputil.valid_status(response.status) + + request = cherrypy.serving.request + + since = request.headers.get('If-Unmodified-Since') + if since and since != lastmod: + if (status >= 200 and status <= 299) or status == 412: + raise cherrypy.HTTPError(412) + + since = request.headers.get('If-Modified-Since') + if since and since == lastmod: + if (status >= 200 and status <= 299) or status == 304: + if request.method in ("GET", "HEAD"): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412) + + +# Tool code # + +def allow(methods=None, debug=False): + """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). + + The given methods are case-insensitive, and may be in any order. + If only one method is allowed, you may supply a single string; + if more than one, supply a list of strings. + + Regardless of whether the current method is allowed or not, this + also emits an 'Allow' response header, containing the given methods. + """ + if not isinstance(methods, (tuple, list)): + methods = [methods] + methods = [m.upper() for m in methods if m] + if not methods: + methods = ['GET', 'HEAD'] + elif 'GET' in methods and 'HEAD' not in methods: + methods.append('HEAD') + + cherrypy.response.headers['Allow'] = ', '.join(methods) + if cherrypy.request.method not in methods: + if debug: + cherrypy.log('request.method %r not in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + raise cherrypy.HTTPError(405) + else: + if debug: + cherrypy.log('request.method %r in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + + +def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', + scheme='X-Forwarded-Proto', debug=False): + """Change the base URL (scheme://host[:port][/path]). + + For running a CP server behind Apache, lighttpd, or other HTTP server. + + For Apache and lighttpd, you should leave the 'local' argument at the + default value of 'X-Forwarded-Host'. For Squid, you probably want to set + tools.proxy.local = 'Origin'. + + If you want the new request.base to include path info (not just the host), + you must explicitly set base to the full base path, and ALSO set 'local' + to '', so that the X-Forwarded-Host request header (which never includes + path info) does not override it. Regardless, the value for 'base' MUST + NOT end in a slash. + + cherrypy.request.remote.ip (the IP address of the client) will be + rewritten if the header specified by the 'remote' arg is valid. + By default, 'remote' is set to 'X-Forwarded-For'. If you do not + want to rewrite remote.ip, set the 'remote' arg to an empty string. + """ + + request = cherrypy.serving.request + + if scheme: + s = request.headers.get(scheme, None) + if debug: + cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') + if s == 'on' and 'ssl' in scheme.lower(): + # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header + scheme = 'https' + else: + # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' + scheme = s + if not scheme: + scheme = request.base[:request.base.find("://")] + + if local: + lbase = request.headers.get(local, None) + if debug: + cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') + if lbase is not None: + base = lbase.split(',')[0] + if not base: + port = request.local.port + if port == 80: + base = '127.0.0.1' + else: + base = '127.0.0.1:%s' % port + + if base.find("://") == -1: + # add http:// or https:// if needed + base = scheme + "://" + base + + request.base = base + + if remote: + xff = request.headers.get(remote) + if debug: + cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') + if xff: + if remote == 'X-Forwarded-For': + # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ + xff = xff.split(',')[-1].strip() + request.remote.ip = xff + + +def ignore_headers(headers=('Range',), debug=False): + """Delete request headers whose field names are included in 'headers'. + + This is a useful tool for working behind certain HTTP servers; + for example, Apache duplicates the work that CP does for 'Range' + headers, and will doubly-truncate the response. + """ + request = cherrypy.serving.request + for name in headers: + if name in request.headers: + if debug: + cherrypy.log('Ignoring request header %r' % name, + 'TOOLS.IGNORE_HEADERS') + del request.headers[name] + + +def response_headers(headers=None, debug=False): + """Set headers on the response.""" + if debug: + cherrypy.log('Setting response headers: %s' % repr(headers), + 'TOOLS.RESPONSE_HEADERS') + for name, value in (headers or []): + cherrypy.serving.response.headers[name] = value +response_headers.failsafe = True + + +def referer(pattern, accept=True, accept_missing=False, error=403, + message='Forbidden Referer header.', debug=False): + """Raise HTTPError if Referer header does/does not match the given pattern. + + pattern + A regular expression pattern to test against the Referer. + + accept + If True, the Referer must match the pattern; if False, + the Referer must NOT match the pattern. + + accept_missing + If True, permit requests with no Referer header. + + error + The HTTP error code to return to the client on failure. + + message + A string to include in the response body on failure. + + """ + try: + ref = cherrypy.serving.request.headers['Referer'] + match = bool(re.match(pattern, ref)) + if debug: + cherrypy.log('Referer %r matches %r' % (ref, pattern), + 'TOOLS.REFERER') + if accept == match: + return + except KeyError: + if debug: + cherrypy.log('No Referer header', 'TOOLS.REFERER') + if accept_missing: + return + + raise cherrypy.HTTPError(error, message) + + +class SessionAuth(object): + """Assert that the user is logged in.""" + + session_key = "username" + debug = False + + def check_username_and_password(self, username, password): + pass + + def anonymous(self): + """Provide a temporary user name for anonymous users.""" + pass + + def on_login(self, username): + pass + + def on_logout(self, username): + pass + + def on_check(self, username): + pass + + def login_screen(self, from_page='..', username='', error_msg='', **kwargs): + return ntob(""" +Message: %(error_msg)s +
+ Login:
+ Password:
+
+ +
+""" % {'from_page': from_page, 'username': username, + 'error_msg': error_msg}, "utf-8") + + def do_login(self, username, password, from_page='..', **kwargs): + """Login. May raise redirect, or return True if request handled.""" + response = cherrypy.serving.response + error_msg = self.check_username_and_password(username, password) + if error_msg: + body = self.login_screen(from_page, username, error_msg) + response.body = body + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + return True + else: + cherrypy.serving.request.login = username + cherrypy.session[self.session_key] = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or "/") + + def do_logout(self, from_page='..', **kwargs): + """Logout. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + username = sess.get(self.session_key) + sess[self.session_key] = None + if username: + cherrypy.serving.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page) + + def do_check(self): + """Assert username. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + request = cherrypy.serving.request + response = cherrypy.serving.response + + username = sess.get(self.session_key) + if not username: + sess[self.session_key] = username = self.anonymous() + if self.debug: + cherrypy.log('No session[username], trying anonymous', 'TOOLS.SESSAUTH') + if not username: + url = cherrypy.url(qs=request.query_string) + if self.debug: + cherrypy.log('No username, routing to login_screen with ' + 'from_page %r' % url, 'TOOLS.SESSAUTH') + response.body = self.login_screen(url) + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + return True + if self.debug: + cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH') + request.login = username + self.on_check(username) + + def run(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + path = request.path_info + if path.endswith('login_screen'): + if self.debug: + cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUTH') + return self.login_screen(**request.params) + elif path.endswith('do_login'): + if request.method != 'POST': + response.headers['Allow'] = "POST" + if self.debug: + cherrypy.log('do_login requires POST', 'TOOLS.SESSAUTH') + raise cherrypy.HTTPError(405) + if self.debug: + cherrypy.log('routing %r to do_login' % path, 'TOOLS.SESSAUTH') + return self.do_login(**request.params) + elif path.endswith('do_logout'): + if request.method != 'POST': + response.headers['Allow'] = "POST" + raise cherrypy.HTTPError(405) + if self.debug: + cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH') + return self.do_logout(**request.params) + else: + if self.debug: + cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUTH') + return self.do_check() + + +def session_auth(**kwargs): + sa = SessionAuth() + for k, v in kwargs.items(): + setattr(sa, k, v) + return sa.run() +session_auth.__doc__ = """Session authentication hook. + +Any attribute of the SessionAuth class may be overridden via a keyword arg +to this function: + +""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__) + for k in dir(SessionAuth) if not k.startswith("__")]) + + +def log_traceback(severity=logging.ERROR, debug=False): + """Write the last error's traceback to the cherrypy error log.""" + cherrypy.log("", "HTTP", severity=severity, traceback=True) + +def log_request_headers(debug=False): + """Write request headers to the cherrypy error log.""" + h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list] + cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP") + +def log_hooks(debug=False): + """Write request.hooks to the cherrypy error log.""" + request = cherrypy.serving.request + + msg = [] + # Sort by the standard points if possible. + from cherrypy import _cprequest + points = _cprequest.hookpoints + for k in request.hooks.keys(): + if k not in points: + points.append(k) + + for k in points: + msg.append(" %s:" % k) + v = request.hooks.get(k, []) + v.sort() + for h in v: + msg.append(" %r" % h) + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + + ':\n' + '\n'.join(msg), "HTTP") + +def redirect(url='', internal=True, debug=False): + """Raise InternalRedirect or HTTPRedirect to the given url.""" + if debug: + cherrypy.log('Redirecting %sto: %s' % + ({True: 'internal ', False: ''}[internal], url), + 'TOOLS.REDIRECT') + if internal: + raise cherrypy.InternalRedirect(url) + else: + raise cherrypy.HTTPRedirect(url) + +def trailing_slash(missing=True, extra=False, status=None, debug=False): + """Redirect if path_info has (missing|extra) trailing slash.""" + request = cherrypy.serving.request + pi = request.path_info + + if debug: + cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % + (request.is_index, missing, extra, pi), + 'TOOLS.TRAILING_SLASH') + if request.is_index is True: + if missing: + if not pi.endswith('/'): + new_url = cherrypy.url(pi + '/', request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + elif request.is_index is False: + if extra: + # If pi == '/', don't redirect to ''! + if pi.endswith('/') and pi != '/': + new_url = cherrypy.url(pi[:-1], request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + +def flatten(debug=False): + """Wrap response.body in a generator that recursively iterates over body. + + This allows cherrypy.response.body to consist of 'nested generators'; + that is, a set of generators that yield generators. + """ + import types + def flattener(input): + numchunks = 0 + for x in input: + if not isinstance(x, types.GeneratorType): + numchunks += 1 + yield x + else: + for y in flattener(x): + numchunks += 1 + yield y + if debug: + cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') + response = cherrypy.serving.response + response.body = flattener(response.body) + + +def accept(media=None, debug=False): + """Return the client's preferred media-type (from the given Content-Types). + + If 'media' is None (the default), no test will be performed. + + If 'media' is provided, it should be the Content-Type value (as a string) + or values (as a list or tuple of strings) which the current resource + can emit. The client's acceptable media ranges (as declared in the + Accept request header) will be matched in order to these Content-Type + values; the first such string is returned. That is, the return value + will always be one of the strings provided in the 'media' arg (or None + if 'media' is None). + + If no match is found, then HTTPError 406 (Not Acceptable) is raised. + Note that most web browsers send */* as a (low-quality) acceptable + media range, which should match any Content-Type. In addition, "...if + no Accept header field is present, then it is assumed that the client + accepts all media types." + + Matching types are checked in order of client preference first, + and then in the order of the given 'media' values. + + Note that this function does not honor accept-params (other than "q"). + """ + if not media: + return + if isinstance(media, basestring): + media = [media] + request = cherrypy.serving.request + + # Parse the Accept request header, and try to match one + # of the requested media-ranges (in order of preference). + ranges = request.headers.elements('Accept') + if not ranges: + # Any media type is acceptable. + if debug: + cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') + return media[0] + else: + # Note that 'ranges' is sorted in order of preference + for element in ranges: + if element.qvalue > 0: + if element.value == "*/*": + # Matches any type or subtype + if debug: + cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') + return media[0] + elif element.value.endswith("/*"): + # Matches any subtype + mtype = element.value[:-1] # Keep the slash + for m in media: + if m.startswith(mtype): + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return m + else: + # Matches exact value + if element.value in media: + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return element.value + + # No suitable media-range found. + ah = request.headers.get('Accept') + if ah is None: + msg = "Your client did not send an Accept header." + else: + msg = "Your client sent this Accept header: %s." % ah + msg += (" But this resource only emits these media types: %s." % + ", ".join(media)) + raise cherrypy.HTTPError(406, msg) + + +class MonitoredHeaderMap(_httputil.HeaderMap): + + def __init__(self): + self.accessed_headers = set() + + def __getitem__(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.__getitem__(self, key) + + def __contains__(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.__contains__(self, key) + + def get(self, key, default=None): + self.accessed_headers.add(key) + return _httputil.HeaderMap.get(self, key, default=default) + + if hasattr({}, 'has_key'): + # Python 2 + def has_key(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.has_key(self, key) + + +def autovary(ignore=None, debug=False): + """Auto-populate the Vary response header based on request.header access.""" + request = cherrypy.serving.request + + req_h = request.headers + request.headers = MonitoredHeaderMap() + request.headers.update(req_h) + if ignore is None: + ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) + + def set_response_header(): + resp_h = cherrypy.serving.response.headers + v = set([e.value for e in resp_h.elements('Vary')]) + if debug: + cherrypy.log('Accessed headers: %s' % request.headers.accessed_headers, + 'TOOLS.AUTOVARY') + v = v.union(request.headers.accessed_headers) + v = v.difference(ignore) + v = list(v) + v.sort() + resp_h['Vary'] = ', '.join(v) + request.hooks.attach('before_finalize', set_response_header, 95) + diff --git a/cherrypy/lib/cptools.pyc b/cherrypy/lib/cptools.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f07b847af1d8564c6f3b5914af1ab2d870558d6e GIT binary patch literal 21963 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 t = u"\u7007\u3040" + # >>> len(t) + # 2 + # >>> len(t.encode("UTF-8")) + # 6 + # >>> len(t.encode("utf7")) + # 8 + del response.headers["Content-Length"] + + # Parse the Accept-Charset request header, and try to provide one + # of the requested charsets (in order of user preference). + encs = request.headers.elements('Accept-Charset') + charsets = [enc.value.lower() for enc in encs] + if self.debug: + cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') + + if self.encoding is not None: + # If specified, force this encoding to be used, or fail. + encoding = self.encoding.lower() + if self.debug: + cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE') + if (not charsets) or "*" in charsets or encoding in charsets: + if self.debug: + cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + else: + if not encs: + if self.debug: + cherrypy.log('Attempting default encoding %r' % + self.default_encoding, 'TOOLS.ENCODE') + # Any character-set is acceptable. + if encoder(self.default_encoding): + return self.default_encoding + else: + raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding) + else: + for element in encs: + if element.qvalue > 0: + if element.value == "*": + # Matches any charset. Try our default. + if self.debug: + cherrypy.log('Attempting default encoding due ' + 'to %r' % element, 'TOOLS.ENCODE') + if encoder(self.default_encoding): + return self.default_encoding + else: + encoding = element.value + if self.debug: + cherrypy.log('Attempting encoding %s (qvalue >' + '0)' % element, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + + if "*" not in charsets: + # If no "*" is present in an Accept-Charset field, then all + # character sets not explicitly mentioned get a quality + # value of 0, except for ISO-8859-1, which gets a quality + # value of 1 if not explicitly mentioned. + iso = 'iso-8859-1' + if iso not in charsets: + if self.debug: + cherrypy.log('Attempting ISO-8859-1 encoding', + 'TOOLS.ENCODE') + if encoder(iso): + return iso + + # No suitable encoding found. + ac = request.headers.get('Accept-Charset') + if ac is None: + msg = "Your client did not send an Accept-Charset header." + else: + msg = "Your client sent this Accept-Charset header: %s." % ac + msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets) + raise cherrypy.HTTPError(406, msg) + + def __call__(self, *args, **kwargs): + response = cherrypy.serving.response + self.body = self.oldhandler(*args, **kwargs) + + if isinstance(self.body, basestring): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if self.body: + self.body = [self.body] + else: + # [''] doesn't evaluate to False, so replace it with []. + self.body = [] + elif hasattr(self.body, 'read'): + self.body = file_generator(self.body) + elif self.body is None: + self.body = [] + + ct = response.headers.elements("Content-Type") + if self.debug: + cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE') + if ct: + ct = ct[0] + if self.text_only: + if ct.value.lower().startswith("text/"): + if self.debug: + cherrypy.log('Content-Type %s starts with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = True + else: + if self.debug: + cherrypy.log('Not finding because Content-Type %s does ' + 'not start with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = False + else: + if self.debug: + cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE') + do_find = True + + if do_find: + # Set "charset=..." param on response Content-Type header + ct.params['charset'] = self.find_acceptable_charset() + if self.add_charset: + if self.debug: + cherrypy.log('Setting Content-Type %s' % ct, + 'TOOLS.ENCODE') + response.headers["Content-Type"] = str(ct) + + return self.body + +# GZIP + +def compress(body, compress_level): + """Compress 'body' at the given compress_level.""" + import zlib + + # See http://www.gzip.org/zlib/rfc-gzip.html + yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker + yield ntob('\x08') # CM: compression method + yield ntob('\x00') # FLG: none set + # MTIME: 4 bytes + yield struct.pack(" 0 is present + * The 'identity' value is given with a qvalue > 0. + + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + set_vary_header(response, "Accept-Encoding") + + if not response.body: + # Response body is empty (might be a 304 for instance) + if debug: + cherrypy.log('No response body', context='TOOLS.GZIP') + return + + # If returning cached content (which should already have been gzipped), + # don't re-zip. + if getattr(request, "cached", False): + if debug: + cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') + return + + acceptable = request.headers.elements('Accept-Encoding') + if not acceptable: + # If no Accept-Encoding field is present in a request, + # the server MAY assume that the client will accept any + # content coding. In this case, if "identity" is one of + # the available content-codings, then the server SHOULD use + # the "identity" content-coding, unless it has additional + # information that a different content-coding is meaningful + # to the client. + if debug: + cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') + return + + ct = response.headers.get('Content-Type', '').split(';')[0] + for coding in acceptable: + if coding.value == 'identity' and coding.qvalue != 0: + if debug: + cherrypy.log('Non-zero identity qvalue: %s' % coding, + context='TOOLS.GZIP') + return + if coding.value in ('gzip', 'x-gzip'): + if coding.qvalue == 0: + if debug: + cherrypy.log('Zero gzip qvalue: %s' % coding, + context='TOOLS.GZIP') + return + + if ct not in mime_types: + # If the list of provided mime-types contains tokens + # such as 'text/*' or 'application/*+xml', + # we go through them and find the most appropriate one + # based on the given content-type. + # The pattern matching is only caring about the most + # common cases, as stated above, and doesn't support + # for extra parameters. + found = False + if '/' in ct: + ct_media_type, ct_sub_type = ct.split('/') + for mime_type in mime_types: + if '/' in mime_type: + media_type, sub_type = mime_type.split('/') + if ct_media_type == media_type: + if sub_type == '*': + found = True + break + elif '+' in sub_type and '+' in ct_sub_type: + ct_left, ct_right = ct_sub_type.split('+') + left, right = sub_type.split('+') + if left == '*' and ct_right == right: + found = True + break + + if not found: + if debug: + cherrypy.log('Content-Type %s not in mime_types %r' % + (ct, mime_types), context='TOOLS.GZIP') + return + + if debug: + cherrypy.log('Gzipping', context='TOOLS.GZIP') + # Return a generator that compresses the page + response.headers['Content-Encoding'] = 'gzip' + response.body = compress(response.body, compress_level) + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + + return + + if debug: + cherrypy.log('No acceptable encoding found.', context='GZIP') + cherrypy.HTTPError(406, "identity, gzip").set_response() + diff --git a/cherrypy/lib/encoding.pyc b/cherrypy/lib/encoding.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a90bc17f02a43171fd863878fd2b0c86cf8b50d GIT binary patch literal 10134 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 self.maxparents: + return [("[%s referrers]" % len(refs), [])] + + try: + ascendcode = self.ascend.__code__ + except AttributeError: + ascendcode = self.ascend.im_func.func_code + for parent in refs: + if inspect.isframe(parent) and parent.f_code is ascendcode: + continue + if parent in self.ignore: + continue + if depth <= self.maxdepth: + parents.append((parent, self.ascend(parent, depth))) + else: + parents.append((parent, [])) + + return parents + + def peek(self, s): + """Return s, restricted to a sane length.""" + if len(s) > (self.peek_length + 3): + half = self.peek_length // 2 + return s[:half] + '...' + s[-half:] + else: + return s + + def _format(self, obj, descend=True): + """Return a string representation of a single object.""" + if inspect.isframe(obj): + filename, lineno, func, context, index = inspect.getframeinfo(obj) + return "" % func + + if not descend: + return self.peek(repr(obj)) + + if isinstance(obj, dict): + return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False), + self._format(v, descend=False)) + for k, v in obj.items()]) + "}" + elif isinstance(obj, list): + return "[" + ", ".join([self._format(item, descend=False) + for item in obj]) + "]" + elif isinstance(obj, tuple): + return "(" + ", ".join([self._format(item, descend=False) + for item in obj]) + ")" + + r = self.peek(repr(obj)) + if isinstance(obj, (str, int, float)): + return r + return "%s: %s" % (type(obj), r) + + def format(self, tree): + """Return a list of string reprs from a nested list of referrers.""" + output = [] + def ascend(branch, depth=1): + for parent, grandparents in branch: + output.append((" " * depth) + self._format(parent)) + if grandparents: + ascend(grandparents, depth + 1) + ascend(tree) + return output + + +def get_instances(cls): + return [x for x in gc.get_objects() if isinstance(x, cls)] + + +class RequestCounter(SimplePlugin): + + def start(self): + self.count = 0 + + def before_request(self): + self.count += 1 + + def after_request(self): + self.count -=1 +request_counter = RequestCounter(cherrypy.engine) +request_counter.subscribe() + + +def get_context(obj): + if isinstance(obj, _cprequest.Request): + return "path=%s;stage=%s" % (obj.path_info, obj.stage) + elif isinstance(obj, _cprequest.Response): + return "status=%s" % obj.status + elif isinstance(obj, _cpwsgi.AppResponse): + return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '') + elif hasattr(obj, "tb_lineno"): + return "tb_lineno=%s" % obj.tb_lineno + return "" + + +class GCRoot(object): + """A CherryPy page handler for testing reference leaks.""" + + classes = [(_cprequest.Request, 2, 2, + "Should be 1 in this request thread and 1 in the main thread."), + (_cprequest.Response, 2, 2, + "Should be 1 in this request thread and 1 in the main thread."), + (_cpwsgi.AppResponse, 1, 1, + "Should be 1 in this request thread only."), + ] + + def index(self): + return "Hello, world!" + index.exposed = True + + def stats(self): + output = ["Statistics:"] + + for trial in range(10): + if request_counter.count > 0: + break + time.sleep(0.5) + else: + output.append("\nNot all requests closed properly.") + + # gc_collect isn't perfectly synchronous, because it may + # break reference cycles that then take time to fully + # finalize. Call it thrice and hope for the best. + gc.collect() + gc.collect() + unreachable = gc.collect() + if unreachable: + if objgraph is not None: + final = objgraph.by_type('Nondestructible') + if final: + objgraph.show_backrefs(final, filename='finalizers.png') + + trash = {} + for x in gc.garbage: + trash[type(x)] = trash.get(type(x), 0) + 1 + if trash: + output.insert(0, "\n%s unreachable objects:" % unreachable) + trash = [(v, k) for k, v in trash.items()] + trash.sort() + for pair in trash: + output.append(" " + repr(pair)) + + # Check declared classes to verify uncollected instances. + # These don't have to be part of a cycle; they can be + # any objects that have unanticipated referrers that keep + # them from being collected. + allobjs = {} + for cls, minobj, maxobj, msg in self.classes: + allobjs[cls] = get_instances(cls) + + for cls, minobj, maxobj, msg in self.classes: + objs = allobjs[cls] + lenobj = len(objs) + if lenobj < minobj or lenobj > maxobj: + if minobj == maxobj: + output.append( + "\nExpected %s %r references, got %s." % + (minobj, cls, lenobj)) + else: + output.append( + "\nExpected %s to %s %r references, got %s." % + (minobj, maxobj, cls, lenobj)) + + for obj in objs: + if objgraph is not None: + ig = [id(objs), id(inspect.currentframe())] + fname = "graph_%s_%s.png" % (cls.__name__, id(obj)) + objgraph.show_backrefs( + obj, extra_ignore=ig, max_depth=4, too_many=20, + filename=fname, extra_info=get_context) + output.append("\nReferrers for %s (refcount=%s):" % + (repr(obj), sys.getrefcount(obj))) + t = ReferrerTree(ignore=[objs], maxdepth=3) + tree = t.ascend(obj) + output.extend(t.format(tree)) + + return "\n".join(output) + stats.exposed = True + diff --git a/cherrypy/lib/http.py b/cherrypy/lib/http.py new file mode 100644 index 0000000..4661d69 --- /dev/null +++ b/cherrypy/lib/http.py @@ -0,0 +1,7 @@ +import warnings +warnings.warn('cherrypy.lib.http has been deprecated and will be removed ' + 'in CherryPy 3.3 use cherrypy.lib.httputil instead.', + DeprecationWarning) + +from cherrypy.lib.httputil import * + diff --git a/cherrypy/lib/httpauth.py b/cherrypy/lib/httpauth.py new file mode 100644 index 0000000..ad7c6eb --- /dev/null +++ b/cherrypy/lib/httpauth.py @@ -0,0 +1,354 @@ +""" +This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`). +This has full compliance with 'Digest' and 'Basic' authentication methods. In +'Digest' it supports both MD5 and MD5-sess algorithms. + +Usage: + First use 'doAuth' to request the client authentication for a + certain resource. You should send an httplib.UNAUTHORIZED response to the + client so he knows he has to authenticate itself. + + Then use 'parseAuthorization' to retrieve the 'auth_map' used in + 'checkResponse'. + + To use 'checkResponse' you must have already verified the password associated + with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse' + function to verify if the password matches the one sent by the client. + +SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms +SUPPORTED_QOP - list of supported 'Digest' 'qop'. +""" +__version__ = 1, 0, 1 +__author__ = "Tiago Cogumbreiro " +__credits__ = """ + Peter van Kampen for its recipe which implement most of Digest authentication: + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378 +""" + +__license__ = """ +Copyright (c) 2005, Tiago Cogumbreiro +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Sylvain Hellegouarch nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", + "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", + "calculateNonce", "SUPPORTED_QOP") + +################################################################################ +import time +from cherrypy._cpcompat import base64_decode, ntob, md5 +from cherrypy._cpcompat import parse_http_list, parse_keqv_list + +MD5 = "MD5" +MD5_SESS = "MD5-sess" +AUTH = "auth" +AUTH_INT = "auth-int" + +SUPPORTED_ALGORITHM = (MD5, MD5_SESS) +SUPPORTED_QOP = (AUTH, AUTH_INT) + +################################################################################ +# doAuth +# +DIGEST_AUTH_ENCODERS = { + MD5: lambda val: md5(ntob(val)).hexdigest(), + MD5_SESS: lambda val: md5(ntob(val)).hexdigest(), +# SHA: lambda val: sha.new(ntob(val)).hexdigest (), +} + +def calculateNonce (realm, algorithm = MD5): + """This is an auxaliary function that calculates 'nonce' value. It is used + to handle sessions.""" + + global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS + assert algorithm in SUPPORTED_ALGORITHM + + try: + encoder = DIGEST_AUTH_ENCODERS[algorithm] + except KeyError: + raise NotImplementedError ("The chosen algorithm (%s) does not have "\ + "an implementation yet" % algorithm) + + return encoder ("%d:%s" % (time.time(), realm)) + +def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH): + """Challenges the client for a Digest authentication.""" + global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP + assert algorithm in SUPPORTED_ALGORITHM + assert qop in SUPPORTED_QOP + + if nonce is None: + nonce = calculateNonce (realm, algorithm) + + return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( + realm, nonce, algorithm, qop + ) + +def basicAuth (realm): + """Challengenes the client for a Basic authentication.""" + assert '"' not in realm, "Realms cannot contain the \" (quote) character." + + return 'Basic realm="%s"' % realm + +def doAuth (realm): + """'doAuth' function returns the challenge string b giving priority over + Digest and fallback to Basic authentication when the browser doesn't + support the first one. + + This should be set in the HTTP header under the key 'WWW-Authenticate'.""" + + return digestAuth (realm) + " " + basicAuth (realm) + + +################################################################################ +# Parse authorization parameters +# +def _parseDigestAuthorization (auth_params): + # Convert the auth params to a dict + items = parse_http_list(auth_params) + params = parse_keqv_list(items) + + # Now validate the params + + # Check for required parameters + required = ["username", "realm", "nonce", "uri", "response"] + for k in required: + if k not in params: + return None + + # If qop is sent then cnonce and nc MUST be present + if "qop" in params and not ("cnonce" in params \ + and "nc" in params): + return None + + # If qop is not sent, neither cnonce nor nc can be present + if ("cnonce" in params or "nc" in params) and \ + "qop" not in params: + return None + + return params + + +def _parseBasicAuthorization (auth_params): + username, password = base64_decode(auth_params).split(":", 1) + return {"username": username, "password": password} + +AUTH_SCHEMES = { + "basic": _parseBasicAuthorization, + "digest": _parseDigestAuthorization, +} + +def parseAuthorization (credentials): + """parseAuthorization will convert the value of the 'Authorization' key in + the HTTP header to a map itself. If the parsing fails 'None' is returned. + """ + + global AUTH_SCHEMES + + auth_scheme, auth_params = credentials.split(" ", 1) + auth_scheme = auth_scheme.lower () + + parser = AUTH_SCHEMES[auth_scheme] + params = parser (auth_params) + + if params is None: + return + + assert "auth_scheme" not in params + params["auth_scheme"] = auth_scheme + return params + + +################################################################################ +# Check provided response for a valid password +# +def md5SessionKey (params, password): + """ + If the "algorithm" directive's value is "MD5-sess", then A1 + [the session key] is calculated only once - on the first request by the + client following receipt of a WWW-Authenticate challenge from the server. + + This creates a 'session key' for the authentication of subsequent + requests and responses which is different for each "authentication + session", thus limiting the amount of material hashed with any one + key. + + Because the server need only use the hash of the user + credentials in order to create the A1 value, this construction could + be used in conjunction with a third party authentication service so + that the web server would not need the actual password value. The + specification of such a protocol is beyond the scope of this + specification. +""" + + keys = ("username", "realm", "nonce", "cnonce") + params_copy = {} + for key in keys: + params_copy[key] = params[key] + + params_copy["algorithm"] = MD5_SESS + return _A1 (params_copy, password) + +def _A1(params, password): + algorithm = params.get ("algorithm", MD5) + H = DIGEST_AUTH_ENCODERS[algorithm] + + if algorithm == MD5: + # If the "algorithm" directive's value is "MD5" or is + # unspecified, then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + return "%s:%s:%s" % (params["username"], params["realm"], password) + + elif algorithm == MD5_SESS: + + # This is A1 if qop is set + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password)) + return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"]) + + +def _A2(params, method, kwargs): + # If the "qop" directive's value is "auth" or is unspecified, then A2 is: + # A2 = Method ":" digest-uri-value + + qop = params.get ("qop", "auth") + if qop == "auth": + return method + ":" + params["uri"] + elif qop == "auth-int": + # If the "qop" value is "auth-int", then A2 is: + # A2 = Method ":" digest-uri-value ":" H(entity-body) + entity_body = kwargs.get ("entity_body", "") + H = kwargs["H"] + + return "%s:%s:%s" % ( + method, + params["uri"], + H(entity_body) + ) + + else: + raise NotImplementedError ("The 'qop' method is unknown: %s" % qop) + +def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs): + """ + Generates a response respecting the algorithm defined in RFC 2617 + """ + params = auth_map + + algorithm = params.get ("algorithm", MD5) + + H = DIGEST_AUTH_ENCODERS[algorithm] + KD = lambda secret, data: H(secret + ":" + data) + + qop = params.get ("qop", None) + + H_A2 = H(_A2(params, method, kwargs)) + + if algorithm == MD5_SESS and A1 is not None: + H_A1 = H(A1) + else: + H_A1 = H(_A1(params, password)) + + if qop in ("auth", "auth-int"): + # If the "qop" value is "auth" or "auth-int": + # request-digest = <"> < KD ( H(A1), unq(nonce-value) + # ":" nc-value + # ":" unq(cnonce-value) + # ":" unq(qop-value) + # ":" H(A2) + # ) <"> + request = "%s:%s:%s:%s:%s" % ( + params["nonce"], + params["nc"], + params["cnonce"], + params["qop"], + H_A2, + ) + elif qop is None: + # If the "qop" directive is not present (this construction is + # for compatibility with RFC 2069): + # request-digest = + # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> + request = "%s:%s" % (params["nonce"], H_A2) + + return KD(H_A1, request) + +def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs): + """This function is used to verify the response given by the client when + he tries to authenticate. + Optional arguments: + entity_body - when 'qop' is set to 'auth-int' you MUST provide the + raw data you are going to send to the client (usually the + HTML page. + request_uri - the uri from the request line compared with the 'uri' + directive of the authorization map. They must represent + the same resource (unused at this time). + """ + + if auth_map['realm'] != kwargs.get('realm', None): + return False + + response = _computeDigestResponse(auth_map, password, method, A1,**kwargs) + + return response == auth_map["response"] + +def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs): + # Note that the Basic response doesn't provide the realm value so we cannot + # test it + try: + return encrypt(auth_map["password"], auth_map["username"]) == password + except TypeError: + return encrypt(auth_map["password"]) == password + +AUTH_RESPONSES = { + "basic": _checkBasicResponse, + "digest": _checkDigestResponse, +} + +def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs): + """'checkResponse' compares the auth_map with the password and optionally + other arguments that each implementation might need. + + If the response is of type 'Basic' then the function has the following + signature:: + + checkBasicResponse (auth_map, password) -> bool + + If the response is of type 'Digest' then the function has the following + signature:: + + checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool + + The 'A1' argument is only used in MD5_SESS algorithm based responses. + Check md5SessionKey() for more info. + """ + checker = AUTH_RESPONSES[auth_map["auth_scheme"]] + return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs) + + + + diff --git a/cherrypy/lib/httpauth.pyc b/cherrypy/lib/httpauth.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a2fbc0c1618f1371ea0abc8b1678fe74d0fc015 GIT binary patch literal 11664 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 H(!~D&0b^(N literal 0 HcmV?d00001 diff --git a/cherrypy/lib/httputil.py b/cherrypy/lib/httputil.py new file mode 100644 index 0000000..5f77d54 --- /dev/null +++ b/cherrypy/lib/httputil.py @@ -0,0 +1,506 @@ +"""HTTP library functions. + +This module contains functions for building an HTTP application +framework: any one, not just one whose name starts with "Ch". ;) If you +reference any modules from some popular framework inside *this* module, +FuManChu will personally hang you up by your thumbs and submit you +to a public caning. +""" + +from binascii import b2a_base64 +from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted +from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs +response_codes = BaseHTTPRequestHandler.responses.copy() + +# From http://www.cherrypy.org/ticket/361 +response_codes[500] = ('Internal Server Error', + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.') +response_codes[503] = ('Service Unavailable', + 'The server is currently unable to handle the ' + 'request due to a temporary overloading or ' + 'maintenance of the server.') + +import re +import urllib + + + +def urljoin(*atoms): + """Return the given path \*atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = "/".join([x for x in atoms if x]) + while "//" in url: + url = url.replace("//", "/") + # Special-case the final url of "", and return "/" instead. + return url or "/" + +def urljoin_bytes(*atoms): + """Return the given path *atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = ntob("/").join([x for x in atoms if x]) + while ntob("//") in url: + url = url.replace(ntob("//"), ntob("/")) + # Special-case the final url of "", and return "/" instead. + return url or ntob("/") + +def protocol_from_http(protocol_str): + """Return a protocol tuple from the given 'HTTP/x.y' string.""" + return int(protocol_str[5]), int(protocol_str[7]) + +def get_ranges(headervalue, content_length): + """Return a list of (start, stop) indices from a Range header, or None. + + Each (start, stop) tuple will be composed of two ints, which are suitable + for use in a slicing operation. That is, the header "Range: bytes=3-6", + if applied against a Python string, is requesting resource[3:7]. This + function will return the list [(3, 7)]. + + If this function returns an empty list, you should return HTTP 416. + """ + + if not headervalue: + return None + + result = [] + bytesunit, byteranges = headervalue.split("=", 1) + for brange in byteranges.split(","): + start, stop = [x.strip() for x in brange.split("-", 1)] + if start: + if not stop: + stop = content_length - 1 + start, stop = int(start), int(stop) + if start >= content_length: + # From rfc 2616 sec 14.16: + # "If the server receives a request (other than one + # including an If-Range request-header field) with an + # unsatisfiable Range request-header field (that is, + # all of whose byte-range-spec values have a first-byte-pos + # value greater than the current length of the selected + # resource), it SHOULD return a response code of 416 + # (Requested range not satisfiable)." + continue + if stop < start: + # From rfc 2616 sec 14.16: + # "If the server ignores a byte-range-spec because it + # is syntactically invalid, the server SHOULD treat + # the request as if the invalid Range header field + # did not exist. (Normally, this means return a 200 + # response containing the full entity)." + return None + result.append((start, stop + 1)) + else: + if not stop: + # See rfc quote above. + return None + # Negative subscript (last N bytes) + result.append((content_length - int(stop), content_length)) + + return result + + +class HeaderElement(object): + """An element (with parameters) from an HTTP header's element list.""" + + def __init__(self, value, params=None): + self.value = value + if params is None: + params = {} + self.params = params + + def __cmp__(self, other): + return cmp(self.value, other.value) + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)] + return "%s%s" % (self.value, "".join(p)) + + def __bytes__(self): + return ntob(self.__str__()) + + def __unicode__(self): + return ntou(self.__str__()) + + def parse(elementstr): + """Transform 'token;key=val' to ('token', {'key': 'val'}).""" + # Split the element into a value and parameters. The 'value' may + # be of the form, "token=token", but we don't split that here. + atoms = [x.strip() for x in elementstr.split(";") if x.strip()] + if not atoms: + initial_value = '' + else: + initial_value = atoms.pop(0).strip() + params = {} + for atom in atoms: + atom = [x.strip() for x in atom.split("=", 1) if x.strip()] + key = atom.pop(0) + if atom: + val = atom[0] + else: + val = "" + params[key] = val + return initial_value, params + parse = staticmethod(parse) + + def from_str(cls, elementstr): + """Construct an instance from a string of the form 'token;key=val'.""" + ival, params = cls.parse(elementstr) + return cls(ival, params) + from_str = classmethod(from_str) + + +q_separator = re.compile(r'; *q *=') + +class AcceptElement(HeaderElement): + """An element (with parameters) from an Accept* header's element list. + + AcceptElement objects are comparable; the more-preferred object will be + "less than" the less-preferred object. They are also therefore sortable; + if you sort a list of AcceptElement objects, they will be listed in + priority order; the most preferred value will be first. Yes, it should + have been the other way around, but it's too late to fix now. + """ + + def from_str(cls, elementstr): + qvalue = None + # The first "q" parameter (if any) separates the initial + # media-range parameter(s) (if any) from the accept-params. + atoms = q_separator.split(elementstr, 1) + media_range = atoms.pop(0).strip() + if atoms: + # The qvalue for an Accept header can have extensions. The other + # headers cannot, but it's easier to parse them as if they did. + qvalue = HeaderElement.from_str(atoms[0].strip()) + + media_type, params = cls.parse(media_range) + if qvalue is not None: + params["q"] = qvalue + return cls(media_type, params) + from_str = classmethod(from_str) + + def qvalue(self): + val = self.params.get("q", "1") + if isinstance(val, HeaderElement): + val = val.value + return float(val) + qvalue = property(qvalue, doc="The qvalue, or priority, of this value.") + + def __cmp__(self, other): + diff = cmp(self.qvalue, other.qvalue) + if diff == 0: + diff = cmp(str(self), str(other)) + return diff + + def __lt__(self, other): + if self.qvalue == other.qvalue: + return str(self) < str(other) + else: + return self.qvalue < other.qvalue + + +def header_elements(fieldname, fieldvalue): + """Return a sorted HeaderElement list from a comma-separated header string.""" + if not fieldvalue: + return [] + + result = [] + for element in fieldvalue.split(","): + if fieldname.startswith("Accept") or fieldname == 'TE': + hv = AcceptElement.from_str(element) + else: + hv = HeaderElement.from_str(element) + result.append(hv) + + return list(reversed(sorted(result))) + +def decode_TEXT(value): + r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr").""" + try: + # Python 3 + from email.header import decode_header + except ImportError: + from email.Header import decode_header + atoms = decode_header(value) + decodedvalue = "" + for atom, charset in atoms: + if charset is not None: + atom = atom.decode(charset) + decodedvalue += atom + return decodedvalue + +def valid_status(status): + """Return legal HTTP status Code, Reason-phrase and Message. + + The status arg must be an int, or a str that begins with an int. + + If status is an int, or a str and no reason-phrase is supplied, + a default reason-phrase will be provided. + """ + + if not status: + status = 200 + + status = str(status) + parts = status.split(" ", 1) + if len(parts) == 1: + # No reason supplied. + code, = parts + reason = None + else: + code, reason = parts + reason = reason.strip() + + try: + code = int(code) + except ValueError: + raise ValueError("Illegal response status from server " + "(%s is non-numeric)." % repr(code)) + + if code < 100 or code > 599: + raise ValueError("Illegal response status from server " + "(%s is out of range)." % repr(code)) + + if code not in response_codes: + # code is unknown but not illegal + default_reason, message = "", "" + else: + default_reason, message = response_codes[code] + + if reason is None: + reason = default_reason + + return code, reason, message + + +# NOTE: the parse_qs functions that follow are modified version of those +# in the python3.0 source - we need to pass through an encoding to the unquote +# method, but the default parse_qs function doesn't allow us to. These do. + +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): + """Parse a query given as a string argument. + + Arguments: + + qs: URL-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + URL encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + Returns a dict, as G-d intended. + """ + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + d = {} + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError("bad query field: %r" % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = unquote_qs(nv[0], encoding) + value = unquote_qs(nv[1], encoding) + if name in d: + if not isinstance(d[name], list): + d[name] = [d[name]] + d[name].append(value) + else: + d[name] = value + return d + + +image_map_pattern = re.compile(r"[0-9]+,[0-9]+") + +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): + """Build a params dictionary from a query_string. + + Duplicate key/value pairs in the provided query_string will be + returned as {'key': [val1, val2, ...]}. Single key/values will + be returned as strings: {'key': 'value'}. + """ + if image_map_pattern.match(query_string): + # Server-side image map. Map the coords to 'x' and 'y' + # (like CGI::Request does). + pm = query_string.split(",") + pm = {'x': int(pm[0]), 'y': int(pm[1])} + else: + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) + return pm + + +class CaseInsensitiveDict(dict): + """A case-insensitive dict subclass. + + Each key is changed on entry to str(key).title(). + """ + + def __getitem__(self, key): + return dict.__getitem__(self, str(key).title()) + + def __setitem__(self, key, value): + dict.__setitem__(self, str(key).title(), value) + + def __delitem__(self, key): + dict.__delitem__(self, str(key).title()) + + def __contains__(self, key): + return dict.__contains__(self, str(key).title()) + + def get(self, key, default=None): + return dict.get(self, str(key).title(), default) + + if hasattr({}, 'has_key'): + def has_key(self, key): + return dict.has_key(self, str(key).title()) + + def update(self, E): + for k in E.keys(): + self[str(k).title()] = E[k] + + def fromkeys(cls, seq, value=None): + newdict = cls() + for k in seq: + newdict[str(k).title()] = value + return newdict + fromkeys = classmethod(fromkeys) + + def setdefault(self, key, x=None): + key = str(key).title() + try: + return self[key] + except KeyError: + self[key] = x + return x + + def pop(self, key, default): + return dict.pop(self, str(key).title(), default) + + +# TEXT = +# +# A CRLF is allowed in the definition of TEXT only as part of a header +# field continuation. It is expected that the folding LWS will be +# replaced with a single SP before interpretation of the TEXT value." +if nativestr == bytestr: + header_translate_table = ''.join([chr(i) for i in xrange(256)]) + header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127) +else: + header_translate_table = None + header_translate_deletechars = bytes(range(32)) + bytes([127]) + + +class HeaderMap(CaseInsensitiveDict): + """A dict subclass for HTTP request and response headers. + + Each key is changed on entry to str(key).title(). This allows headers + to be case-insensitive and avoid duplicates. + + Values are header values (decoded according to :rfc:`2047` if necessary). + """ + + protocol=(1, 1) + encodings = ["ISO-8859-1"] + + # Someday, when http-bis is done, this will probably get dropped + # since few servers, clients, or intermediaries do it. But until then, + # we're going to obey the spec as is. + # "Words of *TEXT MAY contain characters from character sets other than + # ISO-8859-1 only when encoded according to the rules of RFC 2047." + use_rfc_2047 = True + + def elements(self, key): + """Return a sorted list of HeaderElements for the given header.""" + key = str(key).title() + value = self.get(key) + return header_elements(key, value) + + def values(self, key): + """Return a sorted list of HeaderElement.value for the given header.""" + return [e.value for e in self.elements(key)] + + def output(self): + """Transform self into a list of (name, value) tuples.""" + header_list = [] + for k, v in self.items(): + if isinstance(k, unicodestr): + k = self.encode(k) + + if not isinstance(v, basestring): + v = str(v) + + if isinstance(v, unicodestr): + v = self.encode(v) + + # See header_translate_* constants above. + # Replace only if you really know what you're doing. + k = k.translate(header_translate_table, header_translate_deletechars) + v = v.translate(header_translate_table, header_translate_deletechars) + + header_list.append((k, v)) + return header_list + + def encode(self, v): + """Return the given header name or value, encoded for HTTP output.""" + for enc in self.encodings: + try: + return v.encode(enc) + except UnicodeEncodeError: + continue + + if self.protocol == (1, 1) and self.use_rfc_2047: + # Encode RFC-2047 TEXT + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). + # We do our own here instead of using the email module + # because we never want to fold lines--folding has + # been deprecated by the HTTP working group. + v = b2a_base64(v.encode('utf-8')) + return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?=')) + + raise ValueError("Could not encode header part %r using " + "any of the encodings %r." % + (v, self.encodings)) + + +class Host(object): + """An internet address. + + name + Should be the client's host name. If not available (because no DNS + lookup is performed), the IP address should be used instead. + + """ + + ip = "0.0.0.0" + port = 80 + name = "unknown.tld" + + def __init__(self, ip, port, name=None): + self.ip = ip + self.port = port + if name is None: + name = ip + self.name = name + + def __repr__(self): + return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name) diff --git a/cherrypy/lib/httputil.pyc b/cherrypy/lib/httputil.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecc80c4cea5b9465258a15c1d5e4a99a8636fb85 GIT binary patch literal 17803 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 CbYWru literal 0 HcmV?d00001 diff --git a/cherrypy/lib/jsontools.py b/cherrypy/lib/jsontools.py new file mode 100644 index 0000000..2092579 --- /dev/null +++ b/cherrypy/lib/jsontools.py @@ -0,0 +1,87 @@ +import sys +import cherrypy +from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode + +def json_processor(entity): + """Read application/json data into request.json.""" + if not entity.headers.get(ntou("Content-Length"), ntou("")): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + cherrypy.serving.request.json = json_decode(body.decode('utf-8')) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + +def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], + force=True, debug=False, processor = json_processor): + """Add a processor to parse JSON request entities: + The default processor places the parsed data into request.json. + + Incoming request entities which match the given content_type(s) will + be deserialized from JSON to the Python equivalent, and the result + stored at cherrypy.request.json. The 'content_type' argument may + be a Content-Type string or a list of allowable Content-Type strings. + + If the 'force' argument is True (the default), then entities of other + content types will not be allowed; "415 Unsupported Media Type" is + raised instead. + + Supply your own processor to use a custom decoder, or to handle the parsed + data differently. The processor can be configured via + tools.json_in.processor or via the decorator method. + + Note that the deserializer requires the client send a Content-Length + request header, or it will raise "411 Length Required". If for any + other reason the request entity cannot be deserialized from JSON, + it will raise "400 Bad Request: Invalid JSON document". + + You must be using Python 2.6 or greater, or have the 'simplejson' + package importable; otherwise, ValueError is raised during processing. + """ + request = cherrypy.serving.request + if isinstance(content_type, basestring): + content_type = [content_type] + + if force: + if debug: + cherrypy.log('Removing body processors %s' % + repr(request.body.processors.keys()), 'TOOLS.JSON_IN') + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an entity of content type %s' % + ', '.join(content_type)) + + for ct in content_type: + if debug: + cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') + request.body.processors[ct] = processor + +def json_handler(*args, **kwargs): + value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) + return json_encode(value) + +def json_out(content_type='application/json', debug=False, handler=json_handler): + """Wrap request.handler to serialize its output to JSON. Sets Content-Type. + + If the given content_type is None, the Content-Type response header + is not set. + + Provide your own handler to use a custom encoder. For example + cherrypy.config['tools.json_out.handler'] = , or + @json_out(handler=function). + + You must be using Python 2.6 or greater, or have the 'simplejson' + package importable; otherwise, ValueError is raised during processing. + """ + request = cherrypy.serving.request + if debug: + cherrypy.log('Replacing %s with JSON handler' % request.handler, + 'TOOLS.JSON_OUT') + request._json_inner_handler = request.handler + request.handler = handler + if content_type is not None: + if debug: + cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT') + cherrypy.serving.response.headers['Content-Type'] = content_type + diff --git a/cherrypy/lib/jsontools.pyc b/cherrypy/lib/jsontools.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56bd133e8c246f527aaa975ba888152624203ba0 GIT binary patch literal 4068 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 J_~G*We*jcZG9Ul| literal 0 HcmV?d00001 diff --git a/cherrypy/lib/profiler.py b/cherrypy/lib/profiler.py new file mode 100644 index 0000000..785d58a --- /dev/null +++ b/cherrypy/lib/profiler.py @@ -0,0 +1,208 @@ +"""Profiler tools for CherryPy. + +CherryPy users +============== + +You can profile any of your pages as follows:: + + from cherrypy.lib import profiler + + class Root: + p = profile.Profiler("/path/to/profile/dir") + + def index(self): + self.p.run(self._index) + index.exposed = True + + def _index(self): + return "Hello, world!" + + cherrypy.tree.mount(Root()) + +You can also turn on profiling for all requests +using the ``make_app`` function as WSGI middleware. + +CherryPy developers +=================== + +This module can be used whenever you make changes to CherryPy, +to get a quick sanity-check on overall CP performance. Use the +``--profile`` flag when running the test suite. Then, use the ``serve()`` +function to browse the results in a web browser. If you run this +module from the command line, it will call ``serve()`` for you. + +""" + + +def new_func_strip_path(func_name): + """Make profiler output more readable by adding ``__init__`` modules' parents""" + filename, line, name = func_name + if filename.endswith("__init__.py"): + return os.path.basename(filename[:-12]) + filename[-12:], line, name + return os.path.basename(filename), line, name + +try: + import profile + import pstats + pstats.func_strip_path = new_func_strip_path +except ImportError: + profile = None + pstats = None + +import os, os.path +import sys +import warnings + +from cherrypy._cpcompat import BytesIO + +_count = 0 + +class Profiler(object): + + def __init__(self, path=None): + if not path: + path = os.path.join(os.path.dirname(__file__), "profile") + self.path = path + if not os.path.exists(path): + os.makedirs(path) + + def run(self, func, *args, **params): + """Dump profile data into self.path.""" + global _count + c = _count = _count + 1 + path = os.path.join(self.path, "cp_%04d.prof" % c) + prof = profile.Profile() + result = prof.runcall(func, *args, **params) + prof.dump_stats(path) + return result + + def statfiles(self): + """:rtype: list of available profiles. + """ + return [f for f in os.listdir(self.path) + if f.startswith("cp_") and f.endswith(".prof")] + + def stats(self, filename, sortby='cumulative'): + """:rtype stats(index): output of print_stats() for the given profile. + """ + sio = BytesIO() + if sys.version_info >= (2, 5): + s = pstats.Stats(os.path.join(self.path, filename), stream=sio) + s.strip_dirs() + s.sort_stats(sortby) + s.print_stats() + else: + # pstats.Stats before Python 2.5 didn't take a 'stream' arg, + # but just printed to stdout. So re-route stdout. + s = pstats.Stats(os.path.join(self.path, filename)) + s.strip_dirs() + s.sort_stats(sortby) + oldout = sys.stdout + try: + sys.stdout = sio + s.print_stats() + finally: + sys.stdout = oldout + response = sio.getvalue() + sio.close() + return response + + def index(self): + return """ + CherryPy profile data + + + + + + """ + index.exposed = True + + def menu(self): + yield "

Profiling runs

" + yield "

Click on one of the runs below to see profiling data.

" + runs = self.statfiles() + runs.sort() + for i in runs: + yield "%s
" % (i, i) + menu.exposed = True + + def report(self, filename): + import cherrypy + cherrypy.response.headers['Content-Type'] = 'text/plain' + return self.stats(filename) + report.exposed = True + + +class ProfileAggregator(Profiler): + + def __init__(self, path=None): + Profiler.__init__(self, path) + global _count + self.count = _count = _count + 1 + self.profiler = profile.Profile() + + def run(self, func, *args): + path = os.path.join(self.path, "cp_%04d.prof" % self.count) + result = self.profiler.runcall(func, *args) + self.profiler.dump_stats(path) + return result + + +class make_app: + def __init__(self, nextapp, path=None, aggregate=False): + """Make a WSGI middleware app which wraps 'nextapp' with profiling. + + nextapp + the WSGI application to wrap, usually an instance of + cherrypy.Application. + + path + where to dump the profiling output. + + aggregate + if True, profile data for all HTTP requests will go in + a single file. If False (the default), each HTTP request will + dump its profile data into a separate file. + + """ + if profile is None or pstats is None: + msg = ("Your installation of Python does not have a profile module. " + "If you're on Debian, try `sudo apt-get install python-profiler`. " + "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") + warnings.warn(msg) + + self.nextapp = nextapp + self.aggregate = aggregate + if aggregate: + self.profiler = ProfileAggregator(path) + else: + self.profiler = Profiler(path) + + def __call__(self, environ, start_response): + def gather(): + result = [] + for line in self.nextapp(environ, start_response): + result.append(line) + return result + return self.profiler.run(gather) + + +def serve(path=None, port=8080): + if profile is None or pstats is None: + msg = ("Your installation of Python does not have a profile module. " + "If you're on Debian, try `sudo apt-get install python-profiler`. " + "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") + warnings.warn(msg) + + import cherrypy + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': "production", + }) + cherrypy.quickstart(Profiler(path)) + + +if __name__ == "__main__": + serve(*tuple(sys.argv[1:])) + diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py new file mode 100644 index 0000000..ba8ff51 --- /dev/null +++ b/cherrypy/lib/reprconf.py @@ -0,0 +1,485 @@ +"""Generic configuration system using unrepr. + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, Python's +builtin ConfigParser is used (with some extensions). + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. + +The only key that cannot exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +the Config.environments dict. + +You can define your own namespaces to be called when new config is merged +by adding a named handler to Config.namespaces. The name can be any string, +and the handler must be either a callable or a context manager. +""" + +try: + # Python 3.0+ + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser + +try: + set +except NameError: + from sets import Set as set + +try: + basestring +except NameError: + basestring = str + +try: + # Python 3 + import builtins +except ImportError: + # Python 2 + import __builtin__ as builtins + +import operator as _operator +import sys + +def as_dict(config): + """Return a dict from 'config' whether it is a dict, file, or filename.""" + if isinstance(config, basestring): + config = Parser().dict_from_file(config) + elif hasattr(config, 'read'): + config = Parser().dict_from_file(config) + return config + + +class NamespaceSet(dict): + """A dict of config namespace names and handlers. + + Each config entry should begin with a namespace name; the corresponding + namespace handler will be called once for each config entry in that + namespace, and will be passed two arguments: the config key (with the + namespace removed) and the config value. + + Namespace handlers may be any Python callable; they may also be + Python 2.5-style 'context managers', in which case their __enter__ + method should return a callable to be used as the handler. + See cherrypy.tools (the Toolbox class) for an example. + """ + + def __call__(self, config): + """Iterate through config and pass it to each namespace handler. + + config + A flat dict, where keys use dots to separate + namespaces, and values are arbitrary. + + The first name in each config key is used to look up the corresponding + namespace handler. For example, a config entry of {'tools.gzip.on': v} + will call the 'tools' namespace handler with the args: ('gzip.on', v) + """ + # Separate the given config into namespaces + ns_confs = {} + for k in config: + if "." in k: + ns, name = k.split(".", 1) + bucket = ns_confs.setdefault(ns, {}) + bucket[name] = config[k] + + # I chose __enter__ and __exit__ so someday this could be + # rewritten using Python 2.5's 'with' statement: + # for ns, handler in self.iteritems(): + # with handler as callable: + # for k, v in ns_confs.get(ns, {}).iteritems(): + # callable(k, v) + for ns, handler in self.items(): + exit = getattr(handler, "__exit__", None) + if exit: + callable = handler.__enter__() + no_exc = True + try: + try: + for k, v in ns_confs.get(ns, {}).items(): + callable(k, v) + except: + # The exceptional case is handled here + no_exc = False + if exit is None: + raise + if not exit(*sys.exc_info()): + raise + # The exception is swallowed if exit() returns true + finally: + # The normal and non-local-goto cases are handled here + if no_exc and exit: + exit(None, None, None) + else: + for k, v in ns_confs.get(ns, {}).items(): + handler(k, v) + + def __repr__(self): + return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, + dict.__repr__(self)) + + def __copy__(self): + newobj = self.__class__() + newobj.update(self) + return newobj + copy = __copy__ + + +class Config(dict): + """A dict-like set of configuration data, with defaults and namespaces. + + May take a file, filename, or dict. + """ + + defaults = {} + environments = {} + namespaces = NamespaceSet() + + def __init__(self, file=None, **kwargs): + self.reset() + if file is not None: + self.update(file) + if kwargs: + self.update(kwargs) + + def reset(self): + """Reset self to default values.""" + self.clear() + dict.update(self, self.defaults) + + def update(self, config): + """Update self from a dict, file or filename.""" + if isinstance(config, basestring): + # Filename + config = Parser().dict_from_file(config) + elif hasattr(config, 'read'): + # Open file object + config = Parser().dict_from_file(config) + else: + config = config.copy() + self._apply(config) + + def _apply(self, config): + """Update self from a dict.""" + which_env = config.get('environment') + if which_env: + env = self.environments[which_env] + for k in env: + if k not in config: + config[k] = env[k] + + dict.update(self, config) + self.namespaces(config) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, v) + self.namespaces({k: v}) + + +class Parser(ConfigParser): + """Sub-class of ConfigParser that keeps the case of options and that + raises an exception if the file cannot be read. + """ + + def optionxform(self, optionstr): + return optionstr + + def read(self, filenames): + if isinstance(filenames, basestring): + filenames = [filenames] + for filename in filenames: + # try: + # fp = open(filename) + # except IOError: + # continue + fp = open(filename) + try: + self._read(fp, filename) + finally: + fp.close() + + def as_dict(self, raw=False, vars=None): + """Convert an INI file to a dictionary""" + # Load INI file into a dict + result = {} + for section in self.sections(): + if section not in result: + result[section] = {} + for option in self.options(section): + value = self.get(section, option, raw=raw, vars=vars) + try: + value = unrepr(value) + except Exception: + x = sys.exc_info()[1] + msg = ("Config error in section: %r, option: %r, " + "value: %r. Config values must be valid Python." % + (section, option, value)) + raise ValueError(msg, x.__class__.__name__, x.args) + result[section][option] = value + return result + + def dict_from_file(self, file): + if hasattr(file, 'read'): + self.readfp(file) + else: + self.read(file) + return self.as_dict() + + +# public domain "unrepr" implementation, found on the web and then improved. + + +class _Builder2: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError("unrepr does not recognize %s" % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python2 ast Node compiled from a string.""" + try: + import compiler + except ImportError: + # Fallback to eval when compiler package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = compiler.parse("__tempvalue__ = " + s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + + def build_Subscript(self, o): + expr, flags, subs = o.getChildren() + expr = self.build(expr) + subs = self.build(subs) + return expr[subs] + + def build_CallFunc(self, o): + children = map(self.build, o.getChildren()) + callee = children.pop(0) + kwargs = children.pop() or {} + starargs = children.pop() or () + args = tuple(children) + tuple(starargs) + return callee(*args, **kwargs) + + def build_List(self, o): + return map(self.build, o.getChildren()) + + def build_Const(self, o): + return o.value + + def build_Dict(self, o): + d = {} + i = iter(map(self.build, o.getChildren())) + for el in i: + d[el] = i.next() + return d + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.name + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError("unrepr could not resolve the name %s" % repr(name)) + + def build_Add(self, o): + left, right = map(self.build, o.getChildren()) + return left + right + + def build_Mul(self, o): + left, right = map(self.build, o.getChildren()) + return left * right + + def build_Getattr(self, o): + parent = self.build(o.expr) + return getattr(parent, o.attrname) + + def build_NoneType(self, o): + return None + + def build_UnarySub(self, o): + return -self.build(o.getChildren()[0]) + + def build_UnaryAdd(self, o): + return self.build(o.getChildren()[0]) + + +class _Builder3: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError("unrepr does not recognize %s" % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python3 ast Node compiled from a string.""" + try: + import ast + except ImportError: + # Fallback to eval when ast package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = ast.parse("__tempvalue__ = " + s) + return p.body[0].value + + def build_Subscript(self, o): + return self.build(o.value)[self.build(o.slice)] + + def build_Index(self, o): + return self.build(o.value) + + def build_Call(self, o): + callee = self.build(o.func) + + if o.args is None: + args = () + else: + args = tuple([self.build(a) for a in o.args]) + + if o.starargs is None: + starargs = () + else: + starargs = self.build(o.starargs) + + if o.kwargs is None: + kwargs = {} + else: + kwargs = self.build(o.kwargs) + + return callee(*(args + starargs), **kwargs) + + def build_List(self, o): + return list(map(self.build, o.elts)) + + def build_Str(self, o): + return o.s + + def build_Num(self, o): + return o.n + + def build_Dict(self, o): + return dict([(self.build(k), self.build(v)) + for k, v in zip(o.keys, o.values)]) + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.id + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + import builtins + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError("unrepr could not resolve the name %s" % repr(name)) + + def build_UnaryOp(self, o): + op, operand = map(self.build, [o.op, o.operand]) + return op(operand) + + def build_BinOp(self, o): + left, op, right = map(self.build, [o.left, o.op, o.right]) + return op(left, right) + + def build_Add(self, o): + return _operator.add + + def build_Mult(self, o): + return _operator.mul + + def build_USub(self, o): + return _operator.neg + + def build_Attribute(self, o): + parent = self.build(o.value) + return getattr(parent, o.attr) + + def build_NoneType(self, o): + return None + + +def unrepr(s): + """Return a Python object compiled from a string.""" + if not s: + return s + if sys.version_info < (3, 0): + b = _Builder2() + else: + b = _Builder3() + obj = b.astnode(s) + return b.build(obj) + + +def modules(modulePath): + """Load a module and retrieve a reference to that module.""" + try: + mod = sys.modules[modulePath] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(modulePath, globals(), locals(), ['']) + return mod + +def attributes(full_attribute_name): + """Load a module and retrieve an attribute of that module.""" + + # Parse out the path, module, and attribute + last_dot = full_attribute_name.rfind(".") + attr_name = full_attribute_name[last_dot + 1:] + mod_path = full_attribute_name[:last_dot] + + mod = modules(mod_path) + # Let an AttributeError propagate outward. + try: + attr = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + # Return a reference to the attribute. + return attr + + diff --git a/cherrypy/lib/reprconf.pyc b/cherrypy/lib/reprconf.pyc new file mode 100644 index 0000000000000000000000000000000000000000..425a557c85d1b82f5d5b4e6f7ed819f08c85441c GIT binary patch literal 16783 zcmd5@U2GiJb-uexQsjz~D2e)OVtFiECS^-ZD#vys+ifh$KdNoo9m=81N?8wghUAFL znbpj!#8o6Du~f8Z+O!X8(-i$ff#ktJffNN=paz1z`K3<<`d+l?Q_-jPu|WHM-<{c6 zt|XT<)u6QJ>du`z_nv$1`T5Sdv$g-&Q$PMMe>~ST`QI@9{Ti;+uNdPSvuI3r(%{Cg znpVZESIuJ8R2HHq%urA>Yju;nVk(tN-l1A_h$`lCchm$!Mc-ld4U4`t(;6}ByUZdw z6n#h8_Xx(deZR}T!I)VbHR>@TDuzs}ZWiliA$qJw!6(J2c(|Jn*`E~Qe%vhXDPG)b z7WW!|*xbXi_8EV~1QTW%-FC6%t}!1h?l(S&wyk5m)&s_`o1m(`yL+t%jX!RDlzVzj zU=$`mxv$rB$oLbySv+j~{i+->{sC2vlEC@tL*(|qR*iWhh=L?+I?Xs*4p%yfmxXcU zq}?f?Kv=d62OXDM*fPP^R-1K;sd$8+B6W~&&)51Sm! zOS-3R`*PR{B5yr7U5^vBL{8ieB8TlxymT#SW;4#hDq6a6$Bx;>aK&p5-eUc9KEYI4 zU+RRdEQ}mW*?V4+28k1C8Km_yy}!Zb ztp#1ICBfQ)wgS6g4w|9YvTe>(xZaMFES+*< zyq4tKr-xyXI?GACj^VL~?G{LP(kxE2fI26!+?wf|D%ECz=nuenkm>lrau@~Nhs23* zMkS#jGvRG>0J18Eg-LEFsm_1 z;Q7-up;;Y82%SwcFKPz5UGmbvvN0RMQ>!A`UQ{_|gM!kaQf0f)wCbf^mL--a>~86q zM$M>5pFnN)+?6*l`K>EWFhA+GyH{G_(iPeWcAsf?HI|n)IK!9lLQ1+!bzkLBg_rhV zu23Vey~)S?Bj?ZKO8Hg*`vp6RvP!YO_gB-!@W3S-1%v;B7Fz!ebsU_ zUM_^PCuUZRAzD3QOlRs2{#3l?HCKyQq_zxZ-%W_pW?*WAZHnS8QeFktcaIw9tS2AR2PG^y_6O%yBWhot#oKD(sMZ} zwBxcl*8V|`Hjm^a!Fqfn@K0$(JTBIJ!)tYdK7RDJsL)*Kwl(+E&D_Y-4p8&Nh^{(# zt&}E0v*x|d&OCo6&AL!fig>?rrcrjW-FZLrT zG^dz6w@I@4@IYKs&UU+;`2dT|g>@#2<5ucSv(qJ1m*Nel*#bXLX**#1f(;L5Gv`E; z`bwiQhHI80b_Y@#8gr*=Hr_Y4E9S!0cg!6~X2skFHW@L=9#*bF!GvF653W?rogs5) z*xas~Yv?w__tAc5gzuyCvD>H(o0qTeG5CLB<7fr8S{{AkL!RG9u=);HK zt(iNc=Ju#beyM`SYr`~3cg76V{7&86+A!U7X5%?cxvQMA81$Hk!VtU7?J<&Ydp9Yn zT>m?>un@gSY?F<0AWVSC)-cU~nU7%UV8upFHfFBXIp1%AA4e-_zkXDRY5{$31D?H9 zAz>=fV*dxh6}*1=`q~!h0@)>9mn#c+k>k(H)}=qiHe7^+(7k1=N!(fKDK9mJ8sflw z!r4f}?o&uB89n7^#acTF9B=ves5XtXR(bEU#b=n*& z;h?P0eQWPvGQT+`{g_#~6}D&MXzHAE<4$j3GCx#0O=){h-9L#nl~gboC)lOwsbaL# z&W%&O(cn;xnF(1!){KG5Z-iN+F*m)NP4s9fP-P}RmIfIxiPvdmdJqEIr1CQ>K}Me? zPdFDx0c2A{S?i(ZOGzhy>KKE*di!;lDBD7y%ejM%W+RN2<7uk8UP^=3vdc$$9;FRR zM=F}vfB>Khsrr${a;;09<{HMgepE z7VE0uW?s|J38A?s(OEmyX(P}HY$GYN+ZWtKys}*)^R+hPcDK<0B_<`tGT5d)m1)Y2 z`!tKEP)Lj#4L@!+8ZNg*3v{35Bd(BRl+&xWHQ{lI#eM;oK-tLnNOkP&*wEOJmNhH* z*~C-*k#py9rSmBI;TYT(uo%lB;xDih5DcL$VOoW;a}LwS3Yb=>54eYJyNghGoRE!c zPH&*FknKz>Tw}bIIX&!F1f&c$Pg}c^n*rETnE9T8>j!f00HtIehO)7v1ps>B12)J7X-|IxoCx_wyo%1ND;@Cu>mp+ zGIu|2-BT#0`9LzRjTH8#7_8l-u`Ks=YulNNR2b24{x*iOSi4e(P)$Jlr3JLGoOPDZ zpFmMA5f%s}n)BYV-fU)0pj)eKwgNA4r}zd}DY}ZC z+JRa#5cobWtJc+eMSI5m>MaO@sR*s|pa@O-1znH6tOyModVhr0>oYEsV1bmif&ysV zTL|>Lb}oGe)4ea7V7Zcq+LS6QXhoWW0ypE5)H$o%+IT9jOM~lj(_Lx(8PueI4G(FQ zZfR@ef3_9x(Qq&a;E zD`zrdfW8dHoE(fhiB`preUH*Uv7>b>ZGwG=(^yYFhMFP`1Q$1i2=K4_b7+ zKf;wxpkQoE?<@aXov1Xq)x3I)mf;2$Bv#}XOa|~U8B;ZYK)6Ti3*yga8=}RD8;u53 zk-!Hoffl^9BwB)df^E3A6RNu}q5ogv%KhfS%G~rG+?JlmJG0c>OM}^Z<&Hp%Gw~k;+(geAn3U*!bAE6sL(t`XhtS<4Va4 zSb@*T7Qjg%1?$($J*1KmL0M-sN*_cuOuva@zO!^jP=(&ddcr)RsN~&R5VUP*sVEhF z;x^Np)(@$RkZt0H$hg2c0cAG>b#X!%PXT)bz{svK35e_!^EQ2sh?atudX@xHa3Bzc zGr^rH&4O@MK0C5CddOmHOqML!w>OZ0SpS4?a&lu01gEBv5FOt@1x5m-z*9h#lP|Eo z0!sm#3d98$$O!l=v>ecSiZ!0q%&p1XqcDU+Bh&Xj#u1dfi>WSOKY?hj6m`z%az|ax zFW1Q2uzX8{oXWjSoCb0_)`L=^i}bJs^2MDX`f_`_%#pqRDMn5AqAuKsD!|hFp>YL^-6j#T>Z2#lt>7`P@ZKcG6F`DO#4w`UJ=EUd*4@XK&F7! z6@m(bpyYvAS4EznJE7ozMC=`bk$u+;fk`|H`BcIBu(^hZu;a)k+>wOap^#Z1W=E<3 zkP<;TQw5{u8e)D_5X0BZN7zXmT`&b95|1epao#LkKWG-vl*ijoa?x>gBScsI3!H;|B5e3vfRx0#wk&JZXJk!^~6KVCZLoJ;`Bq$##8biwLO zu7^3^aEiPyI)r|nfts<=;wUD_2effvt{>K8P|58vhIHq(LNndNc;FsEp&;|?%z(W{ z`h@>w1ho)BGj6-Y&b`3eYkZ3X1K=?%6_~Y4QnMvMVyguy*xL|)*3*@33npj(dtB*p z6s9&(IjFedaOF6Ja{{hs0wLZ+c=t4iB;R0QIu!-YrJj4q8h@Y+SuNGlQx7sk7Fi3NX|CVL4UN<=W(UK zgQDkp?!gCrX0AH-I|!8Qc_8$3L}3vkCY=W^B4Qd<3nC_+1u|l)s|DwNItS#@;<#FH z?5ATuo-6KE%LF~zKF}nXTtkJ{bQV|oD<~)ef_ktnHnuUB5w!P9qQTXqeIggRP2n8Cc9D9J*`w*RAaiJ?< zLrb~dh3F_OE`TpA?lZEuJtkZRKLB@OdBLtDU<>>~&(Zn7$H2aK!S>-^4Q)B__<1s- zNN3w*`B@x|X3kvf11rSqZJf>c#d)4R$eGb`)|`+hGATIYlYMD4oL3xK+Yo~qn~tjp2Y6T9!67H$B~drBWV5T-%p{2VTrEXDb#XFJBw)<8J*(^^Ru*Al z|H-jzbRyeT+g&|aMkf?oi7c@xB~V`Vilh{*Wej4Gv;`a32YV}T%BN@%he>Ci&LNI* zKS0qJ63{1E9&EG|@c_Br6`R#dJ4@S-#i@8!4S*%nW)ikDBD&n5@$hS(T_tU34`M-~ za>ZnHpm-e80>osMl9DB(OsSHf;c5E?;SQ~pVLMvhq^#nM0V_f`NFghTWg(J0XTZxS zhs^hA(`%S=&PoYtxu~#l-D`_Wu5ce)+HhkeHS6F=#Ky6Tr^Gx-S1u|t0?bJUflH}Y z1^@?VV1P6J}eaP}1c%ZwZSGTrqfTabV=E~e< z7L*CCw$FWWZQCZ6aginVZ5;3KC86|Cru4=X$5~t{x8KTjK@T6f@1oc#71ZCHWe82O z{R3ohCwm^@B5oiAMDAc0WUoK~R-zz*)|PD?r}$jBdRVGgAb6`~FJFHaAd$%tF2M%r z(kaBL$pDAd9^qhqPzU*{FRKy?wg;^paQ^}lF_T<2H^jie9|ty?EO&uhuU$BZO23d` zvG~7Oh|t9x$hcW-F`<`<&<@LBf5rbYTJebTg_@QuNM!m_;6s2tn3icWCGa9qH|?9_Ap zBtUyNXCW6kCsSLZarB0Tk^!M5G>nKW?;8J@v#SVmM4!OJI+$0BsnaUWD!ccM5YL&eZ zsUiHkpZ`{cwWw7+(IQ;!7cG103Wd5Nt8Ohhbm~YUZG3Y;TT0if6-jwo!LmKTO2U=Z zY}@M_!z|V~yz2YUb9toNKtn$N5+=941oswK$yCb@D1WEZ`s#g@lYKqrpTG@Gnbjie zI*a54^$PB?v{z(+^#Mz)^@NT`ip(Pp$d#NNkP;FT+g=0HYL@$N*dxnV%5~tliUx-l z8m)aIX+AZMEps-9z8}?{+M?2{^0<_U`&AOcx%-?|kDc`Po7kxU;ux&L?mRfOK!Wc- zI&WGM`941&1fII^SwtRrD3L!q8lh8flr&CrtNOC2!Wkah0u~4=tY>i})?h4*+hNgV z0e%}7-<28n+bq7r;v*J!QIvwG+SURfmq&R2hD+%KM7O(;PaPQ>MU-1Zy*74)hk*DG z_nTtg=bCs)e^{H1c|S%4i?Hd|d;bq&Ui;-MzoWHZz2ehZ`_=0~hQmxHOS>r2RH|8A z|Ciygdx;a!$$hRs*nOx|yNn^r-QVKIQ}EuQTv|Nz@Syi?bbJ&+uOi;z94Vv^2yma< zQGg37jgkn;q#>ZRaN!D?-K!|%D3@ZtYwn}k5Va{3{@eSBA_i7W3|GkH)H95R0idF1xzj5JPcK|OR3rw_ zq)fm!s^}Ci-^~Fl;Z1E`~-x9{E zDQIQs4iSqYuv-IiLn6^+iF`;vO^x5--13DMwasS<4!!QvsQ-ls?O*ihsF>@le|vxm zvY%O1?bk1(g8hPGaLc4QEVX49u0GQjfnYm}R1vopb#%fPUR6WQIo2Nx4`Wb!4qZbH zUj}Mx`5~kLvC@yJOvXUevV((ZLL!h;#3=Zbgih#YnaKVgFTWBjgsAY&OD=i3loKVV{<&k!Ejzz2o!6qdmdUODCM z#qrA3g#JJz@7}=Z)+gU&O{xA3)=FGMiM39-7p%MNT-wg988@Nj-{h{Gz}pXx)5ixR zbrPUmn?VJVMg`ccE-T8848-dLHm}drQ7(}wr6Izxb%IaR1~uXL@W}mr6x&^7RtAoW zh2Cxdd8rmpS+$@P_9XEfZXUQMiYI)gY{8ZObh;#oUb760rSi+9S?ENeyOyB%T~B*? zU>n$%Px02aTvVB~JRe5ywtrkM?@X!nXtY~K9%FgfuYSN{gBI}~zjdQw8{sDTAX$>VWr6GH{b15R-r?M4%|=t?uHq z6yP^GNqM*npNWHI{B{MVNp)+f&>I#vj{x-R85v;iOG?8%aB!Ocg!G}OrlhINRfXj( z{h_`D(U*nz!iB#S)z1NxHgU-U*JD8ewJ{s9rLf(S^!#&t4eq{)AwD5z_o8C(soM~a zw8pDrJS8L#DU3a%&iT5?`CCV2@R5)F6%PXt6;0=?v*+^*(U*|-2ZM1|t-L?r8hDB5 z65Iu1LkV<(kGSsQS4sKTKgD5w`T{0;JNEEXUdQI+^_?|-a+QPulKCD^^Ox~682)?> zpZ@5(H`{N90*R28_St?{BP75)nDY2<1zmgWTt`czVSm7i^K15BX~j$UO@`29E5<3J zjTvP7?C9@#*{Zduv0n=E7G;=(d|Gn4hO|9DkKR)~VL43!=y96M0Ji|-z$D6_LLW#Z zczxt?N+X_Ej6)QLM*___b?zRNX|s@8C$S6BRBsz%eKKG_5|FnA%q~8t;Kx_Ymq@N( z#O$BNe*9pxFkk#mv7i4g{iyxi>Iu}QPV(2m`c*Z4{lhOY`d;NjO{kPZf*UTa=m8L`@J_au-24N z@h!K1f?6N-xZCPSoTKRavQ>>=`_. +However, CherryPy "recognizes" a session id by looking up the saved session +data for that id. Therefore, if you never save any session data, +**you will get a new session id for every request**. + +================ +Sharing Sessions +================ + +If you run multiple instances of CherryPy (for example via mod_python behind +Apache prefork), you most likely cannot use the RAM session backend, since each +instance of CherryPy will have its own memory space. Use a different backend +instead, and verify that all instances are pointing at the same file or db +location. Alternately, you might try a load balancer which makes sessions +"sticky". Google is your friend, there. + +================ +Expiration Dates +================ + +The response cookie will possess an expiration date to inform the client at +which point to stop sending the cookie back in requests. If the server time +and client time differ, expect sessions to be unreliable. **Make sure the +system time of your server is accurate**. + +CherryPy defaults to a 60-minute session timeout, which also applies to the +cookie which is sent to the client. Unfortunately, some versions of Safari +("4 public beta" on Windows XP at least) appear to have a bug in their parsing +of the GMT expiration date--they appear to interpret the date as one hour in +the past. Sixty minutes minus one hour is pretty close to zero, so you may +experience this bug as a new session id for every request, unless the requests +are less than one second apart. To fix, try increasing the session.timeout. + +On the other extreme, some users report Firefox sending cookies after their +expiration date, although this was on a system with an inaccurate system time. +Maybe FF doesn't trust system time. +""" + +import datetime +import os +import random +import time +import threading +import types +from warnings import warn + +import cherrypy +from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr +from cherrypy.lib import httputil + + +missing = object() + +class Session(object): + """A CherryPy dict-like Session object (one per request).""" + + _id = None + + id_observers = None + "A list of callbacks to which to pass new id's." + + def _get_id(self): + return self._id + def _set_id(self, value): + self._id = value + for o in self.id_observers: + o(value) + id = property(_get_id, _set_id, doc="The current session ID.") + + timeout = 60 + "Number of minutes after which to delete session data." + + locked = False + """ + If True, this session instance has exclusive read/write access + to session data.""" + + loaded = False + """ + If True, data has been retrieved from storage. This should happen + automatically on the first attempt to access session data.""" + + clean_thread = None + "Class-level Monitor which calls self.clean_up." + + clean_freq = 5 + "The poll rate for expired session cleanup in minutes." + + originalid = None + "The session id passed by the client. May be missing or unsafe." + + missing = False + "True if the session requested by the client did not exist." + + regenerated = False + """ + True if the application called session.regenerate(). This is not set by + internal calls to regenerate the session id.""" + + debug=False + + def __init__(self, id=None, **kwargs): + self.id_observers = [] + self._data = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + self.originalid = id + self.missing = False + if id is None: + if self.debug: + cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') + self._regenerate() + else: + self.id = id + if not self._exists(): + if self.debug: + cherrypy.log('Expired or malicious session %r; ' + 'making a new one' % id, 'TOOLS.SESSIONS') + # Expired or malicious session. Make a new one. + # See http://www.cherrypy.org/ticket/709. + self.id = None + self.missing = True + self._regenerate() + + def now(self): + """Generate the session specific concept of 'now'. + + Other session providers can override this to use alternative, + possibly timezone aware, versions of 'now'. + """ + return datetime.datetime.now() + + def regenerate(self): + """Replace the current session (with a new id).""" + self.regenerated = True + self._regenerate() + + def _regenerate(self): + if self.id is not None: + self.delete() + + old_session_was_locked = self.locked + if old_session_was_locked: + self.release_lock() + + self.id = None + while self.id is None: + self.id = self.generate_id() + # Assert that the generated id is not already stored. + if self._exists(): + self.id = None + + if old_session_was_locked: + self.acquire_lock() + + def clean_up(self): + """Clean up expired sessions.""" + pass + + def generate_id(self): + """Return a new session id.""" + return random20() + + def save(self): + """Save session data.""" + try: + # If session data has never been loaded then it's never been + # accessed: no need to save it + if self.loaded: + t = datetime.timedelta(seconds = self.timeout * 60) + expiration_time = self.now() + t + if self.debug: + cherrypy.log('Saving with expiry %s' % expiration_time, + 'TOOLS.SESSIONS') + self._save(expiration_time) + + finally: + if self.locked: + # Always release the lock if the user didn't release it + self.release_lock() + + def load(self): + """Copy stored session data into this session instance.""" + data = self._load() + # data is either None or a tuple (session_data, expiration_time) + if data is None or data[1] < self.now(): + if self.debug: + cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS') + self._data = {} + else: + self._data = data[0] + self.loaded = True + + # Stick the clean_thread in the class, not the instance. + # The instances are created and destroyed per-request. + cls = self.__class__ + if self.clean_freq and not cls.clean_thread: + # clean_up is in instancemethod and not a classmethod, + # so that tool config can be accessed inside the method. + t = cherrypy.process.plugins.Monitor( + cherrypy.engine, self.clean_up, self.clean_freq * 60, + name='Session cleanup') + t.subscribe() + cls.clean_thread = t + t.start() + + def delete(self): + """Delete stored session data.""" + self._delete() + + def __getitem__(self, key): + if not self.loaded: self.load() + return self._data[key] + + def __setitem__(self, key, value): + if not self.loaded: self.load() + self._data[key] = value + + def __delitem__(self, key): + if not self.loaded: self.load() + del self._data[key] + + def pop(self, key, default=missing): + """Remove the specified key and return the corresponding value. + If key is not found, default is returned if given, + otherwise KeyError is raised. + """ + if not self.loaded: self.load() + if default is missing: + return self._data.pop(key) + else: + return self._data.pop(key, default) + + def __contains__(self, key): + if not self.loaded: self.load() + return key in self._data + + if hasattr({}, 'has_key'): + def has_key(self, key): + """D.has_key(k) -> True if D has a key k, else False.""" + if not self.loaded: self.load() + return key in self._data + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + if not self.loaded: self.load() + return self._data.get(key, default) + + def update(self, d): + """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" + if not self.loaded: self.load() + self._data.update(d) + + def setdefault(self, key, default=None): + """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" + if not self.loaded: self.load() + return self._data.setdefault(key, default) + + def clear(self): + """D.clear() -> None. Remove all items from D.""" + if not self.loaded: self.load() + self._data.clear() + + def keys(self): + """D.keys() -> list of D's keys.""" + if not self.loaded: self.load() + return self._data.keys() + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" + if not self.loaded: self.load() + return self._data.items() + + def values(self): + """D.values() -> list of D's values.""" + if not self.loaded: self.load() + return self._data.values() + + +class RamSession(Session): + + # Class-level objects. Don't rebind these! + cache = {} + locks = {} + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + for id, (data, expiration_time) in copyitems(self.cache): + if expiration_time <= now: + try: + del self.cache[id] + except KeyError: + pass + try: + del self.locks[id] + except KeyError: + pass + + # added to remove obsolete lock objects + for id in list(self.locks): + if id not in self.cache: + self.locks.pop(id, None) + + def _exists(self): + return self.id in self.cache + + def _load(self): + return self.cache.get(self.id) + + def _save(self, expiration_time): + self.cache[self.id] = (self._data, expiration_time) + + def _delete(self): + self.cache.pop(self.id, None) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + return len(self.cache) + + +class FileSession(Session): + """Implementation of the File backend for sessions + + storage_path + The folder where session data will be saved. Each session + will be saved as pickle.dump(data, expiration_time) in its own file; + the filename will be self.SESSION_PREFIX + self.id. + + """ + + SESSION_PREFIX = 'session-' + LOCK_SUFFIX = '.lock' + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + Session.__init__(self, id=id, **kwargs) + + def setup(cls, **kwargs): + """Set up the storage system for file-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + + for k, v in kwargs.items(): + setattr(cls, k, v) + + # Warn if any lock files exist at startup. + lockfiles = [fname for fname in os.listdir(cls.storage_path) + if (fname.startswith(cls.SESSION_PREFIX) + and fname.endswith(cls.LOCK_SUFFIX))] + if lockfiles: + plural = ('', 's')[len(lockfiles) > 1] + warn("%s session lockfile%s found at startup. If you are " + "only running one process, then you may need to " + "manually delete the lockfiles found at %r." + % (len(lockfiles), plural, cls.storage_path)) + setup = classmethod(setup) + + def _get_file_path(self): + f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) + if not os.path.abspath(f).startswith(self.storage_path): + raise cherrypy.HTTPError(400, "Invalid session id in cookie.") + return f + + def _exists(self): + path = self._get_file_path() + return os.path.exists(path) + + def _load(self, path=None): + if path is None: + path = self._get_file_path() + try: + f = open(path, "rb") + try: + return pickle.load(f) + finally: + f.close() + except (IOError, EOFError): + return None + + def _save(self, expiration_time): + f = open(self._get_file_path(), "wb") + try: + pickle.dump((self._data, expiration_time), f, self.pickle_protocol) + finally: + f.close() + + def _delete(self): + try: + os.unlink(self._get_file_path()) + except OSError: + pass + + def acquire_lock(self, path=None): + """Acquire an exclusive lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + path += self.LOCK_SUFFIX + while True: + try: + lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) + except OSError: + time.sleep(0.1) + else: + os.close(lockfd) + break + self.locked = True + + def release_lock(self, path=None): + """Release the lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + os.unlink(path + self.LOCK_SUFFIX) + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + # Iterate over all session files in self.storage_path + for fname in os.listdir(self.storage_path): + if (fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX)): + # We have a session file: lock and load it and check + # if it's expired. If it fails, nevermind. + path = os.path.join(self.storage_path, fname) + self.acquire_lock(path) + try: + contents = self._load(path) + # _load returns None on IOError + if contents is not None: + data, expiration_time = contents + if expiration_time < now: + # Session expired: deleting it + os.unlink(path) + finally: + self.release_lock(path) + + def __len__(self): + """Return the number of active sessions.""" + return len([fname for fname in os.listdir(self.storage_path) + if (fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX))]) + + +class PostgresqlSession(Session): + """ Implementation of the PostgreSQL backend for sessions. It assumes + a table like this:: + + create table session ( + id varchar(40), + data text, + expiration_time timestamp + ) + + You must provide your own get_db function. + """ + + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + Session.__init__(self, id, **kwargs) + self.cursor = self.db.cursor() + + def setup(cls, **kwargs): + """Set up the storage system for Postgres-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + self.db = self.get_db() + setup = classmethod(setup) + + def __del__(self): + if self.cursor: + self.cursor.close() + self.db.commit() + + def _exists(self): + # Select session data from table + self.cursor.execute('select data, expiration_time from session ' + 'where id=%s', (self.id,)) + rows = self.cursor.fetchall() + return bool(rows) + + def _load(self): + # Select session data from table + self.cursor.execute('select data, expiration_time from session ' + 'where id=%s', (self.id,)) + rows = self.cursor.fetchall() + if not rows: + return None + + pickled_data, expiration_time = rows[0] + data = pickle.loads(pickled_data) + return data, expiration_time + + def _save(self, expiration_time): + pickled_data = pickle.dumps(self._data, self.pickle_protocol) + self.cursor.execute('update session set data = %s, ' + 'expiration_time = %s where id = %s', + (pickled_data, expiration_time, self.id)) + + def _delete(self): + self.cursor.execute('delete from session where id=%s', (self.id,)) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + # We use the "for update" clause to lock the row + self.locked = True + self.cursor.execute('select id from session where id=%s for update', + (self.id,)) + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + # We just close the cursor and that will remove the lock + # introduced by the "for update" clause + self.cursor.close() + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + self.cursor.execute('delete from session where expiration_time < %s', + (self.now(),)) + + +class MemcachedSession(Session): + + # The most popular memcached client for Python isn't thread-safe. + # Wrap all .get and .set operations in a single lock. + mc_lock = threading.RLock() + + # This is a seperate set of locks per session id. + locks = {} + + servers = ['127.0.0.1:11211'] + + def setup(cls, **kwargs): + """Set up the storage system for memcached-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + import memcache + cls.cache = memcache.Client(cls.servers) + setup = classmethod(setup) + + def _get_id(self): + return self._id + def _set_id(self, value): + # This encode() call is where we differ from the superclass. + # Memcache keys MUST be byte strings, not unicode. + if isinstance(value, unicodestr): + value = value.encode('utf-8') + + self._id = value + for o in self.id_observers: + o(value) + id = property(_get_id, _set_id, doc="The current session ID.") + + def _exists(self): + self.mc_lock.acquire() + try: + return bool(self.cache.get(self.id)) + finally: + self.mc_lock.release() + + def _load(self): + self.mc_lock.acquire() + try: + return self.cache.get(self.id) + finally: + self.mc_lock.release() + + def _save(self, expiration_time): + # Send the expiration time as "Unix time" (seconds since 1/1/1970) + td = int(time.mktime(expiration_time.timetuple())) + self.mc_lock.acquire() + try: + if not self.cache.set(self.id, (self._data, expiration_time), td): + raise AssertionError("Session data for id %r not set." % self.id) + finally: + self.mc_lock.release() + + def _delete(self): + self.cache.delete(self.id) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + raise NotImplementedError + + +# Hook functions (for CherryPy tools) + +def save(): + """Save any changed session data.""" + + if not hasattr(cherrypy.serving, "session"): + return + request = cherrypy.serving.request + response = cherrypy.serving.response + + # Guard against running twice + if hasattr(request, "_sessionsaved"): + return + request._sessionsaved = True + + if response.stream: + # If the body is being streamed, we have to save the data + # *after* the response has been written out + request.hooks.attach('on_end_request', cherrypy.session.save) + else: + # If the body is not being streamed, we save the data now + # (so we can release the lock). + if isinstance(response.body, types.GeneratorType): + response.collapse_body() + cherrypy.session.save() +save.failsafe = True + +def close(): + """Close the session object for this request.""" + sess = getattr(cherrypy.serving, "session", None) + if getattr(sess, "locked", False): + # If the session is still locked we release the lock + sess.release_lock() +close.failsafe = True +close.priority = 90 + + +def init(storage_type='ram', path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, clean_freq=5, + persistent=True, httponly=False, debug=False, **kwargs): + """Initialize session object (using cookies). + + storage_type + One of 'ram', 'file', 'postgresql', 'memcached'. This will be + used to look up the corresponding class in cherrypy.lib.sessions + globals. For example, 'file' will use the FileSession class. + + path + The 'path' value to stick in the response cookie metadata. + + path_header + If 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + The name of the cookie. + + timeout + The expiration timeout (in minutes) for the stored session data. + If 'persistent' is True (the default), this is also the timeout + for the cookie. + + domain + The cookie domain. + + secure + If False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + clean_freq (minutes) + The poll rate for expired session cleanup. + + persistent + If True (the default), the 'timeout' argument will be used + to expire the cookie. If False, the cookie will not have an expiry, + and the cookie will be a "session cookie" which expires when the + browser is closed. + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + Any additional kwargs will be bound to the new Session instance, + and may be specific to the storage type. See the subclass of Session + you're using for more information. + """ + + request = cherrypy.serving.request + + # Guard against running twice + if hasattr(request, "_session_init_flag"): + return + request._session_init_flag = True + + # Check if request came with a session ID + id = None + if name in request.cookie: + id = request.cookie[name].value + if debug: + cherrypy.log('ID obtained from request.cookie: %r' % id, + 'TOOLS.SESSIONS') + + # Find the storage class and call setup (first time only). + storage_class = storage_type.title() + 'Session' + storage_class = globals()[storage_class] + if not hasattr(cherrypy, "session"): + if hasattr(storage_class, "setup"): + storage_class.setup(**kwargs) + + # Create and attach a new Session instance to cherrypy.serving. + # It will possess a reference to (and lock, and lazily load) + # the requested session data. + kwargs['timeout'] = timeout + kwargs['clean_freq'] = clean_freq + cherrypy.serving.session = sess = storage_class(id, **kwargs) + sess.debug = debug + def update_cookie(id): + """Update the cookie every time the session id changes.""" + cherrypy.serving.response.cookie[name] = id + sess.id_observers.append(update_cookie) + + # Create cherrypy.session which will proxy to cherrypy.serving.session + if not hasattr(cherrypy, "session"): + cherrypy.session = cherrypy._ThreadLocalProxy('session') + + if persistent: + cookie_timeout = timeout + else: + # See http://support.microsoft.com/kb/223799/EN-US/ + # and http://support.mozilla.com/en-US/kb/Cookies + cookie_timeout = None + set_response_cookie(path=path, path_header=path_header, name=name, + timeout=cookie_timeout, domain=domain, secure=secure, + httponly=httponly) + + +def set_response_cookie(path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, httponly=False): + """Set a response cookie for the client. + + path + the 'path' value to stick in the response cookie metadata. + + path_header + if 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + the name of the cookie. + + timeout + the expiration timeout for the cookie. If 0 or other boolean + False, no 'expires' param will be set, and the cookie will be a + "session cookie" which expires when the browser is closed. + + domain + the cookie domain. + + secure + if False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + """ + # Set response cookie + cookie = cherrypy.serving.response.cookie + cookie[name] = cherrypy.serving.session.id + cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header) + or '/') + + # We'd like to use the "max-age" param as indicated in + # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't + # save it to disk and the session is lost if people close + # the browser. So we have to use the old "expires" ... sigh ... +## cookie[name]['max-age'] = timeout * 60 + if timeout: + e = time.time() + (timeout * 60) + cookie[name]['expires'] = httputil.HTTPDate(e) + if domain is not None: + cookie[name]['domain'] = domain + if secure: + cookie[name]['secure'] = 1 + if httponly: + if not cookie[name].isReservedKey('httponly'): + raise ValueError("The httponly cookie token is not supported.") + cookie[name]['httponly'] = 1 + +def expire(): + """Expire the current session cookie.""" + name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id') + one_year = 60 * 60 * 24 * 365 + e = time.time() - one_year + cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) + + diff --git a/cherrypy/lib/sessions.pyc b/cherrypy/lib/sessions.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce3d4a0935bc0679db6a74296496f9a968fa792c GIT binary patch literal 30551 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 filename=". If name is None, it will be set + to the basename of path. If disposition is None, no Content-Disposition + header will be written. + """ + + response = cherrypy.serving.response + + # If path is relative, users should fix it by making path absolute. + # That is, CherryPy should not guess where the application root is. + # It certainly should *not* use cwd (since CP may be invoked from a + # variety of paths). If using tools.staticdir, you can make your relative + # paths become absolute by supplying a value for "tools.staticdir.root". + if not os.path.isabs(path): + msg = "'%s' is not an absolute path." % path + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + + try: + st = os.stat(path) + except OSError: + if debug: + cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Check if path is a directory. + if stat.S_ISDIR(st.st_mode): + # Let the caller deal with it as they like. + if debug: + cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + + if content_type is None: + # Set content-type based on filename extension + ext = "" + i = path.rfind('.') + if i != -1: + ext = path[i:].lower() + content_type = mimetypes.types_map.get(ext, None) + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + name = os.path.basename(path) + cd = '%s; filename="%s"' % (disposition, name) + response.headers["Content-Disposition"] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + content_length = st.st_size + fileobj = open(path, 'rb') + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + +def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given file object. + + The Content-Type header will be set to the content_type arg, if provided. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=". If name is None, 'filename' will + not be set. If disposition is None, no Content-Disposition header will + be written. + + CAUTION: If the request contains a 'Range' header, one or more seek()s will + be performed on the file object. This may cause undesired behavior if + the file object is not seekable. It could also produce undesired results + if the caller set the read position of the file object prior to calling + serve_fileobj(), expecting that the data would be served starting from that + position. + """ + + response = cherrypy.serving.response + + try: + st = os.fstat(fileobj.fileno()) + except AttributeError: + if debug: + cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') + content_length = None + except UnsupportedOperation: + content_length = None + else: + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + content_length = st.st_size + + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + cd = disposition + else: + cd = '%s; filename="%s"' % (disposition, name) + response.headers["Content-Disposition"] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + +def _serve_fileobj(fileobj, content_type, content_length, debug=False): + """Internal. Set response.body to the given file object, perhaps ranged.""" + response = cherrypy.serving.response + + # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code + request = cherrypy.serving.request + if request.protocol >= (1, 1): + response.headers["Accept-Ranges"] = "bytes" + r = httputil.get_ranges(request.headers.get('Range'), content_length) + if r == []: + response.headers['Content-Range'] = "bytes */%s" % content_length + message = "Invalid Range (first-byte-pos greater than Content-Length)" + if debug: + cherrypy.log(message, 'TOOLS.STATIC') + raise cherrypy.HTTPError(416, message) + + if r: + if len(r) == 1: + # Return a single-part response. + start, stop = r[0] + if stop > content_length: + stop = content_length + r_len = stop - start + if debug: + cherrypy.log('Single part; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + response.status = "206 Partial Content" + response.headers['Content-Range'] = ( + "bytes %s-%s/%s" % (start, stop - 1, content_length)) + response.headers['Content-Length'] = r_len + fileobj.seek(start) + response.body = file_generator_limited(fileobj, r_len) + else: + # Return a multipart/byteranges response. + response.status = "206 Partial Content" + try: + # Python 3 + from email.generator import _make_boundary as choose_boundary + except ImportError: + # Python 2 + from mimetools import choose_boundary + boundary = choose_boundary() + ct = "multipart/byteranges; boundary=%s" % boundary + response.headers['Content-Type'] = ct + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + + def file_ranges(): + # Apache compatibility: + yield ntob("\r\n") + + for start, stop in r: + if debug: + cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + yield ntob("--" + boundary, 'ascii') + yield ntob("\r\nContent-type: %s" % content_type, 'ascii') + yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n" + % (start, stop - 1, content_length), 'ascii') + fileobj.seek(start) + for chunk in file_generator_limited(fileobj, stop-start): + yield chunk + yield ntob("\r\n") + # Final boundary + yield ntob("--" + boundary + "--", 'ascii') + + # Apache compatibility: + yield ntob("\r\n") + response.body = file_ranges() + return response.body + else: + if debug: + cherrypy.log('No byteranges requested', 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + response.headers['Content-Length'] = content_length + response.body = fileobj + return response.body + +def serve_download(path, name=None): + """Serve 'path' as an application/x-download attachment.""" + # This is such a common idiom I felt it deserved its own wrapper. + return serve_file(path, "application/x-download", "attachment", name) + + +def _attempt(filename, content_types, debug=False): + if debug: + cherrypy.log('Attempting %r (content_types %r)' % + (filename, content_types), 'TOOLS.STATICDIR') + try: + # you can set the content types for a + # complete directory per extension + content_type = None + if content_types: + r, ext = os.path.splitext(filename) + content_type = content_types.get(ext[1:], None) + serve_file(filename, content_type=content_type, debug=debug) + return True + except cherrypy.NotFound: + # If we didn't find the static file, continue handling the + # request. We might find a dynamic handler instead. + if debug: + cherrypy.log('NotFound', 'TOOLS.STATICFILE') + return False + +def staticdir(section, dir, root="", match="", content_types=None, index="", + debug=False): + """Serve a static resource from the given (root +) dir. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + index + If provided, it should be the (relative) name of a file to + serve for directory requests. For example, if the dir argument is + '/home/me', the Request-URI is 'myapp', and the index arg is + 'index.html', the file '/home/me/myapp/index.html' will be sought. + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICDIR') + return False + + # Allow the use of '~' to refer to a user's home directory. + dir = os.path.expanduser(dir) + + # If dir is relative, make absolute using "root". + if not os.path.isabs(dir): + if not root: + msg = "Static dir requires an absolute dir (or root)." + if debug: + cherrypy.log(msg, 'TOOLS.STATICDIR') + raise ValueError(msg) + dir = os.path.join(root, dir) + + # Determine where we are in the object tree relative to 'section' + # (where the static tool was defined). + if section == 'global': + section = "/" + section = section.rstrip(r"\/") + branch = request.path_info[len(section) + 1:] + branch = unquote(branch.lstrip(r"\/")) + + # If branch is "", filename will end in a slash + filename = os.path.join(dir, branch) + if debug: + cherrypy.log('Checking file %r to fulfill %r' % + (filename, request.path_info), 'TOOLS.STATICDIR') + + # There's a chance that the branch pulled from the URL might + # have ".." or similar uplevel attacks in it. Check that the final + # filename is a child of dir. + if not os.path.normpath(filename).startswith(os.path.normpath(dir)): + raise cherrypy.HTTPError(403) # Forbidden + + handled = _attempt(filename, content_types) + if not handled: + # Check for an index file if a folder was requested. + if index: + handled = _attempt(os.path.join(filename, index), content_types) + if handled: + request.is_index = filename[-1] in (r"\/") + return handled + +def staticfile(filename, root=None, match="", content_types=None, debug=False): + """Serve a static resource from the given (root +) filename. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICFILE') + return False + + # If filename is relative, make absolute using "root". + if not os.path.isabs(filename): + if not root: + msg = "Static tool requires an absolute filename (got '%s')." % filename + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + filename = os.path.join(root, filename) + + return _attempt(filename, content_types, debug=debug) diff --git a/cherrypy/lib/static.pyc b/cherrypy/lib/static.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f8f938630281b9454b7054439060ad761aa306d GIT binary patch literal 10849 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 literal 0 HcmV?d00001 diff --git a/cherrypy/lib/xmlrpcutil.py b/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 0000000..9a44464 --- /dev/null +++ b/cherrypy/lib/xmlrpcutil.py @@ -0,0 +1,55 @@ +import sys + +import cherrypy +from cherrypy._cpcompat import ntob + +def get_xmlrpclib(): + try: + import xmlrpc.client as x + except ImportError: + import xmlrpclib as x + return x + +def process_body(): + """Return (params, method) from request body.""" + try: + return get_xmlrpclib().loads(cherrypy.request.body.read()) + except Exception: + return ('ERROR PARAMS', ), 'ERRORMETHOD' + + +def patched_path(path): + """Return 'path', doctored for RPC.""" + if not path.endswith('/'): + path += '/' + if path.startswith('/RPC2/'): + # strip the first /rpc2 + path = path[5:] + return path + + +def _set_response(body): + # The XML-RPC spec (http://www.xmlrpc.com/spec) says: + # "Unless there's a lower-level error, always return 200 OK." + # Since Python's xmlrpclib interprets a non-200 response + # as a "Protocol Error", we'll just return 200 every time. + response = cherrypy.response + response.status = '200 OK' + response.body = ntob(body, 'utf-8') + response.headers['Content-Type'] = 'text/xml' + response.headers['Content-Length'] = len(body) + + +def respond(body, encoding='utf-8', allow_none=0): + xmlrpclib = get_xmlrpclib() + if not isinstance(body, xmlrpclib.Fault): + body = (body,) + _set_response(xmlrpclib.dumps(body, methodresponse=1, + encoding=encoding, + allow_none=allow_none)) + +def on_error(*args, **kwargs): + body = str(sys.exc_info()[1]) + xmlrpclib = get_xmlrpclib() + _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body))) + diff --git a/cherrypy/lib/xmlrpcutil.pyc b/cherrypy/lib/xmlrpcutil.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed5ca4b174aa8ea7f25cd19ac4e06f6ba223ce36 GIT binary patch literal 2174 zcmbW2ZEqVz5XWcl>?95ei4uxO|*Ct)fV$%iTJ@^n82l zu9X-iuiCG{r{g2=0pLG#PV%B}N-W=aZXaiV^PhG2&+_7(zyIoIH2Z}3{Q-{~!Q}WV zDv1uRd_@OJ+5;*B8i#ZclFCDRjX^ESTjI1SZ;LZW`J6cOl;gWVd52yHbO2{j+B>|# zp!*IsEB?i&-&JsZ3#RhsNW$WlHl7_k?jg){oo699M0TR+g6Mg5n_51gmy2Y#NGUbz z54p2a^x~YbOMtn3*`~`I_+^;0acS$Umz9OCyq`nM%;k`N9oNSCU29=GSA)yqsELvT zJL|%b821^xo$n5RNyqxn!)&Cjoz~Ms3_omknfRjY)l=E`So@@DML%!r7I`5Rs&%!j zR+XHbTdq$2EncP(d7@WxY#jF_q>~n%w7KOKI6kk|fMHHBd?OgzGCNlTUkY3Bz~ecrT9jAIFcM4m>lFhNrvF zp6vb9BeP=m?nSxDyszm{!Y*HzMGB_ZQ^}vvtUrd zRY2(~cc+CPdDe{csrB-DT|2z+HJP^-ra(zO@{lLI#4TQ4bQ!Dw$x1N8!bn0qg5?Pu zh>{Btr&n~YC<(}i0st@qX~Erq7R6%CTeJ<4f*nB3n%cCDdTwOemxFITco6OV=J?K6-$dDnuy#!4QMC7;O>6Dm<#1*Vzw7E4=NFlA*$OkOU2Q6-rN+U`Ph5;X zidod6xDmvqtwZDxrLF|G^v3U@Y?uT1Ijb@RZzY(SYTq-nru;7h zELeCmiDf1)z>(9RmZdpODpTn$&unU!I|^4+U|D5aO8Mt>Qu_D9Z^;s-Y1&0wKPG3Gr>7#X-ppmfB+{ z5`1~eZUg3qI2N#|sS^Cp82dGRsT)%7gGiUN6MxD^Dqs!X<@MT2vN|(k6nIkre9gqH yg?OC}r!Quu#-GDnXGy%bzDhW@?Xk%xrT(6|xmB2@U`ee8o%zl}XR))ivhW`+OR<#z literal 0 HcmV?d00001 diff --git a/cherrypy/process/__init__.py b/cherrypy/process/__init__.py new file mode 100644 index 0000000..f15b123 --- /dev/null +++ b/cherrypy/process/__init__.py @@ -0,0 +1,14 @@ +"""Site container for an HTTP server. + +A Web Site Process Bus object is used to connect applications, servers, +and frameworks with site-wide services such as daemonization, process +reload, signal handling, drop privileges, PID file management, logging +for all of these, and many more. + +The 'plugins' module defines a few abstract and concrete services for +use with the bus. Some use tool-specific channels; see the documentation +for each class. +""" + +from cherrypy.process.wspbus import bus +from cherrypy.process import plugins, servers diff --git a/cherrypy/process/__init__.pyc b/cherrypy/process/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c12cfd8794e6b1d824f0f2bd6223eb10283a66d8 GIT binary patch literal 742 zcmZuv&5GMF5SH_^EiQ$<^;YImHV3cixh+3}n~0#ii_$m@Q2PMV!RO6p354MeLz(CA;VbT8 zL@t9@NCY1S0>A%;DuN*x2sK2Xcw9=!zPConXuPB1%tJ*korXHd4i7$jCpehc0&)H8 zgV8v`43h&5ss%})C3fDK`;n)B{!}^-XuZ@-YZ@mlw9IOaYYNc8_lz=k#$tn9c=_c9 zYTiI6ooui}7YneyX&9c5O05N7Lu`?-VBI<)kI?ymEPK-;T=jNfI9+j44@{-8W}^rs z)OY~7rx;{X%y(J63dnXQ<3=T1BWG_;O1TQ`2P$FhJ4}|bW8PjC0nDcBr0?BhsT0jDB_d@Q!mm#5g&0lzj|Ds+DNyHi-*&yV&`, usually cherrypy.engine.""" + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Register this object as a (multi-channel) listener on the bus.""" + for channel in self.bus.listeners: + # Subscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.subscribe(channel, method) + + def unsubscribe(self): + """Unregister this object as a listener on the bus.""" + for channel in self.bus.listeners: + # Unsubscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.unsubscribe(channel, method) + + + +class SignalHandler(object): + """Register bus channels (and listeners) for system signals. + + You can modify what signals your application listens for, and what it does + when it receives signals, by modifying :attr:`SignalHandler.handlers`, + a dict of {signal name: callback} pairs. The default set is:: + + handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + The :func:`SignalHandler.handle_SIGHUP`` method calls + :func:`bus.restart()` + if the process is daemonized, but + :func:`bus.exit()` + if the process is attached to a TTY. This is because Unix window + managers tend to send SIGHUP to terminal windows when the user closes them. + + Feel free to add signals which are not available on every platform. The + :class:`SignalHandler` will ignore errors raised from attempting to register + handlers for unknown signals. + """ + + handlers = {} + """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" + + signals = {} + """A map from signal numbers to names.""" + + for k, v in vars(_signal).items(): + if k.startswith('SIG') and not k.startswith('SIG_'): + signals[v] = k + del k, v + + def __init__(self, bus): + self.bus = bus + # Set default handlers + self.handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + if sys.platform[:4] == 'java': + del self.handlers['SIGUSR1'] + self.handlers['SIGUSR2'] = self.bus.graceful + self.bus.log("SIGUSR1 cannot be set on the JVM platform. " + "Using SIGUSR2 instead.") + self.handlers['SIGINT'] = self._jython_SIGINT_handler + + self._previous_handlers = {} + + def _jython_SIGINT_handler(self, signum=None, frame=None): + # See http://bugs.jython.org/issue1313 + self.bus.log('Keyboard Interrupt: shutting down bus') + self.bus.exit() + + def subscribe(self): + """Subscribe self.handlers to signals.""" + for sig, func in self.handlers.items(): + try: + self.set_handler(sig, func) + except ValueError: + pass + + def unsubscribe(self): + """Unsubscribe self.handlers from signals.""" + for signum, handler in self._previous_handlers.items(): + signame = self.signals[signum] + + if handler is None: + self.bus.log("Restoring %s handler to SIG_DFL." % signame) + handler = _signal.SIG_DFL + else: + self.bus.log("Restoring %s handler %r." % (signame, handler)) + + try: + our_handler = _signal.signal(signum, handler) + if our_handler is None: + self.bus.log("Restored old %s handler %r, but our " + "handler was not registered." % + (signame, handler), level=30) + except ValueError: + self.bus.log("Unable to restore %s handler %r." % + (signame, handler), level=40, traceback=True) + + def set_handler(self, signal, listener=None): + """Subscribe a handler for the given signal (number or name). + + If the optional 'listener' argument is provided, it will be + subscribed as a listener for the given signal's channel. + + If the given signal name or number is not available on the current + platform, ValueError is raised. + """ + if isinstance(signal, basestring): + signum = getattr(_signal, signal, None) + if signum is None: + raise ValueError("No such signal: %r" % signal) + signame = signal + else: + try: + signame = self.signals[signal] + except KeyError: + raise ValueError("No such signal: %r" % signal) + signum = signal + + prev = _signal.signal(signum, self._handle_signal) + self._previous_handlers[signum] = prev + + if listener is not None: + self.bus.log("Listening for %s." % signame) + self.bus.subscribe(signame, listener) + + def _handle_signal(self, signum=None, frame=None): + """Python signal handler (self.set_handler subscribes it for you).""" + signame = self.signals[signum] + self.bus.log("Caught signal %s." % signame) + self.bus.publish(signame) + + def handle_SIGHUP(self): + """Restart if daemonized, else exit.""" + if os.isatty(sys.stdin.fileno()): + # not daemonized (may be foreground or background) + self.bus.log("SIGHUP caught but not daemonized. Exiting.") + self.bus.exit() + else: + self.bus.log("SIGHUP caught while daemonized. Restarting.") + self.bus.restart() + + +try: + import pwd, grp +except ImportError: + pwd, grp = None, None + + +class DropPrivileges(SimplePlugin): + """Drop privileges. uid/gid arguments not available on Windows. + + Special thanks to Gavin Baker: http://antonym.org/node/100. + """ + + def __init__(self, bus, umask=None, uid=None, gid=None): + SimplePlugin.__init__(self, bus) + self.finalized = False + self.uid = uid + self.gid = gid + self.umask = umask + + def _get_uid(self): + return self._uid + def _set_uid(self, val): + if val is not None: + if pwd is None: + self.bus.log("pwd module not available; ignoring uid.", + level=30) + val = None + elif isinstance(val, basestring): + val = pwd.getpwnam(val)[2] + self._uid = val + uid = property(_get_uid, _set_uid, + doc="The uid under which to run. Availability: Unix.") + + def _get_gid(self): + return self._gid + def _set_gid(self, val): + if val is not None: + if grp is None: + self.bus.log("grp module not available; ignoring gid.", + level=30) + val = None + elif isinstance(val, basestring): + val = grp.getgrnam(val)[2] + self._gid = val + gid = property(_get_gid, _set_gid, + doc="The gid under which to run. Availability: Unix.") + + def _get_umask(self): + return self._umask + def _set_umask(self, val): + if val is not None: + try: + os.umask + except AttributeError: + self.bus.log("umask function not available; ignoring umask.", + level=30) + val = None + self._umask = val + umask = property(_get_umask, _set_umask, + doc="""The default permission mode for newly created files and directories. + + Usually expressed in octal format, for example, ``0644``. + Availability: Unix, Windows. + """) + + def start(self): + # uid/gid + def current_ids(): + """Return the current (uid, gid) if available.""" + name, group = None, None + if pwd: + name = pwd.getpwuid(os.getuid())[0] + if grp: + group = grp.getgrgid(os.getgid())[0] + return name, group + + if self.finalized: + if not (self.uid is None and self.gid is None): + self.bus.log('Already running as uid: %r gid: %r' % + current_ids()) + else: + if self.uid is None and self.gid is None: + if pwd or grp: + self.bus.log('uid/gid not set', level=30) + else: + self.bus.log('Started as uid: %r gid: %r' % current_ids()) + if self.gid is not None: + os.setgid(self.gid) + os.setgroups([]) + if self.uid is not None: + os.setuid(self.uid) + self.bus.log('Running as uid: %r gid: %r' % current_ids()) + + # umask + if self.finalized: + if self.umask is not None: + self.bus.log('umask already set to: %03o' % self.umask) + else: + if self.umask is None: + self.bus.log('umask not set', level=30) + else: + old_umask = os.umask(self.umask) + self.bus.log('umask old: %03o, new: %03o' % + (old_umask, self.umask)) + + self.finalized = True + # This is slightly higher than the priority for server.start + # in order to facilitate the most common use: starting on a low + # port (which requires root) and then dropping to another user. + start.priority = 77 + + +class Daemonizer(SimplePlugin): + """Daemonize the running script. + + Use this with a Web Site Process Bus via:: + + Daemonizer(bus).subscribe() + + When this component finishes, the process is completely decoupled from + the parent environment. Please note that when this component is used, + the return code from the parent process will still be 0 if a startup + error occurs in the forked children. Errors in the initial daemonizing + process still return proper exit codes. Therefore, if you use this + plugin to daemonize, don't use the return code as an accurate indicator + of whether the process fully started. In fact, that return code only + indicates if the process succesfully finished the first fork. + """ + + def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', + stderr='/dev/null'): + SimplePlugin.__init__(self, bus) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.finalized = False + + def start(self): + if self.finalized: + self.bus.log('Already deamonized.') + + # forking has issues with threads: + # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html + # "The general problem with making fork() work in a multi-threaded + # world is what to do with all of the threads..." + # So we check for active threads: + if threading.activeCount() != 1: + self.bus.log('There are %r active threads. ' + 'Daemonizing now may cause strange failures.' % + threading.enumerate(), level=30) + + # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) + # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 + + # Finish up with the current stdout/stderr + sys.stdout.flush() + sys.stderr.flush() + + # Do first fork. + try: + pid = os.fork() + if pid == 0: + # This is the child process. Continue. + pass + else: + # This is the first parent. Exit, now that we've forked. + self.bus.log('Forking once.') + os._exit(0) + except OSError: + # Python raises OSError rather than returning negative numbers. + exc = sys.exc_info()[1] + sys.exit("%s: fork #1 failed: (%d) %s\n" + % (sys.argv[0], exc.errno, exc.strerror)) + + os.setsid() + + # Do second fork + try: + pid = os.fork() + if pid > 0: + self.bus.log('Forking twice.') + os._exit(0) # Exit second parent + except OSError: + exc = sys.exc_info()[1] + sys.exit("%s: fork #2 failed: (%d) %s\n" + % (sys.argv[0], exc.errno, exc.strerror)) + + os.chdir("/") + os.umask(0) + + si = open(self.stdin, "r") + so = open(self.stdout, "a+") + se = open(self.stderr, "a+") + + # os.dup2(fd, fd2) will close fd2 if necessary, + # so we don't explicitly close stdin/out/err. + # See http://docs.python.org/lib/os-fd-ops.html + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + self.bus.log('Daemonized to PID: %s' % os.getpid()) + self.finalized = True + start.priority = 65 + + +class PIDFile(SimplePlugin): + """Maintain a PID file via a WSPBus.""" + + def __init__(self, bus, pidfile): + SimplePlugin.__init__(self, bus) + self.pidfile = pidfile + self.finalized = False + + def start(self): + pid = os.getpid() + if self.finalized: + self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) + else: + open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8')) + self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) + self.finalized = True + start.priority = 70 + + def exit(self): + try: + os.remove(self.pidfile) + self.bus.log('PID file removed: %r.' % self.pidfile) + except (KeyboardInterrupt, SystemExit): + raise + except: + pass + + +class PerpetualTimer(threading._Timer): + """A responsive subclass of threading._Timer whose run() method repeats. + + Use this timer only when you really need a very interruptible timer; + this checks its 'finished' condition up to 20 times a second, which can + results in pretty high CPU usage + """ + + def run(self): + while True: + self.finished.wait(self.interval) + if self.finished.isSet(): + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + self.bus.log("Error in perpetual timer thread function %r." % + self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class BackgroundTask(threading.Thread): + """A subclass of threading.Thread whose run() method repeats. + + Use this class for most repeating tasks. It uses time.sleep() to wait + for each interval, which isn't very responsive; that is, even if you call + self.cancel(), you'll have to wait until the sleep() call finishes before + the thread stops. To compensate, it defaults to being daemonic, which means + it won't delay stopping the whole process. + """ + + def __init__(self, interval, function, args=[], kwargs={}, bus=None): + threading.Thread.__init__(self) + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.running = False + self.bus = bus + + def cancel(self): + self.running = False + + def run(self): + self.running = True + while self.running: + time.sleep(self.interval) + if not self.running: + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log("Error in background task thread function %r." + % self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + def _set_daemon(self): + return True + + +class Monitor(SimplePlugin): + """WSPBus listener to periodically run a callback in its own thread.""" + + callback = None + """The function to call at intervals.""" + + frequency = 60 + """The time in seconds between callback runs.""" + + thread = None + """A :class:`BackgroundTask` thread.""" + + def __init__(self, bus, callback, frequency=60, name=None): + SimplePlugin.__init__(self, bus) + self.callback = callback + self.frequency = frequency + self.thread = None + self.name = name + + def start(self): + """Start our callback in its own background thread.""" + if self.frequency > 0: + threadname = self.name or self.__class__.__name__ + if self.thread is None: + self.thread = BackgroundTask(self.frequency, self.callback, + bus = self.bus) + self.thread.setName(threadname) + self.thread.start() + self.bus.log("Started monitor thread %r." % threadname) + else: + self.bus.log("Monitor thread %r already started." % threadname) + start.priority = 70 + + def stop(self): + """Stop our callback's background task thread.""" + if self.thread is None: + self.bus.log("No thread running for %s." % self.name or self.__class__.__name__) + else: + if self.thread is not threading.currentThread(): + name = self.thread.getName() + self.thread.cancel() + if not get_daemon(self.thread): + self.bus.log("Joining %r" % name) + self.thread.join() + self.bus.log("Stopped thread %r." % name) + self.thread = None + + def graceful(self): + """Stop the callback's background task thread and restart it.""" + self.stop() + self.start() + + +class Autoreloader(Monitor): + """Monitor which re-executes the process when files change. + + This :ref:`plugin` restarts the process (via :func:`os.execv`) + if any of the files it monitors change (or is deleted). By default, the + autoreloader monitors all imported modules; you can add to the + set by adding to ``autoreload.files``:: + + cherrypy.engine.autoreload.files.add(myFile) + + If there are imported files you do *not* wish to monitor, you can adjust the + ``match`` attribute, a regular expression. For example, to stop monitoring + cherrypy itself:: + + cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' + + Like all :class:`Monitor` plugins, + the autoreload plugin takes a ``frequency`` argument. The default is + 1 second; that is, the autoreloader will examine files once each second. + """ + + files = None + """The set of files to poll for modifications.""" + + frequency = 1 + """The interval in seconds at which to poll for modified files.""" + + match = '.*' + """A regular expression by which to match filenames.""" + + def __init__(self, bus, frequency=1, match='.*'): + self.mtimes = {} + self.files = set() + self.match = match + Monitor.__init__(self, bus, self.run, frequency) + + def start(self): + """Start our own background task thread for self.run.""" + if self.thread is None: + self.mtimes = {} + Monitor.start(self) + start.priority = 70 + + def sysfiles(self): + """Return a Set of sys.modules filenames to monitor.""" + files = set() + for k, m in sys.modules.items(): + if re.match(self.match, k): + if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'): + f = m.__loader__.archive + else: + f = getattr(m, '__file__', None) + if f is not None and not os.path.isabs(f): + # ensure absolute paths so a os.chdir() in the app doesn't break me + f = os.path.normpath(os.path.join(_module__file__base, f)) + files.add(f) + return files + + def run(self): + """Reload the process if registered files have been modified.""" + for filename in self.sysfiles() | self.files: + if filename: + if filename.endswith('.pyc'): + filename = filename[:-1] + + oldtime = self.mtimes.get(filename, 0) + if oldtime is None: + # Module with no .py file. Skip it. + continue + + try: + mtime = os.stat(filename).st_mtime + except OSError: + # Either a module with no .py file, or it's been deleted. + mtime = None + + if filename not in self.mtimes: + # If a module has no .py file, this will be None. + self.mtimes[filename] = mtime + else: + if mtime is None or mtime > oldtime: + # The file has been deleted or modified. + self.bus.log("Restarting because %s changed." % filename) + self.thread.cancel() + self.bus.log("Stopped thread %r." % self.thread.getName()) + self.bus.restart() + return + + +class ThreadManager(SimplePlugin): + """Manager for HTTP request threads. + + If you have control over thread creation and destruction, publish to + the 'acquire_thread' and 'release_thread' channels (for each thread). + This will register/unregister the current thread and publish to + 'start_thread' and 'stop_thread' listeners in the bus as needed. + + If threads are created and destroyed by code you do not control + (e.g., Apache), then, at the beginning of every HTTP request, + publish to 'acquire_thread' only. You should not publish to + 'release_thread' in this case, since you do not know whether + the thread will be re-used or not. The bus will call + 'stop_thread' listeners for you when it stops. + """ + + threads = None + """A map of {thread ident: index number} pairs.""" + + def __init__(self, bus): + self.threads = {} + SimplePlugin.__init__(self, bus) + self.bus.listeners.setdefault('acquire_thread', set()) + self.bus.listeners.setdefault('start_thread', set()) + self.bus.listeners.setdefault('release_thread', set()) + self.bus.listeners.setdefault('stop_thread', set()) + + def acquire_thread(self): + """Run 'start_thread' listeners for the current thread. + + If the current thread has already been seen, any 'start_thread' + listeners will not be run again. + """ + thread_ident = get_thread_ident() + if thread_ident not in self.threads: + # We can't just use get_ident as the thread ID + # because some platforms reuse thread ID's. + i = len(self.threads) + 1 + self.threads[thread_ident] = i + self.bus.publish('start_thread', i) + + def release_thread(self): + """Release the current thread and run 'stop_thread' listeners.""" + thread_ident = get_thread_ident() + i = self.threads.pop(thread_ident, None) + if i is not None: + self.bus.publish('stop_thread', i) + + def stop(self): + """Release all threads and run all 'stop_thread' listeners.""" + for thread_ident, i in self.threads.items(): + self.bus.publish('stop_thread', i) + self.threads.clear() + graceful = stop + diff --git a/cherrypy/process/plugins.pyc b/cherrypy/process/plugins.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cf4239db7dcf9aada4c1fff1d04cf68abf78179 GIT binary patch literal 24183 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. It's an instance of +:class:`cherrypy._cpserver.Server`, which is a subclass of +:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class +is designed to control other servers, as well. + +Multiple servers/ports +====================== + +If you need to start more than one HTTP server (to serve on multiple ports, or +protocols, etc.), you can manually register each one and then start them all +with engine.start:: + + s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + cherrypy.engine.start() + +.. index:: SCGI + +FastCGI/SCGI +============ + +There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in +:mod:`cherrypy.process.servers`. To start an fcgi server, for example, +wrap an instance of it in a ServerAdapter:: + + addr = ('0.0.0.0', 4000) + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) + s.subscribe() + +The :doc:`cherryd` startup script will do the above for +you via its `-f` flag. +Note that you need to download and install `flup `_ +yourself, whether you use ``cherryd`` or not. + +.. _fastcgi: +.. index:: FastCGI + +FastCGI +------- + +A very simple setup lets your cherry run with FastCGI. +You just need the flup library, +plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. + +CherryPy code +^^^^^^^^^^^^^ + +hello.py:: + + #!/usr/bin/python + import cherrypy + + class HelloWorld: + \"""Sample request handler class.\""" + def index(self): + return "Hello world!" + index.exposed = True + + cherrypy.tree.mount(HelloWorld()) + # CherryPy autoreload must be disabled for the flup server to work + cherrypy.config.update({'engine.autoreload_on':False}) + +Then run :doc:`/deployguide/cherryd` with the '-f' arg:: + + cherryd -c -d -f -i hello.py + +Apache +^^^^^^ + +At the top level in httpd.conf:: + + FastCgiIpcDir /tmp + FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 + +And inside the relevant VirtualHost section:: + + # FastCGI config + AddHandler fastcgi-script .fcgi + ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 + +Lighttpd +^^^^^^^^ + +For `Lighttpd `_ you can follow these +instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is +active within ``server.modules``. Then, within your ``$HTTP["host"]`` +directive, configure your fastcgi script like the following:: + + $HTTP["url"] =~ "" { + fastcgi.server = ( + "/" => ( + "script.fcgi" => ( + "bin-path" => "/path/to/your/script.fcgi", + "socket" => "/tmp/script.sock", + "check-local" => "disable", + "disable-time" => 1, + "min-procs" => 1, + "max-procs" => 1, # adjust as needed + ), + ), + ) + } # end of $HTTP["url"] =~ "^/" + +Please see `Lighttpd FastCGI Docs +`_ for an explanation +of the possible configuration options. +""" + +import sys +import time + + +class ServerAdapter(object): + """Adapter for an HTTP server. + + If you need to start more than one HTTP server (to serve on multiple + ports, or protocols, etc.), you can manually register each one and then + start them all with bus.start: + + s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + bus.start() + """ + + def __init__(self, bus, httpserver=None, bind_addr=None): + self.bus = bus + self.httpserver = httpserver + self.bind_addr = bind_addr + self.interrupt = None + self.running = False + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + + def unsubscribe(self): + self.bus.unsubscribe('start', self.start) + self.bus.unsubscribe('stop', self.stop) + + def start(self): + """Start the HTTP server.""" + if self.bind_addr is None: + on_what = "unknown interface (dynamic?)" + elif isinstance(self.bind_addr, tuple): + host, port = self.bind_addr + on_what = "%s:%s" % (host, port) + else: + on_what = "socket file: %s" % self.bind_addr + + if self.running: + self.bus.log("Already serving on %s" % on_what) + return + + self.interrupt = None + if not self.httpserver: + raise ValueError("No HTTP server has been created.") + + # Start the httpserver in a new thread. + if isinstance(self.bind_addr, tuple): + wait_for_free_port(*self.bind_addr) + + import threading + t = threading.Thread(target=self._start_http_thread) + t.setName("HTTPServer " + t.getName()) + t.start() + + self.wait() + self.running = True + self.bus.log("Serving on %s" % on_what) + start.priority = 75 + + def _start_http_thread(self): + """HTTP servers MUST be running in new threads, so that the + main thread persists to receive KeyboardInterrupt's. If an + exception is raised in the httpserver's thread then it's + trapped here, and the bus (and therefore our httpserver) + are shut down. + """ + try: + self.httpserver.start() + except KeyboardInterrupt: + self.bus.log(" hit: shutting down HTTP server") + self.interrupt = sys.exc_info()[1] + self.bus.exit() + except SystemExit: + self.bus.log("SystemExit raised: shutting down HTTP server") + self.interrupt = sys.exc_info()[1] + self.bus.exit() + raise + except: + self.interrupt = sys.exc_info()[1] + self.bus.log("Error in HTTP server: shutting down", + traceback=True, level=40) + self.bus.exit() + raise + + def wait(self): + """Wait until the HTTP server is ready to receive requests.""" + while not getattr(self.httpserver, "ready", False): + if self.interrupt: + raise self.interrupt + time.sleep(.1) + + # Wait for port to be occupied + if isinstance(self.bind_addr, tuple): + host, port = self.bind_addr + wait_for_occupied_port(host, port) + + def stop(self): + """Stop the HTTP server.""" + if self.running: + # stop() MUST block until the server is *truly* stopped. + self.httpserver.stop() + # Wait for the socket to be truly freed. + if isinstance(self.bind_addr, tuple): + wait_for_free_port(*self.bind_addr) + self.running = False + self.bus.log("HTTP Server %s shut down" % self.httpserver) + else: + self.bus.log("HTTP Server %s already shut down" % self.httpserver) + stop.priority = 25 + + def restart(self): + """Restart the HTTP server.""" + self.stop() + self.start() + + +class FlupCGIServer(object): + """Adapter for a flup.server.cgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the CGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.cgi import WSGIServer + + self.cgiserver = WSGIServer(*self.args, **self.kwargs) + self.ready = True + self.cgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + + +class FlupFCGIServer(object): + """Adapter for a flup.server.fcgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + if kwargs.get('bindAddress', None) is None: + import socket + if not hasattr(socket, 'fromfd'): + raise ValueError( + 'Dynamic FCGI server not available on this platform. ' + 'You must use a static or external one by providing a ' + 'legal bindAddress.') + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the FCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.fcgi import WSGIServer + self.fcgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.fcgiserver._installSignalHandlers = lambda: None + self.fcgiserver._oldSIGs = [] + self.ready = True + self.fcgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + # Forcibly stop the fcgi server main event loop. + self.fcgiserver._keepGoing = False + # Force all worker threads to die off. + self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount + self.ready = False + + +class FlupSCGIServer(object): + """Adapter for a flup.server.scgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the SCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.scgi import WSGIServer + self.scgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.scgiserver._installSignalHandlers = lambda: None + self.scgiserver._oldSIGs = [] + self.ready = True + self.scgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + # Forcibly stop the scgi server main event loop. + self.scgiserver._keepGoing = False + # Force all worker threads to die off. + self.scgiserver._threadPool.maxSpare = 0 + + +def client_host(server_host): + """Return the host on which a client can connect to the given listener.""" + if server_host == '0.0.0.0': + # 0.0.0.0 is INADDR_ANY, which should answer on localhost. + return '127.0.0.1' + if server_host in ('::', '::0', '::0.0.0.0'): + # :: is IN6ADDR_ANY, which should answer on localhost. + # ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY. + return '::1' + return server_host + +def check_port(host, port, timeout=1.0): + """Raise an error if the given port is not free on the given host.""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + host = client_host(host) + port = int(port) + + import socket + + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM) + except socket.gaierror: + if ':' in host: + info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))] + + for res in info: + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(timeout) + s.connect((host, port)) + s.close() + raise IOError("Port %s is in use on %s; perhaps the previous " + "httpserver did not shut down properly." % + (repr(port), repr(host))) + except socket.error: + if s: + s.close() + + +# Feel free to increase these defaults on slow systems: +free_port_timeout = 0.1 +occupied_port_timeout = 1.0 + +def wait_for_free_port(host, port, timeout=None): + """Wait for the specified port to become free (drop requests).""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + if timeout is None: + timeout = free_port_timeout + + for trial in range(50): + try: + # we are expecting a free port, so reduce the timeout + check_port(host, port, timeout=timeout) + except IOError: + # Give the old server thread time to free the port. + time.sleep(timeout) + else: + return + + raise IOError("Port %r not free on %r" % (port, host)) + +def wait_for_occupied_port(host, port, timeout=None): + """Wait for the specified port to become active (receive requests).""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + if timeout is None: + timeout = occupied_port_timeout + + for trial in range(50): + try: + check_port(host, port, timeout=timeout) + except IOError: + return + else: + time.sleep(timeout) + + raise IOError("Port %r not bound on %r" % (port, host)) diff --git a/cherrypy/process/servers.pyc b/cherrypy/process/servers.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01ac44428013a21d46ca9e820d377906cee99655 GIT binary patch literal 13911 zcmd5@TW=gkcCMZwDX~SGl59)%V%I6t_Hv|{X8?-1}rmnF?3q9JES1a#KbGnCY zv8Q{~)kBJFD-SDYfklGNLjo*vd(LZsyd((n0|EreTaecvK%N35K=PdA`%d-r3@^52 zN(M4=H>>MjbMq`n3Mwjg)LkTOx}xq@ zR8Uo6RdtXvrao8du~I+2JEns1LGDDEJFbEwgWRKK?t}^^2f1#Udqf4t_;L5B3Z_&r ztv<)VlPY*l-F2CB_ZVjr%t-#Uq>oGbIi?%66P*0mv{G&(_p>}o)^(KVm5ngVc5dzH z@72%G>$ZILcIqa~w!%zDMn~OV9CpJb4+HHR?I-S17Iv0eSCaK83H7bGkG~gs^2c0k z)%A^hj@8h)$^E1q>a^o7wPW9yrB>O0v)!{p)EhEVYhG_}MC}cZ=xftolLiMi?PX~@ zG^UuMDLS|m_`N*LF#he0P`6sP+d*ziOMfnU4?+{ICs<6L>UNssSsLp!$GWwhkeTN) zx5GHDyY9_?oJUyyplq?1X1Q^f5B$0AjgH<)`#K42_gH_H>u#EbI^V#Kq}Z?Pw{PDn zdeb#bK?X&M?hb~MA?9_OxmanQw$m6>3iEdT%)Ip8Mo(Qo>HBfKqqA^5GFZ0`eQX{F z@DnU;BTTd%9m%dnRd+keH*_dOt4q<+lIv>x%z3@6cWqa#B$KT_uW#<$X!8M`_?|K?lL?Rj+DXneSQJL`voe$Usz z7N(s=Yhi6L)V{1eGq)>6b+=vzLk8i)r6t{1xpu>KulgoOVo|FK)QAdt-G6Pr|D zjr+ZO`s~@OXV2c#Xkuq98$Bp$>@E^!3n4oYmTnr1@ckaQ;>4}uJYsEyF+`{&M`vm4hzi4nOobuQs*xvHAq z>%~#q&!aS1E;lpJ!f;-%VFNFrTL|AWyDx|^(wE(tLg@B$(MFyZQor1>-8I?IXlJ_! zN{FM{+-qg-0>XUVAU zg8jMfB@Jla(MFINbj&fQIK-kjaslBw>nGR-hmjuHVn09)!`lq|I8WgUI-6F*+vzkb{Lx zR$QsHFr!Vo7lXl|#yZh@z26J`Jgj{>SD^f`?`E3JEnNkeg^vJ*kg7!Z!;02HBGt0I z!_ns!I&;9n^#Q>O7S;=GeWAN!hrXy60=>}D3y~hs7i=mFRL~!vxhy76$*fypOzaZI zf$$`qLGQw?>(Px~`*M`&#k|{-r54!*@-FuLd}A?B3jnAKY%W9~_Cg+Y!?d64^KYKh z3kBE$h`*2VtsDb4WFFY`aLZ3}{i7($p-Znrsey&t1iry!UKup$o}5CMYeFaR7TzNd;05g?l6tbU>*n6@gE*gN57{1Kr{mkU)ru=FtWZg!*R-_ z)%=gpFoaa49lei2KV6)4-CJ?!L+zOm>{arzi1M`?`ZA{K4peCt2AF@io4L26&1iAJ zv5Ty~bTbW#_T+p5B&;TYaqK5zvS=5RArAqbO#~DzdCnH6J<7MKyV3vRx0=HnQoD?n zHZtT_#ru6cCc!I&*9C75Z#YZ z96tJLN2CtiHF5UnvBA+hCUwNoyE~=4Ms14Y{*9wlVMz@ZI^sLjg+lUr7EXtBR5~8| zsT>dKYq#@ZwaHo^P8&^Kt8b+q<>dee1XaJ5(;?mLcRZw=y|+8bJS-_HIb-D0-se$fUl5=qA`_^O9G4p-mHNO)N1Vr-Z@zk z!{jdh4o&HNlG!jz#FoxEo>3STg zhR43l>qxCE-Mi*@!=K%=`oFhxuh6ddtXeRz>GGqf*lb2glsB87BW*ZU zUb34WXL*R#c@)dv#bdZ_$~omc%k_**lHJ>7aL)bYnJ_xpHuFJuBaC1K8jOqK*d zvCX3@dx?J-}`8YOHFI`f+ykE!fu zk{DO_1v4QUrye`X!FUZksM97XM_+|C!H zjm)=3LYY%|HN=&t1{wux^KBIMlg$K9CG1`ZWyeQgtQPDfem82rd&W>c-!MyW7&F7J z#GuffC=Qo2@?J#Vr8o=y0B#@rcKT-s8NFdvjYiRGIyxHs9s@rDHhvo|5Vos}vdc+d z%x)c?BBwiyy=XH~gJr+9=TSpoz#}&bm3b zkNe@3EQ2?95(V2npaE*T*@1u5q{0zV%{RC(WLyvhf`qD&zL$s6gDOX*gXEWJKPl~hy}Lc2^$FVl zT&d(L8-DRXm<#?JC%ZscA%Tyb%3z*$670lLA43!yHR>(%9c=lCq!@kkhmG6xf(Ky; zcpgbeuZA1~qX6+Z#8SnVEDeaRk6N~d?uD5_MAXnzhT{>^gZUubSxfyaxG_+NbMT2^ zHX)X6b-IQP*$%}vWB@wzBRcoeX(&#r)p3Yx7o$MHA;<`HmmMG&)a#)Ud~$d?3>pyx zpssbT_(0U0kzmGXhT7X&(wqgujb;O`D%Ogq9!451&=X!J$y&yRyT^p0+D=NRc2YFwW_j( zB2nlmXdbu*r~?UmP*ryTa8x2k6c$e1nHHbaozSlpDmog8)(D~=t3p&b0rNIqcYtjg zL8T}b_8K9U@U19_5ymefVWuwB68*9M$A84%U%$Ir6FL#V0inRe%u-JPlOQgHU_3<{ zn>Y-69+i_-3&&Z58(Kno+HUuIQ5XoUi;*U{>rwZ4-@)rJ!1^-U{XHJ@9A2vO5{yj zsQKjdNQt#R1_nS$A*$&H16=#`|4S$lVp(SsntLegqQdtS`U7P@00c`dt8T?arwZRg z4o{S}(2qdiQO=%DU^vJ><0*E366y>p3^e#wB_d@?)y45dLby;%g2#mkWz`opvr*&3 z4QqG^&+=$&?>??Md9PHf+PJKN2U}h=-t&C?VZ9|fdLjan}RjXrVVZ3 zlTy)a;^d*(6a&bkvJwZtV_R!%5W0Fq7l1T9B?E;W58~*M1k*8tYArOYe=-TOE3y zFi4G9eVf}%dcwZnMV%1@q37(q$viyv_2ipL%KZl(!=9}#Dii0VIr$YlhWiB;1yILI zb;Z$f>_XsmS)}?|aDe%Jqv11{JJ1A{x=-UWqn^#bm{Yok_#PY!{Wnf8#OiMU5R7$SviZn-|*nHjU4$sjrg5K4__>PJm%OZe-zs)9+5u?7)=K#Kr%CK zo=8}U?)w$;HgaNd(5Vg~jxhWy%3PtQhvEKMoa+Zy=wBB?3@=&Ozd4}_{fJ;Dg_!oi zYECkY910KJ%ubeeJAwHW<(I9CtGQE!An@Q*`&)h#^PG#0EDjiqMi4iLh1{)co>aN`c$~VLb^yaH*B~$ zl6qpO7L9x%_Pc9=fALlB6gq>hpb+QWv{7)RFziJ~R%E_~N3l-fSAc!uxPd!AIOMYj zch>7@rg6}?am@^2)cXNCct#j{lJlQJf51@ZC3gnYE~w0XDshz1Ub+*_fn5o81L~1t zuwbhSB(-%w&!HjpC0I-I#d7hx?4SG6+=TspEoF>_Vp~L0Zl!5#i+Osw!t-jOLf3!T z=)ofu?ebJsn4s@>xf!B=t5Z-Y-y~kYg~e4l9Hmz9dTR2860dpL$C}`~*qQ48Fy_)( zwEJSbHV5H#6-`L}L)c3r7&O$Q>~9Z34dw5NMNhQlnNdNaur~i1~%3_8wY$m-u27L!|0ig3Vi;;a7tV8KVFaXMj2tMZ?^> zy09FeGbCPNPgx}Je4s}gp)2f%@L|b}-gPdH!sM;+bp@}d2agX*wyaE6roK^Q?GDD6ngPqO(A18M`*=;x@OHib#Qm zW6o4{%z>H0-+#W_=ETXq+;-XFU!gX35c~&z#Xeix6ox56^vK8OBc!1&r3jHTTDMgd zS9$!jGfV5%Do`4-NW+lD8{zRWo(J7Y9;h*9fM5ux{{y4iFo4l9>G$VS7l%dC0^w|s z6GiBW>2cK=Qyth?n8KKP@Q+drF}r=0arRYKW6b;EU-2C3@+W_x?BUbBarJ0IJvyQu z9aWDe)g#2rG5cd`CzQ@$p<`GCr4T2S{LhZ5`^O|O$WtnOOck@jK-dq-;@K90T{zZ4 zfN`eS9VTSUcp7In^3H>QvKw$*HsE)#ezwnkG1AL}UpT1sF`he^Si#FQCp<>S<=zF4 zb#!DeTv%q6TYIqOY2ovT#~SwoRwgUe<%r}ug&l9hHa=SU zpxL?4t|3F-i;={Tnlnb0`6_?kfyubp9y|!aqHk8~O zAK9}sLQxj>GAWi30pmHUG65fG%9*N6ja6}Cb{zkvoP|oQGL2I*#^PVB zP9q-g!td57n{dkTwO|IGWl83iQT)+k-;_@f& z3vQgICtQd+tZf z(vXKFfX{nNJ8aqM4`M;KHc@W^E4d8b-A?MzorRE}2mlm8D<^=pv-<|z5!{W9{OEXlu7 zD2m3%h*<98fv=?fdzjsm(Z0BKSsNT)J`)Bwjp0WbU=5xgF7Dw9!9EV4Te<5fXTSv> zV#(@B8NlnH@?mIy7VZA@K-%NfeA*cY^=k~2+92O7QyfM@$m!aBm`@%)mB+Z9M`zHZ zQRflN?c4*VXnF|r+A|XlI-IPep4fE~0KH&O4lef4k(_W$;GBP|dTQ&`=~MHk-k3Z# LHAi1`>iPczH(t)| literal 0 HcmV?d00001 diff --git a/cherrypy/process/win32.py b/cherrypy/process/win32.py new file mode 100644 index 0000000..83f99a5 --- /dev/null +++ b/cherrypy/process/win32.py @@ -0,0 +1,174 @@ +"""Windows service. Requires pywin32.""" + +import os +import win32api +import win32con +import win32event +import win32service +import win32serviceutil + +from cherrypy.process import wspbus, plugins + + +class ConsoleCtrlHandler(plugins.SimplePlugin): + """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" + + def __init__(self, bus): + self.is_set = False + plugins.SimplePlugin.__init__(self, bus) + + def start(self): + if self.is_set: + self.bus.log('Handler for console events already set.', level=40) + return + + result = win32api.SetConsoleCtrlHandler(self.handle, 1) + if result == 0: + self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Set handler for console events.', level=40) + self.is_set = True + + def stop(self): + if not self.is_set: + self.bus.log('Handler for console events already off.', level=40) + return + + try: + result = win32api.SetConsoleCtrlHandler(self.handle, 0) + except ValueError: + # "ValueError: The object has not been registered" + result = 1 + + if result == 0: + self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Removed handler for console events.', level=40) + self.is_set = False + + def handle(self, event): + """Handle console control events (like Ctrl-C).""" + if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT): + self.bus.log('Console event %s: shutting down bus' % event) + + # Remove self immediately so repeated Ctrl-C doesn't re-call it. + try: + self.stop() + except ValueError: + pass + + self.bus.exit() + # 'First to return True stops the calls' + return 1 + return 0 + + +class Win32Bus(wspbus.Bus): + """A Web Site Process Bus implementation for Win32. + + Instead of time.sleep, this bus blocks using native win32event objects. + """ + + def __init__(self): + self.events = {} + wspbus.Bus.__init__(self) + + def _get_state_event(self, state): + """Return a win32event for the given state (creating it if needed).""" + try: + return self.events[state] + except KeyError: + event = win32event.CreateEvent(None, 0, 0, + "WSPBus %s Event (pid=%r)" % + (state.name, os.getpid())) + self.events[state] = event + return event + + def _get_state(self): + return self._state + def _set_state(self, value): + self._state = value + event = self._get_state_event(value) + win32event.PulseEvent(event) + state = property(_get_state, _set_state) + + def wait(self, state, interval=0.1, channel=None): + """Wait for the given state(s), KeyboardInterrupt or SystemExit. + + Since this class uses native win32event objects, the interval + argument is ignored. + """ + if isinstance(state, (tuple, list)): + # Don't wait for an event that beat us to the punch ;) + if self.state not in state: + events = tuple([self._get_state_event(s) for s in state]) + win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE) + else: + # Don't wait for an event that beat us to the punch ;) + if self.state != state: + event = self._get_state_event(state) + win32event.WaitForSingleObject(event, win32event.INFINITE) + + +class _ControlCodes(dict): + """Control codes used to "signal" a service via ControlService. + + User-defined control codes are in the range 128-255. We generally use + the standard Python value for the Linux signal and add 128. Example: + + >>> signal.SIGUSR1 + 10 + control_codes['graceful'] = 128 + 10 + """ + + def key_for(self, obj): + """For the given value, return its corresponding key.""" + for key, val in self.items(): + if val is obj: + return key + raise ValueError("The given object could not be found: %r" % obj) + +control_codes = _ControlCodes({'graceful': 138}) + + +def signal_child(service, command): + if command == 'stop': + win32serviceutil.StopService(service) + elif command == 'restart': + win32serviceutil.RestartService(service) + else: + win32serviceutil.ControlService(service, control_codes[command]) + + +class PyWebService(win32serviceutil.ServiceFramework): + """Python Web Service.""" + + _svc_name_ = "Python Web Service" + _svc_display_name_ = "Python Web Service" + _svc_deps_ = None # sequence of service names on which this depends + _exe_name_ = "pywebsvc" + _exe_args_ = None # Default to no arguments + + # Only exists on Windows 2000 or later, ignored on windows NT + _svc_description_ = "Python Web Service" + + def SvcDoRun(self): + from cherrypy import process + process.bus.start() + process.bus.block() + + def SvcStop(self): + from cherrypy import process + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + process.bus.exit() + + def SvcOther(self, control): + process.bus.publish(control_codes.key_for(control)) + + +if __name__ == '__main__': + win32serviceutil.HandleCommandLine(PyWebService) diff --git a/cherrypy/process/win32.pyc b/cherrypy/process/win32.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1fcb0b5949e5af087511b64a61f60e4611883a7 GIT binary patch literal 6980 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 CkjaJs literal 0 HcmV?d00001 diff --git a/cherrypy/process/wspbus.py b/cherrypy/process/wspbus.py new file mode 100644 index 0000000..6ef768d --- /dev/null +++ b/cherrypy/process/wspbus.py @@ -0,0 +1,432 @@ +"""An implementation of the Web Site Process Bus. + +This module is completely standalone, depending only on the stdlib. + +Web Site Process Bus +-------------------- + +A Bus object is used to contain and manage site-wide behavior: +daemonization, HTTP server start/stop, process reload, signal handling, +drop privileges, PID file management, logging for all of these, +and many more. + +In addition, a Bus object provides a place for each web framework +to register code that runs in response to site-wide events (like +process start and stop), or which controls or otherwise interacts with +the site-wide components mentioned above. For example, a framework which +uses file-based templates would add known template filenames to an +autoreload component. + +Ideally, a Bus object will be flexible enough to be useful in a variety +of invocation scenarios: + + 1. The deployer starts a site from the command line via a + framework-neutral deployment script; applications from multiple frameworks + are mixed in a single site. Command-line arguments and configuration + files are used to define site-wide components such as the HTTP server, + WSGI component graph, autoreload behavior, signal handling, etc. + 2. The deployer starts a site via some other process, such as Apache; + applications from multiple frameworks are mixed in a single site. + Autoreload and signal handling (from Python at least) are disabled. + 3. The deployer starts a site via a framework-specific mechanism; + for example, when running tests, exploring tutorials, or deploying + single applications from a single framework. The framework controls + which site-wide components are enabled as it sees fit. + +The Bus object in this package uses topic-based publish-subscribe +messaging to accomplish all this. A few topic channels are built in +('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and +site containers are free to define their own. If a message is sent to a +channel that has not been defined or has no listeners, there is no effect. + +In general, there should only ever be a single Bus object per process. +Frameworks and site containers share a single Bus object by publishing +messages and subscribing listeners. + +The Bus object works as a finite state machine which models the current +state of the process. Bus methods move it from one state to another; +those methods then publish to subscribed listeners on the channel for +the new state.:: + + O + | + V + STOPPING --> STOPPED --> EXITING -> X + A A | + | \___ | + | \ | + | V V + STARTED <-- STARTING + +""" + +import atexit +import os +import sys +import threading +import time +import traceback as _traceback +import warnings + +from cherrypy._cpcompat import set + +# Here I save the value of os.getcwd(), which, if I am imported early enough, +# will be the directory from which the startup script was run. This is needed +# by _do_execv(), to change back to the original directory before execv()ing a +# new process. This is a defense against the application having changed the +# current working directory (which could make sys.executable "not found" if +# sys.executable is a relative-path, and/or cause other problems). +_startup_cwd = os.getcwd() + +class ChannelFailures(Exception): + """Exception raised when errors occur in a listener during Bus.publish().""" + delimiter = '\n' + + def __init__(self, *args, **kwargs): + # Don't use 'super' here; Exceptions are old-style in Py2.4 + # See http://www.cherrypy.org/ticket/959 + Exception.__init__(self, *args, **kwargs) + self._exceptions = list() + + def handle_exception(self): + """Append the current exception to self.""" + self._exceptions.append(sys.exc_info()[1]) + + def get_instances(self): + """Return a list of seen exception instances.""" + return self._exceptions[:] + + def __str__(self): + exception_strings = map(repr, self.get_instances()) + return self.delimiter.join(exception_strings) + + __repr__ = __str__ + + def __bool__(self): + return bool(self._exceptions) + __nonzero__ = __bool__ + +# Use a flag to indicate the state of the bus. +class _StateEnum(object): + class State(object): + name = None + def __repr__(self): + return "states.%s" % self.name + + def __setattr__(self, key, value): + if isinstance(value, self.State): + value.name = key + object.__setattr__(self, key, value) +states = _StateEnum() +states.STOPPED = states.State() +states.STARTING = states.State() +states.STARTED = states.State() +states.STOPPING = states.State() +states.EXITING = states.State() + + +try: + import fcntl +except ImportError: + max_files = 0 +else: + try: + max_files = os.sysconf('SC_OPEN_MAX') + except AttributeError: + max_files = 1024 + + +class Bus(object): + """Process state-machine and messenger for HTTP site deployment. + + All listeners for a given channel are guaranteed to be called even + if others at the same channel fail. Each failure is logged, but + execution proceeds on to the next listener. The only way to stop all + processing from inside a listener is to raise SystemExit and stop the + whole server. + """ + + states = states + state = states.STOPPED + execv = False + max_cloexec_files = max_files + + def __init__(self): + self.execv = False + self.state = states.STOPPED + self.listeners = dict( + [(channel, set()) for channel + in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) + self._priorities = {} + + def subscribe(self, channel, callback, priority=None): + """Add the given callback at the given channel (if not present).""" + if channel not in self.listeners: + self.listeners[channel] = set() + self.listeners[channel].add(callback) + + if priority is None: + priority = getattr(callback, 'priority', 50) + self._priorities[(channel, callback)] = priority + + def unsubscribe(self, channel, callback): + """Discard the given callback (if present).""" + listeners = self.listeners.get(channel) + if listeners and callback in listeners: + listeners.discard(callback) + del self._priorities[(channel, callback)] + + def publish(self, channel, *args, **kwargs): + """Return output of all subscribers for the given channel.""" + if channel not in self.listeners: + return [] + + exc = ChannelFailures() + output = [] + + items = [(self._priorities[(channel, listener)], listener) + for listener in self.listeners[channel]] + try: + items.sort(key=lambda item: item[0]) + except TypeError: + # Python 2.3 had no 'key' arg, but that doesn't matter + # since it could sort dissimilar types just fine. + items.sort() + for priority, listener in items: + try: + output.append(listener(*args, **kwargs)) + except KeyboardInterrupt: + raise + except SystemExit: + e = sys.exc_info()[1] + # If we have previous errors ensure the exit code is non-zero + if exc and e.code == 0: + e.code = 1 + raise + except: + exc.handle_exception() + if channel == 'log': + # Assume any further messages to 'log' will fail. + pass + else: + self.log("Error in %r listener %r" % (channel, listener), + level=40, traceback=True) + if exc: + raise exc + return output + + def _clean_exit(self): + """An atexit handler which asserts the Bus is not running.""" + if self.state != states.EXITING: + warnings.warn( + "The main thread is exiting, but the Bus is in the %r state; " + "shutting it down automatically now. You must either call " + "bus.block() after start(), or call bus.exit() before the " + "main thread exits." % self.state, RuntimeWarning) + self.exit() + + def start(self): + """Start all services.""" + atexit.register(self._clean_exit) + + self.state = states.STARTING + self.log('Bus STARTING') + try: + self.publish('start') + self.state = states.STARTED + self.log('Bus STARTED') + except (KeyboardInterrupt, SystemExit): + raise + except: + self.log("Shutting down due to error in start listener:", + level=40, traceback=True) + e_info = sys.exc_info()[1] + try: + self.exit() + except: + # Any stop/exit errors will be logged inside publish(). + pass + # Re-raise the original error + raise e_info + + def exit(self): + """Stop all services and prepare to exit the process.""" + exitstate = self.state + try: + self.stop() + + self.state = states.EXITING + self.log('Bus EXITING') + self.publish('exit') + # This isn't strictly necessary, but it's better than seeing + # "Waiting for child threads to terminate..." and then nothing. + self.log('Bus EXITED') + except: + # This method is often called asynchronously (whether thread, + # signal handler, console handler, or atexit handler), so we + # can't just let exceptions propagate out unhandled. + # Assume it's been logged and just die. + os._exit(70) # EX_SOFTWARE + + if exitstate == states.STARTING: + # exit() was called before start() finished, possibly due to + # Ctrl-C because a start listener got stuck. In this case, + # we could get stuck in a loop where Ctrl-C never exits the + # process, so we just call os.exit here. + os._exit(70) # EX_SOFTWARE + + def restart(self): + """Restart the process (may close connections). + + This method does not restart the process from the calling thread; + instead, it stops the bus and asks the main thread to call execv. + """ + self.execv = True + self.exit() + + def graceful(self): + """Advise all services to reload.""" + self.log('Bus graceful') + self.publish('graceful') + + def block(self, interval=0.1): + """Wait for the EXITING state, KeyboardInterrupt or SystemExit. + + This function is intended to be called only by the main thread. + After waiting for the EXITING state, it also waits for all threads + to terminate, and then calls os.execv if self.execv is True. This + design allows another thread to call bus.restart, yet have the main + thread perform the actual execv call (required on some platforms). + """ + try: + self.wait(states.EXITING, interval=interval, channel='main') + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.log('Keyboard Interrupt: shutting down bus') + self.exit() + except SystemExit: + self.log('SystemExit raised: shutting down bus') + self.exit() + raise + + # Waiting for ALL child threads to finish is necessary on OS X. + # See http://www.cherrypy.org/ticket/581. + # It's also good to let them all shut down before allowing + # the main thread to call atexit handlers. + # See http://www.cherrypy.org/ticket/751. + self.log("Waiting for child threads to terminate...") + for t in threading.enumerate(): + if t != threading.currentThread() and t.isAlive(): + # Note that any dummy (external) threads are always daemonic. + if hasattr(threading.Thread, "daemon"): + # Python 2.6+ + d = t.daemon + else: + d = t.isDaemon() + if not d: + self.log("Waiting for thread %s." % t.getName()) + t.join() + + if self.execv: + self._do_execv() + + def wait(self, state, interval=0.1, channel=None): + """Poll for the given state(s) at intervals; publish to channel.""" + if isinstance(state, (tuple, list)): + states = state + else: + states = [state] + + def _wait(): + while self.state not in states: + time.sleep(interval) + self.publish(channel) + + # From http://psyco.sourceforge.net/psycoguide/bugs.html: + # "The compiled machine code does not include the regular polling + # done by Python, meaning that a KeyboardInterrupt will not be + # detected before execution comes back to the regular Python + # interpreter. Your program cannot be interrupted if caught + # into an infinite Psyco-compiled loop." + try: + sys.modules['psyco'].cannotcompile(_wait) + except (KeyError, AttributeError): + pass + + _wait() + + def _do_execv(self): + """Re-execute the current process. + + This must be called from the main thread, because certain platforms + (OS X) don't allow execv to be called in a child thread very well. + """ + args = sys.argv[:] + self.log('Re-spawning %s' % ' '.join(args)) + + if sys.platform[:4] == 'java': + from _systemrestart import SystemRestart + raise SystemRestart + else: + args.insert(0, sys.executable) + if sys.platform == 'win32': + args = ['"%s"' % arg for arg in args] + + os.chdir(_startup_cwd) + if self.max_cloexec_files: + self._set_cloexec() + os.execv(sys.executable, args) + + def _set_cloexec(self): + """Set the CLOEXEC flag on all open files (except stdin/out/err). + + If self.max_cloexec_files is an integer (the default), then on + platforms which support it, it represents the max open files setting + for the operating system. This function will be called just before + the process is restarted via os.execv() to prevent open files + from persisting into the new process. + + Set self.max_cloexec_files to 0 to disable this behavior. + """ + for fd in range(3, self.max_cloexec_files): # skip stdin/out/err + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + except IOError: + continue + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + def stop(self): + """Stop all services.""" + self.state = states.STOPPING + self.log('Bus STOPPING') + self.publish('stop') + self.state = states.STOPPED + self.log('Bus STOPPED') + + def start_with_callback(self, func, args=None, kwargs=None): + """Start 'func' in a new thread T, then start self (and return T).""" + if args is None: + args = () + if kwargs is None: + kwargs = {} + args = (func,) + args + + def _callback(func, *a, **kw): + self.wait(states.STARTED) + func(*a, **kw) + t = threading.Thread(target=_callback, args=args, kwargs=kwargs) + t.setName('Bus Callback ' + t.getName()) + t.start() + + self.start() + + return t + + def log(self, msg="", level=20, traceback=False): + """Log the given message. Append the last traceback if requested.""" + if traceback: + msg += "\n" + "".join(_traceback.format_exception(*sys.exc_info())) + self.publish('log', msg, level) + +bus = Bus() diff --git a/cherrypy/process/wspbus.pyc b/cherrypy/process/wspbus.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d30656bda5c07be84f4249bfaa1303ad10752d2 GIT binary patch literal 15599 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 N0*BxAxu2RZ{U5`RQw9J4 literal 0 HcmV?d00001 diff --git a/cherrypy/scaffold/__init__.py b/cherrypy/scaffold/__init__.py new file mode 100644 index 0000000..00964ac --- /dev/null +++ b/cherrypy/scaffold/__init__.py @@ -0,0 +1,61 @@ +""", a CherryPy application. + +Use this as a base for creating new CherryPy applications. When you want +to make a new app, copy and paste this folder to some other location +(maybe site-packages) and rename it to the name of your project, +then tweak as desired. + +Even before any tweaking, this should serve a few demonstration pages. +Change to this directory and run: + + ../cherryd -c site.conf + +""" + +import cherrypy +from cherrypy import tools, url + +import os +local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class Root: + + _cp_config = {'tools.log_tracebacks.on': True, + } + + def index(self): + return """ +Try some other path, +or a default path.
+Or, just look at the pretty picture:
+ +""" % (url("other"), url("else"), + url("files/made_with_cherrypy_small.png")) + index.exposed = True + + def default(self, *args, **kwargs): + return "args: %s kwargs: %s" % (args, kwargs) + default.exposed = True + + def other(self, a=2, b='bananas', c=None): + cherrypy.response.headers['Content-Type'] = 'text/plain' + if c is None: + return "Have %d %s." % (int(a), b) + else: + return "Have %d %s, %s." % (int(a), b, c) + other.exposed = True + + files = cherrypy.tools.staticdir.handler( + section="/files", + dir=os.path.join(local_dir, "static"), + # Ignore .php files, etc. + match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', + ) + + +root = Root() + +# Uncomment the following to use your own favicon instead of CP's default. +#favicon_path = os.path.join(local_dir, "favicon.ico") +#root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/cherrypy/scaffold/apache-fcgi.conf b/cherrypy/scaffold/apache-fcgi.conf new file mode 100644 index 0000000..922398e --- /dev/null +++ b/cherrypy/scaffold/apache-fcgi.conf @@ -0,0 +1,22 @@ +# Apache2 server conf file for using CherryPy with mod_fcgid. + +# This doesn't have to be "C:/", but it has to be a directory somewhere, and +# MUST match the directory used in the FastCgiExternalServer directive, below. +DocumentRoot "C:/" + +ServerName 127.0.0.1 +Listen 80 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +# Send requests for any URI to our fastcgi handler. +RewriteRule ^(.*)$ /fastcgi.pyc [L] + +# The FastCgiExternalServer directive defines filename as an external FastCGI application. +# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. +# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this +# filename will be handled by this external FastCGI application. +FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 \ No newline at end of file diff --git a/cherrypy/scaffold/example.conf b/cherrypy/scaffold/example.conf new file mode 100644 index 0000000..93a6e53 --- /dev/null +++ b/cherrypy/scaffold/example.conf @@ -0,0 +1,3 @@ +[/] +log.error_file: "error.log" +log.access_file: "access.log" \ No newline at end of file diff --git a/cherrypy/scaffold/site.conf b/cherrypy/scaffold/site.conf new file mode 100644 index 0000000..6ed3898 --- /dev/null +++ b/cherrypy/scaffold/site.conf @@ -0,0 +1,14 @@ +[global] +# Uncomment this when you're done developing +#environment: "production" + +server.socket_host: "0.0.0.0" +server.socket_port: 8088 + +# Uncomment the following lines to run on HTTPS at the same time +#server.2.socket_host: "0.0.0.0" +#server.2.socket_port: 8433 +#server.2.ssl_certificate: '../test/test.pem' +#server.2.ssl_private_key: '../test/test.pem' + +tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/cherrypy/scaffold/static/made_with_cherrypy_small.png b/cherrypy/scaffold/static/made_with_cherrypy_small.png new file mode 100644 index 0000000000000000000000000000000000000000..c3aafeed952190f5da9982bb359aa75b107ff079 GIT binary patch literal 7455 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 + + CherryPy Benchmark + + + + +""" + index.exposed = True + + def hello(self): + return "Hello, world\r\n" + hello.exposed = True + + def sizer(self, size): + resp = size_cache.get(size, None) + if resp is None: + size_cache[size] = resp = "X" * int(size) + return resp + sizer.exposed = True + + +cherrypy.config.update({ + 'log.error.file': '', + 'environment': 'production', + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 54583, + 'server.max_request_header_size': 0, + 'server.max_request_body_size': 0, + 'engine.deadlock_poll_freq': 0, + }) + +# Cheat mode on ;) +del cherrypy.config['tools.log_tracebacks.on'] +del cherrypy.config['tools.log_headers.on'] +del cherrypy.config['tools.trailing_slash.on'] + +appconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + } +app = cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf) + + +class NullRequest: + """A null HTTP request class, returning 200 and an empty body.""" + + def __init__(self, local, remote, scheme="http"): + pass + + def close(self): + pass + + def run(self, method, path, query_string, protocol, headers, rfile): + cherrypy.response.status = "200 OK" + cherrypy.response.header_list = [("Content-Type", 'text/html'), + ("Server", "Null CherryPy"), + ("Date", httputil.HTTPDate()), + ("Content-Length", "0"), + ] + cherrypy.response.body = [""] + return cherrypy.response + + +class NullResponse: + pass + + +class ABSession: + """A session of 'ab', the Apache HTTP server benchmarking tool. + +Example output from ab: + +This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 +Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100 requests +Completed 200 requests +Completed 300 requests +Completed 400 requests +Completed 500 requests +Completed 600 requests +Completed 700 requests +Completed 800 requests +Completed 900 requests + + +Server Software: CherryPy/3.1beta +Server Hostname: 127.0.0.1 +Server Port: 54583 + +Document Path: /static/index.html +Document Length: 14 bytes + +Concurrency Level: 10 +Time taken for tests: 9.643867 seconds +Complete requests: 1000 +Failed requests: 0 +Write errors: 0 +Total transferred: 189000 bytes +HTML transferred: 14000 bytes +Requests per second: 103.69 [#/sec] (mean) +Time per request: 96.439 [ms] (mean) +Time per request: 9.644 [ms] (mean, across all concurrent requests) +Transfer rate: 19.08 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 2.9 0 10 +Processing: 20 94 7.3 90 130 +Waiting: 0 43 28.1 40 100 +Total: 20 95 7.3 100 130 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 100 + 75% 100 + 80% 100 + 90% 100 + 95% 100 + 98% 100 + 99% 110 + 100% 130 (longest request) +Finished 1000 requests +""" + + parse_patterns = [('complete_requests', 'Completed', + ntob(r'^Complete requests:\s*(\d+)')), + ('failed_requests', 'Failed', + ntob(r'^Failed requests:\s*(\d+)')), + ('requests_per_second', 'req/sec', + ntob(r'^Requests per second:\s*([0-9.]+)')), + ('time_per_request_concurrent', 'msec/req', + ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')), + ('transfer_rate', 'KB/sec', + ntob(r'^Transfer rate:\s*([0-9.]+)')), + ] + + def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000, concurrency=10): + self.path = path + self.requests = requests + self.concurrency = concurrency + + def args(self): + port = cherrypy.server.socket_port + assert self.concurrency > 0 + assert self.requests > 0 + # Don't use "localhost". + # Cf http://mail.python.org/pipermail/python-win32/2008-March/007050.html + return ("-k -n %s -c %s http://127.0.0.1:%s%s" % + (self.requests, self.concurrency, port, self.path)) + + def run(self): + # Parse output of ab, setting attributes on self + try: + self.output = _cpmodpy.read_process(AB_PATH or "ab", self.args()) + except: + print(_cperror.format_exc()) + raise + + for attr, name, pattern in self.parse_patterns: + val = re.search(pattern, self.output, re.MULTILINE) + if val: + val = val.group(1) + setattr(self, attr, val) + else: + setattr(self, attr, None) + + +safe_threads = (25, 50, 100, 200, 400) +if sys.platform in ("win32",): + # For some reason, ab crashes with > 50 threads on my Win2k laptop. + safe_threads = (10, 20, 30, 40, 50) + + +def thread_report(path=SCRIPT_NAME + "/hello", concurrency=safe_threads): + sess = ABSession(path) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + avg = dict.fromkeys(attrs, 0.0) + + yield ('threads',) + names + for c in concurrency: + sess.concurrency = c + sess.run() + row = [c] + for attr in attrs: + val = getattr(sess, attr) + if val is None: + print(sess.output) + row = None + break + val = float(val) + avg[attr] += float(val) + row.append(val) + if row: + yield row + + # Add a row of averages. + yield ["Average"] + [str(avg[attr] / len(concurrency)) for attr in attrs] + +def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), + concurrency=50): + sess = ABSession(concurrency=concurrency) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + yield ('bytes',) + names + for sz in sizes: + sess.path = "%s/sizer?size=%s" % (SCRIPT_NAME, sz) + sess.run() + yield [sz] + [getattr(sess, attr) for attr in attrs] + +def print_report(rows): + for row in rows: + print("") + for i, val in enumerate(row): + sys.stdout.write(str(val).rjust(10) + " | ") + print("") + + +def run_standard_benchmarks(): + print("") + print("Client Thread Report (1000 requests, 14 byte response body, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(thread_report()) + + print("") + print("Client Thread Report (1000 requests, 14 bytes via staticdir, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(thread_report("%s/static/index.html" % SCRIPT_NAME)) + + print("") + print("Size Report (1000 requests, 50 client threads, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(size_report()) + + +# modpython and other WSGI # + +def startup_modpython(req=None): + """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).""" + if cherrypy.engine.state == cherrypy._cpengine.STOPPED: + if req: + if "nullreq" in req.get_options(): + cherrypy.engine.request_class = NullRequest + cherrypy.engine.response_class = NullResponse + ab_opt = req.get_options().get("ab", "") + if ab_opt: + global AB_PATH + AB_PATH = ab_opt + cherrypy.engine.start() + if cherrypy.engine.state == cherrypy._cpengine.STARTING: + cherrypy.engine.wait() + return 0 # apache.OK + + +def run_modpython(use_wsgi=False): + print("Starting mod_python...") + pyopts = [] + + # Pass the null and ab=path options through Apache + if "--null" in opts: + pyopts.append(("nullreq", "")) + + if "--ab" in opts: + pyopts.append(("ab", opts["--ab"])) + + s = _cpmodpy.ModPythonServer + if use_wsgi: + pyopts.append(("wsgi.application", "cherrypy::tree")) + pyopts.append(("wsgi.startup", "cherrypy.test.benchmark::startup_modpython")) + handler = "modpython_gateway::handler" + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH, handler=handler) + else: + pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython")) + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) + + try: + s.start() + run() + finally: + s.stop() + + + +if __name__ == '__main__': + longopts = ['cpmodpy', 'modpython', 'null', 'notests', + 'help', 'ab=', 'apache='] + try: + switches, args = getopt.getopt(sys.argv[1:], "", longopts) + opts = dict(switches) + except getopt.GetoptError: + print(__doc__) + sys.exit(2) + + if "--help" in opts: + print(__doc__) + sys.exit(0) + + if "--ab" in opts: + AB_PATH = opts['--ab'] + + if "--notests" in opts: + # Return without stopping the server, so that the pages + # can be tested from a standard web browser. + def run(): + port = cherrypy.server.socket_port + print("You may now open http://127.0.0.1:%s%s/" % + (port, SCRIPT_NAME)) + + if "--null" in opts: + print("Using null Request object") + else: + def run(): + end = time.time() - start + print("Started in %s seconds" % end) + if "--null" in opts: + print("\nUsing null Request object") + try: + try: + run_standard_benchmarks() + except: + print(_cperror.format_exc()) + raise + finally: + cherrypy.engine.exit() + + print("Starting CherryPy app server...") + + class NullWriter(object): + """Suppresses the printing of socket errors.""" + def write(self, data): + pass + sys.stderr = NullWriter() + + start = time.time() + + if "--cpmodpy" in opts: + run_modpython() + elif "--modpython" in opts: + run_modpython(use_wsgi=True) + else: + if "--null" in opts: + cherrypy.server.request_class = NullRequest + cherrypy.server.response_class = NullResponse + + cherrypy.engine.start_with_callback(run) + cherrypy.engine.block() diff --git a/cherrypy/test/checkerdemo.py b/cherrypy/test/checkerdemo.py new file mode 100644 index 0000000..32a7dee --- /dev/null +++ b/cherrypy/test/checkerdemo.py @@ -0,0 +1,47 @@ +"""Demonstration app for cherrypy.checker. + +This application is intentionally broken and badly designed. +To demonstrate the output of the CherryPy Checker, simply execute +this module. +""" + +import os +import cherrypy +thisdir = os.path.dirname(os.path.abspath(__file__)) + +class Root: + pass + +if __name__ == '__main__': + conf = {'/base': {'tools.staticdir.root': thisdir, + # Obsolete key. + 'throw_errors': True, + }, + # This entry should be OK. + '/base/static': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on missing folder. + '/base/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'js'}, + # Warn on dir with an abs path even though we provide root. + '/base/static2': {'tools.staticdir.on': True, + 'tools.staticdir.dir': '/static'}, + # Warn on dir with a relative path with no root. + '/static3': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on unknown namespace + '/unknown': {'toobles.gzip.on': True}, + # Warn special on cherrypy..* + '/cpknown': {'cherrypy.tools.encode.on': True}, + # Warn on mismatched types + '/conftype': {'request.show_tracebacks': 14}, + # Warn on unknown tool. + '/web': {'tools.unknown.on': True}, + # Warn on server.* in app config. + '/app1': {'server.socket_host': '0.0.0.0'}, + # Warn on 'localhost' + 'global': {'server.socket_host': 'localhost'}, + # Warn on '[name]' + '[/extra_brackets]': {}, + } + cherrypy.quickstart(Root(), config=conf) diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py new file mode 100644 index 0000000..6053621 --- /dev/null +++ b/cherrypy/test/helper.py @@ -0,0 +1,493 @@ +"""A library of helper functions for the CherryPy test suite.""" + +import datetime +import logging +log = logging.getLogger(__name__) +import os +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + +import re +import sys +import time +import warnings + +import cherrypy +from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob +from cherrypy.lib import httputil +from cherrypy.lib import gctools +from cherrypy.lib.reprconf import unrepr +from cherrypy.test import webtest + +import nose + +_testconfig = None + +def get_tst_config(overconf = {}): + global _testconfig + if _testconfig is None: + conf = { + 'scheme': 'http', + 'protocol': "HTTP/1.1", + 'port': 54583, + 'host': '127.0.0.1', + 'validate': False, + 'conquer': False, + 'server': 'wsgi', + } + try: + import testconfig + _conf = testconfig.config.get('supervisor', None) + if _conf is not None: + for k, v in _conf.items(): + if isinstance(v, basestring): + _conf[k] = unrepr(v) + conf.update(_conf) + except ImportError: + pass + _testconfig = conf + conf = _testconfig.copy() + conf.update(overconf) + + return conf + +class Supervisor(object): + """Base class for modeling and controlling servers during testing.""" + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if k == 'port': + setattr(self, k, int(v)) + setattr(self, k, v) + + +log_to_stderr = lambda msg, level: sys.stderr.write(msg + os.linesep) + +class LocalSupervisor(Supervisor): + """Base class for modeling/controlling servers which run in the same process. + + When the server side runs in a different process, start/stop can dump all + state between each test module easily. When the server side runs in the + same process as the client, however, we have to do a bit more work to ensure + config and mounted apps are reset between tests. + """ + + using_apache = False + using_wsgi = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + cherrypy.server.httpserver = self.httpserver_class + + # This is perhaps the wrong place for this call but this is the only + # place that i've found so far that I KNOW is early enough to set this. + cherrypy.config.update({'log.screen': False}) + engine = cherrypy.engine + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + #engine.subscribe('log', log_to_stderr) + + def start(self, modulename=None): + """Load and start the HTTP server.""" + if modulename: + # Unhook httpserver so cherrypy.server.start() creates a new + # one (with config from setup_server, if declared). + cherrypy.server.httpserver = None + + cherrypy.engine.start() + + self.sync_apps() + + def sync_apps(self): + """Tell the server about any apps which the setup functions mounted.""" + pass + + def stop(self): + td = getattr(self, 'teardown', None) + if td: + td() + + cherrypy.engine.exit() + + for name, server in copyitems(getattr(cherrypy, 'servers', {})): + server.unsubscribe() + del cherrypy.servers[name] + + +class NativeServerSupervisor(LocalSupervisor): + """Server supervisor for the builtin HTTP server.""" + + httpserver_class = "cherrypy._cpnative_server.CPHTTPServer" + using_apache = False + using_wsgi = False + + def __str__(self): + return "Builtin HTTP Server on %s:%s" % (self.host, self.port) + + +class LocalWSGISupervisor(LocalSupervisor): + """Server supervisor for the builtin WSGI server.""" + + httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer" + using_apache = False + using_wsgi = True + + def __str__(self): + return "Builtin WSGI Server on %s:%s" % (self.host, self.port) + + def sync_apps(self): + """Hook a new WSGI app into the origin server.""" + cherrypy.server.httpserver.wsgi_app = self.get_app() + + def get_app(self, app=None): + """Obtain a new (decorated) WSGI app to hook into the origin server.""" + if app is None: + app = cherrypy.tree + + if self.conquer: + try: + import wsgiconq + except ImportError: + warnings.warn("Error importing wsgiconq. pyconquer will not run.") + else: + app = wsgiconq.WSGILogger(app, c_calls=True) + + if self.validate: + try: + from wsgiref import validate + except ImportError: + warnings.warn("Error importing wsgiref. The validator will not run.") + else: + #wraps the app in the validator + app = validate.validator(app) + + return app + + +def get_cpmodpy_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_cpmodpy + return sup + +def get_modpygw_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_modpython_gateway + sup.using_wsgi = True + return sup + +def get_modwsgi_supervisor(**options): + from cherrypy.test import modwsgi + return modwsgi.ModWSGISupervisor(**options) + +def get_modfcgid_supervisor(**options): + from cherrypy.test import modfcgid + return modfcgid.ModFCGISupervisor(**options) + +def get_modfastcgi_supervisor(**options): + from cherrypy.test import modfastcgi + return modfastcgi.ModFCGISupervisor(**options) + +def get_wsgi_u_supervisor(**options): + cherrypy.server.wsgi_version = ('u', 0) + return LocalWSGISupervisor(**options) + + +class CPWebCase(webtest.WebCase): + + script_name = "" + scheme = "http" + + available_servers = {'wsgi': LocalWSGISupervisor, + 'wsgi_u': get_wsgi_u_supervisor, + 'native': NativeServerSupervisor, + 'cpmodpy': get_cpmodpy_supervisor, + 'modpygw': get_modpygw_supervisor, + 'modwsgi': get_modwsgi_supervisor, + 'modfcgid': get_modfcgid_supervisor, + 'modfastcgi': get_modfastcgi_supervisor, + } + default_server = "wsgi" + + def _setup_server(cls, supervisor, conf): + v = sys.version.split()[0] + log.info("Python version used to run this test script: %s" % v) + log.info("CherryPy version: %s" % cherrypy.__version__) + if supervisor.scheme == "https": + ssl = " (ssl)" + else: + ssl = "" + log.info("HTTP server version: %s%s" % (supervisor.protocol, ssl)) + log.info("PID: %s" % os.getpid()) + + cherrypy.server.using_apache = supervisor.using_apache + cherrypy.server.using_wsgi = supervisor.using_wsgi + + if sys.platform[:4] == 'java': + cherrypy.config.update({'server.nodelay': False}) + + if isinstance(conf, basestring): + parser = cherrypy.lib.reprconf.Parser() + conf = parser.dict_from_file(conf).get('global', {}) + else: + conf = conf or {} + baseconf = conf.copy() + baseconf.update({'server.socket_host': supervisor.host, + 'server.socket_port': supervisor.port, + 'server.protocol_version': supervisor.protocol, + 'environment': "test_suite", + }) + if supervisor.scheme == "https": + #baseconf['server.ssl_module'] = 'builtin' + baseconf['server.ssl_certificate'] = serverpem + baseconf['server.ssl_private_key'] = serverpem + + # helper must be imported lazily so the coverage tool + # can run against module-level statements within cherrypy. + # Also, we have to do "from cherrypy.test import helper", + # exactly like each test module does, because a relative import + # would stick a second instance of webtest in sys.modules, + # and we wouldn't be able to globally override the port anymore. + if supervisor.scheme == "https": + webtest.WebCase.HTTP_CONN = HTTPSConnection + return baseconf + _setup_server = classmethod(_setup_server) + + def setup_class(cls): + '' + #Creates a server + conf = get_tst_config() + supervisor_factory = cls.available_servers.get(conf.get('server', 'wsgi')) + if supervisor_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + supervisor = supervisor_factory(**conf) + + #Copied from "run_test_suite" + cherrypy.config.reset() + baseconf = cls._setup_server(supervisor, conf) + cherrypy.config.update(baseconf) + setup_client() + + if hasattr(cls, 'setup_server'): + # Clear the cherrypy tree and clear the wsgi server so that + # it can be updated with the new root + cherrypy.tree = cherrypy._cptree.Tree() + cherrypy.server.httpserver = None + cls.setup_server() + # Add a resource for verifying there are no refleaks + # to *every* test class. + cherrypy.tree.mount(gctools.GCRoot(), '/gc') + cls.do_gc_test = True + supervisor.start(cls.__module__) + + cls.supervisor = supervisor + setup_class = classmethod(setup_class) + + def teardown_class(cls): + '' + if hasattr(cls, 'setup_server'): + cls.supervisor.stop() + teardown_class = classmethod(teardown_class) + + do_gc_test = False + + def test_gc(self): + if self.do_gc_test: + self.getPage("/gc/stats") + self.assertBody("Statistics:") + # Tell nose to run this last in each class + test_gc.compat_co_firstlineno = getattr(sys, 'maxint', None) or float('inf') + + def prefix(self): + return self.script_name.rstrip("/") + + def base(self): + if ((self.scheme == "http" and self.PORT == 80) or + (self.scheme == "https" and self.PORT == 443)): + port = "" + else: + port = ":%s" % self.PORT + + return "%s://%s%s%s" % (self.scheme, self.HOST, port, + self.script_name.rstrip("/")) + + def exit(self): + sys.exit() + + def getPage(self, url, headers=None, method="GET", body=None, protocol=None): + """Open the url. Return status, headers, body.""" + if self.script_name: + url = httputil.urljoin(self.script_name, url) + return webtest.WebCase.getPage(self, url, headers, method, body, protocol) + + def skip(self, msg='skipped '): + raise nose.SkipTest(msg) + + def assertErrorPage(self, status, message=None, pattern=''): + """Compare the response body with a built in error page. + + The function will optionally look for the regexp pattern, + within the exception embedded in the error page.""" + + # This will never contain a traceback + page = cherrypy._cperror.get_error_page(status, message=message) + + # First, test the response body without checking the traceback. + # Stick a match-all group (.*) in to grab the traceback. + esc = re.escape + epage = esc(page) + epage = epage.replace(esc('
'),
+                              esc('
') + '(.*)' + esc('
')) + m = re.match(ntob(epage, self.encoding), self.body, re.DOTALL) + if not m: + self._handlewebError('Error page does not match; expected:\n' + page) + return + + # Now test the pattern against the traceback + if pattern is None: + # Special-case None to mean that there should be *no* traceback. + if m and m.group(1): + self._handlewebError('Error page contains traceback') + else: + if (m is None) or ( + not re.search(ntob(re.escape(pattern), self.encoding), + m.group(1))): + msg = 'Error page does not contain %s in traceback' + self._handlewebError(msg % repr(pattern)) + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +def setup_client(): + """Set up the WebCase classes to match the server's socket settings.""" + webtest.WebCase.PORT = cherrypy.server.socket_port + webtest.WebCase.HOST = cherrypy.server.socket_host + if cherrypy.server.ssl_certificate: + CPWebCase.scheme = 'https' + +# --------------------------- Spawning helpers --------------------------- # + + +class CPProcess(object): + + pid_file = os.path.join(thisdir, 'test.pid') + config_file = os.path.join(thisdir, 'test.conf') + config_template = """[global] +server.socket_host: '%(host)s' +server.socket_port: %(port)s +checker.on: False +log.screen: False +log.error_file: r'%(error_log)s' +log.access_file: r'%(access_log)s' +%(ssl)s +%(extra)s +""" + error_log = os.path.join(thisdir, 'test.error.log') + access_log = os.path.join(thisdir, 'test.access.log') + + def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None): + self.wait = wait + self.daemonize = daemonize + self.ssl = ssl + self.host = socket_host or cherrypy.server.socket_host + self.port = socket_port or cherrypy.server.socket_port + + def write_conf(self, extra=""): + if self.ssl: + serverpem = os.path.join(thisdir, 'test.pem') + ssl = """ +server.ssl_certificate: r'%s' +server.ssl_private_key: r'%s' +""" % (serverpem, serverpem) + else: + ssl = "" + + conf = self.config_template % { + 'host': self.host, + 'port': self.port, + 'error_log': self.error_log, + 'access_log': self.access_log, + 'ssl': ssl, + 'extra': extra, + } + f = open(self.config_file, 'wb') + f.write(ntob(conf, 'utf-8')) + f.close() + + def start(self, imports=None): + """Start cherryd in a subprocess.""" + cherrypy._cpserver.wait_for_free_port(self.host, self.port) + + args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'), + '-c', self.config_file, '-p', self.pid_file] + + if not isinstance(imports, (list, tuple)): + imports = [imports] + for i in imports: + if i: + args.append('-i') + args.append(i) + + if self.daemonize: + args.append('-d') + + env = os.environ.copy() + # Make sure we import the cherrypy package in which this module is defined. + grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) + if env.get('PYTHONPATH', ''): + env['PYTHONPATH'] = os.pathsep.join((grandparentdir, env['PYTHONPATH'])) + else: + env['PYTHONPATH'] = grandparentdir + if self.wait: + self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env) + else: + os.spawnve(os.P_NOWAIT, sys.executable, args, env) + cherrypy._cpserver.wait_for_occupied_port(self.host, self.port) + + # Give the engine a wee bit more time to finish STARTING + if self.daemonize: + time.sleep(2) + else: + time.sleep(1) + + def get_pid(self): + return int(open(self.pid_file, 'rb').read()) + + def join(self): + """Wait for the process to exit.""" + try: + try: + # Mac, UNIX + os.wait() + except AttributeError: + # Windows + try: + pid = self.get_pid() + except IOError: + # Assume the subprocess deleted the pidfile on shutdown. + pass + else: + os.waitpid(pid, 0) + except OSError: + x = sys.exc_info()[1] + if x.args != (10, 'No child processes'): + raise + diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py new file mode 100644 index 0000000..3c6f114 --- /dev/null +++ b/cherrypy/test/logtest.py @@ -0,0 +1,188 @@ +"""logtest, a unittest.TestCase helper for testing log output.""" + +import sys +import time + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, unicodestr + + +try: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty, termios + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class LogCase(object): + """unittest.TestCase mixin for testing log messages. + + logfile: a filename for the desired log. Yes, I know modes are evil, + but it makes the test functions so much cleaner to set this once. + + lastmarker: the last marker in the log. This can be used to search for + messages since the last marker. + + markerPrefix: a string with which to prefix log markers. This should be + unique enough from normal log output to use for marker identification. + """ + + logfile = None + lastmarker = None + markerPrefix = ntob("test suite marker: ") + + def _handleLogError(self, msg, data, marker, pattern): + print("") + print(" ERROR: %s" % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = " Show: [L]og [M]arker [P]attern; [I]gnore, [R]aise, or sys.e[X]it >> " + sys.stdout.write(p + ' ') + # ARGH + sys.stdout.flush() + while True: + i = getchar().upper() + if i not in "MPLIRX": + continue + print(i.upper()) # Also prints new line + if i == "L": + for x, line in enumerate(data): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write("<-- More -->\r ") + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(" \r ") + if m == "q": + break + print(line.rstrip()) + elif i == "M": + print(repr(marker or self.lastmarker)) + elif i == "P": + print(repr(pattern)) + elif i == "I": + # return without raising the normal exception + return + elif i == "R": + raise self.failureException(msg) + elif i == "X": + self.exit() + sys.stdout.write(p + ' ') + + def exit(self): + sys.exit() + + def emptyLog(self): + """Overwrite self.logfile with 0 bytes.""" + open(self.logfile, 'wb').write("") + + def markLog(self, key=None): + """Insert a marker line into the log and set self.lastmarker.""" + if key is None: + key = str(time.time()) + self.lastmarker = key + + open(self.logfile, 'ab+').write(ntob("%s%s\n" % (self.markerPrefix, key),"utf-8")) + + def _read_marked_region(self, marker=None): + """Return lines from self.logfile in the marked region. + + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be returned. + """ +## # Give the logger time to finish writing? +## time.sleep(0.5) + + logfile = self.logfile + marker = marker or self.lastmarker + if marker is None: + return open(logfile, 'rb').readlines() + + if isinstance(marker, unicodestr): + marker = marker.encode('utf-8') + data = [] + in_region = False + for line in open(logfile, 'rb'): + if in_region: + if (line.startswith(self.markerPrefix) and not marker in line): + break + else: + data.append(line) + elif marker in line: + in_region = True + return data + + def assertInLog(self, line, marker=None): + """Fail if the given (partial) line is not in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + return + msg = "%r not found in log" % line + self._handleLogError(msg, data, marker, line) + + def assertNotInLog(self, line, marker=None): + """Fail if the given (partial) line is in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + msg = "%r found in log" % line + self._handleLogError(msg, data, marker, line) + + def assertLog(self, sliceargs, lines, marker=None): + """Fail if log.readlines()[sliceargs] is not contained in 'lines'. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + if isinstance(sliceargs, int): + # Single arg. Use __getitem__ and allow lines to be str or list. + if isinstance(lines, (tuple, list)): + lines = lines[0] + if isinstance(lines, unicodestr): + lines = lines.encode('utf-8') + if lines not in data[sliceargs]: + msg = "%r not found on log line %r" % (lines, sliceargs) + self._handleLogError(msg, [data[sliceargs],"--EXTRA CONTEXT--"] + data[sliceargs+1:sliceargs+6], marker, lines) + else: + # Multiple args. Use __getslice__ and require lines to be list. + if isinstance(lines, tuple): + lines = list(lines) + elif isinstance(lines, basestring): + raise TypeError("The 'lines' arg must be a list when " + "'sliceargs' is a tuple.") + + start, stop = sliceargs + for line, logline in zip(lines, data[start:stop]): + if isinstance(line, unicodestr): + line = line.encode('utf-8') + if line not in logline: + msg = "%r not found in log" % line + self._handleLogError(msg, data[start:stop], marker, line) + diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py new file mode 100644 index 0000000..95acf14 --- /dev/null +++ b/cherrypy/test/modfastcgi.py @@ -0,0 +1,135 @@ +"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. + +To autostart fastcgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "apache2ctl" +CONF_PATH = "fastcgi.conf" + +conf_fastcgi = """ +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog %(root)s/mod_fastcgi.error.log + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + +def erase_script_name(environ, start_response): + environ['SCRIPT_NAME'] = '' + return cherrypy.tree(environ, start_response) + +class ModFCGISupervisor(helper.LocalWSGISupervisor): + + httpserver_class = "cherrypy.process.servers.FlupFCGIServer" + using_apache = True + using_wsgi = True + template = conf_fastcgi + + def __str__(self): + return "FCGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=erase_script_name, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + cherrypy.server.socket_port = 4000 + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + cherrypy.engine.start() + self.sync_apps() + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = output.replace('\r\n', '\n') + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + helper.LocalWSGISupervisor.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app(erase_script_name) + diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py new file mode 100644 index 0000000..736aa4c --- /dev/null +++ b/cherrypy/test/modfcgid.py @@ -0,0 +1,125 @@ +"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. + +To autostart fcgid, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.process import plugins, servers +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "httpd" +CONF_PATH = "fcgi.conf" + +conf_fcgid = """ +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + +class ModFCGISupervisor(helper.LocalSupervisor): + + using_apache = True + using_wsgi = True + template = conf_fcgid + + def __str__(self): + return "FCGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + helper.LocalServer.start(self, modulename) + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = ntob(output.replace('\r\n', '\n')) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + helper.LocalServer.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app() + diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py new file mode 100644 index 0000000..519571f --- /dev/null +++ b/cherrypy/test/modpy.py @@ -0,0 +1,163 @@ +"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. + +To autostart modpython, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + +If you wish to test the WSGI interface instead of our _cpmodpy interface, +you also need the 'modpython_gateway' module at: +http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import time + +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "httpd" +CONF_PATH = "test_mp.conf" + +conf_modpython_gateway = """ +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::wsgisetup +PythonOption testmod %(modulename)s +PythonHandler modpython_gateway::handler +PythonOption wsgi.application cherrypy::tree +PythonOption socket_host %(host)s +PythonDebug On +""" + +conf_cpmodpy = """ +# Apache2 server conf file for testing CherryPy with _cpmodpy. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::cpmodpysetup +PythonHandler cherrypy._cpmodpy::handler +PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server +PythonOption socket_host %(host)s +PythonDebug On +""" + +class ModPythonSupervisor(helper.Supervisor): + + using_apache = True + using_wsgi = False + template = None + + def __str__(self): + return "ModPython Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + f.write(self.template % + {'port': self.port, 'modulename': modulename, + 'host': self.host}) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + + +loaded = False +def wsgisetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + import cherrypy + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options['socket_host'], + }) + + modname = options['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.server.unsubscribe() + cherrypy.engine.start() + from mod_python import apache + return apache.OK + + +def cpmodpysetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + import cherrypy + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options['socket_host'], + }) + from mod_python import apache + return apache.OK + diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py new file mode 100644 index 0000000..309a541 --- /dev/null +++ b/cherrypy/test/modwsgi.py @@ -0,0 +1,148 @@ +"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. + +To autostart modwsgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + + +KNOWN BUGS +========== + +##1. Apache processes Range headers automatically; CherryPy's truncated +## output is then truncated again by Apache. See test_core.testRanges. +## This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +##4. Apache replaces status "reason phrases" automatically. For example, +## CherryPy may set "304 Not modified" but Apache will write out +## "304 Not Modified" (capital "M"). +##5. Apache does not allow custom error codes as per the spec. +##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the +## Request-URI too early. +7. mod_wsgi will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. When responding with 204 No Content, mod_wsgi adds a Content-Length + header for you. +9. When an error is raised, mod_wsgi has no facility for printing a + traceback as the response content (it's sent to the Apache log instead). +10. Startup and shutdown of Apache when running mod_wsgi seems slow. +""" + +import os +curdir = os.path.abspath(os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy.test import helper, webtest + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +if sys.platform == 'win32': + APACHE_PATH = "httpd" +else: + APACHE_PATH = "apache" + +CONF_PATH = "test_mw.conf" + +conf_modwsgi = r""" +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s + +AllowEncodedSlashes On +LoadModule rewrite_module modules/mod_rewrite.so +RewriteEngine on +RewriteMap escaping int:escape + +LoadModule log_config_module modules/mod_log_config.so +LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined +CustomLog "%(curdir)s/apache.access.log" combined +ErrorLog "%(curdir)s/apache.error.log" +LogLevel debug + +LoadModule wsgi_module modules/mod_wsgi.so +LoadModule env_module modules/mod_env.so + +WSGIScriptAlias / "%(curdir)s/modwsgi.py" +SetEnv testmod %(testmod)s +""" + + +class ModWSGISupervisor(helper.Supervisor): + """Server Controller for ModWSGI and CherryPy.""" + + using_apache = True + using_wsgi = True + template=conf_modwsgi + + def __str__(self): + return "ModWSGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + output = (self.template % + {'port': self.port, 'testmod': modulename, + 'curdir': curdir}) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) + if result: + print(result) + + # Make a request so mod_wsgi starts up our app. + # If we don't, concurrent initial requests will 404. + cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port) + webtest.openURL('/ihopetheresnodefault', port=self.port) + time.sleep(1) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + + +loaded = False +def application(environ, start_response): + import cherrypy + global loaded + if not loaded: + loaded = True + modname = "cherrypy.test." + environ['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.error.log"), + "log.access_file": os.path.join(curdir, "test.access.log"), + "environment": "test_suite", + "engine.SIGHUP": None, + "engine.SIGTERM": None, + }) + return cherrypy.tree(environ, start_response) + diff --git a/cherrypy/test/sessiondemo.py b/cherrypy/test/sessiondemo.py new file mode 100644 index 0000000..342e5b5 --- /dev/null +++ b/cherrypy/test/sessiondemo.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +"""A session demonstration app.""" + +import calendar +from datetime import datetime +import sys +import cherrypy +from cherrypy.lib import sessions +from cherrypy._cpcompat import copyitems + + +page = """ + + + + + + + +

Session Demo

+

Reload this page. The session ID should not change from one reload to the next

+

Index | Expire | Regenerate

+ + + + + + + + + +
Session ID:%(sessionid)s

%(changemsg)s

Request Cookie%(reqcookie)s
Response Cookie%(respcookie)s

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
+ +""" + +class Root(object): + + def page(self): + changemsg = [] + if cherrypy.session.id != cherrypy.session.originalid: + if cherrypy.session.originalid is None: + changemsg.append('Created new session because no session id was given.') + if cherrypy.session.missing: + changemsg.append('Created new session due to missing (expired or malicious) session.') + if cherrypy.session.regenerated: + changemsg.append('Application generated a new session.') + + try: + expires = cherrypy.response.cookie['session_id']['expires'] + except KeyError: + expires = '' + + return page % { + 'sessionid': cherrypy.session.id, + 'changemsg': '
'.join(changemsg), + 'respcookie': cherrypy.response.cookie.output(), + 'reqcookie': cherrypy.request.cookie.output(), + 'sessiondata': copyitems(cherrypy.session), + 'servertime': datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC", + 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), + 'cpversion': cherrypy.__version__, + 'pyversion': sys.version, + 'expires': expires, + } + + def index(self): + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'green' + return self.page() + index.exposed = True + + def expire(self): + sessions.expire() + return self.page() + expire.exposed = True + + def regen(self): + cherrypy.session.regenerate() + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'yellow' + return self.page() + regen.exposed = True + +if __name__ == '__main__': + cherrypy.config.update({ + #'environment': 'production', + 'log.screen': True, + 'tools.sessions.on': True, + }) + cherrypy.quickstart(Root()) + diff --git a/cherrypy/test/static/dirback.jpg b/cherrypy/test/static/dirback.jpg new file mode 100644 index 0000000000000000000000000000000000000000..530e6d6a386fc097f3a1dbabbde2d80fec1175ac GIT binary patch literal 18238 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 +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +import cherrypy +from cherrypy._cpcompat import md5, ntob +from cherrypy.lib import auth_basic +from cherrypy.test import helper + + +class BasicAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class BasicProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected2: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + userpassdict = {'xuser' : 'xpassword'} + userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()} + + def checkpasshash(realm, user, password): + p = userhashdict.get(user) + return p and p == md5(ntob(password)).hexdigest() or False + + conf = {'/basic': {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)}, + '/basic2': {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash}, + } + + root = Root() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage("/basic/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2(self): + self.getPage("/basic2/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py new file mode 100644 index 0000000..1960fa8 --- /dev/null +++ b/cherrypy/test/test_auth_digest.py @@ -0,0 +1,115 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + + +import cherrypy +from cherrypy.lib import auth_digest + +from cherrypy.test import helper + +class DigestAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class DigestProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + def fetch_users(): + return {'test': 'test'} + + + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users()) + conf = {'/digest': {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'localhost', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.debug': 'True'}} + + root = Root() + root.digest = DigestProtected() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testDigest(self): + self.getPage("/digest/") + self.assertStatus(401) + + value = None + for k, v in self.headers: + if k.lower() == "www-authenticate": + if v.startswith("Digest"): + value = v + break + + if value is None: + self._handlewebError("Digest authentification scheme was not found") + + value = value[7:] + items = value.split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + missing_msg = "%s is missing" + bad_value_msg = "'%s' was expecting '%s' but found '%s'" + nonce = None + if 'realm' not in tokens: + self._handlewebError(missing_msg % 'realm') + elif tokens['realm'] != '"localhost"': + self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) + if 'nonce' not in tokens: + self._handlewebError(missing_msg % 'nonce') + else: + nonce = tokens['nonce'].strip('"') + if 'algorithm' not in tokens: + self._handlewebError(missing_msg % 'algorithm') + elif tokens['algorithm'] != '"MD5"': + self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) + if 'qop' not in tokens: + self._handlewebError(missing_msg % 'qop') + elif tokens['qop'] != '"auth"': + self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) + + get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'}) + + # Test user agent response with a wrong value for 'realm' + base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1(auth.realm, 'test') + response = auth.request_digest(ha1) + # send response with correct response digest, but wrong realm + auth_header = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth_header)]) + self.assertStatus(401) + + # Test that must pass + base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1('localhost', 'test') + response = auth.request_digest(ha1) + # send response with correct response digest + auth_header = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth_header)]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py new file mode 100644 index 0000000..51c1022 --- /dev/null +++ b/cherrypy/test/test_bus.py @@ -0,0 +1,263 @@ +import threading +import time +import unittest + +import cherrypy +from cherrypy._cpcompat import get_daemon, set +from cherrypy.process import wspbus + + +msg = "Listener %d on channel %s: %s." + + +class PublishSubscribeTests(unittest.TestCase): + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_builtin_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + for channel in b.listeners: + for index, priority in enumerate([100, 50, 0, 51]): + b.subscribe(channel, self.get_listener(channel, index), priority) + + for channel in b.listeners: + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) + b.publish(channel, arg=79347) + expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) + + self.assertEqual(self.responses, expected) + + def test_custom_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + custom_listeners = ('hugh', 'louis', 'dewey') + for channel in custom_listeners: + for index, priority in enumerate([None, 10, 60, 40]): + b.subscribe(channel, self.get_listener(channel, index), priority) + + for channel in custom_listeners: + b.publish(channel, 'ah so') + expected.extend([msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)]) + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) + + self.assertEqual(self.responses, expected) + + def test_listener_errors(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + channels = [c for c in b.listeners if c != 'log'] + + for channel in channels: + b.subscribe(channel, self.get_listener(channel, 1)) + # This will break since the lambda takes no args. + b.subscribe(channel, lambda: None, priority=20) + + for channel in channels: + self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) + expected.append(msg % (1, channel, 123)) + + self.assertEqual(self.responses, expected) + + +class BusMethodTests(unittest.TestCase): + + def log(self, bus): + self._log_entries = [] + def logit(msg, level): + self._log_entries.append(msg) + bus.subscribe('log', logit) + + def assertLog(self, entries): + self.assertEqual(self._log_entries, entries) + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_start(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('start', self.get_listener('start', index)) + + b.start() + try: + # The start method MUST call all 'start' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'start', None) for i in range(num)])) + # The start method MUST move the state to STARTED + # (or EXITING, if errors occur) + self.assertEqual(b.state, b.states.STARTED) + # The start method MUST log its states. + self.assertLog(['Bus STARTING', 'Bus STARTED']) + finally: + # Exit so the atexit handler doesn't complain. + b.exit() + + def test_stop(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + + b.stop() + + # The stop method MUST call all 'stop' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)])) + # The stop method MUST move the state to STOPPED + self.assertEqual(b.state, b.states.STOPPED) + # The stop method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED']) + + def test_graceful(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('graceful', self.get_listener('graceful', index)) + + b.graceful() + + # The graceful method MUST call all 'graceful' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'graceful', None) for i in range(num)])) + # The graceful method MUST log its states. + self.assertLog(['Bus graceful']) + + def test_exit(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + b.subscribe('exit', self.get_listener('exit', index)) + + b.exit() + + # The exit method MUST call all 'stop' listeners, + # and then all 'exit' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)] + + [msg % (i, 'exit', None) for i in range(num)])) + # The exit method MUST move the state to EXITING + self.assertEqual(b.state, b.states.EXITING) + # The exit method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + + def test_wait(self): + b = wspbus.Bus() + + def f(method): + time.sleep(0.2) + getattr(b, method)() + + for method, states in [('start', [b.states.STARTED]), + ('stop', [b.states.STOPPED]), + ('start', [b.states.STARTING, b.states.STARTED]), + ('exit', [b.states.EXITING]), + ]: + threading.Thread(target=f, args=(method,)).start() + b.wait(states) + + # The wait method MUST wait for the given state(s). + if b.state not in states: + self.fail("State %r not in %r" % (b.state, states)) + + def test_block(self): + b = wspbus.Bus() + self.log(b) + + def f(): + time.sleep(0.2) + b.exit() + def g(): + time.sleep(0.4) + threading.Thread(target=f).start() + threading.Thread(target=g).start() + threads = [t for t in threading.enumerate() if not get_daemon(t)] + self.assertEqual(len(threads), 3) + + b.block() + + # The block method MUST wait for the EXITING state. + self.assertEqual(b.state, b.states.EXITING) + # The block method MUST wait for ALL non-main, non-daemon threads to finish. + threads = [t for t in threading.enumerate() if not get_daemon(t)] + self.assertEqual(len(threads), 1) + # The last message will mention an indeterminable thread name; ignore it + self.assertEqual(self._log_entries[:-1], + ['Bus STOPPING', 'Bus STOPPED', + 'Bus EXITING', 'Bus EXITED', + 'Waiting for child threads to terminate...']) + + def test_start_with_callback(self): + b = wspbus.Bus() + self.log(b) + try: + events = [] + def f(*args, **kwargs): + events.append(("f", args, kwargs)) + def g(): + events.append("g") + b.subscribe("start", g) + b.start_with_callback(f, (1, 3, 5), {"foo": "bar"}) + # Give wait() time to run f() + time.sleep(0.2) + + # The callback method MUST wait for the STARTED state. + self.assertEqual(b.state, b.states.STARTED) + # The callback method MUST run after all start methods. + self.assertEqual(events, ["g", ("f", (1, 3, 5), {"foo": "bar"})]) + finally: + b.exit() + + def test_log(self): + b = wspbus.Bus() + self.log(b) + self.assertLog([]) + + # Try a normal message. + expected = [] + for msg in ["O mah darlin'"] * 3 + ["Clementiiiiiiiine"]: + b.log(msg) + expected.append(msg) + self.assertLog(expected) + + # Try an error message + try: + foo + except NameError: + b.log("You are lost and gone forever", traceback=True) + lastmsg = self._log_entries[-1] + if "Traceback" not in lastmsg or "NameError" not in lastmsg: + self.fail("Last log message %r did not contain " + "the expected traceback." % lastmsg) + else: + self.fail("NameError was not raised as expected.") + + +if __name__ == "__main__": + unittest.main() diff --git a/cherrypy/test/test_caching.py b/cherrypy/test/test_caching.py new file mode 100644 index 0000000..c210e6e --- /dev/null +++ b/cherrypy/test/test_caching.py @@ -0,0 +1,328 @@ +import datetime +import gzip +from itertools import count +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import sys +import threading +import time +import urllib + +import cherrypy +from cherrypy._cpcompat import next, ntob, quote, xrange +from cherrypy.lib import httputil + +gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;') + + + +from cherrypy.test import helper + +class CacheTest(helper.CPWebCase): + + def setup_server(): + + class Root: + + _cp_config = {'tools.caching.on': True} + + def __init__(self): + self.counter = 0 + self.control_counter = 0 + self.longlock = threading.Lock() + + def index(self): + self.counter += 1 + msg = "visit #%s" % self.counter + return msg + index.exposed = True + + def control(self): + self.control_counter += 1 + return "visit #%s" % self.control_counter + control.exposed = True + + def a_gif(self): + cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate() + return gif_bytes + a_gif.exposed = True + + def long_process(self, seconds='1'): + try: + self.longlock.acquire() + time.sleep(float(seconds)) + finally: + self.longlock.release() + return 'success!' + long_process.exposed = True + + def clear_cache(self, path): + cherrypy._cache.store[cherrypy.request.base + path].clear() + clear_cache.exposed = True + + class VaryHeaderCachingServer(object): + + _cp_config = {'tools.caching.on': True, + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')], + } + + def __init__(self): + self.counter = count(1) + + def index(self): + return "visit #%s" % next(self.counter) + index.exposed = True + + class UnCached(object): + _cp_config = {'tools.expires.on': True, + 'tools.expires.secs': 60, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + } + + def force(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + self._cp_config['tools.expires.force'] = True + self._cp_config['tools.expires.secs'] = 0 + return "being forceful" + force.exposed = True + force._cp_config = {'tools.expires.secs': 0} + + def dynamic(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + cherrypy.response.headers['Cache-Control'] = 'private' + return "D-d-d-dynamic!" + dynamic.exposed = True + + def cacheable(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + return "Hi, I'm cacheable." + cacheable.exposed = True + + def specific(self): + cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' + return "I am being specific" + specific.exposed = True + specific._cp_config = {'tools.expires.secs': 86400} + + class Foo(object):pass + + def wrongtype(self): + cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' + return "Woops" + wrongtype.exposed = True + wrongtype._cp_config = {'tools.expires.secs': Foo()} + + cherrypy.tree.mount(Root()) + cherrypy.tree.mount(UnCached(), "/expires") + cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers") + cherrypy.config.update({'tools.gzip.on': True}) + setup_server = staticmethod(setup_server) + + def testCaching(self): + elapsed = 0.0 + for trial in range(10): + self.getPage("/") + # The response should be the same every time, + # except for the Age response header. + self.assertBody('visit #1') + if trial != 0: + age = int(self.assertHeader("Age")) + self.assert_(age >= elapsed) + elapsed = age + + # POST, PUT, DELETE should not be cached. + self.getPage("/", method="POST") + self.assertBody('visit #2') + # Because gzip is turned on, the Vary header should always Vary for content-encoding + self.assertHeader('Vary', 'Accept-Encoding') + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage("/", method="GET") + self.assertBody('visit #3') + # ...but this request should get the cached copy. + self.getPage("/", method="GET") + self.assertBody('visit #3') + self.getPage("/", method="DELETE") + self.assertBody('visit #4') + + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertHeader('Vary') + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) + + # Now check that a second request gets the gzip header and gzipped body + # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped + # response body was being gzipped a second time. + self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) + + # Now check that a third request that doesn't accept gzip + # skips the cache (because the 'Vary' header denies it). + self.getPage("/", method="GET") + self.assertNoHeader('Content-Encoding') + self.assertBody('visit #6') + + def testVaryHeader(self): + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertHeaderItemValue('Vary', 'Our-Varying-Header') + self.assertBody('visit #1') + + # Now check that different 'Vary'-fields don't evict each other. + # This test creates 2 requests with different 'Our-Varying-Header' + # and then tests if the first one still exists. + self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus("200 OK") + self.assertBody('visit #2') + + self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus("200 OK") + self.assertBody('visit #2') + + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertBody('visit #1') + + def testExpiresTool(self): + # test setting an expires header + self.getPage("/expires/specific") + self.assertStatus("200 OK") + self.assertHeader("Expires") + + # test exceptions for bad time values + self.getPage("/expires/wrongtype") + self.assertStatus(500) + self.assertInBody("TypeError") + + # static content should not have "cache prevention" headers + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") + + # dynamic content that sets indicators should not have + # "cache prevention" headers + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") + + self.getPage('/expires/dynamic') + self.assertBody("D-d-d-dynamic!") + # the Cache-Control header should be untouched + self.assertHeader("Cache-Control", "private") + self.assertHeader("Expires") + + # configure the tool to ignore indicators and replace existing headers + self.getPage("/expires/force") + self.assertStatus("200 OK") + # This also gives us a chance to test 0 expiry with no other headers + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + # static content should now have "cache prevention" headers + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + # the cacheable handler should now have "cache prevention" headers + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + self.getPage('/expires/dynamic') + self.assertBody("D-d-d-dynamic!") + # dynamic sets Cache-Control to private but it should be + # overwritten here ... + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + def testLastModified(self): + self.getPage("/a.gif") + self.assertStatus(200) + self.assertBody(gif_bytes) + lm1 = self.assertHeader("Last-Modified") + + # this request should get the cached copy. + self.getPage("/a.gif") + self.assertStatus(200) + self.assertBody(gif_bytes) + self.assertHeader("Age") + lm2 = self.assertHeader("Last-Modified") + self.assertEqual(lm1, lm2) + + # this request should match the cached copy, but raise 304. + self.getPage("/a.gif", [('If-Modified-Since', lm1)]) + self.assertStatus(304) + self.assertNoHeader("Last-Modified") + if not getattr(cherrypy.server, "using_apache", False): + self.assertHeader("Age") + + def test_antistampede(self): + SECONDS = 4 + # We MUST make an initial synchronous request in order to create the + # AntiStampedeCache object, and populate its selecting_headers, + # before the actual stampede. + self.getPage("/long_process?seconds=%d" % SECONDS) + self.assertBody('success!') + self.getPage("/clear_cache?path=" + + quote('/long_process?seconds=%d' % SECONDS, safe='')) + self.assertStatus(200) + + start = datetime.datetime.now() + def run(): + self.getPage("/long_process?seconds=%d" % SECONDS) + # The response should be the same every time + self.assertBody('success!') + ts = [threading.Thread(target=run) for i in xrange(100)] + for t in ts: + t.start() + for t in ts: + t.join() + self.assertEqualDates(start, datetime.datetime.now(), + # Allow a second (two, for slow hosts) + # for our thread/TCP overhead etc. + seconds=SECONDS + 2) + + def test_cache_control(self): + self.getPage("/control") + self.assertBody('visit #1') + self.getPage("/control") + self.assertBody('visit #1') + + self.getPage("/control", headers=[('Cache-Control', 'no-cache')]) + self.assertBody('visit #2') + self.getPage("/control") + self.assertBody('visit #2') + + self.getPage("/control", headers=[('Pragma', 'no-cache')]) + self.assertBody('visit #3') + self.getPage("/control") + self.assertBody('visit #3') + + time.sleep(1) + self.getPage("/control", headers=[('Cache-Control', 'max-age=0')]) + self.assertBody('visit #4') + self.getPage("/control") + self.assertBody('visit #4') + diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py new file mode 100644 index 0000000..b1ef6a3 --- /dev/null +++ b/cherrypy/test/test_config.py @@ -0,0 +1,256 @@ +"""Tests for the CherryPy configuration system.""" + +import os, sys +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +from cherrypy._cpcompat import ntob, StringIO +import unittest + +import cherrypy + +def setup_server(): + + class Root: + + _cp_config = {'foo': 'this', + 'bar': 'that'} + + def __init__(self): + cherrypy.config.namespaces['db'] = self.db_namespace + + def db_namespace(self, k, v): + if k == "scheme": + self.db = v + + # @cherrypy.expose(alias=('global_', 'xyz')) + def index(self, key): + return cherrypy.request.config.get(key, "None") + index = cherrypy.expose(index, alias=('global_', 'xyz')) + + def repr(self, key): + return repr(cherrypy.request.config.get(key, None)) + repr.exposed = True + + def dbscheme(self): + return self.db + dbscheme.exposed = True + + def plain(self, x): + return x + plain.exposed = True + plain._cp_config = {'request.body.attempt_charsets': ['utf-16']} + + favicon_ico = cherrypy.tools.staticfile.handler( + filename=os.path.join(localDir, '../favicon.ico')) + + class Foo: + + _cp_config = {'foo': 'this2', + 'baz': 'that2'} + + def index(self, key): + return cherrypy.request.config.get(key, "None") + index.exposed = True + nex = index + + def silly(self): + return 'Hello world' + silly.exposed = True + silly._cp_config = {'response.headers.X-silly': 'sillyval'} + + # Test the expose and config decorators + #@cherrypy.expose + #@cherrypy.config(foo='this3', **{'bax': 'this4'}) + def bar(self, key): + return repr(cherrypy.request.config.get(key, None)) + bar.exposed = True + bar._cp_config = {'foo': 'this3', 'bax': 'this4'} + + class Another: + + def index(self, key): + return str(cherrypy.request.config.get(key, "None")) + index.exposed = True + + + def raw_namespace(key, value): + if key == 'input.map': + handler = cherrypy.request.handler + def wrapper(): + params = cherrypy.request.params + for name, coercer in list(value.items()): + try: + params[name] = coercer(params[name]) + except KeyError: + pass + return handler() + cherrypy.request.handler = wrapper + elif key == 'output': + handler = cherrypy.request.handler + def wrapper(): + # 'value' is a type (like int or str). + return value(handler()) + cherrypy.request.handler = wrapper + + class Raw: + + _cp_config = {'raw.output': repr} + + def incr(self, num): + return num + 1 + incr.exposed = True + incr._cp_config = {'raw.input.map': {'num': int}} + + ioconf = StringIO(""" +[/] +neg: -1234 +filename: os.path.join(sys.prefix, "hello.py") +thing1: cherrypy.lib.httputil.response_codes[404] +thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 +complex: 3+2j +mul: 6*3 +ones: "11" +twos: "22" +stradd: %%(ones)s + %%(twos)s + "33" + +[/favicon.ico] +tools.staticfile.filename = %r +""" % os.path.join(localDir, 'static/dirback.jpg')) + + root = Root() + root.foo = Foo() + root.raw = Raw() + app = cherrypy.tree.mount(root, config=ioconf) + app.request_class.namespaces['raw'] = raw_namespace + + cherrypy.tree.mount(Another(), "/another") + cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', + 'db.scheme': r"sqlite///memory", + }) + + +# Client-side code # + +from cherrypy.test import helper + +class ConfigTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testConfig(self): + tests = [ + ('/', 'nex', 'None'), + ('/', 'foo', 'this'), + ('/', 'bar', 'that'), + ('/xyz', 'foo', 'this'), + ('/foo/', 'foo', 'this2'), + ('/foo/', 'bar', 'that'), + ('/foo/', 'bax', 'None'), + ('/foo/bar', 'baz', "'that2'"), + ('/foo/nex', 'baz', 'that2'), + # If 'foo' == 'this', then the mount point '/another' leaks into '/'. + ('/another/','foo', 'None'), + ] + for path, key, expected in tests: + self.getPage(path + "?key=" + key) + self.assertBody(expected) + + expectedconf = { + # From CP defaults + 'tools.log_headers.on': False, + 'tools.log_tracebacks.on': True, + 'request.show_tracebacks': True, + 'log.screen': False, + 'environment': 'test_suite', + 'engine.autoreload_on': False, + # From global config + 'luxuryyacht': 'throatwobblermangrove', + # From Root._cp_config + 'bar': 'that', + # From Foo._cp_config + 'baz': 'that2', + # From Foo.bar._cp_config + 'foo': 'this3', + 'bax': 'this4', + } + for key, expected in expectedconf.items(): + self.getPage("/foo/bar?key=" + key) + self.assertBody(repr(expected)) + + def testUnrepr(self): + self.getPage("/repr?key=neg") + self.assertBody("-1234") + + self.getPage("/repr?key=filename") + self.assertBody(repr(os.path.join(sys.prefix, "hello.py"))) + + self.getPage("/repr?key=thing1") + self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) + + if not getattr(cherrypy.server, "using_apache", False): + # The object ID's won't match up when using Apache, since the + # server and client are running in different processes. + self.getPage("/repr?key=thing2") + from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) + + self.getPage("/repr?key=complex") + self.assertBody("(3+2j)") + + self.getPage("/repr?key=mul") + self.assertBody("18") + + self.getPage("/repr?key=stradd") + self.assertBody(repr("112233")) + + def testRespNamespaces(self): + self.getPage("/foo/silly") + self.assertHeader('X-silly', 'sillyval') + self.assertBody('Hello world') + + def testCustomNamespaces(self): + self.getPage("/raw/incr?num=12") + self.assertBody("13") + + self.getPage("/dbscheme") + self.assertBody(r"sqlite///memory") + + def testHandlerToolConfigOverride(self): + # Assert that config overrides tool constructor args. Above, we set + # the favicon in the page handler to be '../favicon.ico', + # but then overrode it in config to be './static/dirback.jpg'. + self.getPage("/favicon.ico") + self.assertBody(open(os.path.join(localDir, "static/dirback.jpg"), + "rb").read()) + + def test_request_body_namespace(self): + self.getPage("/plain", method='POST', headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', '13')], + body=ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')) + self.assertBody("abc") + + +class VariableSubstitutionTests(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_config(self): + from textwrap import dedent + + # variable substitution with [DEFAULT] + conf = dedent(""" + [DEFAULT] + dir = "/some/dir" + my.dir = %(dir)s + "/sub" + + [my] + my.dir = %(dir)s + "/my/dir" + my.dir2 = %(my.dir)s + '/dir2' + + """) + + fp = StringIO(conf) + + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir") + self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2") + diff --git a/cherrypy/test/test_config_server.py b/cherrypy/test/test_config_server.py new file mode 100644 index 0000000..0b9718d --- /dev/null +++ b/cherrypy/test/test_config_server.py @@ -0,0 +1,121 @@ +"""Tests for the CherryPy configuration system.""" + +import os, sys +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import socket +import time + +import cherrypy + + +# Client-side code # + +from cherrypy.test import helper + +class ServerConfigTests(helper.CPWebCase): + + def setup_server(): + + class Root: + def index(self): + return cherrypy.request.wsgi_environ['SERVER_PORT'] + index.exposed = True + + def upload(self, file): + return "Size: %s" % len(file.file.read()) + upload.exposed = True + + def tinyupload(self): + return cherrypy.request.body.read() + tinyupload.exposed = True + tinyupload._cp_config = {'request.body.maxbytes': 100} + + cherrypy.tree.mount(Root()) + + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': 9876, + 'server.max_request_body_size': 200, + 'server.max_request_header_size': 500, + 'server.socket_timeout': 0.5, + + # Test explicit server.instance + 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', + 'server.2.socket_port': 9877, + + # Test non-numeric + # Also test default server.instance = builtin server + 'server.yetanother.socket_port': 9878, + }) + setup_server = staticmethod(setup_server) + + PORT = 9876 + + def testBasicConfig(self): + self.getPage("/") + self.assertBody(str(self.PORT)) + + def testAdditionalServers(self): + if self.scheme == 'https': + return self.skip("not available under ssl") + self.PORT = 9877 + self.getPage("/") + self.assertBody(str(self.PORT)) + self.PORT = 9878 + self.getPage("/") + self.assertBody(str(self.PORT)) + + def testMaxRequestSizePerHandler(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + self.getPage('/tinyupload', method="POST", + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '100')], + body="x" * 100) + self.assertStatus(200) + self.assertBody("x" * 100) + + self.getPage('/tinyupload', method="POST", + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '101')], + body="x" * 101) + self.assertStatus(413) + + def testMaxRequestSize(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + for size in (500, 5000, 50000): + self.getPage("/", headers=[('From', "x" * 500)]) + self.assertStatus(413) + + # Test for http://www.cherrypy.org/ticket/421 + # (Incorrect border condition in readline of SizeCheckWrapper). + # This hangs in rev 891 and earlier. + lines256 = "x" * 248 + self.getPage("/", + headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), + ('From', lines256)]) + + # Test upload + body = '\r\n'.join([ + '--x', + 'Content-Disposition: form-data; name="file"; filename="hello.txt"', + 'Content-Type: text/plain', + '', + '%s', + '--x--']) + partlen = 200 - len(body) + b = body % ("x" * partlen) + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b))] + self.getPage('/upload', h, "POST", b) + self.assertBody('Size: %d' % partlen) + + b = body % ("x" * 200) + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b))] + self.getPage('/upload', h, "POST", b) + self.assertStatus(413) + diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py new file mode 100644 index 0000000..1346f59 --- /dev/null +++ b/cherrypy/test/test_conn.py @@ -0,0 +1,734 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import socket +import sys +import time +timeout = 1 + + +import cherrypy +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine +from cherrypy._cpcompat import ntob, urlopen, unicodestr +from cherrypy.test import webtest +from cherrypy import _cperror + + +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + +def setup_server(): + + def raise500(): + raise cherrypy.HTTPError(500) + + class Root: + + def index(self): + return pov + index.exposed = True + page1 = index + page2 = index + page3 = index + + def hello(self): + return "Hello, world!" + hello.exposed = True + + def timeout(self, t): + return str(cherrypy.server.httpserver.timeout) + timeout.exposed = True + + def stream(self, set_cl=False): + if set_cl: + cherrypy.response.headers['Content-Length'] = 10 + + def content(): + for x in range(10): + yield str(x) + + return content() + stream.exposed = True + stream._cp_config = {'response.stream': True} + + def error(self, code=500): + raise cherrypy.HTTPError(code) + error.exposed = True + + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % cherrypy.request.body.read() + upload.exposed = True + + def custom(self, response_code): + cherrypy.response.status = response_code + return "Code = %s" % response_code + custom.exposed = True + + def err_before_read(self): + return "ok" + err_before_read.exposed = True + err_before_read._cp_config = {'hooks.on_start_resource': raise500} + + def one_megabyte_of_a(self): + return ["a" * 1024] * 1024 + one_megabyte_of_a.exposed = True + + def custom_cl(self, body, cl): + cherrypy.response.headers['Content-Length'] = cl + if not isinstance(body, list): + body = [body] + newbody = [] + for chunk in body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode('ISO-8859-1') + newbody.append(chunk) + return newbody + custom_cl.exposed = True + # Turn off the encoding tool so it doens't collapse + # our response body and reclaculate the Content-Length. + custom_cl._cp_config = {'tools.encode.on': False} + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': timeout, + }) + + +from cherrypy.test import helper + +class ConnectionCloseTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make another request on the same connection. + self.getPage("/page1") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Test client-side close. + self.getPage("/page2", headers=[("Connection", "close")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "close") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + def test_Streaming_no_len(self): + self._streaming(set_cl=False) + + def test_Streaming_with_len(self): + self._streaming(set_cl=True) + + def _streaming(self, set_cl): + if cherrypy.server.protocol_version == "HTTP/1.1": + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + self.getPage("/stream?set_cl=Yes") + self.assertHeader("Content-Length") + self.assertNoHeader("Connection", "close") + self.assertNoHeader("Transfer-Encoding") + + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + self.getPage("/stream") + self.assertNoHeader("Content-Length") + self.assertStatus('200 OK') + self.assertBody('0123456789') + + chunked_response = False + for k, v in self.headers: + if k.lower() == "transfer-encoding": + if str(v) == "chunked": + chunked_response = True + + if chunked_response: + self.assertNoHeader("Connection", "close") + else: + self.assertHeader("Connection", "close") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + # Try HEAD. See http://www.cherrypy.org/ticket/864. + self.getPage("/stream", method='HEAD') + self.assertStatus('200 OK') + self.assertBody('') + self.assertNoHeader("Transfer-Encoding") + else: + self.PROTOCOL = "HTTP/1.0" + + self.persistent = True + + # Make the first request and assert Keep-Alive. + self.getPage("/", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "Keep-Alive") + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + self.getPage("/stream?set_cl=Yes", + headers=[("Connection", "Keep-Alive")]) + self.assertHeader("Content-Length") + self.assertHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When a Content-Length is not provided, + # the server should close the connection. + self.getPage("/stream", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody('0123456789') + + self.assertNoHeader("Content-Length") + self.assertNoHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + def test_HTTP10_KeepAlive(self): + self.PROTOCOL = "HTTP/1.0" + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a normal HTTP/1.0 request. + self.getPage("/page2") + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 +## self.assertNoHeader("Connection") + + # Test a keep-alive HTTP/1.0 request. + self.persistent = True + + self.getPage("/page3", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "Keep-Alive") + + # Remove the keep-alive header again. + self.getPage("/page3") + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 +## self.assertNoHeader("Connection") + + +class PipelineTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11_Timeout(self): + # If we timeout without sending any data, + # the server will close the conn with a 408. + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Connect but send nothing. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + # Connect but send half the headers only. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + conn.send(ntob('GET /hello HTTP/1.1')) + conn.send(("Host: %s" % self.HOST).encode('ascii')) + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The conn should have already sent 408. + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + def test_HTTP11_Timeout_after_request(self): + # If we timeout after at least one request has succeeded, + # the server will close the conn without 408. + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(str(timeout)) + + # Make a second request on the same socket + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody("Hello, world!") + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + except: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % + response.read()) + + conn.close() + + # Make another request on a new socket, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + + + # Make another request on the same socket, + # but timeout on the headers + conn.send(ntob('GET /hello HTTP/1.1')) + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + except: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % sys.exc_info()[1]) + else: + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % + response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + conn.close() + + def test_HTTP11_pipelining(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Test pipelining. httplib doesn't support this directly. + self.persistent = True + conn = self.HTTP_CONN + + # Put request 1 + conn.putrequest("GET", "/hello", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method="GET") + response.begin() + body = response.read(13) + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello, world!")) + + # Retrieve final response + response = conn.response_class(conn.sock, method="GET") + response.begin() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello, world!")) + + conn.close() + + def test_100_Continue(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + conn = self.HTTP_CONN + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "4") + conn.endheaders() + conn.send(ntob("d'oh")) + response = conn.response_class(conn.sock, method="POST") + version, status, reason = response._read_status() + self.assertNotEqual(status, 100) + conn.close() + + # Now try a page with an Expect header... + conn.connect() + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "17") + conn.putheader("Expect", "100-continue") + conn.endheaders() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + line = response.fp.readline().strip() + if line: + self.fail("100 Continue should not output any headers. Got %r" % line) + else: + break + + # ...send the body + body = ntob("I am a small file") + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + +class ConnectionTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_readall_or_close(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a max of 0 (the default) and then reset to what it was above. + old_max = cherrypy.server.max_request_body_size + for new_max in (0, old_max): + cherrypy.server.max_request_body_size = new_max + + self.persistent = True + conn = self.HTTP_CONN + + # Get a POST page with an error + conn.putrequest("POST", "/err_before_read", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "1000") + conn.putheader("Expect", "100-continue") + conn.endheaders() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + conn.send(ntob("x" * 1000)) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + + # Now try a working page with an Expect header... + conn._output(ntob('POST /upload HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._output(ntob("Content-Type: text/plain")) + conn._output(ntob("Content-Length: 17")) + conn._output(ntob("Expect: 100-continue")) + conn._send_output() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + body = ntob("I am a small file") + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + def test_No_Message_Body(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make a 204 request on the same connection. + self.getPage("/custom/204") + self.assertStatus(204) + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") + + # Make a 304 request on the same connection. + self.getPage("/custom/304") + self.assertStatus(304) + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") + + def test_Chunked_Encoding(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + if (hasattr(self, 'harness') and + "modpython" in self.harness.__class__.__name__.lower()): + # mod_python forbids chunked encoding + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + conn = self.HTTP_CONN + + # Try a normal chunked request (with extensions) + body = ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n" + "Content-Type: application/json\r\n" + "\r\n") + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Trailer", "Content-Type") + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader("Content-Length", "3") + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus('200 OK') + self.assertBody("thanks for '%s'" % ntob('xx\r\nxxxxyyyyy')) + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n") + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Content-Type", "text/plain") + # Chunked requests don't need a content-length +## conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + conn.close() + + def test_Content_Length_in(self): + # Try a non-chunked request where Content-Length exceeds + # server.max_request_body_size. Assert error before body send. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "9999") + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + self.assertBody("The entity sent with the request exceeds " + "the maximum allowed bytes.") + conn.close() + + def test_Content_Length_out_preheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/custom_cl?body=I+have+too+many+bytes&cl=5", + skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + self.assertBody( + "The requested resource returned more bytes than the " + "declared Content-Length.") + conn.close() + + def test_Content_Length_out_postheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5", + skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("I too") + conn.close() + + def test_598(self): + remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' % + (self.scheme, self.HOST, self.PORT,)) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + else: + buf += data + remaining -= len(data) + + self.assertEqual(len(buf), 1024 * 1024) + self.assertEqual(buf, ntob("a" * 1024 * 1024)) + self.assertEqual(remaining, 0) + remote_data_conn.close() + + +class BadRequestTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_No_CRLF(self): + self.persistent = True + + conn = self.HTTP_CONN + conn.send(ntob('GET /hello HTTP/1.1\n\n')) + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.body = response.read() + self.assertBody("HTTP requires CRLF terminators") + conn.close() + + conn.connect() + conn.send(ntob('GET /hello HTTP/1.1\r\n\n')) + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.body = response.read() + self.assertBody("HTTP requires CRLF terminators") + conn.close() + diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py new file mode 100644 index 0000000..0956a94 --- /dev/null +++ b/cherrypy/test/test_core.py @@ -0,0 +1,688 @@ +"""Basic tests for the CherryPy core: request handling.""" + +import os +localDir = os.path.dirname(__file__) +import sys +import types + +import cherrypy +from cherrypy._cpcompat import IncompleteRead, itervalues, ntob +from cherrypy import _cptools, tools +from cherrypy.lib import httputil, static + + +favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico") + +# Client-side code # + +from cherrypy.test import helper + +class CoreRequestHandlingTest(helper.CPWebCase): + + def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + favicon_ico = tools.staticfile.handler(filename=favicon_path) + + def defct(self, newct): + newct = "text/%s" % newct + cherrypy.config.update({'tools.response_headers.on': True, + 'tools.response_headers.headers': + [('Content-Type', newct)]}) + defct.exposed = True + + def baseurl(self, path_info, relative=None): + return cherrypy.url(path_info, relative=bool(relative)) + baseurl.exposed = True + + root = Root() + + if sys.version_info >= (2, 5): + from cherrypy.test._test_decorators import ExposeExamples + root.expose_dec = ExposeExamples() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in itervalues(dct): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object, ), {}) + + + class URL(Test): + + _cp_config = {'tools.trailing_slash.on': False} + + def index(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def leaf(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + + def log_status(): + Status.statuses.append(cherrypy.response.status) + cherrypy.tools.log_status = cherrypy.Tool('on_end_resource', log_status) + + + class Status(Test): + + def index(self): + return "normal" + + def blank(self): + cherrypy.response.status = "" + + # According to RFC 2616, new status codes are OK as long as they + # are between 100 and 599. + + # Here is an illegal code... + def illegal(self): + cherrypy.response.status = 781 + return "oops" + + # ...and here is an unknown but legal code. + def unknown(self): + cherrypy.response.status = "431 My custom error" + return "funky" + + # Non-numeric code + def bad(self): + cherrypy.response.status = "error" + return "bad news" + + statuses = [] + def on_end_resource_stage(self): + return repr(self.statuses) + on_end_resource_stage._cp_config = {'tools.log_status.on': True} + + + class Redirect(Test): + + class Error: + _cp_config = {"tools.err_redirect.on": True, + "tools.err_redirect.url": "/errpage", + "tools.err_redirect.internal": False, + } + + def index(self): + raise NameError("redirect_test") + index.exposed = True + error = Error() + + def index(self): + return "child" + + def custom(self, url, code): + raise cherrypy.HTTPRedirect(url, code) + + def by_code(self, code): + raise cherrypy.HTTPRedirect("somewhere%20else", code) + by_code._cp_config = {'tools.trailing_slash.extra': True} + + def nomodify(self): + raise cherrypy.HTTPRedirect("", 304) + + def proxy(self): + raise cherrypy.HTTPRedirect("proxy", 305) + + def stringify(self): + return str(cherrypy.HTTPRedirect("/")) + + def fragment(self, frag): + raise cherrypy.HTTPRedirect("/some/url#%s" % frag) + + def login_redir(): + if not getattr(cherrypy.request, "login", None): + raise cherrypy.InternalRedirect("/internalredirect/login") + tools.login_redir = _cptools.Tool('before_handler', login_redir) + + def redir_custom(): + raise cherrypy.InternalRedirect("/internalredirect/custom_err") + + class InternalRedirect(Test): + + def index(self): + raise cherrypy.InternalRedirect("/") + + def choke(self): + return 3 / 0 + choke.exposed = True + choke._cp_config = {'hooks.before_error_response': redir_custom} + + def relative(self, a, b): + raise cherrypy.InternalRedirect("cousin?t=6") + + def cousin(self, t): + assert cherrypy.request.prev.closed + return cherrypy.request.prev.query_string + + def petshop(self, user_id): + if user_id == "parrot": + # Trade it for a slug when redirecting + raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug') + elif user_id == "terrier": + # Trade it for a fish when redirecting + raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish') + else: + # This should pass the user_id through to getImagesByUser + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=%s' % str(user_id)) + + # We support Python 2.3, but the @-deco syntax would look like this: + # @tools.login_redir() + def secure(self): + return "Welcome!" + secure = tools.login_redir()(secure) + # Since calling the tool returns the same function you pass in, + # you could skip binding the return value, and just write: + # tools.login_redir()(secure) + + def login(self): + return "Please log in" + + def custom_err(self): + return "Something went horribly wrong." + + def early_ir(self, arg): + return "whatever" + early_ir._cp_config = {'hooks.before_request_body': redir_custom} + + + class Image(Test): + + def getImagesByUser(self, user_id): + return "0 images for %s" % user_id + + + class Flatten(Test): + + def as_string(self): + return "content" + + def as_list(self): + return ["con", "tent"] + + def as_yield(self): + yield ntob("content") + + def as_dblyield(self): + yield self.as_yield() + as_dblyield._cp_config = {'tools.flatten.on': True} + + def as_refyield(self): + for chunk in self.as_yield(): + yield chunk + + + class Ranges(Test): + + def get_ranges(self, bytes): + return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) + + def slice_file(self): + path = os.path.join(os.getcwd(), os.path.dirname(__file__)) + return static.serve_file(os.path.join(path, "static/index.html")) + + + class Cookies(Test): + + def single(self, name): + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def multiple(self, names): + for name in names: + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def append_headers(header_list, debug=False): + if debug: + cherrypy.log( + "Extending response headers with %s" % repr(header_list), + "TOOLS.APPEND_HEADERS") + cherrypy.serving.response.header_list.extend(header_list) + cherrypy.tools.append_headers = cherrypy.Tool('on_end_resource', append_headers) + + class MultiHeader(Test): + + def header_list(self): + pass + header_list = cherrypy.tools.append_headers(header_list=[ + (ntob('WWW-Authenticate'), ntob('Negotiate')), + (ntob('WWW-Authenticate'), ntob('Basic realm="foo"')), + ])(header_list) + + def commas(self): + cherrypy.response.headers['WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + + + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + + def testStatus(self): + self.getPage("/status/") + self.assertBody('normal') + self.assertStatus(200) + + self.getPage("/status/blank") + self.assertBody('') + self.assertStatus(200) + + self.getPage("/status/illegal") + self.assertStatus(500) + msg = "Illegal response status from server (781 is out of range)." + self.assertErrorPage(500, msg) + + if not getattr(cherrypy.server, 'using_apache', False): + self.getPage("/status/unknown") + self.assertBody('funky') + self.assertStatus(431) + + self.getPage("/status/bad") + self.assertStatus(500) + msg = "Illegal response status from server ('error' is non-numeric)." + self.assertErrorPage(500, msg) + + def test_on_end_resource_status(self): + self.getPage('/status/on_end_resource_stage') + self.assertBody('[]') + self.getPage('/status/on_end_resource_stage') + self.assertBody(repr(["200 OK"])) + + def testSlashes(self): + # Test that requests for index methods without a trailing slash + # get redirected to the same URI path with a trailing slash. + # Make sure GET params are preserved. + self.getPage("/redirect?id=3") + self.assertStatus(301) + self.assertInBody("" + "%s/redirect/?id=3" % (self.base(), self.base())) + + if self.prefix(): + # Corner case: the "trailing slash" redirect could be tricky if + # we're using a virtual root and the URI is "/vroot" (no slash). + self.getPage("") + self.assertStatus(301) + self.assertInBody("%s/" % + (self.base(), self.base())) + + # Test that requests for NON-index methods WITH a trailing slash + # get redirected to the same URI path WITHOUT a trailing slash. + # Make sure GET params are preserved. + self.getPage("/redirect/by_code/?code=307") + self.assertStatus(301) + self.assertInBody("" + "%s/redirect/by_code?code=307" + % (self.base(), self.base())) + + # If the trailing_slash tool is off, CP should just continue + # as if the slashes were correct. But it needs some help + # inside cherrypy.url to form correct output. + self.getPage('/url?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + + def testRedirect(self): + self.getPage("/redirect/") + self.assertBody('child') + self.assertStatus(200) + + self.getPage("/redirect/by_code?code=300") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(300) + + self.getPage("/redirect/by_code?code=301") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(301) + + self.getPage("/redirect/by_code?code=302") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(302) + + self.getPage("/redirect/by_code?code=303") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(303) + + self.getPage("/redirect/by_code?code=307") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(307) + + self.getPage("/redirect/nomodify") + self.assertBody('') + self.assertStatus(304) + + self.getPage("/redirect/proxy") + self.assertBody('') + self.assertStatus(305) + + # HTTPRedirect on error + self.getPage("/redirect/error/") + self.assertStatus(('302 Found', '303 See Other')) + self.assertInBody('/errpage') + + # Make sure str(HTTPRedirect()) works. + self.getPage("/redirect/stringify", protocol="HTTP/1.0") + self.assertStatus(200) + self.assertBody("(['%s/'], 302)" % self.base()) + if cherrypy.server.protocol_version == "HTTP/1.1": + self.getPage("/redirect/stringify", protocol="HTTP/1.1") + self.assertStatus(200) + self.assertBody("(['%s/'], 303)" % self.base()) + + # check that #fragments are handled properly + # http://skrb.org/ietf/http_errata.html#location-fragments + frag = "foo" + self.getPage("/redirect/fragment/%s" % frag) + self.assertMatchesBody(r"\1\/some\/url\#%s" % (frag, frag)) + loc = self.assertHeader('Location') + assert loc.endswith("#%s" % frag) + self.assertStatus(('302 Found', '303 See Other')) + + # check injection protection + # See http://www.cherrypy.org/ticket/1003 + self.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval") + self.assertStatus(303) + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') + + def test_InternalRedirect(self): + # InternalRedirect + self.getPage("/internalredirect/") + self.assertBody('hello') + self.assertStatus(200) + + # Test passthrough + self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film") + self.assertBody('0 images for Sir-not-appearing-in-this-film') + self.assertStatus(200) + + # Test args + self.getPage("/internalredirect/petshop?user_id=parrot") + self.assertBody('0 images for slug') + self.assertStatus(200) + + # Test POST + self.getPage("/internalredirect/petshop", method="POST", + body="user_id=terrier") + self.assertBody('0 images for fish') + self.assertStatus(200) + + # Test ir before body read + self.getPage("/internalredirect/early_ir", method="POST", + body="arg=aha!") + self.assertBody("Something went horribly wrong.") + self.assertStatus(200) + + self.getPage("/internalredirect/secure") + self.assertBody('Please log in') + self.assertStatus(200) + + # Relative path in InternalRedirect. + # Also tests request.prev. + self.getPage("/internalredirect/relative?a=3&b=5") + self.assertBody("a=3&b=5") + self.assertStatus(200) + + # InternalRedirect on error + self.getPage("/internalredirect/choke") + self.assertStatus(200) + self.assertBody("Something went horribly wrong.") + + def testFlatten(self): + for url in ["/flatten/as_string", "/flatten/as_list", + "/flatten/as_yield", "/flatten/as_dblyield", + "/flatten/as_refyield"]: + self.getPage(url) + self.assertBody('content') + + def testRanges(self): + self.getPage("/ranges/get_ranges?bytes=3-6") + self.assertBody("[(3, 7)]") + + # Test multiple ranges and a suffix-byte-range-spec, for good measure. + self.getPage("/ranges/get_ranges?bytes=2-4,-1") + self.assertBody("[(2, 5), (7, 8)]") + + # Get a partial file. + if cherrypy.server.protocol_version == "HTTP/1.1": + self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) + self.assertStatus(206) + self.assertHeader("Content-Type", "text/html;charset=utf-8") + self.assertHeader("Content-Range", "bytes 2-5/14") + self.assertBody("llo,") + + # What happens with overlapping ranges (and out of order, too)? + self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')]) + self.assertStatus(206) + ct = self.assertHeader("Content-Type") + expected_type = "multipart/byteranges; boundary=" + self.assert_(ct.startswith(expected_type)) + boundary = ct[len(expected_type):] + expected_body = ("\r\n--%s\r\n" + "Content-type: text/html\r\n" + "Content-range: bytes 4-6/14\r\n" + "\r\n" + "o, \r\n" + "--%s\r\n" + "Content-type: text/html\r\n" + "Content-range: bytes 2-5/14\r\n" + "\r\n" + "llo,\r\n" + "--%s--\r\n" % (boundary, boundary, boundary)) + self.assertBody(expected_body) + self.assertHeader("Content-Length") + + # Test "416 Requested Range Not Satisfiable" + self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')]) + self.assertStatus(416) + # "When this status code is returned for a byte-range request, + # the response SHOULD include a Content-Range entity-header + # field specifying the current length of the selected resource" + self.assertHeader("Content-Range", "bytes */14") + elif cherrypy.server.protocol_version == "HTTP/1.0": + # Test Range behavior with HTTP/1.0 request + self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) + self.assertStatus(200) + self.assertBody("Hello, world\r\n") + + def testFavicon(self): + # favicon.ico is served by staticfile. + icofilename = os.path.join(localDir, "../favicon.ico") + icofile = open(icofilename, "rb") + data = icofile.read() + icofile.close() + + self.getPage("/favicon.ico") + self.assertBody(data) + + def testCookies(self): + if sys.version_info >= (2, 5): + header_value = lambda x: x + else: + header_value = lambda x: x+';' + + self.getPage("/cookies/single?name=First", + [('Cookie', 'First=Dinsdale;')]) + self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) + + self.getPage("/cookies/multiple?names=First&names=Last", + [('Cookie', 'First=Dinsdale; Last=Piranha;'), + ]) + self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) + self.assertHeader('Set-Cookie', header_value('Last=Piranha')) + + self.getPage("/cookies/single?name=Something-With:Colon", + [('Cookie', 'Something-With:Colon=some-value')]) + self.assertStatus(400) + + def testDefaultContentType(self): + self.getPage('/') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.getPage('/defct/plain') + self.getPage('/') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.getPage('/defct/html') + + def test_multiple_headers(self): + self.getPage('/multiheader/header_list') + self.assertEqual([(k, v) for k, v in self.headers if k == 'WWW-Authenticate'], + [('WWW-Authenticate', 'Negotiate'), + ('WWW-Authenticate', 'Basic realm="foo"'), + ]) + self.getPage('/multiheader/commas') + self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') + + def test_cherrypy_url(self): + # Input relative to current + self.getPage('/url/leaf?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + # Other host header + host = 'www.mydomain.example' + self.getPage('/url/leaf?path_info=page1', + headers=[('Host', host)]) + self.assertBody('%s://%s/url/page1' % (self.scheme, host)) + + # Input is 'absolute'; that is, relative to script_name + self.getPage('/url/leaf?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + + # Single dots + self.getPage('/url/leaf?path_info=./page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/./page1') + self.assertBody('%s/url/other/page1' % self.base()) + self.getPage('/url/?path_info=/other/./page1') + self.assertBody('%s/other/page1' % self.base()) + + # Double dots + self.getPage('/url/leaf?path_info=../page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/../page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=/other/../page1') + self.assertBody('%s/page1' % self.base()) + + # Output relative to current path or script_name + self.getPage('/url/?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=/page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/leaf?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=leaf/page1&relative=True') + self.assertBody('leaf/page1') + self.getPage('/url/leaf?path_info=../page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/?path_info=other/../page1&relative=True') + self.assertBody('page1') + + # Output relative to / + self.getPage('/baseurl?path_info=ab&relative=True') + self.assertBody('ab') + # Output relative to / + self.getPage('/baseurl?path_info=/ab&relative=True') + self.assertBody('ab') + + # absolute-path references ("server-relative") + # Input relative to current + self.getPage('/url/leaf?path_info=page1&relative=server') + self.assertBody('/url/page1') + self.getPage('/url/?path_info=page1&relative=server') + self.assertBody('/url/page1') + # Input is 'absolute'; that is, relative to script_name + self.getPage('/url/leaf?path_info=/page1&relative=server') + self.assertBody('/page1') + self.getPage('/url/?path_info=/page1&relative=server') + self.assertBody('/page1') + + def test_expose_decorator(self): + if not sys.version_info >= (2, 5): + return self.skip("skipped (Python 2.5+ only) ") + + # Test @expose + self.getPage("/expose_dec/no_call") + self.assertStatus(200) + self.assertBody("Mr E. R. Bradshaw") + + # Test @expose() + self.getPage("/expose_dec/call_empty") + self.assertStatus(200) + self.assertBody("Mrs. B.J. Smegma") + + # Test @expose("alias") + self.getPage("/expose_dec/call_alias") + self.assertStatus(200) + self.assertBody("Mr Nesbitt") + # Does the original name work? + self.getPage("/expose_dec/nesbitt") + self.assertStatus(200) + self.assertBody("Mr Nesbitt") + + # Test @expose(["alias1", "alias2"]) + self.getPage("/expose_dec/alias1") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + self.getPage("/expose_dec/alias2") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + # Does the original name work? + self.getPage("/expose_dec/andrews") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + + # Test @expose(alias="alias") + self.getPage("/expose_dec/alias3") + self.assertStatus(200) + self.assertBody("Mr. and Mrs. Watson") + + +class ErrorTests(helper.CPWebCase): + + def setup_server(): + def break_header(): + # Add a header after finalize that is invalid + cherrypy.serving.response.header_list.append((2, 3)) + cherrypy.tools.break_header = cherrypy.Tool('on_end_resource', break_header) + + class Root: + def index(self): + return "hello" + index.exposed = True + + def start_response_error(self): + return "salud!" + start_response_error._cp_config = {'tools.break_header.on': True} + root = Root() + + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + def test_start_response_error(self): + self.getPage("/start_response_error") + self.assertStatus(500) + self.assertInBody("TypeError: response.header_list key 2 is not a byte string.") + diff --git a/cherrypy/test/test_dynamicobjectmapping.py b/cherrypy/test/test_dynamicobjectmapping.py new file mode 100644 index 0000000..0395b7b --- /dev/null +++ b/cherrypy/test/test_dynamicobjectmapping.py @@ -0,0 +1,404 @@ +import cherrypy +from cherrypy._cpcompat import sorted, unicodestr +from cherrypy._cptree import Application +from cherrypy.test import helper + +script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] + + + +def setup_server(): + class SubSubRoot: + def index(self): + return "SubSubRoot index" + index.exposed = True + + def default(self, *args): + return "SubSubRoot default" + default.exposed = True + + def handler(self): + return "SubSubRoot handler" + handler.exposed = True + + def dispatch(self): + return "SubSubRoot dispatch" + dispatch.exposed = True + + subsubnodes = { + '1': SubSubRoot(), + '2': SubSubRoot(), + } + + class SubRoot: + def index(self): + return "SubRoot index" + index.exposed = True + + def default(self, *args): + return "SubRoot %s" % (args,) + default.exposed = True + + def handler(self): + return "SubRoot handler" + handler.exposed = True + + def _cp_dispatch(self, vpath): + return subsubnodes.get(vpath[0], None) + + subnodes = { + '1': SubRoot(), + '2': SubRoot(), + } + class Root: + def index(self): + return "index" + index.exposed = True + + def default(self, *args): + return "default %s" % (args,) + default.exposed = True + + def handler(self): + return "handler" + handler.exposed = True + + def _cp_dispatch(self, vpath): + return subnodes.get(vpath[0]) + + #-------------------------------------------------------------------------- + # DynamicNodeAndMethodDispatcher example. + # This example exposes a fairly naive HTTP api + class User(object): + def __init__(self, id, name): + self.id = id + self.name = name + + def __unicode__(self): + return unicode(self.name) + def __str__(self): + return str(self.name) + + user_lookup = { + 1: User(1, 'foo'), + 2: User(2, 'bar'), + } + + def make_user(name, id=None): + if not id: + id = max(*list(user_lookup.keys())) + 1 + user_lookup[id] = User(id, name) + return id + + class UserContainerNode(object): + exposed = True + + def POST(self, name): + """ + Allow the creation of a new Object + """ + return "POST %d" % make_user(name) + + def GET(self): + return unicodestr(sorted(user_lookup.keys())) + + def dynamic_dispatch(self, vpath): + try: + id = int(vpath[0]) + except (ValueError, IndexError): + return None + return UserInstanceNode(id) + + class UserInstanceNode(object): + exposed = True + def __init__(self, id): + self.id = id + self.user = user_lookup.get(id, None) + + # For all but PUT methods there MUST be a valid user identified + # by self.id + if not self.user and cherrypy.request.method != 'PUT': + raise cherrypy.HTTPError(404) + + def GET(self, *args, **kwargs): + """ + Return the appropriate representation of the instance. + """ + return unicodestr(self.user) + + def POST(self, name): + """ + Update the fields of the user instance. + """ + self.user.name = name + return "POST %d" % self.user.id + + def PUT(self, name): + """ + Create a new user with the specified id, or edit it if it already exists + """ + if self.user: + # Edit the current user + self.user.name = name + return "PUT %d" % self.user.id + else: + # Make a new user with said attributes. + return "PUT %d" % make_user(name, self.id) + + def DELETE(self): + """ + Delete the user specified at the id. + """ + id = self.user.id + del user_lookup[self.user.id] + del self.user + return "DELETE %d" % id + + + class ABHandler: + class CustomDispatch: + def index(self, a, b): + return "custom" + index.exposed = True + + def _cp_dispatch(self, vpath): + """Make sure that if we don't pop anything from vpath, + processing still works. + """ + return self.CustomDispatch() + + def index(self, a, b=None): + body = [ 'a:' + str(a) ] + if b is not None: + body.append(',b:' + str(b)) + return ''.join(body) + index.exposed = True + + def delete(self, a, b): + return 'deleting ' + str(a) + ' and ' + str(b) + delete.exposed = True + + class IndexOnly: + def _cp_dispatch(self, vpath): + """Make sure that popping ALL of vpath still shows the index + handler. + """ + while vpath: + vpath.pop() + return self + + def index(self): + return "IndexOnly index" + index.exposed = True + + class DecoratedPopArgs: + """Test _cp_dispatch with @cherrypy.popargs.""" + def index(self): + return "no params" + index.exposed = True + + def hi(self): + return "hi was not interpreted as 'a' param" + hi.exposed = True + DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs) + + class NonDecoratedPopArgs: + """Test _cp_dispatch = cherrypy.popargs()""" + + _cp_dispatch = cherrypy.popargs('a') + + def index(self, a): + return "index: " + str(a) + index.exposed = True + + class ParameterizedHandler: + """Special handler created for each request""" + + def __init__(self, a): + self.a = a + + def index(self): + if 'a' in cherrypy.request.params: + raise Exception("Parameterized handler argument ended up in request.params") + return self.a + index.exposed = True + + class ParameterizedPopArgs: + """Test cherrypy.popargs() with a function call handler""" + ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs) + + Root.decorated = DecoratedPopArgs() + Root.undecorated = NonDecoratedPopArgs() + Root.index_only = IndexOnly() + Root.parameter_test = ParameterizedPopArgs() + + Root.users = UserContainerNode() + + md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch') + for url in script_names: + conf = {'/': { + 'user': (url or "/").split("/")[-2], + }, + '/users': { + 'request.dispatch': md + }, + } + cherrypy.tree.mount(Root(), url, conf) + +class DynamicObjectMappingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testObjectMapping(self): + for url in script_names: + prefix = self.script_name = url + + self.getPage('/') + self.assertBody('index') + + self.getPage('/handler') + self.assertBody('handler') + + # Dynamic dispatch will succeed here for the subnodes + # so the subroot gets called + self.getPage('/1/') + self.assertBody('SubRoot index') + + self.getPage('/2/') + self.assertBody('SubRoot index') + + self.getPage('/1/handler') + self.assertBody('SubRoot handler') + + self.getPage('/2/handler') + self.assertBody('SubRoot handler') + + # Dynamic dispatch will fail here for the subnodes + # so the default gets called + self.getPage('/asdf/') + self.assertBody("default ('asdf',)") + + self.getPage('/asdf/asdf') + self.assertBody("default ('asdf', 'asdf')") + + self.getPage('/asdf/handler') + self.assertBody("default ('asdf', 'handler')") + + # Dynamic dispatch will succeed here for the subsubnodes + # so the subsubroot gets called + self.getPage('/1/1/') + self.assertBody('SubSubRoot index') + + self.getPage('/2/2/') + self.assertBody('SubSubRoot index') + + self.getPage('/1/1/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/dispatch') + self.assertBody('SubSubRoot dispatch') + + # The exposed dispatch will not be called as a dispatch + # method. + self.getPage('/2/2/foo/foo') + self.assertBody("SubSubRoot default") + + # Dynamic dispatch will fail here for the subsubnodes + # so the SubRoot gets called + self.getPage('/1/asdf/') + self.assertBody("SubRoot ('asdf',)") + + self.getPage('/1/asdf/asdf') + self.assertBody("SubRoot ('asdf', 'asdf')") + + self.getPage('/1/asdf/handler') + self.assertBody("SubRoot ('asdf', 'handler')") + + def testMethodDispatch(self): + # GET acts like a container + self.getPage("/users") + self.assertBody("[1, 2]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to the container URI allows creation + self.getPage("/users", method="POST", body="name=baz") + self.assertBody("POST 3") + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to a specific instanct URI results in a 404 + # as the resource does not exit. + self.getPage("/users/5", method="POST", body="name=baz") + self.assertStatus(404) + + # PUT to a specific instanct URI results in creation + self.getPage("/users/5", method="PUT", body="name=boris") + self.assertBody("PUT 5") + self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT') + + # GET acts like a container + self.getPage("/users") + self.assertBody("[1, 2, 3, 5]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + test_cases = ( + (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'), + (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'), + (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'), + (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'), + ) + for id, name, updatedname, headers in test_cases: + self.getPage("/users/%d" % id) + self.assertBody(name) + self.assertHeader('Allow', headers) + + # Make sure POSTs update already existings resources + self.getPage("/users/%d" % id, method='POST', body="name=%s" % updatedname) + self.assertBody("POST %d" % id) + self.assertHeader('Allow', headers) + + # Make sure PUTs Update already existing resources. + self.getPage("/users/%d" % id, method='PUT', body="name=%s" % updatedname) + self.assertBody("PUT %d" % id) + self.assertHeader('Allow', headers) + + # Make sure DELETES Remove already existing resources. + self.getPage("/users/%d" % id, method='DELETE') + self.assertBody("DELETE %d" % id) + self.assertHeader('Allow', headers) + + + # GET acts like a container + self.getPage("/users") + self.assertBody("[]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + def testVpathDispatch(self): + self.getPage("/decorated/") + self.assertBody("no params") + + self.getPage("/decorated/hi") + self.assertBody("hi was not interpreted as 'a' param") + + self.getPage("/decorated/yo/") + self.assertBody("a:yo") + + self.getPage("/decorated/yo/there/") + self.assertBody("a:yo,b:there") + + self.getPage("/decorated/yo/there/delete") + self.assertBody("deleting yo and there") + + self.getPage("/decorated/yo/there/handled_by_dispatch/") + self.assertBody("custom") + + self.getPage("/undecorated/blah/") + self.assertBody("index: blah") + + self.getPage("/index_only/a/b/c/d/e/f/g/") + self.assertBody("IndexOnly index") + + self.getPage("/parameter_test/argument2/") + self.assertBody("argument2") + diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py new file mode 100644 index 0000000..2d0ce76 --- /dev/null +++ b/cherrypy/test/test_encoding.py @@ -0,0 +1,363 @@ + +import gzip +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou + +europoundUnicode = ntou('\x80\xa3') +sing = ntou("\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?", 'escape') +sing8 = sing.encode('utf-8') +sing16 = sing.encode('utf-16') + + +from cherrypy.test import helper + + +class EncodingTests(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, param): + assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode) + yield europoundUnicode + index.exposed = True + + def mao_zedong(self): + return sing + mao_zedong.exposed = True + + def utf8(self): + return sing8 + utf8.exposed = True + utf8._cp_config = {'tools.encode.encoding': 'utf-8'} + + def cookies_and_headers(self): + # if the headers have non-ascii characters and a cookie has + # any part which is unicode (even ascii), the response + # should not fail. + cherrypy.response.cookie['candy'] = 'bar' + cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org' + cherrypy.response.headers['Some-Header'] = 'My d\xc3\xb6g has fleas' + return 'Any content' + cookies_and_headers.exposed = True + + def reqparams(self, *args, **kwargs): + return ntob(', ').join([": ".join((k, v)).encode('utf8') + for k, v in cherrypy.request.params.items()]) + reqparams.exposed = True + + def nontext(self, *args, **kwargs): + cherrypy.response.headers['Content-Type'] = 'application/binary' + return '\x00\x01\x02\x03' + nontext.exposed = True + nontext._cp_config = {'tools.encode.text_only': False, + 'tools.encode.add_charset': True, + } + + class GZIP: + def index(self): + yield "Hello, world" + index.exposed = True + + def noshow(self): + # Test for ticket #147, where yield showed no exceptions (content- + # encoding was still gzip even though traceback wasn't zipped). + raise IndexError() + yield "Here be dragons" + noshow.exposed = True + # Turn encoding off so the gzip tool is the one doing the collapse. + noshow._cp_config = {'tools.encode.on': False} + + def noshow_stream(self): + # Test for ticket #147, where yield showed no exceptions (content- + # encoding was still gzip even though traceback wasn't zipped). + raise IndexError() + yield "Here be dragons" + noshow_stream.exposed = True + noshow_stream._cp_config = {'response.stream': True} + + class Decode: + def extra_charset(self, *args, **kwargs): + return ', '.join([": ".join((k, v)) + for k, v in cherrypy.request.params.items()]) + extra_charset.exposed = True + extra_charset._cp_config = { + 'tools.decode.on': True, + 'tools.decode.default_encoding': ['utf-16'], + } + + def force_charset(self, *args, **kwargs): + return ', '.join([": ".join((k, v)) + for k, v in cherrypy.request.params.items()]) + force_charset.exposed = True + force_charset._cp_config = { + 'tools.decode.on': True, + 'tools.decode.encoding': 'utf-16', + } + + root = Root() + root.gzip = GZIP() + root.decode = Decode() + cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}}) + setup_server = staticmethod(setup_server) + + def test_query_string_decoding(self): + europoundUtf8 = europoundUnicode.encode('utf-8') + self.getPage(ntob('/?param=') + europoundUtf8) + self.assertBody(europoundUtf8) + + # Encoded utf8 query strings MUST be parsed correctly. + # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX + self.getPage("/reqparams?q=%C2%A3") + # The return value will be encoded as utf8. + self.assertBody(ntob("q: \xc2\xa3")) + + # Query strings that are incorrectly encoded MUST raise 404. + # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX + self.getPage("/reqparams?q=%A3") + self.assertStatus(404) + self.assertErrorPage(404, + "The given query string could not be processed. Query " + "strings for this resource must be encoded with 'utf8'.") + + def test_urlencoded_decoding(self): + # Test the decoding of an application/x-www-form-urlencoded entity. + europoundUtf8 = europoundUnicode.encode('utf-8') + body=ntob("param=") + europoundUtf8 + self.getPage('/', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(europoundUtf8) + + # Encoded utf8 entities MUST be parsed and decoded correctly. + # Here, q is the POUND SIGN U+00A3 encoded in utf8 + body = ntob("q=\xc2\xa3") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # ...and in utf16, which is not in the default attempt_charsets list: + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-16"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # Entities that are incorrectly encoded MUST raise 400. + # Here, q is the POUND SIGN U+00A3 encoded in utf16, but + # the Content-Type incorrectly labels it utf-8. + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertStatus(400) + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['utf-8']") + + def test_decode_tool(self): + # An extra charset should be tried first, and succeed if it matches. + # Here, we add utf-16 as a charset and pass a utf-16 body. + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/decode/extra_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # An extra charset should be tried first, and continue to other default + # charsets if it doesn't match. + # Here, we add utf-16 as a charset but still pass a utf-8 body. + body = ntob("q=\xc2\xa3") + self.getPage('/decode/extra_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # An extra charset should error if force is True and it doesn't match. + # Here, we force utf-16 as a charset but still pass a utf-8 body. + body = ntob("q=\xc2\xa3") + self.getPage('/decode/force_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['utf-16']") + + def test_multipart_decoding(self): + # Test the decoding of a multipart entity when the charset (utf16) is + # explicitly given. + body=ntob('\r\n'.join(['--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("text: ab\xe2\x80\x9cc, submit: Create")) + + def test_multipart_decoding_no_charset(self): + # Test the decoding of a multipart entity when the charset (utf8) is + # NOT explicitly given, but is in the list of charsets to attempt. + body=ntob('\r\n'.join(['--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xe2\x80\x9c', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + 'Create', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("text: \xe2\x80\x9c, submit: Create")) + + def test_multipart_decoding_no_successful_charset(self): + # Test the decoding of a multipart entity when the charset (utf16) is + # NOT explicitly given, and is NOT in the list of charsets to attempt. + body=ntob('\r\n'.join(['--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertStatus(400) + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['us-ascii', 'utf-8']") + + def test_nontext(self): + self.getPage('/nontext') + self.assertHeader('Content-Type', 'application/binary;charset=utf-8') + self.assertBody('\x00\x01\x02\x03') + + def testEncoding(self): + # Default encoding should be utf-8 + self.getPage('/mao_zedong') + self.assertBody(sing8) + + # Ask for utf-16. + self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')]) + self.assertHeader('Content-Type', 'text/html;charset=utf-16') + self.assertBody(sing16) + + # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16 + # should be produced. + self.getPage('/mao_zedong', [('Accept-Charset', + 'iso-8859-1;q=1, utf-16;q=0.5')]) + self.assertBody(sing16) + + # The "*" value should default to our default_encoding, utf-8 + self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')]) + self.assertBody(sing8) + + # Only allow iso-8859-1, which should fail and raise 406. + self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')]) + self.assertStatus("406 Not Acceptable") + self.assertInBody("Your client sent this Accept-Charset header: " + "iso-8859-1, *;q=0. We tried these charsets: " + "iso-8859-1.") + + # Ask for x-mac-ce, which should be unknown. See ticket #569. + self.getPage('/mao_zedong', [('Accept-Charset', + 'us-ascii, ISO-8859-1, x-mac-ce')]) + self.assertStatus("406 Not Acceptable") + self.assertInBody("Your client sent this Accept-Charset header: " + "us-ascii, ISO-8859-1, x-mac-ce. We tried these " + "charsets: ISO-8859-1, us-ascii, x-mac-ce.") + + # Test the 'encoding' arg to encode. + self.getPage('/utf8') + self.assertBody(sing8) + self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')]) + self.assertStatus("406 Not Acceptable") + + def testGzip(self): + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) + zfile.write(ntob("Hello, world")) + zfile.close() + + self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")]) + self.assertInBody(zbuf.getvalue()[:3]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertHeader("Content-Encoding", "gzip") + + # Test when gzip is denied. + self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertNoHeader("Content-Encoding") + self.assertBody("Hello, world") + + self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip;q=0")]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertNoHeader("Content-Encoding") + self.assertBody("Hello, world") + + self.getPage('/gzip/', headers=[("Accept-Encoding", "*;q=0")]) + self.assertStatus(406) + self.assertNoHeader("Content-Encoding") + self.assertErrorPage(406, "identity, gzip") + + # Test for ticket #147 + self.getPage('/gzip/noshow', headers=[("Accept-Encoding", "gzip")]) + self.assertNoHeader('Content-Encoding') + self.assertStatus(500) + self.assertErrorPage(500, pattern="IndexError\n") + + # In this case, there's nothing we can do to deliver a + # readable page, since 1) the gzip header is already set, + # and 2) we may have already written some of the body. + # The fix is to never stream yields when using gzip. + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage('/gzip/noshow_stream', + headers=[("Accept-Encoding", "gzip")]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertInBody('\x1f\x8b\x08\x00') + else: + # The wsgiserver will simply stop sending data, and the HTTP client + # will error due to an incomplete chunk-encoded stream. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + '/gzip/noshow_stream', + headers=[("Accept-Encoding", "gzip")]) + + def test_UnicodeHeaders(self): + self.getPage('/cookies_and_headers') + self.assertBody('Any content') + diff --git a/cherrypy/test/test_etags.py b/cherrypy/test/test_etags.py new file mode 100644 index 0000000..aec1693 --- /dev/null +++ b/cherrypy/test/test_etags.py @@ -0,0 +1,83 @@ +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.test import helper + + +class ETagTest(helper.CPWebCase): + + def setup_server(): + class Root: + def resource(self): + return "Oh wah ta goo Siam." + resource.exposed = True + + def fail(self, code): + code = int(code) + if 300 <= code <= 399: + raise cherrypy.HTTPRedirect([], code) + else: + raise cherrypy.HTTPError(code) + fail.exposed = True + + def unicoded(self): + return ntou('I am a \u1ee4nicode string.', 'escape') + unicoded.exposed = True + # In Python 3, tools.encode is on by default + unicoded._cp_config = {'tools.encode.on': True} + + conf = {'/': {'tools.etags.on': True, + 'tools.etags.autotags': True, + }} + cherrypy.tree.mount(Root(), config=conf) + setup_server = staticmethod(setup_server) + + def test_etags(self): + self.getPage("/resource") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('Oh wah ta goo Siam.') + etag = self.assertHeader('ETag') + + # Test If-Match (both valid and invalid) + self.getPage("/resource", headers=[('If-Match', etag)]) + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "*")]) + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "*")], method="POST") + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "a bogus tag")]) + self.assertStatus("412 Precondition Failed") + + # Test If-None-Match (both valid and invalid) + self.getPage("/resource", headers=[('If-None-Match', etag)]) + self.assertStatus(304) + self.getPage("/resource", method='POST', headers=[('If-None-Match', etag)]) + self.assertStatus("412 Precondition Failed") + self.getPage("/resource", headers=[('If-None-Match', "*")]) + self.assertStatus(304) + self.getPage("/resource", headers=[('If-None-Match', "a bogus tag")]) + self.assertStatus("200 OK") + + def test_errors(self): + self.getPage("/resource") + self.assertStatus(200) + etag = self.assertHeader('ETag') + + # Test raising errors in page handler + self.getPage("/fail/412", headers=[('If-Match', etag)]) + self.assertStatus(412) + self.getPage("/fail/304", headers=[('If-Match', etag)]) + self.assertStatus(304) + self.getPage("/fail/412", headers=[('If-None-Match', "*")]) + self.assertStatus(412) + self.getPage("/fail/304", headers=[('If-None-Match', "*")]) + self.assertStatus(304) + + def test_unicode_body(self): + self.getPage("/unicoded") + self.assertStatus(200) + etag1 = self.assertHeader('ETag') + self.getPage("/unicoded", headers=[('If-Match', etag1)]) + self.assertStatus(200) + self.assertHeader('ETag', etag1) + diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py new file mode 100644 index 0000000..639c6c4 --- /dev/null +++ b/cherrypy/test/test_http.py @@ -0,0 +1,212 @@ +"""Tests for managing HTTP issues (malformed requests, etc).""" + +import errno +import mimetypes +import socket +import sys + +import cherrypy +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob, py3k + + +def encode_multipart_formdata(files): + """Return (content_type, body) ready for httplib.HTTP instance. + + files: a sequence of (name, filename, value) tuples for multipart uploads. + """ + BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$' + L = [] + for key, filename, value in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % + (key, filename)) + ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + L.append('Content-Type: %s' % ct) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = '\r\n'.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + + + +from cherrypy.test import helper + +class HTTPTests(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, *args, **kwargs): + return "Hello world!" + index.exposed = True + + def no_body(self, *args, **kwargs): + return "Hello world!" + no_body.exposed = True + no_body._cp_config = {'request.process_request_body': False} + + def post_multipart(self, file): + """Return a summary ("a * 65536\nb * 65536") of the uploaded file.""" + contents = file.file.read() + summary = [] + curchar = None + count = 0 + for c in contents: + if c == curchar: + count += 1 + else: + if count: + if py3k: curchar = chr(curchar) + summary.append("%s * %d" % (curchar, count)) + count = 1 + curchar = c + if count: + if py3k: curchar = chr(curchar) + summary.append("%s * %d" % (curchar, count)) + return ", ".join(summary) + post_multipart.exposed = True + + cherrypy.tree.mount(Root()) + cherrypy.config.update({'server.max_request_body_size': 30000000}) + setup_server = staticmethod(setup_server) + + def test_no_content_length(self): + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. Even though + # the request is of method POST, this should be OK because we set + # request.process_request_body to False for our handler. + if self.scheme == "https": + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.request("POST", "/no_body") + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(ntob('Hello world!')) + + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + if self.scheme == "https": + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.request("POST", "/") + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(411) + + def test_post_multipart(self): + alphabet = "abcdefghijklmnopqrstuvwxyz" + # generate file contents for a large post + contents = "".join([c * 65536 for c in alphabet]) + + # encode as multipart form data + files=[('file', 'file.txt', contents)] + content_type, body = encode_multipart_formdata(files) + body = body.encode('Latin-1') + + # post file + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.putrequest('POST', '/post_multipart') + c.putheader('Content-Type', content_type) + c.putheader('Content-Length', str(len(body))) + c.endheaders() + c.send(body) + + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(", ".join(["%s * 65536" % c for c in alphabet])) + + def test_malformed_request_line(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences...") + + # Test missing version in Request-Line + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c._output(ntob('GET /')) + c._send_output() + if hasattr(c, 'strict'): + response = c.response_class(c.sock, strict=c.strict, method='GET') + else: + # Python 3.2 removed the 'strict' feature, saying: + # "http.client now always assumes HTTP/1.x compliant servers." + response = c.response_class(c.sock, method='GET') + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) + c.close() + + def test_malformed_header(self): + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See http://www.cherrypy.org/ticket/941 + c._output(ntob('Re, 1.2.3.4#015#012')) + c.endheaders() + + response = c.getresponse() + self.status = str(response.status) + self.assertStatus(400) + self.body = response.fp.read(20) + self.assertBody("Illegal header line.") + + def test_http_over_https(self): + if self.scheme != 'https': + return self.skip("skipped (not running HTTPS)... ") + + # Try connecting without SSL. + conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + self.assertEqual(response.status, 400) + self.body = response.read() + self.assertBody("The client sent a plain HTTP request, but this " + "server only speaks HTTPS on this port.") + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise + + def test_garbage_in(self): + # Connect without SSL regardless of server.scheme + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c._output(ntob('gjkgjklsgjklsgjkljklsg')) + c._send_output() + response = c.response_class(c.sock, method="GET") + try: + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) + c.close() + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise + diff --git a/cherrypy/test/test_httpauth.py b/cherrypy/test/test_httpauth.py new file mode 100644 index 0000000..9d0eecb --- /dev/null +++ b/cherrypy/test/test_httpauth.py @@ -0,0 +1,151 @@ +import cherrypy +from cherrypy._cpcompat import md5, sha, ntob +from cherrypy.lib import httpauth + +from cherrypy.test import helper + +class HTTPAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class DigestProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected2: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + def fetch_users(): + return {'test': 'test'} + + def sha_password_encrypter(password): + return sha(ntob(password)).hexdigest() + + def fetch_password(username): + return sha(ntob('test')).hexdigest() + + conf = {'/digest': {'tools.digest_auth.on': True, + 'tools.digest_auth.realm': 'localhost', + 'tools.digest_auth.users': fetch_users}, + '/basic': {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'localhost', + 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}}, + '/basic2': {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'localhost', + 'tools.basic_auth.users': fetch_password, + 'tools.basic_auth.encrypt': sha_password_encrypter}} + + root = Root() + root.digest = DigestProtected() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage("/basic/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') + + self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')]) + self.assertStatus(401) + + self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + + def testBasic2(self): + self.getPage("/basic2/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') + + self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')]) + self.assertStatus(401) + + self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + + def testDigest(self): + self.getPage("/digest/") + self.assertStatus(401) + + value = None + for k, v in self.headers: + if k.lower() == "www-authenticate": + if v.startswith("Digest"): + value = v + break + + if value is None: + self._handlewebError("Digest authentification scheme was not found") + + value = value[7:] + items = value.split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + missing_msg = "%s is missing" + bad_value_msg = "'%s' was expecting '%s' but found '%s'" + nonce = None + if 'realm' not in tokens: + self._handlewebError(missing_msg % 'realm') + elif tokens['realm'] != '"localhost"': + self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) + if 'nonce' not in tokens: + self._handlewebError(missing_msg % 'nonce') + else: + nonce = tokens['nonce'].strip('"') + if 'algorithm' not in tokens: + self._handlewebError(missing_msg % 'algorithm') + elif tokens['algorithm'] != '"MD5"': + self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) + if 'qop' not in tokens: + self._handlewebError(missing_msg % 'qop') + elif tokens['qop'] != '"auth"': + self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) + + # Test a wrong 'realm' value + base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth = base_auth % (nonce, '', '00000001') + params = httpauth.parseAuthorization(auth) + response = httpauth._computeDigestResponse(params, 'test') + + auth = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth)]) + self.assertStatus(401) + + # Test that must pass + base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth = base_auth % (nonce, '', '00000001') + params = httpauth.parseAuthorization(auth) + response = httpauth._computeDigestResponse(params, 'test') + + auth = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth)]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + diff --git a/cherrypy/test/test_httplib.py b/cherrypy/test/test_httplib.py new file mode 100644 index 0000000..5dc40fd --- /dev/null +++ b/cherrypy/test/test_httplib.py @@ -0,0 +1,29 @@ +"""Tests for cherrypy/lib/httputil.py.""" + +import unittest +from cherrypy.lib import httputil + + +class UtilityTests(unittest.TestCase): + + def test_urljoin(self): + # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO + self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/") + self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi") + self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/") + self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/") + self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/") + self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi") + self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/") + self.assertEqual(httputil.urljoin("/sn", ""), "/sn") + self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/") + self.assertEqual(httputil.urljoin("/", "/pi"), "/pi") + self.assertEqual(httputil.urljoin("/", "/"), "/") + self.assertEqual(httputil.urljoin("/", ""), "/") + self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/") + self.assertEqual(httputil.urljoin("", "/pi"), "/pi") + self.assertEqual(httputil.urljoin("", "/"), "/") + self.assertEqual(httputil.urljoin("", ""), "/") + +if __name__ == '__main__': + unittest.main() diff --git a/cherrypy/test/test_json.py b/cherrypy/test/test_json.py new file mode 100644 index 0000000..a02c076 --- /dev/null +++ b/cherrypy/test/test_json.py @@ -0,0 +1,79 @@ +import cherrypy +from cherrypy.test import helper + +from cherrypy._cpcompat import json + +class JsonTest(helper.CPWebCase): + def setup_server(): + class Root(object): + def plain(self): + return 'hello' + plain.exposed = True + + def json_string(self): + return 'hello' + json_string.exposed = True + json_string._cp_config = {'tools.json_out.on': True} + + def json_list(self): + return ['a', 'b', 42] + json_list.exposed = True + json_list._cp_config = {'tools.json_out.on': True} + + def json_dict(self): + return {'answer': 42} + json_dict.exposed = True + json_dict._cp_config = {'tools.json_out.on': True} + + def json_post(self): + if cherrypy.request.json == [13, 'c']: + return 'ok' + else: + return 'nok' + json_post.exposed = True + json_post._cp_config = {'tools.json_in.on': True} + + root = Root() + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + def test_json_output(self): + if json is None: + self.skip("json not found ") + return + + self.getPage("/plain") + self.assertBody("hello") + + self.getPage("/json_string") + self.assertBody('"hello"') + + self.getPage("/json_list") + self.assertBody('["a", "b", 42]') + + self.getPage("/json_dict") + self.assertBody('{"answer": 42}') + + def test_json_input(self): + if json is None: + self.skip("json not found ") + return + + body = '[13, "c"]' + headers = [('Content-Type', 'application/json'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertBody('ok') + + body = '[13, "c"]' + headers = [('Content-Type', 'text/plain'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertStatus(415, 'Expected an application/json content type') + + body = '[13, -]' + headers = [('Content-Type', 'application/json'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertStatus(400, 'Invalid JSON document') + diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py new file mode 100644 index 0000000..7d506e8 --- /dev/null +++ b/cherrypy/test/test_logging.py @@ -0,0 +1,157 @@ +"""Basic tests for the CherryPy core: request handling.""" + +import os +localDir = os.path.dirname(__file__) + +import cherrypy +from cherrypy._cpcompat import ntob, ntou, py3k + +access_log = os.path.join(localDir, "access.log") +error_log = os.path.join(localDir, "error.log") + +# Some unicode strings. +tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') +erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') + + +def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + def uni_code(self): + cherrypy.request.login = tartaros + cherrypy.request.remote.name = erebos + uni_code.exposed = True + + def slashes(self): + cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1' + slashes.exposed = True + + def whitespace(self): + # User-Agent = "User-Agent" ":" 1*( product | comment ) + # comment = "(" *( ctext | quoted-pair | comment ) ")" + # ctext = + # TEXT = + # LWS = [CRLF] 1*( SP | HT ) + cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)' + whitespace.exposed = True + + def as_string(self): + return "content" + as_string.exposed = True + + def as_yield(self): + yield "content" + as_yield.exposed = True + + def error(self): + raise ValueError() + error.exposed = True + error._cp_config = {'tools.log_tracebacks.on': True} + + root = Root() + + + cherrypy.config.update({'log.error_file': error_log, + 'log.access_file': access_log, + }) + cherrypy.tree.mount(root) + + + +from cherrypy.test import helper, logtest + +class AccessLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = access_log + + def testNormalReturn(self): + self.markLog() + self.getPage("/as_string", + headers=[('Referer', 'http://www.cherrypy.org/'), + ('User-Agent', 'Mozilla/5.0')]) + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + + def testNormalYield(self): + self.markLog() + self.getPage("/as_yield") + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' % + self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""' + % self.prefix()) + + def testEscapedOutput(self): + # Test unicode in access log pieces. + self.markLog() + self.getPage("/uni_code") + self.assertStatus(200) + if py3k: + # The repr of a bytestring in py3k includes a b'' prefix + self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) + else: + self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) + # Test the erebos value. Included inline for your enlightenment. + # Note the 'r' prefix--those backslashes are literals. + self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') + + # Test backslashes in output. + self.markLog() + self.getPage("/slashes") + self.assertStatus(200) + if py3k: + self.assertLog(-1, ntob('"GET /slashed\\path HTTP/1.1"')) + else: + self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') + + # Test whitespace in output. + self.markLog() + self.getPage("/whitespace") + self.assertStatus(200) + # Again, note the 'r' prefix. + self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') + + +class ErrorLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = error_log + + def testTracebacks(self): + # Test that tracebacks get written to the error log. + self.markLog() + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + self.getPage("/error") + self.assertInBody("raise ValueError()") + self.assertLog(0, 'HTTP Traceback (most recent call last):') + self.assertLog(-3, 'raise ValueError()') + finally: + ignore.pop() + diff --git a/cherrypy/test/test_mime.py b/cherrypy/test/test_mime.py new file mode 100644 index 0000000..1605991 --- /dev/null +++ b/cherrypy/test/test_mime.py @@ -0,0 +1,128 @@ +"""Tests for various MIME issues, including the safe_multipart Tool.""" + +import cherrypy +from cherrypy._cpcompat import ntob, ntou, sorted + +def setup_server(): + + class Root: + + def multipart(self, parts): + return repr(parts) + multipart.exposed = True + + def multipart_form_data(self, **kwargs): + return repr(list(sorted(kwargs.items()))) + multipart_form_data.exposed = True + + def flashupload(self, Filedata, Upload, Filename): + return ("Upload: %s, Filename: %s, Filedata: %r" % + (Upload, Filename, Filedata.file.read())) + flashupload.exposed = True + + cherrypy.config.update({'server.max_request_body_size': 0}) + cherrypy.tree.mount(Root()) + + +# Client-side code # + +from cherrypy.test import helper + +class MultipartTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_multipart(self): + text_part = ntou("This is the text version") + html_part = ntou(""" + + + + + + +This is the HTML version + + +""") + body = '\r\n'.join([ + "--123456789", + "Content-Type: text/plain; charset='ISO-8859-1'", + "Content-Transfer-Encoding: 7bit", + "", + text_part, + "--123456789", + "Content-Type: text/html; charset='ISO-8859-1'", + "", + html_part, + "--123456789--"]) + headers = [ + ('Content-Type', 'multipart/mixed; boundary=123456789'), + ('Content-Length', str(len(body))), + ] + self.getPage('/multipart', headers, "POST", body) + self.assertBody(repr([text_part, html_part])) + + def test_multipart_form_data(self): + body='\r\n'.join(['--X', + 'Content-Disposition: form-data; name="foo"', + '', + 'bar', + '--X', + # Test a param with more than one value. + # See http://www.cherrypy.org/ticket/1028 + 'Content-Disposition: form-data; name="baz"', + '', + '111', + '--X', + 'Content-Disposition: form-data; name="baz"', + '', + '333', + '--X--']) + self.getPage('/multipart_form_data', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) + + +class SafeMultipartHandlingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Flash_Upload(self): + headers = [ + ('Accept', 'text/*'), + ('Content-Type', 'multipart/form-data; ' + 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'), + ('User-Agent', 'Shockwave Flash'), + ('Host', 'www.example.com:54583'), + ('Content-Length', '499'), + ('Connection', 'Keep-Alive'), + ('Cache-Control', 'no-cache'), + ] + filedata = ntob('\r\n' + '\r\n' + '\r\n') + body = (ntob( + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; name="Filename"\r\n' + '\r\n' + '.project\r\n' + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; ' + 'name="Filedata"; filename=".project"\r\n' + 'Content-Type: application/octet-stream\r\n' + '\r\n') + + filedata + + ntob('\r\n' + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; name="Upload"\r\n' + '\r\n' + 'Submit Query\r\n' + # Flash apps omit the trailing \r\n on the last line: + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' + )) + self.getPage('/flashupload', headers, "POST", body) + self.assertBody("Upload: Submit Query, Filename: .project, " + "Filedata: %r" % filedata) + diff --git a/cherrypy/test/test_misc_tools.py b/cherrypy/test/test_misc_tools.py new file mode 100644 index 0000000..1dd1429 --- /dev/null +++ b/cherrypy/test/test_misc_tools.py @@ -0,0 +1,207 @@ +import os +localDir = os.path.dirname(__file__) +logfile = os.path.join(localDir, "test_misc_tools.log") + +import cherrypy +from cherrypy import tools + + +def setup_server(): + class Root: + def index(self): + yield "Hello, world" + index.exposed = True + h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')] + tools.response_headers(headers=h)(index) + + def other(self): + return "salut" + other.exposed = True + other._cp_config = { + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [("Content-Language", "fr"), + ('Content-Type', 'text/plain')], + 'tools.log_hooks.on': True, + } + + + class Accept: + _cp_config = {'tools.accept.on': True} + + def index(self): + return 'Atom feed' + index.exposed = True + + # In Python 2.4+, we could use a decorator instead: + # @tools.accept('application/atom+xml') + def feed(self): + return """ + + Unknown Blog +""" + feed.exposed = True + feed._cp_config = {'tools.accept.media': 'application/atom+xml'} + + def select(self): + # We could also write this: mtype = cherrypy.lib.accept.accept(...) + mtype = tools.accept.callable(['text/html', 'text/plain']) + if mtype == 'text/html': + return "

Page Title

" + else: + return "PAGE TITLE" + select.exposed = True + + class Referer: + def accept(self): + return "Accepted!" + accept.exposed = True + reject = accept + + class AutoVary: + def index(self): + # Read a header directly with 'get' + ae = cherrypy.request.headers.get('Accept-Encoding') + # Read a header directly with '__getitem__' + cl = cherrypy.request.headers['Host'] + # Read a header directly with '__contains__' + hasif = 'If-Modified-Since' in cherrypy.request.headers + # Read a header directly with 'has_key' + if hasattr(dict, 'has_key'): + # Python 2 + has = cherrypy.request.headers.has_key('Range') + else: + # Python 3 + has = 'Range' in cherrypy.request.headers + # Call a lib function + mtype = tools.accept.callable(['text/html', 'text/plain']) + return "Hello, world!" + index.exposed = True + + conf = {'/referer': {'tools.referer.on': True, + 'tools.referer.pattern': r'http://[^/]*example\.com', + }, + '/referer/reject': {'tools.referer.accept': False, + 'tools.referer.accept_missing': True, + }, + '/autovary': {'tools.autovary.on': True}, + } + + root = Root() + root.referer = Referer() + root.accept = Accept() + root.autovary = AutoVary() + cherrypy.tree.mount(root, config=conf) + cherrypy.config.update({'log.error_file': logfile}) + + +from cherrypy.test import helper + +class ResponseHeadersTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testResponseHeadersDecorator(self): + self.getPage('/') + self.assertHeader("Content-Language", "en-GB") + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + def testResponseHeaders(self): + self.getPage('/other') + self.assertHeader("Content-Language", "fr") + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + +class RefererTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testReferer(self): + self.getPage('/referer/accept') + self.assertErrorPage(403, 'Forbidden Referer header.') + + self.getPage('/referer/accept', + headers=[('Referer', 'http://www.example.com/')]) + self.assertStatus(200) + self.assertBody('Accepted!') + + # Reject + self.getPage('/referer/reject') + self.assertStatus(200) + self.assertBody('Accepted!') + + self.getPage('/referer/reject', + headers=[('Referer', 'http://www.example.com/')]) + self.assertErrorPage(403, 'Forbidden Referer header.') + + +class AcceptTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Accept_Tool(self): + # Test with no header provided + self.getPage('/accept/feed') + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify exact media type + self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify matching media range + self.getPage('/accept/feed', headers=[('Accept', 'application/*')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify all media ranges + self.getPage('/accept/feed', headers=[('Accept', '*/*')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify unacceptable media types + self.getPage('/accept/feed', headers=[('Accept', 'text/html')]) + self.assertErrorPage(406, + "Your client sent this Accept header: text/html. " + "But this resource only emits these media types: " + "application/atom+xml.") + + # Test resource where tool is 'on' but media is None (not set). + self.getPage('/accept/') + self.assertStatus(200) + self.assertBody('Atom feed') + + def test_accept_selection(self): + # Try both our expected media types + self.getPage('/accept/select', [('Accept', 'text/html')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + self.getPage('/accept/select', [('Accept', 'text/plain')]) + self.assertStatus(200) + self.assertBody('PAGE TITLE') + self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')]) + self.assertStatus(200) + self.assertBody('PAGE TITLE') + + # text/* and */* should prefer text/html since it comes first + # in our 'media' argument to tools.accept + self.getPage('/accept/select', [('Accept', 'text/*')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + self.getPage('/accept/select', [('Accept', '*/*')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + + # Try unacceptable media types + self.getPage('/accept/select', [('Accept', 'application/xml')]) + self.assertErrorPage(406, + "Your client sent this Accept header: application/xml. " + "But this resource only emits these media types: " + "text/html, text/plain.") + + +class AutoVaryTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testAutoVary(self): + self.getPage('/autovary/') + self.assertHeader( + "Vary", 'Accept, Accept-Charset, Accept-Encoding, Host, If-Modified-Since, Range') + diff --git a/cherrypy/test/test_objectmapping.py b/cherrypy/test/test_objectmapping.py new file mode 100644 index 0000000..8dcf2d3 --- /dev/null +++ b/cherrypy/test/test_objectmapping.py @@ -0,0 +1,404 @@ +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy._cptree import Application +from cherrypy.test import helper + +script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] + + +class ObjectMappingTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, name="world"): + return name + index.exposed = True + + def foobar(self): + return "bar" + foobar.exposed = True + + def default(self, *params, **kwargs): + return "default:" + repr(params) + default.exposed = True + + def other(self): + return "other" + other.exposed = True + + def extra(self, *p): + return repr(p) + extra.exposed = True + + def redirect(self): + raise cherrypy.HTTPRedirect('dir1/', 302) + redirect.exposed = True + + def notExposed(self): + return "not exposed" + + def confvalue(self): + return cherrypy.request.config.get("user") + confvalue.exposed = True + + def redirect_via_url(self, path): + raise cherrypy.HTTPRedirect(cherrypy.url(path)) + redirect_via_url.exposed = True + + def translate_html(self): + return "OK" + translate_html.exposed = True + + def mapped_func(self, ID=None): + return "ID is %s" % ID + mapped_func.exposed = True + setattr(Root, "Von B\xfclow", mapped_func) + + + class Exposing: + def base(self): + return "expose works!" + cherrypy.expose(base) + cherrypy.expose(base, "1") + cherrypy.expose(base, "2") + + class ExposingNewStyle(object): + def base(self): + return "expose works!" + cherrypy.expose(base) + cherrypy.expose(base, "1") + cherrypy.expose(base, "2") + + + class Dir1: + def index(self): + return "index for dir1" + index.exposed = True + + def myMethod(self): + return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info) + myMethod.exposed = True + myMethod._cp_config = {'tools.trailing_slash.extra': True} + + def default(self, *params): + return "default for dir1, param is:" + repr(params) + default.exposed = True + + + class Dir2: + def index(self): + return "index for dir2, path is:" + cherrypy.request.path_info + index.exposed = True + + def script_name(self): + return cherrypy.tree.script_name() + script_name.exposed = True + + def cherrypy_url(self): + return cherrypy.url("/extra") + cherrypy_url.exposed = True + + def posparam(self, *vpath): + return "/".join(vpath) + posparam.exposed = True + + + class Dir3: + def default(self): + return "default for dir3, not exposed" + + class Dir4: + def index(self): + return "index for dir4, not exposed" + + class DefNoIndex: + def default(self, *args): + raise cherrypy.HTTPRedirect("contact") + default.exposed = True + + # MethodDispatcher code + class ByMethod: + exposed = True + + def __init__(self, *things): + self.things = list(things) + + def GET(self): + return repr(self.things) + + def POST(self, thing): + self.things.append(thing) + + class Collection: + default = ByMethod('a', 'bit') + + Root.exposing = Exposing() + Root.exposingnew = ExposingNewStyle() + Root.dir1 = Dir1() + Root.dir1.dir2 = Dir2() + Root.dir1.dir2.dir3 = Dir3() + Root.dir1.dir2.dir3.dir4 = Dir4() + Root.defnoindex = DefNoIndex() + Root.bymethod = ByMethod('another') + Root.collection = Collection() + + d = cherrypy.dispatch.MethodDispatcher() + for url in script_names: + conf = {'/': {'user': (url or "/").split("/")[-2]}, + '/bymethod': {'request.dispatch': d}, + '/collection': {'request.dispatch': d}, + } + cherrypy.tree.mount(Root(), url, conf) + + + class Isolated: + def index(self): + return "made it!" + index.exposed = True + + cherrypy.tree.mount(Isolated(), "/isolated") + + class AnotherApp: + + exposed = True + + def GET(self): + return "milk" + + cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}}) + setup_server = staticmethod(setup_server) + + + def testObjectMapping(self): + for url in script_names: + prefix = self.script_name = url + + self.getPage('/') + self.assertBody('world') + + self.getPage("/dir1/myMethod") + self.assertBody("myMethod from dir1, path_info is:'/dir1/myMethod'") + + self.getPage("/this/method/does/not/exist") + self.assertBody("default:('this', 'method', 'does', 'not', 'exist')") + + self.getPage("/extra/too/much") + self.assertBody("('too', 'much')") + + self.getPage("/other") + self.assertBody('other') + + self.getPage("/notExposed") + self.assertBody("default:('notExposed',)") + + self.getPage("/dir1/dir2/") + self.assertBody('index for dir2, path is:/dir1/dir2/') + + # Test omitted trailing slash (should be redirected by default). + self.getPage("/dir1/dir2") + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/dir2/' % self.base()) + + # Test extra trailing slash (should be redirected if configured). + self.getPage("/dir1/myMethod/") + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/myMethod' % self.base()) + + # Test that default method must be exposed in order to match. + self.getPage("/dir1/dir2/dir3/dir4/index") + self.assertBody("default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')") + + # Test *vpath when default() is defined but not index() + # This also tests HTTPRedirect with default. + self.getPage("/defnoindex") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/contact' % self.base()) + self.getPage("/defnoindex/") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) + self.getPage("/defnoindex/page") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) + + self.getPage("/redirect") + self.assertStatus('302 Found') + self.assertHeader('Location', '%s/dir1/' % self.base()) + + if not getattr(cherrypy.server, "using_apache", False): + # Test that we can use URL's which aren't all valid Python identifiers + # This should also test the %XX-unquoting of URL's. + self.getPage("/Von%20B%fclow?ID=14") + self.assertBody("ID is 14") + + # Test that %2F in the path doesn't get unquoted too early; + # that is, it should not be used to separate path components. + # See ticket #393. + self.getPage("/page%2Fname") + self.assertBody("default:('page/name',)") + + self.getPage("/dir1/dir2/script_name") + self.assertBody(url) + self.getPage("/dir1/dir2/cherrypy_url") + self.assertBody("%s/extra" % self.base()) + + # Test that configs don't overwrite each other from diferent apps + self.getPage("/confvalue") + self.assertBody((url or "/").split("/")[-2]) + + self.script_name = "" + + # Test absoluteURI's in the Request-Line + self.getPage('http://%s:%s/' % (self.interface(), self.PORT)) + self.assertBody('world') + + self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' % + (self.interface(), self.PORT)) + self.assertBody("default:('abs',)") + + self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') + self.assertBody("default:('rel',)") + + # Test that the "isolated" app doesn't leak url's into the root app. + # If it did leak, Root.default() would answer with + # "default:('isolated', 'doesnt', 'exist')". + self.getPage("/isolated/") + self.assertStatus("200 OK") + self.assertBody("made it!") + self.getPage("/isolated/doesnt/exist") + self.assertStatus("404 Not Found") + + # Make sure /foobar maps to Root.foobar and not to the app + # mounted at /foo. See http://www.cherrypy.org/ticket/573 + self.getPage("/foobar") + self.assertBody("bar") + + def test_translate(self): + self.getPage("/translate_html") + self.assertStatus("200 OK") + self.assertBody("OK") + + self.getPage("/translate.html") + self.assertStatus("200 OK") + self.assertBody("OK") + + self.getPage("/translate-html") + self.assertStatus("200 OK") + self.assertBody("OK") + + def test_redir_using_url(self): + for url in script_names: + prefix = self.script_name = url + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + def testPositionalParams(self): + self.getPage("/dir1/dir2/posparam/18/24/hut/hike") + self.assertBody("18/24/hut/hike") + + # intermediate index methods should not receive posparams; + # only the "final" index method should do so. + self.getPage("/dir1/dir2/5/3/sir") + self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')") + + # test that extra positional args raises an 404 Not Found + # See http://www.cherrypy.org/ticket/733. + self.getPage("/dir1/dir2/script_name/extra/stuff") + self.assertStatus(404) + + def testExpose(self): + # Test the cherrypy.expose function/decorator + self.getPage("/exposing/base") + self.assertBody("expose works!") + + self.getPage("/exposing/1") + self.assertBody("expose works!") + + self.getPage("/exposing/2") + self.assertBody("expose works!") + + self.getPage("/exposingnew/base") + self.assertBody("expose works!") + + self.getPage("/exposingnew/1") + self.assertBody("expose works!") + + self.getPage("/exposingnew/2") + self.assertBody("expose works!") + + def testMethodDispatch(self): + self.getPage("/bymethod") + self.assertBody("['another']") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="HEAD") + self.assertBody("") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="POST", body="thing=one") + self.assertBody("") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod") + self.assertBody(repr(['another', ntou('one')])) + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="PUT") + self.assertErrorPage(405) + self.assertHeader('Allow', 'GET, HEAD, POST') + + # Test default with posparams + self.getPage("/collection/silly", method="POST") + self.getPage("/collection", method="GET") + self.assertBody("['a', 'bit', 'silly']") + + # Test custom dispatcher set on app root (see #737). + self.getPage("/app") + self.assertBody("milk") + + def testTreeMounting(self): + class Root(object): + def hello(self): + return "Hello world!" + hello.exposed = True + + # When mounting an application instance, + # we can't specify a different script name in the call to mount. + a = Application(Root(), '/somewhere') + self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse') + + # When mounting an application instance... + a = Application(Root(), '/somewhere') + # ...we MUST allow in identical script name in the call to mount... + cherrypy.tree.mount(a, '/somewhere') + self.getPage('/somewhere/hello') + self.assertStatus(200) + # ...and MUST allow a missing script_name. + del cherrypy.tree.apps['/somewhere'] + cherrypy.tree.mount(a) + self.getPage('/somewhere/hello') + self.assertStatus(200) + + # In addition, we MUST be able to create an Application using + # script_name == None for access to the wsgi_environ. + a = Application(Root(), script_name=None) + # However, this does not apply to tree.mount + self.assertRaises(TypeError, cherrypy.tree.mount, a, None) + diff --git a/cherrypy/test/test_proxy.py b/cherrypy/test/test_proxy.py new file mode 100644 index 0000000..2fbb619 --- /dev/null +++ b/cherrypy/test/test_proxy.py @@ -0,0 +1,129 @@ +import cherrypy +from cherrypy.test import helper + +script_names = ["", "/path/to/myapp"] + + +class ProxyTest(helper.CPWebCase): + + def setup_server(): + + # Set up site + cherrypy.config.update({ + 'tools.proxy.on': True, + 'tools.proxy.base': 'www.mydomain.test', + }) + + # Set up application + + class Root: + + def __init__(self, sn): + # Calculate a URL outside of any requests. + self.thisnewpage = cherrypy.url("/this/new/page", script_name=sn) + + def pageurl(self): + return self.thisnewpage + pageurl.exposed = True + + def index(self): + raise cherrypy.HTTPRedirect('dummy') + index.exposed = True + + def remoteip(self): + return cherrypy.request.remote.ip + remoteip.exposed = True + + def xhost(self): + raise cherrypy.HTTPRedirect('blah') + xhost.exposed = True + xhost._cp_config = {'tools.proxy.local': 'X-Host', + 'tools.trailing_slash.extra': True, + } + + def base(self): + return cherrypy.request.base + base.exposed = True + + def ssl(self): + return cherrypy.request.base + ssl.exposed = True + ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'} + + def newurl(self): + return ("Browse to this page." + % cherrypy.url("/this/new/page")) + newurl.exposed = True + + for sn in script_names: + cherrypy.tree.mount(Root(sn), sn) + setup_server = staticmethod(setup_server) + + def testProxy(self): + self.getPage("/") + self.assertHeader('Location', + "%s://www.mydomain.test%s/dummy" % + (self.scheme, self.prefix())) + + # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2) + self.getPage("/", headers=[('X-Forwarded-Host', 'http://www.example.test')]) + self.assertHeader('Location', "http://www.example.test/dummy") + self.getPage("/", headers=[('X-Forwarded-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/dummy" % self.scheme) + # Test multiple X-Forwarded-Host headers + self.getPage("/", headers=[ + ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'), + ]) + self.assertHeader('Location', "http://www.example.test/dummy") + + # Test X-Forwarded-For (Apache2) + self.getPage("/remoteip", + headers=[('X-Forwarded-For', '192.168.0.20')]) + self.assertBody("192.168.0.20") + self.getPage("/remoteip", + headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')]) + self.assertBody("192.168.0.20") + + # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418) + self.getPage("/xhost", headers=[('X-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/blah" % self.scheme) + + # Test X-Forwarded-Proto (lighttpd) + self.getPage("/base", headers=[('X-Forwarded-Proto', 'https')]) + self.assertBody("https://www.mydomain.test") + + # Test X-Forwarded-Ssl (webfaction?) + self.getPage("/ssl", headers=[('X-Forwarded-Ssl', 'on')]) + self.assertBody("https://www.mydomain.test") + + # Test cherrypy.url() + for sn in script_names: + # Test the value inside requests + self.getPage(sn + "/newurl") + self.assertBody("Browse to this page.") + self.getPage(sn + "/newurl", headers=[('X-Forwarded-Host', + 'http://www.example.test')]) + self.assertBody("Browse to this page.") + + # Test the value outside requests + port = "" + if self.scheme == "http" and self.PORT != 80: + port = ":%s" % self.PORT + elif self.scheme == "https" and self.PORT != 443: + port = ":%s" % self.PORT + host = self.HOST + if host in ('0.0.0.0', '::'): + import socket + host = socket.gethostname() + expected = ("%s://%s%s%s/this/new/page" + % (self.scheme, host, port, sn)) + self.getPage(sn + "/pageurl") + self.assertBody(expected) + + # Test trailing slash (see http://www.cherrypy.org/ticket/562). + self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/xhost" + % self.scheme) + diff --git a/cherrypy/test/test_refleaks.py b/cherrypy/test/test_refleaks.py new file mode 100644 index 0000000..279935e --- /dev/null +++ b/cherrypy/test/test_refleaks.py @@ -0,0 +1,59 @@ +"""Tests for refleaks.""" + +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob +import threading + +import cherrypy + + +data = object() + + +from cherrypy.test import helper + + +class ReferenceTests(helper.CPWebCase): + + def setup_server(): + + class Root: + def index(self, *args, **kwargs): + cherrypy.request.thing = data + return "Hello world!" + index.exposed = True + + cherrypy.tree.mount(Root()) + setup_server = staticmethod(setup_server) + + def test_threadlocal_garbage(self): + success = [] + + def getpage(): + host = '%s:%s' % (self.interface(), self.PORT) + if self.scheme == 'https': + c = HTTPSConnection(host) + else: + c = HTTPConnection(host) + try: + c.putrequest('GET', '/') + c.endheaders() + response = c.getresponse() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello world!")) + finally: + c.close() + success.append(True) + + ITERATIONS = 25 + ts = [] + for _ in range(ITERATIONS): + t = threading.Thread(target=getpage) + ts.append(t) + t.start() + + for t in ts: + t.join() + + self.assertEqual(len(success), ITERATIONS) + diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py new file mode 100644 index 0000000..26eea56 --- /dev/null +++ b/cherrypy/test/test_request_obj.py @@ -0,0 +1,737 @@ +"""Basic tests for the cherrypy.Request object.""" + +import os +localDir = os.path.dirname(__file__) +import sys +import types +from cherrypy._cpcompat import IncompleteRead, ntob, ntou, unicodestr + +import cherrypy +from cherrypy import _cptools, tools +from cherrypy.lib import httputil + +defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", + "TRACE", "PROPFIND") + + +# Client-side code # + +from cherrypy.test import helper + +class RequestObjectTests(helper.CPWebCase): + + def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + def scheme(self): + return cherrypy.request.scheme + scheme.exposed = True + + root = Root() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in dct.values(): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) + + class PathInfo(Test): + + def default(self, *args): + return cherrypy.request.path_info + + class Params(Test): + + def index(self, thing): + return repr(thing) + + def ismap(self, x, y): + return "Coordinates: %s, %s" % (x, y) + + def default(self, *args, **kwargs): + return "args: %s kwargs: %s" % (args, kwargs) + default._cp_config = {'request.query_string_encoding': 'latin1'} + + + class ParamErrorsCallable(object): + exposed = True + def __call__(self): + return "data" + + class ParamErrors(Test): + + def one_positional(self, param1): + return "data" + one_positional.exposed = True + + def one_positional_args(self, param1, *args): + return "data" + one_positional_args.exposed = True + + def one_positional_args_kwargs(self, param1, *args, **kwargs): + return "data" + one_positional_args_kwargs.exposed = True + + def one_positional_kwargs(self, param1, **kwargs): + return "data" + one_positional_kwargs.exposed = True + + def no_positional(self): + return "data" + no_positional.exposed = True + + def no_positional_args(self, *args): + return "data" + no_positional_args.exposed = True + + def no_positional_args_kwargs(self, *args, **kwargs): + return "data" + no_positional_args_kwargs.exposed = True + + def no_positional_kwargs(self, **kwargs): + return "data" + no_positional_kwargs.exposed = True + + callable_object = ParamErrorsCallable() + + def raise_type_error(self, **kwargs): + raise TypeError("Client Error") + raise_type_error.exposed = True + + def raise_type_error_with_default_param(self, x, y=None): + return '%d' % 'a' # throw an exception + raise_type_error_with_default_param.exposed = True + + def callable_error_page(status, **kwargs): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + + + class Error(Test): + + _cp_config = {'tools.log_tracebacks.on': True, + } + + def reason_phrase(self): + raise cherrypy.HTTPError("410 Gone fishin'") + + def custom(self, err='404'): + raise cherrypy.HTTPError(int(err), "No, really, not found!") + custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"), + 'error_page.401': callable_error_page, + } + + def custom_default(self): + return 1 + 'a' # raise an unexpected error + custom_default._cp_config = {'error_page.default': callable_error_page} + + def noexist(self): + raise cherrypy.HTTPError(404, "No, really, not found!") + noexist._cp_config = {'error_page.404': "nonexistent.html"} + + def page_method(self): + raise ValueError() + + def page_yield(self): + yield "howdy" + raise ValueError() + + def page_streamed(self): + yield "word up" + raise ValueError() + yield "very oops" + page_streamed._cp_config = {"response.stream": True} + + def cause_err_in_finalize(self): + # Since status must start with an int, this should error. + cherrypy.response.status = "ZOO OK" + cause_err_in_finalize._cp_config = {'request.show_tracebacks': False} + + def rethrow(self): + """Test that an error raised here will be thrown out to the server.""" + raise ValueError() + rethrow._cp_config = {'request.throw_errors': True} + + + class Expect(Test): + + def expectation_failed(self): + expect = cherrypy.request.headers.elements("Expect") + if expect and expect[0].value != '100-continue': + raise cherrypy.HTTPError(400) + raise cherrypy.HTTPError(417, 'Expectation Failed') + + class Headers(Test): + + def default(self, headername): + """Spit back out the value for the requested header.""" + return cherrypy.request.headers[headername] + + def doubledheaders(self): + # From http://www.cherrypy.org/ticket/165: + # "header field names should not be case sensitive sayes the rfc. + # if i set a headerfield in complete lowercase i end up with two + # header fields, one in lowercase, the other in mixed-case." + + # Set the most common headers + hMap = cherrypy.response.headers + hMap['content-type'] = "text/html" + hMap['content-length'] = 18 + hMap['server'] = 'CherryPy headertest' + hMap['location'] = ('%s://%s:%s/headers/' + % (cherrypy.request.local.ip, + cherrypy.request.local.port, + cherrypy.request.scheme)) + + # Set a rare header for fun + hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' + + return "double header test" + + def ifmatch(self): + val = cherrypy.request.headers['If-Match'] + assert isinstance(val, unicodestr) + cherrypy.response.headers['ETag'] = val + return val + + + class HeaderElements(Test): + + def get_elements(self, headername): + e = cherrypy.request.headers.elements(headername) + return "\n".join([unicodestr(x) for x in e]) + + + class Method(Test): + + def index(self): + m = cherrypy.request.method + if m in defined_http_methods or m == "CONNECT": + return m + + if m == "LINK": + raise cherrypy.HTTPError(405) + else: + raise cherrypy.HTTPError(501) + + def parameterized(self, data): + return data + + def request_body(self): + # This should be a file object (temp file), + # which CP will just pipe back out if we tell it to. + return cherrypy.request.body + + def reachable(self): + return "success" + + class Divorce: + """HTTP Method handlers shouldn't collide with normal method names. + For example, a GET-handler shouldn't collide with a method named 'get'. + + If you build HTTP method dispatching into CherryPy, rewrite this class + to use your new dispatch mechanism and make sure that: + "GET /divorce HTTP/1.1" maps to divorce.index() and + "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() + """ + + documents = {} + + def index(self): + yield "

Choose your document

\n" + yield "
    \n" + for id, contents in self.documents.items(): + yield ("
  • %s: %s
  • \n" + % (id, id, contents)) + yield "
" + index.exposed = True + + def get(self, ID): + return ("Divorce document %s: %s" % + (ID, self.documents.get(ID, "empty"))) + get.exposed = True + + root.divorce = Divorce() + + + class ThreadLocal(Test): + + def index(self): + existing = repr(getattr(cherrypy.request, "asdf", None)) + cherrypy.request.asdf = "rassfrassin" + return existing + + appconf = { + '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")}, + } + cherrypy.tree.mount(root, config=appconf) + setup_server = staticmethod(setup_server) + + def test_scheme(self): + self.getPage("/scheme") + self.assertBody(self.scheme) + + def testRelativeURIPathInfo(self): + self.getPage("/pathinfo/foo/bar") + self.assertBody("/pathinfo/foo/bar") + + def testAbsoluteURIPathInfo(self): + # http://cherrypy.org/ticket/1061 + self.getPage("http://localhost/pathinfo/foo/bar") + self.assertBody("/pathinfo/foo/bar") + + def testParams(self): + self.getPage("/params/?thing=a") + self.assertBody(repr(ntou("a"))) + + self.getPage("/params/?thing=a&thing=b&thing=c") + self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) + + # Test friendly error message when given params are not accepted. + cherrypy.config.update({"request.show_mismatched_params": True}) + self.getPage("/params/?notathing=meeting") + self.assertInBody("Missing parameters: thing") + self.getPage("/params/?thing=meeting¬athing=meeting") + self.assertInBody("Unexpected query string parameters: notathing") + + # Test ability to turn off friendly error messages + cherrypy.config.update({"request.show_mismatched_params": False}) + self.getPage("/params/?notathing=meeting") + self.assertInBody("Not Found") + self.getPage("/params/?thing=meeting¬athing=meeting") + self.assertInBody("Not Found") + + # Test "% HEX HEX"-encoded URL, param keys, and values + self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville") + self.assertBody("args: %s kwargs: %s" % + (('\xd4 \xe3', 'cheese'), + {'Gruy\xe8re': ntou('Bulgn\xe9ville')})) + + # Make sure that encoded = and & get parsed correctly + self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2") + self.assertBody("args: %s kwargs: %s" % + (('code',), + {'url': ntou('http://cherrypy.org/index?a=1&b=2')})) + + # Test coordinates sent by + self.getPage("/params/ismap?223,114") + self.assertBody("Coordinates: 223, 114") + + # Test "name[key]" dict-like params + self.getPage("/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz") + self.assertBody("args: %s kwargs: %s" % + (('dictlike',), + {'a[1]': ntou('1'), 'b[bar]': ntou('baz'), + 'b': ntou('foo'), 'a[2]': ntou('2')})) + + def testParamErrors(self): + + # test that all of the handlers work when given + # the correct parameters in order to ensure that the + # errors below aren't coming from some other source. + for uri in ( + '/paramerrors/one_positional?param1=foo', + '/paramerrors/one_positional_args?param1=foo', + '/paramerrors/one_positional_args/foo', + '/paramerrors/one_positional_args/foo/bar/baz', + '/paramerrors/one_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/one_positional_args_kwargs/foo?param2=bar¶m3=baz', + '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs?param1=foo¶m2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs/foo?param4=foo¶m2=bar¶m3=baz', + '/paramerrors/no_positional', + '/paramerrors/no_positional_args/foo', + '/paramerrors/no_positional_args/foo/bar/baz', + '/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/no_positional_args_kwargs/foo?param2=bar', + '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', + '/paramerrors/no_positional_kwargs?param1=foo¶m2=bar', + '/paramerrors/callable_object', + ): + self.getPage(uri) + self.assertStatus(200) + + # query string parameters are part of the URI, so if they are wrong + # for a particular handler, the status MUST be a 404. + error_msgs = [ + 'Missing parameters', + 'Nothing matches the given URI', + 'Multiple values for parameters', + 'Unexpected query string parameters', + 'Unexpected body parameters', + ] + for uri, msg in ( + ('/paramerrors/one_positional', error_msgs[0]), + ('/paramerrors/one_positional?foo=foo', error_msgs[0]), + ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]), + ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]), + ('/paramerrors/one_positional/foo?param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo?param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]), + ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/one_positional_kwargs/foo?param1=foo¶m2=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/no_positional/boo', error_msgs[1]), + ('/paramerrors/no_positional?param1=foo', error_msgs[3]), + ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]), + ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]), + ('/paramerrors/callable_object?param1=foo', error_msgs[3]), + ('/paramerrors/callable_object/boo', error_msgs[1]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("Not Found") + + # if body parameters are wrong, a 400 must be returned. + for uri, body, msg in ( + ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]), + ('/paramerrors/one_positional/foo', 'param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo', 'param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]), + ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/one_positional_kwargs/foo', 'param1=foo¶m2=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), + ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]), + ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(400) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("400 Bad") + + + # even if body parameters are wrong, if we get the uri wrong, then + # it's a 404 + for uri, body, msg in ( + ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]), + ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]), + ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]), + ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar¶m3=baz', error_msgs[1]), + ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]), + ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]), + ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("Not Found") + + # In the case that a handler raises a TypeError we should + # let that type error through. + for uri in ( + '/paramerrors/raise_type_error', + '/paramerrors/raise_type_error_with_default_param?x=0', + '/paramerrors/raise_type_error_with_default_param?x=0&y=0', + ): + self.getPage(uri, method='GET') + self.assertStatus(500) + self.assertTrue('Client Error', self.body) + + def testErrorHandling(self): + self.getPage("/error/missing") + self.assertStatus(404) + self.assertErrorPage(404, "The path '/error/missing' was not found.") + + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + valerr = '\n raise ValueError()\nValueError' + self.getPage("/error/page_method") + self.assertErrorPage(500, pattern=valerr) + + self.getPage("/error/page_yield") + self.assertErrorPage(500, pattern=valerr) + + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage("/error/page_streamed") + # Because this error is raised after the response body has + # started, the status should not change to an error status. + self.assertStatus(200) + self.assertBody("word up") + else: + # Under HTTP/1.1, the chunked transfer-coding is used. + # The HTTP client will choke when the output is incomplete. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + "/error/page_streamed") + + # No traceback should be present + self.getPage("/error/cause_err_in_finalize") + msg = "Illegal response status from server ('ZOO' is non-numeric)." + self.assertErrorPage(500, msg, None) + finally: + ignore.pop() + + # Test HTTPError with a reason-phrase in the status arg. + self.getPage('/error/reason_phrase') + self.assertStatus("410 Gone fishin'") + + # Test custom error page for a specific error. + self.getPage("/error/custom") + self.assertStatus(404) + self.assertBody("Hello, world\r\n" + (" " * 499)) + + # Test custom error page for a specific error. + self.getPage("/error/custom?err=401") + self.assertStatus(401) + self.assertBody("Error 401 Unauthorized - Well, I'm very sorry but you haven't paid!") + + # Test default custom error page. + self.getPage("/error/custom_default") + self.assertStatus(500) + self.assertBody("Error 500 Internal Server Error - Well, I'm very sorry but you haven't paid!".ljust(513)) + + # Test error in custom error page (ticket #305). + # Note that the message is escaped for HTML (ticket #310). + self.getPage("/error/noexist") + self.assertStatus(404) + msg = ("No, <b>really</b>, not found!
" + "In addition, the custom error page failed:\n
" + "IOError: [Errno 2] No such file or directory: 'nonexistent.html'") + self.assertInBody(msg) + + if getattr(cherrypy.server, "using_apache", False): + pass + else: + # Test throw_errors (ticket #186). + self.getPage("/error/rethrow") + self.assertInBody("raise ValueError()") + + def testExpect(self): + e = ('Expect', '100-continue') + self.getPage("/headerelements/get_elements?headername=Expect", [e]) + self.assertBody('100-continue') + + self.getPage("/expect/expectation_failed", [e]) + self.assertStatus(417) + + def testHeaderElements(self): + # Accept-* header elements should be sorted, with most preferred first. + h = [('Accept', 'audio/*; q=0.2, audio/basic')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("audio/basic\n" + "audio/*;q=0.2") + + h = [('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("text/x-c\n" + "text/html\n" + "text/x-dvi;q=0.8\n" + "text/plain;q=0.5") + + # Test that more specific media ranges get priority. + h = [('Accept', 'text/*, text/html, text/html;level=1, */*')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("text/html;level=1\n" + "text/html\n" + "text/*\n" + "*/*") + + # Test Accept-Charset + h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] + self.getPage("/headerelements/get_elements?headername=Accept-Charset", h) + self.assertStatus("200 OK") + self.assertBody("iso-8859-5\n" + "unicode-1-1;q=0.8") + + # Test Accept-Encoding + h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')] + self.getPage("/headerelements/get_elements?headername=Accept-Encoding", h) + self.assertStatus("200 OK") + self.assertBody("gzip;q=1.0\n" + "identity;q=0.5\n" + "*;q=0") + + # Test Accept-Language + h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')] + self.getPage("/headerelements/get_elements?headername=Accept-Language", h) + self.assertStatus("200 OK") + self.assertBody("da\n" + "en-gb;q=0.8\n" + "en;q=0.7") + + # Test malformed header parsing. See http://www.cherrypy.org/ticket/763. + self.getPage("/headerelements/get_elements?headername=Content-Type", + # Note the illegal trailing ";" + headers=[('Content-Type', 'text/html; charset=utf-8;')]) + self.assertStatus(200) + self.assertBody("text/html;charset=utf-8") + + def test_repeated_headers(self): + # Test that two request headers are collapsed into one. + # See http://www.cherrypy.org/ticket/542. + self.getPage("/headers/Accept-Charset", + headers=[("Accept-Charset", "iso-8859-5"), + ("Accept-Charset", "unicode-1-1;q=0.8")]) + self.assertBody("iso-8859-5, unicode-1-1;q=0.8") + + # Tests that each header only appears once, regardless of case. + self.getPage("/headers/doubledheaders") + self.assertBody("double header test") + hnames = [name.title() for name, val in self.headers] + for key in ['Content-Length', 'Content-Type', 'Date', + 'Expires', 'Location', 'Server']: + self.assertEqual(hnames.count(key), 1, self.headers) + + def test_encoded_headers(self): + # First, make sure the innards work like expected. + self.assertEqual(httputil.decode_TEXT(ntou("=?utf-8?q?f=C3=BCr?=")), ntou("f\xfcr")) + + if cherrypy.server.protocol_version == "HTTP/1.1": + # Test RFC-2047-encoded request and response header values + u = ntou('\u212bngstr\xf6m', 'escape') + c = ntou("=E2=84=ABngstr=C3=B6m") + self.getPage("/headers/ifmatch", [('If-Match', ntou('=?utf-8?q?%s?=') % c)]) + # The body should be utf-8 encoded. + self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m")) + # But the Etag header should be RFC-2047 encoded (binary) + self.assertHeader("ETag", ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?=')) + + # Test a *LONG* RFC-2047-encoded request and response header value + self.getPage("/headers/ifmatch", + [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))]) + self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m") * 10) + # Note: this is different output for Python3, but it decodes fine. + etag = self.assertHeader("ETag", + '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm0=?=') + self.assertEqual(httputil.decode_TEXT(etag), u * 10) + + def test_header_presence(self): + # If we don't pass a Content-Type header, it should not be present + # in cherrypy.request.headers + self.getPage("/headers/Content-Type", + headers=[]) + self.assertStatus(500) + + # If Content-Type is present in the request, it should be present in + # cherrypy.request.headers + self.getPage("/headers/Content-Type", + headers=[("Content-type", "application/json")]) + self.assertBody("application/json") + + def test_basic_HTTPMethods(self): + helper.webtest.methods_with_bodies = ("POST", "PUT", "PROPFIND") + + # Test that all defined HTTP methods work. + for m in defined_http_methods: + self.getPage("/method/", method=m) + + # HEAD requests should not return any body. + if m == "HEAD": + self.assertBody("") + elif m == "TRACE": + # Some HTTP servers (like modpy) have their own TRACE support + self.assertEqual(self.body[:5], ntob("TRACE")) + else: + self.assertBody(m) + + # Request a PUT method with a form-urlencoded body + self.getPage("/method/parameterized", method="PUT", + body="data=on+top+of+other+things") + self.assertBody("on top of other things") + + # Request a PUT method with a file body + b = "one thing on top of another" + h = [("Content-Type", "text/plain"), + ("Content-Length", str(len(b)))] + self.getPage("/method/request_body", headers=h, method="PUT", body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a PUT method with a file body but no Content-Type. + # See http://www.cherrypy.org/ticket/790. + b = ntob("one thing on top of another") + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest("PUT", "/method/request_body", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader('Content-Length', str(len(b))) + conn.endheaders() + conn.send(b) + response = conn.response_class(conn.sock, method="PUT") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(b) + finally: + self.persistent = False + + # Request a PUT method with no body whatsoever (not an empty one). + # See http://www.cherrypy.org/ticket/650. + # Provide a C-T or webtest will provide one (and a C-L) for us. + h = [("Content-Type", "text/plain")] + self.getPage("/method/reachable", headers=h, method="PUT") + self.assertStatus(411) + + # Request a custom method with a request body + b = ('\n\n' + '' + '') + h = [('Content-Type', 'text/xml'), + ('Content-Length', str(len(b)))] + self.getPage("/method/request_body", headers=h, method="PROPFIND", body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a disallowed method + self.getPage("/method/", method="LINK") + self.assertStatus(405) + + # Request an unknown method + self.getPage("/method/", method="SEARCH") + self.assertStatus(501) + + # For method dispatchers: make sure that an HTTP method doesn't + # collide with a virtual path atom. If you build HTTP-method + # dispatching into the core, rewrite these handlers to use + # your dispatch idioms. + self.getPage("/divorce/get?ID=13") + self.assertBody('Divorce document 13: empty') + self.assertStatus(200) + self.getPage("/divorce/", method="GET") + self.assertBody('

Choose your document

\n
    \n
') + self.assertStatus(200) + + def test_CONNECT_method(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + self.getPage("/method/", method="CONNECT") + self.assertBody("CONNECT") + + def testEmptyThreadlocals(self): + results = [] + for x in range(20): + self.getPage("/threadlocal/") + results.append(self.body) + self.assertEqual(results, [ntob("None")] * 20) + diff --git a/cherrypy/test/test_routes.py b/cherrypy/test/test_routes.py new file mode 100644 index 0000000..a8062f8 --- /dev/null +++ b/cherrypy/test/test_routes.py @@ -0,0 +1,69 @@ +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +import cherrypy + +from cherrypy.test import helper +import nose + +class RoutesDispatchTest(helper.CPWebCase): + + def setup_server(): + + try: + import routes + except ImportError: + raise nose.SkipTest('Install routes to test RoutesDispatcher code') + + class Dummy: + def index(self): + return "I said good day!" + + class City: + + def __init__(self, name): + self.name = name + self.population = 10000 + + def index(self, **kwargs): + return "Welcome to %s, pop. %s" % (self.name, self.population) + index._cp_config = {'tools.response_headers.on': True, + 'tools.response_headers.headers': [('Content-Language', 'en-GB')]} + + def update(self, **kwargs): + self.population = kwargs['pop'] + return "OK" + + d = cherrypy.dispatch.RoutesDispatcher() + d.connect(action='index', name='hounslow', route='/hounslow', + controller=City('Hounslow')) + d.connect(name='surbiton', route='/surbiton', controller=City('Surbiton'), + action='index', conditions=dict(method=['GET'])) + d.mapper.connect('/surbiton', controller='surbiton', + action='update', conditions=dict(method=['POST'])) + d.connect('main', ':action', controller=Dummy()) + + conf = {'/': {'request.dispatch': d}} + cherrypy.tree.mount(root=None, config=conf) + setup_server = staticmethod(setup_server) + + def test_Routes_Dispatch(self): + self.getPage("/hounslow") + self.assertStatus("200 OK") + self.assertBody("Welcome to Hounslow, pop. 10000") + + self.getPage("/foo") + self.assertStatus("404 Not Found") + + self.getPage("/surbiton") + self.assertStatus("200 OK") + self.assertBody("Welcome to Surbiton, pop. 10000") + + self.getPage("/surbiton", method="POST", body="pop=1327") + self.assertStatus("200 OK") + self.assertBody("OK") + self.getPage("/surbiton") + self.assertStatus("200 OK") + self.assertHeader("Content-Language", "en-GB") + self.assertBody("Welcome to Surbiton, pop. 1327") + diff --git a/cherrypy/test/test_session.py b/cherrypy/test/test_session.py new file mode 100644 index 0000000..9143a1d --- /dev/null +++ b/cherrypy/test/test_session.py @@ -0,0 +1,464 @@ +import os +localDir = os.path.dirname(__file__) +import sys +import threading +import time + +import cherrypy +from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection +from cherrypy.lib import sessions +from cherrypy.lib.httputil import response_codes + +def http_methods_allowed(methods=['GET', 'HEAD']): + method = cherrypy.request.method.upper() + if method not in methods: + cherrypy.response.headers['Allow'] = ", ".join(methods) + raise cherrypy.HTTPError(405) + +cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed) + + +def setup_server(): + + class Root: + + _cp_config = {'tools.sessions.on': True, + 'tools.sessions.storage_type' : 'ram', + 'tools.sessions.storage_path' : localDir, + 'tools.sessions.timeout': (1.0 / 60), + 'tools.sessions.clean_freq': (1.0 / 60), + } + + def clear(self): + cherrypy.session.cache.clear() + clear.exposed = True + + def data(self): + cherrypy.session['aha'] = 'foo' + return repr(cherrypy.session._data) + data.exposed = True + + def testGen(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + yield str(counter) + testGen.exposed = True + + def testStr(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + return str(counter) + testStr.exposed = True + + def setsessiontype(self, newtype): + self.__class__._cp_config.update({'tools.sessions.storage_type': newtype}) + if hasattr(cherrypy, "session"): + del cherrypy.session + cls = getattr(sessions, newtype.title() + 'Session') + if cls.clean_thread: + cls.clean_thread.stop() + cls.clean_thread.unsubscribe() + del cls.clean_thread + setsessiontype.exposed = True + setsessiontype._cp_config = {'tools.sessions.on': False} + + def index(self): + sess = cherrypy.session + c = sess.get('counter', 0) + 1 + time.sleep(0.01) + sess['counter'] = c + return str(c) + index.exposed = True + + def keyin(self, key): + return str(key in cherrypy.session) + keyin.exposed = True + + def delete(self): + cherrypy.session.delete() + sessions.expire() + return "done" + delete.exposed = True + + def delkey(self, key): + del cherrypy.session[key] + return "OK" + delkey.exposed = True + + def blah(self): + return self._cp_config['tools.sessions.storage_type'] + blah.exposed = True + + def iredir(self): + raise cherrypy.InternalRedirect('/blah') + iredir.exposed = True + + def restricted(self): + return cherrypy.request.method + restricted.exposed = True + restricted._cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['GET']} + + def regen(self): + cherrypy.tools.sessions.regenerate() + return "logged in" + regen.exposed = True + + def length(self): + return str(len(cherrypy.session)) + length.exposed = True + + def session_cookie(self): + # Must load() to start the clean thread. + cherrypy.session.load() + return cherrypy.session.id + session_cookie.exposed = True + session_cookie._cp_config = { + 'tools.sessions.path': '/session_cookie', + 'tools.sessions.name': 'temp', + 'tools.sessions.persistent': False} + + cherrypy.tree.mount(Root()) + + +from cherrypy.test import helper + +class SessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def tearDown(self): + # Clean up sessions. + for fname in os.listdir(localDir): + if fname.startswith(sessions.FileSession.SESSION_PREFIX): + os.unlink(os.path.join(localDir, fname)) + + def test_0_Session(self): + self.getPage('/setsessiontype/ram') + self.getPage('/clear') + + # Test that a normal request gets the same id in the cookies. + # Note: this wouldn't work if /data didn't load the session. + self.getPage('/data') + self.assertBody("{'aha': 'foo'}") + c = self.cookies[0] + self.getPage('/data', self.cookies) + self.assertEqual(self.cookies[0], c) + + self.getPage('/testStr') + self.assertBody('1') + cookie_parts = dict([p.strip().split('=') + for p in self.cookies[0][1].split(";")]) + # Assert there is an 'expires' param + self.assertEqual(set(cookie_parts.keys()), + set(['session_id', 'expires', 'Path'])) + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/data', self.cookies) + self.assertBody("{'aha': 'foo', 'counter': 3}") + self.getPage('/length', self.cookies) + self.assertBody('2') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + self.getPage('/setsessiontype/file') + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(2) + self.getPage('/') + self.assertBody('1') + self.getPage('/length', self.cookies) + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody("True") + cookieset1 = self.cookies + + # Make a new session and test __len__ again + self.getPage('/') + self.getPage('/length', self.cookies) + self.assertBody('2') + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody("done") + self.getPage('/delete', cookieset1) + self.assertBody("done") + f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] + self.assertEqual(f(), []) + + # Wait for the cleanup thread to delete remaining session files + self.getPage('/') + f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] + self.assertNotEqual(f(), []) + time.sleep(2) + self.assertEqual(f(), []) + + def test_1_Ram_Concurrency(self): + self.getPage('/setsessiontype/ram') + self._test_Concurrency() + + def test_2_File_Concurrency(self): + self.getPage('/setsessiontype/file') + self._test_Concurrency() + + def _test_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage("/") + self.assertBody("1") + cookies = self.cookies + + data_dict = {} + errors = [] + + def request(index): + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + for i in range(request_count): + c.putrequest('GET', '/') + for k, v in cookies: + c.putheader(k, v) + c.endheaders() + response = c.getresponse() + body = response.read() + if response.status != 200 or not body.isdigit(): + errors.append((response.status, body)) + else: + data_dict[index] = max(data_dict[index], int(body)) + # Uncomment the following line to prove threads overlap. +## sys.stdout.write("%d " % index) + + # Start requests from each of + # concurrent clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + + for e in errors: + print(e) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody("file") + + def test_4_File_deletion(self): + # Start a new session + self.getPage('/testStr') + # Delete the session file manually and retry. + id = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + path = os.path.join(localDir, "session-" + id) + os.unlink(path) + self.getPage('/testStr', self.cookies) + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage(404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) + + def test_6_regenerate(self): + self.getPage('/testStr') + # grab the cookie ID + id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.getPage('/regen') + self.assertBody('logged in') + id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.assertNotEqual(id1, id2) + + self.getPage('/testStr') + # grab the cookie ID + id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.getPage('/testStr', + headers=[('Cookie', + 'session_id=maliciousid; ' + 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')]) + id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.assertNotEqual(id1, id2) + self.assertNotEqual(id2, 'maliciousid') + + def test_7_session_cookies(self): + self.getPage('/setsessiontype/ram') + self.getPage('/clear') + self.getPage('/session_cookie') + # grab the cookie ID + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + id1 = cookie_parts['temp'] + self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) + + # Send another request in the same "browser session". + self.getPage('/session_cookie', self.cookies) + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + self.assertBody(id1) + self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) + + # Simulate a browser close by just not sending the cookies + self.getPage('/session_cookie') + # grab the cookie ID + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + # Assert a new id has been generated... + id2 = cookie_parts['temp'] + self.assertNotEqual(id1, id2) + self.assertEqual(set(sessions.RamSession.cache.keys()), set([id1, id2])) + + # Wait for the session.timeout on both sessions + time.sleep(2.5) + cache = copykeys(sessions.RamSession.cache) + if cache: + if cache == [id2]: + self.fail("The second session did not time out.") + else: + self.fail("Unknown session id in cache: %r", cache) + + +import socket +try: + import memcache + + host, port = '127.0.0.1', 11211 + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + raise + break +except (ImportError, socket.error): + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test(self): + return self.skip("memcached not reachable ") +else: + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_0_Session(self): + self.getPage('/setsessiontype/memcached') + + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/length', self.cookies) + self.assertErrorPage(500) + self.assertInBody("NotImplementedError") + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(1.25) + self.getPage('/') + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody("True") + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody("done") + + def test_1_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage("/") + self.assertBody("1") + cookies = self.cookies + + data_dict = {} + + def request(index): + for i in range(request_count): + self.getPage("/", cookies) + # Uncomment the following line to prove threads overlap. +## sys.stdout.write("%d " % index) + if not self.body.isdigit(): + self.fail(self.body) + data_dict[index] = v = int(self.body) + + # Start concurrent requests from + # each of clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody("memcached") + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage(404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) + diff --git a/cherrypy/test/test_sessionauthenticate.py b/cherrypy/test/test_sessionauthenticate.py new file mode 100644 index 0000000..ab1fe51 --- /dev/null +++ b/cherrypy/test/test_sessionauthenticate.py @@ -0,0 +1,62 @@ +import cherrypy +from cherrypy.test import helper + + +class SessionAuthenticateTest(helper.CPWebCase): + + def setup_server(): + + def check(username, password): + # Dummy check_username_and_password function + if username != 'test' or password != 'password': + return 'Wrong login/password' + + def augment_params(): + # A simple tool to add some things to request.params + # This is to check to make sure that session_auth can handle request + # params (ticket #780) + cherrypy.request.params["test"] = "test" + + cherrypy.tools.augment_params = cherrypy.Tool('before_handler', + augment_params, None, priority=30) + + class Test: + + _cp_config = {'tools.sessions.on': True, + 'tools.session_auth.on': True, + 'tools.session_auth.check_username_and_password': check, + 'tools.augment_params.on': True, + } + + def index(self, **kwargs): + return "Hi %s, you are logged in" % cherrypy.request.login + index.exposed = True + + cherrypy.tree.mount(Test()) + setup_server = staticmethod(setup_server) + + + def testSessionAuthenticate(self): + # request a page and check for login form + self.getPage('/') + self.assertInBody('
') + + # setup credentials + login_body = 'username=test&password=password&from_page=/' + + # attempt a login + self.getPage('/do_login', method='POST', body=login_body) + self.assertStatus((302, 303)) + + # get the page now that we are logged in + self.getPage('/', self.cookies) + self.assertBody('Hi test, you are logged in') + + # do a logout + self.getPage('/do_logout', self.cookies, method='POST') + self.assertStatus((302, 303)) + + # verify we are logged out + self.getPage('/', self.cookies) + self.assertInBody('') + diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py new file mode 100644 index 0000000..6322687 --- /dev/null +++ b/cherrypy/test/test_states.py @@ -0,0 +1,439 @@ +from cherrypy._cpcompat import BadStatusLine, ntob +import os +import sys +import threading +import time + +import cherrypy +engine = cherrypy.engine +thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class Dependency: + + def __init__(self, bus): + self.bus = bus + self.running = False + self.startcount = 0 + self.gracecount = 0 + self.threads = {} + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + self.bus.subscribe('graceful', self.graceful) + self.bus.subscribe('start_thread', self.startthread) + self.bus.subscribe('stop_thread', self.stopthread) + + def start(self): + self.running = True + self.startcount += 1 + + def stop(self): + self.running = False + + def graceful(self): + self.gracecount += 1 + + def startthread(self, thread_id): + self.threads[thread_id] = None + + def stopthread(self, thread_id): + del self.threads[thread_id] + +db_connection = Dependency(engine) + +def setup_server(): + class Root: + def index(self): + return "Hello World" + index.exposed = True + + def ctrlc(self): + raise KeyboardInterrupt() + ctrlc.exposed = True + + def graceful(self): + engine.graceful() + return "app was (gracefully) restarted succesfully" + graceful.exposed = True + + def block_explicit(self): + while True: + if cherrypy.response.timed_out: + cherrypy.response.timed_out = False + return "broken!" + time.sleep(0.01) + block_explicit.exposed = True + + def block_implicit(self): + time.sleep(0.5) + return "response.timeout = %s" % cherrypy.response.timeout + block_implicit.exposed = True + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'environment': 'test_suite', + 'engine.deadlock_poll_freq': 0.1, + }) + + db_connection.subscribe() + + + +# ------------ Enough helpers. Time for real live test cases. ------------ # + + +from cherrypy.test import helper + +class ServerStateTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def setUp(self): + cherrypy.server.socket_timeout = 0.1 + self.do_gc_test = False + + def test_0_NormalStateFlow(self): + engine.stop() + # Our db_connection should not be running + self.assertEqual(db_connection.running, False) + self.assertEqual(db_connection.startcount, 1) + self.assertEqual(len(db_connection.threads), 0) + + # Test server start + engine.start() + self.assertEqual(engine.state, engine.states.STARTED) + + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port) + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.startcount, 2) + self.assertEqual(len(db_connection.threads), 0) + + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(len(db_connection.threads), 1) + + # Test engine stop. This will also stop the HTTP server. + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + + # Verify that our custom stop function was called + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + # Block the main thread now and verify that exit() works. + def exittest(): + self.getPage("/") + self.assertBody("Hello World") + engine.exit() + cherrypy.server.start() + engine.start_with_callback(exittest) + engine.block() + self.assertEqual(engine.state, engine.states.EXITING) + + def test_1_Restart(self): + cherrypy.server.start() + engine.start() + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + grace = db_connection.gracecount + + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from this thread + engine.graceful() + self.assertEqual(engine.state, engine.states.STARTED) + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 1) + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from inside a page handler + self.getPage("/graceful") + self.assertEqual(engine.state, engine.states.STARTED) + self.assertBody("app was (gracefully) restarted succesfully") + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 2) + # Since we are requesting synchronously, is only one thread used? + # Note that the "/graceful" request has been flushed. + self.assertEqual(len(db_connection.threads), 0) + + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + def test_2_KeyboardInterrupt(self): + # Raise a keyboard interrupt in the HTTP server's main thread. + # We must start the server in this, the main thread + engine.start() + cherrypy.server.start() + + self.persistent = True + try: + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody("Hello World") + self.assertNoHeader("Connection") + + cherrypy.server.httpserver.interrupt = KeyboardInterrupt + engine.block() + + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + self.assertEqual(engine.state, engine.states.EXITING) + finally: + self.persistent = False + + # Raise a keyboard interrupt in a page handler; on multithreaded + # servers, this should occur in one of the worker threads. + # This should raise a BadStatusLine error, since the worker + # thread will just die without writing a response. + engine.start() + cherrypy.server.start() + + try: + self.getPage("/ctrlc") + except BadStatusLine: + pass + else: + print(self.body) + self.fail("AssertionError: BadStatusLine not raised") + + engine.block() + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + def test_3_Deadlocks(self): + cherrypy.config.update({'response.timeout': 0.2}) + + engine.start() + cherrypy.server.start() + try: + self.assertNotEqual(engine.timeout_monitor.thread, None) + + # Request a "normal" page. + self.assertEqual(engine.timeout_monitor.servings, []) + self.getPage("/") + self.assertBody("Hello World") + # request.close is called async. + while engine.timeout_monitor.servings: + sys.stdout.write(".") + time.sleep(0.01) + + # Request a page that explicitly checks itself for deadlock. + # The deadlock_timeout should be 2 secs. + self.getPage("/block_explicit") + self.assertBody("broken!") + + # Request a page that implicitly breaks deadlock. + # If we deadlock, we want to touch as little code as possible, + # so we won't even call handle_error, just bail ASAP. + self.getPage("/block_implicit") + self.assertStatus(500) + self.assertInBody("raise cherrypy.TimeoutError()") + finally: + engine.exit() + + def test_4_Autoreload(self): + # Start the demo script in a new process + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_4_Autoreload"') + p.start(imports='cherrypy.test._test_states_demo') + try: + self.getPage("/start") + start = float(self.body) + + # Give the autoreloader time to cache the file time. + time.sleep(2) + + # Touch the file + os.utime(os.path.join(thisdir, "_test_states_demo.py"), None) + + # Give the autoreloader time to re-exec the process + time.sleep(2) + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + cherrypy._cpserver.wait_for_occupied_port(host, port) + + self.getPage("/start") + if not (float(self.body) > start): + raise AssertionError("start time %s not greater than %s" % + (float(self.body), start)) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + def test_5_Start_Error(self): + # If a process errors during start, it should stop the engine + # and exit with a non-zero exit code. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True) + p.write_conf( + extra="""starterror: True +test_case_name: "test_5_Start_Error" +""" + ) + p.start(imports='cherrypy.test._test_states_demo') + if p.exit_code == 0: + self.fail("Process failed to return nonzero exit code.") + + +class PluginTests(helper.CPWebCase): + def test_daemonize(self): + if os.name not in ['posix']: + return self.skip("skipped (not on posix) ") + self.HOST = '127.0.0.1' + self.PORT = 8081 + # Spawn the process and wait, when this returns, the original process + # is finished. If it daemonized properly, we should still be able + # to access pages. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True, + socket_host='127.0.0.1', + socket_port=8081) + p.write_conf( + extra='test_case_name: "test_daemonize"') + p.start(imports='cherrypy.test._test_states_demo') + try: + # Just get the pid of the daemonization process. + self.getPage("/pid") + self.assertStatus(200) + page_pid = int(self.body) + self.assertEqual(page_pid, p.get_pid()) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + # Wait until here to test the exit code because we want to ensure + # that we wait for the daemon to finish running before we fail. + if p.exit_code != 0: + self.fail("Daemonized parent process failed to exit cleanly.") + + +class SignalHandlingTests(helper.CPWebCase): + def test_SIGHUP_tty(self): + # When not daemonized, SIGHUP should shut down the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip("skipped (no SIGHUP) ") + + # Spawn the process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_SIGHUP_tty"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGHUP + os.kill(p.get_pid(), SIGHUP) + # This might hang if things aren't working right, but meh. + p.join() + + def test_SIGHUP_daemonized(self): + # When daemonized, SIGHUP should restart the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip("skipped (no SIGHUP) ") + + if os.name not in ['posix']: + return self.skip("skipped (not on posix) ") + + # Spawn the process and wait, when this returns, the original process + # is finished. If it daemonized properly, we should still be able + # to access pages. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True) + p.write_conf( + extra='test_case_name: "test_SIGHUP_daemonized"') + p.start(imports='cherrypy.test._test_states_demo') + + pid = p.get_pid() + try: + # Send a SIGHUP + os.kill(pid, SIGHUP) + # Give the server some time to restart + time.sleep(2) + self.getPage("/pid") + self.assertStatus(200) + new_pid = int(self.body) + self.assertNotEqual(new_pid, pid) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + def test_SIGTERM(self): + # SIGTERM should shut down the server whether daemonized or not. + try: + from signal import SIGTERM + except ImportError: + return self.skip("skipped (no SIGTERM) ") + + try: + from os import kill + except ImportError: + return self.skip("skipped (no os.kill) ") + + # Spawn a normal, undaemonized process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_SIGTERM"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + if os.name in ['posix']: + # Spawn a daemonized process and test again. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True) + p.write_conf( + extra='test_case_name: "test_SIGTERM_2"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + def test_signal_handler_unsubscribe(self): + try: + from signal import SIGTERM + except ImportError: + return self.skip("skipped (no SIGTERM) ") + + try: + from os import kill + except ImportError: + return self.skip("skipped (no os.kill) ") + + # Spawn a normal, undaemonized process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra="""unsubsig: True +test_case_name: "test_signal_handler_unsubscribe" +""") + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + # Assert the old handler ran. + target_line = open(p.error_log, 'rb').readlines()[-10] + if not ntob("I am an old SIGTERM handler.") in target_line: + self.fail("Old SIGTERM handler did not run.\n%r" % target_line) + diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py new file mode 100644 index 0000000..871420b --- /dev/null +++ b/cherrypy/test/test_static.py @@ -0,0 +1,300 @@ +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob +from cherrypy._cpcompat import BytesIO + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +has_space_filepath = os.path.join(curdir, 'static', 'has space.html') +bigfile_filepath = os.path.join(curdir, "static", "bigfile.log") +BIGFILE_SIZE = 1024 * 1024 +import threading + +import cherrypy +from cherrypy.lib import static +from cherrypy.test import helper + + +class StaticTest(helper.CPWebCase): + + def setup_server(): + if not os.path.exists(has_space_filepath): + open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n')) + if not os.path.exists(bigfile_filepath): + open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE)) + + class Root: + + def bigfile(self): + from cherrypy.lib import static + self.f = static.serve_file(bigfile_filepath) + return self.f + bigfile.exposed = True + bigfile._cp_config = {'response.stream': True} + + def tell(self): + if self.f.input.closed: + return '' + return repr(self.f.input.tell()).rstrip('L') + tell.exposed = True + + def fileobj(self): + f = open(os.path.join(curdir, 'style.css'), 'rb') + return static.serve_fileobj(f, content_type='text/css') + fileobj.exposed = True + + def bytesio(self): + f = BytesIO(ntob('Fee\nfie\nfo\nfum')) + return static.serve_fileobj(f, content_type='text/plain') + bytesio.exposed = True + + class Static: + + def index(self): + return 'You want the Baron? You can have the Baron!' + index.exposed = True + + def dynamic(self): + return "This is a DYNAMIC page" + dynamic.exposed = True + + + root = Root() + root.static = Static() + + rootconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + '/style.css': { + 'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(curdir, 'style.css'), + }, + '/docroot': { + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, + '/error': { + 'tools.staticdir.on': True, + 'request.show_tracebacks': True, + }, + } + rootApp = cherrypy.Application(root) + rootApp.merge(rootconf) + + test_app_conf = { + '/test': { + 'tools.staticdir.index': 'index.html', + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + }, + } + testApp = cherrypy.Application(Static()) + testApp.merge(test_app_conf) + + vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) + cherrypy.tree.graft(vhost) + setup_server = staticmethod(setup_server) + + + def teardown_server(): + for f in (has_space_filepath, bigfile_filepath): + if os.path.exists(f): + try: + os.unlink(f) + except: + pass + teardown_server = staticmethod(teardown_server) + + + def testStatic(self): + self.getPage("/static/index.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Using a staticdir.root value in a subdir... + self.getPage("/docroot/index.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Check a filename with spaces in it + self.getPage("/static/has%20space.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + self.getPage("/style.css") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css') + # Note: The body should be exactly 'Dummy stylesheet\n', but + # unfortunately some tools such as WinZip sometimes turn \n + # into \r\n on Windows when extracting the CherryPy tarball so + # we just check the content + self.assertMatchesBody('^Dummy stylesheet') + + def test_fallthrough(self): + # Test that NotFound will then try dynamic handlers (see [878]). + self.getPage("/static/dynamic") + self.assertBody("This is a DYNAMIC page") + + # Check a directory via fall-through to dynamic handler. + self.getPage("/static/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('You want the Baron? You can have the Baron!') + + def test_index(self): + # Check a directory via "staticdir.index". + self.getPage("/docroot/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + # The same page should be returned even if redirected. + self.getPage("/docroot") + self.assertStatus(301) + self.assertHeader('Location', '%s/docroot/' % self.base()) + self.assertMatchesBody("This resource .* " + "%s/docroot/." % (self.base(), self.base())) + + def test_config_errors(self): + # Check that we get an error if no .file or .dir + self.getPage("/error/thing.html") + self.assertErrorPage(500) + self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 " + "(positional )?arguments \(0 given\)")) + + def test_security(self): + # Test up-level security + self.getPage("/static/../../test/style.css") + self.assertStatus((400, 403)) + + def test_modif(self): + # Test modified-since on a reasonably-large file + self.getPage("/static/dirback.jpg") + self.assertStatus("200 OK") + lastmod = "" + for k, v in self.headers: + if k == 'Last-Modified': + lastmod = v + ims = ("If-Modified-Since", lastmod) + self.getPage("/static/dirback.jpg", headers=[ims]) + self.assertStatus(304) + self.assertNoHeader("Content-Type") + self.assertNoHeader("Content-Length") + self.assertNoHeader("Content-Disposition") + self.assertBody("") + + def test_755_vhost(self): + self.getPage("/test/", [('Host', 'virt.net')]) + self.assertStatus(200) + self.getPage("/test", [('Host', 'virt.net')]) + self.assertStatus(301) + self.assertHeader('Location', self.scheme + '://virt.net/test/') + + def test_serve_fileobj(self): + self.getPage("/fileobj") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + self.assertMatchesBody('^Dummy stylesheet') + + def test_serve_bytesio(self): + self.getPage("/bytesio") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.assertHeader('Content-Length', 14) + self.assertMatchesBody('Fee\nfie\nfo\nfum') + + def test_file_stream(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/bigfile", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + + body = ntob('') + remaining = BIGFILE_SIZE + while remaining > 0: + data = response.fp.read(65536) + if not data: + break + body += data + remaining -= len(data) + + if self.scheme == "https": + newconn = HTTPSConnection + else: + newconn = HTTPConnection + s, h, b = helper.webtest.openURL( + ntob("/tell"), headers=[], host=self.HOST, port=self.PORT, + http_conn=newconn) + if not b: + # The file was closed on the server. + tell_position = BIGFILE_SIZE + else: + tell_position = int(b) + + expected = len(body) + if tell_position >= BIGFILE_SIZE: + # We can't exactly control how much content the server asks for. + # Fudge it by only checking the first half of the reads. + if expected < (BIGFILE_SIZE / 2): + self.fail( + "The file should have advanced to position %r, but has " + "already advanced to the end of the file. It may not be " + "streamed as intended, or at the wrong chunk size (64k)" % + expected) + elif tell_position < expected: + self.fail( + "The file should have advanced to position %r, but has " + "only advanced to position %r. It may not be streamed " + "as intended, or at the wrong chunk size (65536)" % + (expected, tell_position)) + + if body != ntob("x" * BIGFILE_SIZE): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (BIGFILE_SIZE, body[:50], len(body))) + conn.close() + + def test_file_stream_deadlock(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request but abort early. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/bigfile", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + body = response.fp.read(65536) + if body != ntob("x" * len(body)): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (65536, body[:50], len(body))) + response.close() + conn.close() + + # Make a second request, which should fetch the whole file. + self.persistent = False + self.getPage("/bigfile") + if self.body != ntob("x" * BIGFILE_SIZE): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (BIGFILE_SIZE, self.body[:50], len(body))) + diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py new file mode 100644 index 0000000..02bacda --- /dev/null +++ b/cherrypy/test/test_tools.py @@ -0,0 +1,399 @@ +"""Test the various means of instantiating and invoking tools.""" + +import gzip +import sys +from cherrypy._cpcompat import BytesIO, copyitems, itervalues +from cherrypy._cpcompat import IncompleteRead, ntob, ntou, py3k, xrange +import time +timeout = 0.2 +import types + +import cherrypy +from cherrypy import tools + + +europoundUnicode = ntou('\x80\xa3') + + +# Client-side code # + +from cherrypy.test import helper + + +class ToolTests(helper.CPWebCase): + def setup_server(): + + # Put check_access in a custom toolbox with its own namespace + myauthtools = cherrypy._cptools.Toolbox("myauth") + + def check_access(default=False): + if not getattr(cherrypy.request, "userid", default): + raise cherrypy.HTTPError(401) + myauthtools.check_access = cherrypy.Tool('before_request_body', check_access) + + def numerify(): + def number_it(body): + for chunk in body: + for k, v in cherrypy.request.numerify_map: + chunk = chunk.replace(k, v) + yield chunk + cherrypy.response.body = number_it(cherrypy.response.body) + + class NumTool(cherrypy.Tool): + def _setup(self): + def makemap(): + m = self._merged_args().get("map", {}) + cherrypy.request.numerify_map = copyitems(m) + cherrypy.request.hooks.attach('on_start_resource', makemap) + + def critical(): + cherrypy.request.error_response = cherrypy.HTTPError(502).set_response + critical.failsafe = True + + cherrypy.request.hooks.attach('on_start_resource', critical) + cherrypy.request.hooks.attach(self._point, self.callable) + + tools.numerify = NumTool('before_finalize', numerify) + + # It's not mandatory to inherit from cherrypy.Tool. + class NadsatTool: + + def __init__(self): + self.ended = {} + self._name = "nadsat" + + def nadsat(self): + def nadsat_it_up(body): + for chunk in body: + chunk = chunk.replace(ntob("good"), ntob("horrorshow")) + chunk = chunk.replace(ntob("piece"), ntob("lomtick")) + yield chunk + cherrypy.response.body = nadsat_it_up(cherrypy.response.body) + nadsat.priority = 0 + + def cleanup(self): + # This runs after the request has been completely written out. + cherrypy.response.body = [ntob("razdrez")] + id = cherrypy.request.params.get("id") + if id: + self.ended[id] = True + cleanup.failsafe = True + + def _setup(self): + cherrypy.request.hooks.attach('before_finalize', self.nadsat) + cherrypy.request.hooks.attach('on_end_request', self.cleanup) + tools.nadsat = NadsatTool() + + def pipe_body(): + cherrypy.request.process_request_body = False + clen = int(cherrypy.request.headers['Content-Length']) + cherrypy.request.body = cherrypy.request.rfile.read(clen) + + # Assert that we can use a callable object instead of a function. + class Rotator(object): + def __call__(self, scale): + r = cherrypy.response + r.collapse_body() + if py3k: + r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] + else: + r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] + cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) + + def stream_handler(next_handler, *args, **kwargs): + cherrypy.response.output = o = BytesIO() + try: + response = next_handler(*args, **kwargs) + # Ignore the response and return our accumulated output instead. + return o.getvalue() + finally: + o.close() + cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(stream_handler) + + class Root: + def index(self): + return "Howdy earth!" + index.exposed = True + + def tarfile(self): + cherrypy.response.output.write(ntob('I am ')) + cherrypy.response.output.write(ntob('a tarfile')) + tarfile.exposed = True + tarfile._cp_config = {'tools.streamer.on': True} + + def euro(self): + hooks = list(cherrypy.request.hooks['before_finalize']) + hooks.sort() + cbnames = [x.callback.__name__ for x in hooks] + assert cbnames == ['gzip'], cbnames + priorities = [x.priority for x in hooks] + assert priorities == [80], priorities + yield ntou("Hello,") + yield ntou("world") + yield europoundUnicode + euro.exposed = True + + # Bare hooks + def pipe(self): + return cherrypy.request.body + pipe.exposed = True + pipe._cp_config = {'hooks.before_request_body': pipe_body} + + # Multiple decorators; include kwargs just for fun. + # Note that rotator must run before gzip. + def decorated_euro(self, *vpath): + yield ntou("Hello,") + yield ntou("world") + yield europoundUnicode + decorated_euro.exposed = True + decorated_euro = tools.gzip(compress_level=6)(decorated_euro) + decorated_euro = tools.rotator(scale=3)(decorated_euro) + + root = Root() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in itervalues(dct): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) + + + # METHOD ONE: + # Declare Tools in _cp_config + class Demo(Test): + + _cp_config = {"tools.nadsat.on": True} + + def index(self, id=None): + return "A good piece of cherry pie" + + def ended(self, id): + return repr(tools.nadsat.ended[id]) + + def err(self, id=None): + raise ValueError() + + def errinstream(self, id=None): + yield "nonconfidential" + raise ValueError() + yield "confidential" + + # METHOD TWO: decorator using Tool() + # We support Python 2.3, but the @-deco syntax would look like this: + # @tools.check_access() + def restricted(self): + return "Welcome!" + restricted = myauthtools.check_access()(restricted) + userid = restricted + + def err_in_onstart(self): + return "success!" + + def stream(self, id=None): + for x in xrange(100000000): + yield str(x) + stream._cp_config = {'response.stream': True} + + + conf = { + # METHOD THREE: + # Declare Tools in detached config + '/demo': { + 'tools.numerify.on': True, + 'tools.numerify.map': {ntob("pie"): ntob("3.14159")}, + }, + '/demo/restricted': { + 'request.show_tracebacks': False, + }, + '/demo/userid': { + 'request.show_tracebacks': False, + 'myauth.check_access.default': True, + }, + '/demo/errinstream': { + 'response.stream': True, + }, + '/demo/err_in_onstart': { + # Because this isn't a dict, on_start_resource will error. + 'tools.numerify.map': "pie->3.14159" + }, + # Combined tools + '/euro': { + 'tools.gzip.on': True, + 'tools.encode.on': True, + }, + # Priority specified in config + '/decorated_euro/subpath': { + 'tools.gzip.priority': 10, + }, + # Handler wrappers + '/tarfile': {'tools.streamer.on': True} + } + app = cherrypy.tree.mount(root, config=conf) + app.request_class.namespaces['myauth'] = myauthtools + + if sys.version_info >= (2, 5): + from cherrypy.test import _test_decorators + root.tooldecs = _test_decorators.ToolExamples() + setup_server = staticmethod(setup_server) + + def testHookErrors(self): + self.getPage("/demo/?id=1") + # If body is "razdrez", then on_end_request is being called too early. + self.assertBody("A horrorshow lomtick of cherry 3.14159") + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/1") + self.assertBody("True") + + valerr = '\n raise ValueError()\nValueError' + self.getPage("/demo/err?id=3") + # If body is "razdrez", then on_end_request is being called too early. + self.assertErrorPage(502, pattern=valerr) + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/3") + self.assertBody("True") + + # If body is "razdrez", then on_end_request is being called too early. + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage("/demo/errinstream?id=5") + # Because this error is raised after the response body has + # started, the status should not change to an error status. + self.assertStatus("200 OK") + self.assertBody("nonconfidential") + else: + # Because this error is raised after the response body has + # started, and because it's chunked output, an error is raised by + # the HTTP client when it encounters incomplete output. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + "/demo/errinstream?id=5") + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/5") + self.assertBody("True") + + # Test the "__call__" technique (compile-time decorator). + self.getPage("/demo/restricted") + self.assertErrorPage(401) + + # Test compile-time decorator with kwargs from config. + self.getPage("/demo/userid") + self.assertBody("Welcome!") + + def testEndRequestOnDrop(self): + old_timeout = None + try: + httpserver = cherrypy.server.httpserver + old_timeout = httpserver.timeout + except (AttributeError, IndexError): + return self.skip() + + try: + httpserver.timeout = timeout + + # Test that on_end_request is called even if the client drops. + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest("GET", "/demo/stream?id=9", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + # Skip the rest of the request and close the conn. This will + # cause the server's active socket to error, which *should* + # result in the request being aborted, and request.close being + # called all the way up the stack (including WSGI middleware), + # eventually calling our on_end_request hook. + finally: + self.persistent = False + time.sleep(timeout * 2) + # Test that the on_end_request hook was called. + self.getPage("/demo/ended/9") + self.assertBody("True") + finally: + if old_timeout is not None: + httpserver.timeout = old_timeout + + def testGuaranteedHooks(self): + # The 'critical' on_start_resource hook is 'failsafe' (guaranteed + # to run even if there are failures in other on_start methods). + # This is NOT true of the other hooks. + # Here, we have set up a failure in NumerifyTool.numerify_map, + # but our 'critical' hook should run and set the error to 502. + self.getPage("/demo/err_in_onstart") + self.assertErrorPage(502) + self.assertInBody("AttributeError: 'str' object has no attribute 'items'") + + def testCombinedTools(self): + expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8') + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) + zfile.write(expectedResult) + zfile.close() + + self.getPage("/euro", headers=[("Accept-Encoding", "gzip"), + ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")]) + self.assertInBody(zbuf.getvalue()[:3]) + + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6) + zfile.write(expectedResult) + zfile.close() + + self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")]) + self.assertInBody(zbuf.getvalue()[:3]) + + # This returns a different value because gzip's priority was + # lowered in conf, allowing the rotator to run after gzip. + # Of course, we don't want breakage in production apps, + # but it proves the priority was changed. + self.getPage("/decorated_euro/subpath", + headers=[("Accept-Encoding", "gzip")]) + if py3k: + self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) + else: + self.assertInBody(''.join([chr((ord(x) + 3) % 256) for x in zbuf.getvalue()])) + + def testBareHooks(self): + content = "bit of a pain in me gulliver" + self.getPage("/pipe", + headers=[("Content-Length", str(len(content))), + ("Content-Type", "text/plain")], + method="POST", body=content) + self.assertBody(content) + + def testHandlerWrapperTool(self): + self.getPage("/tarfile") + self.assertBody("I am a tarfile") + + def testToolWithConfig(self): + if not sys.version_info >= (2, 5): + return self.skip("skipped (Python 2.5+ only)") + + self.getPage('/tooldecs/blah') + self.assertHeader('Content-Type', 'application/data') + + def testWarnToolOn(self): + # get + try: + numon = cherrypy.tools.numerify.on + except AttributeError: + pass + else: + raise AssertionError("Tool.on did not error as it should have.") + + # set + try: + cherrypy.tools.numerify.on = True + except AttributeError: + pass + else: + raise AssertionError("Tool.on did not error as it should have.") + diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py new file mode 100644 index 0000000..aab2786 --- /dev/null +++ b/cherrypy/test/test_tutorials.py @@ -0,0 +1,201 @@ +import sys + +import cherrypy +from cherrypy.test import helper + + +class TutorialTest(helper.CPWebCase): + + def setup_server(cls): + + conf = cherrypy.config.copy() + + def load_tut_module(name): + """Import or reload tutorial module as needed.""" + cherrypy.config.reset() + cherrypy.config.update(conf) + + target = "cherrypy.tutorial." + name + if target in sys.modules: + module = reload(sys.modules[target]) + else: + module = __import__(target, globals(), locals(), ['']) + # The above import will probably mount a new app at "". + app = cherrypy.tree.apps[""] + + app.root.load_tut_module = load_tut_module + app.root.sessions = sessions + app.root.traceback_setting = traceback_setting + + cls.supervisor.sync_apps() + load_tut_module.exposed = True + + def sessions(): + cherrypy.config.update({"tools.sessions.on": True}) + sessions.exposed = True + + def traceback_setting(): + return repr(cherrypy.request.show_tracebacks) + traceback_setting.exposed = True + + class Dummy: + pass + root = Dummy() + root.load_tut_module = load_tut_module + cherrypy.tree.mount(root) + setup_server = classmethod(setup_server) + + + def test01HelloWorld(self): + self.getPage("/load_tut_module/tut01_helloworld") + self.getPage("/") + self.assertBody('Hello world!') + + def test02ExposeMethods(self): + self.getPage("/load_tut_module/tut02_expose_methods") + self.getPage("/showMessage") + self.assertBody('Hello world!') + + def test03GetAndPost(self): + self.getPage("/load_tut_module/tut03_get_and_post") + + # Try different GET queries + self.getPage("/greetUser?name=Bob") + self.assertBody("Hey Bob, what's up?") + + self.getPage("/greetUser") + self.assertBody('Please enter your name here.') + + self.getPage("/greetUser?name=") + self.assertBody('No, really, enter your name here.') + + # Try the same with POST + self.getPage("/greetUser", method="POST", body="name=Bob") + self.assertBody("Hey Bob, what's up?") + + self.getPage("/greetUser", method="POST", body="name=") + self.assertBody('No, really, enter your name here.') + + def test04ComplexSite(self): + self.getPage("/load_tut_module/tut04_complex_site") + msg = ''' +

Here are some extra useful links:

+ + + +

[Return to links page]

''' + self.getPage("/links/extra/") + self.assertBody(msg) + + def test05DerivedObjects(self): + self.getPage("/load_tut_module/tut05_derived_objects") + msg = ''' + + + Another Page + + +

Another Page

+ +

+ And this is the amazing second page! +

+ + + + ''' + self.getPage("/another/") + self.assertBody(msg) + + def test06DefaultMethod(self): + self.getPage("/load_tut_module/tut06_default_method") + self.getPage('/hendrik') + self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' + '(back)') + + def test07Sessions(self): + self.getPage("/load_tut_module/tut07_sessions") + self.getPage("/sessions") + + self.getPage('/') + self.assertBody("\n During your current session, you've viewed this" + "\n page 1 times! Your life is a patio of fun!" + "\n ") + + self.getPage('/', self.cookies) + self.assertBody("\n During your current session, you've viewed this" + "\n page 2 times! Your life is a patio of fun!" + "\n ") + + def test08GeneratorsAndYield(self): + self.getPage("/load_tut_module/tut08_generators_and_yield") + self.getPage('/') + self.assertBody('

Generators rule!

' + '

List of users:

' + 'Remi
Carlos
Hendrik
Lorenzo Lamas
' + '') + + def test09Files(self): + self.getPage("/load_tut_module/tut09_files") + + # Test upload + filesize = 5 + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", str(105 + filesize))] + b = '--x\n' + \ + 'Content-Disposition: form-data; name="myFile"; filename="hello.txt"\r\n' + \ + 'Content-Type: text/plain\r\n' + \ + '\r\n' + \ + 'a' * filesize + '\n' + \ + '--x--\n' + self.getPage('/upload', h, "POST", b) + self.assertBody(''' + + myFile length: %d
+ myFile filename: hello.txt
+ myFile mime-type: text/plain + + ''' % filesize) + + # Test download + self.getPage('/download') + self.assertStatus("200 OK") + self.assertHeader("Content-Type", "application/x-download") + self.assertHeader("Content-Disposition", + # Make sure the filename is quoted. + 'attachment; filename="pdf_file.pdf"') + self.assertEqual(len(self.body), 85698) + + def test10HTTPErrors(self): + self.getPage("/load_tut_module/tut10_http_errors") + + self.getPage("/") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + + self.getPage("/traceback_setting") + setting = self.body + self.getPage("/toggleTracebacks") + self.assertStatus((302, 303)) + self.getPage("/traceback_setting") + self.assertBody(str(not eval(setting))) + + self.getPage("/error?code=500") + self.assertStatus(500) + self.assertInBody("The server encountered an unexpected condition " + "which prevented it from fulfilling the request.") + + self.getPage("/error?code=403") + self.assertStatus(403) + self.assertInBody("

You can't do that!

") + + self.getPage("/messageArg") + self.assertStatus(500) + self.assertInBody("If you construct an HTTPError with a 'message'") + diff --git a/cherrypy/test/test_virtualhost.py b/cherrypy/test/test_virtualhost.py new file mode 100644 index 0000000..dbd2dbc --- /dev/null +++ b/cherrypy/test/test_virtualhost.py @@ -0,0 +1,107 @@ +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +import cherrypy +from cherrypy.test import helper + + +class VirtualHostTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "Hello, world" + index.exposed = True + + def dom4(self): + return "Under construction" + dom4.exposed = True + + def method(self, value): + return "You sent %s" % value + method.exposed = True + + class VHost: + def __init__(self, sitename): + self.sitename = sitename + + def index(self): + return "Welcome to %s" % self.sitename + index.exposed = True + + def vmethod(self, value): + return "You sent %s" % value + vmethod.exposed = True + + def url(self): + return cherrypy.url("nextpage") + url.exposed = True + + # Test static as a handler (section must NOT include vhost prefix) + static = cherrypy.tools.staticdir.handler(section='/static', dir=curdir) + + root = Root() + root.mydom2 = VHost("Domain 2") + root.mydom3 = VHost("Domain 3") + hostmap = {'www.mydom2.com': '/mydom2', + 'www.mydom3.com': '/mydom3', + 'www.mydom4.com': '/dom4', + } + cherrypy.tree.mount(root, config={ + '/': {'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)}, + # Test static in config (section must include vhost prefix) + '/mydom2/static2': {'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, + }) + setup_server = staticmethod(setup_server) + + def testVirtualHost(self): + self.getPage("/", [('Host', 'www.mydom1.com')]) + self.assertBody('Hello, world') + self.getPage("/mydom2/", [('Host', 'www.mydom1.com')]) + self.assertBody('Welcome to Domain 2') + + self.getPage("/", [('Host', 'www.mydom2.com')]) + self.assertBody('Welcome to Domain 2') + self.getPage("/", [('Host', 'www.mydom3.com')]) + self.assertBody('Welcome to Domain 3') + self.getPage("/", [('Host', 'www.mydom4.com')]) + self.assertBody('Under construction') + + # Test GET, POST, and positional params + self.getPage("/method?value=root") + self.assertBody("You sent root") + self.getPage("/vmethod?value=dom2+GET", [('Host', 'www.mydom2.com')]) + self.assertBody("You sent dom2 GET") + self.getPage("/vmethod", [('Host', 'www.mydom3.com')], method="POST", + body="value=dom3+POST") + self.assertBody("You sent dom3 POST") + self.getPage("/vmethod/pos", [('Host', 'www.mydom3.com')]) + self.assertBody("You sent pos") + + # Test that cherrypy.url uses the browser url, not the virtual url + self.getPage("/url", [('Host', 'www.mydom2.com')]) + self.assertBody("%s://www.mydom2.com/nextpage" % self.scheme) + + def test_VHost_plus_Static(self): + # Test static as a handler + self.getPage("/static/style.css", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + + # Test static in config + self.getPage("/static2/dirback.jpg", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'image/jpeg') + + # Test static config with "index" arg + self.getPage("/static2/", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertBody('Hello, world\r\n') + # Since tools.trailing_slash is on by default, this should redirect + self.getPage("/static2", [('Host', 'www.mydom2.com')]) + self.assertStatus(301) + diff --git a/cherrypy/test/test_wsgi_ns.py b/cherrypy/test/test_wsgi_ns.py new file mode 100644 index 0000000..e3c6ce6 --- /dev/null +++ b/cherrypy/test/test_wsgi_ns.py @@ -0,0 +1,91 @@ +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.test import helper + + +class WSGI_Namespace_Test(helper.CPWebCase): + + def setup_server(): + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + def __iter__(self): + return self + + def next(self): + return self.iter.next() + def __next__(self): + return next(self.iter) + + def close(self): + if hasattr(self.appresults, "close"): + self.appresults.close() + + + class ChangeCase(object): + + def __init__(self, app, to=None): + self.app = app + self.to = to + + def __call__(self, environ, start_response): + res = self.app(environ, start_response) + class CaseResults(WSGIResponse): + def next(this): + return getattr(this.iter.next(), self.to)() + def __next__(this): + return getattr(next(this.iter), self.to)() + return CaseResults(res) + + class Replacer(object): + + def __init__(self, app, map={}): + self.app = app + self.map = map + + def __call__(self, environ, start_response): + res = self.app(environ, start_response) + class ReplaceResults(WSGIResponse): + def next(this): + line = this.iter.next() + for k, v in self.map.iteritems(): + line = line.replace(k, v) + return line + def __next__(this): + line = next(this.iter) + for k, v in self.map.items(): + line = line.replace(k, v) + return line + return ReplaceResults(res) + + class Root(object): + + def index(self): + return "HellO WoRlD!" + index.exposed = True + + + root_conf = {'wsgi.pipeline': [('replace', Replacer)], + 'wsgi.replace.map': {ntob('L'): ntob('X'), + ntob('l'): ntob('r')}, + } + + app = cherrypy.Application(Root()) + app.wsgiapp.pipeline.append(('changecase', ChangeCase)) + app.wsgiapp.config['changecase'] = {'to': 'upper'} + cherrypy.tree.mount(app, config={'/': root_conf}) + setup_server = staticmethod(setup_server) + + + def test_pipeline(self): + if not cherrypy.server.httpserver: + return self.skip() + + self.getPage("/") + # If body is "HEXXO WORXD!", the middleware was applied out of order. + self.assertBody("HERRO WORRD!") + diff --git a/cherrypy/test/test_wsgi_vhost.py b/cherrypy/test/test_wsgi_vhost.py new file mode 100644 index 0000000..abb1a91 --- /dev/null +++ b/cherrypy/test/test_wsgi_vhost.py @@ -0,0 +1,36 @@ +import cherrypy +from cherrypy.test import helper + + +class WSGI_VirtualHost_Test(helper.CPWebCase): + + def setup_server(): + + class ClassOfRoot(object): + + def __init__(self, name): + self.name = name + + def index(self): + return "Welcome to the %s website!" % self.name + index.exposed = True + + + default = cherrypy.Application(None) + + domains = {} + for year in range(1997, 2008): + app = cherrypy.Application(ClassOfRoot('Class of %s' % year)) + domains['www.classof%s.example' % year] = app + + cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains)) + setup_server = staticmethod(setup_server) + + def test_welcome(self): + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + + for year in range(1997, 2008): + self.getPage("/", headers=[('Host', 'www.classof%s.example' % year)]) + self.assertBody("Welcome to the Class of %s website!" % year) + diff --git a/cherrypy/test/test_wsgiapps.py b/cherrypy/test/test_wsgiapps.py new file mode 100644 index 0000000..d4b8b79 --- /dev/null +++ b/cherrypy/test/test_wsgiapps.py @@ -0,0 +1,118 @@ +from cherrypy._cpcompat import ntob +from cherrypy.test import helper + + +class WSGIGraftTests(helper.CPWebCase): + + def setup_server(): + import os + curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + import cherrypy + + def test_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + output = ['Hello, world!\n', + 'This is a wsgi app running within CherryPy!\n\n'] + keys = list(environ.keys()) + keys.sort() + for k in keys: + output.append('%s: %s\n' % (k,environ[k])) + return [ntob(x, 'utf-8') for x in output] + + def test_empty_string_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + return [ntob('Hello'), ntob(''), ntob(' '), ntob(''), ntob('world')] + + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + def __iter__(self): + return self + + def next(self): + return self.iter.next() + def __next__(self): + return next(self.iter) + + def close(self): + if hasattr(self.appresults, "close"): + self.appresults.close() + + + class ReversingMiddleware(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + results = app(environ, start_response) + class Reverser(WSGIResponse): + def next(this): + line = list(this.iter.next()) + line.reverse() + return "".join(line) + def __next__(this): + line = list(next(this.iter)) + line.reverse() + return bytes(line) + return Reverser(results) + + class Root: + def index(self): + return ntob("I'm a regular CherryPy page handler!") + index.exposed = True + + + cherrypy.tree.mount(Root()) + + cherrypy.tree.graft(test_app, '/hosted/app1') + cherrypy.tree.graft(test_empty_string_app, '/hosted/app3') + + # Set script_name explicitly to None to signal CP that it should + # be pulled from the WSGI environ each time. + app = cherrypy.Application(Root(), script_name=None) + cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2') + setup_server = staticmethod(setup_server) + + wsgi_output = '''Hello, world! +This is a wsgi app running within CherryPy!''' + + def test_01_standard_app(self): + self.getPage("/") + self.assertBody("I'm a regular CherryPy page handler!") + + def test_04_pure_wsgi(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app1") + self.assertHeader("Content-Type", "text/plain") + self.assertInBody(self.wsgi_output) + + def test_05_wrapped_cp_app(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app2/") + body = list("I'm a regular CherryPy page handler!") + body.reverse() + body = "".join(body) + self.assertInBody(body) + + def test_06_empty_string_app(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app3") + self.assertHeader("Content-Type", "text/plain") + self.assertInBody('Hello world') + diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py new file mode 100644 index 0000000..f7a6927 --- /dev/null +++ b/cherrypy/test/test_xmlrpc.py @@ -0,0 +1,179 @@ +import sys +from cherrypy._cpcompat import py3k + +try: + from xmlrpclib import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport +except ImportError: + from xmlrpc.client import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport + +if py3k: + HTTPSTransport = SafeTransport + + # Python 3.0's SafeTransport still mistakenly checks for socket.ssl + import socket + if not hasattr(socket, "ssl"): + socket.ssl = True +else: + class HTTPSTransport(SafeTransport): + """Subclass of SafeTransport to fix sock.recv errors (by using file).""" + + def request(self, host, handler, request_body, verbose=0): + # issue XML-RPC request + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + errcode, errmsg, headers = h.getreply() + if errcode != 200: + raise ProtocolError(host + handler, errcode, errmsg, headers) + + self.verbose = verbose + + # Here's where we differ from the superclass. It says: + # try: + # sock = h._conn.sock + # except AttributeError: + # sock = None + # return self._parse_response(h.getfile(), sock) + + return self.parse_response(h.getfile()) + +import cherrypy + + +def setup_server(): + from cherrypy import _cptools + + class Root: + def index(self): + return "I'm a standard index!" + index.exposed = True + + + class XmlRpc(_cptools.XMLRPCController): + + def foo(self): + return "Hello world!" + foo.exposed = True + + def return_single_item_list(self): + return [42] + return_single_item_list.exposed = True + + def return_string(self): + return "here is a string" + return_string.exposed = True + + def return_tuple(self): + return ('here', 'is', 1, 'tuple') + return_tuple.exposed = True + + def return_dict(self): + return dict(a=1, b=2, c=3) + return_dict.exposed = True + + def return_composite(self): + return dict(a=1,z=26), 'hi', ['welcome', 'friend'] + return_composite.exposed = True + + def return_int(self): + return 42 + return_int.exposed = True + + def return_float(self): + return 3.14 + return_float.exposed = True + + def return_datetime(self): + return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)) + return_datetime.exposed = True + + def return_boolean(self): + return True + return_boolean.exposed = True + + def test_argument_passing(self, num): + return num * 2 + test_argument_passing.exposed = True + + def test_returning_Fault(self): + return Fault(1, "custom Fault response") + test_returning_Fault.exposed = True + + root = Root() + root.xmlrpc = XmlRpc() + cherrypy.tree.mount(root, config={'/': { + 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(), + 'tools.xmlrpc.allow_none': 0, + }}) + + +from cherrypy.test import helper + +class XmlRpcTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + def testXmlRpc(self): + + scheme = self.scheme + if scheme == "https": + url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT) + proxy = ServerProxy(url, transport=HTTPSTransport()) + else: + url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT) + proxy = ServerProxy(url) + + # begin the tests ... + self.getPage("/xmlrpc/foo") + self.assertBody("Hello world!") + + self.assertEqual(proxy.return_single_item_list(), [42]) + self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion') + self.assertEqual(proxy.return_string(), "here is a string") + self.assertEqual(proxy.return_tuple(), list(('here', 'is', 1, 'tuple'))) + self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2}) + self.assertEqual(proxy.return_composite(), + [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']]) + self.assertEqual(proxy.return_int(), 42) + self.assertEqual(proxy.return_float(), 3.14) + self.assertEqual(proxy.return_datetime(), + DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))) + self.assertEqual(proxy.return_boolean(), True) + self.assertEqual(proxy.test_argument_passing(22), 22 * 2) + + # Test an error in the page handler (should raise an xmlrpclib.Fault) + try: + proxy.test_argument_passing({}) + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ("unsupported operand type(s) " + "for *: 'dict' and 'int'")) + else: + self.fail("Expected xmlrpclib.Fault") + + # http://www.cherrypy.org/ticket/533 + # if a method is not found, an xmlrpclib.Fault should be raised + try: + proxy.non_method() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, 'method "non_method" is not supported') + else: + self.fail("Expected xmlrpclib.Fault") + + # Test returning a Fault from the page handler. + try: + proxy.test_returning_Fault() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ("custom Fault response")) + else: + self.fail("Expected xmlrpclib.Fault") + diff --git a/cherrypy/test/webtest.py b/cherrypy/test/webtest.py new file mode 100644 index 0000000..50cfbad --- /dev/null +++ b/cherrypy/test/webtest.py @@ -0,0 +1,575 @@ +"""Extensions to unittest for web frameworks. + +Use the WebCase.getPage method to request a page from your HTTP server. + +Framework Integration +===================== + +If you have control over your server process, you can handle errors +in the server-side of the HTTP conversation a bit better. You must run +both the client (your WebCase tests) and the server in the same process +(but in separate threads, obviously). + +When an error occurs in the framework, call server_error. It will print +the traceback to stdout, and keep any assertions you have from running +(the assumption is that, if the server errors, the page output will not +be of further significance to your tests). +""" + +import os +import pprint +import re +import socket +import sys +import time +import traceback +import types + +from unittest import * +from unittest import _TextTestResult + +from cherrypy._cpcompat import basestring, ntob, py3k, HTTPConnection, HTTPSConnection, unicodestr + + + +def interface(host): + """Return an IP address for a client connection given the server host. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost.""" + if host == '0.0.0.0': + # INADDR_ANY, which should respond on localhost. + return "127.0.0.1" + if host == '::': + # IN6ADDR_ANY, which should respond on localhost. + return "::1" + return host + + +class TerseTestResult(_TextTestResult): + + def printErrors(self): + # Overridden to avoid unnecessary empty line + if self.errors or self.failures: + if self.dots or self.showAll: + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + +class TerseTestRunner(TextTestRunner): + """A test runner class that displays results in textual form.""" + + def _makeResult(self): + return TerseTestResult(self.stream, self.descriptions, self.verbosity) + + def run(self, test): + "Run the given test case or test suite." + # Overridden to remove unnecessary empty lines and separators + result = self._makeResult() + test(result) + result.printErrors() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = list(map(len, (result.failures, result.errors))) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + return result + + +class ReloadingTestLoader(TestLoader): + + def loadTestsFromName(self, name, module=None): + """Return a suite of all tests cases given a string specifier. + + The name may resolve either to a module, a test case class, a + test method within a test case class, or a callable object which + returns a TestCase or TestSuite instance. + + The method optionally resolves the names relative to a given module. + """ + parts = name.split('.') + unused_parts = [] + if module is None: + if not parts: + raise ValueError("incomplete test name: %s" % name) + else: + parts_copy = parts[:] + while parts_copy: + target = ".".join(parts_copy) + if target in sys.modules: + module = reload(sys.modules[target]) + parts = unused_parts + break + else: + try: + module = __import__(target) + parts = unused_parts + break + except ImportError: + unused_parts.insert(0,parts_copy[-1]) + del parts_copy[-1] + if not parts_copy: + raise + parts = parts[1:] + obj = module + for part in parts: + obj = getattr(obj, part) + + if type(obj) == types.ModuleType: + return self.loadTestsFromModule(obj) + elif (((py3k and isinstance(obj, type)) + or isinstance(obj, (type, types.ClassType))) + and issubclass(obj, TestCase)): + return self.loadTestsFromTestCase(obj) + elif type(obj) == types.UnboundMethodType: + if py3k: + return obj.__self__.__class__(obj.__name__) + else: + return obj.im_class(obj.__name__) + elif hasattr(obj, '__call__'): + test = obj() + if not isinstance(test, TestCase) and \ + not isinstance(test, TestSuite): + raise ValueError("calling %s returned %s, " + "not a test" % (obj,test)) + return test + else: + raise ValueError("do not know how to make test from: %s" % obj) + + +try: + # Jython support + if sys.platform[:4] == 'java': + def getchar(): + # Hopefully this is enough + return sys.stdin.read(1) + else: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty, termios + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class WebCase(TestCase): + HOST = "127.0.0.1" + PORT = 8000 + HTTP_CONN = HTTPConnection + PROTOCOL = "HTTP/1.1" + + scheme = "http" + url = None + + status = None + headers = None + body = None + + encoding = 'utf-8' + + time = None + + def get_conn(self, auto_open=False): + """Return a connection to our HTTP server.""" + if self.scheme == "https": + cls = HTTPSConnection + else: + cls = HTTPConnection + conn = cls(self.interface(), self.PORT) + # Automatically re-connect? + conn.auto_open = auto_open + conn.connect() + return conn + + def set_persistent(self, on=True, auto_open=False): + """Make our HTTP_CONN persistent (or not). + + If the 'on' argument is True (the default), then self.HTTP_CONN + will be set to an instance of HTTPConnection (or HTTPS + if self.scheme is "https"). This will then persist across requests. + + We only allow for a single open connection, so if you call this + and we currently have an open connection, it will be closed. + """ + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + if on: + self.HTTP_CONN = self.get_conn(auto_open=auto_open) + else: + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + def _get_persistent(self): + return hasattr(self.HTTP_CONN, "__class__") + def _set_persistent(self, on): + self.set_persistent(on) + persistent = property(_get_persistent, _set_persistent) + + def interface(self): + """Return an IP address for a client connection. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost.""" + return interface(self.HOST) + + def getPage(self, url, headers=None, method="GET", body=None, protocol=None): + """Open the url with debugging support. Return status, headers, body.""" + ServerError.on = False + + if isinstance(url, unicodestr): + url = url.encode('utf-8') + if isinstance(body, unicodestr): + body = body.encode('utf-8') + + self.url = url + self.time = None + start = time.time() + result = openURL(url, headers, method, body, self.HOST, self.PORT, + self.HTTP_CONN, protocol or self.PROTOCOL) + self.time = time.time() - start + self.status, self.headers, self.body = result + + # Build a list of request cookies from the previous response cookies. + self.cookies = [('Cookie', v) for k, v in self.headers + if k.lower() == 'set-cookie'] + + if ServerError.on: + raise ServerError() + return result + + interactive = True + console_height = 30 + + def _handlewebError(self, msg): + print("") + print(" ERROR: %s" % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> " + sys.stdout.write(p) + sys.stdout.flush() + while True: + i = getchar().upper() + if not isinstance(i, type("")): + i = i.decode('ascii') + if i not in "BHSUIRX": + continue + print(i.upper()) # Also prints new line + if i == "B": + for x, line in enumerate(self.body.splitlines()): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write("<-- More -->\r") + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(" \r") + if m == "q": + break + print(line) + elif i == "H": + pprint.pprint(self.headers) + elif i == "S": + print(self.status) + elif i == "U": + print(self.url) + elif i == "I": + # return without raising the normal exception + return + elif i == "R": + raise self.failureException(msg) + elif i == "X": + self.exit() + sys.stdout.write(p) + sys.stdout.flush() + + def exit(self): + sys.exit() + + def assertStatus(self, status, msg=None): + """Fail if self.status != status.""" + if isinstance(status, basestring): + if not self.status == status: + if msg is None: + msg = 'Status (%r) != %r' % (self.status, status) + self._handlewebError(msg) + elif isinstance(status, int): + code = int(self.status[:3]) + if code != status: + if msg is None: + msg = 'Status (%r) != %r' % (self.status, status) + self._handlewebError(msg) + else: + # status is a tuple or list. + match = False + for s in status: + if isinstance(s, basestring): + if self.status == s: + match = True + break + elif int(self.status[:3]) == s: + match = True + break + if not match: + if msg is None: + msg = 'Status (%r) not in %r' % (self.status, status) + self._handlewebError(msg) + + def assertHeader(self, key, value=None, msg=None): + """Fail if (key, [value]) not in self.headers.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + if value is None or str(value) == v: + return v + + if msg is None: + if value is None: + msg = '%r not in headers' % key + else: + msg = '%r:%r not in headers' % (key, value) + self._handlewebError(msg) + + def assertHeaderItemValue(self, key, value, msg=None): + """Fail if the header does not contain the specified value""" + actual_value = self.assertHeader(key, msg=msg) + header_values = map(str.strip, actual_value.split(',')) + if value in header_values: + return value + + if msg is None: + msg = "%r not in %r" % (value, header_values) + self._handlewebError(msg) + + def assertNoHeader(self, key, msg=None): + """Fail if key in self.headers.""" + lowkey = key.lower() + matches = [k for k, v in self.headers if k.lower() == lowkey] + if matches: + if msg is None: + msg = '%r in headers' % key + self._handlewebError(msg) + + def assertBody(self, value, msg=None): + """Fail if value != self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value != self.body: + if msg is None: + msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body) + self._handlewebError(msg) + + def assertInBody(self, value, msg=None): + """Fail if value not in self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value not in self.body: + if msg is None: + msg = '%r not in body: %s' % (value, self.body) + self._handlewebError(msg) + + def assertNotInBody(self, value, msg=None): + """Fail if value in self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value in self.body: + if msg is None: + msg = '%r found in body' % value + self._handlewebError(msg) + + def assertMatchesBody(self, pattern, msg=None, flags=0): + """Fail if value (a regex pattern) is not in self.body.""" + if isinstance(pattern, unicodestr): + pattern = pattern.encode(self.encoding) + if re.search(pattern, self.body, flags) is None: + if msg is None: + msg = 'No match for %r in body' % pattern + self._handlewebError(msg) + + +methods_with_bodies = ("POST", "PUT") + +def cleanHeaders(headers, method, body, host, port): + """Return request headers, with required headers added (if missing).""" + if headers is None: + headers = [] + + # Add the required Host request header if not present. + # [This specifies the host:port of the server, not the client.] + found = False + for k, v in headers: + if k.lower() == 'host': + found = True + break + if not found: + if port == 80: + headers.append(("Host", host)) + else: + headers.append(("Host", "%s:%s" % (host, port))) + + if method in methods_with_bodies: + # Stick in default type and length headers if not present + found = False + for k, v in headers: + if k.lower() == 'content-type': + found = True + break + if not found: + headers.append(("Content-Type", "application/x-www-form-urlencoded")) + headers.append(("Content-Length", str(len(body or "")))) + + return headers + + +def shb(response): + """Return status, headers, body the way we like from a response.""" + if py3k: + h = response.getheaders() + else: + h = [] + key, value = None, None + for line in response.msg.headers: + if line: + if line[0] in " \t": + value += line.strip() + else: + if key and value: + h.append((key, value)) + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + if key and value: + h.append((key, value)) + + return "%s %s" % (response.status, response.reason), h, response.read() + + +def openURL(url, headers=None, method="GET", body=None, + host="127.0.0.1", port=8000, http_conn=HTTPConnection, + protocol="HTTP/1.1"): + """Open the given HTTP resource and return status, headers, and body.""" + + headers = cleanHeaders(headers, method, body, host, port) + + # Trying 10 times is simply in case of socket errors. + # Normal case--it should run once. + for trial in range(10): + try: + # Allow http_conn to be a class or an instance + if hasattr(http_conn, "host"): + conn = http_conn + else: + conn = http_conn(interface(host), port) + + conn._http_vsn_str = protocol + conn._http_vsn = int("".join([x for x in protocol if x.isdigit()])) + + # skip_accept_encoding argument added in python version 2.4 + if sys.version_info < (2, 4): + def putheader(self, header, value): + if header == 'Accept-Encoding' and value == 'identity': + return + self.__class__.putheader(self, header, value) + import new + conn.putheader = new.instancemethod(putheader, conn, conn.__class__) + conn.putrequest(method.upper(), url, skip_host=True) + elif not py3k: + conn.putrequest(method.upper(), url, skip_host=True, + skip_accept_encoding=True) + else: + import http.client + # Replace the stdlib method, which only accepts ASCII url's + def putrequest(self, method, url): + if self._HTTPConnection__response and self._HTTPConnection__response.isclosed(): + self._HTTPConnection__response = None + + if self._HTTPConnection__state == http.client._CS_IDLE: + self._HTTPConnection__state = http.client._CS_REQ_STARTED + else: + raise http.client.CannotSendRequest() + + self._method = method + if not url: + url = ntob('/') + request = ntob(' ').join((method.encode("ASCII"), url, + self._http_vsn_str.encode("ASCII"))) + self._output(request) + import types + conn.putrequest = types.MethodType(putrequest, conn) + + conn.putrequest(method.upper(), url) + + for key, value in headers: + conn.putheader(key, ntob(value, "Latin-1")) + conn.endheaders() + + if body is not None: + conn.send(body) + + # Handle response + response = conn.getresponse() + + s, h, b = shb(response) + + if not hasattr(http_conn, "host"): + # We made our own conn instance. Close it. + conn.close() + + return s, h, b + except socket.error: + time.sleep(0.5) + if trial == 9: + raise + + +# Add any exceptions which your web framework handles +# normally (that you don't want server_error to trap). +ignored_exceptions = [] + +# You'll want set this to True when you can't guarantee +# that each response will immediately follow each request; +# for example, when handling requests via multiple threads. +ignore_all = False + +class ServerError(Exception): + on = False + + +def server_error(exc=None): + """Server debug hook. Return True if exception handled, False if ignored. + + You probably want to wrap this, so you can still handle an error using + your framework when it's ignored. + """ + if exc is None: + exc = sys.exc_info() + + if ignore_all or exc[0] in ignored_exceptions: + return False + else: + ServerError.on = True + print("") + print("".join(traceback.format_exception(*exc))) + return True + diff --git a/cherrypy/tutorial/README.txt b/cherrypy/tutorial/README.txt new file mode 100644 index 0000000..60c0086 --- /dev/null +++ b/cherrypy/tutorial/README.txt @@ -0,0 +1,16 @@ +CherryPy Tutorials +------------------------------------------------------------------------ + +This is a series of tutorials explaining how to develop dynamic web +applications using CherryPy. A couple of notes: + + - Each of these tutorials builds on the ones before it. If you're + new to CherryPy, we recommend you start with 01_helloworld.py and + work your way upwards. :) + + - In most of these tutorials, you will notice that all output is done + by returning normal Python strings, often using simple Python + variable substitution. In most real-world applications, you will + probably want to use a separate template package (like Cheetah, + CherryTemplate or XML/XSL). + diff --git a/cherrypy/tutorial/__init__.py b/cherrypy/tutorial/__init__.py new file mode 100644 index 0000000..c4e2c55 --- /dev/null +++ b/cherrypy/tutorial/__init__.py @@ -0,0 +1,3 @@ + +# This is used in test_config to test unrepr of "from A import B" +thing2 = object() \ No newline at end of file diff --git a/cherrypy/tutorial/bonus-sqlobject.py b/cherrypy/tutorial/bonus-sqlobject.py new file mode 100644 index 0000000..c43feb4 --- /dev/null +++ b/cherrypy/tutorial/bonus-sqlobject.py @@ -0,0 +1,168 @@ +''' +Bonus Tutorial: Using SQLObject + +This is a silly little contacts manager application intended to +demonstrate how to use SQLObject from within a CherryPy2 project. It +also shows how to use inline Cheetah templates. + +SQLObject is an Object/Relational Mapper that allows you to access +data stored in an RDBMS in a pythonic fashion. You create data objects +as Python classes and let SQLObject take care of all the nasty details. + +This code depends on the latest development version (0.6+) of SQLObject. +You can get it from the SQLObject Subversion server. You can find all +necessary information at . This code will NOT +work with the 0.5.x version advertised on their website! + +This code also depends on a recent version of Cheetah. You can find +Cheetah at . + +After starting this application for the first time, you will need to +access the /reset URI in order to create the database table and some +sample data. Accessing /reset again will drop and re-create the table, +so you may want to be careful. :-) + +This application isn't supposed to be fool-proof, it's not even supposed +to be very GOOD. Play around with it some, browse the source code, smile. + +:) + +-- Hendrik Mans +''' + +import cherrypy +from Cheetah.Template import Template +from sqlobject import * + +# configure your database connection here +__connection__ = 'mysql://root:@localhost/test' + +# this is our (only) data class. +class Contact(SQLObject): + lastName = StringCol(length = 50, notNone = True) + firstName = StringCol(length = 50, notNone = True) + phone = StringCol(length = 30, notNone = True, default = '') + email = StringCol(length = 30, notNone = True, default = '') + url = StringCol(length = 100, notNone = True, default = '') + + +class ContactManager: + def index(self): + # Let's display a list of all stored contacts. + contacts = Contact.select() + + template = Template(''' +

All Contacts

+ + #for $contact in $contacts +
$contact.lastName, $contact.firstName + [Edit] + [Delete] +
+ #end for + +

[Add new contact]

+ ''', [locals(), globals()]) + + return template.respond() + + index.exposed = True + + + def edit(self, id = 0): + # we really want id as an integer. Since GET/POST parameters + # are always passed as strings, let's convert it. + id = int(id) + + if id > 0: + # if an id is specified, we're editing an existing contact. + contact = Contact.get(id) + title = "Edit Contact" + else: + # if no id is specified, we're entering a new contact. + contact = None + title = "New Contact" + + + # In the following template code, please note that we use + # Cheetah's $getVar() construct for the form values. We have + # to do this because contact may be set to None (see above). + template = Template(''' +

$title

+ + + + Last Name:
+ First Name:
+ Phone:
+ Email:
+ URL:
+ +
+ ''', [locals(), globals()]) + + return template.respond() + + edit.exposed = True + + + def delete(self, id): + # Delete the specified contact + contact = Contact.get(int(id)) + contact.destroySelf() + return 'Deleted. Return to Index' + + delete.exposed = True + + + def store(self, lastName, firstName, phone, email, url, id = None): + if id and int(id) > 0: + # If an id was specified, update an existing contact. + contact = Contact.get(int(id)) + + # We could set one field after another, but that would + # cause multiple UPDATE clauses. So we'll just do it all + # in a single pass through the set() method. + contact.set( + lastName = lastName, + firstName = firstName, + phone = phone, + email = email, + url = url) + else: + # Otherwise, add a new contact. + contact = Contact( + lastName = lastName, + firstName = firstName, + phone = phone, + email = email, + url = url) + + return 'Stored. Return to Index' + + store.exposed = True + + + def reset(self): + # Drop existing table + Contact.dropTable(True) + + # Create new table + Contact.createTable() + + # Create some sample data + Contact( + firstName = 'Hendrik', + lastName = 'Mans', + email = 'hendrik@mans.de', + phone = '++49 89 12345678', + url = 'http://www.mornography.de') + + return "reset completed!" + + reset.exposed = True + + +print("If you're running this application for the first time, please go to http://localhost:8080/reset once in order to create the database!") + +cherrypy.quickstart(ContactManager()) diff --git a/cherrypy/tutorial/custom_error.html b/cherrypy/tutorial/custom_error.html new file mode 100644 index 0000000..d0f30c8 --- /dev/null +++ b/cherrypy/tutorial/custom_error.html @@ -0,0 +1,14 @@ + + + + + 403 Unauthorized + + +

You can't do that!

+

%(message)s

+

This is a custom error page that is read from a file.

+

%(traceback)s
+ + diff --git a/cherrypy/tutorial/pdf_file.pdf b/cherrypy/tutorial/pdf_file.pdf new file mode 100644 index 0000000000000000000000000000000000000000..38b4f15eabdd65d4a674cb32034361245aa7b97e GIT binary patch literal 85698 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 message for you!' + index.exposed = True + + def showMessage(self): + # Here's the important message! + return "Hello world!" + showMessage.exposed = True + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HelloWorld(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/cherrypy/tutorial/tut03_get_and_post.py b/cherrypy/tutorial/tut03_get_and_post.py new file mode 100644 index 0000000..283477d --- /dev/null +++ b/cherrypy/tutorial/tut03_get_and_post.py @@ -0,0 +1,53 @@ +""" +Tutorial - Passing variables + +This tutorial shows you how to pass GET/POST variables to methods. +""" + +import cherrypy + + +class WelcomePage: + + def index(self): + # Ask for the user's name. + return ''' +

+ What is your name? + + +
''' + index.exposed = True + + def greetUser(self, name = None): + # CherryPy passes all GET and POST variables as method parameters. + # It doesn't make a difference where the variables come from, how + # large their contents are, and so on. + # + # You can define default parameter values as usual. In this + # example, the "name" parameter defaults to None so we can check + # if a name was actually specified. + + if name: + # Greet the user! + return "Hey %s, what's up?" % name + else: + if name is None: + # No name was specified + return 'Please enter your name here.' + else: + return 'No, really, enter your name here.' + greetUser.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(WelcomePage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(WelcomePage(), config=tutconf) diff --git a/cherrypy/tutorial/tut04_complex_site.py b/cherrypy/tutorial/tut04_complex_site.py new file mode 100644 index 0000000..b4d820e --- /dev/null +++ b/cherrypy/tutorial/tut04_complex_site.py @@ -0,0 +1,98 @@ +""" +Tutorial - Multiple objects + +This tutorial shows you how to create a site structure through multiple +possibly nested request handler objects. +""" + +import cherrypy + + +class HomePage: + def index(self): + return ''' +

Hi, this is the home page! Check out the other + fun stuff on this site:

+ + ''' + index.exposed = True + + +class JokePage: + def index(self): + return ''' +

"In Python, how do you create a string of random + characters?" -- "Read a Perl file!"

+

[Return]

''' + index.exposed = True + + +class LinksPage: + def __init__(self): + # Request handler objects can create their own nested request + # handler objects. Simply create them inside their __init__ + # methods! + self.extra = ExtraLinksPage() + + def index(self): + # Note the way we link to the extra links page (and back). + # As you can see, this object doesn't really care about its + # absolute position in the site tree, since we use relative + # links exclusively. + return ''' +

Here are some useful links:

+ + + +

You can check out some extra useful + links here.

+ +

[Return]

+ ''' + index.exposed = True + + +class ExtraLinksPage: + def index(self): + # Note the relative link back to the Links page! + return ''' +

Here are some extra useful links:

+ + + +

[Return to links page]

''' + index.exposed = True + + +# Of course we can also mount request handler objects right here! +root = HomePage() +root.joke = JokePage() +root.links = LinksPage() + +# Remember, we don't need to mount ExtraLinksPage here, because +# LinksPage does that itself on initialization. In fact, there is +# no reason why you shouldn't let your root object take care of +# creating all contained request handler objects. + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(root, config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(root, config=tutconf) + diff --git a/cherrypy/tutorial/tut05_derived_objects.py b/cherrypy/tutorial/tut05_derived_objects.py new file mode 100644 index 0000000..3d4ec9b --- /dev/null +++ b/cherrypy/tutorial/tut05_derived_objects.py @@ -0,0 +1,83 @@ +""" +Tutorial - Object inheritance + +You are free to derive your request handler classes from any base +class you wish. In most real-world applications, you will probably +want to create a central base class used for all your pages, which takes +care of things like printing a common page header and footer. +""" + +import cherrypy + + +class Page: + # Store the page title in a class attribute + title = 'Untitled Page' + + def header(self): + return ''' + + + %s + + +

%s

+ ''' % (self.title, self.title) + + def footer(self): + return ''' + + + ''' + + # Note that header and footer don't get their exposed attributes + # set to True. This isn't necessary since the user isn't supposed + # to call header or footer directly; instead, we'll call them from + # within the actually exposed handler methods defined in this + # class' subclasses. + + +class HomePage(Page): + # Different title for this page + title = 'Tutorial 5' + + def __init__(self): + # create a subpage + self.another = AnotherPage() + + def index(self): + # Note that we call the header and footer methods inherited + # from the Page class! + return self.header() + ''' +

+ Isn't this exciting? There's + another page, too! +

+ ''' + self.footer() + index.exposed = True + + +class AnotherPage(Page): + title = 'Another Page' + + def index(self): + return self.header() + ''' +

+ And this is the amazing second page! +

+ ''' + self.footer() + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HomePage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HomePage(), config=tutconf) + diff --git a/cherrypy/tutorial/tut06_default_method.py b/cherrypy/tutorial/tut06_default_method.py new file mode 100644 index 0000000..fe24f38 --- /dev/null +++ b/cherrypy/tutorial/tut06_default_method.py @@ -0,0 +1,64 @@ +""" +Tutorial - The default method + +Request handler objects can implement a method called "default" that +is called when no other suitable method/object could be found. +Essentially, if CherryPy2 can't find a matching request handler object +for the given request URI, it will use the default method of the object +located deepest on the URI path. + +Using this mechanism you can easily simulate virtual URI structures +by parsing the extra URI string, which you can access through +cherrypy.request.virtualPath. + +The application in this tutorial simulates an URI structure looking +like /users/. Since the bit will not be found (as +there are no matching methods), it is handled by the default method. +""" + +import cherrypy + + +class UsersPage: + + def index(self): + # Since this is just a stupid little example, we'll simply + # display a list of links to random, made-up users. In a real + # application, this could be generated from a database result set. + return ''' + Remi Delon
+ Hendrik Mans
+ Lorenzo Lamas
+ ''' + index.exposed = True + + def default(self, user): + # Here we react depending on the virtualPath -- the part of the + # path that could not be mapped to an object method. In a real + # application, we would probably do some database lookups here + # instead of the silly if/elif/else construct. + if user == 'remi': + out = "Remi Delon, CherryPy lead developer" + elif user == 'hendrik': + out = "Hendrik Mans, CherryPy co-developer & crazy German" + elif user == 'lorenzo': + out = "Lorenzo Lamas, famous actor and singer!" + else: + out = "Unknown user. :-(" + + return '%s (back)' % out + default.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(UsersPage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(UsersPage(), config=tutconf) + diff --git a/cherrypy/tutorial/tut07_sessions.py b/cherrypy/tutorial/tut07_sessions.py new file mode 100644 index 0000000..4b1386b --- /dev/null +++ b/cherrypy/tutorial/tut07_sessions.py @@ -0,0 +1,44 @@ +""" +Tutorial - Sessions + +Storing session data in CherryPy applications is very easy: cherrypy +provides a dictionary called "session" that represents the session +data for the current user. If you use RAM based sessions, you can store +any kind of object into that dictionary; otherwise, you are limited to +objects that can be pickled. +""" + +import cherrypy + + +class HitCounter: + + _cp_config = {'tools.sessions.on': True} + + def index(self): + # Increase the silly hit counter + count = cherrypy.session.get('count', 0) + 1 + + # Store the new value in the session dictionary + cherrypy.session['count'] = count + + # And display a silly hit count message! + return ''' + During your current session, you've viewed this + page %s times! Your life is a patio of fun! + ''' % count + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HitCounter(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HitCounter(), config=tutconf) + diff --git a/cherrypy/tutorial/tut08_generators_and_yield.py b/cherrypy/tutorial/tut08_generators_and_yield.py new file mode 100644 index 0000000..a6fbdc2 --- /dev/null +++ b/cherrypy/tutorial/tut08_generators_and_yield.py @@ -0,0 +1,47 @@ +""" +Bonus Tutorial: Using generators to return result bodies + +Instead of returning a complete result string, you can use the yield +statement to return one result part after another. This may be convenient +in situations where using a template package like CherryPy or Cheetah +would be overkill, and messy string concatenation too uncool. ;-) +""" + +import cherrypy + + +class GeneratorDemo: + + def header(self): + return "

Generators rule!

" + + def footer(self): + return "" + + def index(self): + # Let's make up a list of users for presentation purposes + users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas'] + + # Every yield line adds one part to the total result body. + yield self.header() + yield "

List of users:

" + + for user in users: + yield "%s
" % user + + yield self.footer() + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(GeneratorDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(GeneratorDemo(), config=tutconf) + diff --git a/cherrypy/tutorial/tut09_files.py b/cherrypy/tutorial/tut09_files.py new file mode 100644 index 0000000..4c8e581 --- /dev/null +++ b/cherrypy/tutorial/tut09_files.py @@ -0,0 +1,107 @@ +""" + +Tutorial: File upload and download + +Uploads +------- + +When a client uploads a file to a CherryPy application, it's placed +on disk immediately. CherryPy will pass it to your exposed method +as an argument (see "myFile" below); that arg will have a "file" +attribute, which is a handle to the temporary uploaded file. +If you wish to permanently save the file, you need to read() +from myFile.file and write() somewhere else. + +Note the use of 'enctype="multipart/form-data"' and 'input type="file"' +in the HTML which the client uses to upload the file. + + +Downloads +--------- + +If you wish to send a file to the client, you have two options: +First, you can simply return a file-like object from your page handler. +CherryPy will read the file and serve it as the content (HTTP body) +of the response. However, that doesn't tell the client that +the response is a file to be saved, rather than displayed. +Use cherrypy.lib.static.serve_file for that; it takes four +arguments: + +serve_file(path, content_type=None, disposition=None, name=None) + +Set "name" to the filename that you expect clients to use when they save +your file. Note that the "name" argument is ignored if you don't also +provide a "disposition" (usually "attachement"). You can manually set +"content_type", but be aware that if you also use the encoding tool, it +may choke if the file extension is not recognized as belonging to a known +Content-Type. Setting the content_type to "application/x-download" works +in most cases, and should prompt the user with an Open/Save dialog in +popular browsers. + +""" + +import os +localDir = os.path.dirname(__file__) +absDir = os.path.join(os.getcwd(), localDir) + +import cherrypy +from cherrypy.lib import static + + +class FileDemo(object): + + def index(self): + return """ + +

Upload a file

+
+ filename:
+ +
+

Download a file

+ This one + + """ + index.exposed = True + + def upload(self, myFile): + out = """ + + myFile length: %s
+ myFile filename: %s
+ myFile mime-type: %s + + """ + + # Although this just counts the file length, it demonstrates + # how to read large files in chunks instead of all at once. + # CherryPy reads the uploaded file into a temporary file; + # myFile.file.read reads from that. + size = 0 + while True: + data = myFile.file.read(8192) + if not data: + break + size += len(data) + + return out % (size, myFile.filename, myFile.content_type) + upload.exposed = True + + def download(self): + path = os.path.join(absDir, "pdf_file.pdf") + return static.serve_file(path, "application/x-download", + "attachment", os.path.basename(path)) + download.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(FileDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(FileDemo(), config=tutconf) diff --git a/cherrypy/tutorial/tut10_http_errors.py b/cherrypy/tutorial/tut10_http_errors.py new file mode 100644 index 0000000..dfa5733 --- /dev/null +++ b/cherrypy/tutorial/tut10_http_errors.py @@ -0,0 +1,81 @@ +""" + +Tutorial: HTTP errors + +HTTPError is used to return an error response to the client. +CherryPy has lots of options regarding how such errors are +logged, displayed, and formatted. + +""" + +import os +localDir = os.path.dirname(__file__) +curpath = os.path.normpath(os.path.join(os.getcwd(), localDir)) + +import cherrypy + + +class HTTPErrorDemo(object): + + # Set a custom response for 403 errors. + _cp_config = {'error_page.403' : os.path.join(curpath, "custom_error.html")} + + def index(self): + # display some links that will result in errors + tracebacks = cherrypy.request.show_tracebacks + if tracebacks: + trace = 'off' + else: + trace = 'on' + + return """ + +

Toggle tracebacks %s

+

Click me; I'm a broken link!

+

Use a custom error page from a file.

+

These errors are explicitly raised by the application:

+ +

You can also set the response body + when you raise an error.

+ + """ % trace + index.exposed = True + + def toggleTracebacks(self): + # simple function to toggle tracebacks on and off + tracebacks = cherrypy.request.show_tracebacks + cherrypy.config.update({'request.show_tracebacks': not tracebacks}) + + # redirect back to the index + raise cherrypy.HTTPRedirect('/') + toggleTracebacks.exposed = True + + def error(self, code): + # raise an error based on the get query + raise cherrypy.HTTPError(status = code) + error.exposed = True + + def messageArg(self): + message = ("If you construct an HTTPError with a 'message' " + "argument, it wil be placed on the error page " + "(underneath the status line by default).") + raise cherrypy.HTTPError(500, message=message) + messageArg.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HTTPErrorDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HTTPErrorDemo(), config=tutconf) diff --git a/cherrypy/tutorial/tutorial.conf b/cherrypy/tutorial/tutorial.conf new file mode 100644 index 0000000..43dfa60 --- /dev/null +++ b/cherrypy/tutorial/tutorial.conf @@ -0,0 +1,4 @@ +[global] +server.socket_host = "127.0.0.1" +server.socket_port = 8080 +server.thread_pool = 10 diff --git a/cherrypy/wsgiserver/__init__.py b/cherrypy/wsgiserver/__init__.py new file mode 100644 index 0000000..ee6190f --- /dev/null +++ b/cherrypy/wsgiserver/__init__.py @@ -0,0 +1,14 @@ +__all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', + 'WorkerThread', 'ThreadPool', 'SSLAdapter', + 'CherryPyWSGIServer', + 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', + 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] + +import sys +if sys.version_info < (3, 0): + from wsgiserver2 import * +else: + # Le sigh. Boo for backward-incompatible syntax. + exec('from .wsgiserver3 import *') diff --git a/cherrypy/wsgiserver/__init__.pyc b/cherrypy/wsgiserver/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..212f0f455e21686ffb8b7b1d98bc9c630e90e4d2 GIT binary patch literal 734 zcmYjOZEw>s5WX~pZC$&Lb-aE+{aEe8G~gRT2voK*F{)}Qsx(!ytelsax^b{wy6nI3 z>-Y)$05~T>trI`V-97jC?i~KC@WP}@VDWg>P`XRcMdB4K_C;||G;ZCB3{f4&-2>( zCDZHdN@R&kuP>w$u1(Bgy2=&;D@T_cU-_NSKEI2FK!K(DSVz&-xiQ+f4pA<+= (3, 0): + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + return wsgiserver.CP_makefile(sock, mode, bufsize) + else: + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + return wsgiserver.CP_fileobject(sock, mode, bufsize) + diff --git a/cherrypy/wsgiserver/ssl_pyopenssl.py b/cherrypy/wsgiserver/ssl_pyopenssl.py new file mode 100644 index 0000000..f3d9bf5 --- /dev/null +++ b/cherrypy/wsgiserver/ssl_pyopenssl.py @@ -0,0 +1,256 @@ +"""A library for integrating pyOpenSSL with CherryPy. + +The OpenSSL module must be importable for SSL functionality. +You can obtain it from http://pyopenssl.sourceforge.net/ + +To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of +SSLAdapter. There are two ways to use SSL: + +Method One +---------- + + * ``ssl_adapter.context``: an instance of SSL.Context. + +If this is not None, it is assumed to be an SSL.Context instance, +and will be passed to SSL.Connection on bind(). The developer is +responsible for forming a valid Context object. This approach is +to be preferred for more flexibility, e.g. if the cert and key are +streams instead of files, or need decryption, or SSL.SSLv3_METHOD +is desired instead of the default SSL.SSLv23_METHOD, etc. Consult +the pyOpenSSL documentation for complete options. + +Method Two (shortcut) +--------------------- + + * ``ssl_adapter.certificate``: the filename of the server SSL certificate. + * ``ssl_adapter.private_key``: the filename of the server's private key file. + +Both are None by default. If ssl_adapter.context is None, but .private_key +and .certificate are both given and valid, they will be read, and the +context will be automatically created from them. +""" + +import socket +import threading +import time + +from cherrypy import wsgiserver + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + + +class SSL_fileobject(wsgiserver.CP_fileobject): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + errnum = e.args[0] + if is_reader and errnum in wsgiserver.socket_errors_to_ignore: + return "" + raise socket.error(errnum) + except SSL.Error, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise wsgiserver.NoSSLError() + + raise wsgiserver.FatalSSLAlert(*e.args) + except: + raise + + if time.time() - start > self.ssl_timeout: + raise socket.timeout("timed out") + + def recv(self, *args, **kwargs): + buf = [] + r = super(SSL_fileobject, self).recv + while True: + data = self._safe_call(True, r, *args, **kwargs) + buf.append(data) + p = self._sock.pending() + if not p: + return "".join(buf) + + def sendall(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).sendall, + *args, **kwargs) + + def send(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).send, + *args, **kwargs) + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. + """ + + def __init__(self, *args): + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + def shutdown(self, *args): + self._lock.acquire() + try: + # pyOpenSSL.socket.shutdown takes no args + return self._ssl_conn.shutdown() + finally: + self._lock.release() + + +class pyOpenSSLAdapter(wsgiserver.SSLAdapter): + """A wrapper for integrating pyOpenSSL with CherryPy.""" + + context = None + """An instance of SSL.Context.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """Optional. The filename of CA's intermediate certificate bundle. + + This is needed for cheaper "chained root" SSL certificates, and should be + left as None if not required.""" + + def __init__(self, certificate, private_key, certificate_chain=None): + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + self.context = None + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + self._environ = None + + def bind(self, sock): + """Wrap and return the given socket.""" + if self.context is None: + self.context = self.get_context() + conn = SSLConnection(self.context, sock) + self._environ = self.get_environ() + return conn + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + return sock, self._environ.copy() + + def get_context(self): + """Return an SSL.Context from self attributes.""" + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + c = SSL.Context(SSL.SSLv23_METHOD) + c.use_privatekey_file(self.private_key) + if self.certificate_chain: + c.load_verify_locations(self.certificate_chain) + c.use_certificate_file(self.certificate) + return c + + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + ssl_environ = { + "HTTPS": "on", + # pyOpenSSL doesn't provide access to any of these AFAICT +## 'SSL_PROTOCOL': 'SSLv2', +## SSL_CIPHER string The cipher specification name +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + + if self.certificate: + # Server certificate attributes + cert = open(self.certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), +## 'SSL_SERVER_V_START': Validity of server's certificate (start time), +## 'SSL_SERVER_V_END': Validity of server's certificate (end time), + }) + + for prefix, dn in [("I", cert.get_issuer()), + ("S", cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind("=") + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind("/") + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=-1): + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + f = SSL_fileobject(sock, mode, bufsize) + f.ssl_timeout = timeout + return f + else: + return wsgiserver.CP_fileobject(sock, mode, bufsize) + diff --git a/cherrypy/wsgiserver/wsgiserver2.py b/cherrypy/wsgiserver/wsgiserver2.py new file mode 100644 index 0000000..b6bd499 --- /dev/null +++ b/cherrypy/wsgiserver/wsgiserver2.py @@ -0,0 +1,2322 @@ +"""A high-speed, production ready, thread pooled, generic HTTP server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery):: + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!'] + + server = wsgiserver.CherryPyWSGIServer( + ('0.0.0.0', 8070), my_crazy_app, + server_name='www.cherrypy.example') + server.start() + +The CherryPy WSGI server can serve as many WSGI applications +as you want in one instance by using a WSGIPathInfoDispatcher:: + + d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app}) + server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) + +Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance. + +This won't call the CherryPy engine (application side) at all, only the +HTTP server, which is independent from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not its coupling. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = CherryPyWSGIServer(...) + server.start() + while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return +""" + +__all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'CP_fileobject', + 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', + 'WorkerThread', 'ThreadPool', 'SSLAdapter', + 'CherryPyWSGIServer', + 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', + 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] + +import os +try: + import queue +except: + import Queue as queue +import re +import rfc822 +import socket +import sys +if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 +try: + import cStringIO as StringIO +except ImportError: + import StringIO +DEFAULT_BUFFER_SIZE = -1 + +_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring) + +import threading +import time +import traceback +def format_exc(limit=None): + """Like print_exc() but return a string. Backport for Python 2.3.""" + try: + etype, value, tb = sys.exc_info() + return ''.join(traceback.format_exception(etype, value, tb, limit)) + finally: + etype = value = tb = None + + +from urllib import unquote +from urlparse import urlparse +import warnings + +if sys.version_info >= (3, 0): + bytestr = bytes + unicodestr = str + basestring = (bytes, str) + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 3, the native string type is unicode + return n.encode(encoding) +else: + bytestr = str + unicodestr = unicode + basestring = basestring + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + +LF = ntob('\n') +CRLF = ntob('\r\n') +TAB = ntob('\t') +SPACE = ntob(' ') +COLON = ntob(':') +SEMICOLON = ntob(';') +EMPTY = ntob('') +NUMBER_SIGN = ntob('#') +QUESTION_MARK = ntob('?') +ASTERISK = ntob('*') +FORWARD_SLASH = ntob('/') +quoted_slash = re.compile(ntob("(?i)%2F")) + +import errno + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + +socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") + +socket_errors_to_ignore = plat_specific_errors( + "EPIPE", + "EBADF", "WSAEBADF", + "ENOTSOCK", "WSAENOTSOCK", + "ETIMEDOUT", "WSAETIMEDOUT", + "ECONNREFUSED", "WSAECONNREFUSED", + "ECONNRESET", "WSAECONNRESET", + "ECONNABORTED", "WSAECONNABORTED", + "ENETRESET", "WSAENETRESET", + "EHOSTDOWN", "EHOSTUNREACH", + ) +socket_errors_to_ignore.append("timed out") +socket_errors_to_ignore.append("The read operation timed out") + +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +comma_separated_headers = [ntob(h) for h in + ['Accept', 'Accept-Charset', 'Accept-Encoding', + 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', + 'Connection', 'Content-Encoding', 'Content-Language', 'Expect', + 'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE', + 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', + 'WWW-Authenticate']] + + +import logging +if not hasattr(logging, 'statistics'): logging.statistics = {} + + +def read_headers(rfile, hdict=None): + """Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError("Illegal header line.") + # TODO: what about TE and WWW-Authenticate? + k = k.strip().title() + v = v.strip() + hname = k + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = ", ".join((existing, v)) + hdict[hname] = v + + return hdict + + +class MaxSizeExceeded(Exception): + pass + +class SizeCheckWrapper(object): + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded() + + def read(self, size=None): + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See http://www.cherrypy.org/ticket/421 + if len(data) < 256 or data[-1:] == "\n": + return EMPTY.join(res) + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + self._check_length() + return data + + +class KnownLengthRFile(object): + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + if self.remaining == 0: + return '' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + if self.remaining == 0: + return '' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.remaining -= len(data) + return data + + +class ChunkedRFile(object): + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded("Request Entity Too Large", self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError("Bad chunked transfer size: " + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +## if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError("Request Entity Too Large") + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + "got " + repr(crlf) + ")") + + def read(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + + def readline(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + if not self.closed: + raise ValueError( + "Cannot read trailers until the request body has been read.") + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError("Request Entity Too Large") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + yield line + + def close(self): + self.rfile.close() + + def __iter__(self): + # Shamelessly stolen from StringIO + total = 0 + line = self.readline(sizehint) + while line: + yield line + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + def __init__(self, server, conn): + self.server= server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = ntob("http") + if self.server.ssl_adapter is not None: + self.scheme = ntob("https") + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except MaxSizeExceeded: + self.simple_response("414 Request-URI Too Long", + "The Request-URI sent with the request exceeds the maximum " + "allowed bytes.") + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except MaxSizeExceeded: + self.simple_response("413 Request Entity Too Large", + "The headers sent with the request exceed the maximum " + "allowed bytes.") + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response("400 Bad Request", "HTTP requires CRLF terminators") + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + rp = int(req_protocol[5]), int(req_protocol[7]) + except (ValueError, IndexError): + self.simple_response("400 Bad Request", "Malformed Request-Line") + return False + + self.uri = uri + self.method = method + + # uri may be an abs_path (including "http://host.domain.tld"); + scheme, authority, path = self.parse_request_uri(uri) + if NUMBER_SIGN in path: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return False + + if scheme: + self.scheme = scheme + + qs = EMPTY + if QUESTION_MARK in path: + path, qs = path.split(QUESTION_MARK, 1) + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path". + try: + atoms = [unquote(x) for x in quoted_slash.split(path)] + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + path = "%2F".join(atoms) + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return False + + self.request_protocol = req_protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + + # then all the http headers + try: + read_headers(self.rfile, self.inheaders) + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return False + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + # Both server and client are HTTP/1.1 + if self.inheaders.get("Connection", "") == "close": + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get("Connection", "") != "Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = self.inheaders.get("Transfer-Encoding") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == "chunked": + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get("Expect", "") == "100-continue": + # Don't use simple_response here, because it emits headers + # we don't want. See http://www.cherrypy.org/ticket/951 + msg = self.server.protocol + " 100 Continue\r\n\r\n" + try: + self.conn.wfile.sendall(msg) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return True + + def parse_request_uri(self, uri): + """Parse a Request-URI into (scheme, authority, path). + + Note that Request-URI's must be one of:: + + Request-URI = "*" | absoluteURI | abs_path | authority + + Therefore, a Request-URI which starts with a double forward-slash + cannot be a "net_path":: + + net_path = "//" authority [ abs_path ] + + Instead, it must be interpreted as an "abs_path" with an empty first + path segment:: + + abs_path = "/" path_segments + path_segments = segment *( "/" segment ) + segment = *pchar *( ";" param ) + param = *pchar + """ + if uri == ASTERISK: + return None, None, uri + + i = uri.find('://') + if i > 0 and QUESTION_MARK not in uri[:i]: + # An absoluteURI. + # If there's a scheme (and it must be http or https), then: + # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] + scheme, remainder = uri[:i].lower(), uri[i + 3:] + authority, path = remainder.split(FORWARD_SLASH, 1) + path = FORWARD_SLASH + path + return scheme, authority, path + + if uri.startswith(FORWARD_SLASH): + # An abs_path. + return None, None, uri + else: + # An authority. + return None, uri, None + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get("Content-Length", 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + + if (self.ready and not self.sent_headers): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.conn.wfile.sendall("0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = [self.server.protocol + SPACE + + status + CRLF, + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n"] + + if status[:3] in ("413", "414"): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append("Connection: close\r\n") + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = "400 Bad Request" + + buf.append(CRLF) + if msg: + if isinstance(msg, unicodestr): + msg = msg.encode("ISO-8859-1") + buf.append(msg) + + try: + self.conn.wfile.sendall("".join(buf)) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + buf = [hex(len(chunk))[2:], CRLF, chunk, CRLF] + self.conn.wfile.sendall(EMPTY.join(buf)) + else: + self.conn.wfile.sendall(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if (self.response_protocol == 'HTTP/1.1' + and self.method != 'HEAD'): + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append(("Transfer-Encoding", "chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if "date" not in hkeys: + self.outheaders.append(("Date", rfc822.formatdate())) + + if "server" not in hkeys: + self.outheaders.append(("Server", self.server.server_name)) + + buf = [self.server.protocol + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.sendall(EMPTY.join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + pass + + +class CP_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + def __init__(self, *args, **kwargs): + self.bytes_read = 0 + self.bytes_written = 0 + socket._fileobject.__init__(self, *args, **kwargs) + + def sendall(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error, e: + if e.args[0] not in socket_errors_nonblocking: + raise + + def send(self, data): + bytes_sent = self._sock.send(data) + self.bytes_written += bytes_sent + return bytes_sent + + def flush(self): + if self._wbuf: + buffer = "".join(self._wbuf) + self._wbuf = [] + self.sendall(buffer) + + def recv(self, size): + while True: + try: + data = self._sock.recv(size) + self.bytes_read += len(data) + return data + except socket.error, e: + if (e.args[0] not in socket_errors_nonblocking + and e.args[0] not in socket_error_eintr): + raise + + if not _fileobject_uses_str_type: + def read(self, size=-1): + # Use max, disallow tiny reads in a loop as they are very inefficient. + # We never leave read() with any leftover data from a new recv() call + # in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned by + # recv() minimizes memory usage and fragmentation that occurs when + # rbufsize is large compared to the typical return value of recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, "recv(%d) returned %d bytes" % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + #assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + data = None + recv = self.recv + while data != "\n": + data = recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + + buf.seek(0, 2) # seek end + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when returning + # a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + #assert buf_len == buf.tell() + return buf.getvalue() + else: + def read(self, size=-1): + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = "" + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == "" + buffers = [] + while data != "\n": + data = self.recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + +class HTTPConnection(object): + """An HTTP connection (active socket). + + server: the Server object which received this connection. + socket: the raw socket object (usually TCP) for this connection. + makefile: a fileobject class for reading from the socket. + """ + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = DEFAULT_BUFFER_SIZE + wbufsize = DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + + def __init__(self, server, sock, makefile=CP_fileobject): + self.server = server + self.socket = sock + self.rfile = makefile(sock, "rb", self.rbufsize) + self.wfile = makefile(sock, "wb", self.wbufsize) + self.requests_seen = 0 + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error: + e = sys.exc_info()[1] + errnum = e.args[0] + # sadly SSL sockets return a different (longer) time out string + if errnum == 'timed out' or errnum == 'The read operation timed out': + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See http://www.cherrypy.org/ticket/853 + if (not request_seen) or (req and req.started_request): + # Don't bother writing the 408 if the response + # has already started being written. + if req and not req.sent_headers: + try: + req.simple_response("408 Request Timeout") + except FatalSSLAlert: + # Close the connection. + return + elif errnum not in socket_errors_to_ignore: + self.server.error_log("socket.error %s" % repr(errnum), + level=logging.WARNING, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + return + except (KeyboardInterrupt, SystemExit): + raise + except FatalSSLAlert: + # Close the connection. + return + except NoSSLError: + if req and not req.sent_headers: + # Unwrap our wfile + self.wfile = CP_fileobject(self.socket._sock, "wb", self.wbufsize) + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + self.linger = True + except Exception: + e = sys.exc_info()[1] + self.server.error_log(repr(e), level=logging.ERROR, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + + linger = False + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + # Python's socket module does NOT call close on the kernel socket + # when you call socket.close(). We do so manually here because we + # want this server to send a FIN TCP segment immediately. Note this + # must be called *before* calling socket.close(), because the latter + # drops its reference to the kernel socket. + if hasattr(self.socket, '_sock'): + self.socket._sock.close() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + +class TrueyZero(object): + """An object which equals and does math like the integer '0' but evals True.""" + def __add__(self, other): + return other + def __radd__(self, other): + return other +trueyzero = TrueyZero() + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + + def __init__(self, server): + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and trueyzero or self.conn.requests_seen), + 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and trueyzero or self.conn.rfile.bytes_read), + 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and trueyzero or self.conn.wfile.bytes_written), + 'Work Time': lambda s: self.work_time + ((self.start_time is None) and trueyzero or time.time() - self.start_time), + 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6), + 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6), + } + threading.Thread.__init__(self) + + def run(self): + self.server.stats['Worker Threads'][self.getName()] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + if self.server.stats['Enabled']: + self.start_time = time.time() + try: + conn.communicate() + finally: + conn.close() + if self.server.stats['Enabled']: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit): + exc = sys.exc_info()[1] + self.server.interrupt = exc + + +class ThreadPool(object): + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__(self, server, min=10, max=-1): + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue() + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName("CP Server " + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + def _get_idle(self): + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + idle = property(_get_idle, doc=_get_idle.__doc__) + + def put(self, obj): + self._queue.put(obj) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + for i in range(amount): + if self.max > 0 and len(self._threads) >= self.max: + break + worker = WorkerThread(self.server) + worker.setName("CP Server " + worker.getName()) + self._threads.append(worker) + worker.start() + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + if amount > 0: + for i in range(min(amount, len(self._threads) - self.min)): + # Put a number of shutdown requests on the queue equal + # to 'amount'. Once each of those is processed by a worker, + # that worker will terminate and be culled from our list + # in self.put. + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See http://www.cherrypy.org/ticket/691. + KeyboardInterrupt): + pass + + def _get_qsize(self): + return self._queue.qsize() + qsize = property(_get_qsize) + + + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + except ImportError: + def prevent_socket_inheritance(sock): + """Dummy function, since neither fcntl nor ctypes are available.""" + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class SSLAdapter(object): + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object`` + """ + + def __init__(self, certificate, private_key, certificate_chain=None): + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def wrap(self, sock): + raise NotImplemented + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + raise NotImplemented + + +class HTTPServer(object): + """An HTTP server.""" + + _bind_addr = "127.0.0.1" + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create (default -1 = no limit).""" + + server_name = None + """The name of the server; defaults to socket.gethostname().""" + + protocol = "HTTP/1.1" + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections (default 5).""" + + shutdown_timeout = 5 + """The total time, in seconds, to wait for worker threads to cleanly exit.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = "CherryPy/3.2.2" + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``.""" + + ready = False + """An internal flag which marks whether the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of SSLAdapter (or a subclass). + + You must have the corresponding SSL driver library installed.""" + + def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1, + server_name=None): + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.clear_stats() + + def clear_stats(self): + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, "qsize", None), + 'Threads': lambda s: len(getattr(self.requests, "_threads", [])), + 'Threads Idle': lambda s: getattr(self.requests, "idle", None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum([w['Requests'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Read'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Written'](w) for w + in s['Worker Threads'].values()], 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum([w['Work Time'](w) for w + in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats + + def runtime(self): + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, + self.bind_addr) + + def _get_bind_addr(self): + return self._bind_addr + def _set_bind_addr(self, value): + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + "to listen on all active interfaces.") + self._bind_addr = value + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc="""The interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string.""") + + def start(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + if self.software is None: + self.software = "%s Server" % self.version + + # SSL backward compatibility + if (self.ssl_adapter is None and + getattr(self, 'ssl_certificate', None) and + getattr(self, 'ssl_private_key', None)): + warnings.warn( + "SSL attributes are deprecated in CherryPy 3.2, and will " + "be removed in CherryPy 3.3. Use an ssl_adapter attribute " + "instead.", + DeprecationWarning + ) + try: + from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter + except ImportError: + pass + else: + self.ssl_adapter = pyOpenSSLAdapter( + self.ssl_certificate, self.ssl_private_key, + getattr(self, 'ssl_certificate_chain', None)) + + # Select the appropriate socket + if isinstance(self.bind_addr, basestring): + # AF_UNIX socket + + # So we can reuse the socket... + try: os.unlink(self.bind_addr) + except: pass + + # So everyone can access the socket... + try: os.chmod(self.bind_addr, 511) # 0777 + except: pass + + info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + if ':' in self.bind_addr[0]: + info = [(socket.AF_INET6, socket.SOCK_STREAM, + 0, "", self.bind_addr + (0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, + 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.error_log("Error in HTTPServer.tick", level=logging.ERROR, + traceback=True) + + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def error_log(self, msg="", level=20, traceback=False): + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See http://www.cherrypy.org/ticket/871. + if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 + and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')): + try: + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + makefile = CP_fileobject + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except NoSSLError: + msg = ("The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + buf = ["%s 400 Bad Request\r\n" % self.protocol, + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n\r\n", + msg] + + wfile = makefile(s, "wb", DEFAULT_BUFFER_SIZE) + try: + wfile.sendall("".join(buf)) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return + if not s: + return + makefile = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, makefile) + + if not isinstance(self.bind_addr, basestring): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error: + x = sys.exc_info()[1] + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if x.args[0] in socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See http://www.cherrypy.org/ticket/707. + return + if x.args[0] in socket_errors_nonblocking: + # Just try again. See http://www.cherrypy.org/ticket/479. + return + if x.args[0] in socket_errors_to_ignore: + # Our socket was closed. + # See http://www.cherrypy.org/ticket/686. + return + raise + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, basestring): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + # Changed to use error code and not message + # See http://www.cherrypy.org/ticket/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway(object): + """A base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplemented + + +# These may either be wsgiserver.SSLAdapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter', + } + +def get_ssl_adapter_class(name='pyopenssl'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, basestring): + last_dot = adapter.rfind(".") + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter + +# -------------------------------- WSGI Stuff -------------------------------- # + + +class CherryPyWSGIServer(HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): + self.requests = ThreadPool(self, min=numthreads or 1, max=max) + self.wsgi_app = wsgi_app + self.gateway = wsgi_gateways[self.wsgi_version] + + self.bind_addr = bind_addr + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.clear_stats() + + def _get_numthreads(self): + return self.requests.min + def _set_numthreads(self, value): + self.requests.min = value + numthreads = property(_get_numthreads, _set_numthreads) + + +class WSGIGateway(Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + self.req = req + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + raise NotImplemented + + def respond(self): + """Process the current request.""" + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + if isinstance(chunk, unicodestr): + chunk = chunk.encode('ISO-8859-1') + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None + + self.req.status = status + for k, v in headers: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not of type str." % k) + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not of type str." % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + self.req.outheaders.extend(headers) + + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response("500 Internal Server Error", + "The requested resource returned more bytes than the " + "declared Content-Length.") + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + if not self.req.sent_headers: + self.req.sent_headers = True + self.req.send_headers() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + "Response body exceeds the declared Content-Length.") + + +class WSGIGateway_10(WSGIGateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': req.path, + 'QUERY_STRING': req.qs, + 'REMOTE_ADDR': req.conn.remote_addr or '', + 'REMOTE_PORT': str(req.conn.remote_port or ''), + 'REQUEST_METHOD': req.method, + 'REQUEST_URI': req.uri, + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': req.request_protocol, + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': req.scheme, + 'wsgi.version': (1, 0), + } + + if isinstance(req.server.bind_addr, basestring): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env["SERVER_PORT"] = "" + else: + env["SERVER_PORT"] = str(req.server.bind_addr[1]) + + # Request headers + for k, v in req.inheaders.iteritems(): + env["HTTP_" + k.upper().replace("-", "_")] = v + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop("HTTP_CONTENT_TYPE", None) + if ct is not None: + env["CONTENT_TYPE"] = ct + cl = env.pop("HTTP_CONTENT_LENGTH", None) + if cl is not None: + env["CONTENT_LENGTH"] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class WSGIGateway_u0(WSGIGateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys and values + in both Python 2 and Python 3. + """ + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env_10 = WSGIGateway_10.get_environ(self) + env = dict([(k.decode('ISO-8859-1'), v) for k, v in env_10.iteritems()]) + env[u'wsgi.version'] = ('u', 0) + + # Request-URI + env.setdefault(u'wsgi.url_encoding', u'utf-8') + try: + for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]: + env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding']) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env[u'wsgi.url_encoding'] = u'ISO-8859-1' + for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]: + env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding']) + + for k, v in sorted(env.items()): + if isinstance(v, str) and k not in ('REQUEST_URI', 'wsgi.input'): + env[k] = v.decode('ISO-8859-1') + + return env + +wsgi_gateways = { + (1, 0): WSGIGateway_10, + ('u', 0): WSGIGateway_u0, +} + +class WSGIPathInfoDispatcher(object): + """A WSGI dispatcher for dispatch based on the PATH_INFO. + + apps: a dict or list of (path_prefix, app) pairs. + """ + + def __init__(self, apps): + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0]))) + apps.reverse() + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip("/"), a) for p, a in apps] + + def __call__(self, environ, start_response): + path = environ["PATH_INFO"] or "/" + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + "/") or path == p: + environ = environ.copy() + environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p + environ["PATH_INFO"] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + diff --git a/cherrypy/wsgiserver/wsgiserver2.pyc b/cherrypy/wsgiserver/wsgiserver2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c50fc1ec1a69724af5344e1ccaa4121369c79370 GIT binary patch literal 65433 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 while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return +""" + +__all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'CP_makefile', + 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', + 'WorkerThread', 'ThreadPool', 'SSLAdapter', + 'CherryPyWSGIServer', + 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', + 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] + +import os +try: + import queue +except: + import Queue as queue +import re +import email.utils +import socket +import sys +if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 +if sys.version_info < (3,1): + import io +else: + import _pyio as io +DEFAULT_BUFFER_SIZE = io.DEFAULT_BUFFER_SIZE + +import threading +import time +from traceback import format_exc +from urllib.parse import unquote +from urllib.parse import urlparse +from urllib.parse import scheme_chars +import warnings + +if sys.version_info >= (3, 0): + bytestr = bytes + unicodestr = str + basestring = (bytes, str) + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 3, the native string type is unicode + return n.encode(encoding) +else: + bytestr = str + unicodestr = unicode + basestring = basestring + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + +LF = ntob('\n') +CRLF = ntob('\r\n') +TAB = ntob('\t') +SPACE = ntob(' ') +COLON = ntob(':') +SEMICOLON = ntob(';') +EMPTY = ntob('') +NUMBER_SIGN = ntob('#') +QUESTION_MARK = ntob('?') +ASTERISK = ntob('*') +FORWARD_SLASH = ntob('/') +quoted_slash = re.compile(ntob("(?i)%2F")) + +import errno + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + +socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") + +socket_errors_to_ignore = plat_specific_errors( + "EPIPE", + "EBADF", "WSAEBADF", + "ENOTSOCK", "WSAENOTSOCK", + "ETIMEDOUT", "WSAETIMEDOUT", + "ECONNREFUSED", "WSAECONNREFUSED", + "ECONNRESET", "WSAECONNRESET", + "ECONNABORTED", "WSAECONNABORTED", + "ENETRESET", "WSAENETRESET", + "EHOSTDOWN", "EHOSTUNREACH", + ) +socket_errors_to_ignore.append("timed out") +socket_errors_to_ignore.append("The read operation timed out") + +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +comma_separated_headers = [ntob(h) for h in + ['Accept', 'Accept-Charset', 'Accept-Encoding', + 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', + 'Connection', 'Content-Encoding', 'Content-Language', 'Expect', + 'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE', + 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', + 'WWW-Authenticate']] + + +import logging +if not hasattr(logging, 'statistics'): logging.statistics = {} + + +def read_headers(rfile, hdict=None): + """Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError("Illegal header line.") + # TODO: what about TE and WWW-Authenticate? + k = k.strip().title() + v = v.strip() + hname = k + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = b", ".join((existing, v)) + hdict[hname] = v + + return hdict + + +class MaxSizeExceeded(Exception): + pass + +class SizeCheckWrapper(object): + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded() + + def read(self, size=None): + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See http://www.cherrypy.org/ticket/421 + if len(data) < 256 or data[-1:] == "\n": + return EMPTY.join(res) + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + self._check_length() + return data + + +class KnownLengthRFile(object): + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.remaining -= len(data) + return data + + +class ChunkedRFile(object): + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded("Request Entity Too Large", self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError("Bad chunked transfer size: " + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +## if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError("Request Entity Too Large") + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + "got " + repr(crlf) + ")") + + def read(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + + def readline(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + if not self.closed: + raise ValueError( + "Cannot read trailers until the request body has been read.") + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError("Request Entity Too Large") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + yield line + + def close(self): + self.rfile.close() + + def __iter__(self): + # Shamelessly stolen from StringIO + total = 0 + line = self.readline(sizehint) + while line: + yield line + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + def __init__(self, server, conn): + self.server= server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = ntob("http") + if self.server.ssl_adapter is not None: + self.scheme = ntob("https") + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except MaxSizeExceeded: + self.simple_response("414 Request-URI Too Long", + "The Request-URI sent with the request exceeds the maximum " + "allowed bytes.") + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except MaxSizeExceeded: + self.simple_response("413 Request Entity Too Large", + "The headers sent with the request exceed the maximum " + "allowed bytes.") + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response("400 Bad Request", "HTTP requires CRLF terminators") + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + # The [x:y] slicing is necessary for byte strings to avoid getting ord's + rp = int(req_protocol[5:6]), int(req_protocol[7:8]) + except ValueError: + self.simple_response("400 Bad Request", "Malformed Request-Line") + return False + + self.uri = uri + self.method = method + + # uri may be an abs_path (including "http://host.domain.tld"); + scheme, authority, path = self.parse_request_uri(uri) + if NUMBER_SIGN in path: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return False + + if scheme: + self.scheme = scheme + + qs = EMPTY + if QUESTION_MARK in path: + path, qs = path.split(QUESTION_MARK, 1) + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path". + try: + atoms = [self.unquote_bytes(x) for x in quoted_slash.split(path)] + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + path = b"%2F".join(atoms) + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + # The [x:y] slicing is necessary for byte strings to avoid getting ord's + sp = int(self.server.protocol[5:6]), int(self.server.protocol[7:8]) + + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return False + + self.request_protocol = req_protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + + # then all the http headers + try: + read_headers(self.rfile, self.inheaders) + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + if mrbs and int(self.inheaders.get(b"Content-Length", 0)) > mrbs: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return False + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + # Both server and client are HTTP/1.1 + if self.inheaders.get(b"Connection", b"") == b"close": + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get(b"Connection", b"") != b"Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = self.inheaders.get(b"Transfer-Encoding") + if te: + te = [x.strip().lower() for x in te.split(b",") if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == b"chunked": + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get(b"Expect", b"") == b"100-continue": + # Don't use simple_response here, because it emits headers + # we don't want. See http://www.cherrypy.org/ticket/951 + msg = self.server.protocol.encode('ascii') + b" 100 Continue\r\n\r\n" + try: + self.conn.wfile.write(msg) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return True + + def parse_request_uri(self, uri): + """Parse a Request-URI into (scheme, authority, path). + + Note that Request-URI's must be one of:: + + Request-URI = "*" | absoluteURI | abs_path | authority + + Therefore, a Request-URI which starts with a double forward-slash + cannot be a "net_path":: + + net_path = "//" authority [ abs_path ] + + Instead, it must be interpreted as an "abs_path" with an empty first + path segment:: + + abs_path = "/" path_segments + path_segments = segment *( "/" segment ) + segment = *pchar *( ";" param ) + param = *pchar + """ + if uri == ASTERISK: + return None, None, uri + + scheme, sep, remainder = uri.partition(b'://') + if sep and QUESTION_MARK not in scheme: + # An absoluteURI. + # If there's a scheme (and it must be http or https), then: + # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] + authority, path_a, path_b = remainder.partition(FORWARD_SLASH) + return scheme.lower(), authority, path_a+path_b + + if uri.startswith(FORWARD_SLASH): + # An abs_path. + return None, None, uri + else: + # An authority. + return None, uri, None + + def unquote_bytes(self, path): + """takes quoted string and unquotes % encoded values""" + res = path.split(b'%') + + for i in range(1, len(res)): + item = res[i] + try: + res[i] = bytes([int(item[:2], 16)]) + item[2:] + except ValueError: + raise + return b''.join(res) + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get(b"Content-Length", 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + + if (self.ready and not self.sent_headers): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.conn.wfile.write(b"0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = [bytes(self.server.protocol, "ascii") + SPACE + + bytes(status, "ISO-8859-1") + CRLF, + bytes("Content-Length: %s\r\n" % len(msg), "ISO-8859-1"), + b"Content-Type: text/plain\r\n"] + + if status[:3] in ("413", "414"): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append(b"Connection: close\r\n") + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = "400 Bad Request" + + buf.append(CRLF) + if msg: + if isinstance(msg, unicodestr): + msg = msg.encode("ISO-8859-1") + buf.append(msg) + + try: + self.conn.wfile.write(b"".join(buf)) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + buf = [bytes(hex(len(chunk)), 'ASCII')[2:], CRLF, chunk, CRLF] + self.conn.wfile.write(EMPTY.join(buf)) + else: + self.conn.wfile.write(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif b"content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if (self.response_protocol == 'HTTP/1.1' + and self.method != b'HEAD'): + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append((b"Transfer-Encoding", b"chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if b"connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append((b"Connection", b"close")) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append((b"Connection", b"Keep-Alive")) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if b"date" not in hkeys: + self.outheaders.append( + (b"Date", email.utils.formatdate(usegmt=True).encode('ISO-8859-1'))) + + if b"server" not in hkeys: + self.outheaders.append( + (b"Server", self.server.server_name.encode('ISO-8859-1'))) + + buf = [self.server.protocol.encode('ascii') + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.write(EMPTY.join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + pass + + +class CP_BufferedWriter(io.BufferedWriter): + """Faux file object attached to a socket object.""" + + def write(self, b): + self._checkClosed() + if isinstance(b, str): + raise TypeError("can't write str to binary stream") + + with self._write_lock: + self._write_buf.extend(b) + self._flush_unlocked() + return len(b) + + def _flush_unlocked(self): + self._checkClosed("flush of closed file") + while self._write_buf: + try: + # ssl sockets only except 'bytes', not bytearrays + # so perhaps we should conditionally wrap this for perf? + n = self.raw.write(bytes(self._write_buf)) + except io.BlockingIOError as e: + n = e.characters_written + del self._write_buf[:n] + + +def CP_makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + if 'r' in mode: + return io.BufferedReader(socket.SocketIO(sock, mode), bufsize) + else: + return CP_BufferedWriter(socket.SocketIO(sock, mode), bufsize) + +class HTTPConnection(object): + """An HTTP connection (active socket). + + server: the Server object which received this connection. + socket: the raw socket object (usually TCP) for this connection. + makefile: a fileobject class for reading from the socket. + """ + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = DEFAULT_BUFFER_SIZE + wbufsize = DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + + def __init__(self, server, sock, makefile=CP_makefile): + self.server = server + self.socket = sock + self.rfile = makefile(sock, "rb", self.rbufsize) + self.wfile = makefile(sock, "wb", self.wbufsize) + self.requests_seen = 0 + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error: + e = sys.exc_info()[1] + errnum = e.args[0] + # sadly SSL sockets return a different (longer) time out string + if errnum == 'timed out' or errnum == 'The read operation timed out': + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See http://www.cherrypy.org/ticket/853 + if (not request_seen) or (req and req.started_request): + # Don't bother writing the 408 if the response + # has already started being written. + if req and not req.sent_headers: + try: + req.simple_response("408 Request Timeout") + except FatalSSLAlert: + # Close the connection. + return + elif errnum not in socket_errors_to_ignore: + self.server.error_log("socket.error %s" % repr(errnum), + level=logging.WARNING, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + return + except (KeyboardInterrupt, SystemExit): + raise + except FatalSSLAlert: + # Close the connection. + return + except NoSSLError: + if req and not req.sent_headers: + # Unwrap our wfile + self.wfile = CP_makefile(self.socket._sock, "wb", self.wbufsize) + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + self.linger = True + except Exception: + e = sys.exc_info()[1] + self.server.error_log(repr(e), level=logging.ERROR, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + + linger = False + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + # Python's socket module does NOT call close on the kernel socket + # when you call socket.close(). We do so manually here because we + # want this server to send a FIN TCP segment immediately. Note this + # must be called *before* calling socket.close(), because the latter + # drops its reference to the kernel socket. + # Python 3 *probably* fixed this with socket._real_close; hard to tell. +## self.socket._sock.close() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + +class TrueyZero(object): + """An object which equals and does math like the integer '0' but evals True.""" + def __add__(self, other): + return other + def __radd__(self, other): + return other +trueyzero = TrueyZero() + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + + def __init__(self, server): + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and trueyzero or self.conn.requests_seen), + 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and trueyzero or self.conn.rfile.bytes_read), + 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and trueyzero or self.conn.wfile.bytes_written), + 'Work Time': lambda s: self.work_time + ((self.start_time is None) and trueyzero or time.time() - self.start_time), + 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6), + 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6), + } + threading.Thread.__init__(self) + + def run(self): + self.server.stats['Worker Threads'][self.getName()] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + if self.server.stats['Enabled']: + self.start_time = time.time() + try: + conn.communicate() + finally: + conn.close() + if self.server.stats['Enabled']: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit): + exc = sys.exc_info()[1] + self.server.interrupt = exc + + +class ThreadPool(object): + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__(self, server, min=10, max=-1): + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue() + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName("CP Server " + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + def _get_idle(self): + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + idle = property(_get_idle, doc=_get_idle.__doc__) + + def put(self, obj): + self._queue.put(obj) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + for i in range(amount): + if self.max > 0 and len(self._threads) >= self.max: + break + worker = WorkerThread(self.server) + worker.setName("CP Server " + worker.getName()) + self._threads.append(worker) + worker.start() + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + if amount > 0: + for i in range(min(amount, len(self._threads) - self.min)): + # Put a number of shutdown requests on the queue equal + # to 'amount'. Once each of those is processed by a worker, + # that worker will terminate and be culled from our list + # in self.put. + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See http://www.cherrypy.org/ticket/691. + KeyboardInterrupt): + pass + + def _get_qsize(self): + return self._queue.qsize() + qsize = property(_get_qsize) + + + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + except ImportError: + def prevent_socket_inheritance(sock): + """Dummy function, since neither fcntl nor ctypes are available.""" + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class SSLAdapter(object): + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object`` + """ + + def __init__(self, certificate, private_key, certificate_chain=None): + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def wrap(self, sock): + raise NotImplemented + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + raise NotImplemented + + +class HTTPServer(object): + """An HTTP server.""" + + _bind_addr = "127.0.0.1" + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create (default -1 = no limit).""" + + server_name = None + """The name of the server; defaults to socket.gethostname().""" + + protocol = "HTTP/1.1" + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections (default 5).""" + + shutdown_timeout = 5 + """The total time, in seconds, to wait for worker threads to cleanly exit.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = "CherryPy/3.2.2" + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``.""" + + ready = False + """An internal flag which marks whether the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of SSLAdapter (or a subclass). + + You must have the corresponding SSL driver library installed.""" + + def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1, + server_name=None): + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.clear_stats() + + def clear_stats(self): + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, "qsize", None), + 'Threads': lambda s: len(getattr(self.requests, "_threads", [])), + 'Threads Idle': lambda s: getattr(self.requests, "idle", None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum([w['Requests'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Read'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Written'](w) for w + in s['Worker Threads'].values()], 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum([w['Work Time'](w) for w + in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats + + def runtime(self): + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, + self.bind_addr) + + def _get_bind_addr(self): + return self._bind_addr + def _set_bind_addr(self, value): + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + "to listen on all active interfaces.") + self._bind_addr = value + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc="""The interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string.""") + + def start(self): + """Run the server forever.""" + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrpy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self._interrupt = None + + if self.software is None: + self.software = "%s Server" % self.version + + # Select the appropriate socket + if isinstance(self.bind_addr, basestring): + # AF_UNIX socket + + # So we can reuse the socket... + try: os.unlink(self.bind_addr) + except: pass + + # So everyone can access the socket... + try: os.chmod(self.bind_addr, 511) # 0777 + except: pass + + info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + if ':' in self.bind_addr[0]: + info = [(socket.AF_INET6, socket.SOCK_STREAM, + 0, "", self.bind_addr + (0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, + 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.error_log("Error in HTTPServer.tick", level=logging.ERROR, + traceback=True) + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def error_log(self, msg="", level=20, traceback=False): + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See http://www.cherrypy.org/ticket/871. + if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 + and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')): + try: + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + makefile = CP_makefile + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except NoSSLError: + msg = ("The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + buf = ["%s 400 Bad Request\r\n" % self.protocol, + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n\r\n", + msg] + + wfile = makefile(s, "wb", DEFAULT_BUFFER_SIZE) + try: + wfile.write("".join(buf).encode('ISO-8859-1')) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return + if not s: + return + makefile = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, makefile) + + if not isinstance(self.bind_addr, basestring): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error: + x = sys.exc_info()[1] + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if x.args[0] in socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See http://www.cherrypy.org/ticket/707. + return + if x.args[0] in socket_errors_nonblocking: + # Just try again. See http://www.cherrypy.org/ticket/479. + return + if x.args[0] in socket_errors_to_ignore: + # Our socket was closed. + # See http://www.cherrypy.org/ticket/686. + return + raise + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, basestring): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + # Changed to use error code and not message + # See http://www.cherrypy.org/ticket/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway(object): + """A base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplemented + + +# These may either be wsgiserver.SSLAdapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter', + } + +def get_ssl_adapter_class(name='builtin'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, basestring): + last_dot = adapter.rfind(".") + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter + +# -------------------------------- WSGI Stuff -------------------------------- # + + +class CherryPyWSGIServer(HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): + self.requests = ThreadPool(self, min=numthreads or 1, max=max) + self.wsgi_app = wsgi_app + self.gateway = wsgi_gateways[self.wsgi_version] + + self.bind_addr = bind_addr + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.clear_stats() + + def _get_numthreads(self): + return self.requests.min + def _set_numthreads(self, value): + self.requests.min = value + numthreads = property(_get_numthreads, _set_numthreads) + + +class WSGIGateway(Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + self.req = req + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + raise NotImplemented + + def respond(self): + """Process the current request.""" + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + if isinstance(chunk, unicodestr): + chunk = chunk.encode('ISO-8859-1') + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) + finally: + exc_info = None + + # According to PEP 3333, when using Python 3, the response status + # and headers must be bytes masquerading as unicode; that is, they + # must be of type "str" but are restricted to code points in the + # "latin-1" set. + if not isinstance(status, str): + raise TypeError("WSGI response status is not of type str.") + self.req.status = status.encode('ISO-8859-1') + + for k, v in headers: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not of type str." % k) + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not of type str." % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + self.req.outheaders.append((k.encode('ISO-8859-1'), v.encode('ISO-8859-1'))) + + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response("500 Internal Server Error", + "The requested resource returned more bytes than the " + "declared Content-Length.") + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + if not self.req.sent_headers: + self.req.sent_headers = True + self.req.send_headers() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + "Response body exceeds the declared Content-Length.") + + +class WSGIGateway_10(WSGIGateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': req.path.decode('ISO-8859-1'), + 'QUERY_STRING': req.qs.decode('ISO-8859-1'), + 'REMOTE_ADDR': req.conn.remote_addr or '', + 'REMOTE_PORT': str(req.conn.remote_port or ''), + 'REQUEST_METHOD': req.method.decode('ISO-8859-1'), + 'REQUEST_URI': req.uri, + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': req.request_protocol.decode('ISO-8859-1'), + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': req.scheme.decode('ISO-8859-1'), + 'wsgi.version': (1, 0), + } + + if isinstance(req.server.bind_addr, basestring): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env["SERVER_PORT"] = "" + else: + env["SERVER_PORT"] = str(req.server.bind_addr[1]) + + # Request headers + for k, v in req.inheaders.items(): + k = k.decode('ISO-8859-1').upper().replace("-", "_") + env["HTTP_" + k] = v.decode('ISO-8859-1') + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop("HTTP_CONTENT_TYPE", None) + if ct is not None: + env["CONTENT_TYPE"] = ct + cl = env.pop("HTTP_CONTENT_LENGTH", None) + if cl is not None: + env["CONTENT_LENGTH"] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class WSGIGateway_u0(WSGIGateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys and values + in both Python 2 and Python 3. + """ + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env_10 = WSGIGateway_10.get_environ(self) + env = env_10.copy() + env['wsgi.version'] = ('u', 0) + + # Request-URI + env.setdefault('wsgi.url_encoding', 'utf-8') + try: + # SCRIPT_NAME is the empty string, who cares what encoding it is? + env["PATH_INFO"] = req.path.decode(env['wsgi.url_encoding']) + env["QUERY_STRING"] = req.qs.decode(env['wsgi.url_encoding']) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env['wsgi.url_encoding'] = 'ISO-8859-1' + env["PATH_INFO"] = env_10["PATH_INFO"] + env["QUERY_STRING"] = env_10["QUERY_STRING"] + + return env + +wsgi_gateways = { + (1, 0): WSGIGateway_10, + ('u', 0): WSGIGateway_u0, +} + +class WSGIPathInfoDispatcher(object): + """A WSGI dispatcher for dispatch based on the PATH_INFO. + + apps: a dict or list of (path_prefix, app) pairs. + """ + + def __init__(self, apps): + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + apps.sort() + apps.reverse() + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip("/"), a) for p, a in apps] + + def __call__(self, environ, start_response): + path = environ["PATH_INFO"] or "/" + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + "/") or path == p: + environ = environ.copy() + environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p + environ["PATH_INFO"] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + diff --git a/lib/ConfigFunctions.pyc b/lib/ConfigFunctions.pyc index eaa2dcee72ff2346ffe4e2cecc7f85a594421e1e..58912588fe3c5f342cdb1d3b3037753e54ffbfdd 100644 GIT binary patch delta 16 YcmX>id_id_t05{VH=>Px# diff --git a/lib/Constants.pyc b/lib/Constants.pyc index 7e25ee39521eeaaa3e9e5add98c36e53262d53bb..547f007d61a40056469c847b645b14d052bb863a 100644 GIT binary patch delta 20 ccmey#_>+;H`7+;H`7@@d06QfI1ONa4 delta 16 YcmX@$dcc*P`7%#K8ptP delta 16 XcmeyX@mGVL`7>}4VVIxYrR diff --git a/lib/Logger.pyc b/lib/Logger.pyc index ebec5f7a553b8cc5e1e8b4b83afa407e0b3f46dd..85940ae7e454b1f3cc3fed33762352b848604ab6 100644 GIT binary patch delta 16 YcmX@ge3Y4;`7pF delta 16 YcmeyT_)n3Y`7@~ diff --git a/lib/WebRoot.pyc b/lib/WebRoot.pyc index f4f74ad62dc465f28ae916b2a77cd2a5950f77b4..1a3a68a1779dcca1ed72d1358972d3b9a1241ce5 100644 GIT binary patch delta 18 ZcmeC&$=JD*k)8Q7FW0xYe;e7GVgN-H2ZI0r delta 18 ZcmeC&$=JD*k)8Q7FIV#QKO5PbVgN(i2WJ2P diff --git a/lib/__init__.pyc b/lib/__init__.pyc index 5cee9bfc11f7340113adaa1d8155010bb9cc14cd..d58598f5b8974da21de3cef68347e9fd8a618694 100644 GIT binary patch delta 13 Ucmd1LXJ`J*%k?ep-$eEd03t&LMF0Q* delta 13 Ucmd1LXJ`J*%f-0x+eG#Z03EahivR!s