From bf0add09f6f05fa5b6b92ecd21594506ee79dffc Mon Sep 17 00:00:00 2001 From: Cameron Currie Date: Thu, 10 May 2012 12:31:05 -0500 Subject: [PATCH 01/19] Add __init__.py so Printrun can be used as package --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 From 16cf3d764fc795ad82215a472566005eb7c35794 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 30 May 2012 16:13:49 -0500 Subject: [PATCH 02/19] Added Initial Web Framework for Pronterface, right now, read only global settings. --- http.config | 3 + .../CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO | 26 + .../CherryPy.egg-info/SOURCES.txt | 113 + .../CherryPy.egg-info/dependency_links.txt | 1 + .../CherryPy.egg-info/top_level.txt | 1 + libs/CherryPy-3.2.2/PKG-INFO | 26 + libs/CherryPy-3.2.2/README.txt | 13 + .../build/lib/cherrypy/__init__.py | 624 +++++ .../build/lib/cherrypy/_cpchecker.py | 327 +++ .../build/lib/cherrypy/_cpcompat.py | 318 +++ .../build/lib/cherrypy/_cpconfig.py | 295 +++ .../build/lib/cherrypy/_cpdispatch.py | 636 +++++ .../build/lib/cherrypy/_cperror.py | 556 ++++ .../build/lib/cherrypy/_cplogging.py | 440 ++++ .../build/lib/cherrypy/_cpmodpy.py | 344 +++ .../build/lib/cherrypy/_cpnative_server.py | 149 ++ .../build/lib/cherrypy/_cpreqbody.py | 965 +++++++ .../build/lib/cherrypy/_cprequest.py | 956 +++++++ .../build/lib/cherrypy/_cpserver.py | 205 ++ .../build/lib/cherrypy/_cpthreadinglocal.py | 239 ++ .../build/lib/cherrypy/_cptools.py | 510 ++++ .../build/lib/cherrypy/_cptree.py | 290 ++ .../build/lib/cherrypy/_cpwsgi.py | 408 +++ .../build/lib/cherrypy/_cpwsgi_server.py | 63 + .../build/lib/cherrypy/lib/__init__.py | 45 + .../build/lib/cherrypy/lib/auth.py | 87 + .../build/lib/cherrypy/lib/auth_basic.py | 87 + .../build/lib/cherrypy/lib/auth_digest.py | 365 +++ .../build/lib/cherrypy/lib/caching.py | 465 ++++ .../build/lib/cherrypy/lib/covercp.py | 365 +++ .../build/lib/cherrypy/lib/cpstats.py | 662 +++++ .../build/lib/cherrypy/lib/cptools.py | 617 +++++ .../build/lib/cherrypy/lib/encoding.py | 388 +++ .../build/lib/cherrypy/lib/gctools.py | 214 ++ .../build/lib/cherrypy/lib/http.py | 7 + .../build/lib/cherrypy/lib/httpauth.py | 354 +++ .../build/lib/cherrypy/lib/httputil.py | 506 ++++ .../build/lib/cherrypy/lib/jsontools.py | 87 + .../build/lib/cherrypy/lib/profiler.py | 208 ++ .../build/lib/cherrypy/lib/reprconf.py | 485 ++++ .../build/lib/cherrypy/lib/sessions.py | 871 +++++++ .../build/lib/cherrypy/lib/static.py | 363 +++ .../build/lib/cherrypy/lib/xmlrpcutil.py | 55 + .../build/lib/cherrypy/process/__init__.py | 14 + .../build/lib/cherrypy/process/plugins.py | 683 +++++ .../build/lib/cherrypy/process/servers.py | 427 +++ .../build/lib/cherrypy/process/win32.py | 174 ++ .../build/lib/cherrypy/process/wspbus.py | 432 +++ .../build/lib/cherrypy/scaffold/__init__.py | 61 + .../build/lib/cherrypy/test/__init__.py | 27 + .../lib/cherrypy/test/_test_decorators.py | 41 + .../lib/cherrypy/test/_test_states_demo.py | 66 + .../build/lib/cherrypy/test/benchmark.py | 409 +++ .../build/lib/cherrypy/test/checkerdemo.py | 47 + .../build/lib/cherrypy/test/helper.py | 493 ++++ .../build/lib/cherrypy/test/logtest.py | 188 ++ .../build/lib/cherrypy/test/modfastcgi.py | 135 + .../build/lib/cherrypy/test/modfcgid.py | 125 + .../build/lib/cherrypy/test/modpy.py | 163 ++ .../build/lib/cherrypy/test/modwsgi.py | 148 ++ .../build/lib/cherrypy/test/sessiondemo.py | 153 ++ .../lib/cherrypy/test/test_auth_basic.py | 79 + .../lib/cherrypy/test/test_auth_digest.py | 115 + .../build/lib/cherrypy/test/test_bus.py | 263 ++ .../build/lib/cherrypy/test/test_caching.py | 328 +++ .../build/lib/cherrypy/test/test_config.py | 256 ++ .../lib/cherrypy/test/test_config_server.py | 121 + .../build/lib/cherrypy/test/test_conn.py | 734 ++++++ .../build/lib/cherrypy/test/test_core.py | 688 +++++ .../test/test_dynamicobjectmapping.py | 404 +++ .../build/lib/cherrypy/test/test_encoding.py | 363 +++ .../build/lib/cherrypy/test/test_etags.py | 83 + .../build/lib/cherrypy/test/test_http.py | 212 ++ .../build/lib/cherrypy/test/test_httpauth.py | 151 ++ .../build/lib/cherrypy/test/test_httplib.py | 29 + .../build/lib/cherrypy/test/test_json.py | 79 + .../build/lib/cherrypy/test/test_logging.py | 157 ++ .../build/lib/cherrypy/test/test_mime.py | 128 + .../lib/cherrypy/test/test_misc_tools.py | 207 ++ .../lib/cherrypy/test/test_objectmapping.py | 404 +++ .../build/lib/cherrypy/test/test_proxy.py | 129 + .../build/lib/cherrypy/test/test_refleaks.py | 59 + .../lib/cherrypy/test/test_request_obj.py | 737 ++++++ .../build/lib/cherrypy/test/test_routes.py | 69 + .../build/lib/cherrypy/test/test_session.py | 464 ++++ .../cherrypy/test/test_sessionauthenticate.py | 62 + .../build/lib/cherrypy/test/test_states.py | 439 ++++ .../build/lib/cherrypy/test/test_static.py | 300 +++ .../build/lib/cherrypy/test/test_tools.py | 399 +++ .../build/lib/cherrypy/test/test_tutorials.py | 201 ++ .../lib/cherrypy/test/test_virtualhost.py | 107 + .../build/lib/cherrypy/test/test_wsgi_ns.py | 91 + .../lib/cherrypy/test/test_wsgi_vhost.py | 36 + .../build/lib/cherrypy/test/test_wsgiapps.py | 118 + .../build/lib/cherrypy/test/test_xmlrpc.py | 179 ++ .../build/lib/cherrypy/test/webtest.py | 575 ++++ .../build/lib/cherrypy/tutorial/__init__.py | 3 + .../lib/cherrypy/tutorial/bonus-sqlobject.py | 168 ++ .../lib/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 + .../tutorial/tut05_derived_objects.py | 83 + .../cherrypy/tutorial/tut06_default_method.py | 64 + .../lib/cherrypy/tutorial/tut07_sessions.py | 44 + .../tutorial/tut08_generators_and_yield.py | 47 + .../lib/cherrypy/tutorial/tut09_files.py | 107 + .../cherrypy/tutorial/tut10_http_errors.py | 81 + .../build/lib/cherrypy/wsgiserver/__init__.py | 14 + .../lib/cherrypy/wsgiserver/ssl_builtin.py | 91 + .../lib/cherrypy/wsgiserver/ssl_pyopenssl.py | 256 ++ .../lib/cherrypy/wsgiserver/wsgiserver2.py | 2322 +++++++++++++++++ libs/CherryPy-3.2.2/build/scripts-2.7/cherryd | 109 + libs/CherryPy-3.2.2/cherrypy/LICENSE.txt | 25 + libs/CherryPy-3.2.2/cherrypy/__init__.py | 624 +++++ libs/CherryPy-3.2.2/cherrypy/_cpchecker.py | 327 +++ libs/CherryPy-3.2.2/cherrypy/_cpcompat.py | 318 +++ libs/CherryPy-3.2.2/cherrypy/_cpconfig.py | 295 +++ libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py | 636 +++++ libs/CherryPy-3.2.2/cherrypy/_cperror.py | 556 ++++ libs/CherryPy-3.2.2/cherrypy/_cplogging.py | 440 ++++ libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py | 344 +++ .../cherrypy/_cpnative_server.py | 149 ++ libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py | 965 +++++++ libs/CherryPy-3.2.2/cherrypy/_cprequest.py | 956 +++++++ libs/CherryPy-3.2.2/cherrypy/_cpserver.py | 205 ++ .../cherrypy/_cpthreadinglocal.py | 239 ++ libs/CherryPy-3.2.2/cherrypy/_cptools.py | 510 ++++ libs/CherryPy-3.2.2/cherrypy/_cptree.py | 290 ++ libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py | 408 +++ .../CherryPy-3.2.2/cherrypy/_cpwsgi_server.py | 63 + libs/CherryPy-3.2.2/cherrypy/cherryd | 109 + libs/CherryPy-3.2.2/cherrypy/favicon.ico | Bin 0 -> 1406 bytes libs/CherryPy-3.2.2/cherrypy/lib/__init__.py | 45 + libs/CherryPy-3.2.2/cherrypy/lib/auth.py | 87 + .../CherryPy-3.2.2/cherrypy/lib/auth_basic.py | 87 + .../cherrypy/lib/auth_digest.py | 365 +++ libs/CherryPy-3.2.2/cherrypy/lib/caching.py | 465 ++++ libs/CherryPy-3.2.2/cherrypy/lib/covercp.py | 365 +++ libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py | 662 +++++ libs/CherryPy-3.2.2/cherrypy/lib/cptools.py | 617 +++++ libs/CherryPy-3.2.2/cherrypy/lib/encoding.py | 388 +++ libs/CherryPy-3.2.2/cherrypy/lib/gctools.py | 214 ++ libs/CherryPy-3.2.2/cherrypy/lib/http.py | 7 + libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py | 354 +++ libs/CherryPy-3.2.2/cherrypy/lib/httputil.py | 506 ++++ libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py | 87 + libs/CherryPy-3.2.2/cherrypy/lib/profiler.py | 208 ++ libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py | 485 ++++ libs/CherryPy-3.2.2/cherrypy/lib/sessions.py | 871 +++++++ libs/CherryPy-3.2.2/cherrypy/lib/static.py | 363 +++ .../CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py | 55 + .../cherrypy/process/__init__.py | 14 + .../cherrypy/process/plugins.py | 683 +++++ .../cherrypy/process/servers.py | 427 +++ libs/CherryPy-3.2.2/cherrypy/process/win32.py | 174 ++ .../CherryPy-3.2.2/cherrypy/process/wspbus.py | 432 +++ .../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 libs/CherryPy-3.2.2/cherrypy/test/__init__.py | 27 + .../cherrypy/test/_test_decorators.py | 41 + .../cherrypy/test/_test_states_demo.py | 66 + .../CherryPy-3.2.2/cherrypy/test/benchmark.py | 409 +++ .../cherrypy/test/checkerdemo.py | 47 + libs/CherryPy-3.2.2/cherrypy/test/helper.py | 493 ++++ libs/CherryPy-3.2.2/cherrypy/test/logtest.py | 188 ++ .../cherrypy/test/modfastcgi.py | 135 + libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py | 125 + libs/CherryPy-3.2.2/cherrypy/test/modpy.py | 163 ++ libs/CherryPy-3.2.2/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 + libs/CherryPy-3.2.2/cherrypy/test/style.css | 1 + libs/CherryPy-3.2.2/cherrypy/test/test.pem | 38 + .../cherrypy/test/test_auth_basic.py | 79 + .../cherrypy/test/test_auth_digest.py | 115 + libs/CherryPy-3.2.2/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-3.2.2/cherrypy/test/test_conn.py | 734 ++++++ .../CherryPy-3.2.2/cherrypy/test/test_core.py | 688 +++++ .../test/test_dynamicobjectmapping.py | 404 +++ .../cherrypy/test/test_encoding.py | 363 +++ .../cherrypy/test/test_etags.py | 83 + .../CherryPy-3.2.2/cherrypy/test/test_http.py | 212 ++ .../cherrypy/test/test_httpauth.py | 151 ++ .../cherrypy/test/test_httplib.py | 29 + .../CherryPy-3.2.2/cherrypy/test/test_json.py | 79 + .../cherrypy/test/test_logging.py | 157 ++ .../CherryPy-3.2.2/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 ++ libs/CherryPy-3.2.2/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 + .../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/ssl_builtin.py | 91 + .../cherrypy/wsgiserver/ssl_pyopenssl.py | 256 ++ .../cherrypy/wsgiserver/wsgiserver2.py | 2322 +++++++++++++++++ .../cherrypy/wsgiserver/wsgiserver3.py | 2040 +++++++++++++++ .../dist/CherryPy-3.2.2-py2.7.egg | Bin 0 -> 851999 bytes libs/CherryPy-3.2.2/setup.py | 144 + pronterface.py | 5 + webinterface.py | 50 + 238 files changed, 61990 insertions(+) create mode 100644 http.config create mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO create mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt create mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt create mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt create mode 100644 libs/CherryPy-3.2.2/PKG-INFO create mode 100644 libs/CherryPy-3.2.2/README.txt create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py create mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py create mode 100644 libs/CherryPy-3.2.2/build/scripts-2.7/cherryd create mode 100644 libs/CherryPy-3.2.2/cherrypy/LICENSE.txt create mode 100644 libs/CherryPy-3.2.2/cherrypy/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpchecker.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpcompat.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpconfig.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cperror.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cplogging.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cprequest.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpserver.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cptools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cptree.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/cherryd create mode 100644 libs/CherryPy-3.2.2/cherrypy/favicon.ico create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/caching.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/covercp.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/cptools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/encoding.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/gctools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/http.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/httputil.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/profiler.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/sessions.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/static.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/process/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/process/plugins.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/process/servers.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/process/win32.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/process/wspbus.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf create mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf create mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf create mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/static/made_with_cherrypy_small.png create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/_test_decorators.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/_test_states_demo.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/benchmark.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/helper.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/logtest.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modpy.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/static/dirback.jpg create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/static/index.html create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/style.css create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test.pem create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_auth_basic.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_bus.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_caching.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_config.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_conn.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_core.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_etags.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_http.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_json.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_logging.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_mime.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_routes.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_session.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_states.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_static.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_tools.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/test/webtest.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/pdf_file.pdf create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut01_helloworld.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut02_expose_methods.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf create mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py create mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py create mode 100644 libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg create mode 100644 libs/CherryPy-3.2.2/setup.py create mode 100644 webinterface.py diff --git a/http.config b/http.config new file mode 100644 index 0000000..1dbdefc --- /dev/null +++ b/http.config @@ -0,0 +1,3 @@ +[global] +server.socket_host: "localhost" +server.socket_port: 8080 diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO b/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO new file mode 100644 index 0000000..34cae0e --- /dev/null +++ b/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO @@ -0,0 +1,26 @@ +Metadata-Version: 1.0 +Name: CherryPy +Version: 3.2.2 +Summary: Object-Oriented HTTP framework +Home-page: http://www.cherrypy.org +Author: CherryPy Team +Author-email: team@cherrypy.org +License: BSD +Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/ +Description: CherryPy is a pythonic, object-oriented HTTP framework +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: Freely Distributable +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt new file mode 100644 index 0000000..ec7f39c --- /dev/null +++ b/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt @@ -0,0 +1,113 @@ +README.txt +setup.py +CherryPy.egg-info/PKG-INFO +CherryPy.egg-info/SOURCES.txt +CherryPy.egg-info/dependency_links.txt +CherryPy.egg-info/top_level.txt +cherrypy/__init__.py +cherrypy/_cpchecker.py +cherrypy/_cpcompat.py +cherrypy/_cpconfig.py +cherrypy/_cpdispatch.py +cherrypy/_cperror.py +cherrypy/_cplogging.py +cherrypy/_cpmodpy.py +cherrypy/_cpnative_server.py +cherrypy/_cpreqbody.py +cherrypy/_cprequest.py +cherrypy/_cpserver.py +cherrypy/_cpthreadinglocal.py +cherrypy/_cptools.py +cherrypy/_cptree.py +cherrypy/_cpwsgi.py +cherrypy/_cpwsgi_server.py +cherrypy/cherryd +cherrypy/lib/__init__.py +cherrypy/lib/auth.py +cherrypy/lib/auth_basic.py +cherrypy/lib/auth_digest.py +cherrypy/lib/caching.py +cherrypy/lib/covercp.py +cherrypy/lib/cpstats.py +cherrypy/lib/cptools.py +cherrypy/lib/encoding.py +cherrypy/lib/gctools.py +cherrypy/lib/http.py +cherrypy/lib/httpauth.py +cherrypy/lib/httputil.py +cherrypy/lib/jsontools.py +cherrypy/lib/profiler.py +cherrypy/lib/reprconf.py +cherrypy/lib/sessions.py +cherrypy/lib/static.py +cherrypy/lib/xmlrpcutil.py +cherrypy/process/__init__.py +cherrypy/process/plugins.py +cherrypy/process/servers.py +cherrypy/process/win32.py +cherrypy/process/wspbus.py +cherrypy/scaffold/__init__.py +cherrypy/test/__init__.py +cherrypy/test/_test_decorators.py +cherrypy/test/_test_states_demo.py +cherrypy/test/benchmark.py +cherrypy/test/checkerdemo.py +cherrypy/test/helper.py +cherrypy/test/logtest.py +cherrypy/test/modfastcgi.py +cherrypy/test/modfcgid.py +cherrypy/test/modpy.py +cherrypy/test/modwsgi.py +cherrypy/test/sessiondemo.py +cherrypy/test/test_auth_basic.py +cherrypy/test/test_auth_digest.py +cherrypy/test/test_bus.py +cherrypy/test/test_caching.py +cherrypy/test/test_config.py +cherrypy/test/test_config_server.py +cherrypy/test/test_conn.py +cherrypy/test/test_core.py +cherrypy/test/test_dynamicobjectmapping.py +cherrypy/test/test_encoding.py +cherrypy/test/test_etags.py +cherrypy/test/test_http.py +cherrypy/test/test_httpauth.py +cherrypy/test/test_httplib.py +cherrypy/test/test_json.py +cherrypy/test/test_logging.py +cherrypy/test/test_mime.py +cherrypy/test/test_misc_tools.py +cherrypy/test/test_objectmapping.py +cherrypy/test/test_proxy.py +cherrypy/test/test_refleaks.py +cherrypy/test/test_request_obj.py +cherrypy/test/test_routes.py +cherrypy/test/test_session.py +cherrypy/test/test_sessionauthenticate.py +cherrypy/test/test_states.py +cherrypy/test/test_static.py +cherrypy/test/test_tools.py +cherrypy/test/test_tutorials.py +cherrypy/test/test_virtualhost.py +cherrypy/test/test_wsgi_ns.py +cherrypy/test/test_wsgi_vhost.py +cherrypy/test/test_wsgiapps.py +cherrypy/test/test_xmlrpc.py +cherrypy/test/webtest.py +cherrypy/tutorial/__init__.py +cherrypy/tutorial/bonus-sqlobject.py +cherrypy/tutorial/tut01_helloworld.py +cherrypy/tutorial/tut02_expose_methods.py +cherrypy/tutorial/tut03_get_and_post.py +cherrypy/tutorial/tut04_complex_site.py +cherrypy/tutorial/tut05_derived_objects.py +cherrypy/tutorial/tut06_default_method.py +cherrypy/tutorial/tut07_sessions.py +cherrypy/tutorial/tut08_generators_and_yield.py +cherrypy/tutorial/tut09_files.py +cherrypy/tutorial/tut10_http_errors.py +cherrypy/wsgiserver/__init__.py +cherrypy/wsgiserver/ssl_builtin.py +cherrypy/wsgiserver/ssl_pyopenssl.py +cherrypy/wsgiserver/wsgiserver2.py +cherrypy/wsgiserver/wsgiserver3.py \ No newline at end of file diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt new file mode 100644 index 0000000..d718706 --- /dev/null +++ b/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt @@ -0,0 +1 @@ +cherrypy diff --git a/libs/CherryPy-3.2.2/PKG-INFO b/libs/CherryPy-3.2.2/PKG-INFO new file mode 100644 index 0000000..34cae0e --- /dev/null +++ b/libs/CherryPy-3.2.2/PKG-INFO @@ -0,0 +1,26 @@ +Metadata-Version: 1.0 +Name: CherryPy +Version: 3.2.2 +Summary: Object-Oriented HTTP framework +Home-page: http://www.cherrypy.org +Author: CherryPy Team +Author-email: team@cherrypy.org +License: BSD +Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/ +Description: CherryPy is a pythonic, object-oriented HTTP framework +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: Freely Distributable +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks diff --git a/libs/CherryPy-3.2.2/README.txt b/libs/CherryPy-3.2.2/README.txt new file mode 100644 index 0000000..d852128 --- /dev/null +++ b/libs/CherryPy-3.2.2/README.txt @@ -0,0 +1,13 @@ +* To install, change to the directory where setup.py is located and type (python-2.3 or later needed): + + python setup.py install + +* To learn how to use it, look at the examples under cherrypy/tutorial/ or go to http://www.cherrypy.org for more info. + +* To run the regression tests, just go to the cherrypy/test/ directory and type: + + nosetests -s ./ + + Or to run individual tests type: + + nosetests -s test_foo.py diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py new file mode 100644 index 0000000..41e3898 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py new file mode 100644 index 0000000..7ccfd89 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py @@ -0,0 +1,327 @@ +import os +import warnings + +import cherrypy +from cherrypy._cpcompat import iteritems, copykeys, builtins + + +class Checker(object): + """A checker for CherryPy sites and their mounted applications. + + When this object is called at engine startup, it executes each + of its own methods whose names start with ``check_``. If you wish + to disable selected checks, simply add a line in your global + config which sets the appropriate method to False:: + + [global] + checker.check_skipped_app_config = False + + You may also dynamically add or replace ``check_*`` methods in this way. + """ + + on = True + """If True (the default), run all checks; if False, turn off all checks.""" + + + def __init__(self): + self._populate_known_types() + + def __call__(self): + """Run all check_* methods.""" + if self.on: + oldformatwarning = warnings.formatwarning + warnings.formatwarning = self.formatwarning + try: + for name in dir(self): + if name.startswith("check_"): + method = getattr(self, name) + if method and hasattr(method, '__call__'): + method() + finally: + warnings.formatwarning = oldformatwarning + + def formatwarning(self, message, category, filename, lineno, line=None): + """Function to format a warning.""" + return "CherryPy Checker:\n%s\n\n" % message + + # This value should be set inside _cpconfig. + global_config_contained_paths = False + + def check_app_config_entries_dont_start_with_script_name(self): + """Check for Application config with sections that repeat script_name.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + if sn == '': + continue + sn_atoms = sn.strip("/").split("/") + for key in app.config.keys(): + key_atoms = key.strip("/").split("/") + if key_atoms[:len(sn_atoms)] == sn_atoms: + warnings.warn( + "The application mounted at %r has config " \ + "entries that start with its script name: %r" % (sn, key)) + + def check_site_config_entries_in_app_config(self): + """Check for mounted Applications that have site-scoped config.""" + for sn, app in iteritems(cherrypy.tree.apps): + if not isinstance(app, cherrypy.Application): + continue + + msg = [] + for section, entries in iteritems(app.config): + if section.startswith('/'): + for key, value in iteritems(entries): + for n in ("engine.", "server.", "tree.", "checker."): + if key.startswith(n): + msg.append("[%s] %s = %s" % (section, key, value)) + if msg: + msg.insert(0, + "The application mounted at %r contains the following " + "config entries, which are only allowed in site-wide " + "config. Move them to a [global] section and pass them " + "to cherrypy.config.update() instead of tree.mount()." % sn) + warnings.warn(os.linesep.join(msg)) + + def check_skipped_app_config(self): + """Check for mounted Applications that have no config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + msg = "The Application mounted at %r has an empty config." % sn + if self.global_config_contained_paths: + msg += (" It looks like the config you passed to " + "cherrypy.config.update() contains application-" + "specific sections. You must explicitly pass " + "application config via " + "cherrypy.tree.mount(..., config=app_config)") + warnings.warn(msg) + return + + def check_app_config_brackets(self): + """Check for Application config with extraneous brackets in section names.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + for key in app.config.keys(): + if key.startswith("[") or key.endswith("]"): + warnings.warn( + "The application mounted at %r has config " \ + "section names with extraneous brackets: %r. " + "Config *files* need brackets; config *dicts* " + "(e.g. passed to tree.mount) do not." % (sn, key)) + + def check_static_paths(self): + """Check Application config for incorrect static paths.""" + # Use the dummy Request object in the main thread. + request = cherrypy.request + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + request.app = app + for section in app.config: + # get_resource will populate request.config + request.get_resource(section + "/dummy.html") + conf = request.config.get + + if conf("tools.staticdir.on", False): + msg = "" + root = conf("tools.staticdir.root") + dir = conf("tools.staticdir.dir") + if dir is None: + msg = "tools.staticdir.dir is not set." + else: + fulldir = "" + if os.path.isabs(dir): + fulldir = dir + if root: + msg = ("dir is an absolute path, even " + "though a root is provided.") + testdir = os.path.join(root, dir[1:]) + if os.path.exists(testdir): + msg += ("\nIf you meant to serve the " + "filesystem folder at %r, remove " + "the leading slash from dir." % testdir) + else: + if not root: + msg = "dir is a relative path and no root provided." + else: + fulldir = os.path.join(root, dir) + if not os.path.isabs(fulldir): + msg = "%r is not an absolute path." % fulldir + + if fulldir and not os.path.exists(fulldir): + if msg: + msg += "\n" + msg += ("%r (root + dir) is not an existing " + "filesystem path." % fulldir) + + if msg: + warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" + % (msg, section, root, dir)) + + + # -------------------------- Compatibility -------------------------- # + + obsolete = { + 'server.default_content_type': 'tools.response_headers.headers', + 'log_access_file': 'log.access_file', + 'log_config_options': None, + 'log_file': 'log.error_file', + 'log_file_not_found': None, + 'log_request_headers': 'tools.log_headers.on', + 'log_to_screen': 'log.screen', + 'show_tracebacks': 'request.show_tracebacks', + 'throw_errors': 'request.throw_errors', + 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' + 'cherrypy.Application(Root())))'), + } + + deprecated = {} + + def _compat(self, config): + """Process config and warn on each obsolete or deprecated entry.""" + for section, conf in config.items(): + if isinstance(conf, dict): + for k, v in conf.items(): + if k in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead.\n" + "section: [%s]" % + (k, self.obsolete[k], section)) + elif k in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead.\n" + "section: [%s]" % + (k, self.deprecated[k], section)) + else: + if section in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead." + % (section, self.obsolete[section])) + elif section in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead." + % (section, self.deprecated[section])) + + def check_compatibility(self): + """Process config and warn on each obsolete or deprecated entry.""" + self._compat(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._compat(app.config) + + + # ------------------------ Known Namespaces ------------------------ # + + extra_config_namespaces = [] + + def _known_ns(self, app): + ns = ["wsgi"] + ns.extend(copykeys(app.toolboxes)) + ns.extend(copykeys(app.namespaces)) + ns.extend(copykeys(app.request_class.namespaces)) + ns.extend(copykeys(cherrypy.config.namespaces)) + ns += self.extra_config_namespaces + + for section, conf in app.config.items(): + is_path_section = section.startswith("/") + if is_path_section and isinstance(conf, dict): + for k, v in conf.items(): + atoms = k.split(".") + if len(atoms) > 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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py new file mode 100644 index 0000000..ed24c1a --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py new file mode 100644 index 0000000..7b4c6a4 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py new file mode 100644 index 0000000..d614e08 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py new file mode 100644 index 0000000..76a409f --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py @@ -0,0 +1,556 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise themselves; +this means you can set an :class:`HTTPError` +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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py new file mode 100644 index 0000000..e10c942 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py new file mode 100644 index 0000000..76ef6ea --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py new file mode 100644 index 0000000..57f715a --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py new file mode 100644 index 0000000..5d72c85 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py new file mode 100644 index 0000000..5890c72 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py new file mode 100644 index 0000000..2eecd6e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py @@ -0,0 +1,205 @@ +"""Manage HTTP servers with CherryPy.""" + +import warnings + +import cherrypy +from cherrypy.lib import attributes +from cherrypy._cpcompat import basestring, py3k + +# We import * because we want to export check_port +# et al as attributes of this module. +from cherrypy.process.servers import * + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + def _get_socket_host(self): + return self._socket_host + def _set_socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + "interfaces (INADDR_ANY).") + self._socket_host = value + socket_host = property(_get_socket_host, _set_socket_host, + doc="""The hostname or IP address on which to listen for connections. + + 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.""") + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + CPWSGIServer) which cherrypy.server will control. Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + if py3k: + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are: 'builtin' (to use the SSL library built + into recent versions of Python). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + else: + ssl_module = 'pyopenssl' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are 'builtin' (to use the SSL library built + into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL + project, which you must install separately). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" + + def __init__(self): + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, basestring): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + ServerAdapter.start(self) + start.priority = 75 + + def _get_bind_addr(self): + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + def _set_bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, basestring): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError("bind_addr must be a (host, port) tuple " + "(for TCP sockets) or a string (for Unix " + "domain sockets), not %r" % value) + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') + + def base(self): + """Return the base (scheme://host[:port] or sock file) for this server.""" + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = "https" + if port != 443: + host += ":%s" % port + else: + scheme = "http" + if port != 80: + host += ":%s" % port + + return "%s://%s" % (scheme, host) + diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py new file mode 100644 index 0000000..34c17ac --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py new file mode 100644 index 0000000..22316b3 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py new file mode 100644 index 0000000..3aa4b9e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py new file mode 100644 index 0000000..91cd044 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py new file mode 100644 index 0000000..21af513 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py @@ -0,0 +1,63 @@ +"""WSGI server interface (see PEP 333). This adds some CP-specific bits to +the framework-agnostic wsgiserver package. +""" +import sys + +import cherrypy +from cherrypy import wsgiserver + + +class CPWSGIServer(wsgiserver.CherryPyWSGIServer): + """Wrapper for wsgiserver.CherryPyWSGIServer. + + 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 set our own mount points from cherrypy.tree + and apply some attributes from config -> 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py new file mode 100644 index 0000000..3fc0ec5 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py new file mode 100644 index 0000000..7d2f6dc --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py new file mode 100644 index 0000000..2c05e01 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py @@ -0,0 +1,87 @@ +# This file is part of CherryPy +# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py new file mode 100644 index 0000000..67578e0 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py @@ -0,0 +1,365 @@ +# This file is part of CherryPy +# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py new file mode 100644 index 0000000..435b9dc --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py new file mode 100644 index 0000000..9b701b5 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py @@ -0,0 +1,365 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation `_ +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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py new file mode 100644 index 0000000..9be947f --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py new file mode 100644 index 0000000..b426a3e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py new file mode 100644 index 0000000..6459746 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py @@ -0,0 +1,388 @@ +import struct +import time + +import cherrypy +from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr +from cherrypy.lib import file_generator +from cherrypy.lib import set_vary_header + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is given in + the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request entity is + *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = "Response body could not be encoded with %r." + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + try: + body = [] + for chunk in self.body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + body.append(chunk) + self.body = body + except (LookupError, UnicodeError): + return False + else: + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py new file mode 100644 index 0000000..183148b --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py @@ -0,0 +1,214 @@ +import gc +import inspect +import os +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py new file mode 100644 index 0000000..4661d69 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py new file mode 100644 index 0000000..ad7c6eb --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py new file mode 100644 index 0000000..5f77d54 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py new file mode 100644 index 0000000..2092579 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py new file mode 100644 index 0000000..785d58a --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py new file mode 100644 index 0000000..ba8ff51 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py new file mode 100644 index 0000000..9763f12 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py @@ -0,0 +1,871 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_type = "file" + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory /home/site/sessions, +and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions +will be saved in RAM. ``tools.sessions.on`` is the only required line for +working sessions, the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. If you want to control when the +session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. +Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +`_. +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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py new file mode 100644 index 0000000..2d14230 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py @@ -0,0 +1,363 @@ +try: + from io import UnsupportedOperation +except ImportError: + UnsupportedOperation = object() +import logging +import mimetypes +mimetypes.init() +mimetypes.types_map['.dwg']='image/x-dwg' +mimetypes.types_map['.ico']='image/x-icon' +mimetypes.types_map['.bz2']='application/x-bzip2' +mimetypes.types_map['.gz']='application/x-gzip' + +import os +import re +import stat +import time + +import cherrypy +from cherrypy._cpcompat import ntob, unquote +from cherrypy.lib import cptools, httputil, file_generator_limited + + +def serve_file(path, content_type=None, disposition=None, name=None, debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "; 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 0000000..9a44464 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py new file mode 100644 index 0000000..f15b123 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py new file mode 100644 index 0000000..ba618a0 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py @@ -0,0 +1,683 @@ +"""Site services for use with a Web Site Process Bus.""" + +import os +import re +import signal as _signal +import sys +import time +import threading + +from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set + +# _module__file__base is used by Autoreload to make +# absolute any filenames retrieved from sys.modules which are not +# already absolute paths. This is to work around Python's quirk +# of importing the startup script and using a relative filename +# for it in sys.modules. +# +# Autoreload examines sys.modules afresh every time it runs. If an application +# changes the current directory by executing os.chdir(), then the next time +# Autoreload runs, it will not be able to find any filenames which are +# not absolute paths, because the current directory is not the same as when the +# module was first imported. Autoreload will then wrongly conclude the file has +# "changed", and initiate the shutdown/re-exec sequence. +# See ticket #917. +# For this workaround to have a decent probability of success, this module +# needs to be imported as early as possible, before the app has much chance +# to change the working directory. +_module__file__base = os.getcwd() + + +class SimplePlugin(object): + """Plugin base class which auto-subscribes methods for known channels.""" + + bus = None + """A :class:`Bus `, 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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py new file mode 100644 index 0000000..fa714d6 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py @@ -0,0 +1,427 @@ +""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin`. 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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py new file mode 100644 index 0000000..83f99a5 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py new file mode 100644 index 0000000..6ef768d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py new file mode 100644 index 0000000..00964ac --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py new file mode 100644 index 0000000..7703607 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py @@ -0,0 +1,27 @@ +"""Regression test suite for CherryPy. + +Run 'nosetests -s test/' to exercise all tests. + +The '-s' flag instructs nose to output stdout messages, wihch is crucial to +the 'interactive' mode of webtest.py. If you run these tests without the '-s' +flag, don't be surprised if the test seems to hang: it's waiting for your +interactive input. +""" + +import os +import sys + +def newexit(): + os._exit(1) + +def setup(): + # We want to monkey patch sys.exit so that we can get some + # information about where exit is being called. + newexit._old = sys.exit + sys.exit = newexit + +def teardown(): + try: + sys.exit = sys.exit._old + except AttributeError: + sys.exit = sys._exit diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py new file mode 100644 index 0000000..5bcbc1e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py @@ -0,0 +1,41 @@ +"""Test module for the @-decorator syntax, which is version-specific""" + +from cherrypy import expose, tools +from cherrypy._cpcompat import ntob + + +class ExposeExamples(object): + + @expose + def no_call(self): + return "Mr E. R. Bradshaw" + + @expose() + def call_empty(self): + return "Mrs. B.J. Smegma" + + @expose("call_alias") + def nesbitt(self): + return "Mr Nesbitt" + + @expose(["alias1", "alias2"]) + def andrews(self): + return "Mr Ken Andrews" + + @expose(alias="alias3") + def watson(self): + return "Mr. and Mrs. Watson" + + +class ToolExamples(object): + + @expose + @tools.response_headers(headers=[('Content-Type', 'application/data')]) + def blah(self): + yield ntob("blah") + # This is here to demonstrate that _cp_config = {...} overwrites + # the _cp_config attribute added by the Tool decorator. You have + # to write _cp_config[k] = v or _cp_config.update(...) instead. + blah._cp_config['response.stream'] = True + + diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py new file mode 100644 index 0000000..3f8f196 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py @@ -0,0 +1,66 @@ +import os +import sys +import time +starttime = time.time() + +import cherrypy + + +class Root: + + def index(self): + return "Hello World" + index.exposed = True + + def mtimes(self): + return repr(cherrypy.engine.publish("Autoreloader", "mtimes")) + mtimes.exposed = True + + def pid(self): + return str(os.getpid()) + pid.exposed = True + + def start(self): + return repr(starttime) + start.exposed = True + + def exit(self): + # This handler might be called before the engine is STARTED if an + # HTTP worker thread handles it before the HTTP server returns + # control to engine.start. We avoid that race condition here + # by waiting for the Bus to be STARTED. + cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) + cherrypy.engine.exit() + exit.exposed = True + + +def unsub_sig(): + cherrypy.log("unsubsig: %s" % cherrypy.config.get('unsubsig', False)) + if cherrypy.config.get('unsubsig', False): + cherrypy.log("Unsubscribing the default cherrypy signal handler") + cherrypy.engine.signal_handler.unsubscribe() + try: + from signal import signal, SIGTERM + except ImportError: + pass + else: + def old_term_handler(signum=None, frame=None): + cherrypy.log("I am an old SIGTERM handler.") + sys.exit(0) + cherrypy.log("Subscribing the new one.") + signal(SIGTERM, old_term_handler) +cherrypy.engine.subscribe('start', unsub_sig, priority=100) + + +def starterror(): + if cherrypy.config.get('starterror', False): + zerodiv = 1 / 0 +cherrypy.engine.subscribe('start', starterror, priority=6) + +def log_test_case_name(): + if cherrypy.config.get('test_case_name', False): + cherrypy.log("STARTED FROM: %s" % cherrypy.config.get('test_case_name')) +cherrypy.engine.subscribe('start', log_test_case_name, priority=6) + + +cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py new file mode 100644 index 0000000..bd5deb6 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py @@ -0,0 +1,409 @@ +"""CherryPy Benchmark Tool + + Usage: + benchmark.py --null --notests --help --cpmodpy --modpython --ab=path --apache=path + + --null: use a null Request object (to bench the HTTP server only) + --notests: start the server but do not run the tests; this allows + you to check the tested pages with a browser + --help: show this help message + --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) + --modpython: run tests via apache on 54583 (with modpython_gateway) + --ab=path: Use the ab script/executable at 'path' (see below) + --apache=path: Use the apache script/exe at 'path' (see below) + + To run the benchmarks, the Apache Benchmark tool "ab" must either be on + your system path, or specified via the --ab=path option. + + To run the modpython tests, the "apache" executable or script must be + on your system path, or provided via the --apache=path option. On some + platforms, "apache" may be called "apachectl" or "apache2ctl"--create + a symlink to them if needed. +""" + +import getopt +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +import re +import sys +import time +import traceback + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy import _cperror, _cpmodpy +from cherrypy.lib import httputil + + +AB_PATH = "" +APACHE_PATH = "apache" +SCRIPT_NAME = "/cpbench/users/rdelon/apps/blog" + +__all__ = ['ABSession', 'Root', 'print_report', + 'run_standard_benchmarks', 'safe_threads', + 'size_report', 'startup', 'thread_report', + ] + +size_cache = {} + +class Root: + + def index(self): + return """ + + 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py new file mode 100644 index 0000000..32a7dee --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py new file mode 100644 index 0000000..22b8ccc --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py new file mode 100644 index 0000000..3c6f114 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py new file mode 100644 index 0000000..95acf14 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py new file mode 100644 index 0000000..736aa4c --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py new file mode 100644 index 0000000..519571f --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py new file mode 100644 index 0000000..309a541 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py new file mode 100644 index 0000000..342e5b5 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py new file mode 100644 index 0000000..3a9781d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py @@ -0,0 +1,79 @@ +# This file is part of CherryPy +# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py new file mode 100644 index 0000000..1960fa8 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py new file mode 100644 index 0000000..51c1022 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py new file mode 100644 index 0000000..c210e6e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py new file mode 100644 index 0000000..b1ef6a3 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py new file mode 100644 index 0000000..0b9718d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py new file mode 100644 index 0000000..1346f59 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py new file mode 100644 index 0000000..b4e830d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py new file mode 100644 index 0000000..0395b7b --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py new file mode 100644 index 0000000..2d0ce76 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py new file mode 100644 index 0000000..aec1693 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py new file mode 100644 index 0000000..639c6c4 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py new file mode 100644 index 0000000..9d0eecb --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py new file mode 100644 index 0000000..5dc40fd --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py new file mode 100644 index 0000000..a02c076 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py new file mode 100644 index 0000000..7d506e8 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py new file mode 100644 index 0000000..1605991 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py new file mode 100644 index 0000000..1dd1429 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py new file mode 100644 index 0000000..8dcf2d3 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py new file mode 100644 index 0000000..2fbb619 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py new file mode 100644 index 0000000..279935e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py new file mode 100644 index 0000000..26eea56 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py new file mode 100644 index 0000000..a8062f8 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py new file mode 100644 index 0000000..9143a1d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py new file mode 100644 index 0000000..ab1fe51 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py new file mode 100644 index 0000000..6322687 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py new file mode 100644 index 0000000..871420b --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py new file mode 100644 index 0000000..02bacda --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py new file mode 100644 index 0000000..aab2786 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py new file mode 100644 index 0000000..dbd2dbc --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py new file mode 100644 index 0000000..e3c6ce6 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py new file mode 100644 index 0000000..abb1a91 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py new file mode 100644 index 0000000..d4b8b79 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py new file mode 100644 index 0000000..f7a6927 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py new file mode 100644 index 0000000..50cfbad --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py new file mode 100644 index 0000000..c4e2c55 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py new file mode 100644 index 0000000..c43feb4 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py new file mode 100644 index 0000000..ef94760 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py @@ -0,0 +1,35 @@ +""" +Tutorial - Hello World + +The most basic (working) CherryPy application possible. +""" + +# Import CherryPy global namespace +import cherrypy + +class HelloWorld: + """ Sample request handler class. """ + + def index(self): + # CherryPy will call this method for the root URI ("/") and send + # its return value to the client. Because this is tutorial + # lesson number 01, we'll just send something really simple. + # How about... + return "Hello world!" + + # Expose the index method through the web. CherryPy will never + # publish methods that don't have the exposed attribute set to True. + 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(HelloWorld(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py new file mode 100644 index 0000000..600fca3 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py @@ -0,0 +1,32 @@ +""" +Tutorial - Multiple methods + +This tutorial shows you how to link to other methods of your request +handler. +""" + +import cherrypy + +class HelloWorld: + + def index(self): + # Let's link to another method here. + return 'We have an important 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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py new file mode 100644 index 0000000..283477d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py new file mode 100644 index 0000000..b4d820e --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py new file mode 100644 index 0000000..3d4ec9b --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py new file mode 100644 index 0000000..fe24f38 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py new file mode 100644 index 0000000..4b1386b --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py new file mode 100644 index 0000000..a6fbdc2 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py new file mode 100644 index 0000000..4c8e581 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py new file mode 100644 index 0000000..dfa5733 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py new file mode 100644 index 0000000..ee6190f --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py new file mode 100644 index 0000000..03bf05d --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py @@ -0,0 +1,91 @@ +"""A library for integrating Python's builtin ``ssl`` library with CherryPy. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +import sys + +from cherrypy import wsgiserver + + +class BuiltinSSLAdapter(wsgiserver.SSLAdapter): + """A wrapper for integrating Python's builtin ssl module with CherryPy.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + def __init__(self, certificate, private_key, certificate_chain=None): + if ssl is None: + raise ImportError("You must install the ssl module to use HTTPS.") + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + try: + s = ssl.wrap_socket(sock, do_handshake_on_connect=True, + server_side=True, certfile=self.certificate, + keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) + except ssl.SSLError: + e = sys.exc_info()[1] + if e.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return None, {} + elif e.errno == ssl.SSL_ERROR_SSL: + if e.args[1].endswith('http request'): + # The client is speaking HTTP to an HTTPS server. + raise wsgiserver.NoSSLError + elif e.args[1].endswith('unknown protocol'): + # The client is speaking some non-HTTP protocol. + # Drop the conn. + return None, {} + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + "wsgi.url_scheme": "https", + "HTTPS": "on", + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + if sys.version_info >= (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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py new file mode 100644 index 0000000..f3d9bf5 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py new file mode 100644 index 0000000..b6bd499 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/lib/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/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd b/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd new file mode 100644 index 0000000..17c6157 --- /dev/null +++ b/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd @@ -0,0 +1,109 @@ +#!c:\Python27\python.exe +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec("import %s" % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " + "scgi options.", 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload_on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + if fastcgi: + f = servers.FlupFCGIServer(application=cherrypy.tree, + bindAddress=addr) + elif scgi: + f = servers.FlupSCGIServer(application=cherrypy.tree, + bindAddress=addr) + else: + f = servers.FlupCGIServer(application=cherrypy.tree, + bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +if __name__ == '__main__': + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action="append", dest='config', + help="specify config file(s)") + p.add_option('-d', action="store_true", dest='daemonize', + help="run the server as a daemon") + p.add_option('-e', '--environment', dest='environment', default=None, + help="apply the given config environment") + p.add_option('-f', action="store_true", dest='fastcgi', + help="start a fastcgi server instead of the default HTTP server") + p.add_option('-s', action="store_true", dest='scgi', + help="start a scgi server instead of the default HTTP server") + p.add_option('-x', action="store_true", dest='cgi', + help="start a cgi server instead of the default HTTP server") + p.add_option('-i', '--import', action="append", dest='imports', + help="specify modules to import") + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help="store the process id in the given file") + p.add_option('-P', '--Path', action="append", dest='Path', + help="add the given paths to sys.path") + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) + diff --git a/libs/CherryPy-3.2.2/cherrypy/LICENSE.txt b/libs/CherryPy-3.2.2/cherrypy/LICENSE.txt new file mode 100644 index 0000000..8db13fb --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/__init__.py b/libs/CherryPy-3.2.2/cherrypy/__init__.py new file mode 100644 index 0000000..41e3898 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py b/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py new file mode 100644 index 0000000..7ccfd89 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py @@ -0,0 +1,327 @@ +import os +import warnings + +import cherrypy +from cherrypy._cpcompat import iteritems, copykeys, builtins + + +class Checker(object): + """A checker for CherryPy sites and their mounted applications. + + When this object is called at engine startup, it executes each + of its own methods whose names start with ``check_``. If you wish + to disable selected checks, simply add a line in your global + config which sets the appropriate method to False:: + + [global] + checker.check_skipped_app_config = False + + You may also dynamically add or replace ``check_*`` methods in this way. + """ + + on = True + """If True (the default), run all checks; if False, turn off all checks.""" + + + def __init__(self): + self._populate_known_types() + + def __call__(self): + """Run all check_* methods.""" + if self.on: + oldformatwarning = warnings.formatwarning + warnings.formatwarning = self.formatwarning + try: + for name in dir(self): + if name.startswith("check_"): + method = getattr(self, name) + if method and hasattr(method, '__call__'): + method() + finally: + warnings.formatwarning = oldformatwarning + + def formatwarning(self, message, category, filename, lineno, line=None): + """Function to format a warning.""" + return "CherryPy Checker:\n%s\n\n" % message + + # This value should be set inside _cpconfig. + global_config_contained_paths = False + + def check_app_config_entries_dont_start_with_script_name(self): + """Check for Application config with sections that repeat script_name.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + if sn == '': + continue + sn_atoms = sn.strip("/").split("/") + for key in app.config.keys(): + key_atoms = key.strip("/").split("/") + if key_atoms[:len(sn_atoms)] == sn_atoms: + warnings.warn( + "The application mounted at %r has config " \ + "entries that start with its script name: %r" % (sn, key)) + + def check_site_config_entries_in_app_config(self): + """Check for mounted Applications that have site-scoped config.""" + for sn, app in iteritems(cherrypy.tree.apps): + if not isinstance(app, cherrypy.Application): + continue + + msg = [] + for section, entries in iteritems(app.config): + if section.startswith('/'): + for key, value in iteritems(entries): + for n in ("engine.", "server.", "tree.", "checker."): + if key.startswith(n): + msg.append("[%s] %s = %s" % (section, key, value)) + if msg: + msg.insert(0, + "The application mounted at %r contains the following " + "config entries, which are only allowed in site-wide " + "config. Move them to a [global] section and pass them " + "to cherrypy.config.update() instead of tree.mount()." % sn) + warnings.warn(os.linesep.join(msg)) + + def check_skipped_app_config(self): + """Check for mounted Applications that have no config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + msg = "The Application mounted at %r has an empty config." % sn + if self.global_config_contained_paths: + msg += (" It looks like the config you passed to " + "cherrypy.config.update() contains application-" + "specific sections. You must explicitly pass " + "application config via " + "cherrypy.tree.mount(..., config=app_config)") + warnings.warn(msg) + return + + def check_app_config_brackets(self): + """Check for Application config with extraneous brackets in section names.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + for key in app.config.keys(): + if key.startswith("[") or key.endswith("]"): + warnings.warn( + "The application mounted at %r has config " \ + "section names with extraneous brackets: %r. " + "Config *files* need brackets; config *dicts* " + "(e.g. passed to tree.mount) do not." % (sn, key)) + + def check_static_paths(self): + """Check Application config for incorrect static paths.""" + # Use the dummy Request object in the main thread. + request = cherrypy.request + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + request.app = app + for section in app.config: + # get_resource will populate request.config + request.get_resource(section + "/dummy.html") + conf = request.config.get + + if conf("tools.staticdir.on", False): + msg = "" + root = conf("tools.staticdir.root") + dir = conf("tools.staticdir.dir") + if dir is None: + msg = "tools.staticdir.dir is not set." + else: + fulldir = "" + if os.path.isabs(dir): + fulldir = dir + if root: + msg = ("dir is an absolute path, even " + "though a root is provided.") + testdir = os.path.join(root, dir[1:]) + if os.path.exists(testdir): + msg += ("\nIf you meant to serve the " + "filesystem folder at %r, remove " + "the leading slash from dir." % testdir) + else: + if not root: + msg = "dir is a relative path and no root provided." + else: + fulldir = os.path.join(root, dir) + if not os.path.isabs(fulldir): + msg = "%r is not an absolute path." % fulldir + + if fulldir and not os.path.exists(fulldir): + if msg: + msg += "\n" + msg += ("%r (root + dir) is not an existing " + "filesystem path." % fulldir) + + if msg: + warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" + % (msg, section, root, dir)) + + + # -------------------------- Compatibility -------------------------- # + + obsolete = { + 'server.default_content_type': 'tools.response_headers.headers', + 'log_access_file': 'log.access_file', + 'log_config_options': None, + 'log_file': 'log.error_file', + 'log_file_not_found': None, + 'log_request_headers': 'tools.log_headers.on', + 'log_to_screen': 'log.screen', + 'show_tracebacks': 'request.show_tracebacks', + 'throw_errors': 'request.throw_errors', + 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' + 'cherrypy.Application(Root())))'), + } + + deprecated = {} + + def _compat(self, config): + """Process config and warn on each obsolete or deprecated entry.""" + for section, conf in config.items(): + if isinstance(conf, dict): + for k, v in conf.items(): + if k in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead.\n" + "section: [%s]" % + (k, self.obsolete[k], section)) + elif k in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead.\n" + "section: [%s]" % + (k, self.deprecated[k], section)) + else: + if section in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead." + % (section, self.obsolete[section])) + elif section in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead." + % (section, self.deprecated[section])) + + def check_compatibility(self): + """Process config and warn on each obsolete or deprecated entry.""" + self._compat(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._compat(app.config) + + + # ------------------------ Known Namespaces ------------------------ # + + extra_config_namespaces = [] + + def _known_ns(self, app): + ns = ["wsgi"] + ns.extend(copykeys(app.toolboxes)) + ns.extend(copykeys(app.namespaces)) + ns.extend(copykeys(app.request_class.namespaces)) + ns.extend(copykeys(cherrypy.config.namespaces)) + ns += self.extra_config_namespaces + + for section, conf in app.config.items(): + is_path_section = section.startswith("/") + if is_path_section and isinstance(conf, dict): + for k, v in conf.items(): + atoms = k.split(".") + if len(atoms) > 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/libs/CherryPy-3.2.2/cherrypy/_cpcompat.py b/libs/CherryPy-3.2.2/cherrypy/_cpcompat.py new file mode 100644 index 0000000..ed24c1a --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpconfig.py b/libs/CherryPy-3.2.2/cherrypy/_cpconfig.py new file mode 100644 index 0000000..7b4c6a4 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py b/libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py new file mode 100644 index 0000000..d614e08 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cperror.py b/libs/CherryPy-3.2.2/cherrypy/_cperror.py new file mode 100644 index 0000000..76a409f --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/_cperror.py @@ -0,0 +1,556 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise themselves; +this means you can set an :class:`HTTPError` +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/libs/CherryPy-3.2.2/cherrypy/_cplogging.py b/libs/CherryPy-3.2.2/cherrypy/_cplogging.py new file mode 100644 index 0000000..e10c942 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py b/libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py new file mode 100644 index 0000000..76ef6ea --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py b/libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py new file mode 100644 index 0000000..57f715a --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py b/libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py new file mode 100644 index 0000000..5d72c85 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cprequest.py b/libs/CherryPy-3.2.2/cherrypy/_cprequest.py new file mode 100644 index 0000000..5890c72 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpserver.py b/libs/CherryPy-3.2.2/cherrypy/_cpserver.py new file mode 100644 index 0000000..2eecd6e --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/_cpserver.py @@ -0,0 +1,205 @@ +"""Manage HTTP servers with CherryPy.""" + +import warnings + +import cherrypy +from cherrypy.lib import attributes +from cherrypy._cpcompat import basestring, py3k + +# We import * because we want to export check_port +# et al as attributes of this module. +from cherrypy.process.servers import * + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + def _get_socket_host(self): + return self._socket_host + def _set_socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + "interfaces (INADDR_ANY).") + self._socket_host = value + socket_host = property(_get_socket_host, _set_socket_host, + doc="""The hostname or IP address on which to listen for connections. + + 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.""") + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + CPWSGIServer) which cherrypy.server will control. Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + if py3k: + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are: 'builtin' (to use the SSL library built + into recent versions of Python). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + else: + ssl_module = 'pyopenssl' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are 'builtin' (to use the SSL library built + into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL + project, which you must install separately). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" + + def __init__(self): + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, basestring): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + ServerAdapter.start(self) + start.priority = 75 + + def _get_bind_addr(self): + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + def _set_bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, basestring): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError("bind_addr must be a (host, port) tuple " + "(for TCP sockets) or a string (for Unix " + "domain sockets), not %r" % value) + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') + + def base(self): + """Return the base (scheme://host[:port] or sock file) for this server.""" + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = "https" + if port != 443: + host += ":%s" % port + else: + scheme = "http" + if port != 80: + host += ":%s" % port + + return "%s://%s" % (scheme, host) + diff --git a/libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py b/libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py new file mode 100644 index 0000000..34c17ac --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cptools.py b/libs/CherryPy-3.2.2/cherrypy/_cptools.py new file mode 100644 index 0000000..22316b3 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cptree.py b/libs/CherryPy-3.2.2/cherrypy/_cptree.py new file mode 100644 index 0000000..3aa4b9e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py b/libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py new file mode 100644 index 0000000..91cd044 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py b/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py new file mode 100644 index 0000000..21af513 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py @@ -0,0 +1,63 @@ +"""WSGI server interface (see PEP 333). This adds some CP-specific bits to +the framework-agnostic wsgiserver package. +""" +import sys + +import cherrypy +from cherrypy import wsgiserver + + +class CPWSGIServer(wsgiserver.CherryPyWSGIServer): + """Wrapper for wsgiserver.CherryPyWSGIServer. + + 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 set our own mount points from cherrypy.tree + and apply some attributes from config -> 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/libs/CherryPy-3.2.2/cherrypy/cherryd b/libs/CherryPy-3.2.2/cherrypy/cherryd new file mode 100644 index 0000000..adb2a02 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/cherryd @@ -0,0 +1,109 @@ +#! /usr/bin/env python +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec("import %s" % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " + "scgi options.", 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload_on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + if fastcgi: + f = servers.FlupFCGIServer(application=cherrypy.tree, + bindAddress=addr) + elif scgi: + f = servers.FlupSCGIServer(application=cherrypy.tree, + bindAddress=addr) + else: + f = servers.FlupCGIServer(application=cherrypy.tree, + bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +if __name__ == '__main__': + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action="append", dest='config', + help="specify config file(s)") + p.add_option('-d', action="store_true", dest='daemonize', + help="run the server as a daemon") + p.add_option('-e', '--environment', dest='environment', default=None, + help="apply the given config environment") + p.add_option('-f', action="store_true", dest='fastcgi', + help="start a fastcgi server instead of the default HTTP server") + p.add_option('-s', action="store_true", dest='scgi', + help="start a scgi server instead of the default HTTP server") + p.add_option('-x', action="store_true", dest='cgi', + help="start a cgi server instead of the default HTTP server") + p.add_option('-i', '--import', action="append", dest='imports', + help="specify modules to import") + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help="store the process id in the given file") + p.add_option('-P', '--Path', action="append", dest='Path', + help="add the given paths to sys.path") + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) + diff --git a/libs/CherryPy-3.2.2/cherrypy/favicon.ico b/libs/CherryPy-3.2.2/cherrypy/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0d7e61badad3f332cf1e663efb97c0b5be80f5e GIT binary patch literal 1406 zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K zC3>Y)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/libs/CherryPy-3.2.2/cherrypy/lib/__init__.py b/libs/CherryPy-3.2.2/cherrypy/lib/__init__.py new file mode 100644 index 0000000..3fc0ec5 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/auth.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth.py new file mode 100644 index 0000000..7d2f6dc --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py new file mode 100644 index 0000000..2c05e01 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py @@ -0,0 +1,87 @@ +# This file is part of CherryPy +# -*- 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/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py new file mode 100644 index 0000000..67578e0 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py @@ -0,0 +1,365 @@ +# This file is part of CherryPy +# -*- 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/libs/CherryPy-3.2.2/cherrypy/lib/caching.py b/libs/CherryPy-3.2.2/cherrypy/lib/caching.py new file mode 100644 index 0000000..435b9dc --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py b/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py new file mode 100644 index 0000000..9b701b5 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py @@ -0,0 +1,365 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation `_ +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/libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py b/libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py new file mode 100644 index 0000000..9be947f --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/cptools.py b/libs/CherryPy-3.2.2/cherrypy/lib/cptools.py new file mode 100644 index 0000000..b426a3e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py b/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py new file mode 100644 index 0000000..6459746 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py @@ -0,0 +1,388 @@ +import struct +import time + +import cherrypy +from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr +from cherrypy.lib import file_generator +from cherrypy.lib import set_vary_header + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is given in + the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request entity is + *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = "Response body could not be encoded with %r." + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + try: + body = [] + for chunk in self.body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + body.append(chunk) + self.body = body + except (LookupError, UnicodeError): + return False + else: + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> 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/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py b/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py new file mode 100644 index 0000000..183148b --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py @@ -0,0 +1,214 @@ +import gc +import inspect +import os +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > 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/libs/CherryPy-3.2.2/cherrypy/lib/http.py b/libs/CherryPy-3.2.2/cherrypy/lib/http.py new file mode 100644 index 0000000..4661d69 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py b/libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py new file mode 100644 index 0000000..ad7c6eb --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/httputil.py b/libs/CherryPy-3.2.2/cherrypy/lib/httputil.py new file mode 100644 index 0000000..5f77d54 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py b/libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py new file mode 100644 index 0000000..2092579 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/profiler.py b/libs/CherryPy-3.2.2/cherrypy/lib/profiler.py new file mode 100644 index 0000000..785d58a --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py b/libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py new file mode 100644 index 0000000..ba8ff51 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py b/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py new file mode 100644 index 0000000..9763f12 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py @@ -0,0 +1,871 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_type = "file" + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory /home/site/sessions, +and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions +will be saved in RAM. ``tools.sessions.on`` is the only required line for +working sessions, the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. If you want to control when the +session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. +Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +`_. +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/libs/CherryPy-3.2.2/cherrypy/lib/static.py b/libs/CherryPy-3.2.2/cherrypy/lib/static.py new file mode 100644 index 0000000..2d14230 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/lib/static.py @@ -0,0 +1,363 @@ +try: + from io import UnsupportedOperation +except ImportError: + UnsupportedOperation = object() +import logging +import mimetypes +mimetypes.init() +mimetypes.types_map['.dwg']='image/x-dwg' +mimetypes.types_map['.ico']='image/x-icon' +mimetypes.types_map['.bz2']='application/x-bzip2' +mimetypes.types_map['.gz']='application/x-gzip' + +import os +import re +import stat +import time + +import cherrypy +from cherrypy._cpcompat import ntob, unquote +from cherrypy.lib import cptools, httputil, file_generator_limited + + +def serve_file(path, content_type=None, disposition=None, name=None, debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "; 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/libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py b/libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 0000000..9a44464 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/process/__init__.py b/libs/CherryPy-3.2.2/cherrypy/process/__init__.py new file mode 100644 index 0000000..f15b123 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/process/plugins.py b/libs/CherryPy-3.2.2/cherrypy/process/plugins.py new file mode 100644 index 0000000..ba618a0 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/process/plugins.py @@ -0,0 +1,683 @@ +"""Site services for use with a Web Site Process Bus.""" + +import os +import re +import signal as _signal +import sys +import time +import threading + +from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set + +# _module__file__base is used by Autoreload to make +# absolute any filenames retrieved from sys.modules which are not +# already absolute paths. This is to work around Python's quirk +# of importing the startup script and using a relative filename +# for it in sys.modules. +# +# Autoreload examines sys.modules afresh every time it runs. If an application +# changes the current directory by executing os.chdir(), then the next time +# Autoreload runs, it will not be able to find any filenames which are +# not absolute paths, because the current directory is not the same as when the +# module was first imported. Autoreload will then wrongly conclude the file has +# "changed", and initiate the shutdown/re-exec sequence. +# See ticket #917. +# For this workaround to have a decent probability of success, this module +# needs to be imported as early as possible, before the app has much chance +# to change the working directory. +_module__file__base = os.getcwd() + + +class SimplePlugin(object): + """Plugin base class which auto-subscribes methods for known channels.""" + + bus = None + """A :class:`Bus `, 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/libs/CherryPy-3.2.2/cherrypy/process/servers.py b/libs/CherryPy-3.2.2/cherrypy/process/servers.py new file mode 100644 index 0000000..fa714d6 --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/process/servers.py @@ -0,0 +1,427 @@ +""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin`. 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/libs/CherryPy-3.2.2/cherrypy/process/win32.py b/libs/CherryPy-3.2.2/cherrypy/process/win32.py new file mode 100644 index 0000000..83f99a5 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/process/wspbus.py b/libs/CherryPy-3.2.2/cherrypy/process/wspbus.py new file mode 100644 index 0000000..6ef768d --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py b/libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py new file mode 100644 index 0000000..00964ac --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf new file mode 100644 index 0000000..922398e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf new file mode 100644 index 0000000..93a6e53 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf new file mode 100644 index 0000000..6ed3898 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py b/libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py new file mode 100644 index 0000000..32a7dee --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/helper.py b/libs/CherryPy-3.2.2/cherrypy/test/helper.py new file mode 100644 index 0000000..22b8ccc --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/logtest.py b/libs/CherryPy-3.2.2/cherrypy/test/logtest.py new file mode 100644 index 0000000..3c6f114 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py b/libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py new file mode 100644 index 0000000..95acf14 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py b/libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py new file mode 100644 index 0000000..736aa4c --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/modpy.py b/libs/CherryPy-3.2.2/cherrypy/test/modpy.py new file mode 100644 index 0000000..519571f --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py b/libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py new file mode 100644 index 0000000..309a541 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py b/libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py new file mode 100644 index 0000000..342e5b5 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/static/dirback.jpg b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py b/libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py new file mode 100644 index 0000000..1960fa8 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_bus.py b/libs/CherryPy-3.2.2/cherrypy/test/test_bus.py new file mode 100644 index 0000000..51c1022 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_caching.py b/libs/CherryPy-3.2.2/cherrypy/test/test_caching.py new file mode 100644 index 0000000..c210e6e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_config.py b/libs/CherryPy-3.2.2/cherrypy/test/test_config.py new file mode 100644 index 0000000..b1ef6a3 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py b/libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py new file mode 100644 index 0000000..0b9718d --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_conn.py b/libs/CherryPy-3.2.2/cherrypy/test/test_conn.py new file mode 100644 index 0000000..1346f59 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_core.py b/libs/CherryPy-3.2.2/cherrypy/test/test_core.py new file mode 100644 index 0000000..b4e830d --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py b/libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py new file mode 100644 index 0000000..0395b7b --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py b/libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py new file mode 100644 index 0000000..2d0ce76 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_etags.py b/libs/CherryPy-3.2.2/cherrypy/test/test_etags.py new file mode 100644 index 0000000..aec1693 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_http.py b/libs/CherryPy-3.2.2/cherrypy/test/test_http.py new file mode 100644 index 0000000..639c6c4 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py b/libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py new file mode 100644 index 0000000..9d0eecb --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py b/libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py new file mode 100644 index 0000000..5dc40fd --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_json.py b/libs/CherryPy-3.2.2/cherrypy/test/test_json.py new file mode 100644 index 0000000..a02c076 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_logging.py b/libs/CherryPy-3.2.2/cherrypy/test/test_logging.py new file mode 100644 index 0000000..7d506e8 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_mime.py b/libs/CherryPy-3.2.2/cherrypy/test/test_mime.py new file mode 100644 index 0000000..1605991 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py b/libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py new file mode 100644 index 0000000..1dd1429 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py b/libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py new file mode 100644 index 0000000..8dcf2d3 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py b/libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py new file mode 100644 index 0000000..2fbb619 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py b/libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py new file mode 100644 index 0000000..279935e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py b/libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py new file mode 100644 index 0000000..26eea56 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_routes.py b/libs/CherryPy-3.2.2/cherrypy/test/test_routes.py new file mode 100644 index 0000000..a8062f8 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_session.py b/libs/CherryPy-3.2.2/cherrypy/test/test_session.py new file mode 100644 index 0000000..9143a1d --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py b/libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py new file mode 100644 index 0000000..ab1fe51 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_states.py b/libs/CherryPy-3.2.2/cherrypy/test/test_states.py new file mode 100644 index 0000000..6322687 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_static.py b/libs/CherryPy-3.2.2/cherrypy/test/test_static.py new file mode 100644 index 0000000..871420b --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_tools.py b/libs/CherryPy-3.2.2/cherrypy/test/test_tools.py new file mode 100644 index 0000000..02bacda --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py b/libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py new file mode 100644 index 0000000..aab2786 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py b/libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py new file mode 100644 index 0000000..dbd2dbc --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py new file mode 100644 index 0000000..e3c6ce6 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py new file mode 100644 index 0000000..abb1a91 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py new file mode 100644 index 0000000..d4b8b79 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py b/libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py new file mode 100644 index 0000000..f7a6927 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/test/webtest.py b/libs/CherryPy-3.2.2/cherrypy/test/webtest.py new file mode 100644 index 0000000..50cfbad --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt b/libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt new file mode 100644 index 0000000..2b877e1 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py new file mode 100644 index 0000000..c4e2c55 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py new file mode 100644 index 0000000..c43feb4 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html b/libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html new file mode 100644 index 0000000..d0f30c8 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/pdf_file.pdf b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py new file mode 100644 index 0000000..283477d --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py new file mode 100644 index 0000000..b4d820e --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py new file mode 100644 index 0000000..3d4ec9b --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py new file mode 100644 index 0000000..fe24f38 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py new file mode 100644 index 0000000..4b1386b --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py new file mode 100644 index 0000000..a6fbdc2 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py new file mode 100644 index 0000000..4c8e581 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py new file mode 100644 index 0000000..dfa5733 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf b/libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf new file mode 100644 index 0000000..6537fd3 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py new file mode 100644 index 0000000..ee6190f --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py new file mode 100644 index 0000000..03bf05d --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py @@ -0,0 +1,91 @@ +"""A library for integrating Python's builtin ``ssl`` library with CherryPy. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +import sys + +from cherrypy import wsgiserver + + +class BuiltinSSLAdapter(wsgiserver.SSLAdapter): + """A wrapper for integrating Python's builtin ssl module with CherryPy.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + def __init__(self, certificate, private_key, certificate_chain=None): + if ssl is None: + raise ImportError("You must install the ssl module to use HTTPS.") + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + try: + s = ssl.wrap_socket(sock, do_handshake_on_connect=True, + server_side=True, certfile=self.certificate, + keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) + except ssl.SSLError: + e = sys.exc_info()[1] + if e.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return None, {} + elif e.errno == ssl.SSL_ERROR_SSL: + if e.args[1].endswith('http request'): + # The client is speaking HTTP to an HTTPS server. + raise wsgiserver.NoSSLError + elif e.args[1].endswith('unknown protocol'): + # The client is speaking some non-HTTP protocol. + # Drop the conn. + return None, {} + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + "wsgi.url_scheme": "https", + "HTTPS": "on", + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + if sys.version_info >= (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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py new file mode 100644 index 0000000..f3d9bf5 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py new file mode 100644 index 0000000..b6bd499 --- /dev/null +++ b/libs/CherryPy-3.2.2/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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py new file mode 100644 index 0000000..62db5ff --- /dev/null +++ b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py @@ -0,0 +1,2040 @@ +"""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_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/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg b/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..17e220a5c34bc956b83ae8a5a5c3d189e0461578 GIT binary patch literal 851999 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≻5M}%F` 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

Z*yIqON6sP97f6ro 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 z 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

wA-SfBF@e{@7DL2+L9`SWh5w+ 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≫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%0AEs_YUwIXz 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 qtM{^^nP2M>{~?DSHUEZ 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�S@y6LK$8SslsuA&l`vTEH?>#kbQsC8l%vS4v1ry)0TRhIH+ z<%6sAI5~_KM32$sV&z&uYy&l>6s*n-AMs|1q}LQ|nP}e-DBOFZJa>%=b!@WBYlO^e z-igr%M`H-ACWg^c?o_lE#a;B5iYirTOk(t7sZOQmktHn81+!EXFBmFnxy-c^TER-o z^2`(T!A;yjmuh9X67yXE5}og^m#fVnc9CUv7}fJ7Iz^Y+CSkbeXMMsNqcpq^o_Ic3 zSa{yhyw4|eU+VV6Xk(3+Owycrl!sm@H{bR%yD)j_=&4f=zv1Wub7-mg&$EdYO%t@FFkhG>E-gAwm!gb20Qy4ZdQc;3E2h7C z8bEKCiF%H0LM5FyAWFu^Y)R`dC!7=v^I##dCX=HxwrljVTylN(21-I*Dr0-?m|kb4 z*X{2tcLG%v599JW)2FY$W5pr*_?@;>9TA$|%?~}@{E+8S_4s<4uWu|4n8Ci6uaEQf ze!d=yu!e>ouN*mi;ylZHF)CQpPokDde90fvs-O+Kz$13Pf&R)5S} z{g%r@tex1`wEt)(mO8NiAs)>EQY6c3hhs-P0ATfmjEZiBJ>HgbBnNmxCFN z8o^v;o~U|Lk2id2^2n$V#q-UYkc2lX)W|SdGgYAtxfJRPOEnla=q~AnqO%cU3QoU5 zt>L~&SB5(c;PNY>z!`Ljw>rUZAZlLSD1>kP3SlkmAuv|g0)lo(#f<>1E?oqwV6?*J zmE;-oa7}hgb2)0)?%PElW8g|=7)vsf--VI^1XO3i8FjgL;}<5TirmQQ$uW80 zkIUT?W9QD>4EAAs;?mUQrL#|KOgUzs0oBK+E<7`S?$VVh!OZlwC1L#RrHP5+`1vc7 z$$}JH%GeW^ic=d(F=^bAiSeoA6KJes>B5I zyR*bX1xF##3N;aw5OGnTJhHIM){;&%)kxP?8(`}(^B3liJ_DkLtDpzqXG!6lm&5YH zayjbf+xqI-(Xl2tzL*&!M$%K`yql)NGLd~@v_tM|5kUys63%g@u@Dji!TaM`;`4Z0 zK3ggC%jt4hGfkeJo^GuMeT7u60;WD<)YGZqDx;n^IgD!kPIawIg}Z^K3A)ylTD{Mq zO&Pw?0XlX8F9py>j{_+942R#%IVq7EcFk4cC3RVz3DU+)V@1oH+il71XVhLAA!zwH4)Y5j`)3@{7dir){)IkR_OiOmC zGb?-4^g56T7DMzYMk2H+j1<+%mju`nrVh%>5aiTTsgl_P>`Y;h3&aZ{2_h@3su5<> zZ=`eM)f|;btox`Z$cu$QkTNu@Nam8ZF8?XrM3IX@d=V1z!AJZsU#}9k;l*`Y^8{qA zL=udB8O#*R6GW(^0jrk9)ww1rC~ConXR8XXb>B= zS`8KmKBl4n3MHOExrKt0ck@oc$+^95!Oc3oPOm#a_ZjDa)8q8H z{rs*#={aY=v)#3N18DpcO0zD3!l{38G9iz-A0;M5N+q6?Qb{PcQYqnUs^Z%nX z)tqx|`&`q$McY1TH@E<_ILTueikmEJG-UYjEywp+zVFn!54i6FI7j9@PGWJ>jYCr0 zY{R)IZVKo>Mw;L`p^dQI084?nKUxLXD?*47-mTdD8zd3Duj+NLS`HV2(K#%oj7srw z2Nk0g!W|_B`M7b=8Qi$&+awSC1n(v0xQk0lb7srs)oLKZ6$S<+tOm}*t1$|y^PIUD zwQpwh3aHd7ajEp*l_3wEJpHoJ!O>mh?!8_aHZ>gL8%IB0%OWLad@1ku_a;*meb-5cZ1aD=VUtb;f>= zxFt?p%A#k^x@$YtVW%#u_bvC1p$j#jc*D+b(@r2QB;gp!ak*VPYZV=IjBvu~cXm77 z(%o#jyZ>4iCe6yVyb8zks-R~po6XT~ zllva`ki)Qnr?4Zv`9j4?Uo^}z53+iN)LWedZ5J*x&FA1ALktSl)en!{sK(`*J!J{%5}oG2?d)fab-Udd6_N16%XyY8Cux~yu`co5?>@zPf&3^E?2e8t`l^mZ39R0>WyTfs@E1x|{5BUp`P zbGGhF8S3msNN&8Nsh)`Zn$VU9CzMH3PQ$l?J^{2eGjX04b3ta-t5BVIE? zXXe|2)x~lX2gqAcfe`m@K?QCFWdx2bmn*0#p%>&OBtvr|jL}3*yJiDx;ZesDNck-Y zq(!?UfS_H-m~l#^iHI&0zFxnE+@zlojiCl_xqAOBnIotadBC7mF#jo6#z&5x1j(?h zoDYwRFPpW>c$m0Q*<-6_d!>fUqU?5(Rbv>SKE1uNgjNb}zY8?|_j<63-(sQB+rAMa zg-V{e-ALVrl)>n3CS`QjiJEzqshQ~2Q!`gvsF`w#nz4nh-2GJ&G({Zq6yJr*H{sGo z@`XX}bFEZMZ;EQ!`L$9l48~s@f@S{inqXnzJ?7qZps z6+d9HZw)7pTn>%YXr@AkB2i|O8&ZMbe$TL}} zJ2WTiFGF6ek36^>dv?uF73C4wGz`3@D^9CBN|#~=q@}FZ_9%6L-NjAkCc#seUGa%r z=BCp8@oW=kwM;wGElVu`6wjBdk(>oFTjZxW=A@*5)W-FQJ@69NV}UnI-v08=N0Y2J z%r`k!FxA7IwX%s6EiZxzXMr0|7dT<*0^#Pn8h5W9 z;#b{R^@6+OZ7j^-6C8S+t_`Uy4qm!d_<26NDPSkWJsPzSrcQDAn20c0`0qjg$yA#T zQDX~^Zmg>YlLi-@xVQCeO1j^tD-aZGMj(?Of$Y)_q|oDzPE-s$TalLS)LP2Xc-8AQSBeFe&?<-V}CE zcvy9(y$Y5r^JZISk}cy62Yfb3Z-$-^pRWy#cnkGd6flkq??h|9%`Vanf+1eBSm;Hc zTdg!nJ~{r(1vygyiO{Gw_%DHpVo6z%Un3Nm628B1i4QUaTK_{_UQ{i_1gHlV?Q$$hezjh#ymoUTHzEz8}ga;fJyAiL{Mfz0RPG(!s>8D=RhwMYR;qY?J7{?5OZ% zTx|(^Ze}!O!y?Zp?~$9Sr-RrJ8MTs6^58u^Pp7D%4g<}2dSs$`N-F&N=QHSgov(Lzg{fdXprHU2_$7>_ zW>bD}!{BFAgP&~~{Hz^(&!H9H@);lspHsi`$N5y{U)eN^uV&__Q@9KQV8La#n{M7& z^Oc&j2CM&rGR#akP!g0z{yA5cZOQ$t14%rud5rA@6M<*T zHJH?}-axFiXta%p=o1L`ovr(8P%alz6uC$5r~+^&%>63#1B8)bTwAi+@wQm^g9b?! z%c_OnYUeQll#uvLU5`?U&kMecwR5+|i5aIr8?l$q)^|Jmo#cf5?;j8THygNLBYET{ zzNEo|5iG??hz%fkGou&-2qsg2z!nDOpDmBE-)1-c28q+V z+P(e3{r;_$IJvU0pX4bgqw(``hT7tG>CFXKDx6qY8T-aH- zi@55+t+;A1P%T{bHqA!QbbxRsHkyDwG0;I5IA&bC`MS-B#|uVfqQ`LI+nEJd@RtDo zn{N7yczlO;8UaSU-|*i%4gbB%^4}Q0v6^Tk83-{93oC1Io8qmX~I%Z9N8s$o;b(S_u*Z&u_6&D=KO~%}0AvqN&kH?(Zx1Ht;e3~=3 zNd|B+!Z_zK(zszQJlt&4ir1xrDZhsc+^&{-`LWle_nCBNr--RaKsVDOi*YQPJKf>^t+cT*4_x>SJp{Z7AYp7IPtEZty<@*h?yk>Zk@A`x6I zby@36G#m(=PS*|*ub*RHzX)Dmhyf9f(MJxgvB|3-EndKKo7A6oZSnbcq(0}9t(MO} z%AeEl>xBy9jHm65bh!*YVzJE)R%6fNZR}fKG)I95*+z0Xh@$dBaI}RAc<6zLtoh)T z;sqfC>a~UFDJ5#q>6cgziGa&W#8_oYbTgOBtCi*EGHeR!Hv+BeI6A>f)ek-Jpm$rM z{EJ4#)Xsjqm-<&FEM5;Es)K}4ic;r09Lwh z$*vnMbkHYMn#f_9LDZa^qrq>d22_iM=;K)QuVd^2Rg(;1$R1kcoo)x=%&fcdnHN8f z9IaFhtjC5!aSLX#1P_6|!XX!dB~jVgc!W-w-prXashdMq(U0D^FOX|(oO6j9vuHsx zYYF2vHdG@{1hTa(HpE^kM?ZA%!@LU}=jqxo3)^Tspk__`==WQZ*bN{-r5gb67N(usSCTzck|9V>oZXb zAzZE6-Gw~xel!9E6Ypk)r!G_u$22#IGZ;|o-NrG^nsaj-_3sT zirss&kPPg4K`reCQr~MkOo)AHpIYC>#9SAuLVNeCbpay>ctGjk98F;LvBvRS50_zr_SPx<@6f&%N`oQV7HoMGBaWCKAdB@Z#`^^W2Dsk3TF> z^jX@`pk*eA|C$u14+Q0mLF}=^(dgl5_>cuMOk^?u_7cNbdo*64X&8V5p7yVL1D15QVO$Ssg0uM0tL6A6N9XWn#l;cGfrmRE)LP5e< zGNO-xptQv#%8B$30LpE*0I-nC$Z++i@aiX1uM*k>wS^7t2a_9RB1h`f(Y4}d@zKvI zZC$s@|uqpPI{9gRuht*{rQYD!s3cS;#-LD$mX|?xVtC?GNA#1#GPqKJWiFa4JMSx zx)`Up@Q(BbYZt=wh8XRA8cm z!S5|LEQCDcBc`|x3rD*|6xqYJG*rfKrV3zhbd-z04qpAWInwA=D}SwOLtEwC#-|Ob zoFL108ZnoxWx+|lEg0cu2I&iIszT(q-y?AJK7Ej})Q?dKA93vc(oS3fSs!s==du7h zmsnupE-vn~&gxm*oO|IE+s|~s?-T0(9!EEDms+{$=t1cfsApP^p`n-4nu(Ui_L+|E z;Bv-XJqNqt4{DI`4NWtW2GkP6^>r5AI@2p-ZZ_Tv=#80v)ZH}G(np!#c)z;Vslvxx zVsBR-bLrcwHP$DZ90rvVJ;We~;4E~?#bbjQ=Kv=;6pRve+ zK`ML&q^VK}@|Y$S>`*cAR4~hrC?nSh3_cO9TN!r#sftBli2)qfb@*nEDt~dglY#KOvRo0Oeg$jt&Ng9y6Okb6aJWMc*1fE z>&BaYSfm9bp2e#p9x`#xrLBY`Gm&#yOUtE{+j zqa6B2nc7MG_lQ7!7UOfMMhhO5A4+wjr3)s5JW&sh9Xph?(|c>u{1-MCaG@5(1YSqH zN^D1sy<9cIfDIAhy2v|Zia4a(ZxmbRD}lVCx*Ma#CU%eKuf+qP}nwr!)!wr$(C z?WuX1ryKVn|G@boGBeK3wYEOWMt^Gpxt_$cOI1^JT@?>8Udm&ryySBV+vTg+>ETD{ zo8-glxz%t{2IsZyBu$Fo_i=P6x7sd^?W@3iC#>$T(guK829Jxh9mwmI*5H{|&>==z zCK{biikBwtro;XXQ|nt57-C3zvUvYd?M}=5et-2)ahNhq1r&@0bkJu$E-pD}9HmLC zKX6D_C}rl~b9j-@pG89vM=A@|zB7XLJHF?m zD;H&p;F!_0QnrFCY^k_yajFAtkMwMEQ*O07oNt_iL;Y@}=)`kMH_J=Qa4#t^#DF}& zvgOaZu7!vc4!hKYTZS>VdhlSfVLq6n5kn$S?v{5N69O@Ez+MDM)FIsdY-y!8xTX(N zhX{gKqx84`gx3V}=Mk@khPsL^Q^cgGo~b917oM9}w-nU)WaxZH)$&np${PO!s|Vhj zQr>9#=L^p>^zT^?ts?o7jCpfrf60dJ;?f>`Guulp(>0CTaL*?aDTqu0;ez68isWF* zEKUJDd=cX@TZqQPZ~0L`5PdR1gvCfP@bU;}v{+YJCGQALf;FMF_5ONR8HK9SR%ABq9^0nH!K3(HDh+ z*1WD9inbnCd;mtY&s=kMu&DLQf3j$OXIl`=rnCMyFBNRwQfT>1G}<(C%%IqIup+s9 zCSq32N@0y{MbjKS?`3grq)v74#K<*y+YH|^e_2E$UFvAmLJ)OM?tU}cX<1qE4n5xI z&Hi)J9<^uy_P>?9EDvCfp|;W4;nLKRWRThCy5#L3&Sa4%Bw>dzT!FRxjjfO?X-5Gf z!0j2-lFz*CBgfjzO~^s}c!MTV*V!)n|Kz`^J*=;();NxGkON^dw=6_mVV?H~U~W6o zUFsKX?JCFn3Rcff>7-58A;PwpK2i2jBgmy2B}?_KOn3pL?@9k~&>V%BU(Vcp;SV&W zE{>l>A(TW)zBb0c9;-Xun=9(2@=v&sYYPf zsSvw{Mm(43+$64e!iwDBN8NAe<)Mh+Q6R>ISqsGtV1Z4y=fC~&x4?-sgB-pr!D387 zEHV4N3*q6TRoik`Y)IV>h>tq#2efkjweV7utV@c+K1R9#ybk9%iSXT&_$QU${`c6d zFNRf!i!#J4**7qCH4oKlcHG`D!mJi35+vsj9#>0KYw_y@m;~9$AYt>GfWbh!TNFXm zTj%OrZXp(8n%@{ISwN`=c&IhUp!|uwz&R<55o)V_?0t4{`gpXpHNZvjbE<_0~)!oB=s^CxY~~10$_doulk*%%WWzeBk`o z>YObZ|8F!1+=HL3X+KB!y`gY@u?benPkKc&WyqkSV|RUm4YEtCTkKkw*`8idARjb( zhR9)*s@a_%#MB*C>t#3r$Snlpy+xALskXML@2g&0AJuQZ@1akox4lojJ-O})3uyHg z+SN33>->V}%TRqBHW}ujr#QM{dgJJzJ+(3~8YBRsn6b6O3VVE-ux4kajdfRmNLkay zD-0!eUGJOe?Pgbibh{B7(B@&)cc2dhI?o3?EJ^M^{+lb^k*g+u8;sGb+Epuc=5C_N z?VT}5sw zqHzNp*V5e8YP}YqM5QmGf+736ZV5!0CzL@+Zz%KjmEZqn%Pp}t=b&)#c-B=eS&FXF zaLYAI$uj?YBfGE$1QCkJ+=~Y~wM{-zlrJet(gvv*AkzuMf7e@6o2>=}V|_1<+TI-x zhO?<@$bL;nK{ssL+Ur}2=wjtO&u2hufeAOnJyvCaBYA9kJvp7rZLY>@etV5%wQ*9q zzZi=+E4@}zW~^v&8+Rf03e#rRguYEOT}j}eTAWN~sM^;if(j4^xAnU->P`;k3Ws|7 zYo)u>TJmPWr12SW6tSu~rIHC0(pYix8-;tNYH+0}WOg)!;%D%zkxr@_WLA=G6e%`~ zquVBCQYI&@h_7<6C1y7Oj@Me^m-KwkU&hdz&8+HR2z{e)P3VX;b5lr8StNnYgt zv_w;#)tnvdOqhA=%gct!tDxJ)WW#B8-nO1B9DEnXtP8O9uFs6FvGX4YZCGr*YD4XW z4iTS>7OXXSWPNdUX*VRV2gJEApJtQ=H5iIptJOQi(+Gw=wSh7X!C>&3`7*ec(xh}J zEzV5JgUL`nWsF)^>Q<<^P>i`qjPmTuiP%0GtkjZ@O%k&@>h0KC=I3AOSR`VG zc)T0eEfOWa8jSE69JJ5XNg4?KB3jQ%N4V9e5jJVUI|>Td5*E6V^qLP>XU-p)xXw9-$L< zu&Ja!V$$;|DquY`vVagDcbZJu6^iLZgd=?Bdd{ zoyksO4!8quAFKYenv2X9dU_hWnx}ECewzP%EmXKj1`jzf`*xx0cOF^f|zv9?u(yWSuyyD?B?RG6$6@*#H+FELanw=KKv0PXNP_0 z-S79-DD-fDHa)!n`gWR)vle_RyC0KM(0V~@n5AK{%>aVAWVJ9$)o{kyI7jITfPNh|07F-4r`CMDW)JZT=BgE4~7zIk}N)g!&G(u-A6rlR| zlR(g6vPM%GV*S8w+Hm9JH$F_|yo_DBd?xo_pvLS%zH8UjV4Fp;zpJ0->bk!8A_n1>8CE7xJ`CwPL{fma%-x8W-u!s)yKIe|l}P ze}naOqGmSF`B}mD_)!(H#P+4fA@M$vZrsJq>N{?F(&Dc|h^-!cW?E)>2PuOwi=&sk&4 zqllEnT&YN@rhi~%116GJ3n-$cre9zSR+`-Aai@tK!0Sz-AJEPGEBtD8?#uuU%xr5F zD%j)2m+iK7X$w#K7VC*gnSS@Fi_3;>JjqjJ`yMx`jww^^iWK zeI-)nVRJ+t!`WY3=BdVwDVz7)30oq8J*k_9;14$FVm6NL$$EUwisD%&4v@s0vwch4 zxjNJ4^1R-9%h2BmSi53B)>qk!Z2Un7#S;EeCRrY&c-Y;$&jRr@sFPeTP~&vc82mtT zRD`Y&UP!!^<EGjfDOM&=&rR8CkzVd&d!~Ob*A#2s0z(*ftyJm?9i$637gX3NdQ*sELJZ4)>*o zXmKQqp$0AF9Q|c85e6BKi1GR}Tu=>XCGkwZK9tUU5R)j7GMI^p5^DM7%auyGG-Du0}IhF8pAiPvv1&ow7quDMakx)21M|u+ZeghGh$k z1v7Res#Y3_+H1p*->UlX(Pbr98V`geCDJqfQjq3?(OQJ?l^a#adkBF=%(h41v&qlB zLSSkQ&Q`KxRx4eD?Nom^EtDHY6Ql~qL51gFL3P<@i9R9z*T|zA%T$P-dNKk?TPGYc ze}~2fSBuOX==@Xe-?Vl6^i89)kEg;(C^KVrP_abkZ{y83^r0IYgUFA1dMvLk?|8pb zFw%7hKlHz^G|S(Gg*V$DR@|YcF5CwqU-K1MBFsZSroR?&Nh(|AHCfe__^&-w^1G~K z$tx|sm!3XmvCz(JfV?QP!5yW0g9YcEb|nUA2~M~xN`7$w5)*5~u1K?xGlO0B7IBBU zlC58Y1MZ(?;iN{)O5X5!?2@`aT(FpBn^G$Z(AYZA{gkyRZZ-kw_mjIlF-ag0tj zXIzd{|KyUroM#Oq*^87{-VFnnOCnIN0QDX|H?$hH9$e@frkL%Ardsk4VmEQK`5q{wb)B-iHq;AL*9g{k3i_f!{f z>bh9y{Kz=#+|q44HoT#Rh27PV&y@AaF)*^G7}P5Vc}mY;^j}e3WGV*oO(=`i4=G%1 zK0cS@L*?_xs?)iqzz6B}`DAWy1>5_6hRZC8{V$v*4+hkOTnQG2H4ITd9F}66S}|Y# zXSyx3={;$V?pOp6OY2ey%WPB`Zs*C3^bx;Qot#vAvs`ZDn_62Cew!{lAtCC4a4gQE z!Cr53s7pPYlULZDjvY?=T-w}*kdSmI@&g2k3{mabMVQobbrpxL%$F=!>$mBx96 zr3zvn(fU#Wt&RMs^gm*0B)1m*uWsB4^Fi|m2TlP{Fr0U4yh!pMVfHFytlyU_73T<{ zD2Jd0&yn=Z}5%R^AF zUNxAi!}`&0@~m9?;UW0%7)W%`l_B)B5KT~(MN9AkA@J_cl1_%#ITZK?bxZ>~P($=N zDJy=9VnJ6j;8Vk6m;V@fX9rDIGk*Oeps-;*VA`+VV~eenV<`;|hO?#W?U zAuwu&mfL!4P=<>TCYAgL>W~2gz(HelS~{i>PxYWUBn7>{2wDfcGL!pPdXd0Nv)ti* znvg;Hd|u)0(zUlF0m1v`khygcvW;vMW91=6o11GyuIHC= zXI7$;zL+buoZy6Aqrd8)>sgNBq4_GqY0}D%r#`4tw4NQODEDPD_6VZUFIJZ;S=*H+ zU3cq_XPjnBo@UF1f?QM26@3raN0}~9o{@Z$2UGWsQscV&j86Vc%@lJ>RbdkUdEJ2= zpWbH_8o)GtpYq`KAQw)m^XkZvVCQ1*5&zPp&=7F7I!#FfR_d_j0`%^uSl{~t-#5v> z-)Xy5MV8hAhNnxe>pJ%AGQC~_*-yv)28Ecu+1?wCgxz;rA9+`}p#UZWg+;zdP!#en z-@L#mpbED+oO*1_kXm3D;h>vA_HaI{?5!EZlgD|NW^aRN8gLCaH%he>>NmGp{dmn-*2$twQLVeuH0^a8 zBu|3c{Ku;Vo6bX;D?hcU`48f*o?-&YEerUr3(*DZjz{m*_I5@f^Q}NiteXJTSTxzL zWKb8YO2n03iJe*)-hhaaAGIl`7K-& zVl@~^yN(|RoLaC{AF~-MOrVHZ<~MlGCo_f8g81OgI0JM9bVL6_ZjVw=*Xx2cdWmE%LMQ{#T;~PRFrQB`Js-5vg#Z z8uMA``R^ny^;tKQVOK!iJ{K+vWPWdWr!X6KATdeP!|4H&ikl&zukHR>8wmoilYv5h z%ygF>{T*aJYULV~;s$p0WBJ(<^7~gCddOsa*dT|!vtD8!8Vt70Aw2BaO&88OffJzDK;o^%BmKN0AykE;3DDJVU+wQi97GsWHkkR*0Qjf1?){b?`X?RKK^av>?FbM9X% zk%R_Kxi!Qi*BbdwnS0aj@bD2MOpEBz8fVOwK6a;;4iDk55Y}_4$e67I^QvSOQ3p|^ zp&Pe(3ni-%5F5$!y+fCJq&qm;Oq0A#m0bo zFWp3)^j7l{#Ngq!J|#O5pF@ly@*pN8bVt<_EGhwu7M@airgM{09ZWHKpY6?;y7Y%@1obc!0xK7FH=3{OJ55+Xd=A+o~CdQ z-WSMy^;kVi?8WkDT}a_ZVUA2uA^^Kjt5P>>UG(j6OQ}_qiz{yH=Yz9w*|+%*IQb9Z zf$nlosO;WIz6+fXBGeD30bJ*}U#2@zD`sYBe&4~gvbJyw$5Dok zdI8<)kNp;9%U!+Mu+NuBG&-$XzQSEr)9l>wpYV&DH9QdCj)rw2Wr-w`-5Xs>-kdHTSj1j z?x2wRn@5{Mu*HZOr-X$s7Q-Gd?&~h60kavf2)h$3)g|N^FIVK4eESg1^-hY@}-I|<18mGy2U;?@Hj#m`)SV7`A`c-O`jN=Bso>7sZN_KSyoyxVNUIb z;-G0=A*3q9E80>+ijd0TfV2t^zu`GC)p>r63kscXMyK7=Xc=ysIxwu;7Q-o*?k`7n z{>kC}ckE?#@nj}>TBnoQ$N95TZWe=i89nO-bAP$Q2a?R!x}j`D40j5%JssiedJq*z zqxLD?B6N;?Dnn37#SKPn;!i@dOc+mE(W@+Z5jH4=w^XbD?EsY7iiI)jpnMTrU_y#B z%1{k=zg};lhZCoJaPCJJx^5?83f4ecIMMdpNT)tG>nU(L*7_qNkhykE&{M{V>hV4i zUH9sJn{xahIMKGl7CbHvRA?T^kVR^&O}=~|OZu3s7Sm}0o21lk*AX02A}n@d*==u@ zV~(=hYa73y@Jq2}-yci5;_cL9G)T)5Gr`OCeI(kI_0}zR(k^Y{@Qq+J=^z_G`5Tw{ zY=e?yS-Fe5;6Y_a3AG&=fJpmZLN`MGFe&jM7ag6preAwi)0XXYTP^qcA0bz=WbT|s zCFp_Xgi1yr-QmiD@K1eB2F5{7_l;R?odMn6+!gZ-5=_kO8*M&X`D^%77L7I?vWsf>*1oP?>6AYWe|@cxYNb2@W|Q}NiKFqJxv@{64lakc-1^5 z418Ic>Ko5(j1(i5JtVpJ07%arr9rpyZ!gNw(j$H%D^o-?5IDQ*sxYk@L!_ds7KaP}C9fvevL1>;{*NIj*tVZSiGZ*IP%K z>?C@Y%tggVN4CGV4KtuV*L5=+Z*8g(XN7;c)KZIcuU+o9Xxk5xB`2PXD%CU!$c&xm zML5rHPC9_kiK{4oXypUOER4bLUiNlitEC(D!KQDa00b69BH1+?27vH~*THzk$=||0 zJAxVBWtbXj@`ro>PNxr;_BN53NqZPJk_M8`nUAFn8~C0FJ_)n}FD$Q?I+LW8PTYE& z2nsb18h8!E`5-Pluqb=I)?S5RIEI69sPeLcHyxNgtiF(K)n%_*Yb|QYYP_w>^om+d zw2(yepn*DOqT0HYphwPx9i>DYQ&o~J^uepi^()-1RA22FuIqSFcePj1PU)E2O>St* z0Hd`|JtOD^7&FXYHl;N8giL*xnX}fwd%u1y!M!}}yzYIgP3SCLJ<}rm9kVl1fUq|C zAo7$MFd+A0>UMw0k%Y6!qr1YsYgU2&?7+%ILv6IZZT;l4&|;-R{38m?O*&(Zn^>V4 zJNA`1EL6~O8alj?cjt0pf1YH2{%+yAY|Z@A-FNUpL1Z2%n6N=f{r&Z4M709s4HkFY zcbzdwlS0&H2@_%*7B5W&jq@^cvNr=d87bP~DymW)c}QDj%;2h}tChQ^QLAp>mOHyP zR#(^7p7s^+F-3AIBO$wzY|Wg7lq=gt^~a>0NpmeOTTUdZqA3NuaekucGn`2SjW+!m zrX(3bE#kpNa2RF9{${r;I^vxS2~wS|GpV?o?s`Id{8e8%i9edzm$wj%8E2SE zxj-2gE>?_84gXO#IUb%xCsM{Nlc<%4{HqbQn(Ipw+sAob-5l}pt}Y1`sIkX?^^e!3 z@x`kY;ok-%ajtEM7V37~x-`s~bE3VjTcE@NCN<}ht)pF`NZI=G+1+s89EfV>oPVNq zWRJK?ULhf!9;ed1#9*dkmGuPjCL&!_^1pz(Ddfx@f$eMm(FHsX|8@RC>CxRGVhtfd zsa}L)@g9T)eN8Zz`jwr3vcSoNV_4Eu`WVm(aLt}z z1&=VFTJ`7fHWyPUxkclJh^m*-)(>+ubG{ND5ruw(Vvba)ZzmKZi)$0Z$+VM2Aq2ld z&VvIYTvx9}$r(tXH6j%?>!|_1je;`bs8&@p@!qhJj8x0l%6(4}QkIBbW1YcmjIMkO z?`o}~3W8PXvS%9qhL4YQpPrTm}s|TciXCMn6_OpIH*7R0I5csQA(f9 zPQfm9d7Nt6Gs@%2*Wdv-!Is+&awe7)f@ba1@x)k8D+qmw z9)P@UFUVUu_emDqJB^ZVRis=WQ0@_g@IwV?cA+g89<{{9#086?u*^>bNVIjB&`BfwUE{Z&Rsl0m5Q2mFCsQpv~qa0H;Ew``d{CsSeG=znq;@mdUi+ z=FxCo815+aZ)BZD45SbaO~XC8(dCD{(mJw(KcpZj$$%wse_SgBuU3gCQE8c3XtD16 z;STl>PsvKP}sCA69qSxn$K!L1N6S;1qkOvB9CT*>D`0ztPZ!C*w**Br=7?EPoe zl6lr?^{bBP13G$&znk#U1zP(Zt|&60R6>O1y8{Y@nkiTy0)APbNtH&qwxNTujeQ#} z?yYc|cQ=Sp-;t~i=ZBNI!#{T3=O_+Zp#JK`z$-R4y7lAEJ6Gtd)1O}poIh=uKg%jG$rhtHmcGd~uEkb6W~ zavuezX_sgIg#6A8@~WLJeF0PzfQI8JsJbKUQY8)Jh%+CR>1A_F2Hy$0OP1yA0Ca^+ zU9NAb6Knfdy6d+fgcCKl%1fjV=9vB2lI;e!phGy$?ZZ#|0OQqo% z2ovIwLJJmr>fj-;PKnTAgy?n6;SGZ)LmYAxCRbSM_(yrPtw zd#@|j9rMoi=)mC<<=4RBY$e&q2v!#1(=s*lo*8=i!`c}k39Up?V}lAJ5e+zT&c(v2 z&e$5Rv?buST1IlpW~jlG;WjSy2^TpOhob{r$9Gs&fNbUm_hRL{X~tU9fLwgazu5X# z;>_-eV$aL7)DBADTVSd5Js$6kgL6FISqzk4sO!3qe%EQjC_ZcI(qrO$80VE(sv@bh zDFBj?$EdG$O4vP&9B>Y~s@hf8;7j)!Ky8t_N??P!-CCW@&gDX0=zkY><_C)%570+) zkckJpj$d{&Kd`r|_OA~wOl4DYc-+nR_Z|1Mw($-7F#S5PCgF}J2BX8y;kYzE{aGb8 zXU82^VZq(lF7vnS{Cwb%F4uFrGLyY{Oz{hiS+*#Pd0#D=fh}L!%wczkWAZrOHY#w? z>wbDTQdRUscoH}YRpr1uPzQCTBAx&TkdcA0sW?mm3$!fa!$ai>H>qHvmy_+$Ww{Q& zrq=4hl8rVk1d888+>7+oEGo$I{79<2}Zl&|X z4W{9yy>MZHn;d=`Z_Uj@@ov31yz`>ojEu;)RCIkJ0e+6yZN${WnVnn<^#hxE ztq~+(SdHUx2|b?M)zPwLvPaBouZPnA-K-i+=Dmh@P{u|6apxh;O=37hLG^uRNezDB z=91~Tv|*7ML-Gi%zwO$Xs}~THupB z@t+JF^EPvAo}upCKGK;N;%TW9=?7>f4+(d<9SV6xEc z!t<$Q_WLtrAcc&dcvFrYJNcHe^{jh{L9VX~cIpN28??g(3GZ4{dtYqXh(81Lpq6_% zqu6Q&j7J&3zpcP;*8pU=Xf7L7aUk$#KNO?W_gLT!lJD4c(>9ONUmK979y@7KtRNL2Z%%RAW@>x zn}d>!2;*^%C+R;M8_Y47)8E2cOGcYPSPNF{r27cD`qD^L7a9r*z=Z}z681(oQ2YXf zF&H%Y*ec*|iwj{}!(eW6OecW)%>(GFUe{O!{>r^w*HE}bVnbzPO!cWWhH0Tac+jx8^ zjg3SWfdt8PD_h-nlUvNy&^Ns+#eRa~AC24zrpD_(3Itn>oXX@gzC&=x4Q^a@q>f|! zqB5rtl3<*jAqPplafPu9#?N~d=l(|4;-g3RMM#z9_}5kKHQPU9(2!#?3oY}BAGnNt zw}aMc((c%?rU>D9l8y`1N?$p{;fehULDPynfRJUqXysdq;}*hK$pBN`4sk`Xe2yyh z+U52cC7k&KkeZsE!trU^`l$&$=L2JliS|=uXh&T!dmxn&YY}=tnHL%MB6i|Wpr*zO9oaS#5`=EZt* z`~kJ)lzlaW{(P8Q++o!xPH}5U-YsF8VfV;zJVoll1@?Zgud}WzPNMU;`cVA4wQ2EQ z`R4|XX6AU^_n(?j52Yls(w>2Fd&p7|wwFd%U`p=($mGvRm+=@Q8%XycRKL^@{+JXM zUa7*`bDi@5|4=pftCm{yN8Et(srxBSspGcIHL8nuK!Uhre3CWGUZ+g6yPy5!SUePf zYW&E8#XG(RNP)DvbD1+zMo=})ksaDfd~TO#@|Ra$-$mS?&PgMAjf!4c=BPD}Gd}#t zV+QcCzjd(<(e~>DI}0fJj*@Q*@i+1tm}|n|qIf!c+}mZj2w4u-4Cr^N#kkVfs2h%@ z*7asp;}8w54XHgGM7=(EeGV%LbV@4%K*J$$P}ADIwN(7sy{*Y!zKW4>)>vZ#nE&WYl;VRK@` zVv6QPS{Q8bXUQ<&HdC{$WnXrT%Mgg<`oase# z&~n_8E5GLDDWvyd_#&>@&(iAr?b^xnAp`V?zA4R80rw1afceE z0zf+VERv5zB6ZbWw@g(=@_u7kMG1D2e6W9xxs+xin80tOzO;riVhSW6Ou>=jT5-W9 zb$234oL~x>t}dZ?{<~*-FMH?VXzn^^-Ye}$xLp7Y1 zS{tL$W9!^erE<_qst1?B77CYyfU(vohKcVUvSG7!S+t#u;fz{~H-t&r{Kc5H_LDhb zEOu$ow1*rJf1vkK4YG#}in$jWFGX#(#{ppR9%BMO6?n~ojU!^E>%8qsED zHeJ!36OjQ8UN4JRJeW5B;AsGwMPkA))L_g#H*_Kx(obYYP0v+x-FD*VtxVYq zQ^j`Vm)As#ptP`AnBUYDbq!Q?jxKLM_)IDENB#4~iS5xJoA+I@aX_B%M35fDoh>S9 z=tx!J2a3`Hj>6r5h+>V8;tW9;AX4+?vI1Jq$2mqk)i>1}SyUC2+YWKSR;C!22S*ie z6RiE`c_F@wqYinQbcj2^46#76T`!4rMg$dQS6M_eJalDn<$*L$XfIDVx6)VM<&tzx zl$KAcBSc~9SR+;wpvcskd(>eL>=yejrJaNT3HDQXEf2e*qp0!3Rz4_twJgS6tkE{r zQoJ{%VA+VDAEwh@rZQ3oGTn7s<^qBO_pk>jX~j3L{d-r)K)Ie z9p4s|S}*6!O85prLnpieq=SNLDGW~rxyb(e`!0ud0(RMlaU2qu+2M*zwzB|vIeT+y z?|Jmt8eAlTt&(1Gi*M)U|2_Cy7e3o-&lyK9dJL^AM~A~_^Q@f}&xGdE-}bVF86zw8 z6Sm!Lp=^J~wLVsHQGky25RR@miIkuSmsxl-B=s)E07Jawh@zQ&;FuMtn1GGY9llx} z85{g3Z}KQg3L-oi>|6Tcx#4vB75f0#%$pGA3~WIXKW7yX0B5`7tn7wH;3LU7igiHm-C-5b|Sl~9nK1yZ)5200v^pP&sQCGoMg;=4~wNC*4K)OPMnf<2Cbj z^g0}`Lddpz;gr~uB$3mex}}1izG3RAj3C{TrOVWV`tSe(@L*Yq#kl10;Y zMMkKYz=bd_e;n~{05JH)PH*oz1{d9*_hXZnyUrrbrMfkF6Iv)V2<;%oAQ9S2{-1YV zpkTGLc@A~i9-=KO-NbT4G=cadFAO^V7hE(Q;8JEf>T%{Hd*Qh|8@ zYO)WheH8je;zbOczn6k!52M3KZ;`zSG*+HKxnhS>VVNN8fIp}LWx7`cxBBTJE;(gZ)EYdYmIi(s{GVyE1DwkR!fVr9G`Dqv8rKNR$d0|}BVumX zD@N}Bdo!AAzdHs2t9B2vf+ob zq#ke??Xs9YCaM?q1$_FK2D*$usniIJTl~OzXVdI*dEXw+LT#?=&{){)pZTg+024!m*CTq(Ri9!`jkr8_K^S2zr;7i^+ArsO#N+!@&^Jg;#c$Q~Z%p z>PSfrrFI&-Z}<_eZ5yMG7o0>JI%$s48or~%(48r*babj(A@!5jn}U4=q}CNh&*S9l zqGIaw8FhkFu^WGq_Y4}HRt}|f6b}>a%sh(EwRDd4f`WNs>a?tM>~8~7ABbx-i|>$E zN-h=4s=_sW?8KCXmN^9M3}GO#GSijFDHBxUm`60@b4HSh8jUarSV~|}Vr-PyD4fMV zXl^L{UzUp==PY;NdrN=6_p@gQg9=kK0;b z!L30yd*SfFc6meVH#-x@2+N$<b6f@PCvGOFoI`&Fn(b>WYh#j zgK6<6Vj;}gHfsh1f_`diuL~f*9$WS@<$h<}N+sS(?T5|~Gc2hPNd%R&1Hs!0LDz}= zGW?tcpc4?~R|Jg4TdA$!^w9drOm&-ytH3M;=4I6rmO?Pgoyea$f4R#FBQUW|)#pwY z*lAf4fb*Nenbi@JwLaWF+8>bnXW-248ir}i2b}MYr(n~H+7OLdG%abr;|oblAu|vt zwtXGvzqJcsF)R zUvI17%83PKLh@mZ*#JcLk}X$$9gwoUg?tk~>n6Len%WxHe2sr)kZ%P$R{&2SfJNRf zaGjpHKDzn+;3W@KX?8D0+Z+l4%Rf9CajKc9jm$)>e~IrlVFvexSn9UtXpKs3ZwfUr z7~>#_$4;Dc0xS+eAI2!AVWcQustTe}=RwOPs^Nt*lbaWi_QiF-X3f-tb4~0Mttj~+ zwY{)9h$Z6A!Vjm&`QQ}rnVvi$B})3(J-^h8$Pu{qk z-*Ig6v0V>OCR3*C997jcWSA<3nx8t&V)SmOu}mKXyKkrwaZ;HR6ee0k6TO{izM^Ns z463CRd!QDevFoBdh8&34^q5zz>mTSq~_^yNP3Mkh)S zDxdsm=NAWWk!Ve^te6K$BNMG?J@ypl_}^Rh`v2Wb>i15S)I#gZhpe3T9T5z-wf1Jj zSYHLDLsTq{TOk^3?sv6%MHVJ2F$ec0IEmCzdmw`ncS8qL?I)%YEJ#Z?qWuk6M7Ia5 zrkLIjxIGsJp{Cd#Jks62XKM8VOxo?J@BO16;uR4?iWDI7gSsaR_`C(q-h+I$eu zrKa6q^Ejsvczrq+vo0PqZPjNb>IGa*FpiNm$0V&Oqfo2Ipc1H--V>?*GIFnBtkVd=)E zW%n4m%O+%eyy+8d6PGZU5pMg4gAQ&A&IC#(m(mCd9>xf47+eO{%WaqHTojBGjPuz? z7zfAggO4_Mtj%)se{mH94T&yL@l z={*lZOh^y|5`LID^~%OlWql2;$y?_6Nw=esNaR;N ztF-mYQw8dx3u|L-C3?*G7+>Cx$@u-d`o-E{#!~>B0VU{K_5He#{pe%+ z-w^&=a*#8inY{lygp1!H;Q#+aFf^9muvy?kz!~vxHm5D7SSjTe@ecDq{58GEnMn$V z7jTj*pqe*5SzVD})vRsBWyJ20y#b^-!yxuQ}Avo)YHSh2P&jV_ON|PF1*}qUwJ6TueUQ{PFAag$cgNh3I zAdS%?s!!Y#sZY88ws^~3Ga8$&W2{Ojspa8Kn`|dd7<$H-39W^UL&$sQS;N~A8_efl z{2?Yfx0KGX-ij7{vS}|o$t65pJfW1?MxVF{(XCSHJEBp+_?Q`g67zhM8VY3?Vb2gv zOIz`;C9Hc_rqWIR`BKq$|G9^Oi}yWN1GkL~mF1YWYx4Th)7fZda8`(KVbemsd5eMhU{ z>5PMIHXW2~T`B)Khq^|k`07vHIr*=3_q z^G&Hyi}* zgXvEXq4=`=eYiTjVBSknr1#yZclCX@V&8ZBvw2fOS)3_css|JEF7z`s?+qVy4L)#_ z7=jG#`!0K%KOdePQy!2sRqjSh$w%4HFwv6+g`9Ms{KcuKsoR&9$z+^yZSz#8M&`Fj z&$P9UX_ztLs`tif_T0=%M>k1~GM`)Fa+-0c=H_&2wm5&1+vt1oda6aWIcrsP>n)Ur z70S8ah-E&~%~%;bV)uP&QFBnifu_NAoAfUi)!It!G|c5yUo}qFyghZ$>e-_+Zn`8G z4ccEgb7HIG?k2fn`i%J^7i|)bR^2qtubZ~eX2M3T$JA`6O^w40D~k4%9(Uo&mT773 znS1-;;`x)St2s@nO1k>zm{-~sXsKtt&K+-04-=HS5_Zqx){v;M5jIo$4#Y&tv6?q|d@iFbun zo+G_#%E}{WeH+m0Cg_A8F=~x&fdrH>2tm-_ztZDmOYN39m{}}EeQVeLrvWPC21T~C z>KI?RdDk{E$Ryb)fe~7oo$_46gOB*EeQ}A_^w4PFiRng1TO@Pm%g?G=-|_uJ^LYRI z|tjvbpGtOSo=H5NFnYrw8de`3n{8wZA{xt>!j4 z!>@AE(hNP9hSXzPKA8zRo7$PRGu%a#Gm1lv4>SpaG9rG~*sJa7U z8sih398`GW2k+f-m}>PS>&nXLL#dN$-Lti6pDJUy^cfsf{ZN*e7{)&ebjkk*yz(n1fypOzk;|by$R^>M0{T=ZS$jtpwC5oSBJS?O)Tu-AGh2MElXa98! zTWZf9lk=y-6x=q>W8JkHy=K+peX1iu*s82Kbz79HYUr(B7fIW0erI4~x8-|yr8C!! z+N$=|W&eHpQ)a``*|p=meA^dVF>S4%F+MfzIhyD-_hN`!&b-?cY=DSdTYW_a0z(R3 zN__)_$L3IcnL$inG(>F57EIAfpPcpzpN6+oyxylFjv&|M5Jb7}+qv`r7Kg{xrF$_0 z0zw#t&Nh3V8p&PK&FPVDNBdrg+r@uw6D1}(WtZQ0{YiTtHeay# zwbt|7-i9w~bHpB9-Ty7V_~rJ?avK4bI*JBy8X#c)0B&@<(zJco1ds>kfLgu zH2m(%oDZjcEJ`!&qj`qe?l%JbcDUZ&?4n~X;}Lp7edhq9PeT^5gMB#2tE2Z8o;G#8 z{OWV~;PP-4H(iSrKG9!o4b`V_{W7oQQUvv;SN)a>?XN$dJ#;PMe-cbO zSw8vuL%UGt`~GdTC#wr+OWP-vU(TjP4xM}1K2b`OAy)jc^FiEgrJ)iTIytrZ&YJYB zc;$|U%!ovF^-E7<^o#H6YIUBjQ`#PyyE5Rq|8Pcl4lz|lr~gi zHv4?W;VX|`8Xl5uN|CJGC)?$o+cmi2fWPUtDBfW2MLDv01}7&Ueb{knM^|Y~W{0|B zo1f$@#m)t`s-Kf{16mbFdMq=pRZgA2dQ~$if&PiLQ|gVH(}le@&mu#fq}BZLo;+W> zJghpMxh+9Oz}N9XG^jXXL!3x?#cX=zoL*?PHlc~$*lCu&>zI?$*)?P@zs0>H%HDRY-Zs~)mJ4;s zo2RVJ$Y}bpy@6H|8QdaeVhOj@U07eQa<{6>jS=^3t(h^##9C&i zp3KBIndz71r7PWZT$Wj1icCKuJ1#4I+VGt8&)MZ)cHgobT`56p`Brh}!OlqD`+Qlo zm@y|p4ern~6A$W)=UH)*c28`+Sbwi`@QB0>hhmb;LdxFM z&9A;!N=wR8Zyp$#aC2&r&6Cv+el~vk?3HLf;(5z~rcXJ;r)_yGo%c>-w8qMYbD2lV z3vKnt2ePdC3sMW=}gO7+&N?w}Q&s#84;^vcMZ}QzD zbqr+f`JfpcUb1fTkS-B>8xhfAzuT072)c~Tb2<1@36kqlNUrdI&n4ia zKmN(AoRIDmi+}g~MWr%4=yNv!0d0M{F9F{i{-x*L9@HQfo$ap!f8YtVDGB0ei>@Pq zC%lJp(L-2dZDwJ+)B>#&ctX(3Ckx z9*GXtn64cIhakxGDfqGI!00ez=|&J_=(I_F3xHO1A3=uq0zAXtgXKvgqd3kiY%F*~ zng(9RkYz#;1R39QIBrlf?UN z1d)brz0ZYj6CqV>lK->B??wsuYI^;X2%<)M$N5NVBFNSl{1io+U{L|g)igGP%r@Nd z0yiHm$VNW+DZ1(|j6o1$nD|9%H%yOMEM5q82LTeu+6KoC*f6fNZrh0bC! zV0wmwmaZxT93p_pB+;PwEz9u~BzqaC7cfUXmw?YQB?S;9)cvxXkOH6ma4uegr303L z{=!)j3G%u-qvyc*j~L)34EG=f5G4Ez;XPA_He#3zf;RyJ8SNb4)Su|a0AP--Fvq41YoUV$OV2>|g53GYO zC?+QmL`1CmG-Nq+gD&8t*r{1{n*>3jQMoKS&Ph-yd9$YlXG6*80uFX+TFUz45Ck0; z5mh?|4liwl2UR^H12I^h#1xAvn)0Ayu@_8kBe*TLh~>TM7etVBE$u+|d59o!b9}OP z?!=MkRJs?jJ3!?q2WuL-!UoAkXcf_YlI}~uw-_>@H<_Tb-3uW|amkytz7*Pnk3cQpLOra2hl#`dF{nDUhECLGFD)HM1l4S(PBt4LzGJy3diAiLLYiK7+23F;HOjx zrx3)L=DqV!gX%5?Wf^Oib?>{$|=Uj$Jd2bB=(Ee~6TLI_gEo?}ni3gdVS zFKosw6%oU_zZZ{3=C$0PjP?$AcOabJV3XYzQE~!7M1a`gPg-DBdwu*+&JG|!2+6u$ zl5IcSU<{!;_{mXF6iy+CiE=Ec{ti8ty9Hj%@ku0@KpqRy6<*Av zdBQ0KF`Y5*8>T@S@Q2)jbvV(LIE*)!?Jvw%!cC8Bg-9ZZ21Ee1n)uWD!4TxE+A`4M zJhYTd(1P_jRc{=J6TtRh`4Sc%sBN6p^1L0ua|O^JV+SS66;2_DX&&z@E(gp0${Bbu zabY-20Fx5{TPYqSA?-f;u;m4qZdWKGSTWD{2&WLl1Uav;@PYA63BrphOT}ThOfHwj z_9sibQK1EyDw7aIb1hyB`-X4|K}^N5oS-uBm>^^P>fve&9s>n{v~hSh^6A+Ih{?xV z_#9RAOBjP7a3XReMEP9ccd_N8T&CA2`J z82C}itMMAnDetQ9Id?yP6?LvBvg`AR(<``5Wky#Ca6= zLIWMvIWxiFSXWfo*$;*w$3Xq<26L!$99XttIj7^uI9v`bkjy}(8ZWM|f$@aTAy_)I zKMq0AIqq_6OQ^ld;MOp5zCi-&$E~N1@_O?YNUcD^ZWy@U>Yqdq^{9q%K$7LOcM zm?;M zdlA1LlCjX9okiF6qcWJ3H7uSNrF(~)!u6y2LNns;i8mpfO3!VAupR>q7S?G4Klk|v z9<11OY0&?A`B4LWNP@_~XU=m`$eVkhNlSc%X`3b#f{}vy{C>T#Rxzk9$HB_jI+fvor@+oPYH$+UH11NJ zvJi6KSfF52=Nnhy6oMG)T=S!I(5??Qi?Orr>4C?m0)Es^5)R>f05y)RLX__-&~g}7gxE#l)>vT-f{=}o>!0s{sgeUB zSid=Zhy+5ivxrKGtydeM3G<{$D7myW;S_?HvqeYjbHOT;pl=D(&`NRU6w z{^T0U2Z!*N1`SNuFiWh>`BrX@9PtPRUy;=bYrwmMu zoo~q-c*vjFWcuM5n)h0phC?;dKm)YLW6-o8cbAMoL6^Nk_xVPcWE-pnMd&hm9ER3d z_IDb=N(awZ3{nT&Z2~U!p>UkgJ-@g;Gkx5^6S@F59-~gG`X6uvc}m`@=I4NS0$|wi zKUPP|Bk#dt9)!9~g@x$e3Hb1jY3K(-kW==N@nkei<7X(ZSoiE~C*}0+B=+?RH1whk zz8p9JOSdiB_{L(Xs3fk!qMk+RMG<5%XCBnv08!%r>r!m|X%8i5LH9*C+D28qg4;|N zz*4(#B7T4`NA^!5h`MI}4jBfXwFELZELnOk0pCNb9~AlF1FFrY>g$24;0taL)#^Ps zG>!^%{|QKtXuD}?WeIqytS8J!rvx{O2fVCxyfH9YqJa0!AvhX=1d4Cgs2 zrueWb5|D!X*#tfsvV?sDf}zCl&v#h5b;2ev^Z?z>xbE*J_;-l*VG)F_w12T}FjN69 zDz2LZe+&3WACSX%R8O+?WLDM|Wo?KncSs)CU~hQbFNh#1s&KiwIHZjRNE_Iu$o3g2 zNyx^qJnz=j92gcG7D(8k?RecEhaiX{5tly;hV~Fb0K3=<`YaSAWRqBWh2@88sM`BL z5sa_r67ZY9{*6TtmUM6LB2`$G%78d*-WL}cNahKsrQjrqBv(kT-@mp4P(uNQ9V12b zFDQbn;LzKrcEggW2__Lcfw8crAaPK1>zG8-hEDHOuVInv10@nWfv%GMaR`DM7p}+` zhb_P)XtS|)3LZoXBEOjjO<`}o4G!yo`P&V>8`e(eecctXcOqmQteu)v zNLhX?KeDtcc`$Ur0??!YeJ<8cugCSrAqXzHoDAk>dWA>^;_(Ya2tE<;}qtZgJ~> z0um$^bUN8t`m$(`WFpP{=`(Fli&Erau;1BANcMmq>*TQ=+^1i4EmV19zJT zIRx9}mDK$oSb|)^lJXPnkVy8<#80jD8*(mec#=*n^{H2s7bNfdM)>&%Y3-jx5XEIX zSPzteW5CIHQKKZJNT*iV!CBim*Vra>tpqm1`rE3Z{gViy(lxdgy@GX8=4^aEiyTLa zA}a;xT~Wz{huX_S#nc24Hkyv=_X{FO>NVG=DUinwU{OhL>Vpw>Ka?(SL_BhGlR^A9b;|CUr+>D z?3`!v&qWZ#6KpmXWB)Z_!dXE=W>eEJ@KqEHvK4$6>m1SHf2R?|4I8H7sn#~R$I6px|&UM4e`bant0vIUvP zM;!T@q5!Bs@Oo@Cx|aU~jv()@mj3-hsF|=`f#YS~C+DHZZcHwPWQQ7^#?8$w1=--K zwh%;W7;jB~^e;Suz~$GAMuda?&VWel7H3j50Vw=_*m$SuMizhw1#Q?hj^nd_Fa$XV z7d07lS|Z3CD||Fo)ZjV)9Z)h@0W>PzN5`8(IFA&RFy=!VM4*?#^r8E0 z^3Zr%LmGpgw&xP?vvm5(6HX-9&(TELOaZ1d7G7BY5Su}Uqz7_&Y(L7Muhx)_y3w|> zNYY#cxn?VZOSLY>WIzrBp419AWa+>Ucni9$8N4hQgk1(l!Dh3h^)h{?qkFs#7N+Kl zNxxQBbJ)Gj4k)X^-LRHhypfE=3*=4JqrhRRFMAC;z!!f;henoCZms7CK>md2*X)@J zv<|=E|09wx8)GJhslfLrkQ`uQJLXvt!kGFLW-y1%Wm5c@JTEqbEJ=x`sn2x+sqbO* z*pAsF^8dgRHOZNj()6i+6PLiJ}*Kr~sN&-nUeavO*h1L#By zl(zGqp#-VR#>Yei!SJOa4`QVr*dvT;NTI{`@_d=Wl;5{*U{@lj3$)}(I3@v92J!_~ zs&ni=LkUuKe>@Yr2v&uq0&ccuBnYEUgZv)=->hPw%V5~bCi8;g#M$Y_pwC20q0Q-Vq2eZ`w151#5 zc4y_2Ja8~~SbVEt9IP%~7;2^_QUt7AC*AoN1%B_Glfs1DUxvE+{^b13t$;ZRFYH1}wt`Go9~5DXMy)p&#=~SpgW<*l zulo}4@AqY)%gsOcbLw)rz7$#@%a_N(9}A$`EQ;f;J76M4!-g6*uG1g)(I%8f5P1B9 zf08kX+pK|)>sQZ&134jVcp4Q*B}r+io6nEv1Xm6LeE{vb1bj-vzYz%{M>y?Qp6i4l z_uNEqR!sgzj_fr6{(e-{itE=5IHgKK5Wa*6&WfY#2KDw_y%62rA5k7bApX&M-GdhJ z*PSK@_HmlN9U-a?|A@F=m`L_tVHjy-4;1UAje`Tio+m$%Jzfki4F7boUKFJ-s_(PL z`Xb04HP(yR*B8= (3,) + if python3: + exclude_pattern = re.compile('wsgiserver2|ssl_pyopenssl') + else: + exclude_pattern = re.compile('wsgiserver3') + if exclude_pattern.match(module): + return # skip it + return build_py.build_module(self, module, module_file, package) + + +############################################################################### +# arguments for the setup command +############################################################################### +name = "CherryPy" +version = "3.2.2" +desc = "Object-Oriented HTTP framework" +long_desc = "CherryPy is a pythonic, object-oriented HTTP framework" +classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: Freely Distributable", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", + "Topic :: Software Development :: Libraries :: Application Frameworks", +] +author="CherryPy Team" +author_email="team@cherrypy.org" +url="http://www.cherrypy.org" +cp_license="BSD" +packages=[ + "cherrypy", "cherrypy.lib", + "cherrypy.tutorial", "cherrypy.test", + "cherrypy.process", + "cherrypy.scaffold", + "cherrypy.wsgiserver", +] +download_url="http://download.cherrypy.org/cherrypy/3.2.2/" +data_files=[ + ('cherrypy', ['cherrypy/cherryd', + 'cherrypy/favicon.ico', + 'cherrypy/LICENSE.txt', + ]), + ('cherrypy/process', []), + ('cherrypy/scaffold', ['cherrypy/scaffold/example.conf', + 'cherrypy/scaffold/site.conf', + ]), + ('cherrypy/scaffold/static', ['cherrypy/scaffold/static/made_with_cherrypy_small.png', + ]), + ('cherrypy/test', ['cherrypy/test/style.css', + 'cherrypy/test/test.pem', + ]), + ('cherrypy/test/static', ['cherrypy/test/static/index.html', + 'cherrypy/test/static/dirback.jpg',]), + ('cherrypy/tutorial', + [ + 'cherrypy/tutorial/tutorial.conf', + 'cherrypy/tutorial/README.txt', + 'cherrypy/tutorial/pdf_file.pdf', + 'cherrypy/tutorial/custom_error.html', + ] + ), +] +scripts = ["cherrypy/cherryd"] + +cmd_class = dict( + build_py = cherrypy_build_py, +) + +if sys.version_info >= (3, 0): + required_python_version = '3.0' +else: + required_python_version = '2.3' + +############################################################################### +# end arguments for setup +############################################################################### + +# wininst may install data_files in Python/x.y instead of the cherrypy package. +# Django's solution is at http://code.djangoproject.com/changeset/8313 +# See also http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html +if 'bdist_wininst' in sys.argv or '--format=wininst' in sys.argv: + data_files = [(r'\PURELIB\%s' % path, files) for path, files in data_files] + +def main(): + if sys.version < required_python_version: + s = "I'm sorry, but %s %s requires Python %s or later." + print(s % (name, version, required_python_version)) + sys.exit(1) + # set default location for "data_files" to + # platform specific "site-packages" location + for scheme in list(INSTALL_SCHEMES.values()): + scheme['data'] = scheme['purelib'] + + dist = setup( + name=name, + version=version, + description=desc, + long_description=long_desc, + classifiers=classifiers, + author=author, + author_email=author_email, + url=url, + license=cp_license, + packages=packages, + download_url=download_url, + data_files=data_files, + scripts=scripts, + cmdclass=cmd_class, + ) + + +if __name__ == "__main__": + main() diff --git a/pronterface.py b/pronterface.py index 08e5510..72dfd94 100755 --- a/pronterface.py +++ b/pronterface.py @@ -51,6 +51,9 @@ from zbuttons import ZButtons from graph import Graph import pronsole +import cherrypy, webinterface +from threading import Thread + def dosify(name): return os.path.split(name)[1].split(".")[0][:8]+".g" @@ -153,6 +156,8 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.cur_button=None self.hsetpoint=0.0 self.bsetpoint=0.0 + self.webThread = Thread(target=webinterface.StartWebInterfaceThread, args=(self, )) + self.webThread.start() def startcb(self): self.starttime=time.time() diff --git a/webinterface.py b/webinterface.py new file mode 100644 index 0000000..a462e2f --- /dev/null +++ b/webinterface.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +import cherrypy, pronterface + +def PrintHeader(): + return "

main | settings

" + +pronterPtr = 0 +class SettingsPage(object): + def __init__(self): + self.name="

Pronterface Settings

" + def SetPface(self, pface): + self.pface = pface + def index(self): + pageText=self.name+PrintHeader() + pageText=pageText+"" + pageText=pageText+"" + pageText=pageText+"" + pageText=pageText+"" + pageText=pageText+"" + pageText=pageText+"" + return pageText + index.exposed = True + +class WebInterface(object): + def __init__(self, pface): + self.pface = pface + self.name="

Pronterface Settings

" + global pronterPtr + pronterPtr = self.pface + + settings = SettingsPage() + + def index(self): + pageText=self.name+PrintHeader() + return pageText + index.exposed = True + +class WebInterfaceStub(object): + def index(self): + return "Web Interface Must be launched by running Pronterface!" + index.exposed = True + +def StartWebInterfaceThread(pface): + cherrypy.config.update({'engine.autoreload_on':False}) + cherrypy.config.update("http.config") + cherrypy.quickstart(WebInterface(pface)) + +if __name__ == '__main__': + cherrypy.config.update("http.config") + cherrypy.quickstart(WebInterfaceStub()) \ No newline at end of file From c2e68056acda79f327f13939ce3648ca9e488aef Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 30 May 2012 17:04:10 -0500 Subject: [PATCH 03/19] Added Viewing of Console to Web Interface --- pronterface.py | 41 +++++++++++++++++++++++------------------ webinterface.py | 17 ++++++++++++++--- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/pronterface.py b/pronterface.py index 72dfd94..4c347a3 100755 --- a/pronterface.py +++ b/pronterface.py @@ -156,8 +156,10 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.cur_button=None self.hsetpoint=0.0 self.bsetpoint=0.0 - self.webThread = Thread(target=webinterface.StartWebInterfaceThread, args=(self, )) + self.webInterface=webinterface.WebInterface(self) + self.webThread = Thread(target=webinterface.StartWebInterfaceThread, args=(self.webInterface, )) self.webThread.start() + self.webInterface.AddLog("Connected!!!") def startcb(self): self.starttime=time.time() @@ -280,7 +282,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): else: print _("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.") except Exception,x: - print _("You must enter a temperature. (%s)" % (repr(x),)) + print _("You must enter a temperature. (%s)" % (repr(x),)); self.webInterface.AddLog("You must enter a temperature. (%s)" % (repr(x),)) def do_bedtemp(self,l=""): try: @@ -313,11 +315,11 @@ class PronterWindow(wx.Frame,pronsole.pronsole): wx.CallAfter(self.btemp.SetBackgroundColour,"white") wx.CallAfter(self.btemp.Refresh) else: - print _("Printer is not online.") + print _("Printer is not online."); self.webInterface.AddLog("Printer is not online.") else: - print _("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") + print _("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."); self.webInterface.AddLog("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") except: - print _("You must enter a temperature.") + print _("You must enter a temperature."); self.webInterface.AddLog("You must enter a temperature.") def end_macro(self): pronsole.pronsole.end_macro(self) @@ -336,7 +338,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): if dialog.ShowModal()==wx.ID_YES: self.delete_macro(macro_name) return - print _("Cancelled.") + print _("Cancelled."); self.webInterface.AddLog("Cancelled.") return self.cur_macro_name = macro_name self.cur_macro_def = definition @@ -355,6 +357,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.capture_skip_newline = True return wx.CallAfter(self.logbox.AppendText,l) + self.webInterface.AppendLog(l) def scanserial(self): """scan for available ports. return a list of device names.""" @@ -375,7 +378,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): if(self.p.online): projectlayer.setframe(self,self.p).Show() else: - print _("Printer is not online.") + print _("Printer is not online."); self.webInterface.AddLog("Printer is not online.") def popmenu(self): self.menustrip = wx.MenuBar() @@ -449,7 +452,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): print _("Name '%s' is being used by built-in command") % macro return elif len([c for c in macro if not c.isalnum() and c != "_"]): - print _("Macro name may contain only alphanumeric symbols and underscores") + print _("Macro name may contain only alphanumeric symbols and underscores"); self.webInterface.AddLog("Macro name may contain only alphanumeric symbols and underscores") return else: old_def = "" @@ -866,7 +869,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.topsizer.Layout() def help_button(self): - print _('Defines custom button. Usage: button "title" [/c "colour"] command') + print _('Defines custom button. Usage: button "title" [/c "colour"] command'); self.webInterface.AddLog('Defines custom button. Usage: button "title" [/c "colour"] command') def do_button(self,argstr): def nextarg(rest): @@ -888,7 +891,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): pass command=argstr.strip() if num<0 or num>=64: - print _("Custom button number should be between 0 and 63") + print _("Custom button number should be between 0 and 63"); self.webInterface.AddLog("Custom button number should be between 0 and 63") return while num >= len(self.custombuttons): self.custombuttons+=[None] @@ -1142,7 +1145,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.onecmd(e.GetEventObject().properties[1]) self.cur_button=None except: - print _("event object missing") + print _("event object missing"); self.webInterface.AddLog("event object missing") self.cur_button=None raise @@ -1170,12 +1173,12 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.monitor_interval=float(l) wx.CallAfter(self.monitorbox.SetValue,self.monitor_interval>0) except: - print _("Invalid period given.") + print _("Invalid period given."); self.webInterface.AddLog("Invalid period given.") self.setmonitor(None) if self.monitor: - print _("Monitoring printer.") + print _("Monitoring printer."); self.webInterface.AddLog("Monitoring printer.") else: - print _("Done monitoring.") + print _("Done monitoring."); self.webInterface.AddLog("Done monitoring.") def setmonitor(self,e): @@ -1192,6 +1195,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): if not len(command): return wx.CallAfter(self.logbox.AppendText,">>>"+command+"\n") + self.webInterface.AppendLog(">>>"+command+"\n") self.onecmd(str(command)) self.commandbox.SetSelection(0,len(command)) @@ -1353,7 +1357,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): try: import shlex param = self.expandcommand(self.settings.slicecommand).encode() - print "Slicing: ",param + print "Slicing: ",param; self.webInterface.AddLog("Slicing: "+param) pararray=[i.replace("$s",self.filename).replace("$o",self.filename.replace(".stl","_export.gcode").replace(".STL","_export.gcode")).encode() for i in shlex.split(param.replace("\\","\\\\").encode())] #print pararray self.skeinp=subprocess.Popen(pararray,stderr=subprocess.STDOUT,stdout=subprocess.PIPE) @@ -1364,7 +1368,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.skeinp.wait() self.stopsf=1 except: - print _("Failed to execute slicing software: ") + print _("Failed to execute slicing software: "); self.webInterface.AddLog("Failed to execute slicing software: ") self.stopsf=1 traceback.print_exc(file=sys.stdout) @@ -1451,7 +1455,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): def loadviz(self): Xtot,Ytot,Ztot,Xmin,Xmax,Ymin,Ymax,Zmin,Zmax = pronsole.measurements(self.f) print pronsole.totalelength(self.f), _("mm of filament used in this print\n") - print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) + print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot); self.webInterface.AddLog("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) print _("the print goes from %f mm to %f mm in Y\nand is %f mm wide\n") % (Ymin, Ymax, Ytot) print _("the print goes from %f mm to %f mm in Z\nand is %f mm high\n") % (Zmin, Zmax, Ztot) print _("Estimated duration (pessimistic): "), pronsole.estimate_duration(self.f) @@ -1548,7 +1552,8 @@ class PronterWindow(wx.Frame,pronsole.pronsole): pass def connect(self,event): - print _("Connecting...") + print _("Connecting..."); + port=None try: port=self.scanserial()[0] diff --git a/webinterface.py b/webinterface.py index a462e2f..2f9e7a1 100644 --- a/webinterface.py +++ b/webinterface.py @@ -2,7 +2,10 @@ import cherrypy, pronterface def PrintHeader(): - return "

main | settings

" + return "

main | settings

" + +def PrintFooter(): + return "" pronterPtr = 0 class SettingsPage(object): @@ -24,6 +27,7 @@ class SettingsPage(object): class WebInterface(object): def __init__(self, pface): self.pface = pface + self.weblog="Connecting web interface to pronterface..." self.name="

Pronterface Settings

" global pronterPtr pronterPtr = self.pface @@ -32,7 +36,14 @@ class WebInterface(object): def index(self): pageText=self.name+PrintHeader() + pageText=pageText+"" + pageText=pageText+PrintFooter() return pageText + + def AddLog(self, log): + self.weblog=self.weblog+"\n"+log + def AppendLog(self, log): + self.weblog=self.weblog+log index.exposed = True class WebInterfaceStub(object): @@ -40,10 +51,10 @@ class WebInterfaceStub(object): return "Web Interface Must be launched by running Pronterface!" index.exposed = True -def StartWebInterfaceThread(pface): +def StartWebInterfaceThread(webInterface): cherrypy.config.update({'engine.autoreload_on':False}) cherrypy.config.update("http.config") - cherrypy.quickstart(WebInterface(pface)) + cherrypy.quickstart(webInterface) if __name__ == '__main__': cherrypy.config.update("http.config") From 0e647da104a9a3e25c823aa6b541ca49d9beb166 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 11:14:47 -0500 Subject: [PATCH 04/19] Updates for web itnerface, add actions, and custom styling using css/style.css --- css/style.css | 108 ++++++++++++++++++++++++++++++++++ http.config | 4 ++ webinterface.py | 152 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 236 insertions(+), 28 deletions(-) create mode 100644 css/style.css diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..9ebf453 --- /dev/null +++ b/css/style.css @@ -0,0 +1,108 @@ +#title +{ + text-align:center; + color:red; +} + +#mainmenu +{ +margin: 0; +padding: 0 0 20px 10px; +border-bottom: 1px solid #000; +} +#mainmenu ul, #mainmenu li +{ +margin: 0; +padding: 0; +display: inline; +list-style-type: none; +} + +#mainmenu a:link, #mainmenu a:visited +{ +float: left; +line-height: 14px; +font-weight: bold; +margin: 0 10px 4px 10px; +text-decoration: none; +color: #999; +} + +#mainmenu a:link#current, #mainmenu a:visited#current, #mainmenu a:hover +{ +border-bottom: 4px solid #000; +padding-bottom: 2px; +background: transparent; +color: #000; +} + +#mainmenu a:hover { color: #000; } + +#controls +{ + +} + +#controls ul +{ +list-style: none; +margin: 0px; +padding: 0px; +border: none; +} + +#controls ul li +{ +margin: 0px; +padding: 0px; +} + +#controls ul li a +{ +font-size: 80%; +display: block; +border-bottom: 1px dashed #C39C4E; +padding: 5px 0px 2px 4px; +text-decoration: none; +color: #666666; +width:160px; +} + +#controls ul li a:hover, #controls ul li a:focus +{ +color: #000000; +background-color: #eeeeee; +} + +#settings +{ +margin: 0px; +padding-top: 50px; +border: none; +} + +#settings table +{ + font-family: verdana,arial,sans-serif; + font-size:11px; + color:#333333; + border-width: 1px; + border-color: #999999; + border-collapse: collapse; +} +#settings table th { + background-color:#c3dde0; + border-width: 1px; + padding: 8px; + border-style: solid; + border-color: #a9c6c9; +} +#settings table tr { + background-color:#d4e3e5; +} +#settings table td { + border-width: 1px; + padding: 8px; + border-style: solid; + border-color: #a9c6c9; +} \ No newline at end of file diff --git a/http.config b/http.config index 1dbdefc..598483d 100644 --- a/http.config +++ b/http.config @@ -1,3 +1,7 @@ [global] server.socket_host: "localhost" server.socket_port: 8080 + +[/css/style.css] +tools.staticfile.on = True +tools.staticfile.filename = "C:\Printrun-web\Printrun\css\style.css" diff --git a/webinterface.py b/webinterface.py index 2f9e7a1..92e7543 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1,49 +1,140 @@ #!/usr/bin/python -import cherrypy, pronterface +import cherrypy, pronterface, re +import os.path def PrintHeader(): - return "

main | settings

" + return '\n\nPronterface-Web\n\n\n\n' +def PrintMenu(): + return '' + def PrintFooter(): return "" -pronterPtr = 0 +def ReloadPage(action): + return ""+action+"" + +gPronterPtr = 0 +gWeblog = "" +gLogRefresh =5 class SettingsPage(object): def __init__(self): - self.name="

Pronterface Settings

" - def SetPface(self, pface): - self.pface = pface + self.name="
Pronterface Settings
" + def index(self): - pageText=self.name+PrintHeader() - pageText=pageText+"
Build Dimenstions"+str(pronterPtr.settings.build_dimensions)+"
Last Bed Temp"+str(pronterPtr.settings.last_bed_temperature)+"
Last File Path"+pronterPtr.settings.last_file_path+"
Last Temperature"+str(pronterPtr.settings.last_temperature)+"
Preview Extrusion Width"+str(pronterPtr.settings.preview_extrusion_width)+"
Filename"+str(pronterPtr.filename)+"
" - pageText=pageText+"" - pageText=pageText+"" - pageText=pageText+"" - pageText=pageText+"" - pageText=pageText+"" + pageText=PrintHeader()+self.name+PrintMenu() + pageText=pageText+"
Build Dimenstions"+str(pronterPtr.settings.build_dimensions)+"
Last Bed Temp"+str(pronterPtr.settings.last_bed_temperature)+"
Last File Path"+pronterPtr.settings.last_file_path+"
Last Temperature"+str(pronterPtr.settings.last_temperature)+"
Preview Extrusion Width"+str(pronterPtr.settings.preview_extrusion_width)+"
Filename"+str(pronterPtr.filename)+"
\n" + pageText=pageText+"\n \n" + pageText=pageText+" \n \n" + pageText=pageText+" \n \n" + pageText=pageText+" \n \n" + pageText=pageText+" \n \n" + pageText=pageText+" \n " + pageText=pageText+PrintFooter() return pageText index.exposed = True - -class WebInterface(object): - def __init__(self, pface): - self.pface = pface - self.weblog="Connecting web interface to pronterface..." - self.name="

Pronterface Settings

" - global pronterPtr - pronterPtr = self.pface - settings = SettingsPage() +class LogPage(object): + def __init__(self): + self.name="
Pronterface Console
" def index(self): - pageText=self.name+PrintHeader() - pageText=pageText+"" - pageText=pageText+PrintFooter() + pageText="" + pageText+="
" + pageText+=gPronterPtr.status.GetStatusText() + pageText+="
" + pageText=pageText+"
"+gWeblog+"
" + pageText=pageText+"" + return pageText + index.exposed = True + +class ConsolePage(object): + def __init__(self): + self.name="
Pronterface Settings
" + + def index(self): + pageText=PrintHeader()+self.name+PrintMenu() + pageText+="
" + pageText+=PrintFooter() + return pageText + index.exposed = True + +class ConnectButton(object): + def index(self): + #handle connect push, then reload page + gPronterPtr.connect(0) + return ReloadPage("Connect...") + index.exposed = True + +class DisconnectButton(object): + def index(self): + #handle connect push, then reload page + gPronterPtr.disconnect(0) + return ReloadPage("Disconnect...") + index.exposed = True + +class ResetButton(object): + def index(self): + #handle connect push, then reload page + gPronterPtr.reset(0) + return ReloadPage("Reset...") + index.exposed = True + +class PrintButton(object): + def index(self): + #handle connect push, then reload page + gPronterPtr.printfile(0) + return ReloadPage("Print...") + index.exposed = True + +class PauseButton(object): + def index(self): + #handle connect push, then reload page + gPronterPtr.pause(0) + return ReloadPage("Pause...") + index.exposed = True + +class WebInterface(object): + + def __init__(self, pface): + self.pface = pface + global gPronterPtr + global gWeblog + self.name="
Pronterface Web-Interface
" + gWeblog = "Connecting web interface to pronterface..." + gPronterPtr = self.pface + + settings = SettingsPage() + logpage = LogPage() + console = ConsolePage() + + #actions + connect = ConnectButton() + disconnect = DisconnectButton() + reset = ResetButton() + printbutton = PrintButton() + pausebutton = PrintButton() + + def index(self): + pageText=PrintHeader()+self.name+PrintMenu() + pageText+="
" + pageText+="" + pageText+="
" + pageText=pageText+"
File Loaded: "+str(gPronterPtr.filename)+"
" + pageText+="
" + pageText+=PrintFooter() return pageText def AddLog(self, log): - self.weblog=self.weblog+"\n"+log + global gWeblog + gWeblog=gWeblog+"
"+log def AppendLog(self, log): - self.weblog=self.weblog+log + global gWeblog + gWeblog=re.sub("\n", "
", gWeblog)+log index.exposed = True class WebInterfaceStub(object): @@ -52,9 +143,14 @@ class WebInterfaceStub(object): index.exposed = True def StartWebInterfaceThread(webInterface): + current_dir = os.path.dirname(os.path.abspath(__file__)) cherrypy.config.update({'engine.autoreload_on':False}) cherrypy.config.update("http.config") - cherrypy.quickstart(webInterface) + conf = {'/css/style.css': {'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(current_dir, 'css/style.css'), + }} + cherrypy.config.update("http.config") + cherrypy.quickstart(webInterface, '/', config=conf) if __name__ == '__main__': cherrypy.config.update("http.config") From fb42d91156f2ead2a0d1708e26fc801c0ab06ff6 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 11:39:18 -0500 Subject: [PATCH 05/19] Added xml status --- webinterface.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 92e7543..788f82c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6,7 +6,7 @@ def PrintHeader(): return '\n\nPronterface-Web\n\n\n\n' def PrintMenu(): - return '' + return '' def PrintFooter(): return "" @@ -93,7 +93,13 @@ class PauseButton(object): gPronterPtr.pause(0) return ReloadPage("Pause...") index.exposed = True - + +class XMLstatus(object): + def index(self): + #handle connect push, then reload page + return '\n\n '+gPronterPtr.status.GetStatusText()+'\n'; + index.exposed = True + class WebInterface(object): def __init__(self, pface): @@ -114,6 +120,7 @@ class WebInterface(object): reset = ResetButton() printbutton = PrintButton() pausebutton = PrintButton() + status = XMLstatus() def index(self): pageText=PrintHeader()+self.name+PrintMenu() From 471e500d24e36a2d54aca82b5b31d88759be02e9 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 11:42:34 -0500 Subject: [PATCH 06/19] Add styling place holders for style gurus to go to down... --- css/style.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/css/style.css b/css/style.css index 9ebf453..9b199cf 100644 --- a/css/style.css +++ b/css/style.css @@ -105,4 +105,16 @@ border: none; padding: 8px; border-style: solid; border-color: #a9c6c9; +} + +#status{ + +} + +#console{ + +} + +#logframe{ + } \ No newline at end of file From 376bb068cd445d7a327e8be3141abb15bba0e720 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 11:45:53 -0500 Subject: [PATCH 07/19] Update Readme for Web-Interface --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index fac0732..27311bb 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,19 @@ Printrun consists of printcore, pronsole and pronterface, and a small collection * pronsole.py is an interactive command-line host software with tabcompletion goodness * pronterface.py is a graphical host software with the same functionality as pronsole +# Modifications by Beardface (Webinterface) + +## Webinterface Dependencies + +Cherrypy is required for the web interface. In my branch it is available in the libs folder, install by opening a +command prompt there and running python setup.py install. You can also download and install from cherrypy. + +## Webinterface Configuration + * The Web interface port / ip is configurable in http.config + +## Webinterface Styling + * css/style.css can be modified to change the style of the Web Interface. + # INSTALLING DEPENDENCIES ## Windows From 9692593f5ef8f36a24bff511341236b93db8be62 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 12:26:14 -0500 Subject: [PATCH 08/19] Added Authentication and updated status xml format --- README.md | 3 ++- auth.config | 3 +++ http.config | 3 --- webinterface.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 auth.config diff --git a/README.md b/README.md index 27311bb..b12875f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ command prompt there and running python setup.py install. You can also download ## Webinterface Configuration * The Web interface port / ip is configurable in http.config - + * The Default User / Password can be set in auth.config + ## Webinterface Styling * css/style.css can be modified to change the style of the Web Interface. diff --git a/auth.config b/auth.config new file mode 100644 index 0000000..17a02cf --- /dev/null +++ b/auth.config @@ -0,0 +1,3 @@ +[user] +user = admin +pass = password \ No newline at end of file diff --git a/http.config b/http.config index 598483d..b5093db 100644 --- a/http.config +++ b/http.config @@ -2,6 +2,3 @@ server.socket_host: "localhost" server.socket_port: 8080 -[/css/style.css] -tools.staticfile.on = True -tools.staticfile.filename = "C:\Printrun-web\Printrun\css\style.css" diff --git a/webinterface.py b/webinterface.py index 788f82c..758687d 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1,7 +1,9 @@ #!/usr/bin/python -import cherrypy, pronterface, re +import cherrypy, pronterface, re, ConfigParser, io import os.path +users = {} + def PrintHeader(): return '\n\nPronterface-Web\n\n\n\n' @@ -14,6 +16,9 @@ def PrintFooter(): def ReloadPage(action): return ""+action+"" +def clear_text(mypass): + return mypass + gPronterPtr = 0 gWeblog = "" gLogRefresh =5 @@ -65,13 +70,21 @@ class ConnectButton(object): gPronterPtr.connect(0) return ReloadPage("Connect...") index.exposed = True - + index._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} + class DisconnectButton(object): def index(self): #handle connect push, then reload page gPronterPtr.disconnect(0) return ReloadPage("Disconnect...") index.exposed = True + index._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} class ResetButton(object): def index(self): @@ -79,6 +92,10 @@ class ResetButton(object): gPronterPtr.reset(0) return ReloadPage("Reset...") index.exposed = True + index._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} class PrintButton(object): def index(self): @@ -86,6 +103,10 @@ class PrintButton(object): gPronterPtr.printfile(0) return ReloadPage("Print...") index.exposed = True + index._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} class PauseButton(object): def index(self): @@ -93,16 +114,49 @@ class PauseButton(object): gPronterPtr.pause(0) return ReloadPage("Pause...") index.exposed = True + index._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} class XMLstatus(object): def index(self): #handle connect push, then reload page - return '\n\n '+gPronterPtr.status.GetStatusText()+'\n'; + txt='\n\n' + txt=txt+''+str(gPronterPtr.filename)+'\n' + txt=txt+''+str(gPronterPtr.status.GetStatusText())+'\n' + try: + temp = str(float(filter(lambda x:x.startswith("T:"),gPronterPtr.tempreport.split())[0].split(":")[1])) + txt=txt+''+temp+'\n' + except: + txt=txt+'NA\n' + pass + try: + temp = str(float(filter(lambda x:x.startswith("B:"),gPronterPtr.tempreport.split())[0].split(":")[1])) + txt=txt+''+temp+'\n' + except: + txt=txt+'NA\n' + pass + if gPronterPtr.sdprinting: + fractioncomplete = float(gPronterPtr.percentdone/100.0) + txt+= _("%04.2f %%") % (gPronterPtr.percentdone,) + txt+="\n" + elif gPronterPtr.p.printing: + fractioncomplete = float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue) + txt+= _("%04.2f %% |") % (100*float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue),) + txt+="\n" + else: + txt+="NA\n" + txt+='' + return txt index.exposed = True class WebInterface(object): def __init__(self, pface): + config = ConfigParser.SafeConfigParser(allow_no_value=True) + config.read('auth.config') + users[config.get("user", "user")] = config.get("user", "pass") self.pface = pface global gPronterPtr global gWeblog From b3eab2386bea2e517c27e680e8eb4684eec88634 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 12:37:23 -0500 Subject: [PATCH 09/19] remove some excess logging --- pronterface.py | 1 - webinterface.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pronterface.py b/pronterface.py index 4c347a3..d03c241 100755 --- a/pronterface.py +++ b/pronterface.py @@ -159,7 +159,6 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.webInterface=webinterface.WebInterface(self) self.webThread = Thread(target=webinterface.StartWebInterfaceThread, args=(self.webInterface, )) self.webThread.start() - self.webInterface.AddLog("Connected!!!") def startcb(self): self.starttime=time.time() diff --git a/webinterface.py b/webinterface.py index 758687d..0d254fc 100644 --- a/webinterface.py +++ b/webinterface.py @@ -161,7 +161,7 @@ class WebInterface(object): global gPronterPtr global gWeblog self.name="
Pronterface Web-Interface
" - gWeblog = "Connecting web interface to pronterface..." + gWeblog = "" gPronterPtr = self.pface settings = SettingsPage() From 56f7fd6e2e48da372d13aa365e2f3129fca3f544 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 12:45:07 -0500 Subject: [PATCH 10/19] Added status --- webinterface.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webinterface.py b/webinterface.py index 0d254fc..86d0ead 100644 --- a/webinterface.py +++ b/webinterface.py @@ -123,6 +123,17 @@ class XMLstatus(object): def index(self): #handle connect push, then reload page txt='\n\n' + state="Offline" + if self.statuscheck or self.p.online: + state="Idle" + if self.sdprinting: + state="SDPrinting" + if self.p.printing: + state="Printing" + if self.paused: + state="Paused" + + txt=txt+''+state+'\n' txt=txt+''+str(gPronterPtr.filename)+'\n' txt=txt+''+str(gPronterPtr.status.GetStatusText())+'\n' try: From 069ee940d38164c56b21de15075f2d02ce762eca Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 12:45:53 -0500 Subject: [PATCH 11/19] Added State --- webinterface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webinterface.py b/webinterface.py index 86d0ead..4e14829 100644 --- a/webinterface.py +++ b/webinterface.py @@ -124,13 +124,13 @@ class XMLstatus(object): #handle connect push, then reload page txt='\n\n' state="Offline" - if self.statuscheck or self.p.online: + if gPronterPtr.statuscheck or gPronterPtr.p.online: state="Idle" - if self.sdprinting: + if gPronterPtr.sdprinting: state="SDPrinting" - if self.p.printing: + if gPronterPtr.p.printing: state="Printing" - if self.paused: + if gPronterPtr.paused: state="Paused" txt=txt+''+state+'\n' From f2e87c6e7c8f56c3bc932d7781999abc57b85138 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 14:49:32 -0500 Subject: [PATCH 12/19] Kill CherryPy when Pronterface closes --- pronterface.py | 2 ++ webinterface.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pronterface.py b/pronterface.py index d03c241..35d9470 100755 --- a/pronterface.py +++ b/pronterface.py @@ -1161,6 +1161,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): except: pass self.Destroy() + webinterface.KillWebInterfaceThread() def do_monitor(self,l=""): if l.strip()=="": @@ -1686,6 +1687,7 @@ class macroed(wx.Dialog): self.callback(self.e.GetValue().split("\n")) def close(self,ev): self.Destroy() + webinterface.KillWebInterfaceThread() def unindent(self,text): self.indent_chars = text[:len(text)-len(text.lstrip())] if len(self.indent_chars)==0: diff --git a/webinterface.py b/webinterface.py index 4e14829..128f326 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1,5 +1,5 @@ #!/usr/bin/python -import cherrypy, pronterface, re, ConfigParser, io +import cherrypy, pronterface, re, ConfigParser, threading import os.path users = {} @@ -214,6 +214,9 @@ class WebInterfaceStub(object): return "Web Interface Must be launched by running Pronterface!" index.exposed = True +def KillWebInterfaceThread(): + cherrypy.engine.exit() + def StartWebInterfaceThread(webInterface): current_dir = os.path.dirname(os.path.abspath(__file__)) cherrypy.config.update({'engine.autoreload_on':False}) @@ -223,7 +226,7 @@ def StartWebInterfaceThread(webInterface): }} cherrypy.config.update("http.config") cherrypy.quickstart(webInterface, '/', config=conf) - + if __name__ == '__main__': cherrypy.config.update("http.config") cherrypy.quickstart(WebInterfaceStub()) \ No newline at end of file From e2e47c494c13704e35177c72674adf205520a7bb Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 17:10:51 -0500 Subject: [PATCH 13/19] Added custom buttons, gui, cleaned up interface --- css/style.css | 39 +++++++++++++++++- pronterface.py | 3 +- webinterface.py | 102 ++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/css/style.css b/css/style.css index 9b199cf..11cb4b3 100644 --- a/css/style.css +++ b/css/style.css @@ -38,8 +38,37 @@ color: #000; #mainmenu a:hover { color: #000; } +#content{ +padding-top: 25px; +} +#controls{ + float:left; + padding:0 0 1em 0; + overflow:hidden; + width:71%; /* right column content width */ + left:102%; /* 100% plus left column left padding */ +} +#control_xy{ + display:inline; +} + +#control_z{ + display:inline; + position:absolute; +} + +#gui{ + float:left; + padding:0 0 1em 0; + overflow:hidden; + width:21%; /* left column content width (column width minus left and right padding) */ + left:6%; /* (right column left and right padding) plus (left column left padding) */ + +} #controls { + width:21%; /* Width of left column content (column width minus padding on either side) */ + left:31%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */ } @@ -115,6 +144,14 @@ border: none; } +#file{ + position:relative; + float:left; + width:100%; + height:20px; /* Height of the footer */ + background:#eee; +} + #logframe{ -} \ No newline at end of file +} diff --git a/pronterface.py b/pronterface.py index 35d9470..7b8d093 100755 --- a/pronterface.py +++ b/pronterface.py @@ -591,7 +591,7 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.zb = ZButtons(self.panel, self.moveZ) lls.Add(self.zb, pos=(2,7), span=(1,2), flag=wx.ALIGN_CENTER) wx.CallAfter(self.xyb.SetFocus) - + for i in self.cpbuttons: btn=wx.Button(self.panel,-1,i[0])#) btn.SetBackgroundColour(i[3]) @@ -748,7 +748,6 @@ class PronterWindow(wx.Frame,pronsole.pronsole): #uts.Layout() self.cbuttons_reload() - def plate(self,e): import plater print "plate function activated" diff --git a/webinterface.py b/webinterface.py index 128f326..fe43752 100644 --- a/webinterface.py +++ b/webinterface.py @@ -16,6 +16,9 @@ def PrintFooter(): def ReloadPage(action): return ""+action+"" +def TReloadPage(action): + return action + def clear_text(mypass): return mypass @@ -119,6 +122,56 @@ class PauseButton(object): 'tools.basic_auth.users': users, 'tools.basic_auth.encrypt': clear_text} +class MoveButton(object): + def axis(self, *args): + if not args: + raise cherrypy.HTTPError(400, "No Move Command Provided!") + margs=list(args) + axis = margs.pop(0) + if(margs and axis == "x"): + distance = margs.pop(0) + gPronterPtr.onecmd('move X %s' % distance) + return "Moving X Axis " + str(distance) + if(margs and axis == "y"): + distance = margs.pop(0) + gPronterPtr.onecmd('move Y %s' % distance) + return "Moving Y Axis " + str(distance) + if(margs and axis == "z"): + distance = margs.pop(0) + gPronterPtr.onecmd('move Z %s' % distance) + return "Moving Z Axis " + str(distance) + raise cherrypy.HTTPError(400, "Unmached Move Command!") + axis.exposed = True + axis._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} + +class HomeButton(object): + def axis(self, *args): + if not args: + raise cherrypy.HTTPError(400, "No Axis Provided!") + margs=list(args) + taxis = margs.pop(0) + if(taxis == "x"): + gPronterPtr.onecmd('home X') + return TReloadPage("Home X") + if(taxis == "y"): + gPronterPtr.onecmd('home Y') + return TReloadPage("Home Y") + if(taxis == "z"): + gPronterPtr.onecmd('home Z') + return TReloadPage("Home Z") + if(taxis == "all"): + gPronterPtr.onecmd('home') + return TReloadPage("Home All") + + axis.exposed = True + axis._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} + class XMLstatus(object): def index(self): #handle connect push, then reload page @@ -186,16 +239,45 @@ class WebInterface(object): printbutton = PrintButton() pausebutton = PrintButton() status = XMLstatus() + home = HomeButton() + move = MoveButton() def index(self): pageText=PrintHeader()+self.name+PrintMenu() - pageText+="
" - pageText+="" - pageText+="
" + pageText+="
\n" + pageText+="
\n" + pageText+="
  • Connect
  • \n" + pageText+="
  • Disconnect
  • \n" + pageText+="
  • Reset
  • \n" + pageText+="
  • Print
  • \n" + pageText+="
  • Pause
  • \n" + + for i in gPronterPtr.cpbuttons: + pageText+="
  • "+i[0]+"
  • \n" + + #for i in gPronterPtr.custombuttons: + # print(str(i)); + + pageText+="
\n" + pageText+="
\n" + pageText+="
\n" + pageText+="
" + pageText+="" + pageText+='' + pageText+='X Home' + pageText+='Y Home' + pageText+='Z Home' + pageText+='All Home' + #TODO Map X, Y Moves + pageText+="" + pageText+="
\n" #endxy + pageText+="
" + pageText+="" + #TODO Map Z Moves + pageText+="
\n" #endz + pageText+="
\n" #endgui + pageText+="
\n" #endcontent + pageText+="
\n" pageText=pageText+"
File Loaded: "+str(gPronterPtr.filename)+"
" pageText+="
" pageText+=PrintFooter() @@ -223,6 +305,12 @@ def StartWebInterfaceThread(webInterface): cherrypy.config.update("http.config") conf = {'/css/style.css': {'tools.staticfile.on': True, 'tools.staticfile.filename': os.path.join(current_dir, 'css/style.css'), + }, + '/images/control_xy.png': {'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(current_dir, 'images/control_xy.png'), + }, + '/images/control_z.png': {'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(current_dir, 'images/control_z.png'), }} cherrypy.config.update("http.config") cherrypy.quickstart(webInterface, '/', config=conf) From afc22d716991fe76668b2af506ff49a2673d715a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 31 May 2012 17:12:23 -0500 Subject: [PATCH 14/19] remove test interface --- webinterface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webinterface.py b/webinterface.py index fe43752..82ae919 100644 --- a/webinterface.py +++ b/webinterface.py @@ -155,16 +155,16 @@ class HomeButton(object): taxis = margs.pop(0) if(taxis == "x"): gPronterPtr.onecmd('home X') - return TReloadPage("Home X") + return ReloadPage("Home X") if(taxis == "y"): gPronterPtr.onecmd('home Y') - return TReloadPage("Home Y") + return ReloadPage("Home Y") if(taxis == "z"): gPronterPtr.onecmd('home Z') - return TReloadPage("Home Z") + return ReloadPage("Home Z") if(taxis == "all"): gPronterPtr.onecmd('home') - return TReloadPage("Home All") + return ReloadPage("Home All") axis.exposed = True axis._cp_config = {'tools.basic_auth.on': True, From 4dc43e4d8c44a6b0657d054aaf8339ccf7072fb3 Mon Sep 17 00:00:00 2001 From: blddk Date: Sun, 3 Jun 2012 01:07:28 +0300 Subject: [PATCH 15/19] Fixed format for the progress values returned for printing and printing from sd --- webinterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 82ae919..e8ebd06 100644 --- a/webinterface.py +++ b/webinterface.py @@ -203,11 +203,11 @@ class XMLstatus(object): pass if gPronterPtr.sdprinting: fractioncomplete = float(gPronterPtr.percentdone/100.0) - txt+= _("%04.2f %%") % (gPronterPtr.percentdone,) + txt+= _("%04.2f") % (gPronterPtr.percentdone,) txt+="\n" elif gPronterPtr.p.printing: fractioncomplete = float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue) - txt+= _("%04.2f %% |") % (100*float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue),) + txt+= _("%04.2f") % (100*float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue),) txt+="\n" else: txt+="NA\n" From 85d87383ec41c2cf555821f0a4d644b783e50146 Mon Sep 17 00:00:00 2001 From: Michael Andresen Date: Sun, 3 Jun 2012 14:29:53 +0200 Subject: [PATCH 16/19] PHP parser Added a little parser for the XML status returned by pronterface. --- php/parser.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 php/parser.php diff --git a/php/parser.php b/php/parser.php new file mode 100644 index 0000000..86ac16f --- /dev/null +++ b/php/parser.php @@ -0,0 +1,35 @@ +state . "
"; + echo "Hotend: " . round($xml->hotend, 0) . "°c
"; + echo "Bed: " . round($xml->bed, 0) . "°c
"; + if ($xml->progress != "NA") + { + echo "Progress: " . $xml->progress . "%"; + } +} +catch(Exception $e) +{ + echo "ERROR:\n" . $e->getMessage(). " (severity " . $e->getCode() . ")"; +} +?> \ No newline at end of file From 4158bc63ca5c7b63ff0b7fb08b9f91a169fe2f1a Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Jun 2012 13:44:49 -0500 Subject: [PATCH 17/19] Only enable webinterface if CherryPy is available. Fix buttons on the web --- .../dist/CherryPy-3.2.2-py2.7.egg | Bin 851999 -> 851999 bytes libs/CherryPy-3.2.2/files.txt | 231 ++++++++++++++++++ pronterface.py | 95 +++++-- webinterface.py | 61 ++++- 4 files changed, 351 insertions(+), 36 deletions(-) create mode 100644 libs/CherryPy-3.2.2/files.txt diff --git a/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg b/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg index 17e220a5c34bc956b83ae8a5a5c3d189e0461578..b68299a8d1bf6187f7889bf23341b02663e4517e 100644 GIT binary patch delta 2224 zcmXw&dr(w$6vyx0-M#lN_W-USC<_XPiK!q;l%u2AOomgW(C9R2iA+k4C}rwoPaqT@ zJXDY?IlAVMlD37&%$+e3g%uHX!IYGmV>!uwZ^YDV{6+XQC1om$eX~t-x_E!a%3SZxqy*ZkjnBNOJ-;M+1;- zk(#xuNK-Z1SxCW+8nY3prbpxJjWpuQZ=>jrCHz@m)OrDBZDfNT#oKH$0Kr({yRUNZ*&}!rw-!7}hyYK(f2& z+XN(=hyEs=Vza` zq!VqzKXeO`yqLo7!$Yie#b{}Os3R}s%fwcH)GmA@CeqfHWQ!Z8pk^r+m(q;hD$z`P zKGG~klB*Ye;ugBg7+&hvVMVkkt(Ki$y{nLxe#f{0iQ`h;70oqik}}i;vf49Vdf{Xo z4U5OW4Gn!2cA3C&CMEt2X`%c|O&F<7#}A#amuAsBH#EwJA`V z>5dIH{7^n~zLDd$E#f#|jW+Tso zju@>?D)4tu3;gBTA7Q7CcsWsr4)`43#jALZYu70&vSd%?%S72lwd|G)^_Uo&EdNJO z@$(GXPBxYulXbM16{qBb6m7D|J85KOzC4>u9=Is8BgB`<3Pnw~^tv5MtWo z3+r*(jOAdDL~_anFJ3!!jaBi_g9W3K5|an>MiupB9&~Du((_@m7Aao^Gfq%eU4&hj zUw;u25!M&NZiLQ47TR%%g(_m}9d-JVL}3 zh(oBn0!avKOJEN|dkG{U#9n2};8jRNCApL}G+kq%Q)3WV#*~6G*o(=b*BRWo&it&o z!5lW-K}ETo2r@TzG|i<)v%5eYgs6}mKh4DqmX*34yI6R`&~GS zt>f-N27>#2Hsj2FNJk~49W5PZCH?RR!uA399l>Xi9n3li y?Wml4!CZ$8u~63-q`zdB8aNE^;}`#F7(%hCY#2gNaU6k#2p^2VVuaKYIQT!lduJ~I delta 2224 zcmXxjdrTBp6bInh+1+_C2XF-eSx{)OVo?yKv__+7Qj2M+LZfMFTai?$5u^__wNIcZ zK6t1gBQ?6#P^DTbBFQ9{f&wC24eA|W|DZU_4mUhEIxY)qN~V$0b-MSNy!+_4m3pL@)ZYR~9%h_{2nE7mGue+i8eqz^?bstz*!z_Ki zdfM7@HpdP%W@XD64eD5_9yvL8!b2eV`fD(yz~V zKxMLqeN$0h(+nkkC|igj%NzB5l;QXkl=&OOO%GIex*fL2jGh~ENBuT~ z`;^>V7tB5L!L0vd?u8TT$u@2}5ueB%AU8Ua$<>oAE|&`>y7V`Zy>W&ui`uXF}w6FpG2P8@*IB4Ow97i_?6_0!8+bT zp7YUmK9WrJa){qX-en9c3>$DqG%u`|9Mm@*(lKlwKO!(pnyak4E=-rl+CWkUrV1|| zOj?;g@m*-BT6UVoFlIU74Z%(J@(_%Q=W9ZSBQ{tWm~}DDj$sI6d zLZOE|jloLc5qUz5Ey7t7?%2~W*gR2NP2vh~{GRg{i0X6E{xcpC;yY6uRD~pk3%{3f z3=`!ndpB{@mAz5oR}!unixqo`p6(LQ3z%6F#d15;=2Wqh{5Qh?5}io@=xQ;H!>Up% zo*~+JU;HRS%aV)y?UW*aY2Jspq9;L0GT;PkuFtY{EW>mg7kxGoX zFfK*d(g(%l-whSc~MC0$8Xe%BceA)uA#9VTK-6sDK4e zkk=K%Zd~773`vMhC9nt4TS7}aFVoViNhDo?{kX(R87+mB!vS1l zEr$d|#8rq#)Lw;T#D)ski|DR^MD0tEI+%(qEBTevth8dTrpT?PjThF?Gd)$FOz$eaJ+(KA>lueE=Dlgfv4o;(jy8h@B7V8T=zSfl2No zIE4snp}SgJ=sVb9gTq*|YlS0-Q>}0a5!^;mHHpvK=@>8C;W(~2(g8;i^B>dB6_4pW zQJr+2u})e#+yy6b@tfUXLDYBC(_@~fXQ^L#R*!mu_O;7B@GBzdsoGCVYM!bCYWSj8 zZLEQP23d$>&(s@h$=p8mC=I23>VO)eo|9e5=yO=0(;_9HANHf-eyhqiGX{VKsLO0^VOFDJf7%lZpBIA|%Qnkf_ g "title" [/c "colour"] command'); self.webInterface.AddLog('Defines custom button. Usage: button "title" [/c "colour"] command') + print _('Defines custom button. Usage: button "title" [/c "colour"] command'); + if webavail: + self.webInterface.AddLog('Defines custom button. Usage: button "title" [/c "colour"] command') def do_button(self,argstr): def nextarg(rest): @@ -889,7 +913,9 @@ class PronterWindow(wx.Frame,pronsole.pronsole): pass command=argstr.strip() if num<0 or num>=64: - print _("Custom button number should be between 0 and 63"); self.webInterface.AddLog("Custom button number should be between 0 and 63") + print _("Custom button number should be between 0 and 63"); + if webavail: + self.webInterface.AddLog("Custom button number should be between 0 and 63") return while num >= len(self.custombuttons): self.custombuttons+=[None] @@ -1143,7 +1169,9 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.onecmd(e.GetEventObject().properties[1]) self.cur_button=None except: - print _("event object missing"); self.webInterface.AddLog("event object missing") + print _("event object missing"); + if webavail: + self.webInterface.AddLog("event object missing") self.cur_button=None raise @@ -1160,7 +1188,8 @@ class PronterWindow(wx.Frame,pronsole.pronsole): except: pass self.Destroy() - webinterface.KillWebInterfaceThread() + if webavail: + webinterface.KillWebInterfaceThread() def do_monitor(self,l=""): if l.strip()=="": @@ -1172,12 +1201,18 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.monitor_interval=float(l) wx.CallAfter(self.monitorbox.SetValue,self.monitor_interval>0) except: - print _("Invalid period given."); self.webInterface.AddLog("Invalid period given.") + print _("Invalid period given."); + if webavail: + self.webInterface.AddLog("Invalid period given.") self.setmonitor(None) if self.monitor: - print _("Monitoring printer."); self.webInterface.AddLog("Monitoring printer.") + print _("Monitoring printer."); + if webavail: + self.webInterface.AddLog("Monitoring printer.") else: - print _("Done monitoring."); self.webInterface.AddLog("Done monitoring.") + print _("Done monitoring."); + if webavail: + self.webInterface.AddLog("Done monitoring.") def setmonitor(self,e): @@ -1194,7 +1229,8 @@ class PronterWindow(wx.Frame,pronsole.pronsole): if not len(command): return wx.CallAfter(self.logbox.AppendText,">>>"+command+"\n") - self.webInterface.AppendLog(">>>"+command+"\n") + if webavail: + self.webInterface.AppendLog(">>>"+command+"\n") self.onecmd(str(command)) self.commandbox.SetSelection(0,len(command)) @@ -1356,7 +1392,9 @@ class PronterWindow(wx.Frame,pronsole.pronsole): try: import shlex param = self.expandcommand(self.settings.slicecommand).encode() - print "Slicing: ",param; self.webInterface.AddLog("Slicing: "+param) + print "Slicing: ",param; + if webavail: + self.webInterface.AddLog("Slicing: "+param) pararray=[i.replace("$s",self.filename).replace("$o",self.filename.replace(".stl","_export.gcode").replace(".STL","_export.gcode")).encode() for i in shlex.split(param.replace("\\","\\\\").encode())] #print pararray self.skeinp=subprocess.Popen(pararray,stderr=subprocess.STDOUT,stdout=subprocess.PIPE) @@ -1367,7 +1405,9 @@ class PronterWindow(wx.Frame,pronsole.pronsole): self.skeinp.wait() self.stopsf=1 except: - print _("Failed to execute slicing software: "); self.webInterface.AddLog("Failed to execute slicing software: ") + print _("Failed to execute slicing software: "); + if webavail: + self.webInterface.AddLog("Failed to execute slicing software: ") self.stopsf=1 traceback.print_exc(file=sys.stdout) @@ -1454,7 +1494,9 @@ class PronterWindow(wx.Frame,pronsole.pronsole): def loadviz(self): Xtot,Ytot,Ztot,Xmin,Xmax,Ymin,Ymax,Zmin,Zmax = pronsole.measurements(self.f) print pronsole.totalelength(self.f), _("mm of filament used in this print\n") - print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot); self.webInterface.AddLog("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) + print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot); + if webavail: + self.webInterface.AddLog("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) print _("the print goes from %f mm to %f mm in Y\nand is %f mm wide\n") % (Ymin, Ymax, Ytot) print _("the print goes from %f mm to %f mm in Z\nand is %f mm high\n") % (Zmin, Zmax, Ztot) print _("Estimated duration (pessimistic): "), pronsole.estimate_duration(self.f) @@ -1686,7 +1728,8 @@ class macroed(wx.Dialog): self.callback(self.e.GetValue().split("\n")) def close(self,ev): self.Destroy() - webinterface.KillWebInterfaceThread() + if webavail: + webinterface.KillWebInterfaceThread() def unindent(self,text): self.indent_chars = text[:len(text)-len(text.lstrip())] if len(self.indent_chars)==0: diff --git a/webinterface.py b/webinterface.py index e8ebd06..b959807 100644 --- a/webinterface.py +++ b/webinterface.py @@ -131,22 +131,37 @@ class MoveButton(object): if(margs and axis == "x"): distance = margs.pop(0) gPronterPtr.onecmd('move X %s' % distance) - return "Moving X Axis " + str(distance) + return ReloadPage("Moving X Axis " + str(distance)) if(margs and axis == "y"): distance = margs.pop(0) gPronterPtr.onecmd('move Y %s' % distance) - return "Moving Y Axis " + str(distance) + return ReloadPage("Moving Y Axis " + str(distance)) if(margs and axis == "z"): distance = margs.pop(0) gPronterPtr.onecmd('move Z %s' % distance) - return "Moving Z Axis " + str(distance) + return ReloadPage("Moving Z Axis " + str(distance)) raise cherrypy.HTTPError(400, "Unmached Move Command!") axis.exposed = True axis._cp_config = {'tools.basic_auth.on': True, 'tools.basic_auth.realm': 'My Print Server', 'tools.basic_auth.users': users, 'tools.basic_auth.encrypt': clear_text} - + +class CustomButton(object): + def button(self, *args): + if not args: + raise cherrypy.HTTPError(400, "No Custom Command Provided!") + margs=list(args) + command = margs.pop(0) + if(command): + gPronterPtr.onecmd(command) + return ReloadPage(str(command)) + button.exposed = True + button._cp_config = {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'My Print Server', + 'tools.basic_auth.users': users, + 'tools.basic_auth.encrypt': clear_text} + class HomeButton(object): def axis(self, *args): if not args: @@ -241,6 +256,7 @@ class WebInterface(object): status = XMLstatus() home = HomeButton() move = MoveButton() + custom =CustomButton() def index(self): pageText=PrintHeader()+self.name+PrintMenu() @@ -264,15 +280,40 @@ class WebInterface(object): pageText+="
" pageText+="" pageText+='' - pageText+='X Home' - pageText+='Y Home' - pageText+='Z Home' - pageText+='All Home' - #TODO Map X, Y Moves + + pageText+='X Home' + pageText+='Y Home' + pageText+='All Home' + pageText+='Z Home' + pageText+='Y 100' + pageText+='Y 10' + pageText+='Y 1' + pageText+='Y .1' + pageText+='Y -.1' + pageText+='Y -1' + pageText+='Y -10' + pageText+='Y -100' + pageText+='X -100' + pageText+='X 100' + pageText+='X -10' + pageText+='X 10' + pageText+='X -1' + pageText+='X 1' + pageText+='X -.1' + pageText+='X .1' + pageText+="" pageText+="
\n" #endxy pageText+="
" - pageText+="" + pageText+="" + pageText+='' + pageText+='Z 10' + pageText+='Z 1' + pageText+='Z .1' + pageText+='Z -.1' + pageText+='Z -1' + pageText+='Z -10' + pageText+="" #TODO Map Z Moves pageText+="
\n" #endz pageText+="\n" #endgui From a85ab61b634d635ca9f727b4aab8ecbb8490840c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Jun 2012 14:42:24 -0500 Subject: [PATCH 18/19] Add place holder for temp control --- css/style.css | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ webinterface.py | 11 +++++++++++ 2 files changed, 60 insertions(+) diff --git a/css/style.css b/css/style.css index 11cb4b3..427f6c3 100644 --- a/css/style.css +++ b/css/style.css @@ -155,3 +155,52 @@ border: none; #logframe{ } + +#temp{ +} +#tempmenu +{ +padding: 0 0 10px 10px; +position: relative; +float: left; +width: 100%; +} +#tempmenu ul, #tempmenu li +{ +margin: 0; +display: inline; +list-style-type: none; +} + +#tempmenu b +{ +padding-top: 4px; +float: left; +line-height: 14px; +font-weight: bold; +color: #888; +margin: 0 10px 4px 10px; +text-decoration: none; +color: #999; +} + +#tempmenu a:link, #tempmenu a:visited +{ +float: left; +border-bottom: 1px solid #000; +line-height: 14px; +font-weight: bold; +margin: 0 10px 4px 10px; +text-decoration: none; +color: #999; +} + +#tempmenu a:link#tempmenu, #tempmenu a:visited#current, #tempmenu a:hover +{ +border-bottom: 2px solid #000; +padding-bottom: 2px; +background: transparent; +color: #000; +} + +#tempmenu a:hover { color: #000; } \ No newline at end of file diff --git a/webinterface.py b/webinterface.py index b959807..66601f1 100644 --- a/webinterface.py +++ b/webinterface.py @@ -319,6 +319,17 @@ class WebInterface(object): pageText+="\n" #endgui pageText+="\n" #endcontent pageText+="
\n" + + # Temp Control TBD + # pageText+="
" + # pageText+="
" + # pageText+="" + # pageText+="
" + # pageText+="
" + # pageText+="" + # pageText+="
" + # pageText+="
" + pageText=pageText+"
File Loaded: "+str(gPronterPtr.filename)+"
" pageText+="
" pageText+=PrintFooter() From 94b5ddebbbd38c7efedceb66361c80fd5d93cb4d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Jun 2012 23:37:53 -0500 Subject: [PATCH 19/19] Deleted Cherry Py --- .../CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO | 26 - .../CherryPy.egg-info/SOURCES.txt | 113 - .../CherryPy.egg-info/dependency_links.txt | 1 - .../CherryPy.egg-info/top_level.txt | 1 - libs/CherryPy-3.2.2/PKG-INFO | 26 - libs/CherryPy-3.2.2/README.txt | 13 - .../build/lib/cherrypy/__init__.py | 624 ----- .../build/lib/cherrypy/_cpchecker.py | 327 --- .../build/lib/cherrypy/_cpcompat.py | 318 --- .../build/lib/cherrypy/_cpconfig.py | 295 --- .../build/lib/cherrypy/_cpdispatch.py | 636 ----- .../build/lib/cherrypy/_cperror.py | 556 ---- .../build/lib/cherrypy/_cplogging.py | 440 ---- .../build/lib/cherrypy/_cpmodpy.py | 344 --- .../build/lib/cherrypy/_cpnative_server.py | 149 -- .../build/lib/cherrypy/_cpreqbody.py | 965 ------- .../build/lib/cherrypy/_cprequest.py | 956 ------- .../build/lib/cherrypy/_cpserver.py | 205 -- .../build/lib/cherrypy/_cpthreadinglocal.py | 239 -- .../build/lib/cherrypy/_cptools.py | 510 ---- .../build/lib/cherrypy/_cptree.py | 290 -- .../build/lib/cherrypy/_cpwsgi.py | 408 --- .../build/lib/cherrypy/_cpwsgi_server.py | 63 - .../build/lib/cherrypy/lib/__init__.py | 45 - .../build/lib/cherrypy/lib/auth.py | 87 - .../build/lib/cherrypy/lib/auth_basic.py | 87 - .../build/lib/cherrypy/lib/auth_digest.py | 365 --- .../build/lib/cherrypy/lib/caching.py | 465 ---- .../build/lib/cherrypy/lib/covercp.py | 365 --- .../build/lib/cherrypy/lib/cpstats.py | 662 ----- .../build/lib/cherrypy/lib/cptools.py | 617 ----- .../build/lib/cherrypy/lib/encoding.py | 388 --- .../build/lib/cherrypy/lib/gctools.py | 214 -- .../build/lib/cherrypy/lib/http.py | 7 - .../build/lib/cherrypy/lib/httpauth.py | 354 --- .../build/lib/cherrypy/lib/httputil.py | 506 ---- .../build/lib/cherrypy/lib/jsontools.py | 87 - .../build/lib/cherrypy/lib/profiler.py | 208 -- .../build/lib/cherrypy/lib/reprconf.py | 485 ---- .../build/lib/cherrypy/lib/sessions.py | 871 ------- .../build/lib/cherrypy/lib/static.py | 363 --- .../build/lib/cherrypy/lib/xmlrpcutil.py | 55 - .../build/lib/cherrypy/process/__init__.py | 14 - .../build/lib/cherrypy/process/plugins.py | 683 ----- .../build/lib/cherrypy/process/servers.py | 427 --- .../build/lib/cherrypy/process/win32.py | 174 -- .../build/lib/cherrypy/process/wspbus.py | 432 --- .../build/lib/cherrypy/scaffold/__init__.py | 61 - .../build/lib/cherrypy/test/__init__.py | 27 - .../lib/cherrypy/test/_test_decorators.py | 41 - .../lib/cherrypy/test/_test_states_demo.py | 66 - .../build/lib/cherrypy/test/benchmark.py | 409 --- .../build/lib/cherrypy/test/checkerdemo.py | 47 - .../build/lib/cherrypy/test/helper.py | 493 ---- .../build/lib/cherrypy/test/logtest.py | 188 -- .../build/lib/cherrypy/test/modfastcgi.py | 135 - .../build/lib/cherrypy/test/modfcgid.py | 125 - .../build/lib/cherrypy/test/modpy.py | 163 -- .../build/lib/cherrypy/test/modwsgi.py | 148 -- .../build/lib/cherrypy/test/sessiondemo.py | 153 -- .../lib/cherrypy/test/test_auth_basic.py | 79 - .../lib/cherrypy/test/test_auth_digest.py | 115 - .../build/lib/cherrypy/test/test_bus.py | 263 -- .../build/lib/cherrypy/test/test_caching.py | 328 --- .../build/lib/cherrypy/test/test_config.py | 256 -- .../lib/cherrypy/test/test_config_server.py | 121 - .../build/lib/cherrypy/test/test_conn.py | 734 ------ .../build/lib/cherrypy/test/test_core.py | 688 ----- .../test/test_dynamicobjectmapping.py | 404 --- .../build/lib/cherrypy/test/test_encoding.py | 363 --- .../build/lib/cherrypy/test/test_etags.py | 83 - .../build/lib/cherrypy/test/test_http.py | 212 -- .../build/lib/cherrypy/test/test_httpauth.py | 151 -- .../build/lib/cherrypy/test/test_httplib.py | 29 - .../build/lib/cherrypy/test/test_json.py | 79 - .../build/lib/cherrypy/test/test_logging.py | 157 -- .../build/lib/cherrypy/test/test_mime.py | 128 - .../lib/cherrypy/test/test_misc_tools.py | 207 -- .../lib/cherrypy/test/test_objectmapping.py | 404 --- .../build/lib/cherrypy/test/test_proxy.py | 129 - .../build/lib/cherrypy/test/test_refleaks.py | 59 - .../lib/cherrypy/test/test_request_obj.py | 737 ------ .../build/lib/cherrypy/test/test_routes.py | 69 - .../build/lib/cherrypy/test/test_session.py | 464 ---- .../cherrypy/test/test_sessionauthenticate.py | 62 - .../build/lib/cherrypy/test/test_states.py | 439 ---- .../build/lib/cherrypy/test/test_static.py | 300 --- .../build/lib/cherrypy/test/test_tools.py | 399 --- .../build/lib/cherrypy/test/test_tutorials.py | 201 -- .../lib/cherrypy/test/test_virtualhost.py | 107 - .../build/lib/cherrypy/test/test_wsgi_ns.py | 91 - .../lib/cherrypy/test/test_wsgi_vhost.py | 36 - .../build/lib/cherrypy/test/test_wsgiapps.py | 118 - .../build/lib/cherrypy/test/test_xmlrpc.py | 179 -- .../build/lib/cherrypy/test/webtest.py | 575 ---- .../build/lib/cherrypy/tutorial/__init__.py | 3 - .../lib/cherrypy/tutorial/bonus-sqlobject.py | 168 -- .../lib/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 - .../tutorial/tut05_derived_objects.py | 83 - .../cherrypy/tutorial/tut06_default_method.py | 64 - .../lib/cherrypy/tutorial/tut07_sessions.py | 44 - .../tutorial/tut08_generators_and_yield.py | 47 - .../lib/cherrypy/tutorial/tut09_files.py | 107 - .../cherrypy/tutorial/tut10_http_errors.py | 81 - .../build/lib/cherrypy/wsgiserver/__init__.py | 14 - .../lib/cherrypy/wsgiserver/ssl_builtin.py | 91 - .../lib/cherrypy/wsgiserver/ssl_pyopenssl.py | 256 -- .../lib/cherrypy/wsgiserver/wsgiserver2.py | 2322 ----------------- libs/CherryPy-3.2.2/build/scripts-2.7/cherryd | 109 - libs/CherryPy-3.2.2/cherrypy/LICENSE.txt | 25 - libs/CherryPy-3.2.2/cherrypy/__init__.py | 624 ----- libs/CherryPy-3.2.2/cherrypy/_cpchecker.py | 327 --- libs/CherryPy-3.2.2/cherrypy/_cpcompat.py | 318 --- libs/CherryPy-3.2.2/cherrypy/_cpconfig.py | 295 --- libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py | 636 ----- libs/CherryPy-3.2.2/cherrypy/_cperror.py | 556 ---- libs/CherryPy-3.2.2/cherrypy/_cplogging.py | 440 ---- libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py | 344 --- .../cherrypy/_cpnative_server.py | 149 -- libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py | 965 ------- libs/CherryPy-3.2.2/cherrypy/_cprequest.py | 956 ------- libs/CherryPy-3.2.2/cherrypy/_cpserver.py | 205 -- .../cherrypy/_cpthreadinglocal.py | 239 -- libs/CherryPy-3.2.2/cherrypy/_cptools.py | 510 ---- libs/CherryPy-3.2.2/cherrypy/_cptree.py | 290 -- libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py | 408 --- .../CherryPy-3.2.2/cherrypy/_cpwsgi_server.py | 63 - libs/CherryPy-3.2.2/cherrypy/cherryd | 109 - libs/CherryPy-3.2.2/cherrypy/favicon.ico | Bin 1406 -> 0 bytes libs/CherryPy-3.2.2/cherrypy/lib/__init__.py | 45 - libs/CherryPy-3.2.2/cherrypy/lib/auth.py | 87 - .../CherryPy-3.2.2/cherrypy/lib/auth_basic.py | 87 - .../cherrypy/lib/auth_digest.py | 365 --- libs/CherryPy-3.2.2/cherrypy/lib/caching.py | 465 ---- libs/CherryPy-3.2.2/cherrypy/lib/covercp.py | 365 --- libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py | 662 ----- libs/CherryPy-3.2.2/cherrypy/lib/cptools.py | 617 ----- libs/CherryPy-3.2.2/cherrypy/lib/encoding.py | 388 --- libs/CherryPy-3.2.2/cherrypy/lib/gctools.py | 214 -- libs/CherryPy-3.2.2/cherrypy/lib/http.py | 7 - libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py | 354 --- libs/CherryPy-3.2.2/cherrypy/lib/httputil.py | 506 ---- libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py | 87 - libs/CherryPy-3.2.2/cherrypy/lib/profiler.py | 208 -- libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py | 485 ---- libs/CherryPy-3.2.2/cherrypy/lib/sessions.py | 871 ------- libs/CherryPy-3.2.2/cherrypy/lib/static.py | 363 --- .../CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py | 55 - .../cherrypy/process/__init__.py | 14 - .../cherrypy/process/plugins.py | 683 ----- .../cherrypy/process/servers.py | 427 --- libs/CherryPy-3.2.2/cherrypy/process/win32.py | 174 -- .../CherryPy-3.2.2/cherrypy/process/wspbus.py | 432 --- .../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 7455 -> 0 bytes libs/CherryPy-3.2.2/cherrypy/test/__init__.py | 27 - .../cherrypy/test/_test_decorators.py | 41 - .../cherrypy/test/_test_states_demo.py | 66 - .../CherryPy-3.2.2/cherrypy/test/benchmark.py | 409 --- .../cherrypy/test/checkerdemo.py | 47 - libs/CherryPy-3.2.2/cherrypy/test/helper.py | 493 ---- libs/CherryPy-3.2.2/cherrypy/test/logtest.py | 188 -- .../cherrypy/test/modfastcgi.py | 135 - libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py | 125 - libs/CherryPy-3.2.2/cherrypy/test/modpy.py | 163 -- libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py | 148 -- .../cherrypy/test/sessiondemo.py | 153 -- .../cherrypy/test/static/dirback.jpg | Bin 18238 -> 0 bytes .../cherrypy/test/static/index.html | 1 - libs/CherryPy-3.2.2/cherrypy/test/style.css | 1 - libs/CherryPy-3.2.2/cherrypy/test/test.pem | 38 - .../cherrypy/test/test_auth_basic.py | 79 - .../cherrypy/test/test_auth_digest.py | 115 - libs/CherryPy-3.2.2/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-3.2.2/cherrypy/test/test_conn.py | 734 ------ .../CherryPy-3.2.2/cherrypy/test/test_core.py | 688 ----- .../test/test_dynamicobjectmapping.py | 404 --- .../cherrypy/test/test_encoding.py | 363 --- .../cherrypy/test/test_etags.py | 83 - .../CherryPy-3.2.2/cherrypy/test/test_http.py | 212 -- .../cherrypy/test/test_httpauth.py | 151 -- .../cherrypy/test/test_httplib.py | 29 - .../CherryPy-3.2.2/cherrypy/test/test_json.py | 79 - .../cherrypy/test/test_logging.py | 157 -- .../CherryPy-3.2.2/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 -- libs/CherryPy-3.2.2/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 85698 -> 0 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 - .../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/ssl_builtin.py | 91 - .../cherrypy/wsgiserver/ssl_pyopenssl.py | 256 -- .../cherrypy/wsgiserver/wsgiserver2.py | 2322 ----------------- .../cherrypy/wsgiserver/wsgiserver3.py | 2040 --------------- .../dist/CherryPy-3.2.2-py2.7.egg | Bin 851999 -> 0 bytes libs/CherryPy-3.2.2/files.txt | 231 -- libs/CherryPy-3.2.2/setup.py | 144 - 236 files changed, 62163 deletions(-) delete mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO delete mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt delete mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt delete mode 100644 libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt delete mode 100644 libs/CherryPy-3.2.2/PKG-INFO delete mode 100644 libs/CherryPy-3.2.2/README.txt delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py delete mode 100644 libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py delete mode 100644 libs/CherryPy-3.2.2/build/scripts-2.7/cherryd delete mode 100644 libs/CherryPy-3.2.2/cherrypy/LICENSE.txt delete mode 100644 libs/CherryPy-3.2.2/cherrypy/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpchecker.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpcompat.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpconfig.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cperror.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cplogging.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cprequest.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpserver.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cptools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cptree.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/cherryd delete mode 100644 libs/CherryPy-3.2.2/cherrypy/favicon.ico delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/caching.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/covercp.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/cptools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/encoding.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/gctools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/http.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/httputil.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/profiler.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/sessions.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/static.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/process/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/process/plugins.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/process/servers.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/process/win32.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/process/wspbus.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf delete mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf delete mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf delete mode 100644 libs/CherryPy-3.2.2/cherrypy/scaffold/static/made_with_cherrypy_small.png delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/_test_decorators.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/_test_states_demo.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/benchmark.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/helper.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/logtest.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modpy.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/static/dirback.jpg delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/static/index.html delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/style.css delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test.pem delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_auth_basic.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_bus.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_caching.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_config.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_conn.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_core.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_etags.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_http.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_json.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_logging.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_mime.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_routes.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_session.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_states.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_static.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_tools.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/test/webtest.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/pdf_file.pdf delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut01_helloworld.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut02_expose_methods.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf delete mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py delete mode 100644 libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py delete mode 100644 libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg delete mode 100644 libs/CherryPy-3.2.2/files.txt delete mode 100644 libs/CherryPy-3.2.2/setup.py diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO b/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO deleted file mode 100644 index 34cae0e..0000000 --- a/libs/CherryPy-3.2.2/CherryPy.egg-info/PKG-INFO +++ /dev/null @@ -1,26 +0,0 @@ -Metadata-Version: 1.0 -Name: CherryPy -Version: 3.2.2 -Summary: Object-Oriented HTTP framework -Home-page: http://www.cherrypy.org -Author: CherryPy Team -Author-email: team@cherrypy.org -License: BSD -Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/ -Description: CherryPy is a pythonic, object-oriented HTTP framework -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: Freely Distributable -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 -Classifier: Topic :: Internet :: WWW/HTTP -Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content -Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server -Classifier: Topic :: Software Development :: Libraries :: Application Frameworks diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt deleted file mode 100644 index ec7f39c..0000000 --- a/libs/CherryPy-3.2.2/CherryPy.egg-info/SOURCES.txt +++ /dev/null @@ -1,113 +0,0 @@ -README.txt -setup.py -CherryPy.egg-info/PKG-INFO -CherryPy.egg-info/SOURCES.txt -CherryPy.egg-info/dependency_links.txt -CherryPy.egg-info/top_level.txt -cherrypy/__init__.py -cherrypy/_cpchecker.py -cherrypy/_cpcompat.py -cherrypy/_cpconfig.py -cherrypy/_cpdispatch.py -cherrypy/_cperror.py -cherrypy/_cplogging.py -cherrypy/_cpmodpy.py -cherrypy/_cpnative_server.py -cherrypy/_cpreqbody.py -cherrypy/_cprequest.py -cherrypy/_cpserver.py -cherrypy/_cpthreadinglocal.py -cherrypy/_cptools.py -cherrypy/_cptree.py -cherrypy/_cpwsgi.py -cherrypy/_cpwsgi_server.py -cherrypy/cherryd -cherrypy/lib/__init__.py -cherrypy/lib/auth.py -cherrypy/lib/auth_basic.py -cherrypy/lib/auth_digest.py -cherrypy/lib/caching.py -cherrypy/lib/covercp.py -cherrypy/lib/cpstats.py -cherrypy/lib/cptools.py -cherrypy/lib/encoding.py -cherrypy/lib/gctools.py -cherrypy/lib/http.py -cherrypy/lib/httpauth.py -cherrypy/lib/httputil.py -cherrypy/lib/jsontools.py -cherrypy/lib/profiler.py -cherrypy/lib/reprconf.py -cherrypy/lib/sessions.py -cherrypy/lib/static.py -cherrypy/lib/xmlrpcutil.py -cherrypy/process/__init__.py -cherrypy/process/plugins.py -cherrypy/process/servers.py -cherrypy/process/win32.py -cherrypy/process/wspbus.py -cherrypy/scaffold/__init__.py -cherrypy/test/__init__.py -cherrypy/test/_test_decorators.py -cherrypy/test/_test_states_demo.py -cherrypy/test/benchmark.py -cherrypy/test/checkerdemo.py -cherrypy/test/helper.py -cherrypy/test/logtest.py -cherrypy/test/modfastcgi.py -cherrypy/test/modfcgid.py -cherrypy/test/modpy.py -cherrypy/test/modwsgi.py -cherrypy/test/sessiondemo.py -cherrypy/test/test_auth_basic.py -cherrypy/test/test_auth_digest.py -cherrypy/test/test_bus.py -cherrypy/test/test_caching.py -cherrypy/test/test_config.py -cherrypy/test/test_config_server.py -cherrypy/test/test_conn.py -cherrypy/test/test_core.py -cherrypy/test/test_dynamicobjectmapping.py -cherrypy/test/test_encoding.py -cherrypy/test/test_etags.py -cherrypy/test/test_http.py -cherrypy/test/test_httpauth.py -cherrypy/test/test_httplib.py -cherrypy/test/test_json.py -cherrypy/test/test_logging.py -cherrypy/test/test_mime.py -cherrypy/test/test_misc_tools.py -cherrypy/test/test_objectmapping.py -cherrypy/test/test_proxy.py -cherrypy/test/test_refleaks.py -cherrypy/test/test_request_obj.py -cherrypy/test/test_routes.py -cherrypy/test/test_session.py -cherrypy/test/test_sessionauthenticate.py -cherrypy/test/test_states.py -cherrypy/test/test_static.py -cherrypy/test/test_tools.py -cherrypy/test/test_tutorials.py -cherrypy/test/test_virtualhost.py -cherrypy/test/test_wsgi_ns.py -cherrypy/test/test_wsgi_vhost.py -cherrypy/test/test_wsgiapps.py -cherrypy/test/test_xmlrpc.py -cherrypy/test/webtest.py -cherrypy/tutorial/__init__.py -cherrypy/tutorial/bonus-sqlobject.py -cherrypy/tutorial/tut01_helloworld.py -cherrypy/tutorial/tut02_expose_methods.py -cherrypy/tutorial/tut03_get_and_post.py -cherrypy/tutorial/tut04_complex_site.py -cherrypy/tutorial/tut05_derived_objects.py -cherrypy/tutorial/tut06_default_method.py -cherrypy/tutorial/tut07_sessions.py -cherrypy/tutorial/tut08_generators_and_yield.py -cherrypy/tutorial/tut09_files.py -cherrypy/tutorial/tut10_http_errors.py -cherrypy/wsgiserver/__init__.py -cherrypy/wsgiserver/ssl_builtin.py -cherrypy/wsgiserver/ssl_pyopenssl.py -cherrypy/wsgiserver/wsgiserver2.py -cherrypy/wsgiserver/wsgiserver3.py \ No newline at end of file diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/libs/CherryPy-3.2.2/CherryPy.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt b/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt deleted file mode 100644 index d718706..0000000 --- a/libs/CherryPy-3.2.2/CherryPy.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -cherrypy diff --git a/libs/CherryPy-3.2.2/PKG-INFO b/libs/CherryPy-3.2.2/PKG-INFO deleted file mode 100644 index 34cae0e..0000000 --- a/libs/CherryPy-3.2.2/PKG-INFO +++ /dev/null @@ -1,26 +0,0 @@ -Metadata-Version: 1.0 -Name: CherryPy -Version: 3.2.2 -Summary: Object-Oriented HTTP framework -Home-page: http://www.cherrypy.org -Author: CherryPy Team -Author-email: team@cherrypy.org -License: BSD -Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/ -Description: CherryPy is a pythonic, object-oriented HTTP framework -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: Freely Distributable -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 -Classifier: Topic :: Internet :: WWW/HTTP -Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content -Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application -Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server -Classifier: Topic :: Software Development :: Libraries :: Application Frameworks diff --git a/libs/CherryPy-3.2.2/README.txt b/libs/CherryPy-3.2.2/README.txt deleted file mode 100644 index d852128..0000000 --- a/libs/CherryPy-3.2.2/README.txt +++ /dev/null @@ -1,13 +0,0 @@ -* To install, change to the directory where setup.py is located and type (python-2.3 or later needed): - - python setup.py install - -* To learn how to use it, look at the examples under cherrypy/tutorial/ or go to http://www.cherrypy.org for more info. - -* To run the regression tests, just go to the cherrypy/test/ directory and type: - - nosetests -s ./ - - Or to run individual tests type: - - nosetests -s test_foo.py diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py deleted file mode 100644 index 41e3898..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/__init__.py +++ /dev/null @@ -1,624 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py deleted file mode 100644 index 7ccfd89..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpchecker.py +++ /dev/null @@ -1,327 +0,0 @@ -import os -import warnings - -import cherrypy -from cherrypy._cpcompat import iteritems, copykeys, builtins - - -class Checker(object): - """A checker for CherryPy sites and their mounted applications. - - When this object is called at engine startup, it executes each - of its own methods whose names start with ``check_``. If you wish - to disable selected checks, simply add a line in your global - config which sets the appropriate method to False:: - - [global] - checker.check_skipped_app_config = False - - You may also dynamically add or replace ``check_*`` methods in this way. - """ - - on = True - """If True (the default), run all checks; if False, turn off all checks.""" - - - def __init__(self): - self._populate_known_types() - - def __call__(self): - """Run all check_* methods.""" - if self.on: - oldformatwarning = warnings.formatwarning - warnings.formatwarning = self.formatwarning - try: - for name in dir(self): - if name.startswith("check_"): - method = getattr(self, name) - if method and hasattr(method, '__call__'): - method() - finally: - warnings.formatwarning = oldformatwarning - - def formatwarning(self, message, category, filename, lineno, line=None): - """Function to format a warning.""" - return "CherryPy Checker:\n%s\n\n" % message - - # This value should be set inside _cpconfig. - global_config_contained_paths = False - - def check_app_config_entries_dont_start_with_script_name(self): - """Check for Application config with sections that repeat script_name.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - if sn == '': - continue - sn_atoms = sn.strip("/").split("/") - for key in app.config.keys(): - key_atoms = key.strip("/").split("/") - if key_atoms[:len(sn_atoms)] == sn_atoms: - warnings.warn( - "The application mounted at %r has config " \ - "entries that start with its script name: %r" % (sn, key)) - - def check_site_config_entries_in_app_config(self): - """Check for mounted Applications that have site-scoped config.""" - for sn, app in iteritems(cherrypy.tree.apps): - if not isinstance(app, cherrypy.Application): - continue - - msg = [] - for section, entries in iteritems(app.config): - if section.startswith('/'): - for key, value in iteritems(entries): - for n in ("engine.", "server.", "tree.", "checker."): - if key.startswith(n): - msg.append("[%s] %s = %s" % (section, key, value)) - if msg: - msg.insert(0, - "The application mounted at %r contains the following " - "config entries, which are only allowed in site-wide " - "config. Move them to a [global] section and pass them " - "to cherrypy.config.update() instead of tree.mount()." % sn) - warnings.warn(os.linesep.join(msg)) - - def check_skipped_app_config(self): - """Check for mounted Applications that have no config.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - msg = "The Application mounted at %r has an empty config." % sn - if self.global_config_contained_paths: - msg += (" It looks like the config you passed to " - "cherrypy.config.update() contains application-" - "specific sections. You must explicitly pass " - "application config via " - "cherrypy.tree.mount(..., config=app_config)") - warnings.warn(msg) - return - - def check_app_config_brackets(self): - """Check for Application config with extraneous brackets in section names.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - for key in app.config.keys(): - if key.startswith("[") or key.endswith("]"): - warnings.warn( - "The application mounted at %r has config " \ - "section names with extraneous brackets: %r. " - "Config *files* need brackets; config *dicts* " - "(e.g. passed to tree.mount) do not." % (sn, key)) - - def check_static_paths(self): - """Check Application config for incorrect static paths.""" - # Use the dummy Request object in the main thread. - request = cherrypy.request - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - request.app = app - for section in app.config: - # get_resource will populate request.config - request.get_resource(section + "/dummy.html") - conf = request.config.get - - if conf("tools.staticdir.on", False): - msg = "" - root = conf("tools.staticdir.root") - dir = conf("tools.staticdir.dir") - if dir is None: - msg = "tools.staticdir.dir is not set." - else: - fulldir = "" - if os.path.isabs(dir): - fulldir = dir - if root: - msg = ("dir is an absolute path, even " - "though a root is provided.") - testdir = os.path.join(root, dir[1:]) - if os.path.exists(testdir): - msg += ("\nIf you meant to serve the " - "filesystem folder at %r, remove " - "the leading slash from dir." % testdir) - else: - if not root: - msg = "dir is a relative path and no root provided." - else: - fulldir = os.path.join(root, dir) - if not os.path.isabs(fulldir): - msg = "%r is not an absolute path." % fulldir - - if fulldir and not os.path.exists(fulldir): - if msg: - msg += "\n" - msg += ("%r (root + dir) is not an existing " - "filesystem path." % fulldir) - - if msg: - warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" - % (msg, section, root, dir)) - - - # -------------------------- Compatibility -------------------------- # - - obsolete = { - 'server.default_content_type': 'tools.response_headers.headers', - 'log_access_file': 'log.access_file', - 'log_config_options': None, - 'log_file': 'log.error_file', - 'log_file_not_found': None, - 'log_request_headers': 'tools.log_headers.on', - 'log_to_screen': 'log.screen', - 'show_tracebacks': 'request.show_tracebacks', - 'throw_errors': 'request.throw_errors', - 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' - 'cherrypy.Application(Root())))'), - } - - deprecated = {} - - def _compat(self, config): - """Process config and warn on each obsolete or deprecated entry.""" - for section, conf in config.items(): - if isinstance(conf, dict): - for k, v in conf.items(): - if k in self.obsolete: - warnings.warn("%r is obsolete. Use %r instead.\n" - "section: [%s]" % - (k, self.obsolete[k], section)) - elif k in self.deprecated: - warnings.warn("%r is deprecated. Use %r instead.\n" - "section: [%s]" % - (k, self.deprecated[k], section)) - else: - if section in self.obsolete: - warnings.warn("%r is obsolete. Use %r instead." - % (section, self.obsolete[section])) - elif section in self.deprecated: - warnings.warn("%r is deprecated. Use %r instead." - % (section, self.deprecated[section])) - - def check_compatibility(self): - """Process config and warn on each obsolete or deprecated entry.""" - self._compat(cherrypy.config) - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - self._compat(app.config) - - - # ------------------------ Known Namespaces ------------------------ # - - extra_config_namespaces = [] - - def _known_ns(self, app): - ns = ["wsgi"] - ns.extend(copykeys(app.toolboxes)) - ns.extend(copykeys(app.namespaces)) - ns.extend(copykeys(app.request_class.namespaces)) - ns.extend(copykeys(cherrypy.config.namespaces)) - ns += self.extra_config_namespaces - - for section, conf in app.config.items(): - is_path_section = section.startswith("/") - if is_path_section and isinstance(conf, dict): - for k, v in conf.items(): - atoms = k.split(".") - if len(atoms) > 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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py deleted file mode 100644 index ed24c1a..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpcompat.py +++ /dev/null @@ -1,318 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py deleted file mode 100644 index 7b4c6a4..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpconfig.py +++ /dev/null @@ -1,295 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py deleted file mode 100644 index d614e08..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpdispatch.py +++ /dev/null @@ -1,636 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py deleted file mode 100644 index 76a409f..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cperror.py +++ /dev/null @@ -1,556 +0,0 @@ -"""Exception classes for CherryPy. - -CherryPy provides (and uses) exceptions for declaring that the HTTP response -should be a status other than the default "200 OK". You can ``raise`` them like -normal Python exceptions. You can also call them and they will raise themselves; -this means you can set an :class:`HTTPError` -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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py deleted file mode 100644 index e10c942..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cplogging.py +++ /dev/null @@ -1,440 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py deleted file mode 100644 index 76ef6ea..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpmodpy.py +++ /dev/null @@ -1,344 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py deleted file mode 100644 index 57f715a..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpnative_server.py +++ /dev/null @@ -1,149 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py deleted file mode 100644 index 5d72c85..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpreqbody.py +++ /dev/null @@ -1,965 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py deleted file mode 100644 index 5890c72..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cprequest.py +++ /dev/null @@ -1,956 +0,0 @@ - -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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py deleted file mode 100644 index 2eecd6e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpserver.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Manage HTTP servers with CherryPy.""" - -import warnings - -import cherrypy -from cherrypy.lib import attributes -from cherrypy._cpcompat import basestring, py3k - -# We import * because we want to export check_port -# et al as attributes of this module. -from cherrypy.process.servers import * - - -class Server(ServerAdapter): - """An adapter for an HTTP server. - - You can set attributes (like socket_host and socket_port) - on *this* object (which is probably cherrypy.server), and call - quickstart. For example:: - - cherrypy.server.socket_port = 80 - cherrypy.quickstart() - """ - - socket_port = 8080 - """The TCP port on which to listen for connections.""" - - _socket_host = '127.0.0.1' - def _get_socket_host(self): - return self._socket_host - def _set_socket_host(self, value): - if value == '': - raise ValueError("The empty string ('') is not an allowed value. " - "Use '0.0.0.0' instead to listen on all active " - "interfaces (INADDR_ANY).") - self._socket_host = value - socket_host = property(_get_socket_host, _set_socket_host, - doc="""The hostname or IP address on which to listen for connections. - - 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.""") - - socket_file = None - """If given, the name of the UNIX socket to use instead of TCP/IP. - - When this option is not None, the `socket_host` and `socket_port` options - are ignored.""" - - socket_queue_size = 5 - """The 'backlog' argument to socket.listen(); specifies the maximum number - of queued connections (default 5).""" - - socket_timeout = 10 - """The timeout in seconds for accepted connections (default 10).""" - - shutdown_timeout = 5 - """The time to wait for HTTP worker threads to clean up.""" - - protocol_version = 'HTTP/1.1' - """The version string to write in the Status-Line of all HTTP responses, - for example, "HTTP/1.1" (the default). Depending on the HTTP server used, - this should also limit the supported features used in the response.""" - - thread_pool = 10 - """The number of worker threads to start up in the pool.""" - - thread_pool_max = -1 - """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" - - max_request_header_size = 500 * 1024 - """The maximum number of bytes allowable in the request headers. If exceeded, - the HTTP server should return "413 Request Entity Too Large".""" - - max_request_body_size = 100 * 1024 * 1024 - """The maximum number of bytes allowable in the request body. If exceeded, - the HTTP server should return "413 Request Entity Too Large".""" - - instance = None - """If not None, this should be an HTTP server instance (such as - CPWSGIServer) which cherrypy.server will control. Use this when you need - more control over object instantiation than is available in the various - configuration options.""" - - ssl_context = None - """When using PyOpenSSL, an instance of SSL.Context.""" - - ssl_certificate = None - """The filename of the SSL certificate to use.""" - - ssl_certificate_chain = None - """When using PyOpenSSL, the certificate chain to pass to - Context.load_verify_locations.""" - - ssl_private_key = None - """The filename of the private key to use with SSL.""" - - if py3k: - ssl_module = 'builtin' - """The name of a registered SSL adaptation module to use with the builtin - WSGI server. Builtin options are: 'builtin' (to use the SSL library built - into recent versions of Python). You may also register your - own classes in the wsgiserver.ssl_adapters dict.""" - else: - ssl_module = 'pyopenssl' - """The name of a registered SSL adaptation module to use with the builtin - WSGI server. Builtin options are 'builtin' (to use the SSL library built - into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL - project, which you must install separately). You may also register your - own classes in the wsgiserver.ssl_adapters dict.""" - - statistics = False - """Turns statistics-gathering on or off for aware HTTP servers.""" - - nodelay = True - """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" - - wsgi_version = (1, 0) - """The WSGI version tuple to use with the builtin WSGI server. - The provided options are (1, 0) [which includes support for PEP 3333, - which declares it covers WSGI version 1.0.1 but still mandates the - wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. - You may create and register your own experimental versions of the WSGI - protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" - - def __init__(self): - self.bus = cherrypy.engine - self.httpserver = None - self.interrupt = None - self.running = False - - def httpserver_from_self(self, httpserver=None): - """Return a (httpserver, bind_addr) pair based on self attributes.""" - if httpserver is None: - httpserver = self.instance - if httpserver is None: - from cherrypy import _cpwsgi_server - httpserver = _cpwsgi_server.CPWSGIServer(self) - if isinstance(httpserver, basestring): - # Is anyone using this? Can I add an arg? - httpserver = attributes(httpserver)(self) - return httpserver, self.bind_addr - - def start(self): - """Start the HTTP server.""" - if not self.httpserver: - self.httpserver, self.bind_addr = self.httpserver_from_self() - ServerAdapter.start(self) - start.priority = 75 - - def _get_bind_addr(self): - if self.socket_file: - return self.socket_file - if self.socket_host is None and self.socket_port is None: - return None - return (self.socket_host, self.socket_port) - def _set_bind_addr(self, value): - if value is None: - self.socket_file = None - self.socket_host = None - self.socket_port = None - elif isinstance(value, basestring): - self.socket_file = value - self.socket_host = None - self.socket_port = None - else: - try: - self.socket_host, self.socket_port = value - self.socket_file = None - except ValueError: - raise ValueError("bind_addr must be a (host, port) tuple " - "(for TCP sockets) or a string (for Unix " - "domain sockets), not %r" % value) - bind_addr = property(_get_bind_addr, _set_bind_addr, - doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') - - def base(self): - """Return the base (scheme://host[:port] or sock file) for this server.""" - if self.socket_file: - return self.socket_file - - host = self.socket_host - if host in ('0.0.0.0', '::'): - # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. - # Look up the host name, which should be the - # safest thing to spit out in a URL. - import socket - host = socket.gethostname() - - port = self.socket_port - - if self.ssl_certificate: - scheme = "https" - if port != 443: - host += ":%s" % port - else: - scheme = "http" - if port != 80: - host += ":%s" % port - - return "%s://%s" % (scheme, host) - diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py deleted file mode 100644 index 34c17ac..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpthreadinglocal.py +++ /dev/null @@ -1,239 +0,0 @@ -# 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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py deleted file mode 100644 index 22316b3..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptools.py +++ /dev/null @@ -1,510 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py deleted file mode 100644 index 3aa4b9e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cptree.py +++ /dev/null @@ -1,290 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py deleted file mode 100644 index 91cd044..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi.py +++ /dev/null @@ -1,408 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py deleted file mode 100644 index 21af513..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/_cpwsgi_server.py +++ /dev/null @@ -1,63 +0,0 @@ -"""WSGI server interface (see PEP 333). This adds some CP-specific bits to -the framework-agnostic wsgiserver package. -""" -import sys - -import cherrypy -from cherrypy import wsgiserver - - -class CPWSGIServer(wsgiserver.CherryPyWSGIServer): - """Wrapper for wsgiserver.CherryPyWSGIServer. - - 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 set our own mount points from cherrypy.tree - and apply some attributes from config -> 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py deleted file mode 100644 index 3fc0ec5..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py deleted file mode 100644 index 7d2f6dc..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth.py +++ /dev/null @@ -1,87 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py deleted file mode 100644 index 2c05e01..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_basic.py +++ /dev/null @@ -1,87 +0,0 @@ -# This file is part of CherryPy -# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py deleted file mode 100644 index 67578e0..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/auth_digest.py +++ /dev/null @@ -1,365 +0,0 @@ -# This file is part of CherryPy -# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py deleted file mode 100644 index 435b9dc..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/caching.py +++ /dev/null @@ -1,465 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py deleted file mode 100644 index 9b701b5..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/covercp.py +++ /dev/null @@ -1,365 +0,0 @@ -"""Code-coverage tools for CherryPy. - -To use this module, or the coverage tools in the test suite, -you need to download 'coverage.py', either Gareth Rees' `original -implementation `_ -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 = """
- - -\n""" -TEMPLATE_LOC_NOT_COVERED = """ - - -\n""" -TEMPLATE_LOC_EXCLUDED = """ - - -\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 '
settingvalue
Build Dimenstions"+str(gPronterPtr.settings.build_dimensions)+"
Last Bed Temp"+str(gPronterPtr.settings.last_bed_temperature)+"
Last File Path"+gPronterPtr.settings.last_file_path+"
Last Temperature"+str(gPronterPtr.settings.last_temperature)+"
Preview Extrusion Width"+str(gPronterPtr.settings.preview_extrusion_width)+"
Filename"+str(gPronterPtr.filename)+"
%s %s
%s %s
%s %s
\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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py deleted file mode 100644 index 9be947f..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cpstats.py +++ /dev/null @@ -1,662 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py deleted file mode 100644 index b426a3e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/cptools.py +++ /dev/null @@ -1,617 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py deleted file mode 100644 index 6459746..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/encoding.py +++ /dev/null @@ -1,388 +0,0 @@ -import struct -import time - -import cherrypy -from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr -from cherrypy.lib import file_generator -from cherrypy.lib import set_vary_header - - -def decode(encoding=None, default_encoding='utf-8'): - """Replace or extend the list of charsets used to decode a request entity. - - Either argument may be a single string or a list of strings. - - encoding - If not None, restricts the set of charsets attempted while decoding - a request entity to the given set (even if a different charset is given in - the Content-Type request header). - - default_encoding - Only in effect if the 'encoding' argument is not given. - If given, the set of charsets attempted while decoding a request entity is - *extended* with the given value(s). - - """ - body = cherrypy.request.body - if encoding is not None: - if not isinstance(encoding, list): - encoding = [encoding] - body.attempt_charsets = encoding - elif default_encoding: - if not isinstance(default_encoding, list): - default_encoding = [default_encoding] - body.attempt_charsets = body.attempt_charsets + default_encoding - - -class ResponseEncoder: - - default_encoding = 'utf-8' - failmsg = "Response body could not be encoded with %r." - encoding = None - errors = 'strict' - text_only = True - add_charset = True - debug = False - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - - self.attempted_charsets = set() - request = cherrypy.serving.request - if request.handler is not None: - # Replace request.handler with self - if self.debug: - cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') - self.oldhandler = request.handler - request.handler = self - - def encode_stream(self, encoding): - """Encode a streaming response body. - - Use a generator wrapper, and just pray it works as the stream is - being written out. - """ - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - - def encoder(body): - for chunk in body: - if isinstance(chunk, unicodestr): - chunk = chunk.encode(encoding, self.errors) - yield chunk - self.body = encoder(self.body) - return True - - def encode_string(self, encoding): - """Encode a buffered response body.""" - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - - try: - body = [] - for chunk in self.body: - if isinstance(chunk, unicodestr): - chunk = chunk.encode(encoding, self.errors) - body.append(chunk) - self.body = body - except (LookupError, UnicodeError): - return False - else: - return True - - def find_acceptable_charset(self): - request = cherrypy.serving.request - response = cherrypy.serving.response - - if self.debug: - cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') - if response.stream: - encoder = self.encode_stream - else: - encoder = self.encode_string - if "Content-Length" in response.headers: - # Delete Content-Length header so finalize() recalcs it. - # Encoded strings may be of different lengths from their - # unicode equivalents, and even from each other. For example: - # >>> 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py deleted file mode 100644 index 183148b..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/gctools.py +++ /dev/null @@ -1,214 +0,0 @@ -import gc -import inspect -import os -import sys -import time - -try: - import objgraph -except ImportError: - objgraph = None - -import cherrypy -from cherrypy import _cprequest, _cpwsgi -from cherrypy.process.plugins import SimplePlugin - - -class ReferrerTree(object): - """An object which gathers all referrers of an object to a given depth.""" - - peek_length = 40 - - def __init__(self, ignore=None, maxdepth=2, maxparents=10): - self.ignore = ignore or [] - self.ignore.append(inspect.currentframe().f_back) - self.maxdepth = maxdepth - self.maxparents = maxparents - - def ascend(self, obj, depth=1): - """Return a nested list containing referrers of the given object.""" - depth += 1 - parents = [] - - # Gather all referrers in one step to minimize - # cascading references due to repr() logic. - refs = gc.get_referrers(obj) - self.ignore.append(refs) - if len(refs) > 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py deleted file mode 100644 index 4661d69..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/http.py +++ /dev/null @@ -1,7 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py deleted file mode 100644 index ad7c6eb..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httpauth.py +++ /dev/null @@ -1,354 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py deleted file mode 100644 index 5f77d54..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/httputil.py +++ /dev/null @@ -1,506 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py deleted file mode 100644 index 2092579..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/jsontools.py +++ /dev/null @@ -1,87 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py deleted file mode 100644 index 785d58a..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/profiler.py +++ /dev/null @@ -1,208 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py deleted file mode 100644 index ba8ff51..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/reprconf.py +++ /dev/null @@ -1,485 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py deleted file mode 100644 index 9763f12..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/sessions.py +++ /dev/null @@ -1,871 +0,0 @@ -"""Session implementation for CherryPy. - -You need to edit your config file to use sessions. Here's an example:: - - [/] - tools.sessions.on = True - tools.sessions.storage_type = "file" - tools.sessions.storage_path = "/home/site/sessions" - tools.sessions.timeout = 60 - -This sets the session to be stored in files in the directory /home/site/sessions, -and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions -will be saved in RAM. ``tools.sessions.on`` is the only required line for -working sessions, the rest are optional. - -By default, the session ID is passed in a cookie, so the client's browser must -have cookies enabled for your site. - -To set data for the current session, use -``cherrypy.session['fieldname'] = 'fieldvalue'``; -to get data use ``cherrypy.session.get('fieldname')``. - -================ -Locking sessions -================ - -By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means -the session is locked early and unlocked late. If you want to control when the -session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. -Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. -Regardless of which mode you use, the session is guaranteed to be unlocked when -the request is complete. - -================= -Expiring Sessions -================= - -You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. -Simply call that function at the point you want the session to expire, and it -will cause the session cookie to expire client-side. - -=========================== -Session Fixation Protection -=========================== - -If CherryPy receives, via a request cookie, a session id that it does not -recognize, it will reject that id and create a new one to return in the -response cookie. This `helps prevent session fixation attacks -`_. -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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py deleted file mode 100644 index 2d14230..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/static.py +++ /dev/null @@ -1,363 +0,0 @@ -try: - from io import UnsupportedOperation -except ImportError: - UnsupportedOperation = object() -import logging -import mimetypes -mimetypes.init() -mimetypes.types_map['.dwg']='image/x-dwg' -mimetypes.types_map['.ico']='image/x-icon' -mimetypes.types_map['.bz2']='application/x-bzip2' -mimetypes.types_map['.gz']='application/x-gzip' - -import os -import re -import stat -import time - -import cherrypy -from cherrypy._cpcompat import ntob, unquote -from cherrypy.lib import cptools, httputil, file_generator_limited - - -def serve_file(path, content_type=None, disposition=None, name=None, debug=False): - """Set status, headers, and body in order to serve the given path. - - The Content-Type header will be set to the content_type arg, if provided. - If not provided, the Content-Type will be guessed by the file extension - of the 'path' argument. - - If disposition is not None, the Content-Disposition header will be set - to "; 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/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py deleted file mode 100644 index 9a44464..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/lib/xmlrpcutil.py +++ /dev/null @@ -1,55 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py deleted file mode 100644 index f15b123..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/process/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py deleted file mode 100644 index ba618a0..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/process/plugins.py +++ /dev/null @@ -1,683 +0,0 @@ -"""Site services for use with a Web Site Process Bus.""" - -import os -import re -import signal as _signal -import sys -import time -import threading - -from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set - -# _module__file__base is used by Autoreload to make -# absolute any filenames retrieved from sys.modules which are not -# already absolute paths. This is to work around Python's quirk -# of importing the startup script and using a relative filename -# for it in sys.modules. -# -# Autoreload examines sys.modules afresh every time it runs. If an application -# changes the current directory by executing os.chdir(), then the next time -# Autoreload runs, it will not be able to find any filenames which are -# not absolute paths, because the current directory is not the same as when the -# module was first imported. Autoreload will then wrongly conclude the file has -# "changed", and initiate the shutdown/re-exec sequence. -# See ticket #917. -# For this workaround to have a decent probability of success, this module -# needs to be imported as early as possible, before the app has much chance -# to change the working directory. -_module__file__base = os.getcwd() - - -class SimplePlugin(object): - """Plugin base class which auto-subscribes methods for known channels.""" - - bus = None - """A :class:`Bus `, 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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py deleted file mode 100644 index fa714d6..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/process/servers.py +++ /dev/null @@ -1,427 +0,0 @@ -""" -Starting in CherryPy 3.1, cherrypy.server is implemented as an -:ref:`Engine Plugin`. 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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py deleted file mode 100644 index 83f99a5..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/process/win32.py +++ /dev/null @@ -1,174 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py deleted file mode 100644 index 6ef768d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/process/wspbus.py +++ /dev/null @@ -1,432 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py deleted file mode 100644 index 00964ac..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/scaffold/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -""", 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py deleted file mode 100644 index 7703607..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Regression test suite for CherryPy. - -Run 'nosetests -s test/' to exercise all tests. - -The '-s' flag instructs nose to output stdout messages, wihch is crucial to -the 'interactive' mode of webtest.py. If you run these tests without the '-s' -flag, don't be surprised if the test seems to hang: it's waiting for your -interactive input. -""" - -import os -import sys - -def newexit(): - os._exit(1) - -def setup(): - # We want to monkey patch sys.exit so that we can get some - # information about where exit is being called. - newexit._old = sys.exit - sys.exit = newexit - -def teardown(): - try: - sys.exit = sys.exit._old - except AttributeError: - sys.exit = sys._exit diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py deleted file mode 100644 index 5bcbc1e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_decorators.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test module for the @-decorator syntax, which is version-specific""" - -from cherrypy import expose, tools -from cherrypy._cpcompat import ntob - - -class ExposeExamples(object): - - @expose - def no_call(self): - return "Mr E. R. Bradshaw" - - @expose() - def call_empty(self): - return "Mrs. B.J. Smegma" - - @expose("call_alias") - def nesbitt(self): - return "Mr Nesbitt" - - @expose(["alias1", "alias2"]) - def andrews(self): - return "Mr Ken Andrews" - - @expose(alias="alias3") - def watson(self): - return "Mr. and Mrs. Watson" - - -class ToolExamples(object): - - @expose - @tools.response_headers(headers=[('Content-Type', 'application/data')]) - def blah(self): - yield ntob("blah") - # This is here to demonstrate that _cp_config = {...} overwrites - # the _cp_config attribute added by the Tool decorator. You have - # to write _cp_config[k] = v or _cp_config.update(...) instead. - blah._cp_config['response.stream'] = True - - diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py deleted file mode 100644 index 3f8f196..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/_test_states_demo.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import sys -import time -starttime = time.time() - -import cherrypy - - -class Root: - - def index(self): - return "Hello World" - index.exposed = True - - def mtimes(self): - return repr(cherrypy.engine.publish("Autoreloader", "mtimes")) - mtimes.exposed = True - - def pid(self): - return str(os.getpid()) - pid.exposed = True - - def start(self): - return repr(starttime) - start.exposed = True - - def exit(self): - # This handler might be called before the engine is STARTED if an - # HTTP worker thread handles it before the HTTP server returns - # control to engine.start. We avoid that race condition here - # by waiting for the Bus to be STARTED. - cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) - cherrypy.engine.exit() - exit.exposed = True - - -def unsub_sig(): - cherrypy.log("unsubsig: %s" % cherrypy.config.get('unsubsig', False)) - if cherrypy.config.get('unsubsig', False): - cherrypy.log("Unsubscribing the default cherrypy signal handler") - cherrypy.engine.signal_handler.unsubscribe() - try: - from signal import signal, SIGTERM - except ImportError: - pass - else: - def old_term_handler(signum=None, frame=None): - cherrypy.log("I am an old SIGTERM handler.") - sys.exit(0) - cherrypy.log("Subscribing the new one.") - signal(SIGTERM, old_term_handler) -cherrypy.engine.subscribe('start', unsub_sig, priority=100) - - -def starterror(): - if cherrypy.config.get('starterror', False): - zerodiv = 1 / 0 -cherrypy.engine.subscribe('start', starterror, priority=6) - -def log_test_case_name(): - if cherrypy.config.get('test_case_name', False): - cherrypy.log("STARTED FROM: %s" % cherrypy.config.get('test_case_name')) -cherrypy.engine.subscribe('start', log_test_case_name, priority=6) - - -cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py deleted file mode 100644 index bd5deb6..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/benchmark.py +++ /dev/null @@ -1,409 +0,0 @@ -"""CherryPy Benchmark Tool - - Usage: - benchmark.py --null --notests --help --cpmodpy --modpython --ab=path --apache=path - - --null: use a null Request object (to bench the HTTP server only) - --notests: start the server but do not run the tests; this allows - you to check the tested pages with a browser - --help: show this help message - --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) - --modpython: run tests via apache on 54583 (with modpython_gateway) - --ab=path: Use the ab script/executable at 'path' (see below) - --apache=path: Use the apache script/exe at 'path' (see below) - - To run the benchmarks, the Apache Benchmark tool "ab" must either be on - your system path, or specified via the --ab=path option. - - To run the modpython tests, the "apache" executable or script must be - on your system path, or provided via the --apache=path option. On some - platforms, "apache" may be called "apachectl" or "apache2ctl"--create - a symlink to them if needed. -""" - -import getopt -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -import re -import sys -import time -import traceback - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy import _cperror, _cpmodpy -from cherrypy.lib import httputil - - -AB_PATH = "" -APACHE_PATH = "apache" -SCRIPT_NAME = "/cpbench/users/rdelon/apps/blog" - -__all__ = ['ABSession', 'Root', 'print_report', - 'run_standard_benchmarks', 'safe_threads', - 'size_report', 'startup', 'thread_report', - ] - -size_cache = {} - -class Root: - - def index(self): - return """ - - 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py deleted file mode 100644 index 32a7dee..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/checkerdemo.py +++ /dev/null @@ -1,47 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py deleted file mode 100644 index 22b8ccc..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/helper.py +++ /dev/null @@ -1,493 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py deleted file mode 100644 index 3c6f114..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/logtest.py +++ /dev/null @@ -1,188 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py deleted file mode 100644 index 95acf14..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfastcgi.py +++ /dev/null @@ -1,135 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py deleted file mode 100644 index 736aa4c..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modfcgid.py +++ /dev/null @@ -1,125 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py deleted file mode 100644 index 519571f..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modpy.py +++ /dev/null @@ -1,163 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py deleted file mode 100644 index 309a541..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/modwsgi.py +++ /dev/null @@ -1,148 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py deleted file mode 100644 index 342e5b5..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/sessiondemo.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py deleted file mode 100644 index 3a9781d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_basic.py +++ /dev/null @@ -1,79 +0,0 @@ -# This file is part of CherryPy -# -*- 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py deleted file mode 100644 index 1960fa8..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_auth_digest.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py deleted file mode 100644 index 51c1022..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_bus.py +++ /dev/null @@ -1,263 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py deleted file mode 100644 index c210e6e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_caching.py +++ /dev/null @@ -1,328 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py deleted file mode 100644 index b1ef6a3..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config.py +++ /dev/null @@ -1,256 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py deleted file mode 100644 index 0b9718d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_config_server.py +++ /dev/null @@ -1,121 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py deleted file mode 100644 index 1346f59..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_conn.py +++ /dev/null @@ -1,734 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py deleted file mode 100644 index b4e830d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_core.py +++ /dev/null @@ -1,688 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py deleted file mode 100644 index 0395b7b..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_dynamicobjectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py deleted file mode 100644 index 2d0ce76..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_encoding.py +++ /dev/null @@ -1,363 +0,0 @@ - -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py deleted file mode 100644 index aec1693..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_etags.py +++ /dev/null @@ -1,83 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py deleted file mode 100644 index 639c6c4..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_http.py +++ /dev/null @@ -1,212 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py deleted file mode 100644 index 9d0eecb..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httpauth.py +++ /dev/null @@ -1,151 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py deleted file mode 100644 index 5dc40fd..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_httplib.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py deleted file mode 100644 index a02c076..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_json.py +++ /dev/null @@ -1,79 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py deleted file mode 100644 index 7d506e8..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_logging.py +++ /dev/null @@ -1,157 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py deleted file mode 100644 index 1605991..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_mime.py +++ /dev/null @@ -1,128 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py deleted file mode 100644 index 1dd1429..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_misc_tools.py +++ /dev/null @@ -1,207 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py deleted file mode 100644 index 8dcf2d3..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_objectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py deleted file mode 100644 index 2fbb619..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_proxy.py +++ /dev/null @@ -1,129 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py deleted file mode 100644 index 279935e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_refleaks.py +++ /dev/null @@ -1,59 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py deleted file mode 100644 index 26eea56..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_request_obj.py +++ /dev/null @@ -1,737 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py deleted file mode 100644 index a8062f8..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_routes.py +++ /dev/null @@ -1,69 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py deleted file mode 100644 index 9143a1d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_session.py +++ /dev/null @@ -1,464 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py deleted file mode 100644 index ab1fe51..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_sessionauthenticate.py +++ /dev/null @@ -1,62 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py deleted file mode 100644 index 6322687..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_states.py +++ /dev/null @@ -1,439 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py deleted file mode 100644 index 871420b..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_static.py +++ /dev/null @@ -1,300 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py deleted file mode 100644 index 02bacda..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tools.py +++ /dev/null @@ -1,399 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py deleted file mode 100644 index aab2786..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_tutorials.py +++ /dev/null @@ -1,201 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py deleted file mode 100644 index dbd2dbc..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_virtualhost.py +++ /dev/null @@ -1,107 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py deleted file mode 100644 index e3c6ce6..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_ns.py +++ /dev/null @@ -1,91 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py deleted file mode 100644 index abb1a91..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgi_vhost.py +++ /dev/null @@ -1,36 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py deleted file mode 100644 index d4b8b79..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_wsgiapps.py +++ /dev/null @@ -1,118 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py deleted file mode 100644 index f7a6927..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/test_xmlrpc.py +++ /dev/null @@ -1,179 +0,0 @@ -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/libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py deleted file mode 100644 index 50cfbad..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/test/webtest.py +++ /dev/null @@ -1,575 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py deleted file mode 100644 index c4e2c55..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -# 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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py deleted file mode 100644 index c43feb4..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/bonus-sqlobject.py +++ /dev/null @@ -1,168 +0,0 @@ -''' -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py deleted file mode 100644 index ef94760..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut01_helloworld.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Tutorial - Hello World - -The most basic (working) CherryPy application possible. -""" - -# Import CherryPy global namespace -import cherrypy - -class HelloWorld: - """ Sample request handler class. """ - - def index(self): - # CherryPy will call this method for the root URI ("/") and send - # its return value to the client. Because this is tutorial - # lesson number 01, we'll just send something really simple. - # How about... - return "Hello world!" - - # Expose the index method through the web. CherryPy will never - # publish methods that don't have the exposed attribute set to True. - 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(HelloWorld(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py deleted file mode 100644 index 600fca3..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut02_expose_methods.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Tutorial - Multiple methods - -This tutorial shows you how to link to other methods of your request -handler. -""" - -import cherrypy - -class HelloWorld: - - def index(self): - # Let's link to another method here. - return 'We have an important 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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py deleted file mode 100644 index 283477d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut03_get_and_post.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py deleted file mode 100644 index b4d820e..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut04_complex_site.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py deleted file mode 100644 index 3d4ec9b..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut05_derived_objects.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py deleted file mode 100644 index fe24f38..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut06_default_method.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py deleted file mode 100644 index 4b1386b..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut07_sessions.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py deleted file mode 100644 index a6fbdc2..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut08_generators_and_yield.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py deleted file mode 100644 index 4c8e581..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut09_files.py +++ /dev/null @@ -1,107 +0,0 @@ -""" - -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/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py deleted file mode 100644 index dfa5733..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/tutorial/tut10_http_errors.py +++ /dev/null @@ -1,81 +0,0 @@ -""" - -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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py deleted file mode 100644 index ee6190f..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -__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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py deleted file mode 100644 index 03bf05d..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_builtin.py +++ /dev/null @@ -1,91 +0,0 @@ -"""A library for integrating Python's builtin ``ssl`` library with CherryPy. - -The ssl module must be importable for SSL functionality. - -To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of -``BuiltinSSLAdapter``. -""" - -try: - import ssl -except ImportError: - ssl = None - -try: - from _pyio import DEFAULT_BUFFER_SIZE -except ImportError: - try: - from io import DEFAULT_BUFFER_SIZE - except ImportError: - DEFAULT_BUFFER_SIZE = -1 - -import sys - -from cherrypy import wsgiserver - - -class BuiltinSSLAdapter(wsgiserver.SSLAdapter): - """A wrapper for integrating Python's builtin ssl module with CherryPy.""" - - certificate = None - """The filename of the server SSL certificate.""" - - private_key = None - """The filename of the server's private key file.""" - - def __init__(self, certificate, private_key, certificate_chain=None): - if ssl is None: - raise ImportError("You must install the ssl module to use HTTPS.") - self.certificate = certificate - self.private_key = private_key - self.certificate_chain = certificate_chain - - def bind(self, sock): - """Wrap and return the given socket.""" - return sock - - def wrap(self, sock): - """Wrap and return the given socket, plus WSGI environ entries.""" - try: - s = ssl.wrap_socket(sock, do_handshake_on_connect=True, - server_side=True, certfile=self.certificate, - keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) - except ssl.SSLError: - e = sys.exc_info()[1] - if e.errno == ssl.SSL_ERROR_EOF: - # This is almost certainly due to the cherrypy engine - # 'pinging' the socket to assert it's connectable; - # the 'ping' isn't SSL. - return None, {} - elif e.errno == ssl.SSL_ERROR_SSL: - if e.args[1].endswith('http request'): - # The client is speaking HTTP to an HTTPS server. - raise wsgiserver.NoSSLError - elif e.args[1].endswith('unknown protocol'): - # The client is speaking some non-HTTP protocol. - # Drop the conn. - return None, {} - raise - return s, self.get_environ(s) - - # TODO: fill this out more with mod ssl env - def get_environ(self, sock): - """Create WSGI environ entries to be merged into each request.""" - cipher = sock.cipher() - ssl_environ = { - "wsgi.url_scheme": "https", - "HTTPS": "on", - 'SSL_PROTOCOL': cipher[1], - 'SSL_CIPHER': cipher[0] -## SSL_VERSION_INTERFACE string The mod_ssl program version -## SSL_VERSION_LIBRARY string The OpenSSL program version - } - return ssl_environ - - if sys.version_info >= (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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py deleted file mode 100644 index f3d9bf5..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/ssl_pyopenssl.py +++ /dev/null @@ -1,256 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py b/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py deleted file mode 100644 index b6bd499..0000000 --- a/libs/CherryPy-3.2.2/build/lib/cherrypy/wsgiserver/wsgiserver2.py +++ /dev/null @@ -1,2322 +0,0 @@ -"""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/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd b/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd deleted file mode 100644 index 17c6157..0000000 --- a/libs/CherryPy-3.2.2/build/scripts-2.7/cherryd +++ /dev/null @@ -1,109 +0,0 @@ -#!c:\Python27\python.exe -"""The CherryPy daemon.""" - -import sys - -import cherrypy -from cherrypy.process import plugins, servers -from cherrypy import Application - -def start(configfiles=None, daemonize=False, environment=None, - fastcgi=False, scgi=False, pidfile=None, imports=None, - cgi=False): - """Subscribe all engine plugins and start the engine.""" - sys.path = [''] + sys.path - for i in imports or []: - exec("import %s" % i) - - for c in configfiles or []: - cherrypy.config.update(c) - # If there's only one app mounted, merge config into it. - if len(cherrypy.tree.apps) == 1: - for app in cherrypy.tree.apps.values(): - if isinstance(app, Application): - app.merge(c) - - engine = cherrypy.engine - - if environment is not None: - cherrypy.config.update({'environment': environment}) - - # Only daemonize if asked to. - if daemonize: - # Don't print anything to stdout/sterr. - cherrypy.config.update({'log.screen': False}) - plugins.Daemonizer(engine).subscribe() - - if pidfile: - plugins.PIDFile(engine, pidfile).subscribe() - - if hasattr(engine, "signal_handler"): - engine.signal_handler.subscribe() - if hasattr(engine, "console_control_handler"): - engine.console_control_handler.subscribe() - - if (fastcgi and (scgi or cgi)) or (scgi and cgi): - cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " - "scgi options.", 'ENGINE') - sys.exit(1) - elif fastcgi or scgi or cgi: - # Turn off autoreload when using *cgi. - cherrypy.config.update({'engine.autoreload_on': False}) - # Turn off the default HTTP server (which is subscribed by default). - cherrypy.server.unsubscribe() - - addr = cherrypy.server.bind_addr - if fastcgi: - f = servers.FlupFCGIServer(application=cherrypy.tree, - bindAddress=addr) - elif scgi: - f = servers.FlupSCGIServer(application=cherrypy.tree, - bindAddress=addr) - else: - f = servers.FlupCGIServer(application=cherrypy.tree, - bindAddress=addr) - s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) - s.subscribe() - - # Always start the engine; this will start all other services - try: - engine.start() - except: - # Assume the error has been logged already via bus.log. - sys.exit(1) - else: - engine.block() - - -if __name__ == '__main__': - from optparse import OptionParser - - p = OptionParser() - p.add_option('-c', '--config', action="append", dest='config', - help="specify config file(s)") - p.add_option('-d', action="store_true", dest='daemonize', - help="run the server as a daemon") - p.add_option('-e', '--environment', dest='environment', default=None, - help="apply the given config environment") - p.add_option('-f', action="store_true", dest='fastcgi', - help="start a fastcgi server instead of the default HTTP server") - p.add_option('-s', action="store_true", dest='scgi', - help="start a scgi server instead of the default HTTP server") - p.add_option('-x', action="store_true", dest='cgi', - help="start a cgi server instead of the default HTTP server") - p.add_option('-i', '--import', action="append", dest='imports', - help="specify modules to import") - p.add_option('-p', '--pidfile', dest='pidfile', default=None, - help="store the process id in the given file") - p.add_option('-P', '--Path', action="append", dest='Path', - help="add the given paths to sys.path") - options, args = p.parse_args() - - if options.Path: - for p in options.Path: - sys.path.insert(0, p) - - start(options.config, options.daemonize, - options.environment, options.fastcgi, options.scgi, - options.pidfile, options.imports, options.cgi) - diff --git a/libs/CherryPy-3.2.2/cherrypy/LICENSE.txt b/libs/CherryPy-3.2.2/cherrypy/LICENSE.txt deleted file mode 100644 index 8db13fb..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/LICENSE.txt +++ /dev/null @@ -1,25 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/__init__.py b/libs/CherryPy-3.2.2/cherrypy/__init__.py deleted file mode 100644 index 41e3898..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/__init__.py +++ /dev/null @@ -1,624 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py b/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py deleted file mode 100644 index 7ccfd89..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpchecker.py +++ /dev/null @@ -1,327 +0,0 @@ -import os -import warnings - -import cherrypy -from cherrypy._cpcompat import iteritems, copykeys, builtins - - -class Checker(object): - """A checker for CherryPy sites and their mounted applications. - - When this object is called at engine startup, it executes each - of its own methods whose names start with ``check_``. If you wish - to disable selected checks, simply add a line in your global - config which sets the appropriate method to False:: - - [global] - checker.check_skipped_app_config = False - - You may also dynamically add or replace ``check_*`` methods in this way. - """ - - on = True - """If True (the default), run all checks; if False, turn off all checks.""" - - - def __init__(self): - self._populate_known_types() - - def __call__(self): - """Run all check_* methods.""" - if self.on: - oldformatwarning = warnings.formatwarning - warnings.formatwarning = self.formatwarning - try: - for name in dir(self): - if name.startswith("check_"): - method = getattr(self, name) - if method and hasattr(method, '__call__'): - method() - finally: - warnings.formatwarning = oldformatwarning - - def formatwarning(self, message, category, filename, lineno, line=None): - """Function to format a warning.""" - return "CherryPy Checker:\n%s\n\n" % message - - # This value should be set inside _cpconfig. - global_config_contained_paths = False - - def check_app_config_entries_dont_start_with_script_name(self): - """Check for Application config with sections that repeat script_name.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - if sn == '': - continue - sn_atoms = sn.strip("/").split("/") - for key in app.config.keys(): - key_atoms = key.strip("/").split("/") - if key_atoms[:len(sn_atoms)] == sn_atoms: - warnings.warn( - "The application mounted at %r has config " \ - "entries that start with its script name: %r" % (sn, key)) - - def check_site_config_entries_in_app_config(self): - """Check for mounted Applications that have site-scoped config.""" - for sn, app in iteritems(cherrypy.tree.apps): - if not isinstance(app, cherrypy.Application): - continue - - msg = [] - for section, entries in iteritems(app.config): - if section.startswith('/'): - for key, value in iteritems(entries): - for n in ("engine.", "server.", "tree.", "checker."): - if key.startswith(n): - msg.append("[%s] %s = %s" % (section, key, value)) - if msg: - msg.insert(0, - "The application mounted at %r contains the following " - "config entries, which are only allowed in site-wide " - "config. Move them to a [global] section and pass them " - "to cherrypy.config.update() instead of tree.mount()." % sn) - warnings.warn(os.linesep.join(msg)) - - def check_skipped_app_config(self): - """Check for mounted Applications that have no config.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - msg = "The Application mounted at %r has an empty config." % sn - if self.global_config_contained_paths: - msg += (" It looks like the config you passed to " - "cherrypy.config.update() contains application-" - "specific sections. You must explicitly pass " - "application config via " - "cherrypy.tree.mount(..., config=app_config)") - warnings.warn(msg) - return - - def check_app_config_brackets(self): - """Check for Application config with extraneous brackets in section names.""" - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - if not app.config: - continue - for key in app.config.keys(): - if key.startswith("[") or key.endswith("]"): - warnings.warn( - "The application mounted at %r has config " \ - "section names with extraneous brackets: %r. " - "Config *files* need brackets; config *dicts* " - "(e.g. passed to tree.mount) do not." % (sn, key)) - - def check_static_paths(self): - """Check Application config for incorrect static paths.""" - # Use the dummy Request object in the main thread. - request = cherrypy.request - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - request.app = app - for section in app.config: - # get_resource will populate request.config - request.get_resource(section + "/dummy.html") - conf = request.config.get - - if conf("tools.staticdir.on", False): - msg = "" - root = conf("tools.staticdir.root") - dir = conf("tools.staticdir.dir") - if dir is None: - msg = "tools.staticdir.dir is not set." - else: - fulldir = "" - if os.path.isabs(dir): - fulldir = dir - if root: - msg = ("dir is an absolute path, even " - "though a root is provided.") - testdir = os.path.join(root, dir[1:]) - if os.path.exists(testdir): - msg += ("\nIf you meant to serve the " - "filesystem folder at %r, remove " - "the leading slash from dir." % testdir) - else: - if not root: - msg = "dir is a relative path and no root provided." - else: - fulldir = os.path.join(root, dir) - if not os.path.isabs(fulldir): - msg = "%r is not an absolute path." % fulldir - - if fulldir and not os.path.exists(fulldir): - if msg: - msg += "\n" - msg += ("%r (root + dir) is not an existing " - "filesystem path." % fulldir) - - if msg: - warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" - % (msg, section, root, dir)) - - - # -------------------------- Compatibility -------------------------- # - - obsolete = { - 'server.default_content_type': 'tools.response_headers.headers', - 'log_access_file': 'log.access_file', - 'log_config_options': None, - 'log_file': 'log.error_file', - 'log_file_not_found': None, - 'log_request_headers': 'tools.log_headers.on', - 'log_to_screen': 'log.screen', - 'show_tracebacks': 'request.show_tracebacks', - 'throw_errors': 'request.throw_errors', - 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' - 'cherrypy.Application(Root())))'), - } - - deprecated = {} - - def _compat(self, config): - """Process config and warn on each obsolete or deprecated entry.""" - for section, conf in config.items(): - if isinstance(conf, dict): - for k, v in conf.items(): - if k in self.obsolete: - warnings.warn("%r is obsolete. Use %r instead.\n" - "section: [%s]" % - (k, self.obsolete[k], section)) - elif k in self.deprecated: - warnings.warn("%r is deprecated. Use %r instead.\n" - "section: [%s]" % - (k, self.deprecated[k], section)) - else: - if section in self.obsolete: - warnings.warn("%r is obsolete. Use %r instead." - % (section, self.obsolete[section])) - elif section in self.deprecated: - warnings.warn("%r is deprecated. Use %r instead." - % (section, self.deprecated[section])) - - def check_compatibility(self): - """Process config and warn on each obsolete or deprecated entry.""" - self._compat(cherrypy.config) - for sn, app in cherrypy.tree.apps.items(): - if not isinstance(app, cherrypy.Application): - continue - self._compat(app.config) - - - # ------------------------ Known Namespaces ------------------------ # - - extra_config_namespaces = [] - - def _known_ns(self, app): - ns = ["wsgi"] - ns.extend(copykeys(app.toolboxes)) - ns.extend(copykeys(app.namespaces)) - ns.extend(copykeys(app.request_class.namespaces)) - ns.extend(copykeys(cherrypy.config.namespaces)) - ns += self.extra_config_namespaces - - for section, conf in app.config.items(): - is_path_section = section.startswith("/") - if is_path_section and isinstance(conf, dict): - for k, v in conf.items(): - atoms = k.split(".") - if len(atoms) > 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/libs/CherryPy-3.2.2/cherrypy/_cpcompat.py b/libs/CherryPy-3.2.2/cherrypy/_cpcompat.py deleted file mode 100644 index ed24c1a..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpcompat.py +++ /dev/null @@ -1,318 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpconfig.py b/libs/CherryPy-3.2.2/cherrypy/_cpconfig.py deleted file mode 100644 index 7b4c6a4..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpconfig.py +++ /dev/null @@ -1,295 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py b/libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py deleted file mode 100644 index d614e08..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpdispatch.py +++ /dev/null @@ -1,636 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cperror.py b/libs/CherryPy-3.2.2/cherrypy/_cperror.py deleted file mode 100644 index 76a409f..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cperror.py +++ /dev/null @@ -1,556 +0,0 @@ -"""Exception classes for CherryPy. - -CherryPy provides (and uses) exceptions for declaring that the HTTP response -should be a status other than the default "200 OK". You can ``raise`` them like -normal Python exceptions. You can also call them and they will raise themselves; -this means you can set an :class:`HTTPError` -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/libs/CherryPy-3.2.2/cherrypy/_cplogging.py b/libs/CherryPy-3.2.2/cherrypy/_cplogging.py deleted file mode 100644 index e10c942..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cplogging.py +++ /dev/null @@ -1,440 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py b/libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py deleted file mode 100644 index 76ef6ea..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpmodpy.py +++ /dev/null @@ -1,344 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py b/libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py deleted file mode 100644 index 57f715a..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpnative_server.py +++ /dev/null @@ -1,149 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py b/libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py deleted file mode 100644 index 5d72c85..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpreqbody.py +++ /dev/null @@ -1,965 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cprequest.py b/libs/CherryPy-3.2.2/cherrypy/_cprequest.py deleted file mode 100644 index 5890c72..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cprequest.py +++ /dev/null @@ -1,956 +0,0 @@ - -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/libs/CherryPy-3.2.2/cherrypy/_cpserver.py b/libs/CherryPy-3.2.2/cherrypy/_cpserver.py deleted file mode 100644 index 2eecd6e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpserver.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Manage HTTP servers with CherryPy.""" - -import warnings - -import cherrypy -from cherrypy.lib import attributes -from cherrypy._cpcompat import basestring, py3k - -# We import * because we want to export check_port -# et al as attributes of this module. -from cherrypy.process.servers import * - - -class Server(ServerAdapter): - """An adapter for an HTTP server. - - You can set attributes (like socket_host and socket_port) - on *this* object (which is probably cherrypy.server), and call - quickstart. For example:: - - cherrypy.server.socket_port = 80 - cherrypy.quickstart() - """ - - socket_port = 8080 - """The TCP port on which to listen for connections.""" - - _socket_host = '127.0.0.1' - def _get_socket_host(self): - return self._socket_host - def _set_socket_host(self, value): - if value == '': - raise ValueError("The empty string ('') is not an allowed value. " - "Use '0.0.0.0' instead to listen on all active " - "interfaces (INADDR_ANY).") - self._socket_host = value - socket_host = property(_get_socket_host, _set_socket_host, - doc="""The hostname or IP address on which to listen for connections. - - 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.""") - - socket_file = None - """If given, the name of the UNIX socket to use instead of TCP/IP. - - When this option is not None, the `socket_host` and `socket_port` options - are ignored.""" - - socket_queue_size = 5 - """The 'backlog' argument to socket.listen(); specifies the maximum number - of queued connections (default 5).""" - - socket_timeout = 10 - """The timeout in seconds for accepted connections (default 10).""" - - shutdown_timeout = 5 - """The time to wait for HTTP worker threads to clean up.""" - - protocol_version = 'HTTP/1.1' - """The version string to write in the Status-Line of all HTTP responses, - for example, "HTTP/1.1" (the default). Depending on the HTTP server used, - this should also limit the supported features used in the response.""" - - thread_pool = 10 - """The number of worker threads to start up in the pool.""" - - thread_pool_max = -1 - """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" - - max_request_header_size = 500 * 1024 - """The maximum number of bytes allowable in the request headers. If exceeded, - the HTTP server should return "413 Request Entity Too Large".""" - - max_request_body_size = 100 * 1024 * 1024 - """The maximum number of bytes allowable in the request body. If exceeded, - the HTTP server should return "413 Request Entity Too Large".""" - - instance = None - """If not None, this should be an HTTP server instance (such as - CPWSGIServer) which cherrypy.server will control. Use this when you need - more control over object instantiation than is available in the various - configuration options.""" - - ssl_context = None - """When using PyOpenSSL, an instance of SSL.Context.""" - - ssl_certificate = None - """The filename of the SSL certificate to use.""" - - ssl_certificate_chain = None - """When using PyOpenSSL, the certificate chain to pass to - Context.load_verify_locations.""" - - ssl_private_key = None - """The filename of the private key to use with SSL.""" - - if py3k: - ssl_module = 'builtin' - """The name of a registered SSL adaptation module to use with the builtin - WSGI server. Builtin options are: 'builtin' (to use the SSL library built - into recent versions of Python). You may also register your - own classes in the wsgiserver.ssl_adapters dict.""" - else: - ssl_module = 'pyopenssl' - """The name of a registered SSL adaptation module to use with the builtin - WSGI server. Builtin options are 'builtin' (to use the SSL library built - into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL - project, which you must install separately). You may also register your - own classes in the wsgiserver.ssl_adapters dict.""" - - statistics = False - """Turns statistics-gathering on or off for aware HTTP servers.""" - - nodelay = True - """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" - - wsgi_version = (1, 0) - """The WSGI version tuple to use with the builtin WSGI server. - The provided options are (1, 0) [which includes support for PEP 3333, - which declares it covers WSGI version 1.0.1 but still mandates the - wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. - You may create and register your own experimental versions of the WSGI - protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" - - def __init__(self): - self.bus = cherrypy.engine - self.httpserver = None - self.interrupt = None - self.running = False - - def httpserver_from_self(self, httpserver=None): - """Return a (httpserver, bind_addr) pair based on self attributes.""" - if httpserver is None: - httpserver = self.instance - if httpserver is None: - from cherrypy import _cpwsgi_server - httpserver = _cpwsgi_server.CPWSGIServer(self) - if isinstance(httpserver, basestring): - # Is anyone using this? Can I add an arg? - httpserver = attributes(httpserver)(self) - return httpserver, self.bind_addr - - def start(self): - """Start the HTTP server.""" - if not self.httpserver: - self.httpserver, self.bind_addr = self.httpserver_from_self() - ServerAdapter.start(self) - start.priority = 75 - - def _get_bind_addr(self): - if self.socket_file: - return self.socket_file - if self.socket_host is None and self.socket_port is None: - return None - return (self.socket_host, self.socket_port) - def _set_bind_addr(self, value): - if value is None: - self.socket_file = None - self.socket_host = None - self.socket_port = None - elif isinstance(value, basestring): - self.socket_file = value - self.socket_host = None - self.socket_port = None - else: - try: - self.socket_host, self.socket_port = value - self.socket_file = None - except ValueError: - raise ValueError("bind_addr must be a (host, port) tuple " - "(for TCP sockets) or a string (for Unix " - "domain sockets), not %r" % value) - bind_addr = property(_get_bind_addr, _set_bind_addr, - doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') - - def base(self): - """Return the base (scheme://host[:port] or sock file) for this server.""" - if self.socket_file: - return self.socket_file - - host = self.socket_host - if host in ('0.0.0.0', '::'): - # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. - # Look up the host name, which should be the - # safest thing to spit out in a URL. - import socket - host = socket.gethostname() - - port = self.socket_port - - if self.ssl_certificate: - scheme = "https" - if port != 443: - host += ":%s" % port - else: - scheme = "http" - if port != 80: - host += ":%s" % port - - return "%s://%s" % (scheme, host) - diff --git a/libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py b/libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py deleted file mode 100644 index 34c17ac..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpthreadinglocal.py +++ /dev/null @@ -1,239 +0,0 @@ -# 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/libs/CherryPy-3.2.2/cherrypy/_cptools.py b/libs/CherryPy-3.2.2/cherrypy/_cptools.py deleted file mode 100644 index 22316b3..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cptools.py +++ /dev/null @@ -1,510 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cptree.py b/libs/CherryPy-3.2.2/cherrypy/_cptree.py deleted file mode 100644 index 3aa4b9e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cptree.py +++ /dev/null @@ -1,290 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py b/libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py deleted file mode 100644 index 91cd044..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpwsgi.py +++ /dev/null @@ -1,408 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py b/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py deleted file mode 100644 index 21af513..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/_cpwsgi_server.py +++ /dev/null @@ -1,63 +0,0 @@ -"""WSGI server interface (see PEP 333). This adds some CP-specific bits to -the framework-agnostic wsgiserver package. -""" -import sys - -import cherrypy -from cherrypy import wsgiserver - - -class CPWSGIServer(wsgiserver.CherryPyWSGIServer): - """Wrapper for wsgiserver.CherryPyWSGIServer. - - 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 set our own mount points from cherrypy.tree - and apply some attributes from config -> 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/libs/CherryPy-3.2.2/cherrypy/cherryd b/libs/CherryPy-3.2.2/cherrypy/cherryd deleted file mode 100644 index adb2a02..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/cherryd +++ /dev/null @@ -1,109 +0,0 @@ -#! /usr/bin/env python -"""The CherryPy daemon.""" - -import sys - -import cherrypy -from cherrypy.process import plugins, servers -from cherrypy import Application - -def start(configfiles=None, daemonize=False, environment=None, - fastcgi=False, scgi=False, pidfile=None, imports=None, - cgi=False): - """Subscribe all engine plugins and start the engine.""" - sys.path = [''] + sys.path - for i in imports or []: - exec("import %s" % i) - - for c in configfiles or []: - cherrypy.config.update(c) - # If there's only one app mounted, merge config into it. - if len(cherrypy.tree.apps) == 1: - for app in cherrypy.tree.apps.values(): - if isinstance(app, Application): - app.merge(c) - - engine = cherrypy.engine - - if environment is not None: - cherrypy.config.update({'environment': environment}) - - # Only daemonize if asked to. - if daemonize: - # Don't print anything to stdout/sterr. - cherrypy.config.update({'log.screen': False}) - plugins.Daemonizer(engine).subscribe() - - if pidfile: - plugins.PIDFile(engine, pidfile).subscribe() - - if hasattr(engine, "signal_handler"): - engine.signal_handler.subscribe() - if hasattr(engine, "console_control_handler"): - engine.console_control_handler.subscribe() - - if (fastcgi and (scgi or cgi)) or (scgi and cgi): - cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " - "scgi options.", 'ENGINE') - sys.exit(1) - elif fastcgi or scgi or cgi: - # Turn off autoreload when using *cgi. - cherrypy.config.update({'engine.autoreload_on': False}) - # Turn off the default HTTP server (which is subscribed by default). - cherrypy.server.unsubscribe() - - addr = cherrypy.server.bind_addr - if fastcgi: - f = servers.FlupFCGIServer(application=cherrypy.tree, - bindAddress=addr) - elif scgi: - f = servers.FlupSCGIServer(application=cherrypy.tree, - bindAddress=addr) - else: - f = servers.FlupCGIServer(application=cherrypy.tree, - bindAddress=addr) - s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) - s.subscribe() - - # Always start the engine; this will start all other services - try: - engine.start() - except: - # Assume the error has been logged already via bus.log. - sys.exit(1) - else: - engine.block() - - -if __name__ == '__main__': - from optparse import OptionParser - - p = OptionParser() - p.add_option('-c', '--config', action="append", dest='config', - help="specify config file(s)") - p.add_option('-d', action="store_true", dest='daemonize', - help="run the server as a daemon") - p.add_option('-e', '--environment', dest='environment', default=None, - help="apply the given config environment") - p.add_option('-f', action="store_true", dest='fastcgi', - help="start a fastcgi server instead of the default HTTP server") - p.add_option('-s', action="store_true", dest='scgi', - help="start a scgi server instead of the default HTTP server") - p.add_option('-x', action="store_true", dest='cgi', - help="start a cgi server instead of the default HTTP server") - p.add_option('-i', '--import', action="append", dest='imports', - help="specify modules to import") - p.add_option('-p', '--pidfile', dest='pidfile', default=None, - help="store the process id in the given file") - p.add_option('-P', '--Path', action="append", dest='Path', - help="add the given paths to sys.path") - options, args = p.parse_args() - - if options.Path: - for p in options.Path: - sys.path.insert(0, p) - - start(options.config, options.daemonize, - options.environment, options.fastcgi, options.scgi, - options.pidfile, options.imports, options.cgi) - diff --git a/libs/CherryPy-3.2.2/cherrypy/favicon.ico b/libs/CherryPy-3.2.2/cherrypy/favicon.ico deleted file mode 100644 index f0d7e61badad3f332cf1e663efb97c0b5be80f5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1406 zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K zC3>Y)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 diff --git a/libs/CherryPy-3.2.2/cherrypy/lib/__init__.py b/libs/CherryPy-3.2.2/cherrypy/lib/__init__.py deleted file mode 100644 index 3fc0ec5..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/auth.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth.py deleted file mode 100644 index 7d2f6dc..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/auth.py +++ /dev/null @@ -1,87 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py deleted file mode 100644 index 2c05e01..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/auth_basic.py +++ /dev/null @@ -1,87 +0,0 @@ -# This file is part of CherryPy -# -*- 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/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py b/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py deleted file mode 100644 index 67578e0..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/auth_digest.py +++ /dev/null @@ -1,365 +0,0 @@ -# This file is part of CherryPy -# -*- 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/libs/CherryPy-3.2.2/cherrypy/lib/caching.py b/libs/CherryPy-3.2.2/cherrypy/lib/caching.py deleted file mode 100644 index 435b9dc..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/caching.py +++ /dev/null @@ -1,465 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py b/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py deleted file mode 100644 index 9b701b5..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/covercp.py +++ /dev/null @@ -1,365 +0,0 @@ -"""Code-coverage tools for CherryPy. - -To use this module, or the coverage tools in the test suite, -you need to download 'coverage.py', either Gareth Rees' `original -implementation `_ -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/libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py b/libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py deleted file mode 100644 index 9be947f..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/cpstats.py +++ /dev/null @@ -1,662 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/cptools.py b/libs/CherryPy-3.2.2/cherrypy/lib/cptools.py deleted file mode 100644 index b426a3e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/cptools.py +++ /dev/null @@ -1,617 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py b/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py deleted file mode 100644 index 6459746..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/encoding.py +++ /dev/null @@ -1,388 +0,0 @@ -import struct -import time - -import cherrypy -from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr -from cherrypy.lib import file_generator -from cherrypy.lib import set_vary_header - - -def decode(encoding=None, default_encoding='utf-8'): - """Replace or extend the list of charsets used to decode a request entity. - - Either argument may be a single string or a list of strings. - - encoding - If not None, restricts the set of charsets attempted while decoding - a request entity to the given set (even if a different charset is given in - the Content-Type request header). - - default_encoding - Only in effect if the 'encoding' argument is not given. - If given, the set of charsets attempted while decoding a request entity is - *extended* with the given value(s). - - """ - body = cherrypy.request.body - if encoding is not None: - if not isinstance(encoding, list): - encoding = [encoding] - body.attempt_charsets = encoding - elif default_encoding: - if not isinstance(default_encoding, list): - default_encoding = [default_encoding] - body.attempt_charsets = body.attempt_charsets + default_encoding - - -class ResponseEncoder: - - default_encoding = 'utf-8' - failmsg = "Response body could not be encoded with %r." - encoding = None - errors = 'strict' - text_only = True - add_charset = True - debug = False - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - - self.attempted_charsets = set() - request = cherrypy.serving.request - if request.handler is not None: - # Replace request.handler with self - if self.debug: - cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') - self.oldhandler = request.handler - request.handler = self - - def encode_stream(self, encoding): - """Encode a streaming response body. - - Use a generator wrapper, and just pray it works as the stream is - being written out. - """ - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - - def encoder(body): - for chunk in body: - if isinstance(chunk, unicodestr): - chunk = chunk.encode(encoding, self.errors) - yield chunk - self.body = encoder(self.body) - return True - - def encode_string(self, encoding): - """Encode a buffered response body.""" - if encoding in self.attempted_charsets: - return False - self.attempted_charsets.add(encoding) - - try: - body = [] - for chunk in self.body: - if isinstance(chunk, unicodestr): - chunk = chunk.encode(encoding, self.errors) - body.append(chunk) - self.body = body - except (LookupError, UnicodeError): - return False - else: - return True - - def find_acceptable_charset(self): - request = cherrypy.serving.request - response = cherrypy.serving.response - - if self.debug: - cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') - if response.stream: - encoder = self.encode_stream - else: - encoder = self.encode_string - if "Content-Length" in response.headers: - # Delete Content-Length header so finalize() recalcs it. - # Encoded strings may be of different lengths from their - # unicode equivalents, and even from each other. For example: - # >>> 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/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py b/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py deleted file mode 100644 index 183148b..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/gctools.py +++ /dev/null @@ -1,214 +0,0 @@ -import gc -import inspect -import os -import sys -import time - -try: - import objgraph -except ImportError: - objgraph = None - -import cherrypy -from cherrypy import _cprequest, _cpwsgi -from cherrypy.process.plugins import SimplePlugin - - -class ReferrerTree(object): - """An object which gathers all referrers of an object to a given depth.""" - - peek_length = 40 - - def __init__(self, ignore=None, maxdepth=2, maxparents=10): - self.ignore = ignore or [] - self.ignore.append(inspect.currentframe().f_back) - self.maxdepth = maxdepth - self.maxparents = maxparents - - def ascend(self, obj, depth=1): - """Return a nested list containing referrers of the given object.""" - depth += 1 - parents = [] - - # Gather all referrers in one step to minimize - # cascading references due to repr() logic. - refs = gc.get_referrers(obj) - self.ignore.append(refs) - if len(refs) > 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/libs/CherryPy-3.2.2/cherrypy/lib/http.py b/libs/CherryPy-3.2.2/cherrypy/lib/http.py deleted file mode 100644 index 4661d69..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/http.py +++ /dev/null @@ -1,7 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py b/libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py deleted file mode 100644 index ad7c6eb..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/httpauth.py +++ /dev/null @@ -1,354 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/lib/httputil.py b/libs/CherryPy-3.2.2/cherrypy/lib/httputil.py deleted file mode 100644 index 5f77d54..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/httputil.py +++ /dev/null @@ -1,506 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py b/libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py deleted file mode 100644 index 2092579..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/jsontools.py +++ /dev/null @@ -1,87 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/lib/profiler.py b/libs/CherryPy-3.2.2/cherrypy/lib/profiler.py deleted file mode 100644 index 785d58a..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/profiler.py +++ /dev/null @@ -1,208 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py b/libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py deleted file mode 100644 index ba8ff51..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/reprconf.py +++ /dev/null @@ -1,485 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py b/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py deleted file mode 100644 index 9763f12..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/sessions.py +++ /dev/null @@ -1,871 +0,0 @@ -"""Session implementation for CherryPy. - -You need to edit your config file to use sessions. Here's an example:: - - [/] - tools.sessions.on = True - tools.sessions.storage_type = "file" - tools.sessions.storage_path = "/home/site/sessions" - tools.sessions.timeout = 60 - -This sets the session to be stored in files in the directory /home/site/sessions, -and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions -will be saved in RAM. ``tools.sessions.on`` is the only required line for -working sessions, the rest are optional. - -By default, the session ID is passed in a cookie, so the client's browser must -have cookies enabled for your site. - -To set data for the current session, use -``cherrypy.session['fieldname'] = 'fieldvalue'``; -to get data use ``cherrypy.session.get('fieldname')``. - -================ -Locking sessions -================ - -By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means -the session is locked early and unlocked late. If you want to control when the -session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. -Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. -Regardless of which mode you use, the session is guaranteed to be unlocked when -the request is complete. - -================= -Expiring Sessions -================= - -You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. -Simply call that function at the point you want the session to expire, and it -will cause the session cookie to expire client-side. - -=========================== -Session Fixation Protection -=========================== - -If CherryPy receives, via a request cookie, a session id that it does not -recognize, it will reject that id and create a new one to return in the -response cookie. This `helps prevent session fixation attacks -`_. -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/libs/CherryPy-3.2.2/cherrypy/lib/static.py b/libs/CherryPy-3.2.2/cherrypy/lib/static.py deleted file mode 100644 index 2d14230..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/static.py +++ /dev/null @@ -1,363 +0,0 @@ -try: - from io import UnsupportedOperation -except ImportError: - UnsupportedOperation = object() -import logging -import mimetypes -mimetypes.init() -mimetypes.types_map['.dwg']='image/x-dwg' -mimetypes.types_map['.ico']='image/x-icon' -mimetypes.types_map['.bz2']='application/x-bzip2' -mimetypes.types_map['.gz']='application/x-gzip' - -import os -import re -import stat -import time - -import cherrypy -from cherrypy._cpcompat import ntob, unquote -from cherrypy.lib import cptools, httputil, file_generator_limited - - -def serve_file(path, content_type=None, disposition=None, name=None, debug=False): - """Set status, headers, and body in order to serve the given path. - - The Content-Type header will be set to the content_type arg, if provided. - If not provided, the Content-Type will be guessed by the file extension - of the 'path' argument. - - If disposition is not None, the Content-Disposition header will be set - to "; 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/libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py b/libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py deleted file mode 100644 index 9a44464..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/lib/xmlrpcutil.py +++ /dev/null @@ -1,55 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/process/__init__.py b/libs/CherryPy-3.2.2/cherrypy/process/__init__.py deleted file mode 100644 index f15b123..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/process/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/process/plugins.py b/libs/CherryPy-3.2.2/cherrypy/process/plugins.py deleted file mode 100644 index ba618a0..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/process/plugins.py +++ /dev/null @@ -1,683 +0,0 @@ -"""Site services for use with a Web Site Process Bus.""" - -import os -import re -import signal as _signal -import sys -import time -import threading - -from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set - -# _module__file__base is used by Autoreload to make -# absolute any filenames retrieved from sys.modules which are not -# already absolute paths. This is to work around Python's quirk -# of importing the startup script and using a relative filename -# for it in sys.modules. -# -# Autoreload examines sys.modules afresh every time it runs. If an application -# changes the current directory by executing os.chdir(), then the next time -# Autoreload runs, it will not be able to find any filenames which are -# not absolute paths, because the current directory is not the same as when the -# module was first imported. Autoreload will then wrongly conclude the file has -# "changed", and initiate the shutdown/re-exec sequence. -# See ticket #917. -# For this workaround to have a decent probability of success, this module -# needs to be imported as early as possible, before the app has much chance -# to change the working directory. -_module__file__base = os.getcwd() - - -class SimplePlugin(object): - """Plugin base class which auto-subscribes methods for known channels.""" - - bus = None - """A :class:`Bus `, 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/libs/CherryPy-3.2.2/cherrypy/process/servers.py b/libs/CherryPy-3.2.2/cherrypy/process/servers.py deleted file mode 100644 index fa714d6..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/process/servers.py +++ /dev/null @@ -1,427 +0,0 @@ -""" -Starting in CherryPy 3.1, cherrypy.server is implemented as an -:ref:`Engine Plugin`. 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/libs/CherryPy-3.2.2/cherrypy/process/win32.py b/libs/CherryPy-3.2.2/cherrypy/process/win32.py deleted file mode 100644 index 83f99a5..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/process/win32.py +++ /dev/null @@ -1,174 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/process/wspbus.py b/libs/CherryPy-3.2.2/cherrypy/process/wspbus.py deleted file mode 100644 index 6ef768d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/process/wspbus.py +++ /dev/null @@ -1,432 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py b/libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py deleted file mode 100644 index 00964ac..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/scaffold/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -""", 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/libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf deleted file mode 100644 index 922398e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/scaffold/apache-fcgi.conf +++ /dev/null @@ -1,22 +0,0 @@ -# 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/libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf deleted file mode 100644 index 93a6e53..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/scaffold/example.conf +++ /dev/null @@ -1,3 +0,0 @@ -[/] -log.error_file: "error.log" -log.access_file: "access.log" \ No newline at end of file diff --git a/libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf b/libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf deleted file mode 100644 index 6ed3898..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/scaffold/site.conf +++ /dev/null @@ -1,14 +0,0 @@ -[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/libs/CherryPy-3.2.2/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libs/CherryPy-3.2.2/cherrypy/scaffold/static/made_with_cherrypy_small.png deleted file mode 100644 index c3aafeed952190f5da9982bb359aa75b107ff079..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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/libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py b/libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py deleted file mode 100644 index 32a7dee..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/checkerdemo.py +++ /dev/null @@ -1,47 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/helper.py b/libs/CherryPy-3.2.2/cherrypy/test/helper.py deleted file mode 100644 index 22b8ccc..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/helper.py +++ /dev/null @@ -1,493 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/logtest.py b/libs/CherryPy-3.2.2/cherrypy/test/logtest.py deleted file mode 100644 index 3c6f114..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/logtest.py +++ /dev/null @@ -1,188 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py b/libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py deleted file mode 100644 index 95acf14..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/modfastcgi.py +++ /dev/null @@ -1,135 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py b/libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py deleted file mode 100644 index 736aa4c..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/modfcgid.py +++ /dev/null @@ -1,125 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/modpy.py b/libs/CherryPy-3.2.2/cherrypy/test/modpy.py deleted file mode 100644 index 519571f..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/modpy.py +++ /dev/null @@ -1,163 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py b/libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py deleted file mode 100644 index 309a541..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/modwsgi.py +++ /dev/null @@ -1,148 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py b/libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py deleted file mode 100644 index 342e5b5..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/sessiondemo.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/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/libs/CherryPy-3.2.2/cherrypy/test/static/dirback.jpg b/libs/CherryPy-3.2.2/cherrypy/test/static/dirback.jpg deleted file mode 100644 index 530e6d6a386fc097f3a1dbabbde2d80fec1175ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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/libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py b/libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py deleted file mode 100644 index 1960fa8..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_auth_digest.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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/libs/CherryPy-3.2.2/cherrypy/test/test_bus.py b/libs/CherryPy-3.2.2/cherrypy/test/test_bus.py deleted file mode 100644 index 51c1022..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_bus.py +++ /dev/null @@ -1,263 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_caching.py b/libs/CherryPy-3.2.2/cherrypy/test/test_caching.py deleted file mode 100644 index c210e6e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_caching.py +++ /dev/null @@ -1,328 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_config.py b/libs/CherryPy-3.2.2/cherrypy/test/test_config.py deleted file mode 100644 index b1ef6a3..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_config.py +++ /dev/null @@ -1,256 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py b/libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py deleted file mode 100644 index 0b9718d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_config_server.py +++ /dev/null @@ -1,121 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_conn.py b/libs/CherryPy-3.2.2/cherrypy/test/test_conn.py deleted file mode 100644 index 1346f59..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_conn.py +++ /dev/null @@ -1,734 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_core.py b/libs/CherryPy-3.2.2/cherrypy/test/test_core.py deleted file mode 100644 index b4e830d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_core.py +++ /dev/null @@ -1,688 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py b/libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py deleted file mode 100644 index 0395b7b..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_dynamicobjectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py b/libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py deleted file mode 100644 index 2d0ce76..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_encoding.py +++ /dev/null @@ -1,363 +0,0 @@ - -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/libs/CherryPy-3.2.2/cherrypy/test/test_etags.py b/libs/CherryPy-3.2.2/cherrypy/test/test_etags.py deleted file mode 100644 index aec1693..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_etags.py +++ /dev/null @@ -1,83 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_http.py b/libs/CherryPy-3.2.2/cherrypy/test/test_http.py deleted file mode 100644 index 639c6c4..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_http.py +++ /dev/null @@ -1,212 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py b/libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py deleted file mode 100644 index 9d0eecb..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_httpauth.py +++ /dev/null @@ -1,151 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py b/libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py deleted file mode 100644 index 5dc40fd..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_httplib.py +++ /dev/null @@ -1,29 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_json.py b/libs/CherryPy-3.2.2/cherrypy/test/test_json.py deleted file mode 100644 index a02c076..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_json.py +++ /dev/null @@ -1,79 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_logging.py b/libs/CherryPy-3.2.2/cherrypy/test/test_logging.py deleted file mode 100644 index 7d506e8..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_logging.py +++ /dev/null @@ -1,157 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_mime.py b/libs/CherryPy-3.2.2/cherrypy/test/test_mime.py deleted file mode 100644 index 1605991..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_mime.py +++ /dev/null @@ -1,128 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py b/libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py deleted file mode 100644 index 1dd1429..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_misc_tools.py +++ /dev/null @@ -1,207 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py b/libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py deleted file mode 100644 index 8dcf2d3..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_objectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py b/libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py deleted file mode 100644 index 2fbb619..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_proxy.py +++ /dev/null @@ -1,129 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py b/libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py deleted file mode 100644 index 279935e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_refleaks.py +++ /dev/null @@ -1,59 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py b/libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py deleted file mode 100644 index 26eea56..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_request_obj.py +++ /dev/null @@ -1,737 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_routes.py b/libs/CherryPy-3.2.2/cherrypy/test/test_routes.py deleted file mode 100644 index a8062f8..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_routes.py +++ /dev/null @@ -1,69 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_session.py b/libs/CherryPy-3.2.2/cherrypy/test/test_session.py deleted file mode 100644 index 9143a1d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_session.py +++ /dev/null @@ -1,464 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py b/libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py deleted file mode 100644 index ab1fe51..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_sessionauthenticate.py +++ /dev/null @@ -1,62 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_states.py b/libs/CherryPy-3.2.2/cherrypy/test/test_states.py deleted file mode 100644 index 6322687..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_states.py +++ /dev/null @@ -1,439 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_static.py b/libs/CherryPy-3.2.2/cherrypy/test/test_static.py deleted file mode 100644 index 871420b..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_static.py +++ /dev/null @@ -1,300 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_tools.py b/libs/CherryPy-3.2.2/cherrypy/test/test_tools.py deleted file mode 100644 index 02bacda..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_tools.py +++ /dev/null @@ -1,399 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py b/libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py deleted file mode 100644 index aab2786..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_tutorials.py +++ /dev/null @@ -1,201 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py b/libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py deleted file mode 100644 index dbd2dbc..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_virtualhost.py +++ /dev/null @@ -1,107 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py deleted file mode 100644 index e3c6ce6..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_ns.py +++ /dev/null @@ -1,91 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py deleted file mode 100644 index abb1a91..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_wsgi_vhost.py +++ /dev/null @@ -1,36 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py b/libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py deleted file mode 100644 index d4b8b79..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_wsgiapps.py +++ /dev/null @@ -1,118 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py b/libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py deleted file mode 100644 index f7a6927..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/test_xmlrpc.py +++ /dev/null @@ -1,179 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/test/webtest.py b/libs/CherryPy-3.2.2/cherrypy/test/webtest.py deleted file mode 100644 index 50cfbad..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/test/webtest.py +++ /dev/null @@ -1,575 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt b/libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt deleted file mode 100644 index 2b877e1..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/README.txt +++ /dev/null @@ -1,16 +0,0 @@ -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/libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py deleted file mode 100644 index c4e2c55..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -# 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/libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py deleted file mode 100644 index c43feb4..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/bonus-sqlobject.py +++ /dev/null @@ -1,168 +0,0 @@ -''' -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/libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html b/libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html deleted file mode 100644 index d0f30c8..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/custom_error.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - 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/libs/CherryPy-3.2.2/cherrypy/tutorial/pdf_file.pdf b/libs/CherryPy-3.2.2/cherrypy/tutorial/pdf_file.pdf deleted file mode 100644 index 38b4f15eabdd65d4a674cb32034361245aa7b97e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py deleted file mode 100644 index 283477d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut03_get_and_post.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py deleted file mode 100644 index b4d820e..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut04_complex_site.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py deleted file mode 100644 index 3d4ec9b..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut05_derived_objects.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py deleted file mode 100644 index fe24f38..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut06_default_method.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py deleted file mode 100644 index 4b1386b..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut07_sessions.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py deleted file mode 100644 index a6fbdc2..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut08_generators_and_yield.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py deleted file mode 100644 index 4c8e581..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut09_files.py +++ /dev/null @@ -1,107 +0,0 @@ -""" - -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py b/libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py deleted file mode 100644 index dfa5733..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tut10_http_errors.py +++ /dev/null @@ -1,81 +0,0 @@ -""" - -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/libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf b/libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf deleted file mode 100644 index 6537fd3..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/tutorial/tutorial.conf +++ /dev/null @@ -1,4 +0,0 @@ -[global] -server.socket_host = "127.0.0.1" -server.socket_port = 8080 -server.thread_pool = 10 diff --git a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py deleted file mode 100644 index ee6190f..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -__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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py deleted file mode 100644 index 03bf05d..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_builtin.py +++ /dev/null @@ -1,91 +0,0 @@ -"""A library for integrating Python's builtin ``ssl`` library with CherryPy. - -The ssl module must be importable for SSL functionality. - -To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of -``BuiltinSSLAdapter``. -""" - -try: - import ssl -except ImportError: - ssl = None - -try: - from _pyio import DEFAULT_BUFFER_SIZE -except ImportError: - try: - from io import DEFAULT_BUFFER_SIZE - except ImportError: - DEFAULT_BUFFER_SIZE = -1 - -import sys - -from cherrypy import wsgiserver - - -class BuiltinSSLAdapter(wsgiserver.SSLAdapter): - """A wrapper for integrating Python's builtin ssl module with CherryPy.""" - - certificate = None - """The filename of the server SSL certificate.""" - - private_key = None - """The filename of the server's private key file.""" - - def __init__(self, certificate, private_key, certificate_chain=None): - if ssl is None: - raise ImportError("You must install the ssl module to use HTTPS.") - self.certificate = certificate - self.private_key = private_key - self.certificate_chain = certificate_chain - - def bind(self, sock): - """Wrap and return the given socket.""" - return sock - - def wrap(self, sock): - """Wrap and return the given socket, plus WSGI environ entries.""" - try: - s = ssl.wrap_socket(sock, do_handshake_on_connect=True, - server_side=True, certfile=self.certificate, - keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) - except ssl.SSLError: - e = sys.exc_info()[1] - if e.errno == ssl.SSL_ERROR_EOF: - # This is almost certainly due to the cherrypy engine - # 'pinging' the socket to assert it's connectable; - # the 'ping' isn't SSL. - return None, {} - elif e.errno == ssl.SSL_ERROR_SSL: - if e.args[1].endswith('http request'): - # The client is speaking HTTP to an HTTPS server. - raise wsgiserver.NoSSLError - elif e.args[1].endswith('unknown protocol'): - # The client is speaking some non-HTTP protocol. - # Drop the conn. - return None, {} - raise - return s, self.get_environ(s) - - # TODO: fill this out more with mod ssl env - def get_environ(self, sock): - """Create WSGI environ entries to be merged into each request.""" - cipher = sock.cipher() - ssl_environ = { - "wsgi.url_scheme": "https", - "HTTPS": "on", - 'SSL_PROTOCOL': cipher[1], - 'SSL_CIPHER': cipher[0] -## SSL_VERSION_INTERFACE string The mod_ssl program version -## SSL_VERSION_LIBRARY string The OpenSSL program version - } - return ssl_environ - - if sys.version_info >= (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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py deleted file mode 100644 index f3d9bf5..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/ssl_pyopenssl.py +++ /dev/null @@ -1,256 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py deleted file mode 100644 index b6bd499..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver2.py +++ /dev/null @@ -1,2322 +0,0 @@ -"""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/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py b/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py deleted file mode 100644 index 62db5ff..0000000 --- a/libs/CherryPy-3.2.2/cherrypy/wsgiserver/wsgiserver3.py +++ /dev/null @@ -1,2040 +0,0 @@ -"""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_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/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg b/libs/CherryPy-3.2.2/dist/CherryPy-3.2.2-py2.7.egg deleted file mode 100644 index b68299a8d1bf6187f7889bf23341b02663e4517e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 851999 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≻5M}%F` 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 z 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

HDFi-PL|FWk-H$a7wnME-DDAL7sW}n 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 z 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

wA-SfBF@e{@7DL2+L9`SWh5w+ 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�J5Bf+wqfR;5;_L-M^qhuf^ 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 z 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 (3,) - if python3: - exclude_pattern = re.compile('wsgiserver2|ssl_pyopenssl') - else: - exclude_pattern = re.compile('wsgiserver3') - if exclude_pattern.match(module): - return # skip it - return build_py.build_module(self, module, module_file, package) - - -############################################################################### -# arguments for the setup command -############################################################################### -name = "CherryPy" -version = "3.2.2" -desc = "Object-Oriented HTTP framework" -long_desc = "CherryPy is a pythonic, object-oriented HTTP framework" -classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: Freely Distributable", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: HTTP Servers", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", - "Topic :: Software Development :: Libraries :: Application Frameworks", -] -author="CherryPy Team" -author_email="team@cherrypy.org" -url="http://www.cherrypy.org" -cp_license="BSD" -packages=[ - "cherrypy", "cherrypy.lib", - "cherrypy.tutorial", "cherrypy.test", - "cherrypy.process", - "cherrypy.scaffold", - "cherrypy.wsgiserver", -] -download_url="http://download.cherrypy.org/cherrypy/3.2.2/" -data_files=[ - ('cherrypy', ['cherrypy/cherryd', - 'cherrypy/favicon.ico', - 'cherrypy/LICENSE.txt', - ]), - ('cherrypy/process', []), - ('cherrypy/scaffold', ['cherrypy/scaffold/example.conf', - 'cherrypy/scaffold/site.conf', - ]), - ('cherrypy/scaffold/static', ['cherrypy/scaffold/static/made_with_cherrypy_small.png', - ]), - ('cherrypy/test', ['cherrypy/test/style.css', - 'cherrypy/test/test.pem', - ]), - ('cherrypy/test/static', ['cherrypy/test/static/index.html', - 'cherrypy/test/static/dirback.jpg',]), - ('cherrypy/tutorial', - [ - 'cherrypy/tutorial/tutorial.conf', - 'cherrypy/tutorial/README.txt', - 'cherrypy/tutorial/pdf_file.pdf', - 'cherrypy/tutorial/custom_error.html', - ] - ), -] -scripts = ["cherrypy/cherryd"] - -cmd_class = dict( - build_py = cherrypy_build_py, -) - -if sys.version_info >= (3, 0): - required_python_version = '3.0' -else: - required_python_version = '2.3' - -############################################################################### -# end arguments for setup -############################################################################### - -# wininst may install data_files in Python/x.y instead of the cherrypy package. -# Django's solution is at http://code.djangoproject.com/changeset/8313 -# See also http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html -if 'bdist_wininst' in sys.argv or '--format=wininst' in sys.argv: - data_files = [(r'\PURELIB\%s' % path, files) for path, files in data_files] - -def main(): - if sys.version < required_python_version: - s = "I'm sorry, but %s %s requires Python %s or later." - print(s % (name, version, required_python_version)) - sys.exit(1) - # set default location for "data_files" to - # platform specific "site-packages" location - for scheme in list(INSTALL_SCHEMES.values()): - scheme['data'] = scheme['purelib'] - - dist = setup( - name=name, - version=version, - description=desc, - long_description=long_desc, - classifiers=classifiers, - author=author, - author_email=author_email, - url=url, - license=cp_license, - packages=packages, - download_url=download_url, - data_files=data_files, - scripts=scripts, - cmdclass=cmd_class, - ) - - -if __name__ == "__main__": - main()