Codebase list cherrypy3 / 33f68ca
Imported Upstream version 3.3.0 SVN-Git Migration 8 years ago
116 changed file(s) with 8746 addition(s) and 5372 deletion(s). Raw diff Collapse all Expand all
0 Metadata-Version: 1.0
1 Name: CherryPy
2 Version: 3.2.2
3 Summary: Object-Oriented HTTP framework
4 Home-page: http://www.cherrypy.org
5 Author: CherryPy Team
6 Author-email: team@cherrypy.org
7 License: BSD
8 Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/
9 Description: CherryPy is a pythonic, object-oriented HTTP framework
10 Platform: UNKNOWN
11 Classifier: Development Status :: 5 - Production/Stable
12 Classifier: Environment :: Web Environment
13 Classifier: Intended Audience :: Developers
14 Classifier: License :: Freely Distributable
15 Classifier: Operating System :: OS Independent
16 Classifier: Programming Language :: Python
17 Classifier: Programming Language :: Python :: 2
18 Classifier: Programming Language :: Python :: 3
19 Classifier: Topic :: Internet :: WWW/HTTP
20 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
21 Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
22 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
23 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
24 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
25 Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
0 Metadata-Version: 1.1
1 Name: CherryPy
2 Version: 3.3.0
3 Summary: Object-Oriented HTTP framework
4 Home-page: http://www.cherrypy.org
5 Author: CherryPy Team
6 Author-email: team@cherrypy.org
7 License: BSD
8 Description: CherryPy is a pythonic, object-oriented HTTP framework
9 Platform: UNKNOWN
10 Classifier: Development Status :: 5 - Production/Stable
11 Classifier: Environment :: Web Environment
12 Classifier: Intended Audience :: Developers
13 Classifier: License :: Freely Distributable
14 Classifier: Operating System :: OS Independent
15 Classifier: Framework :: CherryPy
16 Classifier: License :: OSI Approved :: BSD License
17 Classifier: Programming Language :: Python
18 Classifier: Programming Language :: Python :: 2
19 Classifier: Programming Language :: Python :: 2.3
20 Classifier: Programming Language :: Python :: 2.4
21 Classifier: Programming Language :: Python :: 2.5
22 Classifier: Programming Language :: Python :: 2.6
23 Classifier: Programming Language :: Python :: 2.7
24 Classifier: Programming Language :: Python :: 3
25 Classifier: Programming Language :: Python :: 3.3
26 Classifier: Programming Language :: Python :: Implementation
27 Classifier: Programming Language :: Python :: Implementation :: CPython
28 Classifier: Programming Language :: Python :: Implementation :: Jython
29 Classifier: Programming Language :: Python :: Implementation :: PyPy
30 Classifier: Topic :: Internet :: WWW/HTTP
31 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
32 Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
33 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
34 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
35 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
36 Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
00 MANIFEST.in
11 README.txt
2 setup.cfg
23 setup.py
34 CherryPy.egg-info/PKG-INFO
45 CherryPy.egg-info/SOURCES.txt
89 cherrypy/__init__.py
910 cherrypy/_cpchecker.py
1011 cherrypy/_cpcompat.py
12 cherrypy/_cpcompat_subprocess.py
1113 cherrypy/_cpconfig.py
1214 cherrypy/_cpdispatch.py
1315 cherrypy/_cperror.py
3840 cherrypy/lib/httpauth.py
3941 cherrypy/lib/httputil.py
4042 cherrypy/lib/jsontools.py
43 cherrypy/lib/lockfile.py
44 cherrypy/lib/locking.py
4145 cherrypy/lib/profiler.py
4246 cherrypy/lib/reprconf.py
4347 cherrypy/lib/sessions.py
7175 cherrypy/test/test_auth_digest.py
7276 cherrypy/test/test_bus.py
7377 cherrypy/test/test_caching.py
78 cherrypy/test/test_compat.py
7479 cherrypy/test/test_config.py
7580 cherrypy/test/test_config_server.py
7681 cherrypy/test/test_conn.py
102107 cherrypy/test/test_wsgiapps.py
103108 cherrypy/test/test_xmlrpc.py
104109 cherrypy/test/webtest.py
110 cherrypy/test/static/404.html
105111 cherrypy/test/static/dirback.jpg
106 cherrypy/test/static/has space.html
107112 cherrypy/test/static/index.html
108113 cherrypy/tutorial/README.txt
109114 cherrypy/tutorial/__init__.py
110 cherrypy/tutorial/bonus-sqlobject.py
111115 cherrypy/tutorial/custom_error.html
112116 cherrypy/tutorial/pdf_file.pdf
113117 cherrypy/tutorial/tut01_helloworld.py
0 Metadata-Version: 1.0
1 Name: CherryPy
2 Version: 3.2.2
3 Summary: Object-Oriented HTTP framework
4 Home-page: http://www.cherrypy.org
5 Author: CherryPy Team
6 Author-email: team@cherrypy.org
7 License: BSD
8 Download-URL: http://download.cherrypy.org/cherrypy/3.2.2/
9 Description: CherryPy is a pythonic, object-oriented HTTP framework
10 Platform: UNKNOWN
11 Classifier: Development Status :: 5 - Production/Stable
12 Classifier: Environment :: Web Environment
13 Classifier: Intended Audience :: Developers
14 Classifier: License :: Freely Distributable
15 Classifier: Operating System :: OS Independent
16 Classifier: Programming Language :: Python
17 Classifier: Programming Language :: Python :: 2
18 Classifier: Programming Language :: Python :: 3
19 Classifier: Topic :: Internet :: WWW/HTTP
20 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
21 Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
22 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
23 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
24 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
25 Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
0 Metadata-Version: 1.1
1 Name: CherryPy
2 Version: 3.3.0
3 Summary: Object-Oriented HTTP framework
4 Home-page: http://www.cherrypy.org
5 Author: CherryPy Team
6 Author-email: team@cherrypy.org
7 License: BSD
8 Description: CherryPy is a pythonic, object-oriented HTTP framework
9 Platform: UNKNOWN
10 Classifier: Development Status :: 5 - Production/Stable
11 Classifier: Environment :: Web Environment
12 Classifier: Intended Audience :: Developers
13 Classifier: License :: Freely Distributable
14 Classifier: Operating System :: OS Independent
15 Classifier: Framework :: CherryPy
16 Classifier: License :: OSI Approved :: BSD License
17 Classifier: Programming Language :: Python
18 Classifier: Programming Language :: Python :: 2
19 Classifier: Programming Language :: Python :: 2.3
20 Classifier: Programming Language :: Python :: 2.4
21 Classifier: Programming Language :: Python :: 2.5
22 Classifier: Programming Language :: Python :: 2.6
23 Classifier: Programming Language :: Python :: 2.7
24 Classifier: Programming Language :: Python :: 3
25 Classifier: Programming Language :: Python :: 3.3
26 Classifier: Programming Language :: Python :: Implementation
27 Classifier: Programming Language :: Python :: Implementation :: CPython
28 Classifier: Programming Language :: Python :: Implementation :: Jython
29 Classifier: Programming Language :: Python :: Implementation :: PyPy
30 Classifier: Topic :: Internet :: WWW/HTTP
31 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
32 Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
33 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
34 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
35 Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
36 Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
5252 * Server API
5353 * WSGI API
5454
55 These API's are described in the CherryPy specification:
56 http://www.cherrypy.org/wiki/CherryPySpec
55 These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
5756 """
5857
59 __version__ = "3.2.2"
58 __version__ = "3.3.0"
6059
6160 from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
6261 from cherrypy._cpcompat import basestring, unicodestr, set
9392 engine.listeners['before_request'] = set()
9493 engine.listeners['after_request'] = set()
9594
95
9696 class _TimeoutMonitor(process.plugins.Monitor):
97
97
9898 def __init__(self, bus):
9999 self.servings = []
100100 process.plugins.Monitor.__init__(self, bus, self.run)
101
101
102102 def before_request(self):
103103 self.servings.append((serving.request, serving.response))
104
104
105105 def after_request(self):
106106 try:
107107 self.servings.remove((serving.request, serving.response))
108108 except ValueError:
109109 pass
110
110
111111 def run(self):
112112 """Check timeout on all responses. (Internal)"""
113113 for req, resp in self.servings:
124124 engine.signal_handler = process.plugins.SignalHandler(engine)
125125
126126
127 class _HandleSignalsPlugin(object):
128
129 """Handle signals from other processes based on the configured
130 platform handlers above."""
131
132 def __init__(self, bus):
133 self.bus = bus
134
135 def subscribe(self):
136 """Add the handlers based on the platform"""
137 if hasattr(self.bus, "signal_handler"):
138 self.bus.signal_handler.subscribe()
139 if hasattr(self.bus, "console_control_handler"):
140 self.bus.console_control_handler.subscribe()
141
142 engine.signals = _HandleSignalsPlugin(engine)
143
144
127145 from cherrypy import _cpserver
128146 server = _cpserver.Server()
129147 server.subscribe()
131149
132150 def quickstart(root=None, script_name="", config=None):
133151 """Mount the given root, start the builtin server (and engine), then block.
134
152
135153 root: an instance of a "controller class" (a collection of page handler
136154 methods) which represents the root of the application.
137155 script_name: a string containing the "mount point" of the application.
139157 at which to mount the given root. For example, if root.index() will
140158 handle requests to "http://www.example.com:8080/dept/app1/", then
141159 the script_name argument would be "/dept/app1".
142
160
143161 It MUST NOT end in a slash. If the script_name refers to the root
144162 of the URI, it MUST be an empty string (not "/").
145163 config: a file or dict containing application config. If this contains
148166 """
149167 if config:
150168 _global_conf_alias.update(config)
151
169
152170 tree.mount(root, script_name, config)
153
154 if hasattr(engine, "signal_handler"):
155 engine.signal_handler.subscribe()
156 if hasattr(engine, "console_control_handler"):
157 engine.console_control_handler.subscribe()
158
171
172 engine.signals.subscribe()
159173 engine.start()
160174 engine.block()
161175
162176
163177 from cherrypy._cpcompat import threadlocal as _local
164178
179
165180 class _Serving(_local):
181
166182 """An interface for registering request and response objects.
167
183
168184 Rather than have a separate "thread local" object for the request and
169185 the response, this class works as a single threadlocal container for
170186 both objects (and any others which developers wish to define). In this
172188 conversation, yet still refer to them as module-level globals in a
173189 thread-safe way.
174190 """
175
191
176192 request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
177193 _httputil.Host("127.0.0.1", 1111))
178194 """
179195 The request object for the current thread. In the main thread,
180196 and any threads which are not receiving HTTP requests, this is None."""
181
197
182198 response = _cprequest.Response()
183199 """
184200 The response object for the current thread. In the main thread,
185201 and any threads which are not receiving HTTP requests, this is None."""
186
202
187203 def load(self, request, response):
188204 self.request = request
189205 self.response = response
190
206
191207 def clear(self):
192208 """Remove all attributes of self."""
193209 self.__dict__.clear()
196212
197213
198214 class _ThreadLocalProxy(object):
199
215
200216 __slots__ = ['__attrname__', '__dict__']
201
217
202218 def __init__(self, attrname):
203219 self.__attrname__ = attrname
204
220
205221 def __getattr__(self, name):
206222 child = getattr(serving, self.__attrname__)
207223 return getattr(child, name)
208
224
209225 def __setattr__(self, name, value):
210226 if name in ("__attrname__", ):
211227 object.__setattr__(self, name, value)
212228 else:
213229 child = getattr(serving, self.__attrname__)
214230 setattr(child, name, value)
215
231
216232 def __delattr__(self, name):
217233 child = getattr(serving, self.__attrname__)
218234 delattr(child, name)
219
235
220236 def _get_dict(self):
221237 child = getattr(serving, self.__attrname__)
222238 d = child.__class__.__dict__.copy()
223239 d.update(child.__dict__)
224240 return d
225241 __dict__ = property(_get_dict)
226
242
227243 def __getitem__(self, key):
228244 child = getattr(serving, self.__attrname__)
229245 return child[key]
230
246
231247 def __setitem__(self, key, value):
232248 child = getattr(serving, self.__attrname__)
233249 child[key] = value
234
250
235251 def __delitem__(self, key):
236252 child = getattr(serving, self.__attrname__)
237253 del child[key]
238
254
239255 def __contains__(self, key):
240256 child = getattr(serving, self.__attrname__)
241257 return key in child
242
258
243259 def __len__(self):
244260 child = getattr(serving, self.__attrname__)
245261 return len(child)
246
262
247263 def __nonzero__(self):
248264 child = getattr(serving, self.__attrname__)
249265 return bool(child)
257273 response = _ThreadLocalProxy('response')
258274
259275 # Create thread_data object as a thread-specific all-purpose storage
276
277
260278 class _ThreadData(_local):
279
261280 """A container for thread-specific data."""
262281 thread_data = _ThreadData()
263282
282301
283302 from cherrypy import _cplogging
284303
304
285305 class _GlobalLogManager(_cplogging.LogManager):
306
286307 """A site-wide LogManager; routes to app.log or global log as appropriate.
287
308
288309 This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
289310 cherrypy.log() and cherrypy.log.access(). If either
290311 function is called during a request, the message will be sent to the
291312 logger for the current Application. If they are called outside of a
292313 request, the message will be sent to the site-wide logger.
293314 """
294
315
295316 def __call__(self, *args, **kwargs):
296 """Log the given message to the app.log or global log as appropriate."""
297 # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
317 """Log the given message to the app.log or global log as appropriate.
318 """
319 # Do NOT use try/except here. See
320 # https://bitbucket.org/cherrypy/cherrypy/issue/945
298321 if hasattr(request, 'app') and hasattr(request.app, 'log'):
299322 log = request.app.log
300323 else:
301324 log = self
302325 return log.error(*args, **kwargs)
303
326
304327 def access(self):
305 """Log an access message to the app.log or global log as appropriate."""
328 """Log an access message to the app.log or global log as appropriate.
329 """
306330 try:
307331 return request.app.log.access()
308332 except AttributeError:
315339 log.error_file = ''
316340 # Using an access file makes CP about 10% slower. Leave off by default.
317341 log.access_file = ''
342
318343
319344 def _buslog(msg, level):
320345 log.error(msg, 'ENGINE', severity=level)
334359 for a in alias:
335360 parents[a.replace(".", "_")] = func
336361 return func
337
338 import sys, types
362
363 import sys
364 import types
339365 if isinstance(func, (types.FunctionType, types.MethodType)):
340366 if alias is None:
341367 # @expose
362388 alias = func
363389 return expose_
364390
391
365392 def popargs(*args, **kwargs):
366 """A decorator for _cp_dispatch
393 """A decorator for _cp_dispatch
367394 (cherrypy.dispatch.Dispatcher.dispatch_method_name).
368395
369396 Optional keyword argument: handler=(Object or Function)
370
371 Provides a _cp_dispatch function that pops off path segments into
397
398 Provides a _cp_dispatch function that pops off path segments into
372399 cherrypy.request.params under the names specified. The dispatch
373400 is then forwarded on to the next vpath element.
374
401
375402 Note that any existing (and exposed) member function of the class that
376403 popargs is applied to will override that value of the argument. For
377404 instance, if you have a method named "list" on the class decorated with
378405 popargs, then accessing "/list" will call that function instead of popping
379 it off as the requested parameter. This restriction applies to all
406 it off as the requested parameter. This restriction applies to all
380407 _cp_dispatch functions. The only way around this restriction is to create
381408 a "blank class" whose only function is to provide _cp_dispatch.
382
409
383410 If there are path elements after the arguments, or more arguments
384411 are requested than are available in the vpath, then the 'handler'
385412 keyword argument specifies the next object to handle the parameterized
388415 will be called with the args specified and the return value from that
389416 function used as the next object INSTEAD of adding the parameters to
390417 cherrypy.request.args.
391
418
392419 This decorator may be used in one of two ways:
393
420
394421 As a class decorator:
395422 @cherrypy.popargs('year', 'month', 'day')
396423 class Blog:
398425 #Process the parameters here; any url like
399426 #/, /2009, /2009/12, or /2009/12/31
400427 #will fill in the appropriate parameters.
401
428
402429 def create(self):
403430 #This link will still be available at /create. Defined functions
404431 #take precedence over arguments.
405
432
406433 Or as a member of a class:
407434 class Blog:
408435 _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
409436 #...
410
437
411438 The handler argument may be used to mix arguments with built in functions.
412439 For instance, the following setup allows different activities at the
413440 day, month, and year level:
414
441
415442 class DayHandler:
416443 def index(self, year, month, day):
417444 #Do something with this day; probably list entries
418
445
419446 def delete(self, year, month, day):
420447 #Delete all entries for this day
421
448
422449 @cherrypy.popargs('day', handler=DayHandler())
423450 class MonthHandler:
424451 def index(self, year, month):
425452 #Do something with this month; probably list entries
426
453
427454 def delete(self, year, month):
428455 #Delete all entries for this month
429
456
430457 @cherrypy.popargs('month', handler=MonthHandler())
431458 class YearHandler:
432459 def index(self, year):
433460 #Do something with this year
434
461
435462 #...
436
463
437464 @cherrypy.popargs('year', handler=YearHandler())
438465 class Root:
439466 def index(self):
440467 #...
441
468
442469 """
443470
444 #Since keyword arg comes after *args, we have to process it ourselves
445 #for lower versions of python.
471 # Since keyword arg comes after *args, we have to process it ourselves
472 # for lower versions of python.
446473
447474 handler = None
448475 handler_call = False
449 for k,v in kwargs.items():
476 for k, v in kwargs.items():
450477 if k == 'handler':
451478 handler = v
452479 else:
453480 raise TypeError(
454 "cherrypy.popargs() got an unexpected keyword argument '{0}'" \
481 "cherrypy.popargs() got an unexpected keyword argument '{0}'"
455482 .format(k)
456 )
483 )
457484
458485 import inspect
459486
460487 if handler is not None \
461 and (hasattr(handler, '__call__') or inspect.isclass(handler)):
488 and (hasattr(handler, '__call__') or inspect.isclass(handler)):
462489 handler_call = True
463
490
464491 def decorated(cls_or_self=None, vpath=None):
465492 if inspect.isclass(cls_or_self):
466 #cherrypy.popargs is a class decorator
493 # cherrypy.popargs is a class decorator
467494 cls = cls_or_self
468495 setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
469496 return cls
470
471 #We're in the actual function
497
498 # We're in the actual function
472499 self = cls_or_self
473500 parms = {}
474501 for arg in args:
475502 if not vpath:
476503 break
477504 parms[arg] = vpath.pop(0)
478
505
479506 if handler is not None:
480507 if handler_call:
481508 return handler(**parms)
482509 else:
483510 request.params.update(parms)
484511 return handler
485
512
486513 request.params.update(parms)
487
488 #If we are the ultimate handler, then to prevent our _cp_dispatch
489 #from being called again, we will resolve remaining elements through
490 #getattr() directly.
514
515 # If we are the ultimate handler, then to prevent our _cp_dispatch
516 # from being called again, we will resolve remaining elements through
517 # getattr() directly.
491518 if vpath:
492519 return getattr(self, vpath.pop(0), None)
493520 else:
494521 return self
495
522
496523 return decorated
524
497525
498526 def url(path="", qs="", script_name=None, base=None, relative=None):
499527 """Create an absolute URL for the given path.
500
528
501529 If 'path' starts with a slash ('/'), this will return
502530 (base + script_name + path + qs).
503531 If it does not start with a slash, this returns
504532 (base + script_name [+ request.path_info] + path + qs).
505
533
506534 If script_name is None, cherrypy.request will be used
507535 to find a script_name, if available.
508
536
509537 If base is None, cherrypy.request.base will be used (if available).
510538 Note that you can use cherrypy.tools.proxy to change this.
511
539
512540 Finally, note that this function can be used to obtain an absolute URL
513541 for the current request path (minus the querystring) by passing no args.
514542 If you call url(qs=cherrypy.request.query_string), you should get the
515543 original browser URL (assuming no internal redirections).
516
544
517545 If relative is None or not provided, request.app.relative_urls will
518546 be used (if available, else False). If False, the output will be an
519547 absolute URL (including the scheme, host, vhost, and script_name).
526554 qs = _urlencode(qs)
527555 if qs:
528556 qs = '?' + qs
529
557
530558 if request.app:
531559 if not path.startswith("/"):
532560 # Append/remove trailing slash from path_info as needed
539567 elif request.is_index is False:
540568 if pi.endswith('/') and pi != '/':
541569 pi = pi[:-1]
542
570
543571 if path == "":
544572 path = pi
545573 else:
546574 path = _urljoin(pi, path)
547
575
548576 if script_name is None:
549577 script_name = request.script_name
550578 if base is None:
551579 base = request.base
552
580
553581 newurl = base + script_name + path + qs
554582 else:
555583 # No request.app (we're being called outside a request).
558586 # if you're using vhosts or tools.proxy.
559587 if base is None:
560588 base = server.base()
561
589
562590 path = (script_name or "") + path
563591 newurl = base + path + qs
564
592
565593 if './' in newurl:
566594 # Normalize the URL by removing ./ and ../
567595 atoms = []
573601 else:
574602 atoms.append(atom)
575603 newurl = '/'.join(atoms)
576
604
577605 # At this point, we should have a fully-qualified absolute URL.
578
606
579607 if relative is None:
580608 relative = getattr(request.app, "relative_urls", False)
581
609
582610 # See http://www.ietf.org/rfc/rfc2396.txt
583611 if relative == 'server':
584612 # "A relative reference beginning with a single slash character is
598626 new.pop(0)
599627 new = (['..'] * len(old)) + new
600628 newurl = '/'.join(new)
601
629
602630 return newurl
603631
604632
612640 'tools.log_headers.on': True,
613641 'tools.trailing_slash.on': True,
614642 'tools.encode.on': True
615 }
643 }
616644 config.namespaces["log"] = lambda k, v: setattr(log, k, v)
617645 config.namespaces["checker"] = lambda k, v: setattr(checker, k, v)
618646 # Must reset to get our defaults applied.
55
66
77 class Checker(object):
8
89 """A checker for CherryPy sites and their mounted applications.
9
10
1011 When this object is called at engine startup, it executes each
1112 of its own methods whose names start with ``check_``. If you wish
1213 to disable selected checks, simply add a line in your global
1314 config which sets the appropriate method to False::
14
15
1516 [global]
1617 checker.check_skipped_app_config = False
17
18
1819 You may also dynamically add or replace ``check_*`` methods in this way.
1920 """
20
21
2122 on = True
2223 """If True (the default), run all checks; if False, turn off all checks."""
23
24
24
2525 def __init__(self):
2626 self._populate_known_types()
27
27
2828 def __call__(self):
2929 """Run all check_* methods."""
3030 if self.on:
3838 method()
3939 finally:
4040 warnings.formatwarning = oldformatwarning
41
41
4242 def formatwarning(self, message, category, filename, lineno, line=None):
4343 """Function to format a warning."""
4444 return "CherryPy Checker:\n%s\n\n" % message
45
45
4646 # This value should be set inside _cpconfig.
4747 global_config_contained_paths = False
48
48
4949 def check_app_config_entries_dont_start_with_script_name(self):
50 """Check for Application config with sections that repeat script_name."""
50 """Check for Application config with sections that repeat script_name.
51 """
5152 for sn, app in cherrypy.tree.apps.items():
5253 if not isinstance(app, cherrypy.Application):
5354 continue
6061 key_atoms = key.strip("/").split("/")
6162 if key_atoms[:len(sn_atoms)] == sn_atoms:
6263 warnings.warn(
63 "The application mounted at %r has config " \
64 "entries that start with its script name: %r" % (sn, key))
65
64 "The application mounted at %r has config "
65 "entries that start with its script name: %r" % (sn,
66 key))
67
6668 def check_site_config_entries_in_app_config(self):
6769 """Check for mounted Applications that have site-scoped config."""
6870 for sn, app in iteritems(cherrypy.tree.apps):
6971 if not isinstance(app, cherrypy.Application):
7072 continue
71
73
7274 msg = []
7375 for section, entries in iteritems(app.config):
7476 if section.startswith('/'):
7577 for key, value in iteritems(entries):
7678 for n in ("engine.", "server.", "tree.", "checker."):
7779 if key.startswith(n):
78 msg.append("[%s] %s = %s" % (section, key, value))
80 msg.append("[%s] %s = %s" %
81 (section, key, value))
7982 if msg:
8083 msg.insert(0,
81 "The application mounted at %r contains the following "
82 "config entries, which are only allowed in site-wide "
83 "config. Move them to a [global] section and pass them "
84 "to cherrypy.config.update() instead of tree.mount()." % sn)
84 "The application mounted at %r contains the "
85 "following config entries, which are only allowed "
86 "in site-wide config. Move them to a [global] "
87 "section and pass them to cherrypy.config.update() "
88 "instead of tree.mount()." % sn)
8589 warnings.warn(os.linesep.join(msg))
86
90
8791 def check_skipped_app_config(self):
8892 """Check for mounted Applications that have no config."""
8993 for sn, app in cherrypy.tree.apps.items():
99103 "cherrypy.tree.mount(..., config=app_config)")
100104 warnings.warn(msg)
101105 return
102
106
103107 def check_app_config_brackets(self):
104 """Check for Application config with extraneous brackets in section names."""
108 """Check for Application config with extraneous brackets in section
109 names.
110 """
105111 for sn, app in cherrypy.tree.apps.items():
106112 if not isinstance(app, cherrypy.Application):
107113 continue
110116 for key in app.config.keys():
111117 if key.startswith("[") or key.endswith("]"):
112118 warnings.warn(
113 "The application mounted at %r has config " \
119 "The application mounted at %r has config "
114120 "section names with extraneous brackets: %r. "
115121 "Config *files* need brackets; config *dicts* "
116122 "(e.g. passed to tree.mount) do not." % (sn, key))
117
123
118124 def check_static_paths(self):
119125 """Check Application config for incorrect static paths."""
120126 # Use the dummy Request object in the main thread.
127133 # get_resource will populate request.config
128134 request.get_resource(section + "/dummy.html")
129135 conf = request.config.get
130
136
131137 if conf("tools.staticdir.on", False):
132138 msg = ""
133139 root = conf("tools.staticdir.root")
143149 "though a root is provided.")
144150 testdir = os.path.join(root, dir[1:])
145151 if os.path.exists(testdir):
146 msg += ("\nIf you meant to serve the "
147 "filesystem folder at %r, remove "
148 "the leading slash from dir." % testdir)
152 msg += (
153 "\nIf you meant to serve the "
154 "filesystem folder at %r, remove the "
155 "leading slash from dir." % (testdir,))
149156 else:
150157 if not root:
151 msg = "dir is a relative path and no root provided."
158 msg = (
159 "dir is a relative path and "
160 "no root provided.")
152161 else:
153162 fulldir = os.path.join(root, dir)
154163 if not os.path.isabs(fulldir):
155 msg = "%r is not an absolute path." % fulldir
156
164 msg = ("%r is not an absolute path." % (
165 fulldir,))
166
157167 if fulldir and not os.path.exists(fulldir):
158168 if msg:
159169 msg += "\n"
160170 msg += ("%r (root + dir) is not an existing "
161171 "filesystem path." % fulldir)
162
172
163173 if msg:
164174 warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
165175 % (msg, section, root, dir))
166
167
176
168177 # -------------------------- Compatibility -------------------------- #
169
170178 obsolete = {
171179 'server.default_content_type': 'tools.response_headers.headers',
172180 'log_access_file': 'log.access_file',
179187 'throw_errors': 'request.throw_errors',
180188 'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
181189 'cherrypy.Application(Root())))'),
182 }
183
190 }
191
184192 deprecated = {}
185
193
186194 def _compat(self, config):
187195 """Process config and warn on each obsolete or deprecated entry."""
188196 for section, conf in config.items():
203211 elif section in self.deprecated:
204212 warnings.warn("%r is deprecated. Use %r instead."
205213 % (section, self.deprecated[section]))
206
214
207215 def check_compatibility(self):
208216 """Process config and warn on each obsolete or deprecated entry."""
209217 self._compat(cherrypy.config)
211219 if not isinstance(app, cherrypy.Application):
212220 continue
213221 self._compat(app.config)
214
215
222
216223 # ------------------------ Known Namespaces ------------------------ #
217
218224 extra_config_namespaces = []
219
225
220226 def _known_ns(self, app):
221227 ns = ["wsgi"]
222228 ns.extend(copykeys(app.toolboxes))
224230 ns.extend(copykeys(app.request_class.namespaces))
225231 ns.extend(copykeys(cherrypy.config.namespaces))
226232 ns += self.extra_config_namespaces
227
233
228234 for section, conf in app.config.items():
229235 is_path_section = section.startswith("/")
230236 if is_path_section and isinstance(conf, dict):
234240 if atoms[0] not in ns:
235241 # Spit out a special warning if a known
236242 # namespace is preceded by "cherrypy."
237 if (atoms[0] == "cherrypy" and atoms[1] in ns):
238 msg = ("The config entry %r is invalid; "
239 "try %r instead.\nsection: [%s]"
240 % (k, ".".join(atoms[1:]), section))
243 if atoms[0] == "cherrypy" and atoms[1] in ns:
244 msg = (
245 "The config entry %r is invalid; "
246 "try %r instead.\nsection: [%s]"
247 % (k, ".".join(atoms[1:]), section))
241248 else:
242 msg = ("The config entry %r is invalid, because "
243 "the %r config namespace is unknown.\n"
244 "section: [%s]" % (k, atoms[0], section))
249 msg = (
250 "The config entry %r is invalid, "
251 "because the %r config namespace "
252 "is unknown.\n"
253 "section: [%s]" % (k, atoms[0], section))
245254 warnings.warn(msg)
246255 elif atoms[0] == "tools":
247256 if atoms[1] not in dir(cherrypy.tools):
248 msg = ("The config entry %r may be invalid, "
249 "because the %r tool was not found.\n"
250 "section: [%s]" % (k, atoms[1], section))
257 msg = (
258 "The config entry %r may be invalid, "
259 "because the %r tool was not found.\n"
260 "section: [%s]" % (k, atoms[1], section))
251261 warnings.warn(msg)
252
262
253263 def check_config_namespaces(self):
254264 """Process config and warn on each unknown config namespace."""
255265 for sn, app in cherrypy.tree.apps.items():
257267 continue
258268 self._known_ns(app)
259269
260
261
262
263270 # -------------------------- Config Types -------------------------- #
264
265271 known_config_types = {}
266
272
267273 def _populate_known_types(self):
268274 b = [x for x in vars(builtins).values()
269275 if type(x) is type(str)]
270
276
271277 def traverse(obj, namespace):
272278 for name in dir(obj):
273279 # Hack for 3.2's warning about body_params
276282 vtype = type(getattr(obj, name, None))
277283 if vtype in b:
278284 self.known_config_types[namespace + "." + name] = vtype
279
285
280286 traverse(cherrypy.request, "request")
281287 traverse(cherrypy.response, "response")
282288 traverse(cherrypy.server, "server")
283289 traverse(cherrypy.engine, "engine")
284290 traverse(cherrypy.log, "log")
285
291
286292 def _known_types(self, config):
287293 msg = ("The config entry %r in section %r is of type %r, "
288294 "which does not match the expected type %r.")
289
295
290296 for section, conf in config.items():
291297 if isinstance(conf, dict):
292298 for k, v in conf.items():
304310 if expected_type and vtype != expected_type:
305311 warnings.warn(msg % (k, section, vtype.__name__,
306312 expected_type.__name__))
307
313
308314 def check_config_types(self):
309315 """Assert that config values are of the same type as default values."""
310316 self._known_types(cherrypy.config)
312318 if not isinstance(app, cherrypy.Application):
313319 continue
314320 self._known_types(app.config)
315
316
321
317322 # -------------------- Specific config warnings -------------------- #
318
319323 def check_localhost(self):
320324 """Warn if any socket_host is 'localhost'. See #711."""
321325 for k, v in cherrypy.config.items():
322326 if k == 'server.socket_host' and v == 'localhost':
323327 warnings.warn("The use of 'localhost' as a socket host can "
324 "cause problems on newer systems, since 'localhost' can "
325 "map to either an IPv4 or an IPv6 address. You should "
326 "use '127.0.0.1' or '[::1]' instead.")
328 "cause problems on newer systems, since "
329 "'localhost' can map to either an IPv4 or an "
330 "IPv6 address. You should use '127.0.0.1' "
331 "or '[::1]' instead.")
1717 import os
1818 import re
1919 import sys
20 import threading
2021
2122 if sys.version_info >= (3, 0):
2223 py3k = True
2425 unicodestr = str
2526 nativestr = unicodestr
2627 basestring = (bytes, str)
28
2729 def ntob(n, encoding='ISO-8859-1'):
28 """Return the given native string as a byte string in the given encoding."""
30 """Return the given native string as a byte string in the given
31 encoding.
32 """
33 assert_native(n)
2934 # In Python 3, the native string type is unicode
3035 return n.encode(encoding)
36
3137 def ntou(n, encoding='ISO-8859-1'):
32 """Return the given native string as a unicode string with the given encoding."""
38 """Return the given native string as a unicode string with the given
39 encoding.
40 """
41 assert_native(n)
3342 # In Python 3, the native string type is unicode
3443 return n
44
3545 def tonative(n, encoding='ISO-8859-1'):
3646 """Return the given string as a native string in the given encoding."""
3747 # In Python 3, the native string type is unicode
4959 unicodestr = unicode
5060 nativestr = bytestr
5161 basestring = basestring
62
5263 def ntob(n, encoding='ISO-8859-1'):
53 """Return the given native string as a byte string in the given encoding."""
64 """Return the given native string as a byte string in the given
65 encoding.
66 """
67 assert_native(n)
5468 # In Python 2, the native string type is bytes. Assume it's already
5569 # in the given encoding, which for ISO-8859-1 is almost always what
5670 # was intended.
5771 return n
72
5873 def ntou(n, encoding='ISO-8859-1'):
59 """Return the given native string as a unicode string with the given encoding."""
74 """Return the given native string as a unicode string with the given
75 encoding.
76 """
77 assert_native(n)
6078 # In Python 2, the native string type is bytes.
61 # First, check for the special encoding 'escape'. The test suite uses this
62 # to signal that it wants to pass a string with embedded \uXXXX escapes,
63 # but without having to prefix it with u'' for Python 2, but no prefix
64 # for Python 3.
79 # First, check for the special encoding 'escape'. The test suite uses
80 # this to signal that it wants to pass a string with embedded \uXXXX
81 # escapes, but without having to prefix it with u'' for Python 2,
82 # but no prefix for Python 3.
6583 if encoding == 'escape':
6684 return unicode(
6785 re.sub(r'\\u([0-9a-zA-Z]{4})',
6886 lambda m: unichr(int(m.group(1), 16)),
6987 n.decode('ISO-8859-1')))
70 # Assume it's already in the given encoding, which for ISO-8859-1 is almost
71 # always what was intended.
88 # Assume it's already in the given encoding, which for ISO-8859-1
89 # is almost always what was intended.
7290 return n.decode(encoding)
91
7392 def tonative(n, encoding='ISO-8859-1'):
7493 """Return the given string as a native string in the given encoding."""
7594 # In Python 2, the native string type is bytes.
85104 # bytes:
86105 BytesIO = StringIO
87106
107
108 def assert_native(n):
109 if not isinstance(n, nativestr):
110 raise TypeError("n must be a native str (got %s)" % type(n).__name__)
111
88112 try:
89113 set = set
90114 except NameError:
98122 # since CherryPy claims compability with Python 2.3, we must use
99123 # the legacy API of base64
100124 from base64 import decodestring as _base64_decodebytes
125
101126
102127 def base64_decode(n, encoding='ISO-8859-1'):
103128 """Return the native string base64-decoded (as a native string)."""
197222 import __builtin__ as builtins
198223
199224 try:
200 # Python 2. We have to do it in this order so Python 2 builds
225 # Python 2. We try Python 2 first clients on Python 2
201226 # don't try to import the 'http' module from cherrypy.lib
202227 from Cookie import SimpleCookie, CookieError
203 from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
228 from httplib import BadStatusLine, HTTPConnection, IncompleteRead
229 from httplib import NotConnected
204230 from BaseHTTPServer import BaseHTTPRequestHandler
205231 except ImportError:
206232 # Python 3
207233 from http.cookies import SimpleCookie, CookieError
208 from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
234 from http.client import BadStatusLine, HTTPConnection, IncompleteRead
235 from http.client import NotConnected
209236 from http.server import BaseHTTPRequestHandler
210237
211 try:
212 # Python 2. We have to do it in this order so Python 2 builds
213 # don't try to import the 'http' module from cherrypy.lib
214 from httplib import HTTPSConnection
215 except ImportError:
238 # Some platforms don't expose HTTPSConnection, so handle it separately
239 if py3k:
216240 try:
217 # Python 3
218241 from http.client import HTTPSConnection
219242 except ImportError:
220243 # Some platforms which don't have SSL don't expose HTTPSConnection
244 HTTPSConnection = None
245 else:
246 try:
247 from httplib import HTTPSConnection
248 except ImportError:
221249 HTTPSConnection = None
222250
223251 try:
232260 # Python 2.6+
233261 def get_daemon(t):
234262 return t.daemon
263
235264 def set_daemon(t, val):
236265 t.daemon = val
237266 else:
238267 def get_daemon(t):
239268 return t.isDaemon()
269
240270 def set_daemon(t, val):
241271 t.setDaemon(val)
242272
243273 try:
244274 from email.utils import formatdate
275
245276 def HTTPDate(timeval=None):
246277 return formatdate(timeval, usegmt=True)
247278 except ImportError:
250281 try:
251282 # Python 3
252283 from urllib.parse import unquote as parse_unquote
284
253285 def unquote_qs(atom, encoding, errors='strict'):
254 return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors)
286 return parse_unquote(
287 atom.replace('+', ' '),
288 encoding=encoding,
289 errors=errors)
255290 except ImportError:
256291 # Python 2
257292 from urllib import unquote as parse_unquote
293
258294 def unquote_qs(atom, encoding, errors='strict'):
259295 return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
260296
261297 try:
262 # Prefer simplejson, which is usually more advanced than the builtin module.
298 # Prefer simplejson, which is usually more advanced than the builtin
299 # module.
263300 import simplejson as json
264301 json_decode = json.JSONDecoder().decode
265 json_encode = json.JSONEncoder().iterencode
266 except ImportError:
267 if py3k:
268 # Python 3.0: json is part of the standard library,
269 # but outputs unicode. We need bytes.
302 _json_encode = json.JSONEncoder().iterencode
303 except ImportError:
304 if sys.version_info >= (2, 6):
305 # Python >=2.6 : json is part of the standard library
270306 import json
271307 json_decode = json.JSONDecoder().decode
272308 _json_encode = json.JSONEncoder().iterencode
309 else:
310 json = None
311
312 def json_decode(s):
313 raise ValueError('No JSON library is available')
314
315 def _json_encode(s):
316 raise ValueError('No JSON library is available')
317 finally:
318 if json and py3k:
319 # The two Python 3 implementations (simplejson/json)
320 # outputs str. We need bytes.
273321 def json_encode(value):
274322 for chunk in _json_encode(value):
275323 yield chunk.encode('utf8')
276 elif sys.version_info >= (2, 6):
277 # Python 2.6: json is part of the standard library
278 import json
279 json_decode = json.JSONDecoder().decode
280 json_encode = json.JSONEncoder().iterencode
281324 else:
282 json = None
283 def json_decode(s):
284 raise ValueError('No JSON library is available')
285 def json_encode(s):
286 raise ValueError('No JSON library is available')
325 json_encode = _json_encode
326
287327
288328 try:
289329 import cPickle as pickle
295335 try:
296336 os.urandom(20)
297337 import binascii
338
298339 def random20():
299340 return binascii.hexlify(os.urandom(20)).decode('ascii')
300341 except (AttributeError, NotImplementedError):
301342 import random
302343 # os.urandom not available until Python 2.4. Fall back to random.random.
344
303345 def random20():
304346 return sha('%s' % random.random()).hexdigest()
305347
315357 # Python 2
316358 def next(i):
317359 return i.next()
360
361 if sys.version_info >= (3, 3):
362 Timer = threading.Timer
363 Event = threading.Event
364 else:
365 # Python 3.2 and earlier
366 Timer = threading._Timer
367 Event = threading._Event
368
369 # Prior to Python 2.6, the Thread class did not have a .daemon property.
370 # This mix-in adds that property.
371
372
373 class SetDaemonProperty:
374
375 def __get_daemon(self):
376 return self.isDaemon()
377
378 def __set_daemon(self, daemon):
379 self.setDaemon(daemon)
380
381 if sys.version_info < (2, 6):
382 daemon = property(__get_daemon, __set_daemon)
0 # subprocess - Subprocesses with accessible I/O streams
1 #
2 # For more information about this module, see PEP 324.
3 #
4 # This module should remain compatible with Python 2.2, see PEP 291.
5 #
6 # Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se>
7 #
8 # Licensed to PSF under a Contributor Agreement.
9 # See http://www.python.org/2.4/license for licensing details.
10
11 r"""subprocess - Subprocesses with accessible I/O streams
12
13 This module allows you to spawn processes, connect to their
14 input/output/error pipes, and obtain their return codes. This module
15 intends to replace several other, older modules and functions, like:
16
17 os.system
18 os.spawn*
19 os.popen*
20 popen2.*
21 commands.*
22
23 Information about how the subprocess module can be used to replace these
24 modules and functions can be found below.
25
26
27
28 Using the subprocess module
29 ===========================
30 This module defines one class called Popen:
31
32 class Popen(args, bufsize=0, executable=None,
33 stdin=None, stdout=None, stderr=None,
34 preexec_fn=None, close_fds=False, shell=False,
35 cwd=None, env=None, universal_newlines=False,
36 startupinfo=None, creationflags=0):
37
38
39 Arguments are:
40
41 args should be a string, or a sequence of program arguments. The
42 program to execute is normally the first item in the args sequence or
43 string, but can be explicitly set by using the executable argument.
44
45 On UNIX, with shell=False (default): In this case, the Popen class
46 uses os.execvp() to execute the child program. args should normally
47 be a sequence. A string will be treated as a sequence with the string
48 as the only item (the program to execute).
49
50 On UNIX, with shell=True: If args is a string, it specifies the
51 command string to execute through the shell. If args is a sequence,
52 the first item specifies the command string, and any additional items
53 will be treated as additional shell arguments.
54
55 On Windows: the Popen class uses CreateProcess() to execute the child
56 program, which operates on strings. If args is a sequence, it will be
57 converted to a string using the list2cmdline method. Please note that
58 not all MS Windows applications interpret the command line the same
59 way: The list2cmdline is designed for applications using the same
60 rules as the MS C runtime.
61
62 bufsize, if given, has the same meaning as the corresponding argument
63 to the built-in open() function: 0 means unbuffered, 1 means line
64 buffered, any other positive value means use a buffer of
65 (approximately) that size. A negative bufsize means to use the system
66 default, which usually means fully buffered. The default value for
67 bufsize is 0 (unbuffered).
68
69 stdin, stdout and stderr specify the executed programs' standard
70 input, standard output and standard error file handles, respectively.
71 Valid values are PIPE, an existing file descriptor (a positive
72 integer), an existing file object, and None. PIPE indicates that a
73 new pipe to the child should be created. With None, no redirection
74 will occur; the child's file handles will be inherited from the
75 parent. Additionally, stderr can be STDOUT, which indicates that the
76 stderr data from the applications should be captured into the same
77 file handle as for stdout.
78
79 If preexec_fn is set to a callable object, this object will be called
80 in the child process just before the child is executed.
81
82 If close_fds is true, all file descriptors except 0, 1 and 2 will be
83 closed before the child process is executed.
84
85 if shell is true, the specified command will be executed through the
86 shell.
87
88 If cwd is not None, the current directory will be changed to cwd
89 before the child is executed.
90
91 If env is not None, it defines the environment variables for the new
92 process.
93
94 If universal_newlines is true, the file objects stdout and stderr are
95 opened as a text files, but lines may be terminated by any of '\n',
96 the Unix end-of-line convention, '\r', the Macintosh convention or
97 '\r\n', the Windows convention. All of these external representations
98 are seen as '\n' by the Python program. Note: This feature is only
99 available if Python is built with universal newline support (the
100 default). Also, the newlines attribute of the file objects stdout,
101 stdin and stderr are not updated by the communicate() method.
102
103 The startupinfo and creationflags, if given, will be passed to the
104 underlying CreateProcess() function. They can specify things such as
105 appearance of the main window and priority for the new process.
106 (Windows only)
107
108
109 This module also defines some shortcut functions:
110
111 call(*popenargs, **kwargs):
112 Run command with arguments. Wait for command to complete, then
113 return the returncode attribute.
114
115 The arguments are the same as for the Popen constructor. Example:
116
117 retcode = call(["ls", "-l"])
118
119 check_call(*popenargs, **kwargs):
120 Run command with arguments. Wait for command to complete. If the
121 exit code was zero then return, otherwise raise
122 CalledProcessError. The CalledProcessError object will have the
123 return code in the returncode attribute.
124
125 The arguments are the same as for the Popen constructor. Example:
126
127 check_call(["ls", "-l"])
128
129 check_output(*popenargs, **kwargs):
130 Run command with arguments and return its output as a byte string.
131
132 If the exit code was non-zero it raises a CalledProcessError. The
133 CalledProcessError object will have the return code in the returncode
134 attribute and output in the output attribute.
135
136 The arguments are the same as for the Popen constructor. Example:
137
138 output = check_output(["ls", "-l", "/dev/null"])
139
140
141 Exceptions
142 ----------
143 Exceptions raised in the child process, before the new program has
144 started to execute, will be re-raised in the parent. Additionally,
145 the exception object will have one extra attribute called
146 'child_traceback', which is a string containing traceback information
147 from the childs point of view.
148
149 The most common exception raised is OSError. This occurs, for
150 example, when trying to execute a non-existent file. Applications
151 should prepare for OSErrors.
152
153 A ValueError will be raised if Popen is called with invalid arguments.
154
155 check_call() and check_output() will raise CalledProcessError, if the
156 called process returns a non-zero return code.
157
158
159 Security
160 --------
161 Unlike some other popen functions, this implementation will never call
162 /bin/sh implicitly. This means that all characters, including shell
163 metacharacters, can safely be passed to child processes.
164
165
166 Popen objects
167 =============
168 Instances of the Popen class have the following methods:
169
170 poll()
171 Check if child process has terminated. Returns returncode
172 attribute.
173
174 wait()
175 Wait for child process to terminate. Returns returncode attribute.
176
177 communicate(input=None)
178 Interact with process: Send data to stdin. Read data from stdout
179 and stderr, until end-of-file is reached. Wait for process to
180 terminate. The optional input argument should be a string to be
181 sent to the child process, or None, if no data should be sent to
182 the child.
183
184 communicate() returns a tuple (stdout, stderr).
185
186 Note: The data read is buffered in memory, so do not use this
187 method if the data size is large or unlimited.
188
189 The following attributes are also available:
190
191 stdin
192 If the stdin argument is PIPE, this attribute is a file object
193 that provides input to the child process. Otherwise, it is None.
194
195 stdout
196 If the stdout argument is PIPE, this attribute is a file object
197 that provides output from the child process. Otherwise, it is
198 None.
199
200 stderr
201 If the stderr argument is PIPE, this attribute is file object that
202 provides error output from the child process. Otherwise, it is
203 None.
204
205 pid
206 The process ID of the child process.
207
208 returncode
209 The child return code. A None value indicates that the process
210 hasn't terminated yet. A negative value -N indicates that the
211 child was terminated by signal N (UNIX only).
212
213
214 Replacing older functions with the subprocess module
215 ====================================================
216 In this section, "a ==> b" means that b can be used as a replacement
217 for a.
218
219 Note: All functions in this section fail (more or less) silently if
220 the executed program cannot be found; this module raises an OSError
221 exception.
222
223 In the following examples, we assume that the subprocess module is
224 imported with "from subprocess import *".
225
226
227 Replacing /bin/sh shell backquote
228 ---------------------------------
229 output=`mycmd myarg`
230 ==>
231 output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]
232
233
234 Replacing shell pipe line
235 -------------------------
236 output=`dmesg | grep hda`
237 ==>
238 p1 = Popen(["dmesg"], stdout=PIPE)
239 p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
240 output = p2.communicate()[0]
241
242
243 Replacing os.system()
244 ---------------------
245 sts = os.system("mycmd" + " myarg")
246 ==>
247 p = Popen("mycmd" + " myarg", shell=True)
248 pid, sts = os.waitpid(p.pid, 0)
249
250 Note:
251
252 * Calling the program through the shell is usually not required.
253
254 * It's easier to look at the returncode attribute than the
255 exitstatus.
256
257 A more real-world example would look like this:
258
259 try:
260 retcode = call("mycmd" + " myarg", shell=True)
261 if retcode < 0:
262 print >>sys.stderr, "Child was terminated by signal", -retcode
263 else:
264 print >>sys.stderr, "Child returned", retcode
265 except OSError, e:
266 print >>sys.stderr, "Execution failed:", e
267
268
269 Replacing os.spawn*
270 -------------------
271 P_NOWAIT example:
272
273 pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
274 ==>
275 pid = Popen(["/bin/mycmd", "myarg"]).pid
276
277
278 P_WAIT example:
279
280 retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
281 ==>
282 retcode = call(["/bin/mycmd", "myarg"])
283
284
285 Vector example:
286
287 os.spawnvp(os.P_NOWAIT, path, args)
288 ==>
289 Popen([path] + args[1:])
290
291
292 Environment example:
293
294 os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
295 ==>
296 Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})
297
298
299 Replacing os.popen*
300 -------------------
301 pipe = os.popen("cmd", mode='r', bufsize)
302 ==>
303 pipe = Popen("cmd", shell=True, bufsize=bufsize, stdout=PIPE).stdout
304
305 pipe = os.popen("cmd", mode='w', bufsize)
306 ==>
307 pipe = Popen("cmd", shell=True, bufsize=bufsize, stdin=PIPE).stdin
308
309
310 (child_stdin, child_stdout) = os.popen2("cmd", mode, bufsize)
311 ==>
312 p = Popen("cmd", shell=True, bufsize=bufsize,
313 stdin=PIPE, stdout=PIPE, close_fds=True)
314 (child_stdin, child_stdout) = (p.stdin, p.stdout)
315
316
317 (child_stdin,
318 child_stdout,
319 child_stderr) = os.popen3("cmd", mode, bufsize)
320 ==>
321 p = Popen("cmd", shell=True, bufsize=bufsize,
322 stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
323 (child_stdin,
324 child_stdout,
325 child_stderr) = (p.stdin, p.stdout, p.stderr)
326
327
328 (child_stdin, child_stdout_and_stderr) = os.popen4("cmd", mode,
329 bufsize)
330 ==>
331 p = Popen("cmd", shell=True, bufsize=bufsize,
332 stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
333 (child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)
334
335 On Unix, os.popen2, os.popen3 and os.popen4 also accept a sequence as
336 the command to execute, in which case arguments will be passed
337 directly to the program without shell intervention. This usage can be
338 replaced as follows:
339
340 (child_stdin, child_stdout) = os.popen2(["/bin/ls", "-l"], mode,
341 bufsize)
342 ==>
343 p = Popen(["/bin/ls", "-l"], bufsize=bufsize, stdin=PIPE, stdout=PIPE)
344 (child_stdin, child_stdout) = (p.stdin, p.stdout)
345
346 Return code handling translates as follows:
347
348 pipe = os.popen("cmd", 'w')
349 ...
350 rc = pipe.close()
351 if rc is not None and rc % 256:
352 print "There were some errors"
353 ==>
354 process = Popen("cmd", 'w', shell=True, stdin=PIPE)
355 ...
356 process.stdin.close()
357 if process.wait() != 0:
358 print "There were some errors"
359
360
361 Replacing popen2.*
362 ------------------
363 (child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
364 ==>
365 p = Popen(["somestring"], shell=True, bufsize=bufsize
366 stdin=PIPE, stdout=PIPE, close_fds=True)
367 (child_stdout, child_stdin) = (p.stdout, p.stdin)
368
369 On Unix, popen2 also accepts a sequence as the command to execute, in
370 which case arguments will be passed directly to the program without
371 shell intervention. This usage can be replaced as follows:
372
373 (child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize,
374 mode)
375 ==>
376 p = Popen(["mycmd", "myarg"], bufsize=bufsize,
377 stdin=PIPE, stdout=PIPE, close_fds=True)
378 (child_stdout, child_stdin) = (p.stdout, p.stdin)
379
380 The popen2.Popen3 and popen2.Popen4 basically works as subprocess.Popen,
381 except that:
382
383 * subprocess.Popen raises an exception if the execution fails
384 * the capturestderr argument is replaced with the stderr argument.
385 * stdin=PIPE and stdout=PIPE must be specified.
386 * popen2 closes all filedescriptors by default, but you have to specify
387 close_fds=True with subprocess.Popen.
388 """
389
390 import sys
391 mswindows = (sys.platform == "win32")
392
393 import os
394 import types
395 import traceback
396 import gc
397 import signal
398 import errno
399
400 try:
401 set
402 except NameError:
403 from sets import Set as set
404
405 # Exception classes used by this module.
406
407
408 class CalledProcessError(Exception):
409
410 """This exception is raised when a process run by check_call() or
411 check_output() returns a non-zero exit status.
412 The exit status will be stored in the returncode attribute;
413 check_output() will also store the output in the output attribute.
414 """
415
416 def __init__(self, returncode, cmd, output=None):
417 self.returncode = returncode
418 self.cmd = cmd
419 self.output = output
420
421 def __str__(self):
422 return "Command '%s' returned non-zero exit status %d" % (
423 self.cmd, self.returncode)
424
425
426 if mswindows:
427 import threading
428 import msvcrt
429 import _subprocess
430
431 class STARTUPINFO:
432 dwFlags = 0
433 hStdInput = None
434 hStdOutput = None
435 hStdError = None
436 wShowWindow = 0
437
438 class pywintypes:
439 error = IOError
440 else:
441 import select
442 _has_poll = hasattr(select, 'poll')
443 import fcntl
444 import pickle
445
446 # When select or poll has indicated that the file is writable,
447 # we can write up to _PIPE_BUF bytes without risk of blocking.
448 # POSIX defines PIPE_BUF as >= 512.
449 _PIPE_BUF = getattr(select, 'PIPE_BUF', 512)
450
451
452 __all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call",
453 "check_output", "CalledProcessError"]
454
455 if mswindows:
456 from _subprocess import CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, \
457 STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, \
458 STD_ERROR_HANDLE, SW_HIDE, \
459 STARTF_USESTDHANDLES, STARTF_USESHOWWINDOW
460
461 __all__.extend(["CREATE_NEW_CONSOLE", "CREATE_NEW_PROCESS_GROUP",
462 "STD_INPUT_HANDLE", "STD_OUTPUT_HANDLE",
463 "STD_ERROR_HANDLE", "SW_HIDE",
464 "STARTF_USESTDHANDLES", "STARTF_USESHOWWINDOW"])
465 try:
466 MAXFD = os.sysconf("SC_OPEN_MAX")
467 except:
468 MAXFD = 256
469
470 _active = []
471
472
473 def _cleanup():
474 for inst in _active[:]:
475 res = inst._internal_poll(_deadstate=sys.maxint)
476 if res is not None:
477 try:
478 _active.remove(inst)
479 except ValueError:
480 # This can happen if two threads create a new Popen instance.
481 # It's harmless that it was already removed, so ignore.
482 pass
483
484 PIPE = -1
485 STDOUT = -2
486
487
488 def _eintr_retry_call(func, *args):
489 while True:
490 try:
491 return func(*args)
492 except (OSError, IOError), e:
493 if e.errno == errno.EINTR:
494 continue
495 raise
496
497
498 def call(*popenargs, **kwargs):
499 """Run command with arguments. Wait for command to complete, then
500 return the returncode attribute.
501
502 The arguments are the same as for the Popen constructor. Example:
503
504 retcode = call(["ls", "-l"])
505 """
506 return Popen(*popenargs, **kwargs).wait()
507
508
509 def check_call(*popenargs, **kwargs):
510 """Run command with arguments. Wait for command to complete. If
511 the exit code was zero then return, otherwise raise
512 CalledProcessError. The CalledProcessError object will have the
513 return code in the returncode attribute.
514
515 The arguments are the same as for the Popen constructor. Example:
516
517 check_call(["ls", "-l"])
518 """
519 retcode = call(*popenargs, **kwargs)
520 if retcode:
521 cmd = kwargs.get("args")
522 if cmd is None:
523 cmd = popenargs[0]
524 raise CalledProcessError(retcode, cmd)
525 return 0
526
527
528 def check_output(*popenargs, **kwargs):
529 r"""Run command with arguments and return its output as a byte string.
530
531 If the exit code was non-zero it raises a CalledProcessError. The
532 CalledProcessError object will have the return code in the returncode
533 attribute and output in the output attribute.
534
535 The arguments are the same as for the Popen constructor. Example:
536
537 >>> check_output(["ls", "-l", "/dev/null"])
538 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n'
539
540 The stdout argument is not allowed as it is used internally.
541 To capture standard error in the result, use stderr=STDOUT.
542
543 >>> check_output(["/bin/sh", "-c",
544 ... "ls -l non_existent_file ; exit 0"],
545 ... stderr=STDOUT)
546 'ls: non_existent_file: No such file or directory\n'
547 """
548 if 'stdout' in kwargs:
549 raise ValueError('stdout argument not allowed, it will be overridden.')
550 process = Popen(stdout=PIPE, *popenargs, **kwargs)
551 output, unused_err = process.communicate()
552 retcode = process.poll()
553 if retcode:
554 cmd = kwargs.get("args")
555 if cmd is None:
556 cmd = popenargs[0]
557 raise CalledProcessError(retcode, cmd, output=output)
558 return output
559
560
561 def list2cmdline(seq):
562 """
563 Translate a sequence of arguments into a command line
564 string, using the same rules as the MS C runtime:
565
566 1) Arguments are delimited by white space, which is either a
567 space or a tab.
568
569 2) A string surrounded by double quotation marks is
570 interpreted as a single argument, regardless of white space
571 contained within. A quoted string can be embedded in an
572 argument.
573
574 3) A double quotation mark preceded by a backslash is
575 interpreted as a literal double quotation mark.
576
577 4) Backslashes are interpreted literally, unless they
578 immediately precede a double quotation mark.
579
580 5) If backslashes immediately precede a double quotation mark,
581 every pair of backslashes is interpreted as a literal
582 backslash. If the number of backslashes is odd, the last
583 backslash escapes the next double quotation mark as
584 described in rule 3.
585 """
586
587 # See
588 # http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
589 # or search http://msdn.microsoft.com for
590 # "Parsing C++ Command-Line Arguments"
591 result = []
592 needquote = False
593 for arg in seq:
594 bs_buf = []
595
596 # Add a space to separate this argument from the others
597 if result:
598 result.append(' ')
599
600 needquote = (" " in arg) or ("\t" in arg) or not arg
601 if needquote:
602 result.append('"')
603
604 for c in arg:
605 if c == '\\':
606 # Don't know if we need to double yet.
607 bs_buf.append(c)
608 elif c == '"':
609 # Double backslashes.
610 result.append('\\' * len(bs_buf) * 2)
611 bs_buf = []
612 result.append('\\"')
613 else:
614 # Normal char
615 if bs_buf:
616 result.extend(bs_buf)
617 bs_buf = []
618 result.append(c)
619
620 # Add remaining backslashes, if any.
621 if bs_buf:
622 result.extend(bs_buf)
623
624 if needquote:
625 result.extend(bs_buf)
626 result.append('"')
627
628 return ''.join(result)
629
630
631 class Popen(object):
632
633 def __init__(self, args, bufsize=0, executable=None,
634 stdin=None, stdout=None, stderr=None,
635 preexec_fn=None, close_fds=False, shell=False,
636 cwd=None, env=None, universal_newlines=False,
637 startupinfo=None, creationflags=0):
638 """Create new Popen instance."""
639 _cleanup()
640
641 self._child_created = False
642 if not isinstance(bufsize, (int, long)):
643 raise TypeError("bufsize must be an integer")
644
645 if mswindows:
646 if preexec_fn is not None:
647 raise ValueError("preexec_fn is not supported on Windows "
648 "platforms")
649 if close_fds and (stdin is not None or stdout is not None or
650 stderr is not None):
651 raise ValueError("close_fds is not supported on Windows "
652 "platforms if you redirect "
653 "stdin/stdout/stderr")
654 else:
655 # POSIX
656 if startupinfo is not None:
657 raise ValueError("startupinfo is only supported on Windows "
658 "platforms")
659 if creationflags != 0:
660 raise ValueError("creationflags is only supported on Windows "
661 "platforms")
662
663 self.stdin = None
664 self.stdout = None
665 self.stderr = None
666 self.pid = None
667 self.returncode = None
668 self.universal_newlines = universal_newlines
669
670 # Input and output objects. The general principle is like
671 # this:
672 #
673 # Parent Child
674 # ------ -----
675 # p2cwrite ---stdin---> p2cread
676 # c2pread <--stdout--- c2pwrite
677 # errread <--stderr--- errwrite
678 #
679 # On POSIX, the child objects are file descriptors. On
680 # Windows, these are Windows file handles. The parent objects
681 # are file descriptors on both platforms. The parent objects
682 # are None when not using PIPEs. The child objects are None
683 # when not redirecting.
684
685 (p2cread, p2cwrite,
686 c2pread, c2pwrite,
687 errread, errwrite) = self._get_handles(stdin, stdout, stderr)
688
689 self._execute_child(args, executable, preexec_fn, close_fds,
690 cwd, env, universal_newlines,
691 startupinfo, creationflags, shell,
692 p2cread, p2cwrite,
693 c2pread, c2pwrite,
694 errread, errwrite)
695
696 if mswindows:
697 if p2cwrite is not None:
698 p2cwrite = msvcrt.open_osfhandle(p2cwrite.Detach(), 0)
699 if c2pread is not None:
700 c2pread = msvcrt.open_osfhandle(c2pread.Detach(), 0)
701 if errread is not None:
702 errread = msvcrt.open_osfhandle(errread.Detach(), 0)
703
704 if p2cwrite is not None:
705 self.stdin = os.fdopen(p2cwrite, 'wb', bufsize)
706 if c2pread is not None:
707 if universal_newlines:
708 self.stdout = os.fdopen(c2pread, 'rU', bufsize)
709 else:
710 self.stdout = os.fdopen(c2pread, 'rb', bufsize)
711 if errread is not None:
712 if universal_newlines:
713 self.stderr = os.fdopen(errread, 'rU', bufsize)
714 else:
715 self.stderr = os.fdopen(errread, 'rb', bufsize)
716
717 def _translate_newlines(self, data):
718 data = data.replace("\r\n", "\n")
719 data = data.replace("\r", "\n")
720 return data
721
722 def __del__(self, _maxint=sys.maxint, _active=_active):
723 # If __init__ hasn't had a chance to execute (e.g. if it
724 # was passed an undeclared keyword argument), we don't
725 # have a _child_created attribute at all.
726 if not getattr(self, '_child_created', False):
727 # We didn't get to successfully create a child process.
728 return
729 # In case the child hasn't been waited on, check if it's done.
730 self._internal_poll(_deadstate=_maxint)
731 if self.returncode is None and _active is not None:
732 # Child is still running, keep us alive until we can wait on it.
733 _active.append(self)
734
735 def communicate(self, input=None):
736 """Interact with process: Send data to stdin. Read data from
737 stdout and stderr, until end-of-file is reached. Wait for
738 process to terminate. The optional input argument should be a
739 string to be sent to the child process, or None, if no data
740 should be sent to the child.
741
742 communicate() returns a tuple (stdout, stderr)."""
743
744 # Optimization: If we are only using one pipe, or no pipe at
745 # all, using select() or threads is unnecessary.
746 if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
747 stdout = None
748 stderr = None
749 if self.stdin:
750 if input:
751 try:
752 self.stdin.write(input)
753 except IOError, e:
754 if e.errno != errno.EPIPE and e.errno != errno.EINVAL:
755 raise
756 self.stdin.close()
757 elif self.stdout:
758 stdout = _eintr_retry_call(self.stdout.read)
759 self.stdout.close()
760 elif self.stderr:
761 stderr = _eintr_retry_call(self.stderr.read)
762 self.stderr.close()
763 self.wait()
764 return (stdout, stderr)
765
766 return self._communicate(input)
767
768 def poll(self):
769 return self._internal_poll()
770
771 if mswindows:
772 #
773 # Windows methods
774 #
775 def _get_handles(self, stdin, stdout, stderr):
776 """Construct and return tuple with IO objects:
777 p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
778 """
779 if stdin is None and stdout is None and stderr is None:
780 return (None, None, None, None, None, None)
781
782 p2cread, p2cwrite = None, None
783 c2pread, c2pwrite = None, None
784 errread, errwrite = None, None
785
786 if stdin is None:
787 p2cread = _subprocess.GetStdHandle(
788 _subprocess.STD_INPUT_HANDLE)
789 if p2cread is None:
790 p2cread, _ = _subprocess.CreatePipe(None, 0)
791 elif stdin == PIPE:
792 p2cread, p2cwrite = _subprocess.CreatePipe(None, 0)
793 elif isinstance(stdin, int):
794 p2cread = msvcrt.get_osfhandle(stdin)
795 else:
796 # Assuming file-like object
797 p2cread = msvcrt.get_osfhandle(stdin.fileno())
798 p2cread = self._make_inheritable(p2cread)
799
800 if stdout is None:
801 c2pwrite = _subprocess.GetStdHandle(
802 _subprocess.STD_OUTPUT_HANDLE)
803 if c2pwrite is None:
804 _, c2pwrite = _subprocess.CreatePipe(None, 0)
805 elif stdout == PIPE:
806 c2pread, c2pwrite = _subprocess.CreatePipe(None, 0)
807 elif isinstance(stdout, int):
808 c2pwrite = msvcrt.get_osfhandle(stdout)
809 else:
810 # Assuming file-like object
811 c2pwrite = msvcrt.get_osfhandle(stdout.fileno())
812 c2pwrite = self._make_inheritable(c2pwrite)
813
814 if stderr is None:
815 errwrite = _subprocess.GetStdHandle(
816 _subprocess.STD_ERROR_HANDLE)
817 if errwrite is None:
818 _, errwrite = _subprocess.CreatePipe(None, 0)
819 elif stderr == PIPE:
820 errread, errwrite = _subprocess.CreatePipe(None, 0)
821 elif stderr == STDOUT:
822 errwrite = c2pwrite
823 elif isinstance(stderr, int):
824 errwrite = msvcrt.get_osfhandle(stderr)
825 else:
826 # Assuming file-like object
827 errwrite = msvcrt.get_osfhandle(stderr.fileno())
828 errwrite = self._make_inheritable(errwrite)
829
830 return (p2cread, p2cwrite,
831 c2pread, c2pwrite,
832 errread, errwrite)
833
834 def _make_inheritable(self, handle):
835 """Return a duplicate of handle, which is inheritable"""
836 return _subprocess.DuplicateHandle(
837 _subprocess.GetCurrentProcess(),
838 handle,
839 _subprocess.GetCurrentProcess(),
840 0,
841 1,
842 _subprocess.DUPLICATE_SAME_ACCESS
843 )
844
845 def _find_w9xpopen(self):
846 """Find and return absolut path to w9xpopen.exe"""
847 w9xpopen = os.path.join(
848 os.path.dirname(_subprocess.GetModuleFileName(0)),
849 "w9xpopen.exe")
850 if not os.path.exists(w9xpopen):
851 # Eeek - file-not-found - possibly an embedding
852 # situation - see if we can locate it in sys.exec_prefix
853 w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix),
854 "w9xpopen.exe")
855 if not os.path.exists(w9xpopen):
856 raise RuntimeError("Cannot locate w9xpopen.exe, which is "
857 "needed for Popen to work with your "
858 "shell or platform.")
859 return w9xpopen
860
861 def _execute_child(self, args, executable, preexec_fn, close_fds,
862 cwd, env, universal_newlines,
863 startupinfo, creationflags, shell,
864 p2cread, p2cwrite,
865 c2pread, c2pwrite,
866 errread, errwrite):
867 """Execute program (MS Windows version)"""
868
869 if not isinstance(args, types.StringTypes):
870 args = list2cmdline(args)
871
872 # Process startup details
873 if startupinfo is None:
874 startupinfo = STARTUPINFO()
875 if None not in (p2cread, c2pwrite, errwrite):
876 startupinfo.dwFlags |= _subprocess.STARTF_USESTDHANDLES
877 startupinfo.hStdInput = p2cread
878 startupinfo.hStdOutput = c2pwrite
879 startupinfo.hStdError = errwrite
880
881 if shell:
882 startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
883 startupinfo.wShowWindow = _subprocess.SW_HIDE
884 comspec = os.environ.get("COMSPEC", "cmd.exe")
885 args = '{} /c "{}"'.format(comspec, args)
886 if (_subprocess.GetVersion() >= 0x80000000 or
887 os.path.basename(comspec).lower() == "command.com"):
888 # Win9x, or using command.com on NT. We need to
889 # use the w9xpopen intermediate program. For more
890 # information, see KB Q150956
891 # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp)
892 w9xpopen = self._find_w9xpopen()
893 args = '"%s" %s' % (w9xpopen, args)
894 # Not passing CREATE_NEW_CONSOLE has been known to
895 # cause random failures on win9x. Specifically a
896 # dialog: "Your program accessed mem currently in
897 # use at xxx" and a hopeful warning about the
898 # stability of your system. Cost is Ctrl+C wont
899 # kill children.
900 creationflags |= _subprocess.CREATE_NEW_CONSOLE
901
902 # Start the process
903 try:
904 try:
905 hp, ht, pid, tid = _subprocess.CreateProcess(
906 executable, args,
907 # no special
908 # security
909 None, None,
910 int(not close_fds),
911 creationflags,
912 env,
913 cwd,
914 startupinfo)
915 except pywintypes.error, e:
916 # Translate pywintypes.error to WindowsError, which is
917 # a subclass of OSError. FIXME: We should really
918 # translate errno using _sys_errlist (or similar), but
919 # how can this be done from Python?
920 raise WindowsError(*e.args)
921 finally:
922 # Child is launched. Close the parent's copy of those pipe
923 # handles that only the child should have open. You need
924 # to make sure that no handles to the write end of the
925 # output pipe are maintained in this process or else the
926 # pipe will not close when the child process exits and the
927 # ReadFile will hang.
928 if p2cread is not None:
929 p2cread.Close()
930 if c2pwrite is not None:
931 c2pwrite.Close()
932 if errwrite is not None:
933 errwrite.Close()
934
935 # Retain the process handle, but close the thread handle
936 self._child_created = True
937 self._handle = hp
938 self.pid = pid
939 ht.Close()
940
941 def _internal_poll(
942 self, _deadstate=None,
943 _WaitForSingleObject=_subprocess.WaitForSingleObject,
944 _WAIT_OBJECT_0=_subprocess.WAIT_OBJECT_0,
945 _GetExitCodeProcess=_subprocess.GetExitCodeProcess
946 ):
947 """Check if child process has terminated. Returns returncode
948 attribute.
949
950 This method is called by __del__, so it can only refer to objects
951 in its local scope.
952
953 """
954 if self.returncode is None:
955 if _WaitForSingleObject(self._handle, 0) == _WAIT_OBJECT_0:
956 self.returncode = _GetExitCodeProcess(self._handle)
957 return self.returncode
958
959 def wait(self):
960 """Wait for child process to terminate. Returns returncode
961 attribute."""
962 if self.returncode is None:
963 _subprocess.WaitForSingleObject(self._handle,
964 _subprocess.INFINITE)
965 self.returncode = _subprocess.GetExitCodeProcess(self._handle)
966 return self.returncode
967
968 def _readerthread(self, fh, buffer):
969 buffer.append(fh.read())
970
971 def _communicate(self, input):
972 stdout = None # Return
973 stderr = None # Return
974
975 if self.stdout:
976 stdout = []
977 stdout_thread = threading.Thread(target=self._readerthread,
978 args=(self.stdout, stdout))
979 stdout_thread.setDaemon(True)
980 stdout_thread.start()
981 if self.stderr:
982 stderr = []
983 stderr_thread = threading.Thread(target=self._readerthread,
984 args=(self.stderr, stderr))
985 stderr_thread.setDaemon(True)
986 stderr_thread.start()
987
988 if self.stdin:
989 if input is not None:
990 try:
991 self.stdin.write(input)
992 except IOError, e:
993 if e.errno != errno.EPIPE:
994 raise
995 self.stdin.close()
996
997 if self.stdout:
998 stdout_thread.join()
999 if self.stderr:
1000 stderr_thread.join()
1001
1002 # All data exchanged. Translate lists into strings.
1003 if stdout is not None:
1004 stdout = stdout[0]
1005 if stderr is not None:
1006 stderr = stderr[0]
1007
1008 # Translate newlines, if requested. We cannot let the file
1009 # object do the translation: It is based on stdio, which is
1010 # impossible to combine with select (unless forcing no
1011 # buffering).
1012 if self.universal_newlines and hasattr(file, 'newlines'):
1013 if stdout:
1014 stdout = self._translate_newlines(stdout)
1015 if stderr:
1016 stderr = self._translate_newlines(stderr)
1017
1018 self.wait()
1019 return (stdout, stderr)
1020
1021 def send_signal(self, sig):
1022 """Send a signal to the process
1023 """
1024 if sig == signal.SIGTERM:
1025 self.terminate()
1026 elif sig == signal.CTRL_C_EVENT:
1027 os.kill(self.pid, signal.CTRL_C_EVENT)
1028 elif sig == signal.CTRL_BREAK_EVENT:
1029 os.kill(self.pid, signal.CTRL_BREAK_EVENT)
1030 else:
1031 raise ValueError("Unsupported signal: {}".format(sig))
1032
1033 def terminate(self):
1034 """Terminates the process
1035 """
1036 _subprocess.TerminateProcess(self._handle, 1)
1037
1038 kill = terminate
1039
1040 else:
1041 #
1042 # POSIX methods
1043 #
1044 def _get_handles(self, stdin, stdout, stderr):
1045 """Construct and return tuple with IO objects:
1046 p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
1047 """
1048 p2cread, p2cwrite = None, None
1049 c2pread, c2pwrite = None, None
1050 errread, errwrite = None, None
1051
1052 if stdin is None:
1053 pass
1054 elif stdin == PIPE:
1055 p2cread, p2cwrite = self.pipe_cloexec()
1056 elif isinstance(stdin, int):
1057 p2cread = stdin
1058 else:
1059 # Assuming file-like object
1060 p2cread = stdin.fileno()
1061
1062 if stdout is None:
1063 pass
1064 elif stdout == PIPE:
1065 c2pread, c2pwrite = self.pipe_cloexec()
1066 elif isinstance(stdout, int):
1067 c2pwrite = stdout
1068 else:
1069 # Assuming file-like object
1070 c2pwrite = stdout.fileno()
1071
1072 if stderr is None:
1073 pass
1074 elif stderr == PIPE:
1075 errread, errwrite = self.pipe_cloexec()
1076 elif stderr == STDOUT:
1077 errwrite = c2pwrite
1078 elif isinstance(stderr, int):
1079 errwrite = stderr
1080 else:
1081 # Assuming file-like object
1082 errwrite = stderr.fileno()
1083
1084 return (p2cread, p2cwrite,
1085 c2pread, c2pwrite,
1086 errread, errwrite)
1087
1088 def _set_cloexec_flag(self, fd, cloexec=True):
1089 try:
1090 cloexec_flag = fcntl.FD_CLOEXEC
1091 except AttributeError:
1092 cloexec_flag = 1
1093
1094 old = fcntl.fcntl(fd, fcntl.F_GETFD)
1095 if cloexec:
1096 fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag)
1097 else:
1098 fcntl.fcntl(fd, fcntl.F_SETFD, old & ~cloexec_flag)
1099
1100 def pipe_cloexec(self):
1101 """Create a pipe with FDs set CLOEXEC."""
1102 # Pipes' FDs are set CLOEXEC by default because we don't want them
1103 # to be inherited by other subprocesses: the CLOEXEC flag is
1104 # removed from the child's FDs by _dup2(), between fork() and
1105 # exec().
1106 # This is not atomic: we would need the pipe2() syscall for that.
1107 r, w = os.pipe()
1108 self._set_cloexec_flag(r)
1109 self._set_cloexec_flag(w)
1110 return r, w
1111
1112 def _close_fds(self, but):
1113 if hasattr(os, 'closerange'):
1114 os.closerange(3, but)
1115 os.closerange(but + 1, MAXFD)
1116 else:
1117 for i in xrange(3, MAXFD):
1118 if i == but:
1119 continue
1120 try:
1121 os.close(i)
1122 except:
1123 pass
1124
1125 def _execute_child(self, args, executable, preexec_fn, close_fds,
1126 cwd, env, universal_newlines,
1127 startupinfo, creationflags, shell,
1128 p2cread, p2cwrite,
1129 c2pread, c2pwrite,
1130 errread, errwrite):
1131 """Execute program (POSIX version)"""
1132
1133 if isinstance(args, types.StringTypes):
1134 args = [args]
1135 else:
1136 args = list(args)
1137
1138 if shell:
1139 args = ["/bin/sh", "-c"] + args
1140 if executable:
1141 args[0] = executable
1142
1143 if executable is None:
1144 executable = args[0]
1145
1146 # For transferring possible exec failure from child to parent
1147 # The first char specifies the exception type: 0 means
1148 # OSError, 1 means some other error.
1149 errpipe_read, errpipe_write = self.pipe_cloexec()
1150 try:
1151 try:
1152 gc_was_enabled = gc.isenabled()
1153 # Disable gc to avoid bug where gc -> file_dealloc ->
1154 # write to stderr -> hang.
1155 # http://bugs.python.org/issue1336
1156 gc.disable()
1157 try:
1158 self.pid = os.fork()
1159 except:
1160 if gc_was_enabled:
1161 gc.enable()
1162 raise
1163 self._child_created = True
1164 if self.pid == 0:
1165 # Child
1166 try:
1167 # Close parent's pipe ends
1168 if p2cwrite is not None:
1169 os.close(p2cwrite)
1170 if c2pread is not None:
1171 os.close(c2pread)
1172 if errread is not None:
1173 os.close(errread)
1174 os.close(errpipe_read)
1175
1176 # When duping fds, if there arises a situation
1177 # where one of the fds is either 0, 1 or 2, it
1178 # is possible that it is overwritten (#12607).
1179 if c2pwrite == 0:
1180 c2pwrite = os.dup(c2pwrite)
1181 if errwrite == 0 or errwrite == 1:
1182 errwrite = os.dup(errwrite)
1183
1184 # Dup fds for child
1185 def _dup2(a, b):
1186 # dup2() removes the CLOEXEC flag but
1187 # we must do it ourselves if dup2()
1188 # would be a no-op (issue #10806).
1189 if a == b:
1190 self._set_cloexec_flag(a, False)
1191 elif a is not None:
1192 os.dup2(a, b)
1193 _dup2(p2cread, 0)
1194 _dup2(c2pwrite, 1)
1195 _dup2(errwrite, 2)
1196
1197 # Close pipe fds. Make sure we don't close the
1198 # same fd more than once, or standard fds.
1199 closed = set([None])
1200 for fd in [p2cread, c2pwrite, errwrite]:
1201 if fd not in closed and fd > 2:
1202 os.close(fd)
1203 closed.add(fd)
1204
1205 # Close all other fds, if asked for
1206 if close_fds:
1207 self._close_fds(but=errpipe_write)
1208
1209 if cwd is not None:
1210 os.chdir(cwd)
1211
1212 if preexec_fn:
1213 preexec_fn()
1214
1215 if env is None:
1216 os.execvp(executable, args)
1217 else:
1218 os.execvpe(executable, args, env)
1219
1220 except:
1221 exc_type, exc_value, tb = sys.exc_info()
1222 # Save the traceback and attach it to the exception
1223 # object
1224 exc_lines = traceback.format_exception(exc_type,
1225 exc_value,
1226 tb)
1227 exc_value.child_traceback = ''.join(exc_lines)
1228 os.write(errpipe_write, pickle.dumps(exc_value))
1229
1230 # This exitcode won't be reported to applications,
1231 # so it really doesn't matter what we return.
1232 os._exit(255)
1233
1234 # Parent
1235 if gc_was_enabled:
1236 gc.enable()
1237 finally:
1238 # be sure the FD is closed no matter what
1239 os.close(errpipe_write)
1240
1241 if p2cread is not None and p2cwrite is not None:
1242 os.close(p2cread)
1243 if c2pwrite is not None and c2pread is not None:
1244 os.close(c2pwrite)
1245 if errwrite is not None and errread is not None:
1246 os.close(errwrite)
1247
1248 # Wait for exec to fail or succeed; possibly raising exception
1249 # Exception limited to 1M
1250 data = _eintr_retry_call(os.read, errpipe_read, 1048576)
1251 finally:
1252 # be sure the FD is closed no matter what
1253 os.close(errpipe_read)
1254
1255 if data != "":
1256 try:
1257 _eintr_retry_call(os.waitpid, self.pid, 0)
1258 except OSError, e:
1259 if e.errno != errno.ECHILD:
1260 raise
1261 child_exception = pickle.loads(data)
1262 for fd in (p2cwrite, c2pread, errread):
1263 if fd is not None:
1264 os.close(fd)
1265 raise child_exception
1266
1267 def _handle_exitstatus(self, sts, _WIFSIGNALED=os.WIFSIGNALED,
1268 _WTERMSIG=os.WTERMSIG, _WIFEXITED=os.WIFEXITED,
1269 _WEXITSTATUS=os.WEXITSTATUS):
1270 # This method is called (indirectly) by __del__, so it cannot
1271 # refer to anything outside of its local scope."""
1272 if _WIFSIGNALED(sts):
1273 self.returncode = -_WTERMSIG(sts)
1274 elif _WIFEXITED(sts):
1275 self.returncode = _WEXITSTATUS(sts)
1276 else:
1277 # Should never happen
1278 raise RuntimeError("Unknown child exit status!")
1279
1280 def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
1281 _WNOHANG=os.WNOHANG, _os_error=os.error):
1282 """Check if child process has terminated. Returns returncode
1283 attribute.
1284
1285 This method is called by __del__, so it cannot reference anything
1286 outside of the local scope (nor can any methods it calls).
1287
1288 """
1289 if self.returncode is None:
1290 try:
1291 pid, sts = _waitpid(self.pid, _WNOHANG)
1292 if pid == self.pid:
1293 self._handle_exitstatus(sts)
1294 except _os_error:
1295 if _deadstate is not None:
1296 self.returncode = _deadstate
1297 return self.returncode
1298
1299 def wait(self):
1300 """Wait for child process to terminate. Returns returncode
1301 attribute."""
1302 if self.returncode is None:
1303 try:
1304 pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
1305 except OSError, e:
1306 if e.errno != errno.ECHILD:
1307 raise
1308 # This happens if SIGCLD is set to be ignored or waiting
1309 # for child processes has otherwise been disabled for our
1310 # process. This child is dead, we can't get the status.
1311 sts = 0
1312 self._handle_exitstatus(sts)
1313 return self.returncode
1314
1315 def _communicate(self, input):
1316 if self.stdin:
1317 # Flush stdio buffer. This might block, if the user has
1318 # been writing to .stdin in an uncontrolled fashion.
1319 self.stdin.flush()
1320 if not input:
1321 self.stdin.close()
1322
1323 if _has_poll:
1324 stdout, stderr = self._communicate_with_poll(input)
1325 else:
1326 stdout, stderr = self._communicate_with_select(input)
1327
1328 # All data exchanged. Translate lists into strings.
1329 if stdout is not None:
1330 stdout = ''.join(stdout)
1331 if stderr is not None:
1332 stderr = ''.join(stderr)
1333
1334 # Translate newlines, if requested. We cannot let the file
1335 # object do the translation: It is based on stdio, which is
1336 # impossible to combine with select (unless forcing no
1337 # buffering).
1338 if self.universal_newlines and hasattr(file, 'newlines'):
1339 if stdout:
1340 stdout = self._translate_newlines(stdout)
1341 if stderr:
1342 stderr = self._translate_newlines(stderr)
1343
1344 self.wait()
1345 return (stdout, stderr)
1346
1347 def _communicate_with_poll(self, input):
1348 stdout = None # Return
1349 stderr = None # Return
1350 fd2file = {}
1351 fd2output = {}
1352
1353 poller = select.poll()
1354
1355 def register_and_append(file_obj, eventmask):
1356 poller.register(file_obj.fileno(), eventmask)
1357 fd2file[file_obj.fileno()] = file_obj
1358
1359 def close_unregister_and_remove(fd):
1360 poller.unregister(fd)
1361 fd2file[fd].close()
1362 fd2file.pop(fd)
1363
1364 if self.stdin and input:
1365 register_and_append(self.stdin, select.POLLOUT)
1366
1367 select_POLLIN_POLLPRI = select.POLLIN | select.POLLPRI
1368 if self.stdout:
1369 register_and_append(self.stdout, select_POLLIN_POLLPRI)
1370 fd2output[self.stdout.fileno()] = stdout = []
1371 if self.stderr:
1372 register_and_append(self.stderr, select_POLLIN_POLLPRI)
1373 fd2output[self.stderr.fileno()] = stderr = []
1374
1375 input_offset = 0
1376 while fd2file:
1377 try:
1378 ready = poller.poll()
1379 except select.error, e:
1380 if e.args[0] == errno.EINTR:
1381 continue
1382 raise
1383
1384 for fd, mode in ready:
1385 if mode & select.POLLOUT:
1386 chunk = input[input_offset: input_offset + _PIPE_BUF]
1387 try:
1388 input_offset += os.write(fd, chunk)
1389 except OSError, e:
1390 if e.errno == errno.EPIPE:
1391 close_unregister_and_remove(fd)
1392 else:
1393 raise
1394 else:
1395 if input_offset >= len(input):
1396 close_unregister_and_remove(fd)
1397 elif mode & select_POLLIN_POLLPRI:
1398 data = os.read(fd, 4096)
1399 if not data:
1400 close_unregister_and_remove(fd)
1401 fd2output[fd].append(data)
1402 else:
1403 # Ignore hang up or errors.
1404 close_unregister_and_remove(fd)
1405
1406 return (stdout, stderr)
1407
1408 def _communicate_with_select(self, input):
1409 read_set = []
1410 write_set = []
1411 stdout = None # Return
1412 stderr = None # Return
1413
1414 if self.stdin and input:
1415 write_set.append(self.stdin)
1416 if self.stdout:
1417 read_set.append(self.stdout)
1418 stdout = []
1419 if self.stderr:
1420 read_set.append(self.stderr)
1421 stderr = []
1422
1423 input_offset = 0
1424 while read_set or write_set:
1425 try:
1426 rlist, wlist, xlist = select.select(
1427 read_set, write_set, [])
1428 except select.error, e:
1429 if e.args[0] == errno.EINTR:
1430 continue
1431 raise
1432
1433 if self.stdin in wlist:
1434 chunk = input[input_offset: input_offset + _PIPE_BUF]
1435 try:
1436 bytes_written = os.write(self.stdin.fileno(), chunk)
1437 except OSError, e:
1438 if e.errno == errno.EPIPE:
1439 self.stdin.close()
1440 write_set.remove(self.stdin)
1441 else:
1442 raise
1443 else:
1444 input_offset += bytes_written
1445 if input_offset >= len(input):
1446 self.stdin.close()
1447 write_set.remove(self.stdin)
1448
1449 if self.stdout in rlist:
1450 data = os.read(self.stdout.fileno(), 1024)
1451 if data == "":
1452 self.stdout.close()
1453 read_set.remove(self.stdout)
1454 stdout.append(data)
1455
1456 if self.stderr in rlist:
1457 data = os.read(self.stderr.fileno(), 1024)
1458 if data == "":
1459 self.stderr.close()
1460 read_set.remove(self.stderr)
1461 stderr.append(data)
1462
1463 return (stdout, stderr)
1464
1465 def send_signal(self, sig):
1466 """Send a signal to the process
1467 """
1468 os.kill(self.pid, sig)
1469
1470 def terminate(self):
1471 """Terminate the process with SIGTERM
1472 """
1473 self.send_signal(signal.SIGTERM)
1474
1475 def kill(self):
1476 """Kill the process with SIGKILL
1477 """
1478 self.send_signal(signal.SIGKILL)
1479
1480
1481 def _demo_posix():
1482 #
1483 # Example 1: Simple redirection: Get process list
1484 #
1485 plist = Popen(["ps"], stdout=PIPE).communicate()[0]
1486 print "Process list:"
1487 print plist
1488
1489 #
1490 # Example 2: Change uid before executing child
1491 #
1492 if os.getuid() == 0:
1493 p = Popen(["id"], preexec_fn=lambda: os.setuid(100))
1494 p.wait()
1495
1496 #
1497 # Example 3: Connecting several subprocesses
1498 #
1499 print "Looking for 'hda'..."
1500 p1 = Popen(["dmesg"], stdout=PIPE)
1501 p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
1502 print repr(p2.communicate()[0])
1503
1504 #
1505 # Example 4: Catch execution error
1506 #
1507 print
1508 print "Trying a weird file..."
1509 try:
1510 print Popen(["/this/path/does/not/exist"]).communicate()
1511 except OSError, e:
1512 if e.errno == errno.ENOENT:
1513 print "The file didn't exist. I thought so..."
1514 print "Child traceback:"
1515 print e.child_traceback
1516 else:
1517 print "Error", e.errno
1518 else:
1519 print >>sys.stderr, "Gosh. No error."
1520
1521
1522 def _demo_windows():
1523 #
1524 # Example 1: Connecting several subprocesses
1525 #
1526 print "Looking for 'PROMPT' in set output..."
1527 p1 = Popen("set", stdout=PIPE, shell=True)
1528 p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE)
1529 print repr(p2.communicate()[0])
1530
1531 #
1532 # Example 2: Simple execution of program
1533 #
1534 print "Executing calc..."
1535 p = Popen("calc")
1536 p.wait()
1537
1538
1539 if __name__ == "__main__":
1540 if mswindows:
1541 _demo_windows()
1542 else:
1543 _demo_posix()
4949
5050 class Demo:
5151 _cp_config = {'tools.gzip.on': True}
52
52
5353 def index(self):
5454 return "Hello world"
5555 index.exposed = True
5656 index._cp_config = {'request.show_tracebacks': False}
5757
5858 .. note::
59
59
6060 This behavior is only guaranteed for the default dispatcher.
6161 Other dispatchers may have different restrictions on where
6262 you can attach _cp_config attributes.
124124 # Deprecated in CherryPy 3.2--remove in 3.3
125125 NamespaceSet = reprconf.NamespaceSet
126126
127
127128 def merge(base, other):
128129 """Merge one app config (from a dict, file, or filename) into another.
129
130
130131 If the given config is a filename, it will be appended to
131132 the list of files to monitor for "autoreload" changes.
132133 """
133134 if isinstance(other, basestring):
134135 cherrypy.engine.autoreload.files.add(other)
135
136
136137 # Load other into base
137138 for section, value_map in reprconf.as_dict(other).items():
138139 if not isinstance(value_map, dict):
145146
146147
147148 class Config(reprconf.Config):
149
148150 """The 'global' configuration data for the entire CherryPy process."""
149151
150152 def update(self, config):
156158
157159 def _apply(self, config):
158160 """Update self from a dict."""
159 if isinstance(config.get("global", None), dict):
161 if isinstance(config.get("global"), dict):
160162 if len(config) > 1:
161163 cherrypy.checker.global_config_contained_paths = True
162164 config = config["global"]
163165 if 'tools.staticdir.dir' in config:
164166 config['tools.staticdir.section'] = "global"
165167 reprconf.Config._apply(self, config)
166
168
167169 def __call__(self, *args, **kwargs):
168170 """Decorator for page handlers to set _cp_config."""
169171 if args:
170172 raise TypeError(
171173 "The cherrypy.config decorator does not accept positional "
172174 "arguments; you must use keyword arguments.")
175
173176 def tool_decorator(f):
174177 if not hasattr(f, "_cp_config"):
175178 f._cp_config = {}
179182 return tool_decorator
180183
181184
185 # Sphinx begin config.environments
182186 Config.environments = environments = {
183187 "staging": {
184 'engine.autoreload_on': False,
188 'engine.autoreload.on': False,
185189 'checker.on': False,
186190 'tools.log_headers.on': False,
187191 'request.show_tracebacks': False,
188192 'request.show_mismatched_params': False,
189 },
193 },
190194 "production": {
191 'engine.autoreload_on': False,
195 'engine.autoreload.on': False,
192196 'checker.on': False,
193197 'tools.log_headers.on': False,
194198 'request.show_tracebacks': False,
195199 'request.show_mismatched_params': False,
196200 'log.screen': False,
197 },
201 },
198202 "embedded": {
199203 # For use with CherryPy embedded in another deployment stack.
200 'engine.autoreload_on': False,
204 'engine.autoreload.on': False,
201205 'checker.on': False,
202206 'tools.log_headers.on': False,
203207 'request.show_tracebacks': False,
205209 'log.screen': False,
206210 'engine.SIGHUP': None,
207211 'engine.SIGTERM': None,
208 },
212 },
209213 "test_suite": {
210 'engine.autoreload_on': False,
214 'engine.autoreload.on': False,
211215 'checker.on': False,
212216 'tools.log_headers.on': False,
213217 'request.show_tracebacks': True,
214218 'request.show_mismatched_params': True,
215219 'log.screen': False,
216 },
217 }
220 },
221 }
222 # Sphinx end config.environments
218223
219224
220225 def _server_namespace_handler(k, v):
225230 # to configure additional HTTP servers.
226231 if not hasattr(cherrypy, "servers"):
227232 cherrypy.servers = {}
228
233
229234 servername, k = atoms
230235 if servername not in cherrypy.servers:
231236 from cherrypy import _cpserver
232237 cherrypy.servers[servername] = _cpserver.Server()
233238 # On by default, but 'on = False' can unsubscribe it (see below).
234239 cherrypy.servers[servername].subscribe()
235
240
236241 if k == 'on':
237242 if v:
238243 cherrypy.servers[servername].subscribe()
244249 setattr(cherrypy.server, k, v)
245250 Config.namespaces["server"] = _server_namespace_handler
246251
252
247253 def _engine_namespace_handler(k, v):
248254 """Backward compatibility handler for the "engine" namespace."""
249255 engine = cherrypy.engine
256
257 deprecated = {
258 'autoreload_on': 'autoreload.on',
259 'autoreload_frequency': 'autoreload.frequency',
260 'autoreload_match': 'autoreload.match',
261 'reload_files': 'autoreload.files',
262 'deadlock_poll_freq': 'timeout_monitor.frequency'
263 }
264
265 if k in deprecated:
266 engine.log(
267 'WARNING: Use of engine.%s is deprecated and will be removed in a '
268 'future version. Use engine.%s instead.' % (k, deprecated[k]))
269
250270 if k == 'autoreload_on':
251271 if v:
252272 engine.autoreload.subscribe()
271291 if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
272292 plugin.subscribe()
273293 return
274 elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'):
294 elif (
295 (not v) and
296 hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
297 ):
275298 plugin.unsubscribe()
276299 return
277300 setattr(plugin, attrname, v)
285308 if isinstance(v, dict):
286309 for script_name, app in v.items():
287310 cherrypy.tree.graft(app, script_name)
288 cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/"))
311 cherrypy.engine.log("Mounted: %s on %s" %
312 (app, script_name or "/"))
289313 else:
290314 cherrypy.tree.graft(v, v.script_name)
291315 cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
292316 Config.namespaces["tree"] = _tree_namespace_handler
293
294
2121
2222
2323 class PageHandler(object):
24
2425 """Callable which sets response.body."""
25
26
2627 def __init__(self, callable, *args, **kwargs):
2728 self.callable = callable
2829 self.args = args
2930 self.kwargs = kwargs
30
31
32 def get_args(self):
33 return cherrypy.serving.request.args
34
35 def set_args(self, args):
36 cherrypy.serving.request.args = args
37 return cherrypy.serving.request.args
38
39 args = property(
40 get_args,
41 set_args,
42 doc="The ordered args should be accessible from post dispatch hooks"
43 )
44
45 def get_kwargs(self):
46 return cherrypy.serving.request.kwargs
47
48 def set_kwargs(self, kwargs):
49 cherrypy.serving.request.kwargs = kwargs
50 return cherrypy.serving.request.kwargs
51
52 kwargs = property(
53 get_kwargs,
54 set_kwargs,
55 doc="The named kwargs should be accessible from post dispatch hooks"
56 )
57
3158 def __call__(self):
3259 try:
3360 return self.callable(*self.args, **self.kwargs)
5380 2. Too little parameters are passed to the function.
5481
5582 There are 3 sources of parameters to a cherrypy handler.
56 1. query string parameters are passed as keyword parameters to the handler.
83 1. query string parameters are passed as keyword parameters to the
84 handler.
5785 2. body parameters are also passed as keyword parameters.
5886 3. when partial matching occurs, the final path atoms are passed as
5987 positional args.
6795 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
6896 except TypeError:
6997 if isinstance(callable, object) and hasattr(callable, '__call__'):
70 (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
98 (args, varargs, varkw,
99 defaults) = inspect.getargspec(callable.__call__)
71100 else:
72 # If it wasn't one of our own types, re-raise
101 # If it wasn't one of our own types, re-raise
73102 # the original error
74103 raise
75104
116145 # 2. not enough body parameters -> 400
117146 # 3. not enough path parts (partial matches) -> 404
118147 #
119 # We can't actually tell which case it is,
148 # We can't actually tell which case it is,
120149 # so I'm raising a 404 because that covers 2/3 of the
121150 # possibilities
122 #
151 #
123152 # In the case where the method does not allow body
124153 # arguments it's definitely a 404.
125154 message = None
126155 if show_mismatched_params:
127 message="Missing parameters: %s" % ",".join(missing_args)
156 message = "Missing parameters: %s" % ",".join(missing_args)
128157 raise cherrypy.HTTPError(404, message=message)
129158
130159 # the extra positional arguments come from the path - 404 Not Found
146175
147176 message = None
148177 if show_mismatched_params:
149 message="Multiple values for parameters: "\
150 "%s" % ",".join(multiple_args)
178 message = "Multiple values for parameters: "\
179 "%s" % ",".join(multiple_args)
151180 raise cherrypy.HTTPError(error, message=message)
152181
153182 if not varkw and varkw_usage > 0:
157186 if extra_qs_params:
158187 message = None
159188 if show_mismatched_params:
160 message="Unexpected query string "\
161 "parameters: %s" % ", ".join(extra_qs_params)
189 message = "Unexpected query string "\
190 "parameters: %s" % ", ".join(extra_qs_params)
162191 raise cherrypy.HTTPError(404, message=message)
163192
164193 # If there were any extra body parameters, it's a 400 Not Found
166195 if extra_body_params:
167196 message = None
168197 if show_mismatched_params:
169 message="Unexpected body parameters: "\
170 "%s" % ", ".join(extra_body_params)
198 message = "Unexpected body parameters: "\
199 "%s" % ", ".join(extra_body_params)
171200 raise cherrypy.HTTPError(400, message=message)
172201
173202
177206 test_callable_spec = lambda callable, args, kwargs: None
178207
179208
180
181209 class LateParamPageHandler(PageHandler):
210
182211 """When passing cherrypy.request.params to the page handler, we do not
183212 want to capture that dict too early; we want to give tools like the
184213 decoding tool a chance to modify the params dict in-between the lookup
186215 takes that into account, and allows request.params to be 'bound late'
187216 (it's more complicated than that, but that's the effect).
188217 """
189
218
190219 def _get_kwargs(self):
191220 kwargs = cherrypy.serving.request.params.copy()
192221 if self._kwargs:
193222 kwargs.update(self._kwargs)
194223 return kwargs
195
224
196225 def _set_kwargs(self, kwargs):
226 cherrypy.serving.request.kwargs = kwargs
197227 self._kwargs = kwargs
198
228
199229 kwargs = property(_get_kwargs, _set_kwargs,
200230 doc='page handler kwargs (with '
201231 'cherrypy.request.params copied in)')
204234 if sys.version_info < (3, 0):
205235 punctuation_to_underscores = string.maketrans(
206236 string.punctuation, '_' * len(string.punctuation))
237
207238 def validate_translator(t):
208239 if not isinstance(t, str) or len(t) != 256:
209 raise ValueError("The translate argument must be a str of len 256.")
240 raise ValueError(
241 "The translate argument must be a str of len 256.")
210242 else:
211243 punctuation_to_underscores = str.maketrans(
212244 string.punctuation, '_' * len(string.punctuation))
245
213246 def validate_translator(t):
214247 if not isinstance(t, dict):
215248 raise ValueError("The translate argument must be a dict.")
216249
250
217251 class Dispatcher(object):
252
218253 """CherryPy Dispatcher which walks a tree of objects to find a handler.
219
254
220255 The tree is rooted at cherrypy.request.app.root, and each hierarchical
221256 component in the path_info argument is matched to a corresponding nested
222257 attribute of the root object. Matching handlers must have an 'exposed'
224259 matches a URI which ends in a slash ("/"). The special method name
225260 "default" may match a portion of the path_info (but only when no longer
226261 substring of the path_info matches some other object).
227
262
228263 This is the default, built-in dispatcher for CherryPy.
229264 """
230
265
231266 dispatch_method_name = '_cp_dispatch'
232267 """
233268 The name of the dispatch method that nodes may optionally implement
234269 to provide their own dynamic dispatch algorithm.
235270 """
236
271
237272 def __init__(self, dispatch_method_name=None,
238273 translate=punctuation_to_underscores):
239274 validate_translator(translate)
245280 """Set handler and config for the current request."""
246281 request = cherrypy.serving.request
247282 func, vpath = self.find_handler(path_info)
248
283
249284 if func:
250285 # Decode any leftover %2F in the virtual_path atoms.
251286 vpath = [x.replace("%2F", "/") for x in vpath]
252287 request.handler = LateParamPageHandler(func, *vpath)
253288 else:
254289 request.handler = cherrypy.NotFound()
255
290
256291 def find_handler(self, path):
257292 """Return the appropriate page handler, plus any virtual path.
258
293
259294 This will return two objects. The first will be a callable,
260295 which can be used to generate page output. Any parameters from
261296 the query string or request body will be sent to that callable
262297 as keyword arguments.
263
298
264299 The callable is found by traversing the application's tree,
265300 starting from cherrypy.request.app.root, and matching path
266301 components to successive objects in the tree. For example, the
267302 URL "/path/to/handler" might return root.path.to.handler.
268
303
269304 The second object returned will be a list of names which are
270305 'virtual path' components: parts of the URL which are dynamic,
271306 and were not used when looking up the handler.
276311 app = request.app
277312 root = app.root
278313 dispatch_name = self.dispatch_method_name
279
314
280315 # Get config for the root object/path.
281316 fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
282317 fullpath_len = len(fullpath)
287322 if "/" in app.config:
288323 nodeconf.update(app.config["/"])
289324 object_trail = [['root', root, nodeconf, segleft]]
290
325
291326 node = root
292327 iternames = fullpath[:]
293328 while iternames:
294329 name = iternames[0]
295330 # map to legal Python identifiers (e.g. replace '.' with '_')
296331 objname = name.translate(self.translate)
297
332
298333 nodeconf = {}
299334 subnode = getattr(node, objname, None)
300335 pre_len = len(iternames)
303338 if dispatch and hasattr(dispatch, '__call__') and not \
304339 getattr(dispatch, 'exposed', False) and \
305340 pre_len > 1:
306 #Don't expose the hidden 'index' token to _cp_dispatch
307 #We skip this if pre_len == 1 since it makes no sense
308 #to call a dispatcher when we have no tokens left.
341 # Don't expose the hidden 'index' token to _cp_dispatch
342 # We skip this if pre_len == 1 since it makes no sense
343 # to call a dispatcher when we have no tokens left.
309344 index_name = iternames.pop()
310345 subnode = dispatch(vpath=iternames)
311346 iternames.append(index_name)
312347 else:
313 #We didn't find a path, but keep processing in case there
314 #is a default() handler.
348 # We didn't find a path, but keep processing in case there
349 # is a default() handler.
315350 iternames.pop(0)
316351 else:
317 #We found the path, remove the vpath entry
352 # We found the path, remove the vpath entry
318353 iternames.pop(0)
319354 segleft = len(iternames)
320355 if segleft > pre_len:
321 #No path segment was removed. Raise an error.
356 # No path segment was removed. Raise an error.
322357 raise cherrypy.CherryPyException(
323358 "A vpath segment was added. Custom dispatchers may only "
324359 + "remove elements. While trying to process "
325360 + "{0} in {1}".format(name, fullpath)
326 )
361 )
327362 elif segleft == pre_len:
328 #Assume that the handler used the current path segment, but
329 #did not pop it. This allows things like
330 #return getattr(self, vpath[0], None)
363 # Assume that the handler used the current path segment, but
364 # did not pop it. This allows things like
365 # return getattr(self, vpath[0], None)
331366 iternames.pop(0)
332367 segleft -= 1
333368 node = subnode
336371 # Get _cp_config attached to this node.
337372 if hasattr(node, "_cp_config"):
338373 nodeconf.update(node._cp_config)
339
374
340375 # Mix in values from app.config for this path.
341376 existing_len = fullpath_len - pre_len
342377 if existing_len != 0:
348383 curpath += '/' + seg
349384 if curpath in app.config:
350385 nodeconf.update(app.config[curpath])
351
386
352387 object_trail.append([name, node, nodeconf, segleft])
353
388
354389 def set_conf():
355 """Collapse all object_trail config into cherrypy.request.config."""
390 """Collapse all object_trail config into cherrypy.request.config.
391 """
356392 base = cherrypy.config.copy()
357393 # Note that we merge the config from each node
358394 # even if that node was None.
359395 for name, obj, conf, segleft in object_trail:
360396 base.update(conf)
361397 if 'tools.staticdir.dir' in conf:
362 base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
398 base['tools.staticdir.section'] = '/' + \
399 '/'.join(fullpath[0:fullpath_len - segleft])
363400 return base
364
401
365402 # Try successive objects (reverse order)
366403 num_candidates = len(object_trail) - 1
367404 for i in range(num_candidates, -1, -1):
368
405
369406 name, candidate, nodeconf, segleft = object_trail[i]
370407 if candidate is None:
371408 continue
372
409
373410 # Try a "default" method on the current leaf.
374411 if hasattr(candidate, "default"):
375412 defhandler = candidate.default
376413 if getattr(defhandler, 'exposed', False):
377414 # Insert any extra _cp_config from the default handler.
378415 conf = getattr(defhandler, "_cp_config", {})
379 object_trail.insert(i+1, ["default", defhandler, conf, segleft])
416 object_trail.insert(
417 i + 1, ["default", defhandler, conf, segleft])
380418 request.config = set_conf()
381 # See http://www.cherrypy.org/ticket/613
419 # See https://bitbucket.org/cherrypy/cherrypy/issue/613
382420 request.is_index = path.endswith("/")
383421 return defhandler, fullpath[fullpath_len - segleft:-1]
384
385 # Uncomment the next line to restrict positional params to "default".
422
423 # Uncomment the next line to restrict positional params to
424 # "default".
386425 # if i < num_candidates - 2: continue
387
426
388427 # Try the current leaf.
389428 if getattr(candidate, 'exposed', False):
390429 request.config = set_conf()
399438 # positional parameters (virtual paths).
400439 request.is_index = False
401440 return candidate, fullpath[fullpath_len - segleft:-1]
402
441
403442 # We didn't find anything
404443 request.config = set_conf()
405444 return None, []
406445
407446
408447 class MethodDispatcher(Dispatcher):
448
409449 """Additional dispatch based on cherrypy.request.method.upper().
410
450
411451 Methods named GET, POST, etc will be called on an exposed class.
412452 The method names must be all caps; the appropriate Allow header
413453 will be output showing all capitalized method names as allowable
414454 HTTP verbs.
415
455
416456 Note that the containing class must be exposed, not the methods.
417457 """
418
458
419459 def __call__(self, path_info):
420460 """Set handler and config for the current request."""
421461 request = cherrypy.serving.request
422462 resource, vpath = self.find_handler(path_info)
423
463
424464 if resource:
425465 # Set Allow header
426466 avail = [m for m in dir(resource) if m.isupper()]
428468 avail.append("HEAD")
429469 avail.sort()
430470 cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
431
471
432472 # Find the subhandler
433473 meth = request.method.upper()
434474 func = getattr(resource, meth, None)
438478 # Grab any _cp_config on the subhandler.
439479 if hasattr(func, "_cp_config"):
440480 request.config.update(func._cp_config)
441
481
442482 # Decode any leftover %2F in the virtual_path atoms.
443483 vpath = [x.replace("%2F", "/") for x in vpath]
444484 request.handler = LateParamPageHandler(func, *vpath)
449489
450490
451491 class RoutesDispatcher(object):
492
452493 """A Routes based dispatcher for CherryPy."""
453
454 def __init__(self, full_result=False):
494
495 def __init__(self, full_result=False, **mapper_options):
455496 """
456497 Routes dispatcher
457498
462503 import routes
463504 self.full_result = full_result
464505 self.controllers = {}
465 self.mapper = routes.Mapper()
506 self.mapper = routes.Mapper(**mapper_options)
466507 self.mapper.controller_scan = self.controllers.keys
467
508
468509 def connect(self, name, route, controller, **kwargs):
469510 self.controllers[name] = controller
470511 self.mapper.connect(name, route, controller=name, **kwargs)
471
512
472513 def redirect(self, url):
473514 raise cherrypy.HTTPRedirect(url)
474
515
475516 def __call__(self, path_info):
476517 """Set handler and config for the current request."""
477518 func = self.find_handler(path_info)
479520 cherrypy.serving.request.handler = LateParamPageHandler(func)
480521 else:
481522 cherrypy.serving.request.handler = cherrypy.NotFound()
482
523
483524 def find_handler(self, path_info):
484525 """Find the right page handler, and set request.config."""
485526 import routes
486
527
487528 request = cherrypy.serving.request
488
529
489530 config = routes.request_config()
490531 config.mapper = self.mapper
491532 if hasattr(request, 'wsgi_environ'):
493534 config.host = request.headers.get('Host', None)
494535 config.protocol = request.scheme
495536 config.redirect = self.redirect
496
537
497538 result = self.mapper.match(path_info)
498
539
499540 config.mapper_dict = result
500541 params = {}
501542 if result:
504545 params.pop('controller', None)
505546 params.pop('action', None)
506547 request.params.update(params)
507
548
508549 # Get config for the root object/path.
509550 request.config = base = cherrypy.config.copy()
510551 curpath = ""
511
552
512553 def merge(nodeconf):
513554 if 'tools.staticdir.dir' in nodeconf:
514555 nodeconf['tools.staticdir.section'] = curpath or "/"
515556 base.update(nodeconf)
516
557
517558 app = request.app
518559 root = app.root
519560 if hasattr(root, "_cp_config"):
520561 merge(root._cp_config)
521562 if "/" in app.config:
522563 merge(app.config["/"])
523
564
524565 # Mix in values from app.config.
525566 atoms = [x for x in path_info.split("/") if x]
526567 if atoms:
531572 curpath = "/".join((curpath, atom))
532573 if curpath in app.config:
533574 merge(app.config[curpath])
534
575
535576 handler = None
536577 if result:
537578 controller = result.get('controller')
542583 # Get config from the controller.
543584 if hasattr(controller, "_cp_config"):
544585 merge(controller._cp_config)
545
586
546587 action = result.get('action')
547588 if action is not None:
548589 handler = getattr(controller, action, None)
549 # Get config from the handler
550 if hasattr(handler, "_cp_config"):
590 # Get config from the handler
591 if hasattr(handler, "_cp_config"):
551592 merge(handler._cp_config)
552593 else:
553594 handler = controller
554
595
555596 # Do the last path atom here so it can
556597 # override the controller's _cp_config.
557598 if last:
558599 curpath = "/".join((curpath, last))
559600 if curpath in app.config:
560601 merge(app.config[curpath])
561
602
562603 return handler
563604
564605
565606 def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
566607 from cherrypy.lib import xmlrpcutil
608
567609 def xmlrpc_dispatch(path_info):
568610 path_info = xmlrpcutil.patched_path(path_info)
569611 return next_dispatcher(path_info)
570612 return xmlrpc_dispatch
571613
572614
573 def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
615 def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
616 **domains):
574617 """
575618 Select a different handler based on the Host header.
576
619
577620 This can be useful when running multiple sites within one CP server.
578621 It allows several domains to point to different parts of a single
579622 website structure. For example::
580
623
581624 http://www.domain.example -> root
582625 http://www.domain2.example -> root/domain2/
583626 http://www.domain2.example:443 -> root/secure
584
627
585628 can be accomplished via the following config::
586
629
587630 [/]
588631 request.dispatch = cherrypy.dispatch.VirtualHost(
589632 **{'www.domain2.example': '/domain2',
590633 'www.domain2.example:443': '/secure',
591634 })
592
635
593636 next_dispatcher
594637 The next dispatcher object in the dispatch chain.
595638 The VirtualHost dispatcher adds a prefix to the URL and calls
596639 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
597
640
598641 use_x_forwarded_host
599642 If True (the default), any "X-Forwarded-Host"
600643 request header will be used instead of the "Host" header. This
601644 is commonly added by HTTP servers (such as Apache) when proxying.
602
645
603646 ``**domains``
604647 A dict of {host header value: virtual prefix} pairs.
605648 The incoming "Host" request header is looked up in this dict,
610653 headers may contain the port number.
611654 """
612655 from cherrypy.lib import httputil
656
613657 def vhost_dispatch(path_info):
614658 request = cherrypy.serving.request
615659 header = request.headers.get
616
660
617661 domain = header('Host', '')
618662 if use_x_forwarded_host:
619663 domain = header("X-Forwarded-Host", domain)
620
664
621665 prefix = domains.get(domain, "")
622666 if prefix:
623667 path_info = httputil.urljoin(prefix, path_info)
624
668
625669 result = next_dispatcher(path_info)
626
627 # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
670
671 # Touch up staticdir config. See
672 # https://bitbucket.org/cherrypy/cherrypy/issue/614.
628673 section = request.config.get('tools.staticdir.section')
629674 if section:
630675 section = section[len(prefix):]
631676 request.config['tools.staticdir.section'] = section
632
677
633678 return result
634679 return vhost_dispatch
635
11
22 CherryPy provides (and uses) exceptions for declaring that the HTTP response
33 should be a status other than the default "200 OK". You can ``raise`` them like
4 normal Python exceptions. You can also call them and they will raise themselves;
5 this means you can set an :class:`HTTPError<cherrypy._cperror.HTTPError>`
4 normal Python exceptions. You can also call them and they will raise
5 themselves; this means you can set an
6 :class:`HTTPError<cherrypy._cperror.HTTPError>`
67 or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
78 :attr:`request.handler<cherrypy._cprequest.Request.handler>`.
89
2021 charge a credit card, you don't want to be charged twice by a redirect!
2122
2223 For this reason, *none* of the 3xx responses permit a user-agent (browser) to
23 resubmit a POST on redirection without first confirming the action with the user:
24 resubmit a POST on redirection without first confirming the action with the
25 user:
2426
2527 ===== ================================= ===========
2628 300 Multiple Choices Confirm with the user
5254 --------------------------
5355
5456 The 'error_page' config namespace can be used to provide custom HTML output for
55 expected responses (like 404 Not Found). Supply a filename from which the output
56 will be read. The contents will be interpolated with the values %(status)s,
57 %(message)s, %(traceback)s, and %(version)s using plain old Python
58 `string formatting <http://www.python.org/doc/2.6.4/library/stdtypes.html#string-formatting-operations>`_.
57 expected responses (like 404 Not Found). Supply a filename from which the
58 output will be read. The contents will be interpolated with the values
59 %(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
60 `string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
5961
6062 ::
6163
62 _cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
64 _cp_config = {
65 'error_page.404': os.path.join(localDir, "static/index.html")
66 }
6367
6468
6569 Beginning in version 3.1, you may also provide a function or other callable as
7175 cherrypy.config.update({'error_page.402': error_page_402})
7276
7377 Also in 3.1, in addition to the numbered error codes, you may also supply
74 "error_page.default" to handle all codes which do not have their own error_page entry.
78 "error_page.default" to handle all codes which do not have their own error_page
79 entry.
7580
7681
7782
8085
8186 CherryPy also has a generic error handling mechanism: whenever an unanticipated
8287 error occurs in your code, it will call
83 :func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to set
84 the response status, headers, and body. By default, this is the same output as
88 :func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
89 set the response status, headers, and body. By default, this is the same
90 output as
8591 :class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
8692 some other behavior, you generally replace "request.error_response".
8793
9298
9399 def handle_error():
94100 cherrypy.response.status = 500
95 cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
96 sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc())
101 cherrypy.response.body = [
102 "<html><body>Sorry, an error occured</body></html>"
103 ]
104 sendMail('error@domain.com',
105 'Error in your web app',
106 _cperror.format_exc())
97107
98108 class Root:
99109 _cp_config = {'request.error_response': handle_error}
100110
101111
102 Note that you have to explicitly set :attr:`response.body <cherrypy._cprequest.Response.body>`
112 Note that you have to explicitly set
113 :attr:`response.body <cherrypy._cprequest.Response.body>`
103114 and not simply return an error message as a result.
104115 """
105116
106117 from cgi import escape as _escape
107118 from sys import exc_info as _exc_info
108119 from traceback import format_exception as _format_exception
109 from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob, tonative, urljoin as _urljoin
120 from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob
121 from cherrypy._cpcompat import tonative, urljoin as _urljoin
110122 from cherrypy.lib import httputil as _httputil
111123
112124
113125 class CherryPyException(Exception):
126
114127 """A base class for CherryPy exceptions."""
115128 pass
116129
117130
118131 class TimeoutError(CherryPyException):
132
119133 """Exception raised when Response.timed_out is detected."""
120134 pass
121135
122136
123137 class InternalRedirect(CherryPyException):
138
124139 """Exception raised to switch to the handler for a different URL.
125
140
126141 This exception will redirect processing to another path within the site
127142 (without informing the client). Provide the new path as an argument when
128 raising the exception. Provide any params in the querystring for the new URL.
143 raising the exception. Provide any params in the querystring for the new
144 URL.
129145 """
130
146
131147 def __init__(self, path, query_string=""):
132148 import cherrypy
133149 self.request = cherrypy.serving.request
134
150
135151 self.query_string = query_string
136152 if "?" in path:
137153 # Separate any params included in the path
138154 path, self.query_string = path.split("?", 1)
139
155
140156 # Note that urljoin will "do the right thing" whether url is:
141157 # 1. a URL relative to root (e.g. "/dummy")
142158 # 2. a URL relative to the current path
143159 # Note that any query string will be discarded.
144160 path = _urljoin(self.request.path_info, path)
145
161
146162 # Set a 'path' member attribute so that code which traps this
147163 # error can have access to it.
148164 self.path = path
149
165
150166 CherryPyException.__init__(self, path, self.query_string)
151167
152168
153169 class HTTPRedirect(CherryPyException):
170
154171 """Exception raised when the request should be redirected.
155
172
156173 This exception will force a HTTP redirect to the URL or URL's you give it.
157174 The new URL must be passed as the first argument to the Exception,
158175 e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
161178
162179 If one of the provided URL is a unicode object, it will be encoded
163180 using the default encoding or the one passed in parameter.
164
181
165182 There are multiple types of redirect, from which you can select via the
166183 ``status`` argument. If you do not provide a ``status`` arg, it defaults to
167184 303 (or 302 if responding with HTTP/1.0).
168
185
169186 Examples::
170
187
171188 raise cherrypy.HTTPRedirect("")
172189 raise cherrypy.HTTPRedirect("/abs/path", 307)
173190 raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
174
191
175192 See :ref:`redirectingpost` for additional caveats.
176193 """
177
194
178195 status = None
179196 """The integer HTTP status code to emit."""
180
197
181198 urls = None
182199 """The list of URL's to emit."""
183200
184201 encoding = 'utf-8'
185202 """The encoding when passed urls are not native strings"""
186
203
187204 def __init__(self, urls, status=None, encoding=None):
188205 import cherrypy
189206 request = cherrypy.serving.request
190
207
191208 if isinstance(urls, basestring):
192209 urls = [urls]
193
210
194211 abs_urls = []
195212 for url in urls:
196213 url = tonative(url, encoding or self.encoding)
197
214
198215 # Note that urljoin will "do the right thing" whether url is:
199216 # 1. a complete URL with host (e.g. "http://www.example.com/test")
200217 # 2. a URL relative to root (e.g. "/dummy")
203220 url = _urljoin(cherrypy.url(), url)
204221 abs_urls.append(url)
205222 self.urls = abs_urls
206
223
207224 # RFC 2616 indicates a 301 response code fits our goal; however,
208225 # browser support for 301 is quite messy. Do 302/303 instead. See
209226 # http://www.alanflavell.org.uk/www/post-redirect.html
216233 status = int(status)
217234 if status < 300 or status > 399:
218235 raise ValueError("status must be between 300 and 399.")
219
236
220237 self.status = status
221238 CherryPyException.__init__(self, abs_urls, status)
222
239
223240 def set_response(self):
224 """Modify cherrypy.response status, headers, and body to represent self.
225
241 """Modify cherrypy.response status, headers, and body to represent
242 self.
243
226244 CherryPy uses this internally, but you can also use it to create an
227245 HTTPRedirect object and set its output without *raising* the exception.
228246 """
229247 import cherrypy
230248 response = cherrypy.serving.response
231249 response.status = status = self.status
232
250
233251 if status in (300, 301, 302, 303, 307):
234252 response.headers['Content-Type'] = "text/html;charset=utf-8"
235253 # "The ... URI SHOULD be given by the Location field
236254 # in the response."
237255 response.headers['Location'] = self.urls[0]
238
256
239257 # "Unless the request method was HEAD, the entity of the response
240258 # SHOULD contain a short hypertext note with a hyperlink to the
241259 # new URI(s)."
242 msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
243 301: "This resource has permanently moved to <a href='%s'>%s</a>.",
244 302: "This resource resides temporarily at <a href='%s'>%s</a>.",
245 303: "This resource can be found at <a href='%s'>%s</a>.",
246 307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
247 }[status]
248 msgs = [msg % (u, u) for u in self.urls]
260 msg = {
261 300: "This resource can be found at ",
262 301: "This resource has permanently moved to ",
263 302: "This resource resides temporarily at ",
264 303: "This resource can be found at ",
265 307: "This resource has moved temporarily to ",
266 }[status]
267 msg += '<a href=%s>%s</a>.'
268 from xml.sax import saxutils
269 msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls]
249270 response.body = ntob("<br />\n".join(msgs), 'utf-8')
250271 # Previous code may have set C-L, so we have to reset it
251272 # (allow finalize to set it).
255276 # "The response MUST include the following header fields:
256277 # Date, unless its omission is required by section 14.18.1"
257278 # The "Date" header should have been set in Response.__init__
258
279
259280 # "...the response SHOULD NOT include other entity-headers."
260281 for key in ('Allow', 'Content-Encoding', 'Content-Language',
261282 'Content-Length', 'Content-Location', 'Content-MD5',
263284 'Last-Modified'):
264285 if key in response.headers:
265286 del response.headers[key]
266
287
267288 # "The 304 response MUST NOT contain a message-body."
268289 response.body = None
269290 # Previous code may have set C-L, so we have to reset it.
277298 response.headers.pop('Content-Length', None)
278299 else:
279300 raise ValueError("The %s status code is unknown." % status)
280
301
281302 def __call__(self):
282303 """Use this exception as a request.handler (raise self)."""
283304 raise self
286307 def clean_headers(status):
287308 """Remove any headers which should not apply to an error response."""
288309 import cherrypy
289
310
290311 response = cherrypy.serving.response
291
312
292313 # Remove headers which applied to the original content,
293314 # but do not apply to the error page.
294315 respheaders = response.headers
297318 "Content-Location", "Content-MD5", "Last-Modified"]:
298319 if key in respheaders:
299320 del respheaders[key]
300
321
301322 if status != 416:
302323 # A server sending a response with status code 416 (Requested
303324 # range not satisfiable) SHOULD include a Content-Range field
310331
311332
312333 class HTTPError(CherryPyException):
334
313335 """Exception used to return an HTTP error code (4xx-5xx) to the client.
314
315 This exception can be used to automatically send a response using a http status
316 code, with an appropriate error page. It takes an optional
336
337 This exception can be used to automatically send a response using a
338 http status code, with an appropriate error page. It takes an optional
317339 ``status`` argument (which must be between 400 and 599); it defaults to 500
318340 ("Internal Server Error"). It also takes an optional ``message`` argument,
319341 which will be returned in the response body. See
320 `RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
342 `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
321343 for a complete list of available error codes and when to use them.
322
344
323345 Examples::
324
346
325347 raise cherrypy.HTTPError(403)
326 raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
348 raise cherrypy.HTTPError(
349 "403 Forbidden", "You are not allowed to access this resource.")
327350 """
328
351
329352 status = None
330 """The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
331
353 """The HTTP status code. May be of type int or str (with a Reason-Phrase).
354 """
355
332356 code = None
333357 """The integer HTTP status code."""
334
358
335359 reason = None
336360 """The HTTP Reason-Phrase string."""
337
361
338362 def __init__(self, status=500, message=None):
339363 self.status = status
340364 try:
341365 self.code, self.reason, defaultmsg = _httputil.valid_status(status)
342366 except ValueError:
343367 raise self.__class__(500, _exc_info()[1].args[0])
344
368
345369 if self.code < 400 or self.code > 599:
346370 raise ValueError("status must be between 400 and 599.")
347
371
348372 # See http://www.python.org/dev/peps/pep-0352/
349373 # self.message = message
350374 self._message = message or defaultmsg
351375 CherryPyException.__init__(self, status, message)
352
376
353377 def set_response(self):
354 """Modify cherrypy.response status, headers, and body to represent self.
355
378 """Modify cherrypy.response status, headers, and body to represent
379 self.
380
356381 CherryPy uses this internally, but you can also use it to create an
357382 HTTPError object and set its output without *raising* the exception.
358383 """
359384 import cherrypy
360
385
361386 response = cherrypy.serving.response
362
387
363388 clean_headers(self.code)
364
389
365390 # In all cases, finalize will be called after this method,
366391 # so don't bother cleaning up response values here.
367392 response.status = self.status
368393 tb = None
369394 if cherrypy.serving.request.show_tracebacks:
370395 tb = format_exc()
371 response.headers['Content-Type'] = "text/html;charset=utf-8"
396
372397 response.headers.pop('Content-Length', None)
373
374 content = ntob(self.get_error_page(self.status, traceback=tb,
375 message=self._message), 'utf-8')
398
399 content = self.get_error_page(self.status, traceback=tb,
400 message=self._message)
376401 response.body = content
377
402
378403 _be_ie_unfriendly(self.code)
379
404
380405 def get_error_page(self, *args, **kwargs):
381406 return get_error_page(*args, **kwargs)
382
407
383408 def __call__(self):
384409 """Use this exception as a request.handler (raise self)."""
385410 raise self
386411
387412
388413 class NotFound(HTTPError):
414
389415 """Exception raised when a URL could not be mapped to any handler (404).
390
416
391417 This is equivalent to raising
392418 :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
393419 """
394
420
395421 def __init__(self, path=None):
396422 if path is None:
397423 import cherrypy
401427 HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
402428
403429
404 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
430 _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
431 "-//W3C//DTD XHTML 1.0 Transitional//EN"
405432 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
406433 <html>
407434 <head>
424451 <p>%(message)s</p>
425452 <pre id="traceback">%(traceback)s</pre>
426453 <div id="powered_by">
427 <span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
454 <span>
455 Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
456 </span>
428457 </div>
429458 </body>
430459 </html>
431460 '''
432461
462
433463 def get_error_page(status, **kwargs):
434464 """Return an HTML page, containing a pretty error response.
435
465
436466 status should be an int or a str.
437467 kwargs will be interpolated into the page template.
438468 """
439469 import cherrypy
440
470
441471 try:
442472 code, reason, message = _httputil.valid_status(status)
443473 except ValueError:
444474 raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
445
475
446476 # We can't use setdefault here, because some
447477 # callers send None for kwarg values.
448478 if kwargs.get('status') is None:
453483 kwargs['traceback'] = ''
454484 if kwargs.get('version') is None:
455485 kwargs['version'] = cherrypy.__version__
456
486
457487 for k, v in iteritems(kwargs):
458488 if v is None:
459489 kwargs[k] = ""
460490 else:
461491 kwargs[k] = _escape(kwargs[k])
462
492
463493 # Use a custom template or callable for the error page?
464494 pages = cherrypy.serving.request.error_page
465495 error_page = pages.get(code) or pages.get('default')
496
497 # Default template, can be overridden below.
498 template = _HTTPErrorTemplate
466499 if error_page:
467500 try:
468501 if hasattr(error_page, '__call__'):
469 return error_page(**kwargs)
502 # The caller function may be setting headers manually,
503 # so we delegate to it completely. We may be returning
504 # an iterator as well as a string here.
505 #
506 # We *must* make sure any content is not unicode.
507 result = error_page(**kwargs)
508 if cherrypy.lib.is_iterator(result):
509 from cherrypy.lib.encoding import UTF8StreamEncoder
510 return UTF8StreamEncoder(result)
511 elif isinstance(result, cherrypy._cpcompat.unicodestr):
512 return result.encode('utf-8')
513 else:
514 if not isinstance(result, cherrypy._cpcompat.bytestr):
515 raise ValueError('error page function did not '
516 'return a bytestring, unicodestring or an '
517 'iterator - returned object of type %s.'
518 % (type(result).__name__))
519 return result
470520 else:
471 data = open(error_page, 'rb').read()
472 return tonative(data) % kwargs
521 # Load the template from this path.
522 template = tonative(open(error_page, 'rb').read())
473523 except:
474524 e = _format_exception(*_exc_info())[-1]
475525 m = kwargs['message']
477527 m += "<br />"
478528 m += "In addition, the custom error page failed:\n<br />%s" % e
479529 kwargs['message'] = m
480
481 return _HTTPErrorTemplate % kwargs
530
531 response = cherrypy.serving.response
532 response.headers['Content-Type'] = "text/html;charset=utf-8"
533 result = template % kwargs
534 return result.encode('utf-8')
535
482536
483537
484538 _ie_friendly_error_sizes = {
485539 400: 512, 403: 256, 404: 512, 405: 256,
486540 406: 512, 408: 512, 409: 512, 410: 256,
487541 500: 512, 501: 512, 505: 512,
488 }
542 }
489543
490544
491545 def _be_ie_unfriendly(status):
492546 import cherrypy
493547 response = cherrypy.serving.response
494
548
495549 # For some statuses, Internet Explorer 5+ shows "friendly error
496550 # messages" instead of our response.body if the body is smaller
497551 # than a given size. Fix this by returning a body over that size
524578 finally:
525579 del exc
526580
581
527582 def bare_error(extrabody=None):
528583 """Produce status, headers, body for a critical error.
529
584
530585 Returns a triple without calling any other questionable functions,
531586 so it should be as error-free as possible. Call it from an HTTP server
532587 if you get errors outside of the request.
533
588
534589 If extrabody is None, a friendly but rather unhelpful error message
535590 is set in the body. If extrabody is a string, it will be appended
536591 as-is to the body.
537592 """
538
593
539594 # The whole point of this function is to be a last line-of-defense
540595 # in handling errors. That is, it must not raise any errors itself;
541596 # it cannot be allowed to fail. Therefore, don't add to it!
542597 # In particular, don't call any other CP functions.
543
598
544599 body = ntob("Unrecoverable error in the server.")
545600 if extrabody is not None:
546601 if not isinstance(extrabody, bytestr):
547602 extrabody = extrabody.encode('utf-8')
548603 body += ntob("\n") + extrabody
549
604
550605 return (ntob("500 Internal Server Error"),
551606 [(ntob('Content-Type'), ntob('text/plain')),
552 (ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))],
607 (ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
553608 [body])
554
555
3333 manager is found at :func:`cherrypy.log`, and the log manager for each
3434 application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
3535 If you're inside a request, the latter is reachable from
36 ``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain
37 a reference to the ``app``: either the return value of
36 ``cherrypy.request.app.log``; if you're outside a request, you'll have to
37 obtain a reference to the ``app``: either the return value of
3838 :func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
39 :func:`quickstart()<cherrypy.quickstart>` instead, via ``cherrypy.tree.apps['/']``.
39 :func:`quickstart()<cherrypy.quickstart>` instead, via
40 ``cherrypy.tree.apps['/']``.
4041
4142 By default, the global logs are named "cherrypy.error" and "cherrypy.access",
4243 and the application logs are named "cherrypy.error.2378745" and
6869
6970 #python
7071 log = app.log
71
72
7273 # Remove the default FileHandlers if present.
7374 log.error_file = ""
7475 log.access_file = ""
75
76
7677 maxBytes = getattr(log, "rot_maxBytes", 10000000)
7778 backupCount = getattr(log, "rot_backupCount", 1000)
78
79
7980 # Make a new RotatingFileHandler for the error log.
8081 fname = getattr(log, "rot_error_file", "error.log")
8182 h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
8283 h.setLevel(DEBUG)
8384 h.setFormatter(_cplogging.logfmt)
8485 log.error_log.addHandler(h)
85
86
8687 # Make a new RotatingFileHandler for the access log.
8788 fname = getattr(log, "rot_access_file", "access.log")
8889 h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
112113
113114
114115 class NullHandler(logging.Handler):
116
115117 """A no-op logging handler to silence the logging.lastResort handler."""
116118
117119 def handle(self, record):
125127
126128
127129 class LogManager(object):
130
128131 """An object to assist both simple and advanced logging.
129
132
130133 ``cherrypy.log`` is an instance of this class.
131134 """
132
135
133136 appid = None
134137 """The id() of the Application object which owns this log manager. If this
135138 is a global log manager, appid is None."""
136
139
137140 error_log = None
138141 """The actual :class:`logging.Logger` instance for error messages."""
139
142
140143 access_log = None
141144 """The actual :class:`logging.Logger` instance for access messages."""
142
145
143146 if py3k:
144147 access_log_format = \
145148 '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
146149 else:
147150 access_log_format = \
148151 '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
149
152
150153 logger_root = None
151154 """The "top-level" logger name.
152
155
153156 This string will be used as the first segment in the Logger names.
154157 The default is "cherrypy", for example, in which case the Logger names
155158 will be of the form::
156
159
157160 cherrypy.error.<appid>
158161 cherrypy.access.<appid>
159162 """
160
163
161164 def __init__(self, appid=None, logger_root="cherrypy"):
162165 self.logger_root = logger_root
163166 self.appid = appid
165168 self.error_log = logging.getLogger("%s.error" % logger_root)
166169 self.access_log = logging.getLogger("%s.access" % logger_root)
167170 else:
168 self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
169 self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
171 self.error_log = logging.getLogger(
172 "%s.error.%s" % (logger_root, appid))
173 self.access_log = logging.getLogger(
174 "%s.access.%s" % (logger_root, appid))
170175 self.error_log.setLevel(logging.INFO)
171176 self.access_log.setLevel(logging.INFO)
172177
185190 h.stream.close()
186191 h.stream = open(h.baseFilename, h.mode)
187192 h.release()
188
189 def error(self, msg='', context='', severity=logging.INFO, traceback=False):
193
194 def error(self, msg='', context='', severity=logging.INFO,
195 traceback=False):
190196 """Write the given ``msg`` to the error log.
191
197
192198 This is not just for errors! Applications may call this at any time
193199 to log application-specific information.
194
200
195201 If ``traceback`` is True, the traceback of the current exception
196202 (if any) will be appended to ``msg``.
197203 """
198204 if traceback:
199205 msg += _cperror.format_exc()
200206 self.error_log.log(severity, ' '.join((self.time(), context, msg)))
201
207
202208 def __call__(self, *args, **kwargs):
203209 """An alias for ``error``."""
204210 return self.error(*args, **kwargs)
205
211
206212 def access(self):
207213 """Write to the access log (in Apache/NCSA Combined Log format).
208
209 See http://httpd.apache.org/docs/2.0/logs.html#combined for format
210 details.
211
214
215 See the
216 `apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_
217 for format details.
218
212219 CherryPy calls this automatically for you. Note there are no arguments;
213220 it collects the data itself from
214221 :class:`cherrypy.request<cherrypy._cprequest.Request>`.
215
222
216223 Like Apache started doing in 2.0.46, non-printable and other special
217224 characters in %r (and we expand that to all parts) are escaped using
218225 \\xhh sequences, where hh stands for the hexadecimal representation
231238 status = response.output_status.split(ntob(" "), 1)[0]
232239 if py3k:
233240 status = status.decode('ISO-8859-1')
234
241
235242 atoms = {'h': remote.name or remote.ip,
236243 'l': '-',
237244 'u': getattr(request, "login", None) or "-",
250257 # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
251258 # and backslash for us. All we have to do is strip the quotes.
252259 v = repr(v)[2:-1]
253
254 # in python 3.0 the repr of bytes (as returned by encode)
260
261 # in python 3.0 the repr of bytes (as returned by encode)
255262 # uses double \'s. But then the logger escapes them yet, again
256263 # resulting in quadruple slashes. Remove the extra one here.
257264 v = v.replace('\\\\', '\\')
258
265
259266 # Escape double-quote.
260267 atoms[k] = v
261
268
262269 try:
263 self.access_log.log(logging.INFO, self.access_log_format.format(**atoms))
270 self.access_log.log(
271 logging.INFO, self.access_log_format.format(**atoms))
264272 except:
265273 self(traceback=True)
266274 else:
274282 v = repr(v)[1:-1]
275283 # Escape double-quote.
276284 atoms[k] = v.replace('"', '\\"')
277
285
278286 try:
279 self.access_log.log(logging.INFO, self.access_log_format % atoms)
287 self.access_log.log(
288 logging.INFO, self.access_log_format % atoms)
280289 except:
281290 self(traceback=True)
282
291
283292 def time(self):
284293 """Return now() in Apache Common Log Format (no timezone)."""
285294 now = datetime.datetime.now()
288297 month = monthnames[now.month - 1].capitalize()
289298 return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
290299 (now.day, month, now.year, now.hour, now.minute, now.second))
291
300
292301 def _get_builtin_handler(self, log, key):
293302 for h in log.handlers:
294303 if getattr(h, "_cpbuiltin", None) == key:
295304 return h
296
297
305
298306 # ------------------------- Screen handlers ------------------------- #
299
300307 def _set_screen_handler(self, log, enable, stream=None):
301308 h = self._get_builtin_handler(log, "screen")
302309 if enable:
303310 if not h:
304311 if stream is None:
305 stream=sys.stderr
312 stream = sys.stderr
306313 h = logging.StreamHandler(stream)
307314 h.setFormatter(logfmt)
308315 h._cpbuiltin = "screen"
309316 log.addHandler(h)
310317 elif h:
311318 log.handlers.remove(h)
312
319
313320 def _get_screen(self):
314321 h = self._get_builtin_handler
315322 has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
316323 return bool(has_h)
317
324
318325 def _set_screen(self, newvalue):
319326 self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
320327 self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
321328 screen = property(_get_screen, _set_screen,
322 doc="""Turn stderr/stdout logging on or off.
323
329 doc="""Turn stderr/stdout logging on or off.
330
324331 If you set this to True, it'll add the appropriate StreamHandler for
325332 you. If you set it to False, it will remove the handler.
326333 """)
327
334
328335 # -------------------------- File handlers -------------------------- #
329
336
330337 def _add_builtin_file_handler(self, log, fname):
331338 h = logging.FileHandler(fname)
332339 h.setFormatter(logfmt)
333340 h._cpbuiltin = "file"
334341 log.addHandler(h)
335
342
336343 def _set_file_handler(self, log, filename):
337344 h = self._get_builtin_handler(log, "file")
338345 if filename:
347354 if h:
348355 h.close()
349356 log.handlers.remove(h)
350
357
351358 def _get_error_file(self):
352359 h = self._get_builtin_handler(self.error_log, "file")
353360 if h:
354361 return h.baseFilename
355362 return ''
363
356364 def _set_error_file(self, newvalue):
357365 self._set_file_handler(self.error_log, newvalue)
358366 error_file = property(_get_error_file, _set_error_file,
359 doc="""The filename for self.error_log.
360
367 doc="""The filename for self.error_log.
368
361369 If you set this to a string, it'll add the appropriate FileHandler for
362370 you. If you set it to ``None`` or ``''``, it will remove the handler.
363371 """)
364
372
365373 def _get_access_file(self):
366374 h = self._get_builtin_handler(self.access_log, "file")
367375 if h:
368376 return h.baseFilename
369377 return ''
378
370379 def _set_access_file(self, newvalue):
371380 self._set_file_handler(self.access_log, newvalue)
372381 access_file = property(_get_access_file, _set_access_file,
373 doc="""The filename for self.access_log.
374
382 doc="""The filename for self.access_log.
383
375384 If you set this to a string, it'll add the appropriate FileHandler for
376385 you. If you set it to ``None`` or ``''``, it will remove the handler.
377386 """)
378
387
379388 # ------------------------- WSGI handlers ------------------------- #
380
389
381390 def _set_wsgi_handler(self, log, enable):
382391 h = self._get_builtin_handler(log, "wsgi")
383392 if enable:
388397 log.addHandler(h)
389398 elif h:
390399 log.handlers.remove(h)
391
400
392401 def _get_wsgi(self):
393402 return bool(self._get_builtin_handler(self.error_log, "wsgi"))
394
403
395404 def _set_wsgi(self, newvalue):
396405 self._set_wsgi_handler(self.error_log, newvalue)
397406 wsgi = property(_get_wsgi, _set_wsgi,
398 doc="""Write errors to wsgi.errors.
399
407 doc="""Write errors to wsgi.errors.
408
400409 If you set this to True, it'll add the appropriate
401410 :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
402411 (which writes errors to ``wsgi.errors``).
405414
406415
407416 class WSGIErrorHandler(logging.Handler):
417
408418 "A handler class which writes logging records to environ['wsgi.errors']."
409
419
410420 def flush(self):
411421 """Flushes the stream."""
412422 try:
415425 pass
416426 else:
417427 stream.flush()
418
428
419429 def emit(self, record):
420430 """Emit a record."""
421431 try:
427437 msg = self.format(record)
428438 fs = "%s\n"
429439 import types
430 if not hasattr(types, "UnicodeType"): #if no unicode support...
440 # if no unicode support...
441 if not hasattr(types, "UnicodeType"):
431442 stream.write(fs % msg)
432443 else:
433444 try:
3434 LoadModule python_module /usr/lib/apache2/modules/mod_python.so
3535
3636 <Location "/">
37 PythonPath "sys.path+['/path/to/my/application']"
38 SetHandler python-program
39 PythonHandler cherrypy._cpmodpy::handler
40 PythonOption cherrypy.setup myapp::setup_server
41 PythonDebug On
42 </Location>
37 PythonPath "sys.path+['/path/to/my/application']"
38 SetHandler python-program
39 PythonHandler cherrypy._cpmodpy::handler
40 PythonOption cherrypy.setup myapp::setup_server
41 PythonDebug On
42 </Location>
4343 # End
4444
4545 The actual path to your mod_python.so is dependent on your
6666 # ------------------------------ Request-handling
6767
6868
69
7069 def setup(req):
7170 from mod_python import apache
72
73 # Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
71
72 # Run any setup functions defined by a "PythonOption cherrypy.setup"
73 # directive.
7474 options = req.get_options()
7575 if 'cherrypy.setup' in options:
7676 for function in options['cherrypy.setup'].split():
8282 mod = __import__(modname, globals(), locals(), [fname])
8383 func = getattr(mod, fname)
8484 func()
85
85
8686 cherrypy.config.update({'log.screen': False,
8787 "tools.ignore_headers.on": True,
8888 "tools.ignore_headers.headers": ['Range'],
8989 })
90
90
9191 engine = cherrypy.engine
9292 if hasattr(engine, "signal_handler"):
9393 engine.signal_handler.unsubscribe()
9595 engine.console_control_handler.unsubscribe()
9696 engine.autoreload.unsubscribe()
9797 cherrypy.server.unsubscribe()
98
98
9999 def _log(msg, level):
100100 newlevel = apache.APLOG_ERR
101101 if logging.DEBUG >= level:
105105 elif logging.WARNING >= level:
106106 newlevel = apache.APLOG_WARNING
107107 # On Windows, req.server is required or the msg will vanish. See
108 # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html.
108 # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
109109 # Also, "When server is not specified...LogLevel does not apply..."
110110 apache.log_error(msg, newlevel, req.server)
111111 engine.subscribe('log', _log)
112
112
113113 engine.start()
114
114
115115 def cherrypy_cleanup(data):
116116 engine.exit()
117117 try:
123123
124124 class _ReadOnlyRequest:
125125 expose = ('read', 'readline', 'readlines')
126
126127 def __init__(self, req):
127128 for method in self.expose:
128129 self.__dict__[method] = getattr(req, method)
131132 recursive = False
132133
133134 _isSetUp = False
135
136
134137 def handler(req):
135138 from mod_python import apache
136139 try:
138141 if not _isSetUp:
139142 setup(req)
140143 _isSetUp = True
141
144
142145 # Obtain a Request object from CherryPy
143146 local = req.connection.local_addr
144 local = httputil.Host(local[0], local[1], req.connection.local_host or "")
147 local = httputil.Host(
148 local[0], local[1], req.connection.local_host or "")
145149 remote = req.connection.remote_addr
146 remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
147
150 remote = httputil.Host(
151 remote[0], remote[1], req.connection.remote_host or "")
152
148153 scheme = req.parsed_uri[0] or 'http'
149154 req.get_basic_auth_pw()
150
155
151156 try:
152157 # apache.mpm_query only became available in mod_python 3.1
153158 q = apache.mpm_query
157162 bad_value = ("You must provide a PythonOption '%s', "
158163 "either 'on' or 'off', when running a version "
159164 "of mod_python < 3.1")
160
165
161166 threaded = options.get('multithread', '').lower()
162167 if threaded == 'on':
163168 threaded = True
165170 threaded = False
166171 else:
167172 raise ValueError(bad_value % "multithread")
168
173
169174 forked = options.get('multiprocess', '').lower()
170175 if forked == 'on':
171176 forked = True
173178 forked = False
174179 else:
175180 raise ValueError(bad_value % "multiprocess")
176
181
177182 sn = cherrypy.tree.script_name(req.uri or "/")
178183 if sn is None:
179184 send_response(req, '404 Not Found', [], '')
186191 headers = copyitems(req.headers_in)
187192 rfile = _ReadOnlyRequest(req)
188193 prev = None
189
194
190195 try:
191196 redirections = []
192197 while True:
197202 request.multiprocess = bool(forked)
198203 request.app = app
199204 request.prev = prev
200
205
201206 # Run the CherryPy Request object and obtain the response
202207 try:
203208 request.run(method, path, qs, reqproto, headers, rfile)
206211 ir = sys.exc_info()[1]
207212 app.release_serving()
208213 prev = request
209
214
210215 if not recursive:
211216 if ir.path in redirections:
212 raise RuntimeError("InternalRedirector visited the "
213 "same URL twice: %r" % ir.path)
217 raise RuntimeError(
218 "InternalRedirector visited the same URL "
219 "twice: %r" % ir.path)
214220 else:
215 # Add the *previous* path_info + qs to redirections.
221 # Add the *previous* path_info + qs to
222 # redirections.
216223 if qs:
217224 qs = "?" + qs
218225 redirections.append(sn + path + qs)
219
226
220227 # Munge environment and try again.
221228 method = "GET"
222229 path = ir.path
223230 qs = ir.query_string
224231 rfile = BytesIO()
225
226 send_response(req, response.output_status, response.header_list,
227 response.body, response.stream)
232
233 send_response(
234 req, response.output_status, response.header_list,
235 response.body, response.stream)
228236 finally:
229237 app.release_serving()
230238 except:
238246 def send_response(req, status, headers, body, stream=False):
239247 # Set response status
240248 req.status = int(status[:3])
241
249
242250 # Set response headers
243251 req.content_type = "text/plain"
244252 for header, value in headers:
246254 req.content_type = value
247255 continue
248256 req.headers_out.add(header, value)
249
257
250258 if stream:
251259 # Flush now so the status and headers are sent immediately.
252260 req.flush()
253
261
254262 # Set response body
255263 if isinstance(body, basestring):
256264 req.write(body)
259267 req.write(seg)
260268
261269
262
263270 # --------------- Startup tools for CherryPy + mod_python --------------- #
264
265
266271 import os
267272 import re
268273 try:
269274 import subprocess
275
270276 def popen(fullcmd):
271277 p = subprocess.Popen(fullcmd, shell=True,
272278 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
283289 pipeout = popen(fullcmd)
284290 try:
285291 firstline = pipeout.readline()
286 if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
287 re.IGNORECASE)):
292 cmd_not_found = re.search(
293 ntob("(not recognized|No such file|not found)"),
294 firstline,
295 re.IGNORECASE
296 )
297 if cmd_not_found:
288298 raise IOError('%s must be on your system path.' % cmd)
289299 output = firstline + pipeout.read()
290300 finally:
293303
294304
295305 class ModPythonServer(object):
296
306
297307 template = """
298308 # Apache2 server configuration file for running CherryPy with mod_python.
299309
308318 %(opts)s
309319 </Location>
310320 """
311
321
312322 def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
313323 handler="cherrypy._cpmodpy::handler"):
314324 self.loc = loc
316326 self.opts = opts
317327 self.apache_path = apache_path
318328 self.handler = handler
319
329
320330 def start(self):
321331 opts = "".join([" PythonOption %s %s\n" % (k, v)
322332 for k, v in self.opts])
325335 "opts": opts,
326336 "handler": self.handler,
327337 }
328
338
329339 mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
330340 f = open(mpconf, 'wb')
331341 try:
332342 f.write(conf_data)
333343 finally:
334344 f.close()
335
345
336346 response = read_process(self.apache_path, "-k start -f %s" % mpconf)
337347 self.ready = True
338348 return response
339
349
340350 def stop(self):
341351 os.popen("apache -k stop")
342352 self.ready = False
343
1010
1111
1212 class NativeGateway(wsgiserver.Gateway):
13
13
1414 recursive = False
15
15
1616 def respond(self):
1717 req = self.req
1818 try:
2121 local = httputil.Host(local[0], local[1], "")
2222 remote = req.conn.remote_addr, req.conn.remote_port
2323 remote = httputil.Host(remote[0], remote[1], "")
24
24
2525 scheme = req.scheme
2626 sn = cherrypy.tree.script_name(req.uri or "/")
2727 if sn is None:
3434 headers = req.inheaders.items()
3535 rfile = req.rfile
3636 prev = None
37
37
3838 try:
3939 redirections = []
4040 while True:
4444 request.multiprocess = False
4545 request.app = app
4646 request.prev = prev
47
48 # Run the CherryPy Request object and obtain the response
47
48 # Run the CherryPy Request object and obtain the
49 # response
4950 try:
50 request.run(method, path, qs, req.request_protocol, headers, rfile)
51 request.run(method, path, qs,
52 req.request_protocol, headers, rfile)
5153 break
5254 except cherrypy.InternalRedirect:
5355 ir = sys.exc_info()[1]
5456 app.release_serving()
5557 prev = request
56
58
5759 if not self.recursive:
5860 if ir.path in redirections:
59 raise RuntimeError("InternalRedirector visited the "
60 "same URL twice: %r" % ir.path)
61 raise RuntimeError(
62 "InternalRedirector visited the same "
63 "URL twice: %r" % ir.path)
6164 else:
62 # Add the *previous* path_info + qs to redirections.
65 # Add the *previous* path_info + qs to
66 # redirections.
6367 if qs:
6468 qs = "?" + qs
6569 redirections.append(sn + path + qs)
66
70
6771 # Munge environment and try again.
6872 method = "GET"
6973 path = ir.path
7074 qs = ir.query_string
7175 rfile = BytesIO()
72
76
7377 self.send_response(
7478 response.output_status, response.header_list,
7579 response.body)
7781 app.release_serving()
7882 except:
7983 tb = format_exc()
80 #print tb
84 # print tb
8185 cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
8286 s, h, b = bare_error()
8387 self.send_response(s, h, b)
84
88
8589 def send_response(self, status, headers, body):
8690 req = self.req
87
91
8892 # Set response status
8993 req.status = str(status or "500 Server Error")
90
94
9195 # Set response headers
9296 for header, value in headers:
9397 req.outheaders.append((header, value))
9498 if (req.ready and not req.sent_headers):
9599 req.sent_headers = True
96100 req.send_headers()
97
101
98102 # Set response body
99103 for seg in body:
100104 req.write(seg)
101105
102106
103107 class CPHTTPServer(wsgiserver.HTTPServer):
108
104109 """Wrapper for wsgiserver.HTTPServer.
105
110
106111 wsgiserver has been designed to not reference CherryPy in any way,
107112 so that it can be used in other frameworks and applications.
108113 Therefore, we wrap it here, so we can apply some attributes
109114 from config -> cherrypy.server -> HTTPServer.
110115 """
111
116
112117 def __init__(self, server_adapter=cherrypy.server):
113118 self.server_adapter = server_adapter
114
119
115120 server_name = (self.server_adapter.socket_host or
116121 self.server_adapter.socket_file or
117122 None)
118
123
119124 wsgiserver.HTTPServer.__init__(
120125 self, server_adapter.bind_addr, NativeGateway,
121126 minthreads=server_adapter.thread_pool,
122127 maxthreads=server_adapter.thread_pool_max,
123128 server_name=server_name)
124
125 self.max_request_header_size = self.server_adapter.max_request_header_size or 0
126 self.max_request_body_size = self.server_adapter.max_request_body_size or 0
129
130 self.max_request_header_size = (
131 self.server_adapter.max_request_header_size or 0)
132 self.max_request_body_size = (
133 self.server_adapter.max_request_body_size or 0)
127134 self.request_queue_size = self.server_adapter.socket_queue_size
128135 self.timeout = self.server_adapter.socket_timeout
129136 self.shutdown_timeout = self.server_adapter.shutdown_timeout
130137 self.protocol = self.server_adapter.protocol_version
131138 self.nodelay = self.server_adapter.nodelay
132
139
133140 ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
134141 if self.server_adapter.ssl_context:
135142 adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
144151 self.server_adapter.ssl_certificate,
145152 self.server_adapter.ssl_private_key,
146153 self.server_adapter.ssl_certificate_chain)
147
148
22 .. versionadded:: 3.2
33
44 Application authors have complete control over the parsing of HTTP request
5 entities. In short, :attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
6 is now always set to an instance of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
5 entities. In short,
6 :attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
7 is now always set to an instance of
8 :class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
79 and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
810
911 When an HTTP request includes an entity body, it is often desirable to
2022 :attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
2123 If the full media
2224 type is not found, then the major type is tried; for example, if no processor
23 is found for the 'image/jpeg' type, then we look for a processor for the 'image'
24 types altogether. If neither the full type nor the major type has a matching
25 processor, then a default processor is used
25 is found for the 'image/jpeg' type, then we look for a processor for the
26 'image' types altogether. If neither the full type nor the major type has a
27 matching processor, then a default processor is used
2628 (:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
2729 types, this means no processing is done, and the body is left unread as a
2830 raw byte stream. Processors are configurable in an 'on_start_resource' hook.
5254
5355 You can add your own processors for any specific or major MIME type. Simply add
5456 it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
55 hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
57 hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
5658 Here's the built-in JSON tool for an example::
5759
5860 def json_in(force=True, debug=False):
6163 \"""Read application/json data into request.json.\"""
6264 if not entity.headers.get("Content-Length", ""):
6365 raise cherrypy.HTTPError(411)
64
66
6567 body = entity.fp.read()
6668 try:
6769 request.json = json_decode(body)
7375 415, 'Expected an application/json content type')
7476 request.body.processors['application/json'] = json_processor
7577
76 We begin by defining a new ``json_processor`` function to stick in the ``processors``
77 dictionary. All processor functions take a single argument, the ``Entity`` instance
78 they are to process. It will be called whenever a request is received (for those
79 URI's where the tool is turned on) which has a ``Content-Type`` of
80 "application/json".
81
82 First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then
83 reads the remaining bytes on the socket. The ``fp`` object knows its own length, so
84 it won't hang waiting for data that never arrives. It will return when all data
85 has been read. Then, we decode those bytes using Python's built-in ``json`` module,
86 and stick the decoded result onto ``request.json`` . If it cannot be decoded, we
87 raise 400.
88
89 If the "force" argument is True (the default), the ``Tool`` clears the ``processors``
90 dict so that request entities of other ``Content-Types`` aren't parsed at all. Since
91 there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body``
92 is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.)
93 But in our case, we want to raise 415, so we replace ``request.body.default_proc``
78 We begin by defining a new ``json_processor`` function to stick in the
79 ``processors`` dictionary. All processor functions take a single argument,
80 the ``Entity`` instance they are to process. It will be called whenever a
81 request is received (for those URI's where the tool is turned on) which
82 has a ``Content-Type`` of "application/json".
83
84 First, it checks for a valid ``Content-Length`` (raising 411 if not valid),
85 then reads the remaining bytes on the socket. The ``fp`` object knows its
86 own length, so it won't hang waiting for data that never arrives. It will
87 return when all data has been read. Then, we decode those bytes using
88 Python's built-in ``json`` module, and stick the decoded result onto
89 ``request.json`` . If it cannot be decoded, we raise 400.
90
91 If the "force" argument is True (the default), the ``Tool`` clears the
92 ``processors`` dict so that request entities of other ``Content-Types``
93 aren't parsed at all. Since there's no entry for those invalid MIME
94 types, the ``default_proc`` method of ``cherrypy.request.body`` is
95 called. But this does nothing by default (usually to provide the page
96 handler an opportunity to handle it.)
97 But in our case, we want to raise 415, so we replace
98 ``request.body.default_proc``
9499 with the error (``HTTPError`` instances, when called, raise themselves).
95100
96 If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry::
101 If we were defining a custom processor, we can do so without making a ``Tool``.
102 Just add the config entry::
97103
98104 request.body.processors = {'application/json': json_processor}
99105
100 Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one.
106 Note that you can only replace the ``processors`` dict wholesale this way,
107 not update the existing one.
101108 """
102109
103110 try:
128135 from cherrypy.lib import httputil
129136
130137
131 # -------------------------------- Processors -------------------------------- #
138 # ------------------------------- Processors -------------------------------- #
132139
133140 def process_urlencoded(entity):
134141 """Read application/x-www-form-urlencoded data into entity.params."""
140147 for pair in aparam.split(ntob(';')):
141148 if not pair:
142149 continue
143
150
144151 atoms = pair.split(ntob('='), 1)
145152 if len(atoms) == 1:
146153 atoms.append(ntob(''))
147
154
148155 key = unquote_plus(atoms[0]).decode(charset)
149156 value = unquote_plus(atoms[1]).decode(charset)
150
157
151158 if key in params:
152159 if not isinstance(params[key], list):
153160 params[key] = [params[key]]
163170 raise cherrypy.HTTPError(
164171 400, "The request entity could not be decoded. The following "
165172 "charsets were attempted: %s" % repr(entity.attempt_charsets))
166
173
167174 # Now that all values have been successfully parsed and decoded,
168175 # apply them to the entity.params dict.
169176 for key, value in params.items():
184191 # is often necessary to enclose the boundary parameter values in quotes
185192 # on the Content-type line"
186193 ib = entity.content_type.params['boundary'].strip('"')
187
194
188195 if not re.match("^[ -~]{0,200}[!-~]$", ib):
189196 raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
190
197
191198 ib = ('--' + ib).encode('ascii')
192
199
193200 # Find the first marker
194201 while True:
195202 b = entity.readline()
196203 if not b:
197204 return
198
205
199206 b = b.strip()
200207 if b == ib:
201208 break
202
209
203210 # Read all parts
204211 while True:
205212 part = entity.part_class.from_fp(entity.fp, ib)
208215 if part.fp.done:
209216 break
210217
218
211219 def process_multipart_form_data(entity):
212 """Read all multipart/form-data parts into entity.parts or entity.params."""
220 """Read all multipart/form-data parts into entity.parts or entity.params.
221 """
213222 process_multipart(entity)
214
223
215224 kept_parts = []
216225 for part in entity.parts:
217226 if part.name is None:
224233 # It's a file upload. Retain the whole part so consumer code
225234 # has access to its .file and .filename attributes.
226235 value = part
227
236
228237 if part.name in entity.params:
229238 if not isinstance(entity.params[part.name], list):
230239 entity.params[part.name] = [entity.params[part.name]]
231240 entity.params[part.name].append(value)
232241 else:
233242 entity.params[part.name] = value
234
243
235244 entity.parts = kept_parts
245
236246
237247 def _old_process_multipart(entity):
238248 """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
239249 process_multipart(entity)
240
250
241251 params = entity.params
242
252
243253 for part in entity.parts:
244254 if part.name is None:
245255 key = ntou('parts')
246256 else:
247257 key = part.name
248
258
249259 if part.filename is None:
250260 # It's a regular field
251261 value = part.fullvalue()
253263 # It's a file upload. Retain the whole part so consumer code
254264 # has access to its .file and .filename attributes.
255265 value = part
256
266
257267 if key in params:
258268 if not isinstance(params[key], list):
259269 params[key] = [params[key]]
262272 params[key] = value
263273
264274
265
266 # --------------------------------- Entities --------------------------------- #
267
268
275 # -------------------------------- Entities --------------------------------- #
269276 class Entity(object):
277
270278 """An HTTP request body, or MIME multipart body.
271
279
272280 This class collects information about the HTTP request entity. When a
273281 given entity is of MIME type "multipart", each part is parsed into its own
274282 Entity instance, and the set of parts stored in
275283 :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
276
284
277285 Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
278286 tries to process the request body (if any) by calling
279 :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
280 This uses the ``content_type`` of the Entity to look up a suitable processor
281 in :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, a dict.
287 :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`.
288 This uses the ``content_type`` of the Entity to look up a suitable
289 processor in
290 :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`,
291 a dict.
282292 If a matching processor cannot be found for the complete Content-Type,
283293 it tries again using the major type. For example, if a request with an
284294 entity of type "image/jpeg" arrives, but no processor can be found for
285295 that complete type, then one is sought for the major type "image". If a
286296 processor is still not found, then the
287 :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the
288 Entity is called (which does nothing by default; you can override this too).
289
297 :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method
298 of the Entity is called (which does nothing by default; you can
299 override this too).
300
290301 CherryPy includes processors for the "application/x-www-form-urlencoded"
291302 type, the "multipart/form-data" type, and the "multipart" major type.
292303 CherryPy 3.2 processes these types almost exactly as older versions.
297308 case it will have ``file`` and ``filename`` attributes, or possibly a
298309 ``value`` attribute). Each Part is itself a subclass of
299310 Entity, and has its own ``process`` method and ``processors`` dict.
300
311
301312 There is a separate processor for the "multipart" major type which is more
302313 flexible, and simply stores all multipart parts in
303314 :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
304315 enable it with::
305
316
306317 cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
307
318
308319 in an ``on_start_resource`` tool.
309320 """
310
321
311322 # http://tools.ietf.org/html/rfc2046#section-4.1.2:
312323 # "The default character set, which must be assumed in the
313324 # absence of a charset parameter, is US-ASCII."
314325 # However, many browsers send data in utf-8 with no charset.
315326 attempt_charsets = ['utf-8']
316327 """A list of strings, each of which should be a known encoding.
317
328
318329 When the Content-Type of the request body warrants it, each of the given
319330 encodings will be tried in order. The first one to successfully decode the
320331 entity without raising an error is stored as
321332 :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
322 to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
323 `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
333 to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
334 `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
324335 but ``['us-ascii', 'utf-8']`` for multipart parts.
325336 """
326
337
327338 charset = None
328339 """The successful decoding; see "attempt_charsets" above."""
329
340
330341 content_type = None
331342 """The value of the Content-Type request header.
332
343
333344 If the Entity is part of a multipart payload, this will be the Content-Type
334345 given in the MIME headers for this part.
335346 """
336
347
337348 default_content_type = 'application/x-www-form-urlencoded'
338349 """This defines a default ``Content-Type`` to use if no Content-Type header
339350 is given. The empty string is used for RequestBody, which results in the
343354 declares that a part with no Content-Type defaults to "text/plain"
344355 (see :class:`Part<cherrypy._cpreqbody.Part>`).
345356 """
346
357
347358 filename = None
348359 """The ``Content-Disposition.filename`` header, if available."""
349
360
350361 fp = None
351362 """The readable socket file object."""
352
363
353364 headers = None
354365 """A dict of request/multipart header names and values.
355
366
356367 This is a copy of the ``request.headers`` for the ``request.body``;
357368 for multipart parts, it is the set of headers for that part.
358369 """
359
370
360371 length = None
361372 """The value of the ``Content-Length`` header, if provided."""
362
373
363374 name = None
364375 """The "name" parameter of the ``Content-Disposition`` header, if any."""
365
376
366377 params = None
367378 """
368379 If the request Content-Type is 'application/x-www-form-urlencoded' or
372383 can be sent with various HTTP method verbs). This value is set between
373384 the 'before_request_body' and 'before_handler' hooks (assuming that
374385 process_request_body is True)."""
375
386
376387 processors = {'application/x-www-form-urlencoded': process_urlencoded,
377388 'multipart/form-data': process_multipart_form_data,
378389 'multipart': process_multipart,
379390 }
380391 """A dict of Content-Type names to processor methods."""
381
392
382393 parts = None
383 """A list of Part instances if ``Content-Type`` is of major type "multipart"."""
384
394 """A list of Part instances if ``Content-Type`` is of major type
395 "multipart"."""
396
385397 part_class = None
386398 """The class used for multipart parts.
387
399
388400 You can replace this with custom subclasses to alter the processing of
389401 multipart parts.
390402 """
391
403
392404 def __init__(self, fp, headers, params=None, parts=None):
393405 # Make an instance-specific copy of the class processors
394406 # so Tools, etc. can replace them per-request.
395407 self.processors = self.processors.copy()
396
408
397409 self.fp = fp
398410 self.headers = headers
399
411
400412 if params is None:
401413 params = {}
402414 self.params = params
403
415
404416 if parts is None:
405417 parts = []
406418 self.parts = parts
407
419
408420 # Content-Type
409421 self.content_type = headers.elements('Content-Type')
410422 if self.content_type:
412424 else:
413425 self.content_type = httputil.HeaderElement.from_str(
414426 self.default_content_type)
415
416 # Copy the class 'attempt_charsets', prepending any Content-Type charset
427
428 # Copy the class 'attempt_charsets', prepending any Content-Type
429 # charset
417430 dec = self.content_type.params.get("charset", None)
418431 if dec:
419432 self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
420433 if c != dec]
421434 else:
422435 self.attempt_charsets = self.attempt_charsets[:]
423
436
424437 # Length
425438 self.length = None
426439 clen = headers.get('Content-Length', None)
427440 # If Transfer-Encoding is 'chunked', ignore any Content-Length.
428 if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''):
441 if (
442 clen is not None and
443 'chunked' not in headers.get('Transfer-Encoding', '')
444 ):
429445 try:
430446 self.length = int(clen)
431447 except ValueError:
432448 pass
433
449
434450 # Content-Disposition
435451 self.name = None
436452 self.filename = None
443459 self.name = self.name[1:-1]
444460 if 'filename' in disp.params:
445461 self.filename = disp.params['filename']
446 if self.filename.startswith('"') and self.filename.endswith('"'):
462 if (
463 self.filename.startswith('"') and
464 self.filename.endswith('"')
465 ):
447466 self.filename = self.filename[1:-1]
448
467
449468 # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
450 type = property(lambda self: self.content_type,
451 doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
452
469 type = property(
470 lambda self: self.content_type,
471 doc="A deprecated alias for "
472 ":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`."
473 )
474
453475 def read(self, size=None, fp_out=None):
454476 return self.fp.read(size, fp_out)
455
477
456478 def readline(self, size=None):
457479 return self.fp.readline(size)
458
480
459481 def readlines(self, sizehint=None):
460482 return self.fp.readlines(sizehint)
461
483
462484 def __iter__(self):
463485 return self
464
486
465487 def __next__(self):
466488 line = self.readline()
467489 if not line:
470492
471493 def next(self):
472494 return self.__next__()
473
495
474496 def read_into_file(self, fp_out=None):
475 """Read the request body into fp_out (or make_file() if None). Return fp_out."""
497 """Read the request body into fp_out (or make_file() if None).
498
499 Return fp_out.
500 """
476501 if fp_out is None:
477502 fp_out = self.make_file()
478503 self.read(fp_out=fp_out)
479504 return fp_out
480
505
481506 def make_file(self):
482507 """Return a file-like object into which the request body will be read.
483
508
484509 By default, this will return a TemporaryFile. Override as needed.
485510 See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
486511 return tempfile.TemporaryFile()
487
512
488513 def fullvalue(self):
489514 """Return this entity as a string, whether stored in a file or not."""
490515 if self.file:
495520 else:
496521 value = self.value
497522 return value
498
523
499524 def process(self):
500525 """Execute the best-match processor for the given media type."""
501526 proc = None
512537 self.default_proc()
513538 else:
514539 proc(self)
515
540
516541 def default_proc(self):
517 """Called if a more-specific processor is not found for the ``Content-Type``."""
542 """Called if a more-specific processor is not found for the
543 ``Content-Type``.
544 """
518545 # Leave the fp alone for someone else to read. This works fine
519546 # for request.body, but the Part subclasses need to override this
520547 # so they can move on to the next part.
522549
523550
524551 class Part(Entity):
552
525553 """A MIME part entity, part of a multipart entity."""
526
554
527555 # "The default character set, which must be assumed in the absence of a
528556 # charset parameter, is US-ASCII."
529557 attempt_charsets = ['us-ascii', 'utf-8']
530558 """A list of strings, each of which should be a known encoding.
531
559
532560 When the Content-Type of the request body warrants it, each of the given
533561 encodings will be tried in order. The first one to successfully decode the
534562 entity without raising an error is stored as
535563 :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
536 to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
537 `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
564 to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
565 `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
538566 but ``['us-ascii', 'utf-8']`` for multipart parts.
539567 """
540
568
541569 boundary = None
542570 """The MIME multipart boundary."""
543
571
544572 default_content_type = 'text/plain'
545573 """This defines a default ``Content-Type`` to use if no Content-Type header
546574 is given. The empty string is used for RequestBody, which results in the
550578 the MIME spec declares that a part with no Content-Type defaults to
551579 "text/plain".
552580 """
553
581
554582 # This is the default in stdlib cgi. We may want to increase it.
555583 maxrambytes = 1000
556 """The threshold of bytes after which point the ``Part`` will store its data
557 in a file (generated by :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
558 instead of a string. Defaults to 1000, just like the :mod:`cgi` module in
559 Python's standard library.
560 """
561
584 """The threshold of bytes after which point the ``Part`` will store
585 its data in a file (generated by
586 :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
587 instead of a string. Defaults to 1000, just like the :mod:`cgi`
588 module in Python's standard library.
589 """
590
562591 def __init__(self, fp, headers, boundary):
563592 Entity.__init__(self, fp, headers)
564593 self.boundary = boundary
565594 self.file = None
566595 self.value = None
567
596
568597 def from_fp(cls, fp, boundary):
569598 headers = cls.read_headers(fp)
570599 return cls(fp, headers, boundary)
571600 from_fp = classmethod(from_fp)
572
601
573602 def read_headers(cls, fp):
574603 headers = httputil.HeaderMap()
575604 while True:
577606 if not line:
578607 # No more data--illegal end of headers
579608 raise EOFError("Illegal end of headers.")
580
609
581610 if line == ntob('\r\n'):
582611 # Normal end of headers
583612 break
584613 if not line.endswith(ntob('\r\n')):
585614 raise ValueError("MIME requires CRLF terminators: %r" % line)
586
615
587616 if line[0] in ntob(' \t'):
588617 # It's a continuation line.
589618 v = line.strip().decode('ISO-8859-1')
591620 k, v = line.split(ntob(":"), 1)
592621 k = k.strip().decode('ISO-8859-1')
593622 v = v.strip().decode('ISO-8859-1')
594
623
595624 existing = headers.get(k)
596625 if existing:
597626 v = ", ".join((existing, v))
598627 headers[k] = v
599
628
600629 return headers
601630 read_headers = classmethod(read_headers)
602
631
603632 def read_lines_to_boundary(self, fp_out=None):
604633 """Read bytes from self.fp and return or write them to a file.
605
634
606635 If the 'fp_out' argument is None (the default), all bytes read are
607636 returned in a single byte string.
608
609 If the 'fp_out' argument is not None, it must be a file-like object that
610 supports the 'write' method; all bytes read will be written to the fp,
611 and that fp is returned.
637
638 If the 'fp_out' argument is not None, it must be a file-like
639 object that supports the 'write' method; all bytes read will be
640 written to the fp, and that fp is returned.
612641 """
613642 endmarker = self.boundary + ntob("--")
614643 delim = ntob("")
616645 lines = []
617646 seen = 0
618647 while True:
619 line = self.fp.readline(1<<16)
648 line = self.fp.readline(1 << 16)
620649 if not line:
621650 raise EOFError("Illegal end of multipart body.")
622651 if line.startswith(ntob("--")) and prev_lf:
626655 if strippedline == endmarker:
627656 self.fp.finish()
628657 break
629
658
630659 line = delim + line
631
660
632661 if line.endswith(ntob("\r\n")):
633662 delim = ntob("\r\n")
634663 line = line[:-2]
640669 else:
641670 delim = ntob("")
642671 prev_lf = False
643
672
644673 if fp_out is None:
645674 lines.append(line)
646675 seen += len(line)
650679 fp_out.write(line)
651680 else:
652681 fp_out.write(line)
653
682
654683 if fp_out is None:
655684 result = ntob('').join(lines)
656685 for charset in self.attempt_charsets:
663692 return result
664693 else:
665694 raise cherrypy.HTTPError(
666 400, "The request entity could not be decoded. The following "
667 "charsets were attempted: %s" % repr(self.attempt_charsets))
695 400,
696 "The request entity could not be decoded. The following "
697 "charsets were attempted: %s" % repr(self.attempt_charsets)
698 )
668699 else:
669700 fp_out.seek(0)
670701 return fp_out
671
702
672703 def default_proc(self):
673 """Called if a more-specific processor is not found for the ``Content-Type``."""
704 """Called if a more-specific processor is not found for the
705 ``Content-Type``.
706 """
674707 if self.filename:
675708 # Always read into a file if a .filename was given.
676709 self.file = self.read_into_file()
680713 self.value = result
681714 else:
682715 self.file = result
683
716
684717 def read_into_file(self, fp_out=None):
685 """Read the request body into fp_out (or make_file() if None). Return fp_out."""
718 """Read the request body into fp_out (or make_file() if None).
719
720 Return fp_out.
721 """
686722 if fp_out is None:
687723 fp_out = self.make_file()
688724 self.read_lines_to_boundary(fp_out=fp_out)
695731 except ValueError:
696732 # Python 2.4 and lower
697733 class Infinity(object):
734
698735 def __cmp__(self, other):
699736 return 1
737
700738 def __sub__(self, other):
701739 return self
702740 inf = Infinity()
703741
704742
705 comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
706 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection',
707 'Content-Encoding', 'Content-Language', 'Expect', 'If-Match',
708 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer',
709 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate']
743 comma_separated_headers = [
744 'Accept', 'Accept-Charset', 'Accept-Encoding',
745 'Accept-Language', 'Accept-Ranges', 'Allow',
746 'Cache-Control', 'Connection', 'Content-Encoding',
747 'Content-Language', 'Expect', 'If-Match',
748 'If-None-Match', 'Pragma', 'Proxy-Authenticate',
749 'Te', 'Trailer', 'Transfer-Encoding', 'Upgrade',
750 'Vary', 'Via', 'Warning', 'Www-Authenticate'
751 ]
710752
711753
712754 class SizedReader:
713
714 def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False):
755
756 def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
757 has_trailers=False):
715758 # Wrap our fp in a buffer so peek() works
716759 self.fp = fp
717760 self.length = length
721764 self.bytes_read = 0
722765 self.done = False
723766 self.has_trailers = has_trailers
724
767
725768 def read(self, size=None, fp_out=None):
726769 """Read bytes from the request body and return or write them to a file.
727
770
728771 A number of bytes less than or equal to the 'size' argument are read
729772 off the socket. The actual number of bytes read are tracked in
730773 self.bytes_read. The number may be smaller than 'size' when 1) the
731774 client sends fewer bytes, 2) the 'Content-Length' request header
732775 specifies fewer bytes than requested, or 3) the number of bytes read
733776 exceeds self.maxbytes (in which case, 413 is raised).
734
777
735778 If the 'fp_out' argument is None (the default), all bytes read are
736779 returned in a single byte string.
737
738 If the 'fp_out' argument is not None, it must be a file-like object that
739 supports the 'write' method; all bytes read will be written to the fp,
740 and None is returned.
780
781 If the 'fp_out' argument is not None, it must be a file-like
782 object that supports the 'write' method; all bytes read will be
783 written to the fp, and None is returned.
741784 """
742
785
743786 if self.length is None:
744787 if size is None:
745788 remaining = inf
755798 return ntob('')
756799 else:
757800 return None
758
801
759802 chunks = []
760
803
761804 # Read bytes from the buffer.
762805 if self.buffer:
763806 if remaining is inf:
768811 self.buffer = self.buffer[remaining:]
769812 datalen = len(data)
770813 remaining -= datalen
771
814
772815 # Check lengths.
773816 self.bytes_read += datalen
774817 if self.maxbytes and self.bytes_read > self.maxbytes:
775818 raise cherrypy.HTTPError(413)
776
819
777820 # Store the data.
778821 if fp_out is None:
779822 chunks.append(data)
780823 else:
781824 fp_out.write(data)
782
825
783826 # Read bytes from the socket.
784827 while remaining > 0:
785828 chunksize = min(remaining, self.bufsize)
798841 break
799842 datalen = len(data)
800843 remaining -= datalen
801
844
802845 # Check lengths.
803846 self.bytes_read += datalen
804847 if self.maxbytes and self.bytes_read > self.maxbytes:
805848 raise cherrypy.HTTPError(413)
806
849
807850 # Store the data.
808851 if fp_out is None:
809852 chunks.append(data)
810853 else:
811854 fp_out.write(data)
812
855
813856 if fp_out is None:
814857 return ntob('').join(chunks)
815
858
816859 def readline(self, size=None):
817860 """Read a line from the request body and return it."""
818861 chunks = []
833876 else:
834877 chunks.append(data)
835878 return ntob('').join(chunks)
836
879
837880 def readlines(self, sizehint=None):
838881 """Read lines from the request body and return them."""
839882 if self.length is not None:
841884 sizehint = self.length - self.bytes_read
842885 else:
843886 sizehint = min(sizehint, self.length - self.bytes_read)
844
887
845888 lines = []
846889 seen = 0
847890 while True:
853896 if seen >= sizehint:
854897 break
855898 return lines
856
899
857900 def finish(self):
858901 self.done = True
859902 if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
860903 self.trailers = {}
861
904
862905 try:
863906 for line in self.fp.read_trailer_lines():
864907 if line[0] in ntob(' \t'):
871914 raise ValueError("Illegal header line.")
872915 k = k.strip().title()
873916 v = v.strip()
874
917
875918 if k in comma_separated_headers:
876919 existing = self.trailers.get(envname)
877920 if existing:
888931
889932
890933 class RequestBody(Entity):
934
891935 """The entity of the HTTP request."""
892
936
893937 bufsize = 8 * 1024
894938 """The buffer size used when reading the socket."""
895
939
896940 # Don't parse the request body at all if the client didn't provide
897 # a Content-Type header. See http://www.cherrypy.org/ticket/790
941 # a Content-Type header. See
942 # https://bitbucket.org/cherrypy/cherrypy/issue/790
898943 default_content_type = ''
899944 """This defines a default ``Content-Type`` to use if no Content-Type header
900945 is given. The empty string is used for RequestBody, which results in the
904949 declares that a part with no Content-Type defaults to "text/plain"
905950 (see :class:`Part<cherrypy._cpreqbody.Part>`).
906951 """
907
952
908953 maxbytes = None
909 """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
910
954 """Raise ``MaxSizeExceeded`` if more bytes than this are read from
955 the socket.
956 """
957
911958 def __init__(self, fp, headers, params=None, request_params=None):
912959 Entity.__init__(self, fp, headers, params)
913
960
914961 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
915962 # When no explicit charset parameter is provided by the
916963 # sender, media subtypes of the "text" type are defined
922969 break
923970 else:
924971 self.attempt_charsets.append('ISO-8859-1')
925
972
926973 # Temporary fix while deprecating passing .parts as .params.
927974 self.processors['multipart'] = _old_process_multipart
928
975
929976 if request_params is None:
930977 request_params = {}
931978 self.request_params = request_params
932
979
933980 def process(self):
934981 """Process the request entity based on its Content-Type."""
935982 # "The presence of a message-body in a request is signaled by the
941988 h = cherrypy.serving.request.headers
942989 if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
943990 raise cherrypy.HTTPError(411)
944
991
945992 self.fp = SizedReader(self.fp, self.length,
946993 self.maxbytes, bufsize=self.bufsize,
947994 has_trailers='Trailer' in h)
948995 super(RequestBody, self).process()
949
996
950997 # Body params should also be a part of the request_params
951998 # add them in here.
952999 request_params = self.request_params
9531000 for key, value in self.params.items():
954 # Python 2 only: keyword arguments must be byte strings (type 'str').
1001 # Python 2 only: keyword arguments must be byte strings (type
1002 # 'str').
9551003 if sys.version_info < (3, 0):
9561004 if isinstance(key, unicode):
9571005 key = key.encode('ISO-8859-1')
958
1006
9591007 if key in request_params:
9601008 if not isinstance(request_params[key], list):
9611009 request_params[key] = [request_params[key]]
1212
1313
1414 class Hook(object):
15
1516 """A callback and its metadata: failsafe, priority, and kwargs."""
16
17
1718 callback = None
1819 """
1920 The bare callable that this Hook object is wrapping, which will
2021 be called when the Hook is called."""
21
22
2223 failsafe = False
2324 """
2425 If True, the callback is guaranteed to run even if other callbacks
2526 from the same call point raise exceptions."""
26
27
2728 priority = 50
2829 """
2930 Defines the order of execution for a list of Hooks. Priority numbers
3031 should be limited to the closed interval [0, 100], but values outside
3132 this range are acceptable, as are fractional values."""
32
33
3334 kwargs = {}
3435 """
3536 A set of keyword arguments that will be passed to the
3637 callable on each call."""
37
38
3839 def __init__(self, callback, failsafe=None, priority=None, **kwargs):
3940 self.callback = callback
40
41
4142 if failsafe is None:
4243 failsafe = getattr(callback, "failsafe", False)
4344 self.failsafe = failsafe
44
45
4546 if priority is None:
4647 priority = getattr(callback, "priority", 50)
4748 self.priority = priority
48
49
4950 self.kwargs = kwargs
50
51
5152 def __lt__(self, other):
5253 # Python 3
5354 return self.priority < other.priority
5556 def __cmp__(self, other):
5657 # Python 2
5758 return cmp(self.priority, other.priority)
58
59
5960 def __call__(self):
6061 """Run self.callback(**self.kwargs)."""
6162 return self.callback(**self.kwargs)
62
63
6364 def __repr__(self):
6465 cls = self.__class__
6566 return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
7071
7172
7273 class HookMap(dict):
74
7375 """A map of call points to lists of callbacks (Hook objects)."""
74
76
7577 def __new__(cls, points=None):
7678 d = dict.__new__(cls)
7779 for p in points or []:
7880 d[p] = []
7981 return d
80
82
8183 def __init__(self, *a, **kw):
8284 pass
83
85
8486 def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
8587 """Append a new Hook made from the supplied arguments."""
8688 self[point].append(Hook(callback, failsafe, priority, **kwargs))
87
89
8890 def run(self, point):
8991 """Execute all registered Hooks (callbacks) for the given point."""
9092 exc = None
109111 cherrypy.log(traceback=True, severity=40)
110112 if exc:
111113 raise exc
112
114
113115 def __copy__(self):
114116 newmap = self.__class__()
115117 # We can't just use 'update' because we want copies of the
118120 newmap[k] = v[:]
119121 return newmap
120122 copy = __copy__
121
123
122124 def __repr__(self):
123125 cls = self.__class__
124 return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
126 return "%s.%s(points=%r)" % (
127 cls.__module__,
128 cls.__name__,
129 copykeys(self)
130 )
125131
126132
127133 # Config namespace handlers
138144 v = Hook(v)
139145 cherrypy.serving.request.hooks[hookpoint].append(v)
140146
147
141148 def request_namespace(k, v):
142149 """Attach request attributes declared in config."""
143 # Provides config entries to set request.body attrs (like attempt_charsets).
150 # Provides config entries to set request.body attrs (like
151 # attempt_charsets).
144152 if k[:5] == 'body.':
145153 setattr(cherrypy.serving.request.body, k[5:], v)
146154 else:
147155 setattr(cherrypy.serving.request, k, v)
156
148157
149158 def response_namespace(k, v):
150159 """Attach response attributes declared in config."""
155164 else:
156165 setattr(cherrypy.serving.response, k, v)
157166
167
158168 def error_page_namespace(k, v):
159169 """Attach error pages declared in config."""
160170 if k != 'default':
169179
170180
171181 class Request(object):
182
172183 """An HTTP request.
173
184
174185 This object represents the metadata of an HTTP request message;
175186 that is, it contains attributes which describe the environment
176187 in which the request URL, headers, and body were sent (if you
180191 also contains data regarding the configuration in effect for
181192 the given URL, and the execution plan for generating a response.
182193 """
183
194
184195 prev = None
185196 """
186197 The previous Request object (if any). This should be None
187198 unless we are processing an InternalRedirect."""
188
199
189200 # Conversation/connection attributes
190201 local = httputil.Host("127.0.0.1", 80)
191202 "An httputil.Host(ip, port, hostname) object for the server socket."
192
203
193204 remote = httputil.Host("127.0.0.1", 1111)
194205 "An httputil.Host(ip, port, hostname) object for the client socket."
195
206
196207 scheme = "http"
197208 """
198209 The protocol used between client and server. In most cases,
199210 this will be either 'http' or 'https'."""
200
211
201212 server_protocol = "HTTP/1.1"
202213 """
203214 The HTTP version for which the HTTP server is at least
204215 conditionally compliant."""
205
216
206217 base = ""
207218 """The (scheme://host) portion of the requested URL.
208219 In some cases (e.g. when proxying via mod_rewrite), this may contain
209220 path segments which cherrypy.url uses when constructing url's, but
210221 which otherwise are ignored by CherryPy. Regardless, this value
211222 MUST NOT end in a slash."""
212
223
213224 # Request-Line attributes
214225 request_line = ""
215226 """
216227 The complete Request-Line received from the client. This is a
217228 single string consisting of the request method, URI, and protocol
218229 version (joined by spaces). Any final CRLF is removed."""
219
230
220231 method = "GET"
221232 """
222233 Indicates the HTTP method to be performed on the resource identified
224235 DELETE. CherryPy allows any extension method; however, various HTTP
225236 servers and gateways may restrict the set of allowable methods.
226237 CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
227
238
228239 query_string = ""
229240 """
230241 The query component of the Request-URI, a string of information to be
232243 path component, and is separated by a '?'. For example, the URI
233244 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
234245 'a=3&b=4'."""
235
246
236247 query_string_encoding = 'utf8'
237248 """
238249 The encoding expected for query string arguments after % HEX HEX decoding).
241252 arbitrary encodings to not error, set this to 'Latin-1'; you can then
242253 encode back to bytes and re-decode to whatever encoding you like later.
243254 """
244
255
245256 protocol = (1, 1)
246257 """The HTTP protocol version corresponding to the set
247258 of features which should be allowed in the response. If BOTH
249260 compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
250261 If either is 1.0, this attribute will be the tuple (1, 0).
251262 Lower HTTP protocol versions are not explicitly supported."""
252
263
253264 params = {}
254265 """
255266 A dict which combines query string (GET) and request entity (POST)
256267 variables. This is populated in two stages: GET params are added
257268 before the 'on_start_resource' hook, and POST params are added
258269 between the 'before_request_body' and 'before_handler' hooks."""
259
270
260271 # Message attributes
261272 header_list = []
262273 """
263274 A list of the HTTP request headers as (name, value) tuples.
264275 In general, you should use request.headers (a dict) instead."""
265
276
266277 headers = httputil.HeaderMap()
267278 """
268279 A dict-like object containing the request headers. Keys are header
271282 headers['content-type'] refer to the same value. Values are header
272283 values (decoded according to :rfc:`2047` if necessary). See also:
273284 httputil.HeaderMap, httputil.HeaderElement."""
274
285
275286 cookie = SimpleCookie()
276287 """See help(Cookie)."""
277
288
278289 rfile = None
279290 """
280291 If the request included an entity (body), it will be available
282293 be read for you between the 'before_request_body' hook and the
283294 'before_handler' hook, and the resulting string is placed into
284295 either request.params or the request.body attribute.
285
296
286297 You may disable the automatic consumption of the rfile by setting
287298 request.process_request_body to False, either in config for the desired
288299 path, or in an 'on_start_resource' or 'before_request_body' hook.
289
300
290301 WARNING: In almost every case, you should not attempt to read from the
291302 rfile stream after CherryPy's automatic mechanism has read it. If you
292303 turn off the automatic parsing of rfile, you should read exactly the
294305 Ignoring either of these warnings may result in a hung request thread
295306 or in corruption of the next (pipelined) request.
296307 """
297
308
298309 process_request_body = True
299310 """
300311 If True, the rfile (if any) is automatically read and parsed,
301312 and the result placed into request.params or request.body."""
302
313
303314 methods_with_bodies = ("POST", "PUT")
304315 """
305316 A sequence of HTTP methods for which CherryPy will automatically
306 attempt to read a body from the rfile."""
307
317 attempt to read a body from the rfile. If you are going to change
318 this property, modify it on the configuration (recommended)
319 or on the "hook point" `on_start_resource`.
320 """
321
308322 body = None
309323 """
310324 If the request Content-Type is 'application/x-www-form-urlencoded'
312326 of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
313327 can .read()); this value is set between the 'before_request_body' and
314328 'before_handler' hooks (assuming that process_request_body is True)."""
315
329
316330 # Dispatch attributes
317331 dispatch = cherrypy.dispatch.Dispatcher()
318332 """
321335 request attributes, and the application architecture. The core
322336 calls the dispatcher as early as possible, passing it a 'path_info'
323337 argument.
324
338
325339 The default dispatcher discovers the page handler by matching path_info
326340 to a hierarchical arrangement of objects, starting at request.app.root.
327341 See help(cherrypy.dispatch) for more information."""
328
342
329343 script_name = ""
330344 """
331345 The 'mount point' of the application which is handling this request.
332
346
333347 This attribute MUST NOT end in a slash. If the script_name refers to
334348 the root of the URI, it MUST be an empty string (not "/").
335349 """
336
350
337351 path_info = "/"
338352 """
339353 The 'relative path' portion of the Request-URI. This is relative
345359 When authentication is used during the request processing this is
346360 set to 'False' if it failed and to the 'username' value if it succeeded.
347361 The default 'None' implies that no authentication happened."""
348
362
349363 # Note that cherrypy.url uses "if request.app:" to determine whether
350364 # the call is during a real HTTP request or not. So leave this None.
351365 app = None
352366 """The cherrypy.Application object which is handling this request."""
353
367
354368 handler = None
355369 """
356370 The function, method, or other callable which CherryPy will call to
359373 By default, the handler is discovered by walking a tree of objects
360374 starting at request.app.root, and is then passed all HTTP params
361375 (from the query string and POST body) as keyword arguments."""
362
376
363377 toolmaps = {}
364378 """
365379 A nested dict of all Toolboxes and Tools in effect for this request,
366380 of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
367
381
368382 config = None
369383 """
370384 A flat dict of all configuration entries which apply to the
374388 effect for this request; by default, handler config can be attached
375389 anywhere in the tree between request.app.root and the final handler,
376390 and inherits downward)."""
377
391
378392 is_index = None
379393 """
380394 This will be True if the current request is mapped to an 'index'
382396 a slash). The value may be used to automatically redirect the
383397 user-agent to a 'more canonical' URL which either adds or removes
384398 the trailing slash. See cherrypy.tools.trailing_slash."""
385
399
386400 hooks = HookMap(hookpoints)
387401 """
388402 A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
391405 The list of hooks is generally populated as early as possible (mostly
392406 from Tools specified in config), but may be extended at any time.
393407 See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
394
408
395409 error_response = cherrypy.HTTPError(500).set_response
396410 """
397411 The no-arg callable which will handle unexpected, untrapped errors
401415 via request.error_page or by overriding HTTPError.set_response).
402416 By default, error_response uses HTTPError(500) to return a generic
403417 error response to the user-agent."""
404
418
405419 error_page = {}
406420 """
407421 A dict of {error code: response filename or callable} pairs.
408
422
409423 The error code must be an int representing a given HTTP error code,
410424 or the string 'default', which will be used if no matching entry
411425 is found for a given numeric code.
412
426
413427 If a filename is provided, the file should contain a Python string-
414 formatting template, and can expect by default to receive format
428 formatting template, and can expect by default to receive format
415429 values with the mapping keys %(status)s, %(message)s, %(traceback)s,
416430 and %(version)s. The set of format mappings can be extended by
417431 overriding HTTPError.set_response.
418
432
419433 If a callable is provided, it will be called by default with keyword
420434 arguments 'status', 'message', 'traceback', and 'version', as for a
421 string-formatting template. The callable must return a string or iterable of
422 strings which will be set to response.body. It may also override headers or
423 perform any other processing.
424
435 string-formatting template. The callable must return a string or
436 iterable of strings which will be set to response.body. It may also
437 override headers or perform any other processing.
438
425439 If no entry is given for an error code, and no 'default' entry exists,
426440 a default template will be used.
427441 """
428
442
429443 show_tracebacks = True
430444 """
431445 If True, unexpected errors encountered during request processing will
435449 """
436450 If True, mismatched parameters encountered during PageHandler invocation
437451 processing will be included in the response body."""
438
452
439453 throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
440454 """The sequence of exceptions which Request.run does not trap."""
441
455
442456 throw_errors = False
443457 """
444458 If True, Request.run will not trap any errors (except HTTPRedirect and
445459 HTTPError, which are more properly called 'exceptions', not errors)."""
446
460
447461 closed = False
448462 """True once the close method has been called, False otherwise."""
449
463
450464 stage = None
451465 """
452466 A string containing the stage reached in the request-handling process.
453467 This is useful when debugging a live server with hung requests."""
454
468
455469 namespaces = _cpconfig.NamespaceSet(
456470 **{"hooks": hooks_namespace,
457471 "request": request_namespace,
459473 "error_page": error_page_namespace,
460474 "tools": cherrypy.tools,
461475 })
462
476
463477 def __init__(self, local_host, remote_host, scheme="http",
464478 server_protocol="HTTP/1.1"):
465479 """Populate a new Request object.
466
480
467481 local_host should be an httputil.Host object with the server info.
468482 remote_host should be an httputil.Host object with the client info.
469483 scheme should be a string, either "http" or "https".
472486 self.remote = remote_host
473487 self.scheme = scheme
474488 self.server_protocol = server_protocol
475
489
476490 self.closed = False
477
491
478492 # Put a *copy* of the class error_page into self.
479493 self.error_page = self.error_page.copy()
480
494
481495 # Put a *copy* of the class namespaces into self.
482496 self.namespaces = self.namespaces.copy()
483
497
484498 self.stage = None
485
499
486500 def close(self):
487501 """Run cleanup code. (Core)"""
488502 if not self.closed:
490504 self.stage = 'on_end_request'
491505 self.hooks.run('on_end_request')
492506 self.stage = 'close'
493
507
494508 def run(self, method, path, query_string, req_protocol, headers, rfile):
495509 r"""Process the Request. (Core)
496
510
497511 method, path, query_string, and req_protocol should be pulled directly
498512 from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
499
513
500514 path
501515 This should be %XX-unquoted, but query_string should not be.
502
516
503517 When using Python 2, they both MUST be byte strings,
504518 not unicode strings.
505
519
506520 When using Python 3, they both MUST be unicode strings,
507521 not byte strings, and preferably not bytes \x00-\xFF
508522 disguised as unicode.
509
523
510524 headers
511525 A list of (name, value) tuples.
512
526
513527 rfile
514528 A file-like object containing the HTTP request entity.
515
529
516530 When run() is done, the returned object should have 3 attributes:
517
531
518532 * status, e.g. "200 OK"
519533 * header_list, a list of (name, value) tuples
520534 * body, an iterable yielding strings
521
535
522536 Consumer code (HTTP servers) should then access these response
523537 attributes to build the outbound stream.
524
538
525539 """
526540 response = cherrypy.serving.response
527541 self.stage = 'run'
528542 try:
529543 self.error_response = cherrypy.HTTPError(500).set_response
530
544
531545 self.method = method
532546 path = path or "/"
533547 self.query_string = query_string or ''
534548 self.params = {}
535
549
536550 # Compare request and server HTTP protocol versions, in case our
537551 # server does not support the requested protocol. Limit our output
538552 # to min(req, server). We want the following output:
549563 sp = int(self.server_protocol[5]), int(self.server_protocol[7])
550564 self.protocol = min(rp, sp)
551565 response.headers.protocol = self.protocol
552
566
553567 # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
554568 url = path
555569 if query_string:
556570 url += '?' + query_string
557571 self.request_line = '%s %s %s' % (method, url, req_protocol)
558
572
559573 self.header_list = list(headers)
560574 self.headers = httputil.HeaderMap()
561
575
562576 self.rfile = rfile
563577 self.body = None
564
578
565579 self.cookie = SimpleCookie()
566580 self.handler = None
567
581
568582 # path_info should be the path from the
569583 # app root (script_name) to the handler.
570584 self.script_name = self.app.script_name
571585 self.path_info = pi = path[len(self.script_name):]
572
586
573587 self.stage = 'respond'
574588 self.respond(pi)
575
589
576590 except self.throws:
577591 raise
578592 except:
588602 body = ""
589603 r = bare_error(body)
590604 response.output_status, response.header_list, response.body = r
591
605
592606 if self.method == "HEAD":
593607 # HEAD requests MUST NOT return a message-body in the response.
594608 response.body = []
595
609
596610 try:
597611 cherrypy.log.access()
598612 except:
599613 cherrypy.log.error(traceback=True)
600
614
601615 if response.timed_out:
602616 raise cherrypy.TimeoutError()
603
617
604618 return response
605
619
606620 # Uncomment for stage debugging
607621 # stage = property(lambda self: self._stage, lambda self, v: print(v))
608
622
609623 def respond(self, path_info):
610624 """Generate a response for the resource at self.path_info. (Core)"""
611625 response = cherrypy.serving.response
614628 try:
615629 if self.app is None:
616630 raise cherrypy.NotFound()
617
631
618632 # Get the 'Host' header, so we can HTTPRedirect properly.
619633 self.stage = 'process_headers'
620634 self.process_headers()
621
635
622636 # Make a copy of the class hooks
623637 self.hooks = self.__class__.hooks.copy()
624638 self.toolmaps = {}
625
639
626640 self.stage = 'get_resource'
627641 self.get_resource(path_info)
628
642
629643 self.body = _cpreqbody.RequestBody(
630644 self.rfile, self.headers, request_params=self.params)
631
645
632646 self.namespaces(self.config)
633
647
634648 self.stage = 'on_start_resource'
635649 self.hooks.run('on_start_resource')
636
650
637651 # Parse the querystring
638652 self.stage = 'process_query_string'
639653 self.process_query_string()
640
654
641655 # Process the body
642656 if self.process_request_body:
643657 if self.method not in self.methods_with_bodies:
646660 self.hooks.run('before_request_body')
647661 if self.process_request_body:
648662 self.body.process()
649
663
650664 # Run the handler
651665 self.stage = 'before_handler'
652666 self.hooks.run('before_handler')
653667 if self.handler:
654668 self.stage = 'handler'
655669 response.body = self.handler()
656
670
657671 # Finalize
658672 self.stage = 'before_finalize'
659673 self.hooks.run('before_finalize')
673687 if self.throw_errors:
674688 raise
675689 self.handle_error()
676
690
677691 def process_query_string(self):
678692 """Parse the query string into Python structures. (Core)"""
679693 try:
684698 404, "The given query string could not be processed. Query "
685699 "strings for this resource must be encoded with %r." %
686700 self.query_string_encoding)
687
701
688702 # Python 2 only: keyword arguments must be byte strings (type 'str').
689703 if not py3k:
690704 for key, value in p.items():
692706 del p[key]
693707 p[key.encode(self.query_string_encoding)] = value
694708 self.params.update(p)
695
709
696710 def process_headers(self):
697711 """Parse HTTP header data into Python structures. (Core)"""
698712 # Process the headers into self.headers
702716 # so title doesn't have to be called twice.
703717 name = name.title()
704718 value = value.strip()
705
706 # Warning: if there is more than one header entry for cookies (AFAIK,
707 # only Konqueror does that), only the last one will remain in headers
708 # (but they will be correctly stored in request.cookie).
719
720 # Warning: if there is more than one header entry for cookies
721 # (AFAIK, only Konqueror does that), only the last one will
722 # remain in headers (but they will be correctly stored in
723 # request.cookie).
709724 if "=?" in value:
710725 dict.__setitem__(headers, name, httputil.decode_TEXT(value))
711726 else:
712727 dict.__setitem__(headers, name, value)
713
728
714729 # Handle cookies differently because on Konqueror, multiple
715730 # cookies come on different lines with the same key
716731 if name == 'Cookie':
719734 except CookieError:
720735 msg = "Illegal cookie name %s" % value.split('=')[0]
721736 raise cherrypy.HTTPError(400, msg)
722
737
723738 if not dict.__contains__(headers, 'Host'):
724739 # All Internet-based HTTP/1.1 servers MUST respond with a 400
725740 # (Bad Request) status code to any HTTP/1.1 request message
731746 if not host:
732747 host = self.local.name or self.local.ip
733748 self.base = "%s://%s" % (self.scheme, host)
734
749
735750 def get_resource(self, path):
736751 """Call a dispatcher (which sets self.handler and .config). (Core)"""
737752 # First, see if there is a custom dispatch at this URI. Custom
738753 # dispatchers can only be specified in app.config, not in _cp_config
739754 # (since custom dispatchers may not even have an app.root).
740 dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
741
755 dispatch = self.app.find_config(
756 path, "request.dispatch", self.dispatch)
757
742758 # dispatch() should set self.handler and self.config
743759 dispatch(path)
744
760
745761 def handle_error(self):
746762 """Handle the last unanticipated exception. (Core)"""
747763 try:
754770 inst = sys.exc_info()[1]
755771 inst.set_response()
756772 cherrypy.serving.response.finalize()
757
773
758774 # ------------------------- Properties ------------------------- #
759
775
760776 def _get_body_params(self):
761777 warnings.warn(
762 "body_params is deprecated in CherryPy 3.2, will be removed in "
763 "CherryPy 3.3.",
764 DeprecationWarning
765 )
778 "body_params is deprecated in CherryPy 3.2, will be removed in "
779 "CherryPy 3.3.",
780 DeprecationWarning
781 )
766782 return self.body.params
767783 body_params = property(_get_body_params,
768 doc= """
784 doc="""
769785 If the request Content-Type is 'application/x-www-form-urlencoded' or
770786 multipart, this will be a dict of the params pulled from the entity
771787 body; that is, it will be the portion of request.params that come
773789 can be sent with various HTTP method verbs). This value is set between
774790 the 'before_request_body' and 'before_handler' hooks (assuming that
775791 process_request_body is True).
776
792
777793 Deprecated in 3.2, will be removed for 3.3 in favor of
778794 :attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
779795
780796
781797 class ResponseBody(object):
798
782799 """The body of the HTTP response (the response entity)."""
783
800
784801 if py3k:
785802 unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
786803 "if you wish to return unicode.")
787
804
788805 def __get__(self, obj, objclass=None):
789806 if obj is None:
790807 # When calling on the class instead of an instance...
791808 return self
792809 else:
793810 return obj._body
794
811
795812 def __set__(self, obj, value):
796813 # Convert the given value to an iterable object.
797814 if py3k and isinstance(value, str):
798815 raise ValueError(self.unicode_err)
799
816
800817 if isinstance(value, basestring):
801818 # strings get wrapped in a list because iterating over a single
802819 # item list is much faster than iterating over every character
807824 # [''] doesn't evaluate to False, so replace it with [].
808825 value = []
809826 elif py3k and isinstance(value, list):
810 # every item in a list must be bytes...
827 # every item in a list must be bytes...
811828 for i, item in enumerate(value):
812829 if isinstance(item, str):
813830 raise ValueError(self.unicode_err)
821838
822839
823840 class Response(object):
841
824842 """An HTTP Response, including status, headers, and body."""
825
843
826844 status = ""
827845 """The HTTP Status-Code and Reason-Phrase."""
828
846
829847 header_list = []
830848 """
831849 A list of the HTTP response headers as (name, value) tuples.
832850 In general, you should use response.headers (a dict) instead. This
833851 attribute is generated from response.headers and is not valid until
834852 after the finalize phase."""
835
853
836854 headers = httputil.HeaderMap()
837855 """
838856 A dict-like object containing the response headers. Keys are header
840858 a case-insensitive manner. That is, headers['Content-Type'] and
841859 headers['content-type'] refer to the same value. Values are header
842860 values (decoded according to :rfc:`2047` if necessary).
843
861
844862 .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
845863 """
846
864
847865 cookie = SimpleCookie()
848866 """See help(Cookie)."""
849
867
850868 body = ResponseBody()
851869 """The body (entity) of the HTTP response."""
852
870
853871 time = None
854872 """The value of time.time() when created. Use in HTTP dates."""
855
873
856874 timeout = 300
857875 """Seconds after which the response will be aborted."""
858
876
859877 timed_out = False
860878 """
861879 Flag to indicate the response should be aborted, because it has
862880 exceeded its timeout."""
863
881
864882 stream = False
865883 """If False, buffer the response body."""
866
884
867885 def __init__(self):
868886 self.status = None
869887 self.header_list = None
870888 self._body = []
871889 self.time = time.time()
872
890
873891 self.headers = httputil.HeaderMap()
874892 # Since we know all our keys are titled strings, we can
875893 # bypass HeaderMap.update and get a big speed boost.
879897 "Date": httputil.HTTPDate(self.time),
880898 })
881899 self.cookie = SimpleCookie()
882
900
883901 def collapse_body(self):
884902 """Collapse self.body to a single string; replace it and return it."""
885903 if isinstance(self.body, basestring):
886904 return self.body
887
905
888906 newbody = []
889907 for chunk in self.body:
890908 if py3k and not isinstance(chunk, bytes):
891 raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk))
909 raise TypeError("Chunk %s is not of type 'bytes'." %
910 repr(chunk))
892911 newbody.append(chunk)
893912 newbody = ntob('').join(newbody)
894
913
895914 self.body = newbody
896915 return newbody
897
916
898917 def finalize(self):
899918 """Transform headers (and cookies) into self.header_list. (Core)"""
900919 try:
901920 code, reason, _ = httputil.valid_status(self.status)
902921 except ValueError:
903922 raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
904
923
905924 headers = self.headers
906
925
907926 self.status = "%s %s" % (code, reason)
908 self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
909
927 self.output_status = ntob(str(code), 'ascii') + \
928 ntob(" ") + headers.encode(reason)
929
910930 if self.stream:
911931 # The upshot: wsgiserver will chunk the response if
912932 # you pop Content-Length (or set it explicitly to None).
925945 if dict.get(headers, 'Content-Length') is None:
926946 content = self.collapse_body()
927947 dict.__setitem__(headers, 'Content-Length', len(content))
928
948
929949 # Transform our header dict into a list of tuples.
930950 self.header_list = h = headers.output()
931
951
932952 cookie = self.cookie.output()
933953 if cookie:
934954 for line in cookie.split("\n"):
941961 if isinstance(value, unicodestr):
942962 value = headers.encode(value)
943963 h.append((name, value))
944
964
945965 def check_timeout(self):
946966 """If now > self.time + self.timeout, set self.timed_out.
947
967
948968 This purposefully sets a flag, rather than raising an error,
949969 so that a monitor thread can interrupt the Response thread.
950970 """
951971 if time.time() > self.time + self.timeout:
952972 self.timed_out = True
953
954
955
1111
1212
1313 class Server(ServerAdapter):
14
1415 """An adapter for an HTTP server.
15
16
1617 You can set attributes (like socket_host and socket_port)
1718 on *this* object (which is probably cherrypy.server), and call
1819 quickstart. For example::
19
20
2021 cherrypy.server.socket_port = 80
2122 cherrypy.quickstart()
2223 """
23
24
2425 socket_port = 8080
2526 """The TCP port on which to listen for connections."""
26
27
2728 _socket_host = '127.0.0.1'
29
2830 def _get_socket_host(self):
2931 return self._socket_host
32
3033 def _set_socket_host(self, value):
3134 if value == '':
3235 raise ValueError("The empty string ('') is not an allowed value. "
3336 "Use '0.0.0.0' instead to listen on all active "
3437 "interfaces (INADDR_ANY).")
3538 self._socket_host = value
36 socket_host = property(_get_socket_host, _set_socket_host,
39 socket_host = property(
40 _get_socket_host,
41 _set_socket_host,
3742 doc="""The hostname or IP address on which to listen for connections.
38
43
3944 Host values may be any IPv4 or IPv6 address, or any valid hostname.
4045 The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
4146 your hosts file prefers IPv6). The string '0.0.0.0' is a special
4247 IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
4348 is the similar IN6ADDR_ANY for IPv6. The empty string or None are
4449 not allowed.""")
45
50
4651 socket_file = None
4752 """If given, the name of the UNIX socket to use instead of TCP/IP.
48
53
4954 When this option is not None, the `socket_host` and `socket_port` options
5055 are ignored."""
51
56
5257 socket_queue_size = 5
5358 """The 'backlog' argument to socket.listen(); specifies the maximum number
5459 of queued connections (default 5)."""
55
60
5661 socket_timeout = 10
5762 """The timeout in seconds for accepted connections (default 10)."""
58
63
5964 shutdown_timeout = 5
6065 """The time to wait for HTTP worker threads to clean up."""
61
66
6267 protocol_version = 'HTTP/1.1'
6368 """The version string to write in the Status-Line of all HTTP responses,
6469 for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
6570 this should also limit the supported features used in the response."""
66
71
6772 thread_pool = 10
6873 """The number of worker threads to start up in the pool."""
69
74
7075 thread_pool_max = -1
71 """The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
72
76 """The maximum size of the worker-thread pool. Use -1 to indicate no limit.
77 """
78
7379 max_request_header_size = 500 * 1024
74 """The maximum number of bytes allowable in the request headers. If exceeded,
75 the HTTP server should return "413 Request Entity Too Large"."""
76
80 """The maximum number of bytes allowable in the request headers.
81 If exceeded, the HTTP server should return "413 Request Entity Too Large".
82 """
83
7784 max_request_body_size = 100 * 1024 * 1024
7885 """The maximum number of bytes allowable in the request body. If exceeded,
7986 the HTTP server should return "413 Request Entity Too Large"."""
80
87
8188 instance = None
8289 """If not None, this should be an HTTP server instance (such as
8390 CPWSGIServer) which cherrypy.server will control. Use this when you need
8491 more control over object instantiation than is available in the various
8592 configuration options."""
86
93
8794 ssl_context = None
8895 """When using PyOpenSSL, an instance of SSL.Context."""
89
96
9097 ssl_certificate = None
9198 """The filename of the SSL certificate to use."""
92
99
93100 ssl_certificate_chain = None
94101 """When using PyOpenSSL, the certificate chain to pass to
95102 Context.load_verify_locations."""
96
103
97104 ssl_private_key = None
98105 """The filename of the private key to use with SSL."""
99
106
100107 if py3k:
101108 ssl_module = 'builtin'
102 """The name of a registered SSL adaptation module to use with the builtin
103 WSGI server. Builtin options are: 'builtin' (to use the SSL library built
104 into recent versions of Python). You may also register your
105 own classes in the wsgiserver.ssl_adapters dict."""
109 """The name of a registered SSL adaptation module to use with
110 the builtin WSGI server. Builtin options are: 'builtin' (to
111 use the SSL library built into recent versions of Python).
112 You may also register your own classes in the
113 wsgiserver.ssl_adapters dict."""
106114 else:
107115 ssl_module = 'pyopenssl'
108 """The name of a registered SSL adaptation module to use with the builtin
109 WSGI server. Builtin options are 'builtin' (to use the SSL library built
110 into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
111 project, which you must install separately). You may also register your
112 own classes in the wsgiserver.ssl_adapters dict."""
113
116 """The name of a registered SSL adaptation module to use with the
117 builtin WSGI server. Builtin options are 'builtin' (to use the SSL
118 library built into recent versions of Python) and 'pyopenssl' (to
119 use the PyOpenSSL project, which you must install separately). You
120 may also register your own classes in the wsgiserver.ssl_adapters
121 dict."""
122
114123 statistics = False
115124 """Turns statistics-gathering on or off for aware HTTP servers."""
116
125
117126 nodelay = True
118127 """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
119
128
120129 wsgi_version = (1, 0)
121130 """The WSGI version tuple to use with the builtin WSGI server.
122131 The provided options are (1, 0) [which includes support for PEP 3333,
124133 wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
125134 You may create and register your own experimental versions of the WSGI
126135 protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
127
136
128137 def __init__(self):
129138 self.bus = cherrypy.engine
130139 self.httpserver = None
131140 self.interrupt = None
132141 self.running = False
133
142
134143 def httpserver_from_self(self, httpserver=None):
135144 """Return a (httpserver, bind_addr) pair based on self attributes."""
136145 if httpserver is None:
142151 # Is anyone using this? Can I add an arg?
143152 httpserver = attributes(httpserver)(self)
144153 return httpserver, self.bind_addr
145
154
146155 def start(self):
147156 """Start the HTTP server."""
148157 if not self.httpserver:
149158 self.httpserver, self.bind_addr = self.httpserver_from_self()
150159 ServerAdapter.start(self)
151160 start.priority = 75
152
161
153162 def _get_bind_addr(self):
154163 if self.socket_file:
155164 return self.socket_file
156165 if self.socket_host is None and self.socket_port is None:
157166 return None
158167 return (self.socket_host, self.socket_port)
168
159169 def _set_bind_addr(self, value):
160170 if value is None:
161171 self.socket_file = None
173183 raise ValueError("bind_addr must be a (host, port) tuple "
174184 "(for TCP sockets) or a string (for Unix "
175185 "domain sockets), not %r" % value)
176 bind_addr = property(_get_bind_addr, _set_bind_addr,
177 doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
178
186 bind_addr = property(
187 _get_bind_addr,
188 _set_bind_addr,
189 doc='A (host, port) tuple for TCP sockets or '
190 'a str for Unix domain sockets.')
191
179192 def base(self):
180 """Return the base (scheme://host[:port] or sock file) for this server."""
193 """Return the base (scheme://host[:port] or sock file) for this server.
194 """
181195 if self.socket_file:
182196 return self.socket_file
183
197
184198 host = self.socket_host
185199 if host in ('0.0.0.0', '::'):
186200 # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
188202 # safest thing to spit out in a URL.
189203 import socket
190204 host = socket.gethostname()
191
205
192206 port = self.socket_port
193
207
194208 if self.ssl_certificate:
195209 scheme = "https"
196210 if port != 443:
199213 scheme = "http"
200214 if port != 80:
201215 host += ":%s" % port
202
216
203217 return "%s://%s" % (scheme, host)
204
136136
137137 # Threading import is at end
138138
139
139140 class _localbase(object):
140141 __slots__ = '_local__key', '_local__args', '_local__lock'
141142
156157 currentThread().__dict__[key] = dict
157158
158159 return self
160
159161
160162 def _patch(self):
161163 key = object.__getattribute__(self, '_local__key')
174176 else:
175177 object.__setattr__(self, '__dict__', d)
176178
179
177180 class local(_localbase):
178181
179182 def __getattribute__(self, name):
203206 finally:
204207 lock.release()
205208
206
207209 def __del__():
208210 threading_enumerate = enumerate
209211 __getattribute__ = object.__getattribute__
230232 try:
231233 del __dict__[key]
232234 except KeyError:
233 pass # didn't have anything in this thread
235 pass # didn't have anything in this thread
234236
235237 return __del__
236238 __del__ = __del__()
11
22 Tools are usually designed to be used in a variety of ways (although some
33 may only offer one if they choose):
4
4
55 Library calls
66 All tools are callables that can be used wherever needed.
77 The arguments are straightforward and should be detailed within the
88 docstring.
9
9
1010 Function decorators
1111 All tools, when called, may be used as decorators which configure
1212 individual CherryPy page handlers (methods on the CherryPy tree).
1313 That is, "@tools.anytool()" should "turn on" the tool via the
1414 decorated function's _cp_config attribute.
15
15
1616 CherryPy config
1717 If a tool exposes a "_setup" callable, it will be called
1818 once per Request (if the feature is "turned on" via config).
4242 return co.co_varnames[:co.co_argcount]
4343
4444
45 _attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
46 "on via config, or use them as decorators on your page handlers.")
45 _attr_error = (
46 "CherryPy Tools cannot be turned on directly. Instead, turn them "
47 "on via config, or use them as decorators on your page handlers."
48 )
49
4750
4851 class Tool(object):
52
4953 """A registered function for use with CherryPy request-processing hooks.
50
54
5155 help(tool.callable) should give you more information about this Tool.
5256 """
53
57
5458 namespace = "tools"
55
59
5660 def __init__(self, point, callable, name=None, priority=50):
5761 self._point = point
5862 self.callable = callable
6064 self._priority = priority
6165 self.__doc__ = self.callable.__doc__
6266 self._setargs()
63
67
6468 def _get_on(self):
6569 raise AttributeError(_attr_error)
70
6671 def _set_on(self, value):
6772 raise AttributeError(_attr_error)
6873 on = property(_get_on, _set_on)
69
74
7075 def _setargs(self):
7176 """Copy func parameter names to obj attributes."""
7277 try:
8590 # but if we trap it here it doesn't prevent CP from working.
8691 except IndexError:
8792 pass
88
93
8994 def _merged_args(self, d=None):
9095 """Return a dict of configuration entries for this Tool."""
9196 if d:
9297 conf = d.copy()
9398 else:
9499 conf = {}
95
100
96101 tm = cherrypy.serving.request.toolmaps[self.namespace]
97102 if self._name in tm:
98103 conf.update(tm[self._name])
99
104
100105 if "on" in conf:
101106 del conf["on"]
102
107
103108 return conf
104
109
105110 def __call__(self, *args, **kwargs):
106111 """Compile-time decorator (turn on the tool in config).
107
112
108113 For example::
109
114
110115 @tools.proxy()
111116 def whats_my_base(self):
112117 return cherrypy.request.base
116121 raise TypeError("The %r Tool does not accept positional "
117122 "arguments; you must use keyword arguments."
118123 % self._name)
124
119125 def tool_decorator(f):
120126 if not hasattr(f, "_cp_config"):
121127 f._cp_config = {}
125131 f._cp_config[subspace + k] = v
126132 return f
127133 return tool_decorator
128
134
129135 def _setup(self):
130136 """Hook this tool into cherrypy.request.
131
137
132138 The standard CherryPy request object will automatically call this
133139 method when the tool is "turned on" in config.
134140 """
141147
142148
143149 class HandlerTool(Tool):
150
144151 """Tool which is called 'before main', that may skip normal handlers.
145
152
146153 If the tool successfully handles the request (by setting response.body),
147154 if should return True. This will cause CherryPy to skip any 'normal' page
148155 handler. If the tool did not handle the request, it should return False
150157 tool is declared AS a page handler (see the 'handler' method), returning
151158 False will raise NotFound.
152159 """
153
160
154161 def __init__(self, callable, name=None):
155162 Tool.__init__(self, 'before_handler', callable, name)
156
163
157164 def handler(self, *args, **kwargs):
158165 """Use this tool as a CherryPy page handler.
159
166
160167 For example::
161
168
162169 class Root:
163170 nav = tools.staticdir.handler(section="/nav", dir="nav",
164171 root=absDir)
170177 return cherrypy.serving.response.body
171178 handle_func.exposed = True
172179 return handle_func
173
180
174181 def _wrapper(self, **kwargs):
175182 if self.callable(**kwargs):
176183 cherrypy.serving.request.handler = None
177
184
178185 def _setup(self):
179186 """Hook this tool into cherrypy.request.
180
187
181188 The standard CherryPy request object will automatically call this
182189 method when the tool is "turned on" in config.
183190 """
190197
191198
192199 class HandlerWrapperTool(Tool):
200
193201 """Tool which wraps request.handler in a provided wrapper function.
194
202
195203 The 'newhandler' arg must be a handler wrapper function that takes a
196204 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
197205 page handler
198206 functions, it must return an iterable for use as cherrypy.response.body.
199
207
200208 For example, to allow your 'inner' page handlers to return dicts
201209 which then get interpolated into a template::
202
210
203211 def interpolator(next_handler, *args, **kwargs):
204212 filename = cherrypy.request.config.get('template')
205213 cherrypy.response.template = env.get_template(filename)
207215 return cherrypy.response.template.render(**response_dict)
208216 cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
209217 """
210
211 def __init__(self, newhandler, point='before_handler', name=None, priority=50):
218
219 def __init__(self, newhandler, point='before_handler', name=None,
220 priority=50):
212221 self.newhandler = newhandler
213222 self._point = point
214223 self._name = name
215224 self._priority = priority
216
225
217226 def callable(self, debug=False):
218227 innerfunc = cherrypy.serving.request.handler
228
219229 def wrap(*args, **kwargs):
220230 return self.newhandler(innerfunc, *args, **kwargs)
221231 cherrypy.serving.request.handler = wrap
222232
223233
224234 class ErrorTool(Tool):
235
225236 """Tool which is used to replace the default request.error_response."""
226
237
227238 def __init__(self, callable, name=None):
228239 Tool.__init__(self, None, callable, name)
229
240
230241 def _wrapper(self):
231242 self.callable(**self._merged_args())
232
243
233244 def _setup(self):
234245 """Hook this tool into cherrypy.request.
235
246
236247 The standard CherryPy request object will automatically call this
237248 method when the tool is "turned on" in config.
238249 """
248259
249260
250261 class SessionTool(Tool):
262
251263 """Session Tool for CherryPy.
252
264
253265 sessions.locking
254266 When 'implicit' (the default), the session will be locked for you,
255267 just before running the page handler.
256
268
257269 When 'early', the session will be locked before reading the request
258270 body. This is off by default for safety reasons; for example,
259271 a large upload would block the session, denying an AJAX
260 progress meter (see http://www.cherrypy.org/ticket/630).
261
272 progress meter
273 (`issue <https://bitbucket.org/cherrypy/cherrypy/issue/630>`_).
274
262275 When 'explicit' (or any other value), you need to call
263276 cherrypy.session.acquire_lock() yourself before using
264277 session data.
265278 """
266
279
267280 def __init__(self):
268281 # _sessions.init must be bound after headers are read
269282 Tool.__init__(self, 'before_request_body', _sessions.init)
270
283
271284 def _lock_session(self):
272285 cherrypy.serving.session.acquire_lock()
273
286
274287 def _setup(self):
275288 """Hook this tool into cherrypy.request.
276
289
277290 The standard CherryPy request object will automatically call this
278291 method when the tool is "turned on" in config.
279292 """
280293 hooks = cherrypy.serving.request.hooks
281
294
282295 conf = self._merged_args()
283
296
284297 p = conf.pop("priority", None)
285298 if p is None:
286299 p = getattr(self.callable, "priority", self._priority)
287
300
288301 hooks.attach(self._point, self.callable, priority=p, **conf)
289
302
290303 locking = conf.pop('locking', 'implicit')
291304 if locking == 'implicit':
292305 hooks.attach('before_handler', self._lock_session)
297310 else:
298311 # Don't lock
299312 pass
300
313
301314 hooks.attach('before_finalize', _sessions.save)
302315 hooks.attach('on_end_request', _sessions.close)
303
316
304317 def regenerate(self):
305318 """Drop the current session and make a new one (with a new id)."""
306319 sess = cherrypy.serving.session
307320 sess.regenerate()
308
321
309322 # Grab cookie-relevant tool args
310323 conf = dict([(k, v) for k, v in self._merged_args().items()
311324 if k in ('path', 'path_header', 'name', 'timeout',
313326 _sessions.set_response_cookie(**conf)
314327
315328
316
317
318329 class XMLRPCController(object):
330
319331 """A Controller (page handler collection) for XML-RPC.
320
332
321333 To use it, have your controllers subclass this base class (it will
322334 turn on the tool for you).
323
335
324336 You can also supply the following optional config entries::
325
337
326338 tools.xmlrpc.encoding: 'utf-8'
327339 tools.xmlrpc.allow_none: 0
328
340
329341 XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
330342 appropriate handler must first be performed according to the URL, and
331343 then a second dispatch step must take place according to the RPC method
333345 prefix in the URL, supplies its own handler args in the body, and
334346 requires a 200 OK "Fault" response instead of 404 when the desired
335347 method is not found.
336
348
337349 Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
338350 This Controller acts as the dispatch target for the first half (based
339351 on the URL); it then reads the RPC method from the request body and
340352 does its own second dispatch step based on that method. It also reads
341353 body params, and returns a Fault on error.
342
354
343355 The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
344356 in your URL's, you can safely skip turning on the XMLRPCDispatcher.
345357 Otherwise, you need to use declare it in config::
346
358
347359 request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
348360 """
349
361
350362 # Note we're hard-coding this into the 'tools' namespace. We could do
351363 # a huge amount of work to make it relocatable, but the only reason why
352364 # would be if someone actually disabled the default_toolbox. Meh.
353365 _cp_config = {'tools.xmlrpc.on': True}
354
366
355367 def default(self, *vpath, **params):
356368 rpcparams, rpcmethod = _xmlrpc.process_body()
357
369
358370 subhandler = self
359371 for attr in str(rpcmethod).split('.'):
360372 subhandler = getattr(subhandler, attr, None)
361
373
362374 if subhandler and getattr(subhandler, "exposed", False):
363375 body = subhandler(*(vpath + rpcparams), **params)
364
376
365377 else:
366 # http://www.cherrypy.org/ticket/533
378 # https://bitbucket.org/cherrypy/cherrypy/issue/533
367379 # if a method is not found, an xmlrpclib.Fault should be returned
368380 # raising an exception here will do that; see
369381 # cherrypy.lib.xmlrpcutil.on_error
370382 raise Exception('method "%s" is not supported' % attr)
371
383
372384 conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
373385 _xmlrpc.respond(body,
374386 conf.get('encoding', 'utf-8'),
378390
379391
380392 class SessionAuthTool(HandlerTool):
381
393
382394 def _setargs(self):
383395 for name in dir(cptools.SessionAuth):
384396 if not name.startswith("__"):
386398
387399
388400 class CachingTool(Tool):
401
389402 """Caching Tool for CherryPy."""
390
403
391404 def _wrapper(self, **kwargs):
392405 request = cherrypy.serving.request
393406 if _caching.get(**kwargs):
396409 if request.cacheable:
397410 # Note the devious technique here of adding hooks on the fly
398411 request.hooks.attach('before_finalize', _caching.tee_output,
399 priority = 90)
412 priority=90)
400413 _wrapper.priority = 20
401
414
402415 def _setup(self):
403416 """Hook caching into cherrypy.request."""
404417 conf = self._merged_args()
405
418
406419 p = conf.pop("priority", None)
407420 cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
408421 priority=p, **conf)
409422
410423
411
412424 class Toolbox(object):
425
413426 """A collection of Tools.
414
427
415428 This object also functions as a config namespace handler for itself.
416429 Custom toolboxes should be added to each Application's toolboxes dict.
417430 """
418
431
419432 def __init__(self, namespace):
420433 self.namespace = namespace
421
434
422435 def __setattr__(self, name, value):
423436 # If the Tool._name is None, supply it from the attribute name.
424437 if isinstance(value, Tool):
426439 value._name = name
427440 value.namespace = self.namespace
428441 object.__setattr__(self, name, value)
429
442
430443 def __enter__(self):
431444 """Populate request.toolmaps from tools specified in config."""
432445 cherrypy.serving.request.toolmaps[self.namespace] = map = {}
446
433447 def populate(k, v):
434448 toolname, arg = k.split(".", 1)
435449 bucket = map.setdefault(toolname, {})
436450 bucket[arg] = v
437451 return populate
438
452
439453 def __exit__(self, exc_type, exc_val, exc_tb):
440454 """Run tool._setup() for each tool in our toolmap."""
441455 map = cherrypy.serving.request.toolmaps.get(self.namespace)
447461
448462
449463 class DeprecatedTool(Tool):
450
464
451465 _name = None
452466 warnmsg = "This Tool is deprecated."
453
467
454468 def __init__(self, point, warnmsg=None):
455469 self.point = point
456470 if warnmsg is not None:
457471 self.warnmsg = warnmsg
458
472
459473 def __call__(self, *args, **kwargs):
460474 warnings.warn(self.warnmsg)
475
461476 def tool_decorator(f):
462477 return f
463478 return tool_decorator
464
479
465480 def _setup(self):
466481 warnings.warn(self.warnmsg)
467482
486501 _d.xmlrpc = ErrorTool(_xmlrpc.on_error)
487502 _d.caching = CachingTool('before_handler', _caching.get, 'caching')
488503 _d.expires = Tool('before_finalize', _caching.expires)
489 _d.tidy = DeprecatedTool('before_finalize',
490 "The tidy tool has been removed from the standard distribution of CherryPy. "
491 "The most recent version can be found at http://tools.cherrypy.org/browser.")
492 _d.nsgmls = DeprecatedTool('before_finalize',
493 "The nsgmls tool has been removed from the standard distribution of CherryPy. "
494 "The most recent version can be found at http://tools.cherrypy.org/browser.")
504 _d.tidy = DeprecatedTool(
505 'before_finalize',
506 "The tidy tool has been removed from the standard distribution of "
507 "CherryPy. The most recent version can be found at "
508 "http://tools.cherrypy.org/browser.")
509 _d.nsgmls = DeprecatedTool(
510 'before_finalize',
511 "The nsgmls tool has been removed from the standard distribution of "
512 "CherryPy. The most recent version can be found at "
513 "http://tools.cherrypy.org/browser.")
495514 _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
496515 _d.referer = Tool('before_request_body', cptools.referer)
497516 _d.basic_auth = Tool('on_start_resource', auth.basic_auth)
00 """CherryPy Application and Tree objects."""
11
22 import os
3 import sys
43
54 import cherrypy
65 from cherrypy._cpcompat import ntou, py3k
98
109
1110 class Application(object):
11
1212 """A CherryPy Application.
13
13
1414 Servers and gateways should not instantiate Request objects directly.
1515 Instead, they should ask an Application object for a request object.
16
16
1717 An instance of this class may also be used as a WSGI callable
1818 (WSGI application object) for itself.
1919 """
20
20
2121 root = None
2222 """The top-most container of page handlers for this app. Handlers should
2323 be arranged in a hierarchy of attributes, matching the expected URI
2424 hierarchy; the default dispatcher then searches this hierarchy for a
2525 matching handler. When using a dispatcher other than the default,
2626 this value may be None."""
27
27
2828 config = {}
2929 """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
3030 of {key: value} pairs."""
31
31
3232 namespaces = _cpconfig.NamespaceSet()
3333 toolboxes = {'tools': cherrypy.tools}
34
34
3535 log = None
3636 """A LogManager instance. See _cplogging."""
37
37
3838 wsgiapp = None
3939 """A CPWSGIApp instance. See _cpwsgi."""
40
40
4141 request_class = _cprequest.Request
4242 response_class = _cprequest.Response
43
43
4444 relative_urls = False
45
45
4646 def __init__(self, root, script_name="", config=None):
4747 self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
4848 self.root = root
4949 self.script_name = script_name
5050 self.wsgiapp = _cpwsgi.CPWSGIApp(self)
51
51
5252 self.namespaces = self.namespaces.copy()
5353 self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
5454 self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
55
55
5656 self.config = self.__class__.config.copy()
5757 if config:
5858 self.merge(config)
59
59
6060 def __repr__(self):
6161 return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
6262 self.root, self.script_name)
63
64 script_name_doc = """The URI "mount point" for this app. A mount point is that portion of
65 the URI which is constant for all URIs that are serviced by this
66 application; it does not include scheme, host, or proxy ("virtual host")
67 portions of the URI.
68
63
64 script_name_doc = """The URI "mount point" for this app. A mount point
65 is that portion of the URI which is constant for all URIs that are
66 serviced by this application; it does not include scheme, host, or proxy
67 ("virtual host") portions of the URI.
68
6969 For example, if script_name is "/my/cool/app", then the URL
7070 "http://www.example.com/my/cool/app/page1" might be handled by a
7171 "page1" method on the root object.
72
72
7373 The value of script_name MUST NOT end in a slash. If the script_name
7474 refers to the root of the URI, it MUST be an empty string (not "/").
75
75
7676 If script_name is explicitly set to None, then the script_name will be
7777 provided for each call from request.wsgi_environ['SCRIPT_NAME'].
7878 """
79
7980 def _get_script_name(self):
80 if self._script_name is None:
81 # None signals that the script name should be pulled from WSGI environ.
82 return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
83 return self._script_name
81 if self._script_name is not None:
82 return self._script_name
83
84 # A `_script_name` with a value of None signals that the script name
85 # should be pulled from WSGI environ.
86 return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
87
8488 def _set_script_name(self, value):
8589 if value:
8690 value = value.rstrip("/")
8791 self._script_name = value
8892 script_name = property(fget=_get_script_name, fset=_set_script_name,
8993 doc=script_name_doc)
90
94
9195 def merge(self, config):
9296 """Merge the given config into self.config."""
9397 _cpconfig.merge(self.config, config)
94
98
9599 # Handle namespaces specified in config.
96100 self.namespaces(self.config.get("/", {}))
97
101
98102 def find_config(self, path, key, default=None):
99103 """Return the most-specific value for key along path, or default."""
100104 trail = path or "/"
101105 while trail:
102106 nodeconf = self.config.get(trail, {})
103
107
104108 if key in nodeconf:
105109 return nodeconf[key]
106
110
107111 lastslash = trail.rfind("/")
108112 if lastslash == -1:
109113 break
111115 trail = "/"
112116 else:
113117 trail = trail[:lastslash]
114
118
115119 return default
116
120
117121 def get_serving(self, local, remote, scheme, sproto):
118122 """Create and return a Request and Response object."""
119123 req = self.request_class(local, remote, scheme, sproto)
120124 req.app = self
121
125
122126 for name, toolbox in self.toolboxes.items():
123127 req.namespaces[name] = toolbox
124
128
125129 resp = self.response_class()
126130 cherrypy.serving.load(req, resp)
127131 cherrypy.engine.publish('acquire_thread')
128132 cherrypy.engine.publish('before_request')
129
133
130134 return req, resp
131
135
132136 def release_serving(self):
133137 """Release the current serving (request and response)."""
134138 req = cherrypy.serving.request
135
139
136140 cherrypy.engine.publish('after_request')
137
141
138142 try:
139143 req.close()
140144 except:
141145 cherrypy.log(traceback=True, severity=40)
142
146
143147 cherrypy.serving.clear()
144
148
145149 def __call__(self, environ, start_response):
146150 return self.wsgiapp(environ, start_response)
147151
148152
149153 class Tree(object):
154
150155 """A registry of CherryPy applications, mounted at diverse points.
151
156
152157 An instance of this class may also be used as a WSGI callable
153158 (WSGI application object), in which case it dispatches to all
154159 mounted apps.
155160 """
156
161
157162 apps = {}
158163 """
159164 A dict of the form {script name: application}, where "script name"
160165 is a string declaring the URI mount point (no trailing slash), and
161166 "application" is an instance of cherrypy.Application (or an arbitrary
162167 WSGI callable if you happen to be using a WSGI server)."""
163
168
164169 def __init__(self):
165170 self.apps = {}
166
171
167172 def mount(self, root, script_name="", config=None):
168173 """Mount a new app from a root object, script_name, and config.
169
174
170175 root
171176 An instance of a "controller class" (a collection of page
172177 handler methods) which represents the root of the application.
173178 This may also be an Application instance, or None if using
174179 a dispatcher other than the default.
175
180
176181 script_name
177182 A string containing the "mount point" of the application.
178183 This should start with a slash, and be the path portion of the
179184 URL at which to mount the given root. For example, if root.index()
180185 will handle requests to "http://www.example.com:8080/dept/app1/",
181186 then the script_name argument would be "/dept/app1".
182
187
183188 It MUST NOT end in a slash. If the script_name refers to the
184189 root of the URI, it MUST be an empty string (not "/").
185
190
186191 config
187192 A file or dict containing application config.
188193 """
193198 "order to inpect the WSGI environ for SCRIPT_NAME upon each "
194199 "request). You cannot mount such Applications on this Tree; "
195200 "you must pass them to a WSGI server interface directly.")
196
201
197202 # Next line both 1) strips trailing slash and 2) maps "/" -> "".
198203 script_name = script_name.rstrip("/")
199
204
200205 if isinstance(root, Application):
201206 app = root
202207 if script_name != "" and script_name != app.script_name:
203 raise ValueError("Cannot specify a different script name and "
204 "pass an Application instance to cherrypy.mount")
208 raise ValueError(
209 "Cannot specify a different script name and pass an "
210 "Application instance to cherrypy.mount")
205211 script_name = app.script_name
206212 else:
207213 app = Application(root, script_name)
208
214
209215 # If mounted at "", add favicon.ico
210216 if (script_name == "" and root is not None
211217 and not hasattr(root, "favicon_ico")):
212218 favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
213219 "favicon.ico")
214220 root.favicon_ico = tools.staticfile.handler(favicon)
215
221
216222 if config:
217223 app.merge(config)
218
224
219225 self.apps[script_name] = app
220
226
221227 return app
222
228
223229 def graft(self, wsgi_callable, script_name=""):
224230 """Mount a wsgi callable at the given script_name."""
225231 # Next line both 1) strips trailing slash and 2) maps "/" -> "".
226232 script_name = script_name.rstrip("/")
227233 self.apps[script_name] = wsgi_callable
228
234
229235 def script_name(self, path=None):
230236 """The script_name of the app at the given path, or None.
231
237
232238 If path is None, cherrypy.request is used.
233239 """
234240 if path is None:
238244 request.path_info)
239245 except AttributeError:
240246 return None
241
247
242248 while True:
243249 if path in self.apps:
244250 return path
245
251
246252 if path == "":
247253 return None
248
254
249255 # Move one node up the tree and try again.
250256 path = path[:path.rfind("/")]
251
257
252258 def __call__(self, environ, start_response):
253259 # If you're calling this, then you're probably setting SCRIPT_NAME
254260 # to '' (some WSGI servers always set SCRIPT_NAME to '').
262268 if sn is None:
263269 start_response('404 Not Found', [])
264270 return []
265
271
266272 app = self.apps[sn]
267
273
268274 # Correct the SCRIPT_NAME and PATH_INFO environ entries.
269275 environ = environ.copy()
270276 if not py3k:
272278 # Python 2/WSGI u.0: all strings MUST be of type unicode
273279 enc = environ[ntou('wsgi.url_encoding')]
274280 environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
275 environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc)
281 environ[ntou('PATH_INFO')] = path[
282 len(sn.rstrip("/")):].decode(enc)
276283 else:
277284 # Python 2/WSGI 1.x: all strings MUST be of type str
278285 environ['SCRIPT_NAME'] = sn
284291 environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
285292 else:
286293 # Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str
287 environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1')
288 environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
294 environ['SCRIPT_NAME'] = sn.encode(
295 'utf-8').decode('ISO-8859-1')
296 environ['PATH_INFO'] = path[
297 len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
289298 return app(environ, start_response)
1515
1616
1717 def downgrade_wsgi_ux_to_1x(environ):
18 """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
18 """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
19 """
1920 env1x = {}
20
21
2122 url_encoding = environ[ntou('wsgi.url_encoding')]
2223 for k, v in list(environ.items()):
2324 if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
2526 elif isinstance(v, unicodestr):
2627 v = v.encode('ISO-8859-1')
2728 env1x[k.encode('ISO-8859-1')] = v
28
29
2930 return env1x
3031
3132
3233 class VirtualHost(object):
34
3335 """Select a different WSGI application based on the Host header.
34
36
3537 This can be useful when running multiple sites within one CP server.
3638 It allows several domains to point to different applications. For example::
37
39
3840 root = Root()
3941 RootApp = cherrypy.Application(root)
4042 Domain2App = cherrypy.Application(root)
4143 SecureApp = cherrypy.Application(Secure())
42
44
4345 vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
4446 domains={'www.domain2.example': Domain2App,
4547 'www.domain2.example:443': SecureApp,
4648 })
47
49
4850 cherrypy.tree.graft(vhost)
4951 """
5052 default = None
5153 """Required. The default WSGI application."""
52
54
5355 use_x_forwarded_host = True
5456 """If True (the default), any "X-Forwarded-Host"
5557 request header will be used instead of the "Host" header. This
5658 is commonly added by HTTP servers (such as Apache) when proxying."""
57
59
5860 domains = {}
5961 """A dict of {host header value: application} pairs.
6062 The incoming "Host" request header is looked up in this dict,
6365 separate entries for "example.com" and "www.example.com".
6466 In addition, "Host" headers may contain the port number.
6567 """
66
68
6769 def __init__(self, default, domains=None, use_x_forwarded_host=True):
6870 self.default = default
6971 self.domains = domains or {}
7072 self.use_x_forwarded_host = use_x_forwarded_host
71
73
7274 def __call__(self, environ, start_response):
7375 domain = environ.get('HTTP_HOST', '')
7476 if self.use_x_forwarded_host:
7577 domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
76
78
7779 nextapp = self.domains.get(domain)
7880 if nextapp is None:
7981 nextapp = self.default
8183
8284
8385 class InternalRedirector(object):
86
8487 """WSGI middleware that handles raised cherrypy.InternalRedirect."""
85
88
8689 def __init__(self, nextapp, recursive=False):
8790 self.nextapp = nextapp
8891 self.recursive = recursive
89
92
9093 def __call__(self, environ, start_response):
9194 redirections = []
9295 while True:
98101 sn = environ.get('SCRIPT_NAME', '')
99102 path = environ.get('PATH_INFO', '')
100103 qs = environ.get('QUERY_STRING', '')
101
104
102105 # Add the *previous* path_info + qs to redirections.
103106 old_uri = sn + path
104107 if qs:
105108 old_uri += "?" + qs
106109 redirections.append(old_uri)
107
110
108111 if not self.recursive:
109 # Check to see if the new URI has been redirected to already
112 # Check to see if the new URI has been redirected to
113 # already
110114 new_uri = sn + ir.path
111115 if ir.query_string:
112116 new_uri += "?" + ir.query_string
114118 ir.request.close()
115119 raise RuntimeError("InternalRedirector visited the "
116120 "same URL twice: %r" % new_uri)
117
121
118122 # Munge the environment and try again.
119123 environ['REQUEST_METHOD'] = "GET"
120124 environ['PATH_INFO'] = ir.path
125129
126130
127131 class ExceptionTrapper(object):
132
128133 """WSGI middleware that traps exceptions."""
129
134
130135 def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
131136 self.nextapp = nextapp
132137 self.throws = throws
133
138
134139 def __call__(self, environ, start_response):
135 return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
140 return _TrappedResponse(
141 self.nextapp,
142 environ,
143 start_response,
144 self.throws
145 )
136146
137147
138148 class _TrappedResponse(object):
139
149
140150 response = iter([])
141
151
142152 def __init__(self, nextapp, environ, start_response, throws):
143153 self.nextapp = nextapp
144154 self.environ = environ
145155 self.start_response = start_response
146156 self.throws = throws
147157 self.started_response = False
148 self.response = self.trap(self.nextapp, self.environ, self.start_response)
158 self.response = self.trap(
159 self.nextapp, self.environ, self.start_response)
149160 self.iter_response = iter(self.response)
150
161
151162 def __iter__(self):
152163 self.started_response = True
153164 return self
154
165
155166 if py3k:
156167 def __next__(self):
157168 return self.trap(next, self.iter_response)
158169 else:
159170 def next(self):
160171 return self.trap(self.iter_response.next)
161
172
162173 def close(self):
163174 if hasattr(self.response, 'close'):
164175 self.response.close()
165
176
166177 def trap(self, func, *args, **kwargs):
167178 try:
168179 return func(*args, **kwargs)
187198 self.iter_response = iter([])
188199 else:
189200 self.iter_response = iter(b)
190
201
191202 try:
192203 self.start_response(s, h, _sys.exc_info())
193204 except:
198209 # But we still log and call close() to clean up ourselves.
199210 _cherrypy.log(traceback=True, severity=40)
200211 raise
201
212
202213 if self.started_response:
203214 return ntob("").join(b)
204215 else:
209220
210221
211222 class AppResponse(object):
223
212224 """WSGI response iterable for CherryPy applications."""
213
225
214226 def __init__(self, environ, start_response, cpapp):
215227 self.cpapp = cpapp
216228 try:
225237 outstatus = r.output_status
226238 if not isinstance(outstatus, bytestr):
227239 raise TypeError("response.output_status is not a byte string.")
228
240
229241 outheaders = []
230242 for k, v in r.header_list:
231243 if not isinstance(k, bytestr):
232 raise TypeError("response.header_list key %r is not a byte string." % k)
244 raise TypeError(
245 "response.header_list key %r is not a byte string." %
246 k)
233247 if not isinstance(v, bytestr):
234 raise TypeError("response.header_list value %r is not a byte string." % v)
248 raise TypeError(
249 "response.header_list value %r is not a byte string." %
250 v)
235251 outheaders.append((k, v))
236
252
237253 if py3k:
238 # According to PEP 3333, when using Python 3, the response status
239 # and headers must be bytes masquerading as unicode; that is, they
240 # must be of type "str" but are restricted to code points in the
241 # "latin-1" set.
254 # According to PEP 3333, when using Python 3, the response
255 # status and headers must be bytes masquerading as unicode;
256 # that is, they must be of type "str" but are restricted to
257 # code points in the "latin-1" set.
242258 outstatus = outstatus.decode('ISO-8859-1')
243259 outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
244260 for k, v in outheaders]
248264 except:
249265 self.close()
250266 raise
251
267
252268 def __iter__(self):
253269 return self
254
270
255271 if py3k:
256272 def __next__(self):
257273 return next(self.iter_response)
258274 else:
259275 def next(self):
260276 return self.iter_response.next()
261
277
262278 def close(self):
263279 """Close and de-reference the current request and response. (Core)"""
264280 self.cpapp.release_serving()
265
281
266282 def run(self):
267283 """Create a Request object using environ."""
268284 env = self.environ.get
269
285
270286 local = httputil.Host('', int(env('SERVER_PORT', 80)),
271 env('SERVER_NAME', ''))
287 env('SERVER_NAME', ''))
272288 remote = httputil.Host(env('REMOTE_ADDR', ''),
273289 int(env('REMOTE_PORT', -1) or -1),
274290 env('REMOTE_HOST', ''))
275291 scheme = env('wsgi.url_scheme')
276292 sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
277293 request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
278
294
279295 # LOGON_USER is served by IIS, and is the name of the
280296 # user after having been mapped to a local account.
281297 # Both IIS and Apache set REMOTE_USER, when possible.
284300 request.multiprocess = self.environ['wsgi.multiprocess']
285301 request.wsgi_environ = self.environ
286302 request.prev = env('cherrypy.previous_request', None)
287
303
288304 meth = self.environ['REQUEST_METHOD']
289
305
290306 path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
291307 self.environ.get('PATH_INFO', ''))
292308 qs = self.environ.get('QUERY_STRING', '')
293309
294310 if py3k:
295 # This isn't perfect; if the given PATH_INFO is in the wrong encoding,
296 # it may fail to match the appropriate config section URI. But meh.
311 # This isn't perfect; if the given PATH_INFO is in the
312 # wrong encoding, it may fail to match the appropriate config
313 # section URI. But meh.
297314 old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
298315 new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''),
299316 "request.uri_encoding", 'utf-8')
300317 if new_enc.lower() != old_enc.lower():
301 # Even though the path and qs are unicode, the WSGI server is
302 # required by PEP 3333 to coerce them to ISO-8859-1 masquerading
303 # as unicode. So we have to encode back to bytes and then decode
304 # again using the "correct" encoding.
318 # Even though the path and qs are unicode, the WSGI server
319 # is required by PEP 3333 to coerce them to ISO-8859-1
320 # masquerading as unicode. So we have to encode back to
321 # bytes and then decode again using the "correct" encoding.
305322 try:
306323 u_path = path.encode(old_enc).decode(new_enc)
307324 u_qs = qs.encode(old_enc).decode(new_enc)
312329 # Only set transcoded values if they both succeed.
313330 path = u_path
314331 qs = u_qs
315
332
316333 rproto = self.environ.get('SERVER_PROTOCOL')
317334 headers = self.translate_headers(self.environ)
318335 rfile = self.environ['wsgi.input']
319336 request.run(meth, path, qs, rproto, headers, rfile)
320
337
321338 headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
322339 'CONTENT_LENGTH': 'Content-Length',
323340 'CONTENT_TYPE': 'Content-Type',
324341 'REMOTE_HOST': 'Remote-Host',
325342 'REMOTE_ADDR': 'Remote-Addr',
326343 }
327
344
328345 def translate_headers(self, environ):
329346 """Translate CGI-environ header names to HTTP header names."""
330347 for cgiName in environ:
338355
339356
340357 class CPWSGIApp(object):
358
341359 """A WSGI application object for a CherryPy Application."""
342
360
343361 pipeline = [('ExceptionTrapper', ExceptionTrapper),
344362 ('InternalRedirector', InternalRedirector),
345363 ]
348366 plus optional keyword arguments, and returns a WSGI application
349367 (that takes environ and start_response arguments). The 'name' can
350368 be any you choose, and will correspond to keys in self.config."""
351
369
352370 head = None
353371 """Rather than nest all apps in the pipeline on each call, it's only
354372 done the first time, and the result is memoized into self.head. Set
355373 this to None again if you change self.pipeline after calling self."""
356
374
357375 config = {}
358376 """A dict whose keys match names listed in the pipeline. Each
359377 value is a further dict which will be passed to the corresponding
360378 named WSGI callable (from the pipeline) as keyword arguments."""
361
379
362380 response_class = AppResponse
363 """The class to instantiate and return as the next app in the WSGI chain."""
364
381 """The class to instantiate and return as the next app in the WSGI chain.
382 """
383
365384 def __init__(self, cpapp, pipeline=None):
366385 self.cpapp = cpapp
367386 self.pipeline = self.pipeline[:]
368387 if pipeline:
369388 self.pipeline.extend(pipeline)
370389 self.config = self.config.copy()
371
390
372391 def tail(self, environ, start_response):
373392 """WSGI application callable for the actual CherryPy application.
374
393
375394 You probably shouldn't call this; call self.__call__ instead,
376395 so that any WSGI middleware in self.pipeline can run first.
377396 """
378397 return self.response_class(environ, start_response, self.cpapp)
379
398
380399 def __call__(self, environ, start_response):
381400 head = self.head
382401 if head is None:
388407 head = callable(head, **conf)
389408 self.head = head
390409 return head(environ, start_response)
391
410
392411 def namespace_handler(self, k, v):
393412 """Config handler for the 'wsgi' namespace."""
394413 if k == "pipeline":
404423 name, arg = k.split(".", 1)
405424 bucket = self.config.setdefault(name, {})
406425 bucket[arg] = v
407
77
88
99 class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
10
1011 """Wrapper for wsgiserver.CherryPyWSGIServer.
11
12
1213 wsgiserver has been designed to not reference CherryPy in any way,
1314 so that it can be used in other frameworks and applications. Therefore,
1415 we wrap it here, so we can set our own mount points from cherrypy.tree
1516 and apply some attributes from config -> cherrypy.server -> wsgiserver.
1617 """
17
18
1819 def __init__(self, server_adapter=cherrypy.server):
1920 self.server_adapter = server_adapter
20 self.max_request_header_size = self.server_adapter.max_request_header_size or 0
21 self.max_request_body_size = self.server_adapter.max_request_body_size or 0
22
21 self.max_request_header_size = (
22 self.server_adapter.max_request_header_size or 0
23 )
24 self.max_request_body_size = (
25 self.server_adapter.max_request_body_size or 0
26 )
27
2328 server_name = (self.server_adapter.socket_host or
2429 self.server_adapter.socket_file or
2530 None)
26
31
2732 self.wsgi_version = self.server_adapter.wsgi_version
2833 s = wsgiserver.CherryPyWSGIServer
2934 s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
3035 self.server_adapter.thread_pool,
3136 server_name,
32 max = self.server_adapter.thread_pool_max,
33 request_queue_size = self.server_adapter.socket_queue_size,
34 timeout = self.server_adapter.socket_timeout,
35 shutdown_timeout = self.server_adapter.shutdown_timeout,
37 max=self.server_adapter.thread_pool_max,
38 request_queue_size=self.server_adapter.socket_queue_size,
39 timeout=self.server_adapter.socket_timeout,
40 shutdown_timeout=self.server_adapter.shutdown_timeout,
3641 )
3742 self.protocol = self.server_adapter.protocol_version
3843 self.nodelay = self.server_adapter.nodelay
5459 self.server_adapter.ssl_certificate,
5560 self.server_adapter.ssl_private_key,
5661 self.server_adapter.ssl_certificate_chain)
57
58 self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False)
62
63 self.stats['Enabled'] = getattr(
64 self.server_adapter, 'statistics', False)
5965
6066 def error_log(self, msg="", level=20, traceback=False):
6167 cherrypy.engine.log(msg, level, traceback)
62
66 from cherrypy.process import plugins, servers
77 from cherrypy import Application
88
9
910 def start(configfiles=None, daemonize=False, environment=None,
1011 fastcgi=False, scgi=False, pidfile=None, imports=None,
1112 cgi=False):
1314 sys.path = [''] + sys.path
1415 for i in imports or []:
1516 exec("import %s" % i)
16
17
1718 for c in configfiles or []:
1819 cherrypy.config.update(c)
1920 # If there's only one app mounted, merge config into it.
2122 for app in cherrypy.tree.apps.values():
2223 if isinstance(app, Application):
2324 app.merge(c)
24
25
2526 engine = cherrypy.engine
26
27
2728 if environment is not None:
2829 cherrypy.config.update({'environment': environment})
29
30
3031 # Only daemonize if asked to.
3132 if daemonize:
3233 # Don't print anything to stdout/sterr.
3334 cherrypy.config.update({'log.screen': False})
3435 plugins.Daemonizer(engine).subscribe()
35
36
3637 if pidfile:
3738 plugins.PIDFile(engine, pidfile).subscribe()
38
39
3940 if hasattr(engine, "signal_handler"):
4041 engine.signal_handler.subscribe()
4142 if hasattr(engine, "console_control_handler"):
4243 engine.console_control_handler.subscribe()
43
44
4445 if (fastcgi and (scgi or cgi)) or (scgi and cgi):
4546 cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
4647 "scgi options.", 'ENGINE')
5051 cherrypy.config.update({'engine.autoreload_on': False})
5152 # Turn off the default HTTP server (which is subscribed by default).
5253 cherrypy.server.unsubscribe()
53
54
5455 addr = cherrypy.server.bind_addr
5556 if fastcgi:
5657 f = servers.FlupFCGIServer(application=cherrypy.tree,
6364 bindAddress=addr)
6465 s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
6566 s.subscribe()
66
67
6768 # Always start the engine; this will start all other services
6869 try:
6970 engine.start()
7677
7778 if __name__ == '__main__':
7879 from optparse import OptionParser
79
80
8081 p = OptionParser()
8182 p.add_option('-c', '--config', action="append", dest='config',
8283 help="specify config file(s)")
8586 p.add_option('-e', '--environment', dest='environment', default=None,
8687 help="apply the given config environment")
8788 p.add_option('-f', action="store_true", dest='fastcgi',
88 help="start a fastcgi server instead of the default HTTP server")
89 help="start a fastcgi server instead of the default HTTP "
90 "server")
8991 p.add_option('-s', action="store_true", dest='scgi',
9092 help="start a scgi server instead of the default HTTP server")
9193 p.add_option('-x', action="store_true", dest='cgi',
9799 p.add_option('-P', '--Path', action="append", dest='Path',
98100 help="add the given paths to sys.path")
99101 options, args = p.parse_args()
100
102
101103 if options.Path:
102104 for p in options.Path:
103105 sys.path.insert(0, p)
104
106
105107 start(options.config, options.daemonize,
106108 options.environment, options.fastcgi, options.scgi,
107109 options.pidfile, options.imports, options.cgi)
108
22 # Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
33 from cherrypy.lib.reprconf import unrepr, modules, attributes
44
5 def is_iterator(obj):
6 '''Returns a boolean indicating if the object provided implements
7 the iterator protocol (i.e. like a generator). This will return
8 false for objects which iterable, but not iterators themselves.'''
9 from types import GeneratorType
10 if isinstance(obj, GeneratorType):
11 return True
12 elif not hasattr(obj, '__iter__'):
13 return False
14 else:
15 # Types which implement the protocol must return themselves when
16 # invoking 'iter' upon them.
17 return iter(obj) is obj
18
519 class file_generator(object):
20
621 """Yield the given input (a file object) in chunks (default 64k). (Core)"""
7
22
823 def __init__(self, input, chunkSize=65536):
924 self.input = input
1025 self.chunkSize = chunkSize
11
26
1227 def __iter__(self):
1328 return self
14
29
1530 def __next__(self):
1631 chunk = self.input.read(self.chunkSize)
1732 if chunk:
2136 self.input.close()
2237 raise StopIteration()
2338 next = __next__
39
2440
2541 def file_generator_limited(fileobj, count, chunk_size=65536):
2642 """Yield the given file object in chunks, stopping after `count`
3551 remaining -= chunklen
3652 yield chunk
3753
54
3855 def set_vary_header(response, header_name):
3956 "Add a Vary header to a response"
4057 varies = response.headers.get("Vary", "")
22
33
44 def check_auth(users, encrypt=None, realm=None):
5 """If an authorization header contains credentials, return True, else False."""
5 """If an authorization header contains credentials, return True or False.
6 """
67 request = cherrypy.serving.request
78 if 'authorization' in request.headers:
89 # make sure the provided credentials are correctly set
910 ah = httpauth.parseAuthorization(request.headers['authorization'])
1011 if ah is None:
1112 raise cherrypy.HTTPError(400, 'Bad Request')
12
13
1314 if not encrypt:
1415 encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
15
16
1617 if hasattr(users, '__call__'):
1718 try:
1819 # backward compatibility
19 users = users() # expect it to return a dictionary
20
20 users = users() # expect it to return a dictionary
21
2122 if not isinstance(users, dict):
22 raise ValueError("Authentication users must be a dictionary")
23
23 raise ValueError(
24 "Authentication users must be a dictionary")
25
2426 # fetch the user password
2527 password = users.get(ah["username"], None)
2628 except TypeError:
2931 else:
3032 if not isinstance(users, dict):
3133 raise ValueError("Authentication users must be a dictionary")
32
34
3335 # fetch the user password
3436 password = users.get(ah["username"], None)
35
37
3638 # validate the authorization by re-computing it here
3739 # and compare it with what the user-agent provided
3840 if httpauth.checkResponse(ah, password, method=request.method,
3941 encrypt=encrypt, realm=realm):
4042 request.login = ah["username"]
4143 return True
42
44
4345 request.login = False
4446 return False
4547
48
4649 def basic_auth(realm, users, encrypt=None, debug=False):
4750 """If auth fails, raise 401 with a basic authentication header.
48
51
4952 realm
5053 A string containing the authentication realm.
51
54
5255 users
53 A dict of the form: {username: password} or a callable returning a dict.
54
56 A dict of the form: {username: password} or a callable returning
57 a dict.
58
5559 encrypt
5660 callable used to encrypt the password returned from the user-agent.
5761 if None it defaults to a md5 encryption.
58
62
5963 """
6064 if check_auth(users, encrypt):
6165 if debug:
6266 cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
6367 return
64
68
6569 # inform the user-agent this path is protected
66 cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
67
68 raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
70 cherrypy.serving.response.headers[
71 'www-authenticate'] = httpauth.basicAuth(realm)
72
73 raise cherrypy.HTTPError(
74 401, "You are not authorized to access that resource")
75
6976
7077 def digest_auth(realm, users, debug=False):
7178 """If auth fails, raise 401 with a digest authentication header.
72
79
7380 realm
7481 A string containing the authentication realm.
7582 users
76 A dict of the form: {username: password} or a callable returning a dict.
83 A dict of the form: {username: password} or a callable returning
84 a dict.
7785 """
7886 if check_auth(users, realm=realm):
7987 if debug:
8088 cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
8189 return
82
90
8391 # inform the user-agent this path is protected
84 cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
85
86 raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
92 cherrypy.serving.response.headers[
93 'www-authenticate'] = httpauth.digestAuth(realm)
94
95 raise cherrypy.HTTPError(
96 401, "You are not authorized to access that resource")
22 # vim:ts=4:sw=4:expandtab:fileencoding=utf-8
33
44 __doc__ = """This module provides a CherryPy 3.x tool which implements
5 the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`.
5 the server-side of HTTP Basic Access Authentication, as described in
6 :rfc:`2617`.
67
78 Example usage, using the built-in checkpassword_dict function which uses a dict
89 as the credentials store::
5960 username and password are the values obtained from the request's
6061 'authorization' header. If authentication succeeds, checkpassword
6162 returns True, else it returns False.
62
63
6364 """
64
65
6566 if '"' in realm:
6667 raise ValueError('Realm cannot contain the " (quote) character.')
6768 request = cherrypy.serving.request
68
69
6970 auth_header = request.headers.get('authorization')
7071 if auth_header is not None:
7172 try:
7677 if debug:
7778 cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
7879 request.login = username
79 return # successful authentication
80 except (ValueError, binascii.Error): # split() error, base64.decodestring() error
80 return # successful authentication
81 # split() error, base64.decodestring() error
82 except (ValueError, binascii.Error):
8183 raise cherrypy.HTTPError(400, 'Bad Request')
82
84
8385 # Respond with 401 status and a WWW-Authenticate header
84 cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
85 raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
86
86 cherrypy.serving.response.headers[
87 'www-authenticate'] = 'Basic realm="%s"' % realm
88 raise cherrypy.HTTPError(
89 401, "You are not authorized to access that resource")
4040
4141 # Three helper functions for users of the tool, providing three variants
4242 # of get_ha1() functions for three different kinds of credential stores.
43
44
4345 def get_ha1_dict_plain(user_password_dict):
4446 """Returns a get_ha1 function which obtains a plaintext password from a
4547 dictionary of the form: {username : password}.
5658
5759 return get_ha1
5860
61
5962 def get_ha1_dict(user_ha1_dict):
6063 """Returns a get_ha1 function which obtains a HA1 password hash from a
6164 dictionary of the form: {username : HA1}.
6669 argument to digest_auth().
6770 """
6871 def get_ha1(realm, username):
69 return user_ha1_dict.get(user)
72 return user_ha1_dict.get(username)
7073
7174 return get_ha1
75
7276
7377 def get_ha1_file_htdigest(filename):
7478 """Returns a get_ha1 function which obtains a HA1 password hash from a
98102
99103
100104 def synthesize_nonce(s, key, timestamp=None):
101 """Synthesize a nonce value which resists spoofing and can be checked for staleness.
102 Returns a string suitable as the value for 'nonce' in the www-authenticate header.
105 """Synthesize a nonce value which resists spoofing and can be checked
106 for staleness. Returns a string suitable as the value for 'nonce' in
107 the www-authenticate header.
103108
104109 s
105110 A string related to the resource, such as the hostname of the server.
106111
107112 key
108113 A secret string known only to the server.
109
114
110115 timestamp
111116 An integer seconds-since-the-epoch timestamp
112
117
113118 """
114119 if timestamp is None:
115120 timestamp = int(time.time())
124129
125130
126131 class HttpDigestAuthorization (object):
132
127133 """Class to parse a Digest Authorization header and perform re-calculation
128134 of the digest.
129135 """
134140 def __init__(self, auth_header, http_method, debug=False):
135141 self.http_method = http_method
136142 self.debug = debug
137 scheme, params = auth_header.split(" ", 1)
143 scheme, params = auth_header.split(" ", 1)
138144 self.scheme = scheme.lower()
139145 if self.scheme != 'digest':
140146 raise ValueError('Authorization scheme is not "Digest"')
150156 self.nonce = paramsd.get('nonce')
151157 self.uri = paramsd.get('uri')
152158 self.method = paramsd.get('method')
153 self.response = paramsd.get('response') # the response digest
154 self.algorithm = paramsd.get('algorithm', 'MD5')
159 self.response = paramsd.get('response') # the response digest
160 self.algorithm = paramsd.get('algorithm', 'MD5').upper()
155161 self.cnonce = paramsd.get('cnonce')
156162 self.opaque = paramsd.get('opaque')
157 self.qop = paramsd.get('qop') # qop
158 self.nc = paramsd.get('nc') # nonce count
163 self.qop = paramsd.get('qop') # qop
164 self.nc = paramsd.get('nc') # nonce count
159165
160166 # perform some correctness checks
161167 if self.algorithm not in valid_algorithms:
162 raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm))
163
164 has_reqd = self.username and \
165 self.realm and \
166 self.nonce and \
167 self.uri and \
168 self.response
168 raise ValueError(
169 self.errmsg("Unsupported value for algorithm: '%s'" %
170 self.algorithm))
171
172 has_reqd = (
173 self.username and
174 self.realm and
175 self.nonce and
176 self.uri and
177 self.response
178 )
169179 if not has_reqd:
170 raise ValueError(self.errmsg("Not all required parameters are present."))
180 raise ValueError(
181 self.errmsg("Not all required parameters are present."))
171182
172183 if self.qop:
173184 if self.qop not in valid_qops:
174 raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop))
185 raise ValueError(
186 self.errmsg("Unsupported value for qop: '%s'" % self.qop))
175187 if not (self.cnonce and self.nc):
176 raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present"))
188 raise ValueError(
189 self.errmsg("If qop is sent then "
190 "cnonce and nc MUST be present"))
177191 else:
178192 if self.cnonce or self.nc:
179 raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present"))
180
193 raise ValueError(
194 self.errmsg("If qop is not sent, "
195 "neither cnonce nor nc can be present"))
181196
182197 def __str__(self):
183198 return 'authorization : %s' % self.auth_header
184199
185200 def validate_nonce(self, s, key):
186201 """Validate the nonce.
187 Returns True if nonce was generated by synthesize_nonce() and the timestamp
188 is not spoofed, else returns False.
202 Returns True if nonce was generated by synthesize_nonce() and the
203 timestamp is not spoofed, else returns False.
189204
190205 s
191 A string related to the resource, such as the hostname of the server.
192
206 A string related to the resource, such as the hostname of
207 the server.
208
193209 key
194210 A secret string known only to the server.
195
196 Both s and key must be the same values which were used to synthesize the nonce
197 we are trying to validate.
211
212 Both s and key must be the same values which were used to synthesize
213 the nonce we are trying to validate.
198214 """
199215 try:
200216 timestamp, hashpart = self.nonce.split(':', 1)
201 s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1)
217 s_timestamp, s_hashpart = synthesize_nonce(
218 s, key, timestamp).split(':', 1)
202219 is_valid = s_hashpart == hashpart
203220 if self.debug:
204221 TRACE('validate_nonce: %s' % is_valid)
205222 return is_valid
206 except ValueError: # split() error
223 except ValueError: # split() error
207224 pass
208225 return False
209226
210
211227 def is_nonce_stale(self, max_age_seconds=600):
212228 """Returns True if a validated nonce is stale. The nonce contains a
213 timestamp in plaintext and also a secure hash of the timestamp. You should
214 first validate the nonce to ensure the plaintext timestamp is not spoofed.
229 timestamp in plaintext and also a secure hash of the timestamp.
230 You should first validate the nonce to ensure the plaintext
231 timestamp is not spoofed.
215232 """
216233 try:
217234 timestamp, hashpart = self.nonce.split(':', 1)
218235 if int(timestamp) + max_age_seconds > int(time.time()):
219236 return False
220 except ValueError: # int() error
237 except ValueError: # int() error
221238 pass
222239 if self.debug:
223240 TRACE("nonce is stale")
224241 return True
225242
226
227243 def HA2(self, entity_body=''):
228244 """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
229245 # RFC 2617 3.2.2.3
230 # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
246 # If the "qop" directive's value is "auth" or is unspecified,
247 # then A2 is:
231248 # A2 = method ":" digest-uri-value
232249 #
233250 # If the "qop" value is "auth-int", then A2 is:
237254 elif self.qop == "auth-int":
238255 a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body))
239256 else:
240 # in theory, this should never happen, since I validate qop in __init__()
257 # in theory, this should never happen, since I validate qop in
258 # __init__()
241259 raise ValueError(self.errmsg("Unrecognized value for qop!"))
242260 return H(a2)
243
244261
245262 def request_digest(self, ha1, entity_body=''):
246263 """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
252269 If 'qop' is set to 'auth-int', then A2 includes a hash
253270 of the "entity body". The entity body is the part of the
254271 message which follows the HTTP headers. See :rfc:`2617` section
255 4.3. This refers to the entity the user agent sent in the request which
256 has the Authorization header. Typically GET requests don't have an entity,
257 and POST requests do.
258
272 4.3. This refers to the entity the user agent sent in the
273 request which has the Authorization header. Typically GET
274 requests don't have an entity, and POST requests do.
275
259276 """
260277 ha2 = self.HA2(entity_body)
261278 # Request-Digest -- RFC 2617 3.2.2.1
262279 if self.qop:
263 req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2)
280 req = "%s:%s:%s:%s:%s" % (
281 self.nonce, self.nc, self.cnonce, self.qop, ha2)
264282 else:
265283 req = "%s:%s" % (self.nonce, ha2)
266284
267285 # RFC 2617 3.2.2.2
268286 #
269 # If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is:
270 # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
287 # If the "algorithm" directive's value is "MD5" or is unspecified,
288 # then A1 is:
289 # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
271290 #
272291 # If the "algorithm" directive's value is "MD5-sess", then A1 is
273292 # calculated only once - on the first request by the client following
281300 return digest
282301
283302
284
285 def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False):
303 def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
304 stale=False):
286305 """Constructs a WWW-Authenticate header for Digest authentication."""
287306 if qop not in valid_qops:
288307 raise ValueError("Unsupported value for qop: '%s'" % qop)
292311 if nonce is None:
293312 nonce = synthesize_nonce(realm, key)
294313 s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
295 realm, nonce, algorithm, qop)
314 realm, nonce, algorithm, qop)
296315 if stale:
297316 s += ', stale="true"'
298317 return s
301320 def digest_auth(realm, get_ha1, key, debug=False):
302321 """A CherryPy tool which hooks at before_handler to perform
303322 HTTP Digest Access Authentication, as specified in :rfc:`2617`.
304
305 If the request has an 'authorization' header with a 'Digest' scheme, this
306 tool authenticates the credentials supplied in that header. If
307 the request has no 'authorization' header, or if it does but the scheme is
308 not "Digest", or if authentication fails, the tool sends a 401 response with
309 a 'WWW-Authenticate' Digest header.
310
323
324 If the request has an 'authorization' header with a 'Digest' scheme,
325 this tool authenticates the credentials supplied in that header.
326 If the request has no 'authorization' header, or if it does but the
327 scheme is not "Digest", or if authentication fails, the tool sends
328 a 401 response with a 'WWW-Authenticate' Digest header.
329
311330 realm
312331 A string containing the authentication realm.
313
332
314333 get_ha1
315334 A callable which looks up a username in a credentials store
316335 and returns the HA1 string, which is defined in the RFC to be
319338 where username is obtained from the request's 'authorization' header.
320339 If username is not found in the credentials store, get_ha1() returns
321340 None.
322
341
323342 key
324 A secret string known only to the server, used in the synthesis of nonces.
325
343 A secret string known only to the server, used in the synthesis
344 of nonces.
345
326346 """
327347 request = cherrypy.serving.request
328
348
329349 auth_header = request.headers.get('authorization')
330350 nonce_is_stale = False
331351 if auth_header is not None:
332352 try:
333 auth = HttpDigestAuthorization(auth_header, request.method, debug=debug)
353 auth = HttpDigestAuthorization(
354 auth_header, request.method, debug=debug)
334355 except ValueError:
335 raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
336
356 raise cherrypy.HTTPError(
357 400, "The Authorization header could not be parsed.")
358
337359 if debug:
338360 TRACE(str(auth))
339
361
340362 if auth.validate_nonce(realm, key):
341363 ha1 = get_ha1(realm, auth.username)
342364 if ha1 is not None:
343 # note that for request.body to be available we need to hook in at
344 # before_handler, not on_start_resource like 3.1.x digest_auth does.
365 # note that for request.body to be available we need to
366 # hook in at before_handler, not on_start_resource like
367 # 3.1.x digest_auth does.
345368 digest = auth.request_digest(ha1, entity_body=request.body)
346 if digest == auth.response: # authenticated
369 if digest == auth.response: # authenticated
347370 if debug:
348371 TRACE("digest matches auth.response")
349372 # Now check if nonce is stale.
350 # The choice of ten minutes' lifetime for nonce is somewhat arbitrary
373 # The choice of ten minutes' lifetime for nonce is somewhat
374 # arbitrary
351375 nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
352376 if not nonce_is_stale:
353377 request.login = auth.username
354378 if debug:
355 TRACE("authentication of %s successful" % auth.username)
379 TRACE("authentication of %s successful" %
380 auth.username)
356381 return
357
382
358383 # Respond with 401 status and a WWW-Authenticate header
359384 header = www_authenticate(realm, key, stale=nonce_is_stale)
360385 if debug:
361386 TRACE(header)
362387 cherrypy.serving.response.headers['WWW-Authenticate'] = header
363 raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
364
388 raise cherrypy.HTTPError(
389 401, "You are not authorized to access that resource")
00 """
1 CherryPy implements a simple caching system as a pluggable Tool. This tool tries
2 to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
3 it's probably good enough for most sites.
1 CherryPy implements a simple caching system as a pluggable Tool. This tool
2 tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
3 yet, but it's probably good enough for most sites.
44
55 In general, GET responses are cached (along with selecting headers) and, if
66 another request arrives for the same resource, the caching Tool will return 304
88 request.cached to True if serving a cached representation, and sets
99 request.cacheable to False (so it doesn't get cached again).
1010
11 If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
12 (delete) any cached response.
11 If POST, PUT, or DELETE requests are made for a cached resource, they
12 invalidate (delete) any cached response.
1313
1414 Usage
1515 =====
3838
3939 import cherrypy
4040 from cherrypy.lib import cptools, httputil
41 from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
41 from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted, Event
4242
4343
4444 class Cache(object):
45
4546 """Base class for Cache implementations."""
46
47
4748 def get(self):
4849 """Return the current variant if in the cache, else None."""
4950 raise NotImplemented
50
51
5152 def put(self, obj, size):
5253 """Store the current variant in the cache."""
5354 raise NotImplemented
54
55
5556 def delete(self):
5657 """Remove ALL cached variants of the current resource."""
5758 raise NotImplemented
58
59
5960 def clear(self):
6061 """Reset the cache to its initial, empty state."""
6162 raise NotImplemented
6263
6364
64
65 # ------------------------------- Memory Cache ------------------------------- #
66
67
65 # ------------------------------ Memory Cache ------------------------------- #
6866 class AntiStampedeCache(dict):
67
6968 """A storage system for cached items which reduces stampede collisions."""
70
69
7170 def wait(self, key, timeout=5, debug=False):
7271 """Return the cached value for the given key, or None.
73
72
7473 If timeout is not None, and the value is already
7574 being calculated by another thread, wait until the given timeout has
7675 elapsed. If the value is available before the timeout expires, it is
7776 returned. If not, None is returned, and a sentinel placed in the cache
7877 to signal other threads to wait.
79
78
8079 If timeout is None, no waiting is performed nor sentinels used.
8180 """
8281 value = self.get(key)
83 if isinstance(value, threading._Event):
82 if isinstance(value, Event):
8483 if timeout is None:
8584 # Ignore the other thread and recalc it ourselves.
8685 if debug:
8786 cherrypy.log('No timeout', 'TOOLS.CACHING')
8887 return None
89
88
9089 # Wait until it's done or times out.
9190 if debug:
92 cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
91 cherrypy.log('Waiting up to %s seconds' %
92 timeout, 'TOOLS.CACHING')
9393 value.wait(timeout)
9494 if value.result is not None:
9595 # The other thread finished its calculation. Use it.
103103 e = threading.Event()
104104 e.result = None
105105 dict.__setitem__(self, key, e)
106
106
107107 return None
108108 elif value is None:
109109 # Stick an Event in the slot so other threads wait
114114 e.result = None
115115 dict.__setitem__(self, key, e)
116116 return value
117
117
118118 def __setitem__(self, key, value):
119119 """Set the cached value for the given key."""
120120 existing = self.get(key)
121121 dict.__setitem__(self, key, value)
122 if isinstance(existing, threading._Event):
122 if isinstance(existing, Event):
123123 # Set Event.result so other threads waiting on it have
124124 # immediate access without needing to poll the cache again.
125125 existing.result = value
127127
128128
129129 class MemoryCache(Cache):
130
130131 """An in-memory cache for varying response content.
131
132
132133 Each key in self.store is a URI, and each value is an AntiStampedeCache.
133134 The response for any given URI may vary based on the values of
134135 "selecting request headers"; that is, those named in the Vary
135136 response header. We assume the list of header names to be constant
136137 for each URI throughout the lifetime of the application, and store
137138 that list in ``self.store[uri].selecting_headers``.
138
139
139140 The items contained in ``self.store[uri]`` have keys which are tuples of
140141 request header values (in the same order as the names in its
141142 selecting_headers), and values which are the actual responses.
142143 """
143
144
144145 maxobjects = 1000
145146 """The maximum number of cached objects; defaults to 1000."""
146
147
147148 maxobj_size = 100000
148149 """The maximum size of each cached object in bytes; defaults to 100 KB."""
149
150
150151 maxsize = 10000000
151152 """The maximum size of the entire cache in bytes; defaults to 10 MB."""
152
153
153154 delay = 600
154 """Seconds until the cached content expires; defaults to 600 (10 minutes)."""
155
155 """Seconds until the cached content expires; defaults to 600 (10 minutes).
156 """
157
156158 antistampede_timeout = 5
157159 """Seconds to wait for other threads to release a cache lock."""
158
160
159161 expire_freq = 0.1
160162 """Seconds to sleep between cache expiration sweeps."""
161
163
162164 debug = False
163
165
164166 def __init__(self):
165167 self.clear()
166
168
167169 # Run self.expire_cache in a separate daemon thread.
168170 t = threading.Thread(target=self.expire_cache, name='expire_cache')
169171 self.expiration_thread = t
170172 set_daemon(t, True)
171173 t.start()
172
174
173175 def clear(self):
174176 """Reset the cache to its initial, empty state."""
175177 self.store = {}
180182 self.tot_expires = 0
181183 self.tot_non_modified = 0
182184 self.cursize = 0
183
185
184186 def expire_cache(self):
185187 """Continuously examine cached objects, expiring stale ones.
186
188
187189 This function is designed to be run in its own daemon thread,
188190 referenced at ``self.expiration_thread``.
189191 """
206208 pass
207209 del self.expirations[expiration_time]
208210 time.sleep(self.expire_freq)
209
211
210212 def get(self):
211213 """Return the current variant if in the cache, else None."""
212214 request = cherrypy.serving.request
213215 self.tot_gets += 1
214
216
215217 uri = cherrypy.url(qs=request.query_string)
216218 uricache = self.store.get(uri)
217219 if uricache is None:
218220 return None
219
221
220222 header_values = [request.headers.get(h, '')
221223 for h in uricache.selecting_headers]
222224 variant = uricache.wait(key=tuple(sorted(header_values)),
225227 if variant is not None:
226228 self.tot_hist += 1
227229 return variant
228
230
229231 def put(self, variant, size):
230232 """Store the current variant in the cache."""
231233 request = cherrypy.serving.request
232234 response = cherrypy.serving.response
233
235
234236 uri = cherrypy.url(qs=request.query_string)
235237 uricache = self.store.get(uri)
236238 if uricache is None:
238240 uricache.selecting_headers = [
239241 e.value for e in response.headers.elements('Vary')]
240242 self.store[uri] = uricache
241
243
242244 if len(self.store) < self.maxobjects:
243245 total_size = self.cursize + size
244
246
245247 # checks if there's space for the object
246248 if (size < self.maxobj_size and total_size < self.maxsize):
247249 # add to the expirations list
248250 expiration_time = response.time + self.delay
249251 bucket = self.expirations.setdefault(expiration_time, [])
250252 bucket.append((size, uri, uricache.selecting_headers))
251
253
252254 # add to the cache
253255 header_values = [request.headers.get(h, '')
254256 for h in uricache.selecting_headers]
255257 uricache[tuple(sorted(header_values))] = variant
256258 self.tot_puts += 1
257259 self.cursize = total_size
258
260
259261 def delete(self):
260262 """Remove ALL cached variants of the current resource."""
261263 uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
264266
265267 def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
266268 """Try to obtain cached output. If fresh enough, raise HTTPError(304).
267
269
268270 If POST, PUT, or DELETE:
269271 * invalidates (deletes) any cached response for this resource
270272 * sets request.cached = False
271273 * sets request.cacheable = False
272
274
273275 else if a cached copy exists:
274276 * sets request.cached = True
275277 * sets request.cacheable = False
279281 if necessary.
280282 * sets response.status and response.body to the cached values
281283 * returns True
282
284
283285 otherwise:
284286 * sets request.cached = False
285287 * sets request.cacheable = True
287289 """
288290 request = cherrypy.serving.request
289291 response = cherrypy.serving.response
290
292
291293 if not hasattr(cherrypy, "_cache"):
292294 # Make a process-wide Cache object.
293295 cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
294
296
295297 # Take all remaining kwargs and set them on the Cache object.
296298 for k, v in kwargs.items():
297299 setattr(cherrypy._cache, k, v)
298300 cherrypy._cache.debug = debug
299
301
300302 # POST, PUT, DELETE should invalidate (delete) the cached copy.
301303 # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
302304 if request.method in invalid_methods:
307309 request.cached = False
308310 request.cacheable = False
309311 return False
310
312
311313 if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
312314 request.cached = False
313315 request.cacheable = True
314316 return False
315
317
316318 cache_data = cherrypy._cache.get()
317319 request.cached = bool(cache_data)
318320 request.cacheable = not request.cached
324326 directive = atoms.pop(0)
325327 if directive == 'max-age':
326328 if len(atoms) != 1 or not atoms[0].isdigit():
327 raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
329 raise cherrypy.HTTPError(
330 400, "Invalid Cache-Control header")
328331 max_age = int(atoms[0])
329332 break
330333 elif directive == 'no-cache':
331334 if debug:
332 cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
333 'TOOLS.CACHING')
335 cherrypy.log(
336 'Ignoring cache due to Cache-Control: no-cache',
337 'TOOLS.CACHING')
334338 request.cached = False
335339 request.cacheable = True
336340 return False
337
341
338342 if debug:
339343 cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
340344 s, h, b, create_time = cache_data
346350 request.cached = False
347351 request.cacheable = True
348352 return False
349
350 # Copy the response headers. See http://www.cherrypy.org/ticket/721.
353
354 # Copy the response headers. See
355 # https://bitbucket.org/cherrypy/cherrypy/issue/721.
351356 response.headers = rh = httputil.HeaderMap()
352357 for k in h:
353358 dict.__setitem__(rh, k, dict.__getitem__(h, k))
354
359
355360 # Add the required Age header
356361 response.headers["Age"] = str(age)
357
362
358363 try:
359364 # Note that validate_since depends on a Last-Modified header;
360365 # this was put into the cached copy, and should have been
365370 if x.status == 304:
366371 cherrypy._cache.tot_non_modified += 1
367372 raise
368
373
369374 # serve it & get out from the request
370375 response.status = s
371376 response.body = b
378383 def tee_output():
379384 """Tee response output to cache storage. Internal."""
380385 # Used by CachingTool by attaching to request.hooks
381
386
382387 request = cherrypy.serving.request
383388 if 'no-store' in request.headers.values('Cache-Control'):
384389 return
385
390
386391 def tee(body):
387392 """Tee response.body into a list."""
388393 if ('no-cache' in response.headers.values('Pragma') or
389 'no-store' in response.headers.values('Cache-Control')):
394 'no-store' in response.headers.values('Cache-Control')):
390395 for chunk in body:
391396 yield chunk
392397 return
393
398
394399 output = []
395400 for chunk in body:
396401 output.append(chunk)
397402 yield chunk
398
403
399404 # save the cache data
400405 body = ntob('').join(output)
401406 cherrypy._cache.put((response.status, response.headers or {},
402407 body, response.time), len(body))
403
408
404409 response = cherrypy.serving.response
405410 response.body = tee(response.body)
406411
414419 expire. The 'Expires' header will be set to response.time + secs.
415420 If secs is zero, the 'Expires' header is set one year in the past, and
416421 the following "cache prevention" headers are also set:
417
422
418423 * Pragma: no-cache
419424 * Cache-Control': no-cache, must-revalidate
420425
421426 force
422427 If False, the following headers are checked:
423
428
424429 * Etag
425430 * Last-Modified
426431 * Age
427432 * Expires
428
433
429434 If any are already present, none of the above response headers are set.
430
435
431436 """
432
437
433438 response = cherrypy.serving.response
434439 headers = response.headers
435
440
436441 cacheable = False
437442 if not force:
438443 # some header names that indicate that the response can be cached
440445 if indicator in headers:
441446 cacheable = True
442447 break
443
448
444449 if not cacheable and not force:
445450 if debug:
446451 cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
449454 cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
450455 if isinstance(secs, datetime.timedelta):
451456 secs = (86400 * secs.days) + secs.seconds
452
457
453458 if secs == 0:
454459 if force or ("Pragma" not in headers):
455460 headers["Pragma"] = "no-cache"
00 """Code-coverage tools for CherryPy.
11
22 To use this module, or the coverage tools in the test suite,
3 you need to download 'coverage.py', either Gareth Rees' `original
3 you need to download 'coverage.py', either Gareth Rees' `original
44 implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
55 or Ned Batchelder's `enhanced version:
66 <http://www.nedbatchelder.com/code/modules/coverage.html>`_
2323 import sys
2424 import cgi
2525 from cherrypy._cpcompat import quote_plus
26 import os, os.path
26 import os
27 import os.path
2728 localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
2829
2930 the_coverage = None
3031 try:
3132 from coverage import coverage
3233 the_coverage = coverage(data_file=localFile)
34
3335 def start():
3436 the_coverage.start()
3537 except ImportError:
3638 # Setting the_coverage to None will raise errors
3739 # that need to be trapped downstream.
3840 the_coverage = None
39
41
4042 import warnings
41 warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
42
43 warnings.warn(
44 "No code coverage will be performed; "
45 "coverage.py could not be imported.")
46
4347 def start():
4448 pass
4549 start.priority = 20
6872 font-size: small;
6973 font-weight: bold;
7074 font-style: italic;
71 margin-top: 5px;
75 margin-top: 5px;
7276 }
7377 input { border: 1px solid #ccc; padding: 2px; }
7478 .directory {
117121 <div id="options">
118122 <form action='menu' method=GET>
119123 <input type='hidden' name='base' value='%(base)s' />
120 Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
121 Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
124 Show percentages
125 <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
126 Hide files over
127 <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
122128 Exclude files matching<br />
123 <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
129 <input type='text' id='exclude' name='exclude'
130 value='%(exclude)s' size='20' />
124131 <br />
125132
126133 <input type='submit' value='Change view' id="submit"/>
127134 </form>
128 </div>"""
135 </div>"""
129136
130137 TEMPLATE_FRAMESET = """<html>
131138 <head><title>CherryPy coverage data</title></head>
172179 <td>%s</td>
173180 </tr>\n"""
174181
175 TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
182 TEMPLATE_ITEM = (
183 "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
184 )
185
176186
177187 def _percent(statements, missing):
178188 s = len(statements)
181191 return int(round(100.0 * e / s))
182192 return 0
183193
194
184195 def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
185196 coverage=the_coverage):
186
197
187198 # Show the directory name and any of our children
188199 dirs = [k for k, v in root.items() if v]
189200 dirs.sort()
190201 for name in dirs:
191202 newpath = os.path.join(path, name)
192
203
193204 if newpath.lower().startswith(base):
194205 relpath = newpath[len(base):]
195206 yield "| " * relpath.count(os.sep)
196 yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
197 (newpath, quote_plus(exclude), name)
198
199 for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
207 yield (
208 "<a class='directory' "
209 "href='menu?base=%s&exclude=%s'>%s</a>\n" %
210 (newpath, quote_plus(exclude), name)
211 )
212
213 for chunk in _show_branch(
214 root[name], base, newpath, pct, showpct,
215 exclude, coverage=coverage
216 ):
200217 yield chunk
201
218
202219 # Now list the files
203220 if path.lower().startswith(base):
204221 relpath = path[len(base):]
206223 files.sort()
207224 for name in files:
208225 newpath = os.path.join(path, name)
209
226
210227 pc_str = ""
211228 if showpct:
212229 try:
216233 pass
217234 else:
218235 pc = _percent(statements, missing)
219 pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
236 pc_str = ("%3d%% " % pc).replace(' ', '&nbsp;')
220237 if pc < float(pct) or pc == -1:
221238 pc_str = "<span class='fail'>%s</span>" % pc_str
222239 else:
223240 pc_str = "<span class='pass'>%s</span>" % pc_str
224
241
225242 yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
226243 pc_str, newpath, name)
244
227245
228246 def _skip_file(path, exclude):
229247 if exclude:
230248 return bool(re.search(exclude, path))
231249
250
232251 def _graft(path, tree):
233252 d = tree
234
253
235254 p = path
236255 atoms = []
237256 while True:
242261 atoms.append(p)
243262 if p != "/":
244263 atoms.append("/")
245
264
246265 atoms.reverse()
247266 for node in atoms:
248267 if node:
249268 d = d.setdefault(node, {})
269
250270
251271 def get_tree(base, exclude, coverage=the_coverage):
252272 """Return covered module names as a nested dict."""
257277 _graft(path, tree)
258278 return tree
259279
280
260281 class CoverStats(object):
261
282
262283 def __init__(self, coverage, root=None):
263284 self.coverage = coverage
264285 if root is None:
267288 import cherrypy
268289 root = os.path.dirname(cherrypy.__file__)
269290 self.root = root
270
291
271292 def index(self):
272293 return TEMPLATE_FRAMESET % self.root.lower()
273294 index.exposed = True
274
295
275296 def menu(self, base="/", pct="50", showpct="",
276297 exclude=r'python\d\.\d|test|tut\d|tutorial'):
277
298
278299 # The coverage module uses all-lower-case names.
279300 base = base.lower().rstrip(os.sep)
280
301
281302 yield TEMPLATE_MENU
282303 yield TEMPLATE_FORM % locals()
283
304
284305 # Start by showing links for parent paths
285306 yield "<div id='crumbs'>"
286307 path = ""
291312 yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
292313 % (path, quote_plus(exclude), atom, os.sep))
293314 yield "</div>"
294
315
295316 yield "<div id='tree'>"
296
317
297318 # Then display the tree
298319 tree = get_tree(base, exclude, self.coverage)
299320 if not tree:
300321 yield "<p>No modules covered.</p>"
301322 else:
302323 for chunk in _show_branch(tree, base, "/", pct,
303 showpct=='checked', exclude, coverage=self.coverage):
324 showpct == 'checked', exclude,
325 coverage=self.coverage):
304326 yield chunk
305
327
306328 yield "</div>"
307329 yield "</body></html>"
308330 menu.exposed = True
309
331
310332 def annotated_file(self, filename, statements, excluded, missing):
311333 source = open(filename, 'r')
312334 buffer = []
328350 yield template % (lno, cgi.escape(pastline))
329351 buffer = []
330352 yield template % (lineno, cgi.escape(line))
331
353
332354 def report(self, name):
333 filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
355 filename, statements, excluded, missing, _ = self.coverage.analysis2(
356 name)
334357 pc = _percent(statements, missing)
335358 yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
336359 fullpath=name,
349372 if coverage is None:
350373 raise ImportError("The coverage module could not be imported.")
351374 from coverage import coverage
352 cov = coverage(data_file = path)
375 cov = coverage(data_file=path)
353376 cov.load()
354
377
355378 import cherrypy
356379 cherrypy.config.update({'server.socket_port': int(port),
357380 'server.thread_pool': 10,
361384
362385 if __name__ == "__main__":
363386 serve(*tuple(sys.argv[1:]))
364
2020 re-use the `logging` module by adding a `statistics` object to it.
2121
2222 That `logging.statistics` object is a nested dict. It is not a custom class,
23 because that would 1) require libraries and applications to import a third-
24 party module in order to participate, 2) inhibit innovation in extrapolation
25 approaches and in reporting tools, and 3) be slow. There are, however, some
26 specifications regarding the structure of the dict.
27
28 {
29 +----"SQLAlchemy": {
30 | "Inserts": 4389745,
31 | "Inserts per Second":
32 | lambda s: s["Inserts"] / (time() - s["Start"]),
33 | C +---"Table Statistics": {
34 | o | "widgets": {-----------+
35 N | l | "Rows": 1.3M, | Record
36 a | l | "Inserts": 400, |
37 m | e | },---------------------+
38 e | c | "froobles": {
39 s | t | "Rows": 7845,
40 p | i | "Inserts": 0,
41 a | o | },
42 c | n +---},
43 e | "Slow Queries":
44 | [{"Query": "SELECT * FROM widgets;",
45 | "Processing Time": 47.840923343,
46 | },
47 | ],
48 +----},
49 }
23 because that would:
24
25 1. require libraries and applications to import a third-party module in
26 order to participate
27 2. inhibit innovation in extrapolation approaches and in reporting tools, and
28 3. be slow.
29
30 There are, however, some specifications regarding the structure of the dict.::
31
32 {
33 +----"SQLAlchemy": {
34 | "Inserts": 4389745,
35 | "Inserts per Second":
36 | lambda s: s["Inserts"] / (time() - s["Start"]),
37 | C +---"Table Statistics": {
38 | o | "widgets": {-----------+
39 N | l | "Rows": 1.3M, | Record
40 a | l | "Inserts": 400, |
41 m | e | },---------------------+
42 e | c | "froobles": {
43 s | t | "Rows": 7845,
44 p | i | "Inserts": 0,
45 a | o | },
46 c | n +---},
47 e | "Slow Queries":
48 | [{"Query": "SELECT * FROM widgets;",
49 | "Processing Time": 47.840923343,
50 | },
51 | ],
52 +----},
53 }
5054
5155 The `logging.statistics` dict has four levels. The topmost level is nothing
5256 more than a set of names to introduce modularity, usually along the lines of
6468 good on a report: spaces and capitalization are just fine.
6569
6670 In addition to scalars, values in a namespace MAY be a (third-layer)
67 dict, or a list, called a "collection". For example, the CherryPy StatsTool
68 keeps track of what each request is doing (or has most recently done)
69 in a 'Requests' collection, where each key is a thread ID; each
71 dict, or a list, called a "collection". For example, the CherryPy
72 :class:`StatsTool` keeps track of what each request is doing (or has most
73 recently done) in a 'Requests' collection, where each key is a thread ID; each
7074 value in the subdict MUST be a fourth dict (whew!) of statistical data about
7175 each thread. We call each subdict in the collection a "record". Similarly,
72 the StatsTool also keeps a list of slow queries, where each record contains
73 data about each slow query, in order.
76 the :class:`StatsTool` also keeps a list of slow queries, where each record
77 contains data about each slow query, in order.
7478
7579 Values in a namespace or record may also be functions, which brings us to:
7680
8589
8690 When it comes time to report on the gathered data, however, we usually have
8791 much more freedom in what we can calculate. Therefore, whenever reporting
88 tools (like the provided StatsPage CherryPy class) fetch the contents of
89 `logging.statistics` for reporting, they first call `extrapolate_statistics`
90 (passing the whole `statistics` dict as the only argument). This makes a
91 deep copy of the statistics dict so that the reporting tool can both iterate
92 over it and even change it without harming the original. But it also expands
93 any functions in the dict by calling them. For example, you might have a
94 'Current Time' entry in the namespace with the value "lambda scope: time.time()".
95 The "scope" parameter is the current namespace dict (or record, if we're
96 currently expanding one of those instead), allowing you access to existing
97 static entries. If you're truly evil, you can even modify more than one entry
98 at a time.
92 tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
93 of `logging.statistics` for reporting, they first call
94 `extrapolate_statistics` (passing the whole `statistics` dict as the only
95 argument). This makes a deep copy of the statistics dict so that the
96 reporting tool can both iterate over it and even change it without harming
97 the original. But it also expands any functions in the dict by calling them.
98 For example, you might have a 'Current Time' entry in the namespace with the
99 value "lambda scope: time.time()". The "scope" parameter is the current
100 namespace dict (or record, if we're currently expanding one of those
101 instead), allowing you access to existing static entries. If you're truly
102 evil, you can even modify more than one entry at a time.
99103
100104 However, don't try to calculate an entry and then use its value in further
101105 extrapolations; the order in which the functions are called is not guaranteed.
107111 Reporting
108112 ---------
109113
110 The StatsPage class grabs the `logging.statistics` dict, extrapolates it all,
111 and then transforms it to HTML for easy viewing. Each namespace gets its own
112 header and attribute table, plus an extra table for each collection. This is
113 NOT part of the statistics specification; other tools can format how they like.
114 The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
115 it all, and then transforms it to HTML for easy viewing. Each namespace gets
116 its own header and attribute table, plus an extra table for each collection.
117 This is NOT part of the statistics specification; other tools can format how
118 they like.
114119
115120 You can control which columns are output and how they are formatted by updating
116121 StatsPage.formatting, which is a dict that mirrors the keys and nesting of
117122 `logging.statistics`. The difference is that, instead of data values, it has
118123 formatting values. Use None for a given key to indicate to the StatsPage that a
119 given column should not be output. Use a string with formatting (such as '%.3f')
120 to interpolate the value(s), or use a callable (such as lambda v: v.isoformat())
121 for more advanced formatting. Any entry which is not mentioned in the formatting
122 dict is output unchanged.
124 given column should not be output. Use a string with formatting
125 (such as '%.3f') to interpolate the value(s), or use a callable (such as
126 lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
127 mentioned in the formatting dict is output unchanged.
123128
124129 Monitoring
125130 ----------
144149 Usage
145150 =====
146151
147 To collect statistics on CherryPy applications:
152 To collect statistics on CherryPy applications::
148153
149154 from cherrypy.lib import cpstats
150155 appconfig['/']['tools.cpstats.on'] = True
151156
152 To collect statistics on your own code:
157 To collect statistics on your own code::
153158
154159 import logging
155160 # Initialize the repository
171176 if mystats.get('Enabled', False):
172177 mystats['Important Events'] += 1
173178
174 To report statistics:
179 To report statistics::
175180
176181 root.cpstats = cpstats.StatsPage()
177182
178 To format statistics reports:
183 To format statistics reports::
179184
180185 See 'Reporting', above.
181186
182187 """
183188
184 # -------------------------------- Statistics -------------------------------- #
189 # ------------------------------- Statistics -------------------------------- #
185190
186191 import logging
187 if not hasattr(logging, 'statistics'): logging.statistics = {}
192 if not hasattr(logging, 'statistics'):
193 logging.statistics = {}
194
188195
189196 def extrapolate_statistics(scope):
190197 """Return an extrapolated copy of the given scope."""
200207 return c
201208
202209
203 # --------------------- CherryPy Applications Statistics --------------------- #
210 # -------------------- CherryPy Applications Statistics --------------------- #
204211
205212 import threading
206213 import time
210217 appstats = logging.statistics.setdefault('CherryPy Applications', {})
211218 appstats.update({
212219 'Enabled': True,
213 'Bytes Read/Request': lambda s: (s['Total Requests'] and
214 (s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0),
220 'Bytes Read/Request': lambda s: (
221 s['Total Requests'] and
222 (s['Total Bytes Read'] / float(s['Total Requests'])) or
223 0.0
224 ),
215225 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
216 'Bytes Written/Request': lambda s: (s['Total Requests'] and
217 (s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0),
218 'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s),
226 'Bytes Written/Request': lambda s: (
227 s['Total Requests'] and
228 (s['Total Bytes Written'] / float(s['Total Requests'])) or
229 0.0
230 ),
231 'Bytes Written/Second': lambda s: (
232 s['Total Bytes Written'] / s['Uptime'](s)
233 ),
219234 'Current Time': lambda s: time.time(),
220235 'Current Requests': 0,
221236 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
227242 'Total Time': 0,
228243 'Uptime': lambda s: time.time() - s['Start Time'],
229244 'Requests': {},
230 })
245 })
231246
232247 proc_time = lambda s: time.time() - s['Start Time']
233248
234249
235250 class ByteCountWrapper(object):
251
236252 """Wraps a file-like object, counting the number of bytes read."""
237
253
238254 def __init__(self, rfile):
239255 self.rfile = rfile
240256 self.bytes_read = 0
241
257
242258 def read(self, size=-1):
243259 data = self.rfile.read(size)
244260 self.bytes_read += len(data)
245261 return data
246
262
247263 def readline(self, size=-1):
248264 data = self.rfile.readline(size)
249265 self.bytes_read += len(data)
250266 return data
251
267
252268 def readlines(self, sizehint=0):
253269 # Shamelessly stolen from StringIO
254270 total = 0
261277 break
262278 line = self.readline()
263279 return lines
264
280
265281 def close(self):
266282 self.rfile.close()
267
283
268284 def __iter__(self):
269285 return self
270
286
271287 def next(self):
272288 data = self.rfile.next()
273289 self.bytes_read += len(data)
278294
279295
280296 class StatsTool(cherrypy.Tool):
297
281298 """Record various information about the current request."""
282
299
283300 def __init__(self):
284301 cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
285
302
286303 def _setup(self):
287304 """Hook this tool into cherrypy.request.
288
305
289306 The standard CherryPy request object will automatically call this
290307 method when the tool is "turned on" in config.
291308 """
292309 if appstats.get('Enabled', False):
293310 cherrypy.Tool._setup(self)
294311 self.record_start()
295
312
296313 def record_start(self):
297314 """Record the beginning of a request."""
298315 request = cherrypy.serving.request
299316 if not hasattr(request.rfile, 'bytes_read'):
300317 request.rfile = ByteCountWrapper(request.rfile)
301318 request.body.fp = request.rfile
302
319
303320 r = request.remote
304
321
305322 appstats['Current Requests'] += 1
306323 appstats['Total Requests'] += 1
307324 appstats['Requests'][threading._get_ident()] = {
314331 'Request-Line': request.request_line,
315332 'Response Status': None,
316333 'Start Time': time.time(),
317 }
318
319 def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100,
320 debug=False, **kwargs):
334 }
335
336 def record_stop(
337 self, uriset=None, slow_queries=1.0, slow_queries_count=100,
338 debug=False, **kwargs):
321339 """Record the end of a request."""
322340 resp = cherrypy.serving.response
323341 w = appstats['Requests'][threading._get_ident()]
324
342
325343 r = cherrypy.request.rfile.bytes_read
326344 w['Bytes Read'] = r
327345 appstats['Total Bytes Read'] += r
328
346
329347 if resp.stream:
330348 w['Bytes Written'] = 'chunked'
331349 else:
332350 cl = int(resp.headers.get('Content-Length', 0))
333351 w['Bytes Written'] = cl
334352 appstats['Total Bytes Written'] += cl
335
336 w['Response Status'] = getattr(resp, 'output_status', None) or resp.status
337
353
354 w['Response Status'] = getattr(
355 resp, 'output_status', None) or resp.status
356
338357 w['End Time'] = time.time()
339358 p = w['End Time'] - w['Start Time']
340359 w['Processing Time'] = p
341360 appstats['Total Time'] += p
342
361
343362 appstats['Current Requests'] -= 1
344
363
345364 if debug:
346365 cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
347
366
348367 if uriset:
349368 rs = appstats.setdefault('URI Set Tracking', {})
350369 r = rs.setdefault(uriset, {
356375 r['Max'] = p
357376 r['Count'] += 1
358377 r['Sum'] += p
359
378
360379 if slow_queries and p > slow_queries:
361380 sq = appstats.setdefault('Slow Queries', [])
362381 sq.append(w.copy())
386405
387406 locale_date = lambda v: time.strftime('%c', time.gmtime(v))
388407 iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
408
389409
390410 def pause_resume(ns):
391411 def _pause_resume(enabled):
409429
410430
411431 class StatsPage(object):
412
432
413433 formatting = {
414434 'CherryPy Applications': {
415435 'Enabled': pause_resume('CherryPy Applications'),
426446 'End Time': None,
427447 'Processing Time': '%.3f',
428448 'Start Time': iso_format,
429 },
449 },
430450 'URI Set Tracking': {
431451 'Avg': '%.3f',
432452 'Max': '%.3f',
433453 'Min': '%.3f',
434454 'Sum': '%.3f',
435 },
455 },
436456 'Requests': {
437457 'Bytes Read': '%s',
438458 'Bytes Written': '%s',
439459 'End Time': None,
440460 'Processing Time': '%.3f',
441461 'Start Time': None,
442 },
462 },
443463 },
444464 'CherryPy WSGIServer': {
445465 'Enabled': pause_resume('CherryPy WSGIServer'),
447467 'Start time': iso_format,
448468 },
449469 }
450
451
470
452471 def index(self):
453472 # Transform the raw data into pretty output for HTML
454473 yield """
499518 """ % title
500519 for i, (key, value) in enumerate(scalars):
501520 colnum = i % 3
502 if colnum == 0: yield """
521 if colnum == 0:
522 yield """
503523 <tr>"""
524 yield (
525 """
526 <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
527 vars()
528 )
529 if colnum == 2:
530 yield """
531 </tr>"""
532
533 if colnum == 0:
504534 yield """
505 <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
506 if colnum == 2: yield """
507 </tr>"""
508
509 if colnum == 0: yield """
510535 <th></th><td></td>
511536 <th></th><td></td>
512537 </tr>"""
513 elif colnum == 1: yield """
538 elif colnum == 1:
539 yield """
514540 <th></th><td></td>
515541 </tr>"""
516542 yield """
546572 </html>
547573 """
548574 index.exposed = True
549
575
550576 def get_namespaces(self):
551577 """Yield (title, scalars, collections) for each namespace."""
552578 s = extrapolate_statistics(logging.statistics)
573599 v = format % v
574600 scalars.append((k, v))
575601 yield title, scalars, collections
576
602
577603 def get_dict_collection(self, v, formatting):
578604 """Return ([headers], [rows]) for the given collection."""
579605 # E.g., the 'Requests' dict.
587613 if k3 not in headers:
588614 headers.append(k3)
589615 headers.sort()
590
616
591617 subrows = []
592618 for k2, record in sorted(v.items()):
593619 subrow = [k2]
603629 v3 = format % v3
604630 subrow.append(v3)
605631 subrows.append(subrow)
606
632
607633 return headers, subrows
608
634
609635 def get_list_collection(self, v, formatting):
610636 """Return ([headers], [subrows]) for the given collection."""
611637 # E.g., the 'Slow Queries' list.
619645 if k3 not in headers:
620646 headers.append(k3)
621647 headers.sort()
622
648
623649 subrows = []
624650 for record in v:
625651 subrow = []
635661 v3 = format % v3
636662 subrow.append(v3)
637663 subrows.append(subrow)
638
664
639665 return headers, subrows
640
666
641667 if json is not None:
642668 def data(self):
643669 s = extrapolate_statistics(logging.statistics)
644670 cherrypy.response.headers['Content-Type'] = 'application/json'
645671 return json.dumps(s, sort_keys=True, indent=4)
646672 data.exposed = True
647
673
648674 def pause(self, namespace):
649675 logging.statistics.get(namespace, {})['Enabled'] = False
650676 raise cherrypy.HTTPRedirect('./')
651677 pause.exposed = True
652678 pause.cp_config = {'tools.allow.on': True,
653679 'tools.allow.methods': ['POST']}
654
680
655681 def resume(self, namespace):
656682 logging.statistics.get(namespace, {})['Enabled'] = True
657683 raise cherrypy.HTTPRedirect('./')
658684 resume.exposed = True
659685 resume.cp_config = {'tools.allow.on': True,
660686 'tools.allow.methods': ['POST']}
661
33 import re
44
55 import cherrypy
6 from cherrypy._cpcompat import basestring, ntob, md5, set
6 from cherrypy._cpcompat import basestring, md5, set, unicodestr
77 from cherrypy.lib import httputil as _httputil
8 from cherrypy.lib import is_iterator
89
910
1011 # Conditional HTTP request support #
1112
1213 def validate_etags(autotags=False, debug=False):
1314 """Validate the current ETag against If-Match, If-None-Match headers.
14
15
1516 If autotags is True, an ETag response-header value will be provided
1617 from an MD5 hash of the response body (unless some other code has
1718 already provided an ETag header). If False (the default), the ETag
1819 will not be automatic.
19
20
2021 WARNING: the autotags feature is not designed for URL's which allow
2122 methods other than GET. For example, if a POST to the same URL returns
2223 no content, the automatic ETag will be incorrect, breaking a fundamental
2627 See :rfc:`2616` Section 14.24.
2728 """
2829 response = cherrypy.serving.response
29
30
3031 # Guard against being run twice.
3132 if hasattr(response, "ETag"):
3233 return
33
34
3435 status, reason, msg = _httputil.valid_status(response.status)
35
36
3637 etag = response.headers.get('ETag')
37
38
3839 # Automatic ETag generation. See warning in docstring.
3940 if etag:
4041 if debug:
5152 if debug:
5253 cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
5354 response.headers['ETag'] = etag
54
55
5556 response.ETag = etag
56
57
5758 # "If the request would, without the If-Match header field, result in
5859 # anything other than a 2xx or 412 status, then the If-Match header
5960 # MUST be ignored."
6162 cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
6263 if status >= 200 and status <= 299:
6364 request = cherrypy.serving.request
64
65
6566 conditions = request.headers.elements('If-Match') or []
6667 conditions = [str(x) for x in conditions]
6768 if debug:
7071 if conditions and not (conditions == ["*"] or etag in conditions):
7172 raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
7273 "not match %r" % (etag, conditions))
73
74
7475 conditions = request.headers.elements('If-None-Match') or []
7576 conditions = [str(x) for x in conditions]
7677 if debug:
7879 'TOOLS.ETAGS')
7980 if conditions == ["*"] or etag in conditions:
8081 if debug:
81 cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS')
82 cherrypy.log('request.method: %s' %
83 request.method, 'TOOLS.ETAGS')
8284 if request.method in ("GET", "HEAD"):
8385 raise cherrypy.HTTPRedirect([], 304)
8486 else:
8587 raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
8688 "matched %r" % (etag, conditions))
8789
90
8891 def validate_since():
8992 """Validate the current Last-Modified against If-Modified-Since headers.
90
93
9194 If no code has set the Last-Modified response header, then no validation
9295 will be performed.
9396 """
9598 lastmod = response.headers.get('Last-Modified')
9699 if lastmod:
97100 status, reason, msg = _httputil.valid_status(response.status)
98
101
99102 request = cherrypy.serving.request
100
103
101104 since = request.headers.get('If-Unmodified-Since')
102105 if since and since != lastmod:
103106 if (status >= 200 and status <= 299) or status == 412:
104107 raise cherrypy.HTTPError(412)
105
108
106109 since = request.headers.get('If-Modified-Since')
107110 if since and since == lastmod:
108111 if (status >= 200 and status <= 299) or status == 304:
116119
117120 def allow(methods=None, debug=False):
118121 """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
119
122
120123 The given methods are case-insensitive, and may be in any order.
121124 If only one method is allowed, you may supply a single string;
122125 if more than one, supply a list of strings.
123
126
124127 Regardless of whether the current method is allowed or not, this
125128 also emits an 'Allow' response header, containing the given methods.
126129 """
131134 methods = ['GET', 'HEAD']
132135 elif 'GET' in methods and 'HEAD' not in methods:
133136 methods.append('HEAD')
134
137
135138 cherrypy.response.headers['Allow'] = ', '.join(methods)
136139 if cherrypy.request.method not in methods:
137140 if debug:
147150 def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
148151 scheme='X-Forwarded-Proto', debug=False):
149152 """Change the base URL (scheme://host[:port][/path]).
150
153
151154 For running a CP server behind Apache, lighttpd, or other HTTP server.
152
155
153156 For Apache and lighttpd, you should leave the 'local' argument at the
154157 default value of 'X-Forwarded-Host'. For Squid, you probably want to set
155158 tools.proxy.local = 'Origin'.
156
159
157160 If you want the new request.base to include path info (not just the host),
158161 you must explicitly set base to the full base path, and ALSO set 'local'
159162 to '', so that the X-Forwarded-Host request header (which never includes
160163 path info) does not override it. Regardless, the value for 'base' MUST
161164 NOT end in a slash.
162
165
163166 cherrypy.request.remote.ip (the IP address of the client) will be
164167 rewritten if the header specified by the 'remote' arg is valid.
165168 By default, 'remote' is set to 'X-Forwarded-For'. If you do not
166169 want to rewrite remote.ip, set the 'remote' arg to an empty string.
167170 """
168
171
169172 request = cherrypy.serving.request
170
173
171174 if scheme:
172175 s = request.headers.get(scheme, None)
173176 if debug:
180183 scheme = s
181184 if not scheme:
182185 scheme = request.base[:request.base.find("://")]
183
186
184187 if local:
185188 lbase = request.headers.get(local, None)
186189 if debug:
193196 base = '127.0.0.1'
194197 else:
195198 base = '127.0.0.1:%s' % port
196
199
197200 if base.find("://") == -1:
198201 # add http:// or https:// if needed
199202 base = scheme + "://" + base
200
203
201204 request.base = base
202
205
203206 if remote:
204207 xff = request.headers.get(remote)
205208 if debug:
206209 cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
207210 if xff:
208211 if remote == 'X-Forwarded-For':
209 # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
210 xff = xff.split(',')[-1].strip()
212 #Bug #1268
213 xff = xff.split(',')[0].strip()
211214 request.remote.ip = xff
212215
213216
214217 def ignore_headers(headers=('Range',), debug=False):
215218 """Delete request headers whose field names are included in 'headers'.
216
219
217220 This is a useful tool for working behind certain HTTP servers;
218221 for example, Apache duplicates the work that CP does for 'Range'
219222 headers, and will doubly-truncate the response.
240243 def referer(pattern, accept=True, accept_missing=False, error=403,
241244 message='Forbidden Referer header.', debug=False):
242245 """Raise HTTPError if Referer header does/does not match the given pattern.
243
246
244247 pattern
245248 A regular expression pattern to test against the Referer.
246
249
247250 accept
248251 If True, the Referer must match the pattern; if False,
249252 the Referer must NOT match the pattern.
253256
254257 error
255258 The HTTP error code to return to the client on failure.
256
259
257260 message
258261 A string to include in the response body on failure.
259
262
260263 """
261264 try:
262265 ref = cherrypy.serving.request.headers['Referer']
271274 cherrypy.log('No Referer header', 'TOOLS.REFERER')
272275 if accept_missing:
273276 return
274
277
275278 raise cherrypy.HTTPError(error, message)
276279
277280
278281 class SessionAuth(object):
282
279283 """Assert that the user is logged in."""
280
284
281285 session_key = "username"
282286 debug = False
283
287
284288 def check_username_and_password(self, username, password):
285289 pass
286
290
287291 def anonymous(self):
288292 """Provide a temporary user name for anonymous users."""
289293 pass
290
294
291295 def on_login(self, username):
292296 pass
293
297
294298 def on_logout(self, username):
295299 pass
296
300
297301 def on_check(self, username):
298302 pass
299
300 def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
301 return ntob("""<html><body>
303
304 def login_screen(self, from_page='..', username='', error_msg='',
305 **kwargs):
306 return (unicodestr("""<html><body>
302307 Message: %(error_msg)s
303308 <form method="post" action="do_login">
304 Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
305 Password: <input type="password" name="password" size="10" /><br />
306 <input type="hidden" name="from_page" value="%(from_page)s" /><br />
309 Login: <input type="text" name="username" value="%(username)s" size="10" />
310 <br />
311 Password: <input type="password" name="password" size="10" />
312 <br />
313 <input type="hidden" name="from_page" value="%(from_page)s" />
314 <br />
307315 <input type="submit" />
308316 </form>
309 </body></html>""" % {'from_page': from_page, 'username': username,
310 'error_msg': error_msg}, "utf-8")
311
317 </body></html>""") % vars()).encode("utf-8")
318
312319 def do_login(self, username, password, from_page='..', **kwargs):
313320 """Login. May raise redirect, or return True if request handled."""
314321 response = cherrypy.serving.response
325332 cherrypy.session[self.session_key] = username
326333 self.on_login(username)
327334 raise cherrypy.HTTPRedirect(from_page or "/")
328
335
329336 def do_logout(self, from_page='..', **kwargs):
330337 """Logout. May raise redirect, or return True if request handled."""
331338 sess = cherrypy.session
335342 cherrypy.serving.request.login = None
336343 self.on_logout(username)
337344 raise cherrypy.HTTPRedirect(from_page)
338
345
339346 def do_check(self):
340 """Assert username. May raise redirect, or return True if request handled."""
347 """Assert username. Raise redirect, or return True if request handled.
348 """
341349 sess = cherrypy.session
342350 request = cherrypy.serving.request
343351 response = cherrypy.serving.response
344
352
345353 username = sess.get(self.session_key)
346354 if not username:
347355 sess[self.session_key] = username = self.anonymous()
348356 if self.debug:
349 cherrypy.log('No session[username], trying anonymous', 'TOOLS.SESSAUTH')
357 cherrypy.log(
358 'No session[username], trying anonymous', 'TOOLS.SESSAUTH')
350359 if not username:
351360 url = cherrypy.url(qs=request.query_string)
352361 if self.debug:
358367 del response.headers["Content-Length"]
359368 return True
360369 if self.debug:
361 cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH')
370 cherrypy.log('Setting request.login to %r' %
371 username, 'TOOLS.SESSAUTH')
362372 request.login = username
363373 self.on_check(username)
364
374
365375 def run(self):
366376 request = cherrypy.serving.request
367377 response = cherrypy.serving.response
368
378
369379 path = request.path_info
370380 if path.endswith('login_screen'):
371381 if self.debug:
372 cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUTH')
382 cherrypy.log('routing %r to login_screen' %
383 path, 'TOOLS.SESSAUTH')
373384 return self.login_screen(**request.params)
374385 elif path.endswith('do_login'):
375386 if request.method != 'POST':
385396 response.headers['Allow'] = "POST"
386397 raise cherrypy.HTTPError(405)
387398 if self.debug:
388 cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH')
399 cherrypy.log('routing %r to do_logout' %
400 path, 'TOOLS.SESSAUTH')
389401 return self.do_logout(**request.params)
390402 else:
391403 if self.debug:
392 cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUTH')
404 cherrypy.log('No special path, running do_check',
405 'TOOLS.SESSAUTH')
393406 return self.do_check()
394407
395408
411424 """Write the last error's traceback to the cherrypy error log."""
412425 cherrypy.log("", "HTTP", severity=severity, traceback=True)
413426
427
414428 def log_request_headers(debug=False):
415429 """Write request headers to the cherrypy error log."""
416430 h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list]
417431 cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
418432
433
419434 def log_hooks(debug=False):
420435 """Write request.hooks to the cherrypy error log."""
421436 request = cherrypy.serving.request
422
437
423438 msg = []
424439 # Sort by the standard points if possible.
425440 from cherrypy import _cprequest
427442 for k in request.hooks.keys():
428443 if k not in points:
429444 points.append(k)
430
445
431446 for k in points:
432447 msg.append(" %s:" % k)
433448 v = request.hooks.get(k, [])
436451 msg.append(" %r" % h)
437452 cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
438453 ':\n' + '\n'.join(msg), "HTTP")
454
439455
440456 def redirect(url='', internal=True, debug=False):
441457 """Raise InternalRedirect or HTTPRedirect to the given url."""
448464 else:
449465 raise cherrypy.HTTPRedirect(url)
450466
467
451468 def trailing_slash(missing=True, extra=False, status=None, debug=False):
452469 """Redirect if path_info has (missing|extra) trailing slash."""
453470 request = cherrypy.serving.request
454471 pi = request.path_info
455
472
456473 if debug:
457474 cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
458475 (request.is_index, missing, extra, pi),
469486 new_url = cherrypy.url(pi[:-1], request.query_string)
470487 raise cherrypy.HTTPRedirect(new_url, status=status or 301)
471488
489
472490 def flatten(debug=False):
473491 """Wrap response.body in a generator that recursively iterates over body.
474
492
475493 This allows cherrypy.response.body to consist of 'nested generators';
476494 that is, a set of generators that yield generators.
477495 """
478 import types
479496 def flattener(input):
480497 numchunks = 0
481498 for x in input:
482 if not isinstance(x, types.GeneratorType):
499 if not is_iterator(x):
483500 numchunks += 1
484501 yield x
485502 else:
494511
495512 def accept(media=None, debug=False):
496513 """Return the client's preferred media-type (from the given Content-Types).
497
514
498515 If 'media' is None (the default), no test will be performed.
499
516
500517 If 'media' is provided, it should be the Content-Type value (as a string)
501518 or values (as a list or tuple of strings) which the current resource
502519 can emit. The client's acceptable media ranges (as declared in the
504521 values; the first such string is returned. That is, the return value
505522 will always be one of the strings provided in the 'media' arg (or None
506523 if 'media' is None).
507
524
508525 If no match is found, then HTTPError 406 (Not Acceptable) is raised.
509526 Note that most web browsers send */* as a (low-quality) acceptable
510527 media range, which should match any Content-Type. In addition, "...if
511528 no Accept header field is present, then it is assumed that the client
512529 accepts all media types."
513
530
514531 Matching types are checked in order of client preference first,
515532 and then in the order of the given 'media' values.
516
533
517534 Note that this function does not honor accept-params (other than "q").
518535 """
519536 if not media:
521538 if isinstance(media, basestring):
522539 media = [media]
523540 request = cherrypy.serving.request
524
541
525542 # Parse the Accept request header, and try to match one
526543 # of the requested media-ranges (in order of preference).
527544 ranges = request.headers.elements('Accept')
555572 cherrypy.log('Match due to %s' % element.value,
556573 'TOOLS.ACCEPT')
557574 return element.value
558
575
559576 # No suitable media-range found.
560577 ah = request.headers.get('Accept')
561578 if ah is None:
568585
569586
570587 class MonitoredHeaderMap(_httputil.HeaderMap):
571
588
572589 def __init__(self):
573590 self.accessed_headers = set()
574
591
575592 def __getitem__(self, key):
576593 self.accessed_headers.add(key)
577594 return _httputil.HeaderMap.__getitem__(self, key)
578
595
579596 def __contains__(self, key):
580597 self.accessed_headers.add(key)
581598 return _httputil.HeaderMap.__contains__(self, key)
582
599
583600 def get(self, key, default=None):
584601 self.accessed_headers.add(key)
585602 return _httputil.HeaderMap.get(self, key, default=default)
586
603
587604 if hasattr({}, 'has_key'):
588605 # Python 2
589606 def has_key(self, key):
592609
593610
594611 def autovary(ignore=None, debug=False):
595 """Auto-populate the Vary response header based on request.header access."""
612 """Auto-populate the Vary response header based on request.header access.
613 """
596614 request = cherrypy.serving.request
597
615
598616 req_h = request.headers
599617 request.headers = MonitoredHeaderMap()
600618 request.headers.update(req_h)
601619 if ignore is None:
602620 ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
603
621
604622 def set_response_header():
605623 resp_h = cherrypy.serving.response.headers
606624 v = set([e.value for e in resp_h.elements('Vary')])
607625 if debug:
608 cherrypy.log('Accessed headers: %s' % request.headers.accessed_headers,
609 'TOOLS.AUTOVARY')
626 cherrypy.log(
627 'Accessed headers: %s' % request.headers.accessed_headers,
628 'TOOLS.AUTOVARY')
610629 v = v.union(request.headers.accessed_headers)
611630 v = v.difference(ignore)
612631 v = list(v)
613632 v.sort()
614633 resp_h['Vary'] = ', '.join(v)
615634 request.hooks.attach('before_finalize', set_response_header, 95)
616
88
99 def decode(encoding=None, default_encoding='utf-8'):
1010 """Replace or extend the list of charsets used to decode a request entity.
11
11
1212 Either argument may be a single string or a list of strings.
13
13
1414 encoding
1515 If not None, restricts the set of charsets attempted while decoding
16 a request entity to the given set (even if a different charset is given in
17 the Content-Type request header).
18
16 a request entity to the given set (even if a different charset is
17 given in the Content-Type request header).
18
1919 default_encoding
2020 Only in effect if the 'encoding' argument is not given.
21 If given, the set of charsets attempted while decoding a request entity is
22 *extended* with the given value(s).
23
21 If given, the set of charsets attempted while decoding a request
22 entity is *extended* with the given value(s).
23
2424 """
2525 body = cherrypy.request.body
2626 if encoding is not None:
3131 if not isinstance(default_encoding, list):
3232 default_encoding = [default_encoding]
3333 body.attempt_charsets = body.attempt_charsets + default_encoding
34
35 class UTF8StreamEncoder:
36 def __init__(self, iterator):
37 self._iterator = iterator
38
39 def __iter__(self):
40 return self
41
42 def next(self):
43 return self.__next__()
44
45 def __next__(self):
46 res = next(self._iterator)
47 if isinstance(res, unicodestr):
48 res = res.encode('utf-8')
49 return res
50
51 def __getattr__(self, attr):
52 if attr.startswith('__'):
53 raise AttributeError(self, attr)
54 return getattr(self._iterator, attr)
3455
3556
3657 class ResponseEncoder:
37
58
3859 default_encoding = 'utf-8'
3960 failmsg = "Response body could not be encoded with %r."
4061 encoding = None
4263 text_only = True
4364 add_charset = True
4465 debug = False
45
66
4667 def __init__(self, **kwargs):
4768 for k, v in kwargs.items():
4869 setattr(self, k, v)
49
70
5071 self.attempted_charsets = set()
5172 request = cherrypy.serving.request
5273 if request.handler is not None:
5576 cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
5677 self.oldhandler = request.handler
5778 request.handler = self
58
79
5980 def encode_stream(self, encoding):
6081 """Encode a streaming response body.
61
82
6283 Use a generator wrapper, and just pray it works as the stream is
6384 being written out.
6485 """
6586 if encoding in self.attempted_charsets:
6687 return False
6788 self.attempted_charsets.add(encoding)
68
89
6990 def encoder(body):
7091 for chunk in body:
7192 if isinstance(chunk, unicodestr):
7394 yield chunk
7495 self.body = encoder(self.body)
7596 return True
76
97
7798 def encode_string(self, encoding):
7899 """Encode a buffered response body."""
79100 if encoding in self.attempted_charsets:
80101 return False
81102 self.attempted_charsets.add(encoding)
82
83 try:
84 body = []
85 for chunk in self.body:
86 if isinstance(chunk, unicodestr):
103 body = []
104 for chunk in self.body:
105 if isinstance(chunk, unicodestr):
106 try:
87107 chunk = chunk.encode(encoding, self.errors)
88 body.append(chunk)
89 self.body = body
90 except (LookupError, UnicodeError):
91 return False
92 else:
93 return True
94
108 except (LookupError, UnicodeError):
109 return False
110 body.append(chunk)
111 self.body = body
112 return True
113
95114 def find_acceptable_charset(self):
96115 request = cherrypy.serving.request
97116 response = cherrypy.serving.response
98
117
99118 if self.debug:
100 cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
119 cherrypy.log('response.stream %r' %
120 response.stream, 'TOOLS.ENCODE')
101121 if response.stream:
102122 encoder = self.encode_stream
103123 else:
114134 # >>> len(t.encode("utf7"))
115135 # 8
116136 del response.headers["Content-Length"]
117
137
118138 # Parse the Accept-Charset request header, and try to provide one
119139 # of the requested charsets (in order of user preference).
120140 encs = request.headers.elements('Accept-Charset')
121141 charsets = [enc.value.lower() for enc in encs]
122142 if self.debug:
123143 cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
124
144
125145 if self.encoding is not None:
126146 # If specified, force this encoding to be used, or fail.
127147 encoding = self.encoding.lower()
128148 if self.debug:
129 cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE')
149 cherrypy.log('Specified encoding %r' %
150 encoding, 'TOOLS.ENCODE')
130151 if (not charsets) or "*" in charsets or encoding in charsets:
131152 if self.debug:
132 cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE')
153 cherrypy.log('Attempting encoding %r' %
154 encoding, 'TOOLS.ENCODE')
133155 if encoder(encoding):
134156 return encoding
135157 else:
141163 if encoder(self.default_encoding):
142164 return self.default_encoding
143165 else:
144 raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding)
166 raise cherrypy.HTTPError(500, self.failmsg %
167 self.default_encoding)
145168 else:
146169 for element in encs:
147170 if element.qvalue > 0:
159182 '0)' % element, 'TOOLS.ENCODE')
160183 if encoder(encoding):
161184 return encoding
162
185
163186 if "*" not in charsets:
164187 # If no "*" is present in an Accept-Charset field, then all
165188 # character sets not explicitly mentioned get a quality
172195 'TOOLS.ENCODE')
173196 if encoder(iso):
174197 return iso
175
198
176199 # No suitable encoding found.
177200 ac = request.headers.get('Accept-Charset')
178201 if ac is None:
179202 msg = "Your client did not send an Accept-Charset header."
180203 else:
181204 msg = "Your client sent this Accept-Charset header: %s." % ac
182 msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
205 _charsets = ", ".join(sorted(self.attempted_charsets))
206 msg += " We tried these charsets: %s." % (_charsets,)
183207 raise cherrypy.HTTPError(406, msg)
184
208
185209 def __call__(self, *args, **kwargs):
186210 response = cherrypy.serving.response
187211 self.body = self.oldhandler(*args, **kwargs)
188
212
189213 if isinstance(self.body, basestring):
190214 # strings get wrapped in a list because iterating over a single
191215 # item list is much faster than iterating over every character
199223 self.body = file_generator(self.body)
200224 elif self.body is None:
201225 self.body = []
202
226
203227 ct = response.headers.elements("Content-Type")
204228 if self.debug:
205 cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
206 if ct:
229 cherrypy.log('Content-Type: %r' % [str(h)
230 for h in ct], 'TOOLS.ENCODE')
231 if ct and self.add_charset:
207232 ct = ct[0]
208233 if self.text_only:
209234 if ct.value.lower().startswith("text/"):
210235 if self.debug:
211 cherrypy.log('Content-Type %s starts with "text/"' % ct,
212 'TOOLS.ENCODE')
236 cherrypy.log(
237 'Content-Type %s starts with "text/"' % ct,
238 'TOOLS.ENCODE')
213239 do_find = True
214240 else:
215241 if self.debug:
216 cherrypy.log('Not finding because Content-Type %s does '
217 'not start with "text/"' % ct,
242 cherrypy.log('Not finding because Content-Type %s '
243 'does not start with "text/"' % ct,
218244 'TOOLS.ENCODE')
219245 do_find = False
220246 else:
221247 if self.debug:
222 cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
248 cherrypy.log('Finding because not text_only',
249 'TOOLS.ENCODE')
223250 do_find = True
224
251
225252 if do_find:
226253 # Set "charset=..." param on response Content-Type header
227254 ct.params['charset'] = self.find_acceptable_charset()
228 if self.add_charset:
229 if self.debug:
230 cherrypy.log('Setting Content-Type %s' % ct,
231 'TOOLS.ENCODE')
232 response.headers["Content-Type"] = str(ct)
233
255 if self.debug:
256 cherrypy.log('Setting Content-Type %s' % ct,
257 'TOOLS.ENCODE')
258 response.headers["Content-Type"] = str(ct)
259
234260 return self.body
235261
236262 # GZIP
263
237264
238265 def compress(body, compress_level):
239266 """Compress 'body' at the given compress_level."""
240267 import zlib
241
268
242269 # See http://www.gzip.org/zlib/rfc-gzip.html
243270 yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
244271 yield ntob('\x08') # CM: compression method
247274 yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
248275 yield ntob('\x02') # XFL: max compression, slowest algo
249276 yield ntob('\xff') # OS: unknown
250
277
251278 crc = zlib.crc32(ntob(""))
252279 size = 0
253280 zobj = zlib.compressobj(compress_level,
258285 crc = zlib.crc32(line, crc)
259286 yield zobj.compress(line)
260287 yield zobj.flush()
261
288
262289 # CRC32: 4 bytes
263290 yield struct.pack("<L", crc & int('FFFFFFFF', 16))
264291 # ISIZE: 4 bytes
265292 yield struct.pack("<L", size & int('FFFFFFFF', 16))
266293
294
267295 def decompress(body):
268296 import gzip
269
297
270298 zbuf = BytesIO()
271299 zbuf.write(body)
272300 zbuf.seek(0)
276304 return data
277305
278306
279 def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
307 def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
308 debug=False):
280309 """Try to gzip the response body if Content-Type in mime_types.
281
310
282311 cherrypy.response.headers['Content-Type'] must be set to one of the
283312 values in the mime_types arg before calling this function.
284313
286315 * type/subtype
287316 * type/*
288317 * type/*+subtype
289
318
290319 No compression is performed if any of the following hold:
291320 * The client sends no Accept-Encoding request header
292321 * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
293322 * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
294323 * The 'identity' value is given with a qvalue > 0.
295
324
296325 """
297326 request = cherrypy.serving.request
298327 response = cherrypy.serving.response
299
328
300329 set_vary_header(response, "Accept-Encoding")
301
330
302331 if not response.body:
303332 # Response body is empty (might be a 304 for instance)
304333 if debug:
305334 cherrypy.log('No response body', context='TOOLS.GZIP')
306335 return
307
336
308337 # If returning cached content (which should already have been gzipped),
309338 # don't re-zip.
310339 if getattr(request, "cached", False):
311340 if debug:
312341 cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
313342 return
314
343
315344 acceptable = request.headers.elements('Accept-Encoding')
316345 if not acceptable:
317346 # If no Accept-Encoding field is present in a request,
324353 if debug:
325354 cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
326355 return
327
356
328357 ct = response.headers.get('Content-Type', '').split(';')[0]
329358 for coding in acceptable:
330359 if coding.value == 'identity' and coding.qvalue != 0:
338367 cherrypy.log('Zero gzip qvalue: %s' % coding,
339368 context='TOOLS.GZIP')
340369 return
341
370
342371 if ct not in mime_types:
343372 # If the list of provided mime-types contains tokens
344373 # such as 'text/*' or 'application/*+xml',
369398 cherrypy.log('Content-Type %s not in mime_types %r' %
370399 (ct, mime_types), context='TOOLS.GZIP')
371400 return
372
401
373402 if debug:
374403 cherrypy.log('Gzipping', context='TOOLS.GZIP')
375404 # Return a generator that compresses the page
378407 if "Content-Length" in response.headers:
379408 # Delete Content-Length header so finalize() recalcs it.
380409 del response.headers["Content-Length"]
381
410
382411 return
383
412
384413 if debug:
385414 cherrypy.log('No acceptable encoding found.', context='GZIP')
386415 cherrypy.HTTPError(406, "identity, gzip").set_response()
387
22 import os
33 import sys
44 import time
5
5
66 try:
77 import objgraph
88 except ImportError:
1414
1515
1616 class ReferrerTree(object):
17
1718 """An object which gathers all referrers of an object to a given depth."""
1819
1920 peek_length = 40
8889 def format(self, tree):
8990 """Return a list of string reprs from a nested list of referrers."""
9091 output = []
92
9193 def ascend(branch, depth=1):
9294 for parent, grandparents in branch:
9395 output.append((" " * depth) + self._format(parent))
102104
103105
104106 class RequestCounter(SimplePlugin):
105
107
106108 def start(self):
107109 self.count = 0
108
110
109111 def before_request(self):
110112 self.count += 1
111
113
112114 def after_request(self):
113 self.count -=1
115 self.count -= 1
114116 request_counter = RequestCounter(cherrypy.engine)
115117 request_counter.subscribe()
116118
128130
129131
130132 class GCRoot(object):
133
131134 """A CherryPy page handler for testing reference leaks."""
132135
133 classes = [(_cprequest.Request, 2, 2,
134 "Should be 1 in this request thread and 1 in the main thread."),
135 (_cprequest.Response, 2, 2,
136 "Should be 1 in this request thread and 1 in the main thread."),
137 (_cpwsgi.AppResponse, 1, 1,
138 "Should be 1 in this request thread only."),
139 ]
136 classes = [
137 (_cprequest.Request, 2, 2,
138 "Should be 1 in this request thread and 1 in the main thread."),
139 (_cprequest.Response, 2, 2,
140 "Should be 1 in this request thread and 1 in the main thread."),
141 (_cpwsgi.AppResponse, 1, 1,
142 "Should be 1 in this request thread only."),
143 ]
140144
141145 def index(self):
142146 return "Hello, world!"
144148
145149 def stats(self):
146150 output = ["Statistics:"]
147
151
148152 for trial in range(10):
149153 if request_counter.count > 0:
150154 break
151155 time.sleep(0.5)
152156 else:
153157 output.append("\nNot all requests closed properly.")
154
158
155159 # gc_collect isn't perfectly synchronous, because it may
156160 # break reference cycles that then take time to fully
157161 # finalize. Call it thrice and hope for the best.
207211 t = ReferrerTree(ignore=[objs], maxdepth=3)
208212 tree = t.ascend(obj)
209213 output.extend(t.format(tree))
210
214
211215 return "\n".join(output)
212216 stats.exposed = True
213
33 DeprecationWarning)
44
55 from cherrypy.lib.httputil import *
6
00 """
1 This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`).
1 This module defines functions to implement HTTP Digest Authentication
2 (:rfc:`2617`).
23 This has full compliance with 'Digest' and 'Basic' authentication methods. In
34 'Digest' it supports both MD5 and MD5-sess algorithms.
45
67 First use 'doAuth' to request the client authentication for a
78 certain resource. You should send an httplib.UNAUTHORIZED response to the
89 client so he knows he has to authenticate itself.
9
10
1011 Then use 'parseAuthorization' to retrieve the 'auth_map' used in
1112 'checkResponse'.
1213
13 To use 'checkResponse' you must have already verified the password associated
14 with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse'
15 function to verify if the password matches the one sent by the client.
14 To use 'checkResponse' you must have already verified the password
15 associated with the 'username' key in 'auth_map' dict. Then you use the
16 'checkResponse' function to verify if the password matches the one sent
17 by the client.
1618
1719 SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
1820 SUPPORTED_QOP - list of supported 'Digest' 'qop'.
2022 __version__ = 1, 0, 1
2123 __author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
2224 __credits__ = """
23 Peter van Kampen for its recipe which implement most of Digest authentication:
25 Peter van Kampen for its recipe which implement most of Digest
26 authentication:
2427 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
2528 """
2629
2831 Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
2932 All rights reserved.
3033
31 Redistribution and use in source and binary forms, with or without modification,
32 are permitted provided that the following conditions are met:
33
34 * Redistributions of source code must retain the above copyright notice,
34 Redistribution and use in source and binary forms, with or without
35 modification, are permitted provided that the following conditions are met:
36
37 * Redistributions of source code must retain the above copyright notice,
3538 this list of conditions and the following disclaimer.
36 * Redistributions in binary form must reproduce the above copyright notice,
37 this list of conditions and the following disclaimer in the documentation
39 * Redistributions in binary form must reproduce the above copyright notice,
40 this list of conditions and the following disclaimer in the documentation
3841 and/or other materials provided with the distribution.
39 * Neither the name of Sylvain Hellegouarch nor the names of his contributors
40 may be used to endorse or promote products derived from this software
41 without specific prior written permission.
42
43 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
44 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
45 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
46 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
47 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
48 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
49 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
50 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
51 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
42 * Neither the name of Sylvain Hellegouarch nor the names of his
43 contributors may be used to endorse or promote products derived from
44 this software without specific prior written permission.
45
46 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
47 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
48 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
49 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
50 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
51 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
52 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
53 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
54 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
5255 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
5356 """
5457
5659 "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
5760 "calculateNonce", "SUPPORTED_QOP")
5861
59 ################################################################################
62 ##########################################################################
6063 import time
6164 from cherrypy._cpcompat import base64_decode, ntob, md5
6265 from cherrypy._cpcompat import parse_http_list, parse_keqv_list
6972 SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
7073 SUPPORTED_QOP = (AUTH, AUTH_INT)
7174
72 ################################################################################
75 ##########################################################################
7376 # doAuth
7477 #
7578 DIGEST_AUTH_ENCODERS = {
7679 MD5: lambda val: md5(ntob(val)).hexdigest(),
7780 MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
78 # SHA: lambda val: sha.new(ntob(val)).hexdigest (),
81 # SHA: lambda val: sha.new(ntob(val)).hexdigest (),
7982 }
8083
81 def calculateNonce (realm, algorithm = MD5):
84
85 def calculateNonce(realm, algorithm=MD5):
8286 """This is an auxaliary function that calculates 'nonce' value. It is used
8387 to handle sessions."""
8488
8892 try:
8993 encoder = DIGEST_AUTH_ENCODERS[algorithm]
9094 except KeyError:
91 raise NotImplementedError ("The chosen algorithm (%s) does not have "\
92 "an implementation yet" % algorithm)
93
94 return encoder ("%d:%s" % (time.time(), realm))
95
96 def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
95 raise NotImplementedError("The chosen algorithm (%s) does not have "
96 "an implementation yet" % algorithm)
97
98 return encoder("%d:%s" % (time.time(), realm))
99
100
101 def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
97102 """Challenges the client for a Digest authentication."""
98103 global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
99104 assert algorithm in SUPPORTED_ALGORITHM
100105 assert qop in SUPPORTED_QOP
101106
102107 if nonce is None:
103 nonce = calculateNonce (realm, algorithm)
108 nonce = calculateNonce(realm, algorithm)
104109
105110 return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
106111 realm, nonce, algorithm, qop
107112 )
108113
109 def basicAuth (realm):
114
115 def basicAuth(realm):
110116 """Challengenes the client for a Basic authentication."""
111117 assert '"' not in realm, "Realms cannot contain the \" (quote) character."
112118
113119 return 'Basic realm="%s"' % realm
114120
115 def doAuth (realm):
121
122 def doAuth(realm):
116123 """'doAuth' function returns the challenge string b giving priority over
117124 Digest and fallback to Basic authentication when the browser doesn't
118125 support the first one.
119
126
120127 This should be set in the HTTP header under the key 'WWW-Authenticate'."""
121128
122 return digestAuth (realm) + " " + basicAuth (realm)
123
124
125 ################################################################################
129 return digestAuth(realm) + " " + basicAuth(realm)
130
131
132 ##########################################################################
126133 # Parse authorization parameters
127134 #
128 def _parseDigestAuthorization (auth_params):
135 def _parseDigestAuthorization(auth_params):
129136 # Convert the auth params to a dict
130137 items = parse_http_list(auth_params)
131138 params = parse_keqv_list(items)
139146 return None
140147
141148 # If qop is sent then cnonce and nc MUST be present
142 if "qop" in params and not ("cnonce" in params \
143 and "nc" in params):
149 if "qop" in params and not ("cnonce" in params
150 and "nc" in params):
144151 return None
145152
146153 # If qop is not sent, neither cnonce nor nc can be present
151158 return params
152159
153160
154 def _parseBasicAuthorization (auth_params):
161 def _parseBasicAuthorization(auth_params):
155162 username, password = base64_decode(auth_params).split(":", 1)
156163 return {"username": username, "password": password}
157164
160167 "digest": _parseDigestAuthorization,
161168 }
162169
163 def parseAuthorization (credentials):
170
171 def parseAuthorization(credentials):
164172 """parseAuthorization will convert the value of the 'Authorization' key in
165173 the HTTP header to a map itself. If the parsing fails 'None' is returned.
166174 """
167175
168176 global AUTH_SCHEMES
169177
170 auth_scheme, auth_params = credentials.split(" ", 1)
171 auth_scheme = auth_scheme.lower ()
178 auth_scheme, auth_params = credentials.split(" ", 1)
179 auth_scheme = auth_scheme.lower()
172180
173181 parser = AUTH_SCHEMES[auth_scheme]
174 params = parser (auth_params)
182 params = parser(auth_params)
175183
176184 if params is None:
177185 return
181189 return params
182190
183191
184 ################################################################################
192 ##########################################################################
185193 # Check provided response for a valid password
186194 #
187 def md5SessionKey (params, password):
188 """
189 If the "algorithm" directive's value is "MD5-sess", then A1
195 def md5SessionKey(params, password):
196 """
197 If the "algorithm" directive's value is "MD5-sess", then A1
190198 [the session key] is calculated only once - on the first request by the
191199 client following receipt of a WWW-Authenticate challenge from the server.
192200
209217 params_copy[key] = params[key]
210218
211219 params_copy["algorithm"] = MD5_SESS
212 return _A1 (params_copy, password)
220 return _A1(params_copy, password)
221
213222
214223 def _A1(params, password):
215 algorithm = params.get ("algorithm", MD5)
224 algorithm = params.get("algorithm", MD5)
216225 H = DIGEST_AUTH_ENCODERS[algorithm]
217226
218227 if algorithm == MD5:
226235 # This is A1 if qop is set
227236 # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
228237 # ":" unq(nonce-value) ":" unq(cnonce-value)
229 h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
238 h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password))
230239 return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
231240
232241
234243 # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
235244 # A2 = Method ":" digest-uri-value
236245
237 qop = params.get ("qop", "auth")
246 qop = params.get("qop", "auth")
238247 if qop == "auth":
239248 return method + ":" + params["uri"]
240249 elif qop == "auth-int":
241250 # If the "qop" value is "auth-int", then A2 is:
242251 # A2 = Method ":" digest-uri-value ":" H(entity-body)
243 entity_body = kwargs.get ("entity_body", "")
252 entity_body = kwargs.get("entity_body", "")
244253 H = kwargs["H"]
245254
246255 return "%s:%s:%s" % (
250259 )
251260
252261 else:
253 raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
254
255 def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
262 raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
263
264
265 def _computeDigestResponse(auth_map, password, method="GET", A1=None,
266 **kwargs):
256267 """
257268 Generates a response respecting the algorithm defined in RFC 2617
258269 """
259270 params = auth_map
260271
261 algorithm = params.get ("algorithm", MD5)
272 algorithm = params.get("algorithm", MD5)
262273
263274 H = DIGEST_AUTH_ENCODERS[algorithm]
264275 KD = lambda secret, data: H(secret + ":" + data)
265276
266 qop = params.get ("qop", None)
277 qop = params.get("qop", None)
267278
268279 H_A2 = H(_A2(params, method, kwargs))
269280
296307
297308 return KD(H_A1, request)
298309
299 def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
310
311 def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
300312 """This function is used to verify the response given by the client when
301313 he tries to authenticate.
302314 Optional arguments:
311323 if auth_map['realm'] != kwargs.get('realm', None):
312324 return False
313325
314 response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
326 response = _computeDigestResponse(
327 auth_map, password, method, A1, **kwargs)
315328
316329 return response == auth_map["response"]
317330
318 def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
331
332 def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
333 **kwargs):
319334 # Note that the Basic response doesn't provide the realm value so we cannot
320335 # test it
321336 try:
328343 "digest": _checkDigestResponse,
329344 }
330345
331 def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
346
347 def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
332348 """'checkResponse' compares the auth_map with the password and optionally
333349 other arguments that each implementation might need.
334
350
335351 If the response is of type 'Basic' then the function has the following
336352 signature::
337
338 checkBasicResponse (auth_map, password) -> bool
339
353
354 checkBasicResponse(auth_map, password) -> bool
355
340356 If the response is of type 'Digest' then the function has the following
341357 signature::
342
343 checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
344
358
359 checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
360
345361 The 'A1' argument is only used in MD5_SESS algorithm based responses.
346362 Check md5SessionKey() for more info.
347363 """
348364 checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
349 return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
350
351
352
353
365 return checker(auth_map, password, method=method, encrypt=encrypt,
366 **kwargs)
77 """
88
99 from binascii import b2a_base64
10 from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
11 from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs
10 from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou
11 from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr
12 from cherrypy._cpcompat import reversed, sorted, unicodestr, unquote_qs
1213 response_codes = BaseHTTPRequestHandler.responses.copy()
1314
14 # From http://www.cherrypy.org/ticket/361
15 # From https://bitbucket.org/cherrypy/cherrypy/issue/361
1516 response_codes[500] = ('Internal Server Error',
16 'The server encountered an unexpected condition '
17 'which prevented it from fulfilling the request.')
17 'The server encountered an unexpected condition '
18 'which prevented it from fulfilling the request.')
1819 response_codes[503] = ('Service Unavailable',
19 'The server is currently unable to handle the '
20 'request due to a temporary overloading or '
21 'maintenance of the server.')
20 'The server is currently unable to handle the '
21 'request due to a temporary overloading or '
22 'maintenance of the server.')
2223
2324 import re
2425 import urllib
2526
2627
27
2828 def urljoin(*atoms):
2929 """Return the given path \*atoms, joined into a single URL.
30
30
3131 This will correctly join a SCRIPT_NAME and PATH_INFO into the
3232 original URL, even if either atom is blank.
3333 """
3737 # Special-case the final url of "", and return "/" instead.
3838 return url or "/"
3939
40
4041 def urljoin_bytes(*atoms):
4142 """Return the given path *atoms, joined into a single URL.
42
43
4344 This will correctly join a SCRIPT_NAME and PATH_INFO into the
4445 original URL, even if either atom is blank.
4546 """
4950 # Special-case the final url of "", and return "/" instead.
5051 return url or ntob("/")
5152
53
5254 def protocol_from_http(protocol_str):
5355 """Return a protocol tuple from the given 'HTTP/x.y' string."""
5456 return int(protocol_str[5]), int(protocol_str[7])
5557
58
5659 def get_ranges(headervalue, content_length):
5760 """Return a list of (start, stop) indices from a Range header, or None.
58
61
5962 Each (start, stop) tuple will be composed of two ints, which are suitable
6063 for use in a slicing operation. That is, the header "Range: bytes=3-6",
6164 if applied against a Python string, is requesting resource[3:7]. This
6265 function will return the list [(3, 7)].
63
66
6467 If this function returns an empty list, you should return HTTP 416.
6568 """
66
69
6770 if not headervalue:
6871 return None
69
72
7073 result = []
7174 bytesunit, byteranges = headervalue.split("=", 1)
7275 for brange in byteranges.split(","):
100103 return None
101104 # Negative subscript (last N bytes)
102105 result.append((content_length - int(stop), content_length))
103
106
104107 return result
105108
106109
107110 class HeaderElement(object):
111
108112 """An element (with parameters) from an HTTP header's element list."""
109
113
110114 def __init__(self, value, params=None):
111115 self.value = value
112116 if params is None:
113117 params = {}
114118 self.params = params
115
119
116120 def __cmp__(self, other):
117121 return cmp(self.value, other.value)
118
122
119123 def __lt__(self, other):
120124 return self.value < other.value
121
125
122126 def __str__(self):
123127 p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
124 return "%s%s" % (self.value, "".join(p))
125
128 return str("%s%s" % (self.value, "".join(p)))
129
126130 def __bytes__(self):
127131 return ntob(self.__str__())
128132
129133 def __unicode__(self):
130134 return ntou(self.__str__())
131
135
132136 def parse(elementstr):
133137 """Transform 'token;key=val' to ('token', {'key': 'val'})."""
134138 # Split the element into a value and parameters. The 'value' may
149153 params[key] = val
150154 return initial_value, params
151155 parse = staticmethod(parse)
152
156
153157 def from_str(cls, elementstr):
154158 """Construct an instance from a string of the form 'token;key=val'."""
155159 ival, params = cls.parse(elementstr)
159163
160164 q_separator = re.compile(r'; *q *=')
161165
166
162167 class AcceptElement(HeaderElement):
168
163169 """An element (with parameters) from an Accept* header's element list.
164
170
165171 AcceptElement objects are comparable; the more-preferred object will be
166172 "less than" the less-preferred object. They are also therefore sortable;
167173 if you sort a list of AcceptElement objects, they will be listed in
168174 priority order; the most preferred value will be first. Yes, it should
169175 have been the other way around, but it's too late to fix now.
170176 """
171
177
172178 def from_str(cls, elementstr):
173179 qvalue = None
174180 # The first "q" parameter (if any) separates the initial
179185 # The qvalue for an Accept header can have extensions. The other
180186 # headers cannot, but it's easier to parse them as if they did.
181187 qvalue = HeaderElement.from_str(atoms[0].strip())
182
188
183189 media_type, params = cls.parse(media_range)
184190 if qvalue is not None:
185191 params["q"] = qvalue
186192 return cls(media_type, params)
187193 from_str = classmethod(from_str)
188
194
189195 def qvalue(self):
190196 val = self.params.get("q", "1")
191197 if isinstance(val, HeaderElement):
192198 val = val.value
193199 return float(val)
194200 qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
195
201
196202 def __cmp__(self, other):
197203 diff = cmp(self.qvalue, other.qvalue)
198204 if diff == 0:
199205 diff = cmp(str(self), str(other))
200206 return diff
201
207
202208 def __lt__(self, other):
203209 if self.qvalue == other.qvalue:
204210 return str(self) < str(other)
205211 else:
206212 return self.qvalue < other.qvalue
207213
208
214 RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
209215 def header_elements(fieldname, fieldvalue):
210 """Return a sorted HeaderElement list from a comma-separated header string."""
216 """Return a sorted HeaderElement list from a comma-separated header string.
217 """
211218 if not fieldvalue:
212219 return []
213
220
214221 result = []
215 for element in fieldvalue.split(","):
222 for element in RE_HEADER_SPLIT.split(fieldvalue):
216223 if fieldname.startswith("Accept") or fieldname == 'TE':
217224 hv = AcceptElement.from_str(element)
218225 else:
219226 hv = HeaderElement.from_str(element)
220227 result.append(hv)
221
228
222229 return list(reversed(sorted(result)))
230
223231
224232 def decode_TEXT(value):
225233 r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
236244 decodedvalue += atom
237245 return decodedvalue
238246
247
239248 def valid_status(status):
240249 """Return legal HTTP status Code, Reason-phrase and Message.
241
250
242251 The status arg must be an int, or a str that begins with an int.
243
252
244253 If status is an int, or a str and no reason-phrase is supplied,
245254 a default reason-phrase will be provided.
246255 """
247
256
248257 if not status:
249258 status = 200
250
259
251260 status = str(status)
252261 parts = status.split(" ", 1)
253262 if len(parts) == 1:
257266 else:
258267 code, reason = parts
259268 reason = reason.strip()
260
269
261270 try:
262271 code = int(code)
263272 except ValueError:
264273 raise ValueError("Illegal response status from server "
265274 "(%s is non-numeric)." % repr(code))
266
275
267276 if code < 100 or code > 599:
268277 raise ValueError("Illegal response status from server "
269278 "(%s is out of range)." % repr(code))
270
279
271280 if code not in response_codes:
272281 # code is unknown but not illegal
273282 default_reason, message = "", ""
274283 else:
275284 default_reason, message = response_codes[code]
276
285
277286 if reason is None:
278287 reason = default_reason
279
288
280289 return code, reason, message
281290
282291
286295
287296 def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
288297 """Parse a query given as a string argument.
289
298
290299 Arguments:
291
300
292301 qs: URL-encoded query string to be parsed
293
302
294303 keep_blank_values: flag indicating whether blank values in
295304 URL encoded queries should be treated as blank strings. A
296305 true value indicates that blanks should be retained as blank
297306 strings. The default false value indicates that blank values
298307 are to be ignored and treated as if they were not included.
299
308
300309 strict_parsing: flag indicating what to do with parsing errors. If
301310 false (the default), errors are silently ignored. If true,
302311 errors raise a ValueError exception.
303
312
304313 Returns a dict, as G-d intended.
305314 """
306315 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
331340
332341 image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
333342
343
334344 def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
335345 """Build a params dictionary from a query_string.
336
346
337347 Duplicate key/value pairs in the provided query_string will be
338348 returned as {'key': [val1, val2, ...]}. Single key/values will
339349 be returned as strings: {'key': 'value'}.
349359
350360
351361 class CaseInsensitiveDict(dict):
362
352363 """A case-insensitive dict subclass.
353
364
354365 Each key is changed on entry to str(key).title().
355366 """
356
367
357368 def __getitem__(self, key):
358369 return dict.__getitem__(self, str(key).title())
359
370
360371 def __setitem__(self, key, value):
361372 dict.__setitem__(self, str(key).title(), value)
362
373
363374 def __delitem__(self, key):
364375 dict.__delitem__(self, str(key).title())
365
376
366377 def __contains__(self, key):
367378 return dict.__contains__(self, str(key).title())
368
379
369380 def get(self, key, default=None):
370381 return dict.get(self, str(key).title(), default)
371
382
372383 if hasattr({}, 'has_key'):
373384 def has_key(self, key):
374 return dict.has_key(self, str(key).title())
375
385 return str(key).title() in self
386
376387 def update(self, E):
377388 for k in E.keys():
378389 self[str(k).title()] = E[k]
379
390
380391 def fromkeys(cls, seq, value=None):
381392 newdict = cls()
382393 for k in seq:
383394 newdict[str(k).title()] = value
384395 return newdict
385396 fromkeys = classmethod(fromkeys)
386
397
387398 def setdefault(self, key, x=None):
388399 key = str(key).title()
389400 try:
391402 except KeyError:
392403 self[key] = x
393404 return x
394
405
395406 def pop(self, key, default):
396407 return dict.pop(self, str(key).title(), default)
397408
403414 # replaced with a single SP before interpretation of the TEXT value."
404415 if nativestr == bytestr:
405416 header_translate_table = ''.join([chr(i) for i in xrange(256)])
406 header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127)
417 header_translate_deletechars = ''.join(
418 [chr(i) for i in xrange(32)]) + chr(127)
407419 else:
408420 header_translate_table = None
409421 header_translate_deletechars = bytes(range(32)) + bytes([127])
410422
411423
412424 class HeaderMap(CaseInsensitiveDict):
425
413426 """A dict subclass for HTTP request and response headers.
414
427
415428 Each key is changed on entry to str(key).title(). This allows headers
416429 to be case-insensitive and avoid duplicates.
417
430
418431 Values are header values (decoded according to :rfc:`2047` if necessary).
419432 """
420
421 protocol=(1, 1)
433
434 protocol = (1, 1)
422435 encodings = ["ISO-8859-1"]
423
436
424437 # Someday, when http-bis is done, this will probably get dropped
425438 # since few servers, clients, or intermediaries do it. But until then,
426439 # we're going to obey the spec as is.
427440 # "Words of *TEXT MAY contain characters from character sets other than
428441 # ISO-8859-1 only when encoded according to the rules of RFC 2047."
429442 use_rfc_2047 = True
430
443
431444 def elements(self, key):
432445 """Return a sorted list of HeaderElements for the given header."""
433446 key = str(key).title()
434447 value = self.get(key)
435448 return header_elements(key, value)
436
449
437450 def values(self, key):
438451 """Return a sorted list of HeaderElement.value for the given header."""
439452 return [e.value for e in self.elements(key)]
440
453
441454 def output(self):
442455 """Transform self into a list of (name, value) tuples."""
443 header_list = []
444 for k, v in self.items():
456 return list(self.encode_header_items(self.items()))
457
458 def encode_header_items(cls, header_items):
459 """
460 Prepare the sequence of name, value tuples into a form suitable for
461 transmitting on the wire for HTTP.
462 """
463 for k, v in header_items:
445464 if isinstance(k, unicodestr):
446 k = self.encode(k)
447
465 k = cls.encode(k)
466
448467 if not isinstance(v, basestring):
449468 v = str(v)
450
469
451470 if isinstance(v, unicodestr):
452 v = self.encode(v)
453
471 v = cls.encode(v)
472
454473 # See header_translate_* constants above.
455474 # Replace only if you really know what you're doing.
456 k = k.translate(header_translate_table, header_translate_deletechars)
457 v = v.translate(header_translate_table, header_translate_deletechars)
458
459 header_list.append((k, v))
460 return header_list
461
462 def encode(self, v):
475 k = k.translate(header_translate_table,
476 header_translate_deletechars)
477 v = v.translate(header_translate_table,
478 header_translate_deletechars)
479
480 yield (k, v)
481 encode_header_items = classmethod(encode_header_items)
482
483 def encode(cls, v):
463484 """Return the given header name or value, encoded for HTTP output."""
464 for enc in self.encodings:
485 for enc in cls.encodings:
465486 try:
466487 return v.encode(enc)
467488 except UnicodeEncodeError:
468489 continue
469
470 if self.protocol == (1, 1) and self.use_rfc_2047:
471 # Encode RFC-2047 TEXT
472 # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
490
491 if cls.protocol == (1, 1) and cls.use_rfc_2047:
492 # Encode RFC-2047 TEXT
493 # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
473494 # We do our own here instead of using the email module
474495 # because we never want to fold lines--folding has
475496 # been deprecated by the HTTP working group.
476497 v = b2a_base64(v.encode('utf-8'))
477498 return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
478
499
479500 raise ValueError("Could not encode header part %r using "
480501 "any of the encodings %r." %
481 (v, self.encodings))
502 (v, cls.encodings))
503 encode = classmethod(encode)
482504
483505
484506 class Host(object):
507
485508 """An internet address.
486
509
487510 name
488511 Should be the client's host name. If not available (because no DNS
489512 lookup is performed), the IP address should be used instead.
490
491 """
492
513
514 """
515
493516 ip = "0.0.0.0"
494517 port = 80
495518 name = "unknown.tld"
496
519
497520 def __init__(self, ip, port, name=None):
498521 self.ip = ip
499522 self.port = port
500523 if name is None:
501524 name = ip
502525 self.name = name
503
526
504527 def __repr__(self):
505528 return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
0 import sys
10 import cherrypy
2 from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode
1 from cherrypy._cpcompat import basestring, ntou, json_encode, json_decode
2
33
44 def json_processor(entity):
55 """Read application/json data into request.json."""
66 if not entity.headers.get(ntou("Content-Length"), ntou("")):
77 raise cherrypy.HTTPError(411)
8
8
99 body = entity.fp.read()
1010 try:
1111 cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
1212 except ValueError:
1313 raise cherrypy.HTTPError(400, 'Invalid JSON document')
1414
15
1516 def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
16 force=True, debug=False, processor = json_processor):
17 force=True, debug=False, processor=json_processor):
1718 """Add a processor to parse JSON request entities:
1819 The default processor places the parsed data into request.json.
1920
2122 be deserialized from JSON to the Python equivalent, and the result
2223 stored at cherrypy.request.json. The 'content_type' argument may
2324 be a Content-Type string or a list of allowable Content-Type strings.
24
25
2526 If the 'force' argument is True (the default), then entities of other
2627 content types will not be allowed; "415 Unsupported Media Type" is
2728 raised instead.
28
29
2930 Supply your own processor to use a custom decoder, or to handle the parsed
3031 data differently. The processor can be configured via
3132 tools.json_in.processor or via the decorator method.
3435 request header, or it will raise "411 Length Required". If for any
3536 other reason the request entity cannot be deserialized from JSON,
3637 it will raise "400 Bad Request: Invalid JSON document".
37
38
3839 You must be using Python 2.6 or greater, or have the 'simplejson'
3940 package importable; otherwise, ValueError is raised during processing.
4041 """
4142 request = cherrypy.serving.request
4243 if isinstance(content_type, basestring):
4344 content_type = [content_type]
44
45
4546 if force:
4647 if debug:
4748 cherrypy.log('Removing body processors %s' %
5051 request.body.default_proc = cherrypy.HTTPError(
5152 415, 'Expected an entity of content type %s' %
5253 ', '.join(content_type))
53
54
5455 for ct in content_type:
5556 if debug:
5657 cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
5758 request.body.processors[ct] = processor
5859
60
5961 def json_handler(*args, **kwargs):
6062 value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
6163 return json_encode(value)
6264
63 def json_out(content_type='application/json', debug=False, handler=json_handler):
65
66 def json_out(content_type='application/json', debug=False,
67 handler=json_handler):
6468 """Wrap request.handler to serialize its output to JSON. Sets Content-Type.
65
69
6670 If the given content_type is None, the Content-Type response header
6771 is not set.
6872
7478 package importable; otherwise, ValueError is raised during processing.
7579 """
7680 request = cherrypy.serving.request
81 # request.handler may be set to None by e.g. the caching tool
82 # to signal to all components that a response body has already
83 # been attached, in which case we don't need to wrap anything.
84 if request.handler is None:
85 return
7786 if debug:
7887 cherrypy.log('Replacing %s with JSON handler' % request.handler,
7988 'TOOLS.JSON_OUT')
8190 request.handler = handler
8291 if content_type is not None:
8392 if debug:
84 cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT')
93 cherrypy.log('Setting Content-Type to %s' %
94 content_type, 'TOOLS.JSON_OUT')
8595 cherrypy.serving.response.headers['Content-Type'] = content_type
86
0 """
1 Platform-independent file locking. Inspired by and modeled after zc.lockfile.
2 """
3
4 import os
5
6 try:
7 import msvcrt
8 except ImportError:
9 pass
10
11 try:
12 import fcntl
13 except ImportError:
14 pass
15
16
17 class LockError(Exception):
18
19 "Could not obtain a lock"
20
21 msg = "Unable to lock %r"
22
23 def __init__(self, path):
24 super(LockError, self).__init__(self.msg % path)
25
26
27 class UnlockError(LockError):
28
29 "Could not release a lock"
30
31 msg = "Unable to unlock %r"
32
33
34 # first, a default, naive locking implementation
35 class LockFile(object):
36
37 """
38 A default, naive locking implementation. Always fails if the file
39 already exists.
40 """
41
42 def __init__(self, path):
43 self.path = path
44 try:
45 fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
46 except OSError:
47 raise LockError(self.path)
48 os.close(fd)
49
50 def release(self):
51 os.remove(self.path)
52
53 def remove(self):
54 pass
55
56
57 class SystemLockFile(object):
58
59 """
60 An abstract base class for platform-specific locking.
61 """
62
63 def __init__(self, path):
64 self.path = path
65
66 try:
67 # Open lockfile for writing without truncation:
68 self.fp = open(path, 'r+')
69 except IOError:
70 # If the file doesn't exist, IOError is raised; Use a+ instead.
71 # Note that there may be a race here. Multiple processes
72 # could fail on the r+ open and open the file a+, but only
73 # one will get the the lock and write a pid.
74 self.fp = open(path, 'a+')
75
76 try:
77 self._lock_file()
78 except:
79 self.fp.seek(1)
80 self.fp.close()
81 del self.fp
82 raise
83
84 self.fp.write(" %s\n" % os.getpid())
85 self.fp.truncate()
86 self.fp.flush()
87
88 def release(self):
89 if not hasattr(self, 'fp'):
90 return
91 self._unlock_file()
92 self.fp.close()
93 del self.fp
94
95 def remove(self):
96 """
97 Attempt to remove the file
98 """
99 try:
100 os.remove(self.path)
101 except:
102 pass
103
104 #@abc.abstract_method
105 # def _lock_file(self):
106 # """Attempt to obtain the lock on self.fp. Raise LockError if not
107 # acquired."""
108
109 def _unlock_file(self):
110 """Attempt to obtain the lock on self.fp. Raise UnlockError if not
111 released."""
112
113
114 class WindowsLockFile(SystemLockFile):
115
116 def _lock_file(self):
117 # Lock just the first byte
118 try:
119 msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
120 except IOError:
121 raise LockError(self.fp.name)
122
123 def _unlock_file(self):
124 try:
125 self.fp.seek(0)
126 msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
127 except IOError:
128 raise UnlockError(self.fp.name)
129
130 if 'msvcrt' in globals():
131 LockFile = WindowsLockFile
132
133
134 class UnixLockFile(SystemLockFile):
135
136 def _lock_file(self):
137 flags = fcntl.LOCK_EX | fcntl.LOCK_NB
138 try:
139 fcntl.flock(self.fp.fileno(), flags)
140 except IOError:
141 raise LockError(self.fp.name)
142
143 # no need to implement _unlock_file, it will be unlocked on close()
144
145 if 'fcntl' in globals():
146 LockFile = UnixLockFile
0 import datetime
1
2
3 class NeverExpires(object):
4 def expired(self):
5 return False
6
7
8 class Timer(object):
9 """
10 A simple timer that will indicate when an expiration time has passed.
11 """
12 def __init__(self, expiration):
13 "Create a timer that expires at `expiration` (UTC datetime)"
14 self.expiration = expiration
15
16 @classmethod
17 def after(cls, elapsed):
18 """
19 Return a timer that will expire after `elapsed` passes.
20 """
21 return cls(datetime.datetime.utcnow() + elapsed)
22
23 def expired(self):
24 return datetime.datetime.utcnow() >= self.expiration
25
26
27 class LockTimeout(Exception):
28 "An exception when a lock could not be acquired before a timeout period"
29
30
31 class LockChecker(object):
32 """
33 Keep track of the time and detect if a timeout has expired
34 """
35 def __init__(self, session_id, timeout):
36 self.session_id = session_id
37 if timeout:
38 self.timer = Timer.after(timeout)
39 else:
40 self.timer = NeverExpires()
41
42 def expired(self):
43 if self.timer.expired():
44 raise LockTimeout(
45 "Timeout acquiring lock for %(session_id)s" % vars(self))
46 return False
55 You can profile any of your pages as follows::
66
77 from cherrypy.lib import profiler
8
8
99 class Root:
1010 p = profile.Profiler("/path/to/profile/dir")
11
11
1212 def index(self):
1313 self.p.run(self._index)
1414 index.exposed = True
15
15
1616 def _index(self):
1717 return "Hello, world!"
18
18
1919 cherrypy.tree.mount(Root())
2020
2121 You can also turn on profiling for all requests
3434
3535
3636 def new_func_strip_path(func_name):
37 """Make profiler output more readable by adding ``__init__`` modules' parents"""
37 """Make profiler output more readable by adding `__init__` modules' parents
38 """
3839 filename, line, name = func_name
3940 if filename.endswith("__init__.py"):
4041 return os.path.basename(filename[:-12]) + filename[-12:], line, name
4849 profile = None
4950 pstats = None
5051
51 import os, os.path
52 import os
53 import os.path
5254 import sys
5355 import warnings
5456
55 from cherrypy._cpcompat import BytesIO
57 from cherrypy._cpcompat import StringIO
5658
5759 _count = 0
5860
61
5962 class Profiler(object):
60
63
6164 def __init__(self, path=None):
6265 if not path:
6366 path = os.path.join(os.path.dirname(__file__), "profile")
6467 self.path = path
6568 if not os.path.exists(path):
6669 os.makedirs(path)
67
70
6871 def run(self, func, *args, **params):
6972 """Dump profile data into self.path."""
7073 global _count
7477 result = prof.runcall(func, *args, **params)
7578 prof.dump_stats(path)
7679 return result
77
80
7881 def statfiles(self):
7982 """:rtype: list of available profiles.
8083 """
8184 return [f for f in os.listdir(self.path)
8285 if f.startswith("cp_") and f.endswith(".prof")]
83
86
8487 def stats(self, filename, sortby='cumulative'):
8588 """:rtype stats(index): output of print_stats() for the given profile.
8689 """
87 sio = BytesIO()
90 sio = StringIO()
8891 if sys.version_info >= (2, 5):
8992 s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
9093 s.strip_dirs()
105108 response = sio.getvalue()
106109 sio.close()
107110 return response
108
111
109112 def index(self):
110113 return """<html>
111114 <head><title>CherryPy profile data</title></head>
116119 </html>
117120 """
118121 index.exposed = True
119
122
120123 def menu(self):
121124 yield "<h2>Profiling runs</h2>"
122125 yield "<p>Click on one of the runs below to see profiling data.</p>"
123126 runs = self.statfiles()
124127 runs.sort()
125128 for i in runs:
126 yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
129 yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
130 i, i)
127131 menu.exposed = True
128
132
129133 def report(self, filename):
130134 import cherrypy
131135 cherrypy.response.headers['Content-Type'] = 'text/plain'
134138
135139
136140 class ProfileAggregator(Profiler):
137
141
138142 def __init__(self, path=None):
139143 Profiler.__init__(self, path)
140144 global _count
141145 self.count = _count = _count + 1
142146 self.profiler = profile.Profile()
143
144 def run(self, func, *args):
147
148 def run(self, func, *args, **params):
145149 path = os.path.join(self.path, "cp_%04d.prof" % self.count)
146 result = self.profiler.runcall(func, *args)
150 result = self.profiler.runcall(func, *args, **params)
147151 self.profiler.dump_stats(path)
148152 return result
149153
150154
151155 class make_app:
156
152157 def __init__(self, nextapp, path=None, aggregate=False):
153158 """Make a WSGI middleware app which wraps 'nextapp' with profiling.
154
159
155160 nextapp
156161 the WSGI application to wrap, usually an instance of
157162 cherrypy.Application.
158
163
159164 path
160165 where to dump the profiling output.
161
166
162167 aggregate
163168 if True, profile data for all HTTP requests will go in
164169 a single file. If False (the default), each HTTP request will
165170 dump its profile data into a separate file.
166
171
167172 """
168173 if profile is None or pstats is None:
169 msg = ("Your installation of Python does not have a profile module. "
170 "If you're on Debian, try `sudo apt-get install python-profiler`. "
171 "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
174 msg = ("Your installation of Python does not have a profile "
175 "module. If you're on Debian, try "
176 "`sudo apt-get install python-profiler`. "
177 "See http://www.cherrypy.org/wiki/ProfilingOnDebian "
178 "for details.")
172179 warnings.warn(msg)
173
180
174181 self.nextapp = nextapp
175182 self.aggregate = aggregate
176183 if aggregate:
177184 self.profiler = ProfileAggregator(path)
178185 else:
179186 self.profiler = Profiler(path)
180
187
181188 def __call__(self, environ, start_response):
182189 def gather():
183190 result = []
190197 def serve(path=None, port=8080):
191198 if profile is None or pstats is None:
192199 msg = ("Your installation of Python does not have a profile module. "
193 "If you're on Debian, try `sudo apt-get install python-profiler`. "
194 "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
200 "If you're on Debian, try "
201 "`sudo apt-get install python-profiler`. "
202 "See http://www.cherrypy.org/wiki/ProfilingOnDebian "
203 "for details.")
195204 warnings.warn(msg)
196
205
197206 import cherrypy
198207 cherrypy.config.update({'server.socket_port': int(port),
199208 'server.thread_pool': 10,
204213
205214 if __name__ == "__main__":
206215 serve(*tuple(sys.argv[1:]))
207
4242
4343 import operator as _operator
4444 import sys
45
4546
4647 def as_dict(config):
4748 """Return a dict from 'config' whether it is a dict, file, or filename."""
5354
5455
5556 class NamespaceSet(dict):
57
5658 """A dict of config namespace names and handlers.
57
59
5860 Each config entry should begin with a namespace name; the corresponding
5961 namespace handler will be called once for each config entry in that
6062 namespace, and will be passed two arguments: the config key (with the
6163 namespace removed) and the config value.
62
64
6365 Namespace handlers may be any Python callable; they may also be
6466 Python 2.5-style 'context managers', in which case their __enter__
6567 method should return a callable to be used as the handler.
6668 See cherrypy.tools (the Toolbox class) for an example.
6769 """
68
70
6971 def __call__(self, config):
7072 """Iterate through config and pass it to each namespace handler.
71
73
7274 config
7375 A flat dict, where keys use dots to separate
7476 namespaces, and values are arbitrary.
75
77
7678 The first name in each config key is used to look up the corresponding
7779 namespace handler. For example, a config entry of {'tools.gzip.on': v}
7880 will call the 'tools' namespace handler with the args: ('gzip.on', v)
8486 ns, name = k.split(".", 1)
8587 bucket = ns_confs.setdefault(ns, {})
8688 bucket[name] = config[k]
87
89
8890 # I chose __enter__ and __exit__ so someday this could be
8991 # rewritten using Python 2.5's 'with' statement:
9092 # for ns, handler in self.iteritems():
115117 else:
116118 for k, v in ns_confs.get(ns, {}).items():
117119 handler(k, v)
118
120
119121 def __repr__(self):
120122 return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
121123 dict.__repr__(self))
122
124
123125 def __copy__(self):
124126 newobj = self.__class__()
125127 newobj.update(self)
128130
129131
130132 class Config(dict):
133
131134 """A dict-like set of configuration data, with defaults and namespaces.
132
135
133136 May take a file, filename, or dict.
134137 """
135
138
136139 defaults = {}
137140 environments = {}
138141 namespaces = NamespaceSet()
139
142
140143 def __init__(self, file=None, **kwargs):
141144 self.reset()
142145 if file is not None:
143146 self.update(file)
144147 if kwargs:
145148 self.update(kwargs)
146
149
147150 def reset(self):
148151 """Reset self to default values."""
149152 self.clear()
150153 dict.update(self, self.defaults)
151
154
152155 def update(self, config):
153156 """Update self from a dict, file or filename."""
154157 if isinstance(config, basestring):
160163 else:
161164 config = config.copy()
162165 self._apply(config)
163
166
164167 def _apply(self, config):
165168 """Update self from a dict."""
166169 which_env = config.get('environment')
169172 for k in env:
170173 if k not in config:
171174 config[k] = env[k]
172
175
173176 dict.update(self, config)
174177 self.namespaces(config)
175
178
176179 def __setitem__(self, k, v):
177180 dict.__setitem__(self, k, v)
178181 self.namespaces({k: v})
179182
180183
181184 class Parser(ConfigParser):
182 """Sub-class of ConfigParser that keeps the case of options and that
185
186 """Sub-class of ConfigParser that keeps the case of options and that
183187 raises an exception if the file cannot be read.
184188 """
185
189
186190 def optionxform(self, optionstr):
187191 return optionstr
188
192
189193 def read(self, filenames):
190194 if isinstance(filenames, basestring):
191195 filenames = [filenames]
199203 self._read(fp, filename)
200204 finally:
201205 fp.close()
202
206
203207 def as_dict(self, raw=False, vars=None):
204208 """Convert an INI file to a dictionary"""
205209 # Load INI file into a dict
219223 raise ValueError(msg, x.__class__.__name__, x.args)
220224 result[section][option] = value
221225 return result
222
226
223227 def dict_from_file(self, file):
224228 if hasattr(file, 'read'):
225229 self.readfp(file)
232236
233237
234238 class _Builder2:
235
239
236240 def build(self, o):
237241 m = getattr(self, 'build_' + o.__class__.__name__, None)
238242 if m is None:
239243 raise TypeError("unrepr does not recognize %s" %
240244 repr(o.__class__.__name__))
241245 return m(o)
242
246
243247 def astnode(self, s):
244248 """Return a Python2 ast Node compiled from a string."""
245249 try:
248252 # Fallback to eval when compiler package is not available,
249253 # e.g. IronPython 1.0.
250254 return eval(s)
251
255
252256 p = compiler.parse("__tempvalue__ = " + s)
253257 return p.getChildren()[1].getChildren()[0].getChildren()[1]
254
258
255259 def build_Subscript(self, o):
256260 expr, flags, subs = o.getChildren()
257261 expr = self.build(expr)
258262 subs = self.build(subs)
259263 return expr[subs]
260
264
261265 def build_CallFunc(self, o):
262 children = map(self.build, o.getChildren())
263 callee = children.pop(0)
264 kwargs = children.pop() or {}
265 starargs = children.pop() or ()
266 args = tuple(children) + tuple(starargs)
266 children = o.getChildren()
267 # Build callee from first child
268 callee = self.build(children[0])
269 # Build args and kwargs from remaining children
270 args = []
271 kwargs = {}
272 for child in children[1:]:
273 class_name = child.__class__.__name__
274 # None is ignored
275 if class_name == 'NoneType':
276 continue
277 # Keywords become kwargs
278 if class_name == 'Keyword':
279 kwargs.update(self.build(child))
280 # Everything else becomes args
281 else :
282 args.append(self.build(child))
267283 return callee(*args, **kwargs)
268
284
285 def build_Keyword(self, o):
286 key, value_obj = o.getChildren()
287 value = self.build(value_obj)
288 kw_dict = {key: value}
289 return kw_dict
290
269291 def build_List(self, o):
270292 return map(self.build, o.getChildren())
271
293
272294 def build_Const(self, o):
273295 return o.value
274
296
275297 def build_Dict(self, o):
276298 d = {}
277299 i = iter(map(self.build, o.getChildren()))
278300 for el in i:
279301 d[el] = i.next()
280302 return d
281
303
282304 def build_Tuple(self, o):
283305 return tuple(self.build_List(o))
284
306
285307 def build_Name(self, o):
286308 name = o.name
287309 if name == 'None':
290312 return True
291313 if name == 'False':
292314 return False
293
315
294316 # See if the Name is a package or module. If it is, import it.
295317 try:
296318 return modules(name)
297319 except ImportError:
298320 pass
299
321
300322 # See if the Name is in builtins.
301323 try:
302324 return getattr(builtins, name)
303325 except AttributeError:
304326 pass
305
327
306328 raise TypeError("unrepr could not resolve the name %s" % repr(name))
307
329
308330 def build_Add(self, o):
309331 left, right = map(self.build, o.getChildren())
310332 return left + right
312334 def build_Mul(self, o):
313335 left, right = map(self.build, o.getChildren())
314336 return left * right
315
337
316338 def build_Getattr(self, o):
317339 parent = self.build(o.expr)
318340 return getattr(parent, o.attrname)
319
341
320342 def build_NoneType(self, o):
321343 return None
322
344
323345 def build_UnarySub(self, o):
324346 return -self.build(o.getChildren()[0])
325
347
326348 def build_UnaryAdd(self, o):
327349 return self.build(o.getChildren()[0])
328350
329351
330352 class _Builder3:
331
353
332354 def build(self, o):
333355 m = getattr(self, 'build_' + o.__class__.__name__, None)
334356 if m is None:
335357 raise TypeError("unrepr does not recognize %s" %
336358 repr(o.__class__.__name__))
337359 return m(o)
338
360
339361 def astnode(self, s):
340362 """Return a Python3 ast Node compiled from a string."""
341363 try:
350372
351373 def build_Subscript(self, o):
352374 return self.build(o.value)[self.build(o.slice)]
353
375
354376 def build_Index(self, o):
355377 return self.build(o.value)
356
378
357379 def build_Call(self, o):
358380 callee = self.build(o.func)
359
381
360382 if o.args is None:
361383 args = ()
362 else:
363 args = tuple([self.build(a) for a in o.args])
364
384 else:
385 args = tuple([self.build(a) for a in o.args])
386
365387 if o.starargs is None:
366388 starargs = ()
367389 else:
368390 starargs = self.build(o.starargs)
369
391
370392 if o.kwargs is None:
371393 kwargs = {}
372394 else:
373395 kwargs = self.build(o.kwargs)
374
396
375397 return callee(*(args + starargs), **kwargs)
376
398
377399 def build_List(self, o):
378400 return list(map(self.build, o.elts))
379
401
380402 def build_Str(self, o):
381403 return o.s
382
404
383405 def build_Num(self, o):
384406 return o.n
385
407
386408 def build_Dict(self, o):
387409 return dict([(self.build(k), self.build(v))
388410 for k, v in zip(o.keys, o.values)])
389
411
390412 def build_Tuple(self, o):
391413 return tuple(self.build_List(o))
392
414
393415 def build_Name(self, o):
394416 name = o.id
395417 if name == 'None':
398420 return True
399421 if name == 'False':
400422 return False
401
423
402424 # See if the Name is a package or module. If it is, import it.
403425 try:
404426 return modules(name)
405427 except ImportError:
406428 pass
407
429
408430 # See if the Name is in builtins.
409431 try:
410432 import builtins
411433 return getattr(builtins, name)
412434 except AttributeError:
413435 pass
414
436
415437 raise TypeError("unrepr could not resolve the name %s" % repr(name))
416
438
439 def build_NameConstant(self, o):
440 return o.value
441
417442 def build_UnaryOp(self, o):
418443 op, operand = map(self.build, [o.op, o.operand])
419444 return op(operand)
420
445
421446 def build_BinOp(self, o):
422 left, op, right = map(self.build, [o.left, o.op, o.right])
447 left, op, right = map(self.build, [o.left, o.op, o.right])
423448 return op(left, right)
424449
425450 def build_Add(self, o):
427452
428453 def build_Mult(self, o):
429454 return _operator.mul
430
455
431456 def build_USub(self, o):
432457 return _operator.neg
433458
453478
454479 def modules(modulePath):
455480 """Load a module and retrieve a reference to that module."""
456 try:
457 mod = sys.modules[modulePath]
458 if mod is None:
459 raise KeyError()
460 except KeyError:
461 # The last [''] is important.
462 mod = __import__(modulePath, globals(), locals(), [''])
463 return mod
481 __import__(modulePath)
482 return sys.modules[modulePath]
483
464484
465485 def attributes(full_attribute_name):
466486 """Load a module and retrieve an attribute of that module."""
467
487
468488 # Parse out the path, module, and attribute
469489 last_dot = full_attribute_name.rfind(".")
470490 attr_name = full_attribute_name[last_dot + 1:]
471491 mod_path = full_attribute_name[:last_dot]
472
492
473493 mod = modules(mod_path)
474494 # Let an AttributeError propagate outward.
475495 try:
477497 except AttributeError:
478498 raise AttributeError("'%s' object has no attribute '%s'"
479499 % (mod_path, attr_name))
480
500
481501 # Return a reference to the attribute.
482502 return attr
483
484
77 tools.sessions.storage_path = "/home/site/sessions"
88 tools.sessions.timeout = 60
99
10 This sets the session to be stored in files in the directory /home/site/sessions,
11 and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions
12 will be saved in RAM. ``tools.sessions.on`` is the only required line for
13 working sessions, the rest are optional.
10 This sets the session to be stored in files in the directory
11 /home/site/sessions, and the session timeout to 60 minutes. If you omit
12 ``storage_type`` the sessions will be saved in RAM.
13 ``tools.sessions.on`` is the only required line for working sessions,
14 the rest are optional.
1415
1516 By default, the session ID is passed in a cookie, so the client's browser must
1617 have cookies enabled for your site.
2425 ================
2526
2627 By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
27 the session is locked early and unlocked late. If you want to control when the
28 session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``.
29 Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
28 the session is locked early and unlocked late. Be mindful of this default mode
29 for any requests that take a long time to process (streaming responses,
30 expensive calculations, database lookups, API calls, etc), as other concurrent
31 requests that also utilize sessions will hang until the session is unlocked.
32
33 If you want to control when the session data is locked and unlocked,
34 set ``tools.sessions.locking = 'explicit'``. Then call
35 ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
3036 Regardless of which mode you use, the session is guaranteed to be unlocked when
3137 the request is complete.
3238
8288 expiration date, although this was on a system with an inaccurate system time.
8389 Maybe FF doesn't trust system time.
8490 """
85
91 import sys
8692 import datetime
8793 import os
88 import random
8994 import time
9095 import threading
9196 import types
92 from warnings import warn
9397
9498 import cherrypy
9599 from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr
96100 from cherrypy.lib import httputil
97
101 from cherrypy.lib import lockfile
102 from cherrypy.lib import locking
103 from cherrypy.lib import is_iterator
98104
99105 missing = object()
100106
107
101108 class Session(object):
109
102110 """A CherryPy dict-like Session object (one per request)."""
103
111
104112 _id = None
105
113
106114 id_observers = None
107115 "A list of callbacks to which to pass new id's."
108
116
109117 def _get_id(self):
110118 return self._id
119
111120 def _set_id(self, value):
112121 self._id = value
113122 for o in self.id_observers:
114123 o(value)
115124 id = property(_get_id, _set_id, doc="The current session ID.")
116
125
117126 timeout = 60
118127 "Number of minutes after which to delete session data."
119
128
120129 locked = False
121130 """
122131 If True, this session instance has exclusive read/write access
123132 to session data."""
124
133
125134 loaded = False
126135 """
127136 If True, data has been retrieved from storage. This should happen
128137 automatically on the first attempt to access session data."""
129
138
130139 clean_thread = None
131140 "Class-level Monitor which calls self.clean_up."
132
141
133142 clean_freq = 5
134143 "The poll rate for expired session cleanup in minutes."
135
144
136145 originalid = None
137146 "The session id passed by the client. May be missing or unsafe."
138
147
139148 missing = False
140149 "True if the session requested by the client did not exist."
141
150
142151 regenerated = False
143152 """
144153 True if the application called session.regenerate(). This is not set by
145154 internal calls to regenerate the session id."""
146
147 debug=False
148
155
156 debug = False
157 "If True, log debug information."
158
159 # --------------------- Session management methods --------------------- #
160
149161 def __init__(self, id=None, **kwargs):
150162 self.id_observers = []
151163 self._data = {}
152
164
153165 for k, v in kwargs.items():
154166 setattr(self, k, v)
155
167
156168 self.originalid = id
157169 self.missing = False
158170 if id is None:
161173 self._regenerate()
162174 else:
163175 self.id = id
164 if not self._exists():
176 if self._exists():
177 if self.debug:
178 cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS')
179 else:
165180 if self.debug:
166181 cherrypy.log('Expired or malicious session %r; '
167182 'making a new one' % id, 'TOOLS.SESSIONS')
168183 # Expired or malicious session. Make a new one.
169 # See http://www.cherrypy.org/ticket/709.
184 # See https://bitbucket.org/cherrypy/cherrypy/issue/709.
170185 self.id = None
171186 self.missing = True
172187 self._regenerate()
183198 """Replace the current session (with a new id)."""
184199 self.regenerated = True
185200 self._regenerate()
186
201
187202 def _regenerate(self):
188203 if self.id is not None:
204 if self.debug:
205 cherrypy.log(
206 'Deleting the existing session %r before '
207 'regeneration.' % self.id,
208 'TOOLS.SESSIONS')
189209 self.delete()
190
210
191211 old_session_was_locked = self.locked
192212 if old_session_was_locked:
193213 self.release_lock()
194
214 if self.debug:
215 cherrypy.log('Old lock released.', 'TOOLS.SESSIONS')
216
195217 self.id = None
196218 while self.id is None:
197219 self.id = self.generate_id()
198220 # Assert that the generated id is not already stored.
199221 if self._exists():
200222 self.id = None
201
223 if self.debug:
224 cherrypy.log('Set id to generated %s.' % self.id,
225 'TOOLS.SESSIONS')
226
202227 if old_session_was_locked:
203228 self.acquire_lock()
204
229 if self.debug:
230 cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS')
231
205232 def clean_up(self):
206233 """Clean up expired sessions."""
207234 pass
208
235
209236 def generate_id(self):
210237 """Return a new session id."""
211238 return random20()
212
239
213240 def save(self):
214241 """Save session data."""
215242 try:
216243 # If session data has never been loaded then it's never been
217244 # accessed: no need to save it
218245 if self.loaded:
219 t = datetime.timedelta(seconds = self.timeout * 60)
246 t = datetime.timedelta(seconds=self.timeout * 60)
220247 expiration_time = self.now() + t
221248 if self.debug:
222 cherrypy.log('Saving with expiry %s' % expiration_time,
249 cherrypy.log('Saving session %r with expiry %s' %
250 (self.id, expiration_time),
223251 'TOOLS.SESSIONS')
224252 self._save(expiration_time)
225
253 else:
254 if self.debug:
255 cherrypy.log(
256 'Skipping save of session %r (no session loaded).' %
257 self.id, 'TOOLS.SESSIONS')
226258 finally:
227259 if self.locked:
228260 # Always release the lock if the user didn't release it
229261 self.release_lock()
230
262 if self.debug:
263 cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS')
264
231265 def load(self):
232266 """Copy stored session data into this session instance."""
233267 data = self._load()
234268 # data is either None or a tuple (session_data, expiration_time)
235269 if data is None or data[1] < self.now():
236270 if self.debug:
237 cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS')
271 cherrypy.log('Expired session %r, flushing data.' % self.id,
272 'TOOLS.SESSIONS')
238273 self._data = {}
239274 else:
275 if self.debug:
276 cherrypy.log('Data loaded for session %r.' % self.id,
277 'TOOLS.SESSIONS')
240278 self._data = data[0]
241279 self.loaded = True
242
280
243281 # Stick the clean_thread in the class, not the instance.
244282 # The instances are created and destroyed per-request.
245283 cls = self.__class__
246284 if self.clean_freq and not cls.clean_thread:
247 # clean_up is in instancemethod and not a classmethod,
285 # clean_up is an instancemethod and not a classmethod,
248286 # so that tool config can be accessed inside the method.
249287 t = cherrypy.process.plugins.Monitor(
250288 cherrypy.engine, self.clean_up, self.clean_freq * 60,
252290 t.subscribe()
253291 cls.clean_thread = t
254292 t.start()
255
293 if self.debug:
294 cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS')
295
256296 def delete(self):
257297 """Delete stored session data."""
258298 self._delete()
259
299 if self.debug:
300 cherrypy.log('Deleted session %s.' % self.id,
301 'TOOLS.SESSIONS')
302
303 # -------------------- Application accessor methods -------------------- #
304
260305 def __getitem__(self, key):
261 if not self.loaded: self.load()
306 if not self.loaded:
307 self.load()
262308 return self._data[key]
263
309
264310 def __setitem__(self, key, value):
265 if not self.loaded: self.load()
311 if not self.loaded:
312 self.load()
266313 self._data[key] = value
267
314
268315 def __delitem__(self, key):
269 if not self.loaded: self.load()
316 if not self.loaded:
317 self.load()
270318 del self._data[key]
271
319
272320 def pop(self, key, default=missing):
273321 """Remove the specified key and return the corresponding value.
274322 If key is not found, default is returned if given,
275323 otherwise KeyError is raised.
276324 """
277 if not self.loaded: self.load()
325 if not self.loaded:
326 self.load()
278327 if default is missing:
279328 return self._data.pop(key)
280329 else:
281330 return self._data.pop(key, default)
282
331
283332 def __contains__(self, key):
284 if not self.loaded: self.load()
333 if not self.loaded:
334 self.load()
285335 return key in self._data
286
336
287337 if hasattr({}, 'has_key'):
288338 def has_key(self, key):
289339 """D.has_key(k) -> True if D has a key k, else False."""
290 if not self.loaded: self.load()
340 if not self.loaded:
341 self.load()
291342 return key in self._data
292
343
293344 def get(self, key, default=None):
294345 """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
295 if not self.loaded: self.load()
346 if not self.loaded:
347 self.load()
296348 return self._data.get(key, default)
297
349
298350 def update(self, d):
299351 """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
300 if not self.loaded: self.load()
352 if not self.loaded:
353 self.load()
301354 self._data.update(d)
302
355
303356 def setdefault(self, key, default=None):
304357 """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
305 if not self.loaded: self.load()
358 if not self.loaded:
359 self.load()
306360 return self._data.setdefault(key, default)
307
361
308362 def clear(self):
309363 """D.clear() -> None. Remove all items from D."""
310 if not self.loaded: self.load()
364 if not self.loaded:
365 self.load()
311366 self._data.clear()
312
367
313368 def keys(self):
314369 """D.keys() -> list of D's keys."""
315 if not self.loaded: self.load()
370 if not self.loaded:
371 self.load()
316372 return self._data.keys()
317
373
318374 def items(self):
319375 """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
320 if not self.loaded: self.load()
376 if not self.loaded:
377 self.load()
321378 return self._data.items()
322
379
323380 def values(self):
324381 """D.values() -> list of D's values."""
325 if not self.loaded: self.load()
382 if not self.loaded:
383 self.load()
326384 return self._data.values()
327385
328386
329387 class RamSession(Session):
330
388
331389 # Class-level objects. Don't rebind these!
332390 cache = {}
333391 locks = {}
334
392
335393 def clean_up(self):
336394 """Clean up expired sessions."""
337395 now = self.now()
345403 del self.locks[id]
346404 except KeyError:
347405 pass
348
406
349407 # added to remove obsolete lock objects
350408 for id in list(self.locks):
351409 if id not in self.cache:
352410 self.locks.pop(id, None)
353
411
354412 def _exists(self):
355413 return self.id in self.cache
356
414
357415 def _load(self):
358416 return self.cache.get(self.id)
359
417
360418 def _save(self, expiration_time):
361419 self.cache[self.id] = (self._data, expiration_time)
362
420
363421 def _delete(self):
364422 self.cache.pop(self.id, None)
365
423
366424 def acquire_lock(self):
367425 """Acquire an exclusive lock on the currently-loaded session data."""
368426 self.locked = True
369427 self.locks.setdefault(self.id, threading.RLock()).acquire()
370
428
371429 def release_lock(self):
372430 """Release the lock on the currently-loaded session data."""
373431 self.locks[self.id].release()
374432 self.locked = False
375
433
376434 def __len__(self):
377435 """Return the number of active sessions."""
378436 return len(self.cache)
379437
380438
381439 class FileSession(Session):
440
382441 """Implementation of the File backend for sessions
383
442
384443 storage_path
385444 The folder where session data will be saved. Each session
386445 will be saved as pickle.dump(data, expiration_time) in its own file;
387446 the filename will be self.SESSION_PREFIX + self.id.
388
447
448 lock_timeout
449 A timedelta or numeric seconds indicating how long
450 to block acquiring a lock. If None (default), acquiring a lock
451 will block indefinitely.
389452 """
390
453
391454 SESSION_PREFIX = 'session-'
392455 LOCK_SUFFIX = '.lock'
393456 pickle_protocol = pickle.HIGHEST_PROTOCOL
394
457
395458 def __init__(self, id=None, **kwargs):
396459 # The 'storage_path' arg is required for file-based sessions.
397460 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
461 kwargs.setdefault('lock_timeout', None)
462
398463 Session.__init__(self, id=id, **kwargs)
399
464
465 # validate self.lock_timeout
466 if isinstance(self.lock_timeout, (int, float)):
467 self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
468 if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
469 raise ValueError("Lock timeout must be numeric seconds or "
470 "a timedelta instance.")
471
400472 def setup(cls, **kwargs):
401473 """Set up the storage system for file-based sessions.
402
474
403475 This should only be called once per process; this will be done
404476 automatically when using sessions.init (as the built-in Tool does).
405477 """
406478 # The 'storage_path' arg is required for file-based sessions.
407479 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
408
480
409481 for k, v in kwargs.items():
410482 setattr(cls, k, v)
411
412 # Warn if any lock files exist at startup.
413 lockfiles = [fname for fname in os.listdir(cls.storage_path)
414 if (fname.startswith(cls.SESSION_PREFIX)
415 and fname.endswith(cls.LOCK_SUFFIX))]
416 if lockfiles:
417 plural = ('', 's')[len(lockfiles) > 1]
418 warn("%s session lockfile%s found at startup. If you are "
419 "only running one process, then you may need to "
420 "manually delete the lockfiles found at %r."
421 % (len(lockfiles), plural, cls.storage_path))
422483 setup = classmethod(setup)
423
484
424485 def _get_file_path(self):
425486 f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
426487 if not os.path.abspath(f).startswith(self.storage_path):
427488 raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
428489 return f
429
490
430491 def _exists(self):
431492 path = self._get_file_path()
432493 return os.path.exists(path)
433
494
434495 def _load(self, path=None):
496 assert self.locked, ("The session load without being locked. "
497 "Check your tools' priority levels.")
435498 if path is None:
436499 path = self._get_file_path()
437500 try:
441504 finally:
442505 f.close()
443506 except (IOError, EOFError):
507 e = sys.exc_info()[1]
508 if self.debug:
509 cherrypy.log("Error loading the session pickle: %s" %
510 e, 'TOOLS.SESSIONS')
444511 return None
445
512
446513 def _save(self, expiration_time):
514 assert self.locked, ("The session was saved without being locked. "
515 "Check your tools' priority levels.")
447516 f = open(self._get_file_path(), "wb")
448517 try:
449518 pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
450519 finally:
451520 f.close()
452
521
453522 def _delete(self):
523 assert self.locked, ("The session deletion without being locked. "
524 "Check your tools' priority levels.")
454525 try:
455526 os.unlink(self._get_file_path())
456527 except OSError:
457528 pass
458
529
459530 def acquire_lock(self, path=None):
460531 """Acquire an exclusive lock on the currently-loaded session data."""
461532 if path is None:
462533 path = self._get_file_path()
463534 path += self.LOCK_SUFFIX
464 while True:
535 checker = locking.LockChecker(self.id, self.lock_timeout)
536 while not checker.expired():
465537 try:
466 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
467 except OSError:
538 self.lock = lockfile.LockFile(path)
539 except lockfile.LockError:
468540 time.sleep(0.1)
469541 else:
470 os.close(lockfd)
471542 break
472543 self.locked = True
473
544 if self.debug:
545 cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
546
474547 def release_lock(self, path=None):
475548 """Release the lock on the currently-loaded session data."""
476 if path is None:
477 path = self._get_file_path()
478 os.unlink(path + self.LOCK_SUFFIX)
549 self.lock.release()
550 self.lock.remove()
479551 self.locked = False
480
552
481553 def clean_up(self):
482554 """Clean up expired sessions."""
483555 now = self.now()
484556 # Iterate over all session files in self.storage_path
485557 for fname in os.listdir(self.storage_path):
486558 if (fname.startswith(self.SESSION_PREFIX)
487 and not fname.endswith(self.LOCK_SUFFIX)):
559 and not fname.endswith(self.LOCK_SUFFIX)):
488560 # We have a session file: lock and load it and check
489561 # if it's expired. If it fails, nevermind.
490562 path = os.path.join(self.storage_path, fname)
491563 self.acquire_lock(path)
564 if self.debug:
565 # This is a bit of a hack, since we're calling clean_up
566 # on the first instance rather than the entire class,
567 # so depending on whether you have "debug" set on the
568 # path of the first session called, this may not run.
569 cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS')
570
492571 try:
493572 contents = self._load(path)
494573 # _load returns None on IOError
499578 os.unlink(path)
500579 finally:
501580 self.release_lock(path)
502
581
503582 def __len__(self):
504583 """Return the number of active sessions."""
505584 return len([fname for fname in os.listdir(self.storage_path)
508587
509588
510589 class PostgresqlSession(Session):
590
511591 """ Implementation of the PostgreSQL backend for sessions. It assumes
512592 a table like this::
513593
516596 data text,
517597 expiration_time timestamp
518598 )
519
599
520600 You must provide your own get_db function.
521601 """
522
602
523603 pickle_protocol = pickle.HIGHEST_PROTOCOL
524
604
525605 def __init__(self, id=None, **kwargs):
526606 Session.__init__(self, id, **kwargs)
527607 self.cursor = self.db.cursor()
528
608
529609 def setup(cls, **kwargs):
530610 """Set up the storage system for Postgres-based sessions.
531
611
532612 This should only be called once per process; this will be done
533613 automatically when using sessions.init (as the built-in Tool does).
534614 """
535615 for k, v in kwargs.items():
536616 setattr(cls, k, v)
537
617
538618 self.db = self.get_db()
539619 setup = classmethod(setup)
540
620
541621 def __del__(self):
542622 if self.cursor:
543623 self.cursor.close()
544624 self.db.commit()
545
625
546626 def _exists(self):
547627 # Select session data from table
548628 self.cursor.execute('select data, expiration_time from session '
549629 'where id=%s', (self.id,))
550630 rows = self.cursor.fetchall()
551631 return bool(rows)
552
632
553633 def _load(self):
554634 # Select session data from table
555635 self.cursor.execute('select data, expiration_time from session '
557637 rows = self.cursor.fetchall()
558638 if not rows:
559639 return None
560
640
561641 pickled_data, expiration_time = rows[0]
562642 data = pickle.loads(pickled_data)
563643 return data, expiration_time
564
644
565645 def _save(self, expiration_time):
566646 pickled_data = pickle.dumps(self._data, self.pickle_protocol)
567647 self.cursor.execute('update session set data = %s, '
568648 'expiration_time = %s where id = %s',
569649 (pickled_data, expiration_time, self.id))
570
650
571651 def _delete(self):
572652 self.cursor.execute('delete from session where id=%s', (self.id,))
573
653
574654 def acquire_lock(self):
575655 """Acquire an exclusive lock on the currently-loaded session data."""
576656 # We use the "for update" clause to lock the row
577657 self.locked = True
578658 self.cursor.execute('select id from session where id=%s for update',
579659 (self.id,))
580
660 if self.debug:
661 cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
662
581663 def release_lock(self):
582664 """Release the lock on the currently-loaded session data."""
583665 # We just close the cursor and that will remove the lock
584666 # introduced by the "for update" clause
585667 self.cursor.close()
586668 self.locked = False
587
669
588670 def clean_up(self):
589671 """Clean up expired sessions."""
590672 self.cursor.execute('delete from session where expiration_time < %s',
592674
593675
594676 class MemcachedSession(Session):
595
677
596678 # The most popular memcached client for Python isn't thread-safe.
597679 # Wrap all .get and .set operations in a single lock.
598680 mc_lock = threading.RLock()
599
681
600682 # This is a seperate set of locks per session id.
601683 locks = {}
602
684
603685 servers = ['127.0.0.1:11211']
604
686
605687 def setup(cls, **kwargs):
606688 """Set up the storage system for memcached-based sessions.
607
689
608690 This should only be called once per process; this will be done
609691 automatically when using sessions.init (as the built-in Tool does).
610692 """
611693 for k, v in kwargs.items():
612694 setattr(cls, k, v)
613
695
614696 import memcache
615697 cls.cache = memcache.Client(cls.servers)
616698 setup = classmethod(setup)
617
699
618700 def _get_id(self):
619701 return self._id
702
620703 def _set_id(self, value):
621704 # This encode() call is where we differ from the superclass.
622705 # Memcache keys MUST be byte strings, not unicode.
627710 for o in self.id_observers:
628711 o(value)
629712 id = property(_get_id, _set_id, doc="The current session ID.")
630
713
631714 def _exists(self):
632715 self.mc_lock.acquire()
633716 try:
634717 return bool(self.cache.get(self.id))
635718 finally:
636719 self.mc_lock.release()
637
720
638721 def _load(self):
639722 self.mc_lock.acquire()
640723 try:
641724 return self.cache.get(self.id)
642725 finally:
643726 self.mc_lock.release()
644
727
645728 def _save(self, expiration_time):
646729 # Send the expiration time as "Unix time" (seconds since 1/1/1970)
647730 td = int(time.mktime(expiration_time.timetuple()))
648731 self.mc_lock.acquire()
649732 try:
650733 if not self.cache.set(self.id, (self._data, expiration_time), td):
651 raise AssertionError("Session data for id %r not set." % self.id)
734 raise AssertionError(
735 "Session data for id %r not set." % self.id)
652736 finally:
653737 self.mc_lock.release()
654
738
655739 def _delete(self):
656740 self.cache.delete(self.id)
657
741
658742 def acquire_lock(self):
659743 """Acquire an exclusive lock on the currently-loaded session data."""
660744 self.locked = True
661745 self.locks.setdefault(self.id, threading.RLock()).acquire()
662
746 if self.debug:
747 cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
748
663749 def release_lock(self):
664750 """Release the lock on the currently-loaded session data."""
665751 self.locks[self.id].release()
666752 self.locked = False
667
753
668754 def __len__(self):
669755 """Return the number of active sessions."""
670756 raise NotImplementedError
674760
675761 def save():
676762 """Save any changed session data."""
677
763
678764 if not hasattr(cherrypy.serving, "session"):
679765 return
680766 request = cherrypy.serving.request
681767 response = cherrypy.serving.response
682
768
683769 # Guard against running twice
684770 if hasattr(request, "_sessionsaved"):
685771 return
686772 request._sessionsaved = True
687
773
688774 if response.stream:
689775 # If the body is being streamed, we have to save the data
690776 # *after* the response has been written out
692778 else:
693779 # If the body is not being streamed, we save the data now
694780 # (so we can release the lock).
695 if isinstance(response.body, types.GeneratorType):
781 if is_iterator(response.body):
696782 response.collapse_body()
697783 cherrypy.session.save()
698784 save.failsafe = True
785
699786
700787 def close():
701788 """Close the session object for this request."""
703790 if getattr(sess, "locked", False):
704791 # If the session is still locked we release the lock
705792 sess.release_lock()
793 if sess.debug:
794 cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
706795 close.failsafe = True
707796 close.priority = 90
708797
711800 timeout=60, domain=None, secure=False, clean_freq=5,
712801 persistent=True, httponly=False, debug=False, **kwargs):
713802 """Initialize session object (using cookies).
714
803
715804 storage_type
716805 One of 'ram', 'file', 'postgresql', 'memcached'. This will be
717806 used to look up the corresponding class in cherrypy.lib.sessions
718807 globals. For example, 'file' will use the FileSession class.
719
808
720809 path
721810 The 'path' value to stick in the response cookie metadata.
722
811
723812 path_header
724813 If 'path' is None (the default), then the response
725814 cookie 'path' will be pulled from request.headers[path_header].
726
815
727816 name
728817 The name of the cookie.
729
818
730819 timeout
731820 The expiration timeout (in minutes) for the stored session data.
732821 If 'persistent' is True (the default), this is also the timeout
733822 for the cookie.
734
823
735824 domain
736825 The cookie domain.
737
826
738827 secure
739828 If False (the default) the cookie 'secure' value will not
740829 be set. If True, the cookie 'secure' value will be set (to 1).
741
830
742831 clean_freq (minutes)
743832 The poll rate for expired session cleanup.
744
833
745834 persistent
746835 If True (the default), the 'timeout' argument will be used
747836 to expire the cookie. If False, the cookie will not have an expiry,
748837 and the cookie will be a "session cookie" which expires when the
749838 browser is closed.
750
839
751840 httponly
752841 If False (the default) the cookie 'httponly' value will not be set.
753842 If True, the cookie 'httponly' value will be set (to 1).
754
843
755844 Any additional kwargs will be bound to the new Session instance,
756845 and may be specific to the storage type. See the subclass of Session
757846 you're using for more information.
758847 """
759
848
760849 request = cherrypy.serving.request
761
850
762851 # Guard against running twice
763852 if hasattr(request, "_session_init_flag"):
764853 return
765854 request._session_init_flag = True
766
855
767856 # Check if request came with a session ID
768857 id = None
769858 if name in request.cookie:
771860 if debug:
772861 cherrypy.log('ID obtained from request.cookie: %r' % id,
773862 'TOOLS.SESSIONS')
774
863
775864 # Find the storage class and call setup (first time only).
776865 storage_class = storage_type.title() + 'Session'
777866 storage_class = globals()[storage_class]
778867 if not hasattr(cherrypy, "session"):
779868 if hasattr(storage_class, "setup"):
780869 storage_class.setup(**kwargs)
781
870
782871 # Create and attach a new Session instance to cherrypy.serving.
783872 # It will possess a reference to (and lock, and lazily load)
784873 # the requested session data.
786875 kwargs['clean_freq'] = clean_freq
787876 cherrypy.serving.session = sess = storage_class(id, **kwargs)
788877 sess.debug = debug
878
789879 def update_cookie(id):
790880 """Update the cookie every time the session id changes."""
791881 cherrypy.serving.response.cookie[name] = id
792882 sess.id_observers.append(update_cookie)
793
883
794884 # Create cherrypy.session which will proxy to cherrypy.serving.session
795885 if not hasattr(cherrypy, "session"):
796886 cherrypy.session = cherrypy._ThreadLocalProxy('session')
797
887
798888 if persistent:
799889 cookie_timeout = timeout
800890 else:
809899 def set_response_cookie(path=None, path_header=None, name='session_id',
810900 timeout=60, domain=None, secure=False, httponly=False):
811901 """Set a response cookie for the client.
812
902
813903 path
814904 the 'path' value to stick in the response cookie metadata.
815905
840930 # Set response cookie
841931 cookie = cherrypy.serving.response.cookie
842932 cookie[name] = cherrypy.serving.session.id
843 cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
844 or '/')
845
933 cookie[name]['path'] = (
934 path or
935 cherrypy.serving.request.headers.get(path_header) or
936 '/'
937 )
938
846939 # We'd like to use the "max-age" param as indicated in
847940 # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
848941 # save it to disk and the session is lost if people close
860953 raise ValueError("The httponly cookie token is not supported.")
861954 cookie[name]['httponly'] = 1
862955
956
863957 def expire():
864958 """Expire the current session cookie."""
865 name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id')
959 name = cherrypy.serving.request.config.get(
960 'tools.sessions.name', 'session_id')
866961 one_year = 60 * 60 * 24 * 365
867962 e = time.time() - one_year
868963 cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
869
870
44 import logging
55 import mimetypes
66 mimetypes.init()
7 mimetypes.types_map['.dwg']='image/x-dwg'
8 mimetypes.types_map['.ico']='image/x-icon'
9 mimetypes.types_map['.bz2']='application/x-bzip2'
10 mimetypes.types_map['.gz']='application/x-gzip'
7 mimetypes.types_map['.dwg'] = 'image/x-dwg'
8 mimetypes.types_map['.ico'] = 'image/x-icon'
9 mimetypes.types_map['.bz2'] = 'application/x-bzip2'
10 mimetypes.types_map['.gz'] = 'application/x-gzip'
1111
1212 import os
1313 import re
1919 from cherrypy.lib import cptools, httputil, file_generator_limited
2020
2121
22 def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
22 def serve_file(path, content_type=None, disposition=None, name=None,
23 debug=False):
2324 """Set status, headers, and body in order to serve the given path.
24
25
2526 The Content-Type header will be set to the content_type arg, if provided.
2627 If not provided, the Content-Type will be guessed by the file extension
2728 of the 'path' argument.
28
29
2930 If disposition is not None, the Content-Disposition header will be set
3031 to "<disposition>; filename=<name>". If name is None, it will be set
3132 to the basename of path. If disposition is None, no Content-Disposition
3233 header will be written.
3334 """
34
35
3536 response = cherrypy.serving.response
36
37
3738 # If path is relative, users should fix it by making path absolute.
3839 # That is, CherryPy should not guess where the application root is.
3940 # It certainly should *not* use cwd (since CP may be invoked from a
4445 if debug:
4546 cherrypy.log(msg, 'TOOLS.STATICFILE')
4647 raise ValueError(msg)
47
48
4849 try:
4950 st = os.stat(path)
5051 except OSError:
5152 if debug:
5253 cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
5354 raise cherrypy.NotFound()
54
55
5556 # Check if path is a directory.
5657 if stat.S_ISDIR(st.st_mode):
5758 # Let the caller deal with it as they like.
5859 if debug:
5960 cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
6061 raise cherrypy.NotFound()
61
62
6263 # Set the Last-Modified response header, so that
6364 # modified-since validation code can work.
6465 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
6566 cptools.validate_since()
66
67
6768 if content_type is None:
6869 # Set content-type based on filename extension
6970 ext = ""
7576 response.headers['Content-Type'] = content_type
7677 if debug:
7778 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
78
79
7980 cd = None
8081 if disposition is not None:
8182 if name is None:
8485 response.headers["Content-Disposition"] = cd
8586 if debug:
8687 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
87
88
8889 # Set Content-Length and use an iterable (file object)
8990 # this way CP won't load the whole file in memory
9091 content_length = st.st_size
9192 fileobj = open(path, 'rb')
9293 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
9394
95
9496 def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
9597 debug=False):
9698 """Set status, headers, and body in order to serve the given file object.
97
99
98100 The Content-Type header will be set to the content_type arg, if provided.
99
101
100102 If disposition is not None, the Content-Disposition header will be set
101103 to "<disposition>; filename=<name>". If name is None, 'filename' will
102104 not be set. If disposition is None, no Content-Disposition header will
109111 serve_fileobj(), expecting that the data would be served starting from that
110112 position.
111113 """
112
114
113115 response = cherrypy.serving.response
114
116
115117 try:
116118 st = os.fstat(fileobj.fileno())
117119 except AttributeError:
126128 response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
127129 cptools.validate_since()
128130 content_length = st.st_size
129
131
130132 if content_type is not None:
131133 response.headers['Content-Type'] = content_type
132134 if debug:
133135 cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
134
136
135137 cd = None
136138 if disposition is not None:
137139 if name is None:
141143 response.headers["Content-Disposition"] = cd
142144 if debug:
143145 cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
144
146
145147 return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
148
146149
147150 def _serve_fileobj(fileobj, content_type, content_length, debug=False):
148151 """Internal. Set response.body to the given file object, perhaps ranged."""
149152 response = cherrypy.serving.response
150
153
151154 # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
152155 request = cherrypy.serving.request
153156 if request.protocol >= (1, 1):
155158 r = httputil.get_ranges(request.headers.get('Range'), content_length)
156159 if r == []:
157160 response.headers['Content-Range'] = "bytes */%s" % content_length
158 message = "Invalid Range (first-byte-pos greater than Content-Length)"
161 message = ("Invalid Range (first-byte-pos greater than "
162 "Content-Length)")
159163 if debug:
160164 cherrypy.log(message, 'TOOLS.STATIC')
161165 raise cherrypy.HTTPError(416, message)
162
166
163167 if r:
164168 if len(r) == 1:
165169 # Return a single-part response.
168172 stop = content_length
169173 r_len = stop - start
170174 if debug:
171 cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
172 'TOOLS.STATIC')
175 cherrypy.log(
176 'Single part; start: %r, stop: %r' % (start, stop),
177 'TOOLS.STATIC')
173178 response.status = "206 Partial Content"
174179 response.headers['Content-Range'] = (
175180 "bytes %s-%s/%s" % (start, stop - 1, content_length))
181186 response.status = "206 Partial Content"
182187 try:
183188 # Python 3
184 from email.generator import _make_boundary as choose_boundary
189 from email.generator import _make_boundary as make_boundary
185190 except ImportError:
186191 # Python 2
187 from mimetools import choose_boundary
188 boundary = choose_boundary()
192 from mimetools import choose_boundary as make_boundary
193 boundary = make_boundary()
189194 ct = "multipart/byteranges; boundary=%s" % boundary
190195 response.headers['Content-Type'] = ct
191196 if "Content-Length" in response.headers:
192197 # Delete Content-Length header so finalize() recalcs it.
193198 del response.headers["Content-Length"]
194
199
195200 def file_ranges():
196201 # Apache compatibility:
197202 yield ntob("\r\n")
198
203
199204 for start, stop in r:
200205 if debug:
201 cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
202 'TOOLS.STATIC')
206 cherrypy.log(
207 'Multipart; start: %r, stop: %r' % (
208 start, stop),
209 'TOOLS.STATIC')
203210 yield ntob("--" + boundary, 'ascii')
204 yield ntob("\r\nContent-type: %s" % content_type, 'ascii')
205 yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
206 % (start, stop - 1, content_length), 'ascii')
211 yield ntob("\r\nContent-type: %s" % content_type,
212 'ascii')
213 yield ntob(
214 "\r\nContent-range: bytes %s-%s/%s\r\n\r\n" % (
215 start, stop - 1, content_length),
216 'ascii')
207217 fileobj.seek(start)
208 for chunk in file_generator_limited(fileobj, stop-start):
218 gen = file_generator_limited(fileobj, stop - start)
219 for chunk in gen:
209220 yield chunk
210221 yield ntob("\r\n")
211222 # Final boundary
212223 yield ntob("--" + boundary + "--", 'ascii')
213
224
214225 # Apache compatibility:
215226 yield ntob("\r\n")
216227 response.body = file_ranges()
218229 else:
219230 if debug:
220231 cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
221
232
222233 # Set Content-Length and use an iterable (file object)
223234 # this way CP won't load the whole file in memory
224235 response.headers['Content-Length'] = content_length
225236 response.body = fileobj
226237 return response.body
238
227239
228240 def serve_download(path, name=None):
229241 """Serve 'path' as an application/x-download attachment."""
251263 cherrypy.log('NotFound', 'TOOLS.STATICFILE')
252264 return False
253265
266
254267 def staticdir(section, dir, root="", match="", content_types=None, index="",
255268 debug=False):
256269 """Serve a static resource from the given (root +) dir.
257
270
258271 match
259272 If given, request.path_info will be searched for the given
260273 regular expression before attempting to serve static content.
261
274
262275 content_types
263276 If given, it should be a Python dictionary of
264277 {file-extension: content-type} pairs, where 'file-extension' is
265278 a string (e.g. "gif") and 'content-type' is the value to write
266279 out in the Content-Type response header (e.g. "image/gif").
267
280
268281 index
269282 If provided, it should be the (relative) name of a file to
270283 serve for directory requests. For example, if the dir argument is
276289 if debug:
277290 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
278291 return False
279
292
280293 if match and not re.search(match, request.path_info):
281294 if debug:
282295 cherrypy.log('request.path_info %r does not match pattern %r' %
283296 (request.path_info, match), 'TOOLS.STATICDIR')
284297 return False
285
298
286299 # Allow the use of '~' to refer to a user's home directory.
287300 dir = os.path.expanduser(dir)
288301
294307 cherrypy.log(msg, 'TOOLS.STATICDIR')
295308 raise ValueError(msg)
296309 dir = os.path.join(root, dir)
297
310
298311 # Determine where we are in the object tree relative to 'section'
299312 # (where the static tool was defined).
300313 if section == 'global':
302315 section = section.rstrip(r"\/")
303316 branch = request.path_info[len(section) + 1:]
304317 branch = unquote(branch.lstrip(r"\/"))
305
318
306319 # If branch is "", filename will end in a slash
307320 filename = os.path.join(dir, branch)
308321 if debug:
309322 cherrypy.log('Checking file %r to fulfill %r' %
310323 (filename, request.path_info), 'TOOLS.STATICDIR')
311
324
312325 # There's a chance that the branch pulled from the URL might
313326 # have ".." or similar uplevel attacks in it. Check that the final
314327 # filename is a child of dir.
315328 if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
316 raise cherrypy.HTTPError(403) # Forbidden
317
329 raise cherrypy.HTTPError(403) # Forbidden
330
318331 handled = _attempt(filename, content_types)
319332 if not handled:
320333 # Check for an index file if a folder was requested.
324337 request.is_index = filename[-1] in (r"\/")
325338 return handled
326339
340
327341 def staticfile(filename, root=None, match="", content_types=None, debug=False):
328342 """Serve a static resource from the given (root +) filename.
329
343
330344 match
331345 If given, request.path_info will be searched for the given
332346 regular expression before attempting to serve static content.
333
347
334348 content_types
335349 If given, it should be a Python dictionary of
336350 {file-extension: content-type} pairs, where 'file-extension' is
337351 a string (e.g. "gif") and 'content-type' is the value to write
338352 out in the Content-Type response header (e.g. "image/gif").
339
353
340354 """
341355 request = cherrypy.serving.request
342356 if request.method not in ('GET', 'HEAD'):
343357 if debug:
344358 cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
345359 return False
346
360
347361 if match and not re.search(match, request.path_info):
348362 if debug:
349363 cherrypy.log('request.path_info %r does not match pattern %r' %
350364 (request.path_info, match), 'TOOLS.STATICFILE')
351365 return False
352
366
353367 # If filename is relative, make absolute using "root".
354368 if not os.path.isabs(filename):
355369 if not root:
356 msg = "Static tool requires an absolute filename (got '%s')." % filename
370 msg = "Static tool requires an absolute filename (got '%s')." % (
371 filename,)
357372 if debug:
358373 cherrypy.log(msg, 'TOOLS.STATICFILE')
359374 raise ValueError(msg)
360375 filename = os.path.join(root, filename)
361
376
362377 return _attempt(filename, content_types, debug=debug)
11
22 import cherrypy
33 from cherrypy._cpcompat import ntob
4
45
56 def get_xmlrpclib():
67 try:
89 except ImportError:
910 import xmlrpclib as x
1011 return x
12
1113
1214 def process_body():
1315 """Return (params, method) from request body."""
4749 encoding=encoding,
4850 allow_none=allow_none))
4951
52
5053 def on_error(*args, **kwargs):
5154 body = str(sys.exc_info()[1])
5255 xmlrpclib = get_xmlrpclib()
5356 _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))
54
66 import time
77 import threading
88
9 from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set
9 from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident
10 from cherrypy._cpcompat import ntob, set, Timer, SetDaemonProperty
1011
1112 # _module__file__base is used by Autoreload to make
1213 # absolute any filenames retrieved from sys.modules which are not
1819 # changes the current directory by executing os.chdir(), then the next time
1920 # Autoreload runs, it will not be able to find any filenames which are
2021 # not absolute paths, because the current directory is not the same as when the
21 # module was first imported. Autoreload will then wrongly conclude the file has
22 # "changed", and initiate the shutdown/re-exec sequence.
22 # module was first imported. Autoreload will then wrongly conclude the file
23 # has "changed", and initiate the shutdown/re-exec sequence.
2324 # See ticket #917.
2425 # For this workaround to have a decent probability of success, this module
2526 # needs to be imported as early as possible, before the app has much chance
2829
2930
3031 class SimplePlugin(object):
32
3133 """Plugin base class which auto-subscribes methods for known channels."""
32
34
3335 bus = None
34 """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
35
36 """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
37 """
38
3639 def __init__(self, bus):
3740 self.bus = bus
38
41
3942 def subscribe(self):
4043 """Register this object as a (multi-channel) listener on the bus."""
4144 for channel in self.bus.listeners:
4346 method = getattr(self, channel, None)
4447 if method is not None:
4548 self.bus.subscribe(channel, method)
46
49
4750 def unsubscribe(self):
4851 """Unregister this object as a listener on the bus."""
4952 for channel in self.bus.listeners:
5356 self.bus.unsubscribe(channel, method)
5457
5558
56
5759 class SignalHandler(object):
60
5861 """Register bus channels (and listeners) for system signals.
59
62
6063 You can modify what signals your application listens for, and what it does
6164 when it receives signals, by modifying :attr:`SignalHandler.handlers`,
6265 a dict of {signal name: callback} pairs. The default set is::
63
66
6467 handlers = {'SIGTERM': self.bus.exit,
6568 'SIGHUP': self.handle_SIGHUP,
6669 'SIGUSR1': self.bus.graceful,
6770 }
68
71
6972 The :func:`SignalHandler.handle_SIGHUP`` method calls
7073 :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
7174 if the process is daemonized, but
7275 :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
7376 if the process is attached to a TTY. This is because Unix window
7477 managers tend to send SIGHUP to terminal windows when the user closes them.
75
76 Feel free to add signals which are not available on every platform. The
77 :class:`SignalHandler` will ignore errors raised from attempting to register
78 handlers for unknown signals.
79 """
80
78
79 Feel free to add signals which are not available on every platform.
80 The :class:`SignalHandler` will ignore errors raised from attempting
81 to register handlers for unknown signals.
82 """
83
8184 handlers = {}
8285 """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
83
86
8487 signals = {}
8588 """A map from signal numbers to names."""
86
89
8790 for k, v in vars(_signal).items():
8891 if k.startswith('SIG') and not k.startswith('SIG_'):
8992 signals[v] = k
9093 del k, v
91
94
9295 def __init__(self, bus):
9396 self.bus = bus
9497 # Set default handlers
105108 self.handlers['SIGINT'] = self._jython_SIGINT_handler
106109
107110 self._previous_handlers = {}
108
111
109112 def _jython_SIGINT_handler(self, signum=None, frame=None):
110113 # See http://bugs.jython.org/issue1313
111114 self.bus.log('Keyboard Interrupt: shutting down bus')
112115 self.bus.exit()
113
116
114117 def subscribe(self):
115118 """Subscribe self.handlers to signals."""
116119 for sig, func in self.handlers.items():
118121 self.set_handler(sig, func)
119122 except ValueError:
120123 pass
121
124
122125 def unsubscribe(self):
123126 """Unsubscribe self.handlers from signals."""
124127 for signum, handler in self._previous_handlers.items():
125128 signame = self.signals[signum]
126
129
127130 if handler is None:
128131 self.bus.log("Restoring %s handler to SIG_DFL." % signame)
129132 handler = _signal.SIG_DFL
130133 else:
131134 self.bus.log("Restoring %s handler %r." % (signame, handler))
132
135
133136 try:
134137 our_handler = _signal.signal(signum, handler)
135138 if our_handler is None:
139142 except ValueError:
140143 self.bus.log("Unable to restore %s handler %r." %
141144 (signame, handler), level=40, traceback=True)
142
145
143146 def set_handler(self, signal, listener=None):
144147 """Subscribe a handler for the given signal (number or name).
145
148
146149 If the optional 'listener' argument is provided, it will be
147150 subscribed as a listener for the given signal's channel.
148
151
149152 If the given signal name or number is not available on the current
150153 platform, ValueError is raised.
151154 """
160163 except KeyError:
161164 raise ValueError("No such signal: %r" % signal)
162165 signum = signal
163
166
164167 prev = _signal.signal(signum, self._handle_signal)
165168 self._previous_handlers[signum] = prev
166
169
167170 if listener is not None:
168171 self.bus.log("Listening for %s." % signame)
169172 self.bus.subscribe(signame, listener)
170
173
171174 def _handle_signal(self, signum=None, frame=None):
172175 """Python signal handler (self.set_handler subscribes it for you)."""
173176 signame = self.signals[signum]
174177 self.bus.log("Caught signal %s." % signame)
175178 self.bus.publish(signame)
176
179
177180 def handle_SIGHUP(self):
178181 """Restart if daemonized, else exit."""
179182 if os.isatty(sys.stdin.fileno()):
186189
187190
188191 try:
189 import pwd, grp
192 import pwd
193 import grp
190194 except ImportError:
191195 pwd, grp = None, None
192196
193197
194198 class DropPrivileges(SimplePlugin):
199
195200 """Drop privileges. uid/gid arguments not available on Windows.
196
197 Special thanks to Gavin Baker: http://antonym.org/node/100.
198 """
199
201
202 Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
203 """
204
200205 def __init__(self, bus, umask=None, uid=None, gid=None):
201206 SimplePlugin.__init__(self, bus)
202207 self.finalized = False
203208 self.uid = uid
204209 self.gid = gid
205210 self.umask = umask
206
211
207212 def _get_uid(self):
208213 return self._uid
214
209215 def _set_uid(self, val):
210216 if val is not None:
211217 if pwd is None:
216222 val = pwd.getpwnam(val)[2]
217223 self._uid = val
218224 uid = property(_get_uid, _set_uid,
219 doc="The uid under which to run. Availability: Unix.")
220
225 doc="The uid under which to run. Availability: Unix.")
226
221227 def _get_gid(self):
222228 return self._gid
229
223230 def _set_gid(self, val):
224231 if val is not None:
225232 if grp is None:
230237 val = grp.getgrnam(val)[2]
231238 self._gid = val
232239 gid = property(_get_gid, _set_gid,
233 doc="The gid under which to run. Availability: Unix.")
234
240 doc="The gid under which to run. Availability: Unix.")
241
235242 def _get_umask(self):
236243 return self._umask
244
237245 def _set_umask(self, val):
238246 if val is not None:
239247 try:
243251 level=30)
244252 val = None
245253 self._umask = val
246 umask = property(_get_umask, _set_umask,
247 doc="""The default permission mode for newly created files and directories.
248
254 umask = property(
255 _get_umask,
256 _set_umask,
257 doc="""The default permission mode for newly created files and
258 directories.
259
249260 Usually expressed in octal format, for example, ``0644``.
250261 Availability: Unix, Windows.
251262 """)
252
263
253264 def start(self):
254265 # uid/gid
255266 def current_ids():
260271 if grp:
261272 group = grp.getgrgid(os.getgid())[0]
262273 return name, group
263
274
264275 if self.finalized:
265276 if not (self.uid is None and self.gid is None):
266277 self.bus.log('Already running as uid: %r gid: %r' %
277288 if self.uid is not None:
278289 os.setuid(self.uid)
279290 self.bus.log('Running as uid: %r gid: %r' % current_ids())
280
291
281292 # umask
282293 if self.finalized:
283294 if self.umask is not None:
289300 old_umask = os.umask(self.umask)
290301 self.bus.log('umask old: %03o, new: %03o' %
291302 (old_umask, self.umask))
292
303
293304 self.finalized = True
294305 # This is slightly higher than the priority for server.start
295306 # in order to facilitate the most common use: starting on a low
298309
299310
300311 class Daemonizer(SimplePlugin):
312
301313 """Daemonize the running script.
302
314
303315 Use this with a Web Site Process Bus via::
304
316
305317 Daemonizer(bus).subscribe()
306
318
307319 When this component finishes, the process is completely decoupled from
308320 the parent environment. Please note that when this component is used,
309321 the return code from the parent process will still be 0 if a startup
313325 of whether the process fully started. In fact, that return code only
314326 indicates if the process succesfully finished the first fork.
315327 """
316
328
317329 def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
318330 stderr='/dev/null'):
319331 SimplePlugin.__init__(self, bus)
321333 self.stdout = stdout
322334 self.stderr = stderr
323335 self.finalized = False
324
336
325337 def start(self):
326338 if self.finalized:
327339 self.bus.log('Already deamonized.')
328
340
329341 # forking has issues with threads:
330342 # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
331343 # "The general problem with making fork() work in a multi-threaded
335347 self.bus.log('There are %r active threads. '
336348 'Daemonizing now may cause strange failures.' %
337349 threading.enumerate(), level=30)
338
350
339351 # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
340352 # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
341353 # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
342
354
343355 # Finish up with the current stdout/stderr
344356 sys.stdout.flush()
345357 sys.stderr.flush()
346
358
347359 # Do first fork.
348360 try:
349361 pid = os.fork()
359371 exc = sys.exc_info()[1]
360372 sys.exit("%s: fork #1 failed: (%d) %s\n"
361373 % (sys.argv[0], exc.errno, exc.strerror))
362
374
363375 os.setsid()
364
376
365377 # Do second fork
366378 try:
367379 pid = os.fork()
368380 if pid > 0:
369381 self.bus.log('Forking twice.')
370 os._exit(0) # Exit second parent
382 os._exit(0) # Exit second parent
371383 except OSError:
372384 exc = sys.exc_info()[1]
373385 sys.exit("%s: fork #2 failed: (%d) %s\n"
374386 % (sys.argv[0], exc.errno, exc.strerror))
375
387
376388 os.chdir("/")
377389 os.umask(0)
378
390
379391 si = open(self.stdin, "r")
380392 so = open(self.stdout, "a+")
381393 se = open(self.stderr, "a+")
386398 os.dup2(si.fileno(), sys.stdin.fileno())
387399 os.dup2(so.fileno(), sys.stdout.fileno())
388400 os.dup2(se.fileno(), sys.stderr.fileno())
389
401
390402 self.bus.log('Daemonized to PID: %s' % os.getpid())
391403 self.finalized = True
392404 start.priority = 65
393405
394406
395407 class PIDFile(SimplePlugin):
408
396409 """Maintain a PID file via a WSPBus."""
397
410
398411 def __init__(self, bus, pidfile):
399412 SimplePlugin.__init__(self, bus)
400413 self.pidfile = pidfile
401414 self.finalized = False
402
415
403416 def start(self):
404417 pid = os.getpid()
405418 if self.finalized:
406419 self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
407420 else:
408 open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8'))
421 open(self.pidfile, "wb").write(ntob("%s\n" % pid, 'utf8'))
409422 self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
410423 self.finalized = True
411424 start.priority = 70
412
425
413426 def exit(self):
414427 try:
415428 os.remove(self.pidfile)
420433 pass
421434
422435
423 class PerpetualTimer(threading._Timer):
424 """A responsive subclass of threading._Timer whose run() method repeats.
425
436 class PerpetualTimer(Timer):
437
438 """A responsive subclass of threading.Timer whose run() method repeats.
439
426440 Use this timer only when you really need a very interruptible timer;
427441 this checks its 'finished' condition up to 20 times a second, which can
428 results in pretty high CPU usage
429 """
430
442 results in pretty high CPU usage
443 """
444
445 def __init__(self, *args, **kwargs):
446 "Override parent constructor to allow 'bus' to be provided."
447 self.bus = kwargs.pop('bus', None)
448 super(PerpetualTimer, self).__init__(*args, **kwargs)
449
431450 def run(self):
432451 while True:
433452 self.finished.wait(self.interval)
436455 try:
437456 self.function(*self.args, **self.kwargs)
438457 except Exception:
439 self.bus.log("Error in perpetual timer thread function %r." %
440 self.function, level=40, traceback=True)
458 if self.bus:
459 self.bus.log(
460 "Error in perpetual timer thread function %r." %
461 self.function, level=40, traceback=True)
441462 # Quit on first error to avoid massive logs.
442463 raise
443464
444465
445 class BackgroundTask(threading.Thread):
466 class BackgroundTask(SetDaemonProperty, threading.Thread):
467
446468 """A subclass of threading.Thread whose run() method repeats.
447
469
448470 Use this class for most repeating tasks. It uses time.sleep() to wait
449471 for each interval, which isn't very responsive; that is, even if you call
450472 self.cancel(), you'll have to wait until the sleep() call finishes before
451473 the thread stops. To compensate, it defaults to being daemonic, which means
452474 it won't delay stopping the whole process.
453475 """
454
476
455477 def __init__(self, interval, function, args=[], kwargs={}, bus=None):
456478 threading.Thread.__init__(self)
457479 self.interval = interval
460482 self.kwargs = kwargs
461483 self.running = False
462484 self.bus = bus
463
485
486 # default to daemonic
487 self.daemon = True
488
464489 def cancel(self):
465490 self.running = False
466
491
467492 def run(self):
468493 self.running = True
469494 while self.running:
478503 % self.function, level=40, traceback=True)
479504 # Quit on first error to avoid massive logs.
480505 raise
481
482 def _set_daemon(self):
483 return True
484506
485507
486508 class Monitor(SimplePlugin):
509
487510 """WSPBus listener to periodically run a callback in its own thread."""
488
511
489512 callback = None
490513 """The function to call at intervals."""
491
514
492515 frequency = 60
493516 """The time in seconds between callback runs."""
494
517
495518 thread = None
496 """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
497
519 """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
520 thread.
521 """
522
498523 def __init__(self, bus, callback, frequency=60, name=None):
499524 SimplePlugin.__init__(self, bus)
500525 self.callback = callback
501526 self.frequency = frequency
502527 self.thread = None
503528 self.name = name
504
529
505530 def start(self):
506531 """Start our callback in its own background thread."""
507532 if self.frequency > 0:
508533 threadname = self.name or self.__class__.__name__
509534 if self.thread is None:
510535 self.thread = BackgroundTask(self.frequency, self.callback,
511 bus = self.bus)
536 bus=self.bus)
512537 self.thread.setName(threadname)
513538 self.thread.start()
514539 self.bus.log("Started monitor thread %r." % threadname)
515540 else:
516541 self.bus.log("Monitor thread %r already started." % threadname)
517542 start.priority = 70
518
543
519544 def stop(self):
520545 """Stop our callback's background task thread."""
521546 if self.thread is None:
522 self.bus.log("No thread running for %s." % self.name or self.__class__.__name__)
547 self.bus.log("No thread running for %s." %
548 self.name or self.__class__.__name__)
523549 else:
524550 if self.thread is not threading.currentThread():
525551 name = self.thread.getName()
529555 self.thread.join()
530556 self.bus.log("Stopped thread %r." % name)
531557 self.thread = None
532
558
533559 def graceful(self):
534560 """Stop the callback's background task thread and restart it."""
535561 self.stop()
537563
538564
539565 class Autoreloader(Monitor):
566
540567 """Monitor which re-executes the process when files change.
541
568
542569 This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
543570 if any of the files it monitors change (or is deleted). By default, the
544571 autoreloader monitors all imported modules; you can add to the
545572 set by adding to ``autoreload.files``::
546
573
547574 cherrypy.engine.autoreload.files.add(myFile)
548
549 If there are imported files you do *not* wish to monitor, you can adjust the
550 ``match`` attribute, a regular expression. For example, to stop monitoring
551 cherrypy itself::
552
575
576 If there are imported files you do *not* wish to monitor, you can
577 adjust the ``match`` attribute, a regular expression. For example,
578 to stop monitoring cherrypy itself::
579
553580 cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
554
581
555582 Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
556583 the autoreload plugin takes a ``frequency`` argument. The default is
557584 1 second; that is, the autoreloader will examine files once each second.
558585 """
559
586
560587 files = None
561588 """The set of files to poll for modifications."""
562
589
563590 frequency = 1
564591 """The interval in seconds at which to poll for modified files."""
565
592
566593 match = '.*'
567594 """A regular expression by which to match filenames."""
568
595
569596 def __init__(self, bus, frequency=1, match='.*'):
570597 self.mtimes = {}
571598 self.files = set()
572599 self.match = match
573600 Monitor.__init__(self, bus, self.run, frequency)
574
601
575602 def start(self):
576603 """Start our own background task thread for self.run."""
577604 if self.thread is None:
578605 self.mtimes = {}
579606 Monitor.start(self)
580 start.priority = 70
581
607 start.priority = 70
608
582609 def sysfiles(self):
583610 """Return a Set of sys.modules filenames to monitor."""
584611 files = set()
585 for k, m in sys.modules.items():
612 for k, m in list(sys.modules.items()):
586613 if re.match(self.match, k):
587 if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'):
614 if (
615 hasattr(m, '__loader__') and
616 hasattr(m.__loader__, 'archive')
617 ):
588618 f = m.__loader__.archive
589619 else:
590620 f = getattr(m, '__file__', None)
591621 if f is not None and not os.path.isabs(f):
592 # ensure absolute paths so a os.chdir() in the app doesn't break me
593 f = os.path.normpath(os.path.join(_module__file__base, f))
622 # ensure absolute paths so a os.chdir() in the app
623 # doesn't break me
624 f = os.path.normpath(
625 os.path.join(_module__file__base, f))
594626 files.add(f)
595627 return files
596
628
597629 def run(self):
598630 """Reload the process if registered files have been modified."""
599631 for filename in self.sysfiles() | self.files:
600632 if filename:
601633 if filename.endswith('.pyc'):
602634 filename = filename[:-1]
603
635
604636 oldtime = self.mtimes.get(filename, 0)
605637 if oldtime is None:
606638 # Module with no .py file. Skip it.
607639 continue
608
640
609641 try:
610642 mtime = os.stat(filename).st_mtime
611643 except OSError:
612644 # Either a module with no .py file, or it's been deleted.
613645 mtime = None
614
646
615647 if filename not in self.mtimes:
616648 # If a module has no .py file, this will be None.
617649 self.mtimes[filename] = mtime
618650 else:
619651 if mtime is None or mtime > oldtime:
620652 # The file has been deleted or modified.
621 self.bus.log("Restarting because %s changed." % filename)
653 self.bus.log("Restarting because %s changed." %
654 filename)
622655 self.thread.cancel()
623 self.bus.log("Stopped thread %r." % self.thread.getName())
656 self.bus.log("Stopped thread %r." %
657 self.thread.getName())
624658 self.bus.restart()
625659 return
626660
627661
628662 class ThreadManager(SimplePlugin):
663
629664 """Manager for HTTP request threads.
630
665
631666 If you have control over thread creation and destruction, publish to
632667 the 'acquire_thread' and 'release_thread' channels (for each thread).
633668 This will register/unregister the current thread and publish to
634669 'start_thread' and 'stop_thread' listeners in the bus as needed.
635
670
636671 If threads are created and destroyed by code you do not control
637672 (e.g., Apache), then, at the beginning of every HTTP request,
638673 publish to 'acquire_thread' only. You should not publish to
640675 the thread will be re-used or not. The bus will call
641676 'stop_thread' listeners for you when it stops.
642677 """
643
678
644679 threads = None
645680 """A map of {thread ident: index number} pairs."""
646
681
647682 def __init__(self, bus):
648683 self.threads = {}
649684 SimplePlugin.__init__(self, bus)
654689
655690 def acquire_thread(self):
656691 """Run 'start_thread' listeners for the current thread.
657
692
658693 If the current thread has already been seen, any 'start_thread'
659694 listeners will not be run again.
660695 """
665700 i = len(self.threads) + 1
666701 self.threads[thread_ident] = i
667702 self.bus.publish('start_thread', i)
668
703
669704 def release_thread(self):
670705 """Release the current thread and run 'stop_thread' listeners."""
671706 thread_ident = get_thread_ident()
672707 i = self.threads.pop(thread_ident, None)
673708 if i is not None:
674709 self.bus.publish('stop_thread', i)
675
710
676711 def stop(self):
677712 """Release all threads and run all 'stop_thread' listeners."""
678713 for thread_ident, i in self.threads.items():
679714 self.bus.publish('stop_thread', i)
680715 self.threads.clear()
681716 graceful = stop
682
1212 with engine.start::
1313
1414 s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
15 s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True))
15 s2 = ServerAdapter(cherrypy.engine,
16 another.HTTPServer(host='127.0.0.1',
17 SSL=True))
1618 s1.subscribe()
1719 s2.subscribe()
1820 cherrypy.engine.start()
5355
5456 #!/usr/bin/python
5557 import cherrypy
56
58
5759 class HelloWorld:
5860 \"""Sample request handler class.\"""
5961 def index(self):
6062 return "Hello world!"
6163 index.exposed = True
62
64
6365 cherrypy.tree.mount(HelloWorld())
6466 # CherryPy autoreload must be disabled for the flup server to work
65 cherrypy.config.update({'engine.autoreload_on':False})
67 cherrypy.config.update({'engine.autoreload.on':False})
6668
6769 Then run :doc:`/deployguide/cherryd` with the '-f' arg::
6870
106108 } # end of $HTTP["url"] =~ "^/"
107109
108110 Please see `Lighttpd FastCGI Docs
109 <http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation
110 of the possible configuration options.
111 <http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
112 an explanation of the possible configuration options.
111113 """
112114
113115 import sys
114116 import time
117 import warnings
115118
116119
117120 class ServerAdapter(object):
121
118122 """Adapter for an HTTP server.
119
123
120124 If you need to start more than one HTTP server (to serve on multiple
121125 ports, or protocols, etc.), you can manually register each one and then
122126 start them all with bus.start:
123
127
124128 s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
125129 s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
126130 s1.subscribe()
127131 s2.subscribe()
128132 bus.start()
129133 """
130
134
131135 def __init__(self, bus, httpserver=None, bind_addr=None):
132136 self.bus = bus
133137 self.httpserver = httpserver
134138 self.bind_addr = bind_addr
135139 self.interrupt = None
136140 self.running = False
137
141
138142 def subscribe(self):
139143 self.bus.subscribe('start', self.start)
140144 self.bus.subscribe('stop', self.stop)
141
145
142146 def unsubscribe(self):
143147 self.bus.unsubscribe('start', self.start)
144148 self.bus.unsubscribe('stop', self.stop)
145
149
146150 def start(self):
147151 """Start the HTTP server."""
148152 if self.bind_addr is None:
149153 on_what = "unknown interface (dynamic?)"
150154 elif isinstance(self.bind_addr, tuple):
151 host, port = self.bind_addr
152 on_what = "%s:%s" % (host, port)
155 on_what = self._get_base()
153156 else:
154157 on_what = "socket file: %s" % self.bind_addr
155
158
156159 if self.running:
157160 self.bus.log("Already serving on %s" % on_what)
158161 return
159
162
160163 self.interrupt = None
161164 if not self.httpserver:
162165 raise ValueError("No HTTP server has been created.")
163
166
164167 # Start the httpserver in a new thread.
165168 if isinstance(self.bind_addr, tuple):
166169 wait_for_free_port(*self.bind_addr)
167
170
168171 import threading
169172 t = threading.Thread(target=self._start_http_thread)
170173 t.setName("HTTPServer " + t.getName())
171174 t.start()
172
175
173176 self.wait()
174177 self.running = True
175178 self.bus.log("Serving on %s" % on_what)
176179 start.priority = 75
177
180
181 def _get_base(self):
182 if not self.httpserver:
183 return ''
184 host, port = self.bind_addr
185 if getattr(self.httpserver, 'ssl_certificate', None):
186 scheme = "https"
187 if port != 443:
188 host += ":%s" % port
189 else:
190 scheme = "http"
191 if port != 80:
192 host += ":%s" % port
193
194 return "%s://%s" % (scheme, host)
195
178196 def _start_http_thread(self):
179197 """HTTP servers MUST be running in new threads, so that the
180198 main thread persists to receive KeyboardInterrupt's. If an
199217 traceback=True, level=40)
200218 self.bus.exit()
201219 raise
202
220
203221 def wait(self):
204222 """Wait until the HTTP server is ready to receive requests."""
205223 while not getattr(self.httpserver, "ready", False):
206224 if self.interrupt:
207225 raise self.interrupt
208226 time.sleep(.1)
209
227
210228 # Wait for port to be occupied
211229 if isinstance(self.bind_addr, tuple):
212230 host, port = self.bind_addr
213231 wait_for_occupied_port(host, port)
214
232
215233 def stop(self):
216234 """Stop the HTTP server."""
217235 if self.running:
225243 else:
226244 self.bus.log("HTTP Server %s already shut down" % self.httpserver)
227245 stop.priority = 25
228
246
229247 def restart(self):
230248 """Restart the HTTP server."""
231249 self.stop()
233251
234252
235253 class FlupCGIServer(object):
254
236255 """Adapter for a flup.server.cgi.WSGIServer."""
237
256
238257 def __init__(self, *args, **kwargs):
239258 self.args = args
240259 self.kwargs = kwargs
241260 self.ready = False
242
261
243262 def start(self):
244263 """Start the CGI server."""
245264 # We have to instantiate the server class here because its __init__
246265 # starts a threadpool. If we do it too early, daemonize won't work.
247266 from flup.server.cgi import WSGIServer
248
267
249268 self.cgiserver = WSGIServer(*self.args, **self.kwargs)
250269 self.ready = True
251270 self.cgiserver.run()
252
271
253272 def stop(self):
254273 """Stop the HTTP server."""
255274 self.ready = False
256275
257276
258277 class FlupFCGIServer(object):
278
259279 """Adapter for a flup.server.fcgi.WSGIServer."""
260
280
261281 def __init__(self, *args, **kwargs):
262282 if kwargs.get('bindAddress', None) is None:
263283 import socket
269289 self.args = args
270290 self.kwargs = kwargs
271291 self.ready = False
272
292
273293 def start(self):
274294 """Start the FCGI server."""
275295 # We have to instantiate the server class here because its __init__
289309 self.fcgiserver._oldSIGs = []
290310 self.ready = True
291311 self.fcgiserver.run()
292
312
293313 def stop(self):
294314 """Stop the HTTP server."""
295315 # Forcibly stop the fcgi server main event loop.
296316 self.fcgiserver._keepGoing = False
297317 # Force all worker threads to die off.
298 self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
318 self.fcgiserver._threadPool.maxSpare = (
319 self.fcgiserver._threadPool._idleCount)
299320 self.ready = False
300321
301322
302323 class FlupSCGIServer(object):
324
303325 """Adapter for a flup.server.scgi.WSGIServer."""
304
326
305327 def __init__(self, *args, **kwargs):
306328 self.args = args
307329 self.kwargs = kwargs
308330 self.ready = False
309
331
310332 def start(self):
311333 """Start the SCGI server."""
312334 # We have to instantiate the server class here because its __init__
326348 self.scgiserver._oldSIGs = []
327349 self.ready = True
328350 self.scgiserver.run()
329
351
330352 def stop(self):
331353 """Stop the HTTP server."""
332354 self.ready = False
343365 return '127.0.0.1'
344366 if server_host in ('::', '::0', '::0.0.0.0'):
345367 # :: is IN6ADDR_ANY, which should answer on localhost.
346 # ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY.
368 # ::0 and ::0.0.0.0 are non-canonical but common
369 # ways to write IN6ADDR_ANY.
347370 return '::1'
348371 return server_host
372
349373
350374 def check_port(host, port, timeout=1.0):
351375 """Raise an error if the given port is not free on the given host."""
353377 raise ValueError("Host values of '' or None are not allowed.")
354378 host = client_host(host)
355379 port = int(port)
356
380
357381 import socket
358
382
359383 # AF_INET or AF_INET6 socket
360384 # Get the correct address family for our host (allows IPv6 addresses)
361385 try:
363387 socket.SOCK_STREAM)
364388 except socket.gaierror:
365389 if ':' in host:
366 info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
390 info = [(
391 socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0)
392 )]
367393 else:
368394 info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
369
395
370396 for res in info:
371397 af, socktype, proto, canonname, sa = res
372398 s = None
377403 s.settimeout(timeout)
378404 s.connect((host, port))
379405 s.close()
406 except socket.error:
407 if s:
408 s.close()
409 else:
380410 raise IOError("Port %s is in use on %s; perhaps the previous "
381411 "httpserver did not shut down properly." %
382412 (repr(port), repr(host)))
383 except socket.error:
384 if s:
385 s.close()
386413
387414
388415 # Feel free to increase these defaults on slow systems:
389416 free_port_timeout = 0.1
390417 occupied_port_timeout = 1.0
418
391419
392420 def wait_for_free_port(host, port, timeout=None):
393421 """Wait for the specified port to become free (drop requests)."""
395423 raise ValueError("Host values of '' or None are not allowed.")
396424 if timeout is None:
397425 timeout = free_port_timeout
398
426
399427 for trial in range(50):
400428 try:
401429 # we are expecting a free port, so reduce the timeout
405433 time.sleep(timeout)
406434 else:
407435 return
408
436
409437 raise IOError("Port %r not free on %r" % (port, host))
438
410439
411440 def wait_for_occupied_port(host, port, timeout=None):
412441 """Wait for the specified port to become active (receive requests)."""
414443 raise ValueError("Host values of '' or None are not allowed.")
415444 if timeout is None:
416445 timeout = occupied_port_timeout
417
446
418447 for trial in range(50):
419448 try:
420449 check_port(host, port, timeout=timeout)
421450 except IOError:
451 # port is occupied
422452 return
423453 else:
424454 time.sleep(timeout)
425
426 raise IOError("Port %r not bound on %r" % (port, host))
455
456 if host == client_host(host):
457 raise IOError("Port %r not bound on %r" % (port, host))
458
459 # On systems where a loopback interface is not available and the
460 # server is bound to all interfaces, it's difficult to determine
461 # whether the server is in fact occupying the port. In this case,
462 # just issue a warning and move on. See issue #1100.
463 msg = "Unable to verify that the server is bound on %r" % port
464 warnings.warn(msg)
1010
1111
1212 class ConsoleCtrlHandler(plugins.SimplePlugin):
13
1314 """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
14
15
1516 def __init__(self, bus):
1617 self.is_set = False
1718 plugins.SimplePlugin.__init__(self, bus)
18
19
1920 def start(self):
2021 if self.is_set:
2122 self.bus.log('Handler for console events already set.', level=40)
2223 return
23
24
2425 result = win32api.SetConsoleCtrlHandler(self.handle, 1)
2526 if result == 0:
2627 self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
2829 else:
2930 self.bus.log('Set handler for console events.', level=40)
3031 self.is_set = True
31
32
3233 def stop(self):
3334 if not self.is_set:
3435 self.bus.log('Handler for console events already off.', level=40)
3536 return
36
37
3738 try:
3839 result = win32api.SetConsoleCtrlHandler(self.handle, 0)
3940 except ValueError:
4041 # "ValueError: The object has not been registered"
4142 result = 1
42
43
4344 if result == 0:
4445 self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
4546 win32api.GetLastError(), level=40)
4647 else:
4748 self.bus.log('Removed handler for console events.', level=40)
4849 self.is_set = False
49
50
5051 def handle(self, event):
5152 """Handle console control events (like Ctrl-C)."""
5253 if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
5354 win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
5455 win32con.CTRL_CLOSE_EVENT):
5556 self.bus.log('Console event %s: shutting down bus' % event)
56
57
5758 # Remove self immediately so repeated Ctrl-C doesn't re-call it.
5859 try:
5960 self.stop()
6061 except ValueError:
6162 pass
62
63
6364 self.bus.exit()
6465 # 'First to return True stops the calls'
6566 return 1
6768
6869
6970 class Win32Bus(wspbus.Bus):
71
7072 """A Web Site Process Bus implementation for Win32.
71
73
7274 Instead of time.sleep, this bus blocks using native win32event objects.
7375 """
74
76
7577 def __init__(self):
7678 self.events = {}
7779 wspbus.Bus.__init__(self)
78
80
7981 def _get_state_event(self, state):
8082 """Return a win32event for the given state (creating it if needed)."""
8183 try:
8688 (state.name, os.getpid()))
8789 self.events[state] = event
8890 return event
89
91
9092 def _get_state(self):
9193 return self._state
94
9295 def _set_state(self, value):
9396 self._state = value
9497 event = self._get_state_event(value)
9598 win32event.PulseEvent(event)
9699 state = property(_get_state, _set_state)
97
100
98101 def wait(self, state, interval=0.1, channel=None):
99102 """Wait for the given state(s), KeyboardInterrupt or SystemExit.
100
103
101104 Since this class uses native win32event objects, the interval
102105 argument is ignored.
103106 """
105108 # Don't wait for an event that beat us to the punch ;)
106109 if self.state not in state:
107110 events = tuple([self._get_state_event(s) for s in state])
108 win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
111 win32event.WaitForMultipleObjects(
112 events, 0, win32event.INFINITE)
109113 else:
110114 # Don't wait for an event that beat us to the punch ;)
111115 if self.state != state:
114118
115119
116120 class _ControlCodes(dict):
121
117122 """Control codes used to "signal" a service via ControlService.
118
123
119124 User-defined control codes are in the range 128-255. We generally use
120125 the standard Python value for the Linux signal and add 128. Example:
121
126
122127 >>> signal.SIGUSR1
123128 10
124129 control_codes['graceful'] = 128 + 10
125130 """
126
131
127132 def key_for(self, obj):
128133 """For the given value, return its corresponding key."""
129134 for key, val in self.items():
144149
145150
146151 class PyWebService(win32serviceutil.ServiceFramework):
152
147153 """Python Web Service."""
148
154
149155 _svc_name_ = "Python Web Service"
150156 _svc_display_name_ = "Python Web Service"
151157 _svc_deps_ = None # sequence of service names on which this depends
152158 _exe_name_ = "pywebsvc"
153159 _exe_args_ = None # Default to no arguments
154
160
155161 # Only exists on Windows 2000 or later, ignored on windows NT
156162 _svc_description_ = "Python Web Service"
157
163
158164 def SvcDoRun(self):
159165 from cherrypy import process
160166 process.bus.start()
161167 process.bus.block()
162
168
163169 def SvcStop(self):
164170 from cherrypy import process
165171 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
166172 process.bus.exit()
167
173
168174 def SvcOther(self, control):
169175 process.bus.publish(control_codes.key_for(control))
170176
7777 # sys.executable is a relative-path, and/or cause other problems).
7878 _startup_cwd = os.getcwd()
7979
80
8081 class ChannelFailures(Exception):
81 """Exception raised when errors occur in a listener during Bus.publish()."""
82
83 """Exception raised when errors occur in a listener during Bus.publish().
84 """
8285 delimiter = '\n'
83
86
8487 def __init__(self, *args, **kwargs):
8588 # Don't use 'super' here; Exceptions are old-style in Py2.4
86 # See http://www.cherrypy.org/ticket/959
89 # See https://bitbucket.org/cherrypy/cherrypy/issue/959
8790 Exception.__init__(self, *args, **kwargs)
8891 self._exceptions = list()
89
92
9093 def handle_exception(self):
9194 """Append the current exception to self."""
9295 self._exceptions.append(sys.exc_info()[1])
93
96
9497 def get_instances(self):
9598 """Return a list of seen exception instances."""
9699 return self._exceptions[:]
97
100
98101 def __str__(self):
99102 exception_strings = map(repr, self.get_instances())
100103 return self.delimiter.join(exception_strings)
106109 __nonzero__ = __bool__
107110
108111 # Use a flag to indicate the state of the bus.
112
113
109114 class _StateEnum(object):
115
110116 class State(object):
111117 name = None
118
112119 def __repr__(self):
113120 return "states.%s" % self.name
114
121
115122 def __setattr__(self, key, value):
116123 if isinstance(value, self.State):
117124 value.name = key
136143
137144
138145 class Bus(object):
146
139147 """Process state-machine and messenger for HTTP site deployment.
140
148
141149 All listeners for a given channel are guaranteed to be called even
142150 if others at the same channel fail. Each failure is logged, but
143151 execution proceeds on to the next listener. The only way to stop all
144152 processing from inside a listener is to raise SystemExit and stop the
145153 whole server.
146154 """
147
155
148156 states = states
149157 state = states.STOPPED
150158 execv = False
151159 max_cloexec_files = max_files
152
160
153161 def __init__(self):
154162 self.execv = False
155163 self.state = states.STOPPED
157165 [(channel, set()) for channel
158166 in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
159167 self._priorities = {}
160
168
161169 def subscribe(self, channel, callback, priority=None):
162170 """Add the given callback at the given channel (if not present)."""
163171 if channel not in self.listeners:
164172 self.listeners[channel] = set()
165173 self.listeners[channel].add(callback)
166
174
167175 if priority is None:
168176 priority = getattr(callback, 'priority', 50)
169177 self._priorities[(channel, callback)] = priority
170
178
171179 def unsubscribe(self, channel, callback):
172180 """Discard the given callback (if present)."""
173181 listeners = self.listeners.get(channel)
174182 if listeners and callback in listeners:
175183 listeners.discard(callback)
176184 del self._priorities[(channel, callback)]
177
185
178186 def publish(self, channel, *args, **kwargs):
179187 """Return output of all subscribers for the given channel."""
180188 if channel not in self.listeners:
181189 return []
182
190
183191 exc = ChannelFailures()
184192 output = []
185
193
186194 items = [(self._priorities[(channel, listener)], listener)
187195 for listener in self.listeners[channel]]
188196 try:
213221 if exc:
214222 raise exc
215223 return output
216
224
217225 def _clean_exit(self):
218226 """An atexit handler which asserts the Bus is not running."""
219227 if self.state != states.EXITING:
223231 "bus.block() after start(), or call bus.exit() before the "
224232 "main thread exits." % self.state, RuntimeWarning)
225233 self.exit()
226
234
227235 def start(self):
228236 """Start all services."""
229237 atexit.register(self._clean_exit)
230
238
231239 self.state = states.STARTING
232240 self.log('Bus STARTING')
233241 try:
247255 pass
248256 # Re-raise the original error
249257 raise e_info
250
258
251259 def exit(self):
252260 """Stop all services and prepare to exit the process."""
253261 exitstate = self.state
254262 try:
255263 self.stop()
256
264
257265 self.state = states.EXITING
258266 self.log('Bus EXITING')
259267 self.publish('exit')
265273 # signal handler, console handler, or atexit handler), so we
266274 # can't just let exceptions propagate out unhandled.
267275 # Assume it's been logged and just die.
268 os._exit(70) # EX_SOFTWARE
269
276 os._exit(70) # EX_SOFTWARE
277
270278 if exitstate == states.STARTING:
271279 # exit() was called before start() finished, possibly due to
272280 # Ctrl-C because a start listener got stuck. In this case,
273281 # we could get stuck in a loop where Ctrl-C never exits the
274282 # process, so we just call os.exit here.
275 os._exit(70) # EX_SOFTWARE
276
283 os._exit(70) # EX_SOFTWARE
284
277285 def restart(self):
278286 """Restart the process (may close connections).
279
287
280288 This method does not restart the process from the calling thread;
281289 instead, it stops the bus and asks the main thread to call execv.
282290 """
283291 self.execv = True
284292 self.exit()
285
293
286294 def graceful(self):
287295 """Advise all services to reload."""
288296 self.log('Bus graceful')
289297 self.publish('graceful')
290
298
291299 def block(self, interval=0.1):
292300 """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
293
301
294302 This function is intended to be called only by the main thread.
295303 After waiting for the EXITING state, it also waits for all threads
296304 to terminate, and then calls os.execv if self.execv is True. This
308316 self.log('SystemExit raised: shutting down bus')
309317 self.exit()
310318 raise
311
319
312320 # Waiting for ALL child threads to finish is necessary on OS X.
313 # See http://www.cherrypy.org/ticket/581.
321 # See https://bitbucket.org/cherrypy/cherrypy/issue/581.
314322 # It's also good to let them all shut down before allowing
315323 # the main thread to call atexit handlers.
316 # See http://www.cherrypy.org/ticket/751.
324 # See https://bitbucket.org/cherrypy/cherrypy/issue/751.
317325 self.log("Waiting for child threads to terminate...")
318326 for t in threading.enumerate():
319 if t != threading.currentThread() and t.isAlive():
327 # Validate the we're not trying to join the MainThread
328 # that will cause a deadlock and the case exist when
329 # implemented as a windows service and in any other case
330 # that another thread executes cherrypy.engine.exit()
331 if (
332 t != threading.currentThread() and
333 t.isAlive() and
334 not isinstance(t, threading._MainThread)
335 ):
320336 # Note that any dummy (external) threads are always daemonic.
321337 if hasattr(threading.Thread, "daemon"):
322338 # Python 2.6+
326342 if not d:
327343 self.log("Waiting for thread %s." % t.getName())
328344 t.join()
329
345
330346 if self.execv:
331347 self._do_execv()
332
348
333349 def wait(self, state, interval=0.1, channel=None):
334350 """Poll for the given state(s) at intervals; publish to channel."""
335351 if isinstance(state, (tuple, list)):
336352 states = state
337353 else:
338354 states = [state]
339
355
340356 def _wait():
341357 while self.state not in states:
342358 time.sleep(interval)
343359 self.publish(channel)
344
360
345361 # From http://psyco.sourceforge.net/psycoguide/bugs.html:
346362 # "The compiled machine code does not include the regular polling
347363 # done by Python, meaning that a KeyboardInterrupt will not be
352368 sys.modules['psyco'].cannotcompile(_wait)
353369 except (KeyError, AttributeError):
354370 pass
355
371
356372 _wait()
357
373
358374 def _do_execv(self):
359375 """Re-execute the current process.
360
376
361377 This must be called from the main thread, because certain platforms
362378 (OS X) don't allow execv to be called in a child thread very well.
363379 """
364380 args = sys.argv[:]
365381 self.log('Re-spawning %s' % ' '.join(args))
366
382
367383 if sys.platform[:4] == 'java':
368384 from _systemrestart import SystemRestart
369385 raise SystemRestart
376392 if self.max_cloexec_files:
377393 self._set_cloexec()
378394 os.execv(sys.executable, args)
379
395
380396 def _set_cloexec(self):
381397 """Set the CLOEXEC flag on all open files (except stdin/out/err).
382
398
383399 If self.max_cloexec_files is an integer (the default), then on
384400 platforms which support it, it represents the max open files setting
385401 for the operating system. This function will be called just before
386402 the process is restarted via os.execv() to prevent open files
387403 from persisting into the new process.
388
404
389405 Set self.max_cloexec_files to 0 to disable this behavior.
390406 """
391 for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
407 for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
392408 try:
393409 flags = fcntl.fcntl(fd, fcntl.F_GETFD)
394410 except IOError:
395411 continue
396412 fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
397
413
398414 def stop(self):
399415 """Stop all services."""
400416 self.state = states.STOPPING
402418 self.publish('stop')
403419 self.state = states.STOPPED
404420 self.log('Bus STOPPED')
405
421
406422 def start_with_callback(self, func, args=None, kwargs=None):
407423 """Start 'func' in a new thread T, then start self (and return T)."""
408424 if args is None:
410426 if kwargs is None:
411427 kwargs = {}
412428 args = (func,) + args
413
429
414430 def _callback(func, *a, **kw):
415431 self.wait(states.STARTED)
416432 func(*a, **kw)
417433 t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
418434 t.setName('Bus Callback ' + t.getName())
419435 t.start()
420
436
421437 self.start()
422
438
423439 return t
424
440
425441 def log(self, msg="", level=20, traceback=False):
426442 """Log the given message. Append the last traceback if requested."""
427443 if traceback:
1919
2020
2121 class Root:
22
22
2323 _cp_config = {'tools.log_tracebacks.on': True,
2424 }
25
25
2626 def index(self):
2727 return """<html>
2828 <body>Try some <a href='%s?a=7'>other</a> path,
3232 </body></html>""" % (url("other"), url("else"),
3333 url("files/made_with_cherrypy_small.png"))
3434 index.exposed = True
35
35
3636 def default(self, *args, **kwargs):
3737 return "args: %s kwargs: %s" % (args, kwargs)
3838 default.exposed = True
39
39
4040 def other(self, a=2, b='bananas', c=None):
4141 cherrypy.response.headers['Content-Type'] = 'text/plain'
4242 if c is None:
4444 else:
4545 return "Have %d %s, %s." % (int(a), b, c)
4646 other.exposed = True
47
47
4848 files = cherrypy.tools.staticdir.handler(
49 section="/files",
50 dir=os.path.join(local_dir, "static"),
51 # Ignore .php files, etc.
49 section="/files",
50 dir=os.path.join(local_dir, "static"),
51 # Ignore .php files, etc.
5252 match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
53 )
53 )
5454
5555
5656 root = Root()
1010 import os
1111 import sys
1212
13
1314 def newexit():
1415 os._exit(1)
16
1517
1618 def setup():
1719 # We want to monkey patch sys.exit so that we can get some
1921 newexit._old = sys.exit
2022 sys.exit = newexit
2123
24
2225 def teardown():
2326 try:
2427 sys.exit = sys.exit._old
44
55
66 class ExposeExamples(object):
7
7
88 @expose
99 def no_call(self):
1010 return "Mr E. R. Bradshaw"
11
11
1212 @expose()
1313 def call_empty(self):
1414 return "Mrs. B.J. Smegma"
15
15
1616 @expose("call_alias")
1717 def nesbitt(self):
1818 return "Mr Nesbitt"
19
19
2020 @expose(["alias1", "alias2"])
2121 def andrews(self):
2222 return "Mr Ken Andrews"
23
23
2424 @expose(alias="alias3")
2525 def watson(self):
2626 return "Mr. and Mrs. Watson"
2727
2828
2929 class ToolExamples(object):
30
30
3131 @expose
3232 @tools.response_headers(headers=[('Content-Type', 'application/data')])
3333 def blah(self):
3636 # the _cp_config attribute added by the Tool decorator. You have
3737 # to write _cp_config[k] = v or _cp_config.update(...) instead.
3838 blah._cp_config['response.stream'] = True
39
40
66
77
88 class Root:
9
9
1010 def index(self):
1111 return "Hello World"
1212 index.exposed = True
13
13
1414 def mtimes(self):
1515 return repr(cherrypy.engine.publish("Autoreloader", "mtimes"))
1616 mtimes.exposed = True
17
17
1818 def pid(self):
1919 return str(os.getpid())
2020 pid.exposed = True
21
21
2222 def start(self):
2323 return repr(starttime)
2424 start.exposed = True
25
25
2626 def exit(self):
2727 # This handler might be called before the engine is STARTED if an
2828 # HTTP worker thread handles it before the HTTP server returns
3131 cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
3232 cherrypy.engine.exit()
3333 exit.exposed = True
34
34
3535
3636 def unsub_sig():
3737 cherrypy.log("unsubsig: %s" % cherrypy.config.get('unsubsig', False))
5656 zerodiv = 1 / 0
5757 cherrypy.engine.subscribe('start', starterror, priority=6)
5858
59
5960 def log_test_case_name():
6061 if cherrypy.config.get('test_case_name', False):
61 cherrypy.log("STARTED FROM: %s" % cherrypy.config.get('test_case_name'))
62 cherrypy.log("STARTED FROM: %s" %
63 cherrypy.config.get('test_case_name'))
6264 cherrypy.engine.subscribe('start', log_test_case_name, priority=6)
6365
6466
00 """CherryPy Benchmark Tool
11
22 Usage:
3 benchmark.py --null --notests --help --cpmodpy --modpython --ab=path --apache=path
4
3 benchmark.py [options]
4
55 --null: use a null Request object (to bench the HTTP server only)
66 --notests: start the server but do not run the tests; this allows
77 you to check the tested pages with a browser
1010 --modpython: run tests via apache on 54583 (with modpython_gateway)
1111 --ab=path: Use the ab script/executable at 'path' (see below)
1212 --apache=path: Use the apache script/exe at 'path' (see below)
13
13
1414 To run the benchmarks, the Apache Benchmark tool "ab" must either be on
1515 your system path, or specified via the --ab=path option.
16
16
1717 To run the modpython tests, the "apache" executable or script must be
1818 on your system path, or provided via the --apache=path option. On some
1919 platforms, "apache" may be called "apachectl" or "apache2ctl"--create
4646
4747 size_cache = {}
4848
49
4950 class Root:
50
51
5152 def index(self):
5253 return """<html>
5354 <head>
6465 </body>
6566 </html>"""
6667 index.exposed = True
67
68
6869 def hello(self):
6970 return "Hello, world\r\n"
7071 hello.exposed = True
71
72
7273 def sizer(self, size):
7374 resp = size_cache.get(size, None)
7475 if resp is None:
8586 'server.max_request_header_size': 0,
8687 'server.max_request_body_size': 0,
8788 'engine.deadlock_poll_freq': 0,
88 })
89 })
8990
9091 # Cheat mode on ;)
9192 del cherrypy.config['tools.log_tracebacks.on']
9798 'tools.staticdir.on': True,
9899 'tools.staticdir.dir': 'static',
99100 'tools.staticdir.root': curdir,
100 },
101 }
101 },
102 }
102103 app = cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf)
103104
104105
105106 class NullRequest:
107
106108 """A null HTTP request class, returning 200 and an empty body."""
107
109
108110 def __init__(self, local, remote, scheme="http"):
109111 pass
110
112
111113 def close(self):
112114 pass
113
115
114116 def run(self, method, path, query_string, protocol, headers, rfile):
115117 cherrypy.response.status = "200 OK"
116118 cherrypy.response.header_list = [("Content-Type", 'text/html'),
127129
128130
129131 class ABSession:
132
130133 """A session of 'ab', the Apache HTTP server benchmarking tool.
131134
132135 Example output from ab:
185188 100% 130 (longest request)
186189 Finished 1000 requests
187190 """
188
189 parse_patterns = [('complete_requests', 'Completed',
190 ntob(r'^Complete requests:\s*(\d+)')),
191 ('failed_requests', 'Failed',
192 ntob(r'^Failed requests:\s*(\d+)')),
193 ('requests_per_second', 'req/sec',
194 ntob(r'^Requests per second:\s*([0-9.]+)')),
195 ('time_per_request_concurrent', 'msec/req',
196 ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
197 ('transfer_rate', 'KB/sec',
198 ntob(r'^Transfer rate:\s*([0-9.]+)')),
199 ]
200
201 def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000, concurrency=10):
191
192 parse_patterns = [
193 ('complete_requests', 'Completed',
194 ntob(r'^Complete requests:\s*(\d+)')),
195 ('failed_requests', 'Failed',
196 ntob(r'^Failed requests:\s*(\d+)')),
197 ('requests_per_second', 'req/sec',
198 ntob(r'^Requests per second:\s*([0-9.]+)')),
199 ('time_per_request_concurrent', 'msec/req',
200 ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
201 ('transfer_rate', 'KB/sec',
202 ntob(r'^Transfer rate:\s*([0-9.]+)'))
203 ]
204
205 def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000,
206 concurrency=10):
202207 self.path = path
203208 self.requests = requests
204209 self.concurrency = concurrency
205
210
206211 def args(self):
207212 port = cherrypy.server.socket_port
208213 assert self.concurrency > 0
209214 assert self.requests > 0
210215 # Don't use "localhost".
211 # Cf http://mail.python.org/pipermail/python-win32/2008-March/007050.html
216 # Cf
217 # http://mail.python.org/pipermail/python-win32/2008-March/007050.html
212218 return ("-k -n %s -c %s http://127.0.0.1:%s%s" %
213219 (self.requests, self.concurrency, port, self.path))
214
220
215221 def run(self):
216222 # Parse output of ab, setting attributes on self
217223 try:
219225 except:
220226 print(_cperror.format_exc())
221227 raise
222
228
223229 for attr, name, pattern in self.parse_patterns:
224230 val = re.search(pattern, self.output, re.MULTILINE)
225231 if val:
239245 sess = ABSession(path)
240246 attrs, names, patterns = list(zip(*sess.parse_patterns))
241247 avg = dict.fromkeys(attrs, 0.0)
242
248
243249 yield ('threads',) + names
244250 for c in concurrency:
245251 sess.concurrency = c
256262 row.append(val)
257263 if row:
258264 yield row
259
265
260266 # Add a row of averages.
261267 yield ["Average"] + [str(avg[attr] / len(concurrency)) for attr in attrs]
262268
269
263270 def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
264 concurrency=50):
271 concurrency=50):
265272 sess = ABSession(concurrency=concurrency)
266273 attrs, names, patterns = list(zip(*sess.parse_patterns))
267274 yield ('bytes',) + names
270277 sess.run()
271278 yield [sz] + [getattr(sess, attr) for attr in attrs]
272279
280
273281 def print_report(rows):
274282 for row in rows:
275283 print("")
281289 def run_standard_benchmarks():
282290 print("")
283291 print("Client Thread Report (1000 requests, 14 byte response body, "
284 "%s server threads):" % cherrypy.server.thread_pool)
292 "%s server threads):" % cherrypy.server.thread_pool)
285293 print_report(thread_report())
286
294
287295 print("")
288296 print("Client Thread Report (1000 requests, 14 bytes via staticdir, "
289 "%s server threads):" % cherrypy.server.thread_pool)
297 "%s server threads):" % cherrypy.server.thread_pool)
290298 print_report(thread_report("%s/static/index.html" % SCRIPT_NAME))
291
299
292300 print("")
293301 print("Size Report (1000 requests, 50 client threads, "
294 "%s server threads):" % cherrypy.server.thread_pool)
302 "%s server threads):" % cherrypy.server.thread_pool)
295303 print_report(size_report())
296304
297305
298306 # modpython and other WSGI #
299307
300308 def startup_modpython(req=None):
301 """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI)."""
309 """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
310 """
302311 if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
303312 if req:
304313 if "nullreq" in req.get_options():
311320 cherrypy.engine.start()
312321 if cherrypy.engine.state == cherrypy._cpengine.STARTING:
313322 cherrypy.engine.wait()
314 return 0 # apache.OK
323 return 0 # apache.OK
315324
316325
317326 def run_modpython(use_wsgi=False):
318327 print("Starting mod_python...")
319328 pyopts = []
320
329
321330 # Pass the null and ab=path options through Apache
322331 if "--null" in opts:
323332 pyopts.append(("nullreq", ""))
324
333
325334 if "--ab" in opts:
326335 pyopts.append(("ab", opts["--ab"]))
327
336
328337 s = _cpmodpy.ModPythonServer
329338 if use_wsgi:
330339 pyopts.append(("wsgi.application", "cherrypy::tree"))
331 pyopts.append(("wsgi.startup", "cherrypy.test.benchmark::startup_modpython"))
340 pyopts.append(
341 ("wsgi.startup", "cherrypy.test.benchmark::startup_modpython"))
332342 handler = "modpython_gateway::handler"
333 s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH, handler=handler)
343 s = s(port=54583, opts=pyopts,
344 apache_path=APACHE_PATH, handler=handler)
334345 else:
335 pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython"))
346 pyopts.append(
347 ("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython"))
336348 s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
337
349
338350 try:
339351 s.start()
340352 run()
341353 finally:
342354 s.stop()
343
344355
345356
346357 if __name__ == '__main__':
352363 except getopt.GetoptError:
353364 print(__doc__)
354365 sys.exit(2)
355
366
356367 if "--help" in opts:
357368 print(__doc__)
358369 sys.exit(0)
359
370
360371 if "--ab" in opts:
361372 AB_PATH = opts['--ab']
362
373
363374 if "--notests" in opts:
364375 # Return without stopping the server, so that the pages
365376 # can be tested from a standard web browser.
366377 def run():
367378 port = cherrypy.server.socket_port
368379 print("You may now open http://127.0.0.1:%s%s/" %
369 (port, SCRIPT_NAME))
370
380 (port, SCRIPT_NAME))
381
371382 if "--null" in opts:
372383 print("Using null Request object")
373384 else:
384395 raise
385396 finally:
386397 cherrypy.engine.exit()
387
398
388399 print("Starting CherryPy app server...")
389
400
390401 class NullWriter(object):
402
391403 """Suppresses the printing of socket errors."""
404
392405 def write(self, data):
393406 pass
394407 sys.stderr = NullWriter()
395
408
396409 start = time.time()
397
410
398411 if "--cpmodpy" in opts:
399412 run_modpython()
400413 elif "--modpython" in opts:
403416 if "--null" in opts:
404417 cherrypy.server.request_class = NullRequest
405418 cherrypy.server.response_class = NullResponse
406
419
407420 cherrypy.engine.start_with_callback(run)
408421 cherrypy.engine.block()
88 import cherrypy
99 thisdir = os.path.dirname(os.path.abspath(__file__))
1010
11
1112 class Root:
1213 pass
1314
1819 },
1920 # This entry should be OK.
2021 '/base/static': {'tools.staticdir.on': True,
21 'tools.staticdir.dir': 'static'},
22 'tools.staticdir.dir': 'static'},
2223 # Warn on missing folder.
2324 '/base/js': {'tools.staticdir.on': True,
24 'tools.staticdir.dir': 'js'},
25 'tools.staticdir.dir': 'js'},
2526 # Warn on dir with an abs path even though we provide root.
2627 '/base/static2': {'tools.staticdir.on': True,
27 'tools.staticdir.dir': '/static'},
28 'tools.staticdir.dir': '/static'},
2829 # Warn on dir with a relative path with no root.
2930 '/static3': {'tools.staticdir.on': True,
3031 'tools.staticdir.dir': 'static'},
1818 from cherrypy.lib.reprconf import unrepr
1919 from cherrypy.test import webtest
2020
21 # Use subprocess module from Python 2.7 on Python 2.3-2.6
22 if sys.version_info < (2, 7):
23 import cherrypy._cpcompat_subprocess as subprocess
24 else:
25 import subprocess
26
2127 import nose
2228
2329 _testconfig = None
2430
25 def get_tst_config(overconf = {}):
31
32 def get_tst_config(overconf={}):
2633 global _testconfig
2734 if _testconfig is None:
2835 conf = {
5057
5158 return conf
5259
60
5361 class Supervisor(object):
62
5463 """Base class for modeling and controlling servers during testing."""
5564
5665 def __init__(self, **kwargs):
6271
6372 log_to_stderr = lambda msg, level: sys.stderr.write(msg + os.linesep)
6473
74
6575 class LocalSupervisor(Supervisor):
66 """Base class for modeling/controlling servers which run in the same process.
76
77 """Base class for modeling/controlling servers which run in the same
78 process.
6779
6880 When the server side runs in a different process, start/stop can dump all
6981 state between each test module easily. When the server side runs in the
70 same process as the client, however, we have to do a bit more work to ensure
71 config and mounted apps are reset between tests.
82 same process as the client, however, we have to do a bit more work to
83 ensure config and mounted apps are reset between tests.
7284 """
7385
7486 using_apache = False
109121 td = getattr(self, 'teardown', None)
110122 if td:
111123 td()
112
124
113125 cherrypy.engine.exit()
114
126
115127 for name, server in copyitems(getattr(cherrypy, 'servers', {})):
116128 server.unsubscribe()
117129 del cherrypy.servers[name]
118130
119131
120132 class NativeServerSupervisor(LocalSupervisor):
133
121134 """Server supervisor for the builtin HTTP server."""
122135
123136 httpserver_class = "cherrypy._cpnative_server.CPHTTPServer"
129142
130143
131144 class LocalWSGISupervisor(LocalSupervisor):
145
132146 """Server supervisor for the builtin WSGI server."""
133147
134148 httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer"
146160 """Obtain a new (decorated) WSGI app to hook into the origin server."""
147161 if app is None:
148162 app = cherrypy.tree
149
163
150164 if self.conquer:
151165 try:
152166 import wsgiconq
153167 except ImportError:
154 warnings.warn("Error importing wsgiconq. pyconquer will not run.")
168 warnings.warn(
169 "Error importing wsgiconq. pyconquer will not run.")
155170 else:
156171 app = wsgiconq.WSGILogger(app, c_calls=True)
157
172
158173 if self.validate:
159174 try:
160175 from wsgiref import validate
161176 except ImportError:
162 warnings.warn("Error importing wsgiref. The validator will not run.")
177 warnings.warn(
178 "Error importing wsgiref. The validator will not run.")
163179 else:
164 #wraps the app in the validator
180 # wraps the app in the validator
165181 app = validate.validator(app)
166
182
167183 return app
168184
169185
172188 sup = modpy.ModPythonSupervisor(**options)
173189 sup.template = modpy.conf_cpmodpy
174190 return sup
191
175192
176193 def get_modpygw_supervisor(**options):
177194 from cherrypy.test import modpy
180197 sup.using_wsgi = True
181198 return sup
182199
200
183201 def get_modwsgi_supervisor(**options):
184202 from cherrypy.test import modwsgi
185203 return modwsgi.ModWSGISupervisor(**options)
186204
205
187206 def get_modfcgid_supervisor(**options):
188207 from cherrypy.test import modfcgid
189208 return modfcgid.ModFCGISupervisor(**options)
190209
210
191211 def get_modfastcgi_supervisor(**options):
192212 from cherrypy.test import modfastcgi
193213 return modfastcgi.ModFCGISupervisor(**options)
194214
215
195216 def get_wsgi_u_supervisor(**options):
196217 cherrypy.server.wsgi_version = ('u', 0)
197218 return LocalWSGISupervisor(**options)
198219
199220
200221 class CPWebCase(webtest.WebCase):
201
222
202223 script_name = ""
203224 scheme = "http"
204225
212233 'modfastcgi': get_modfastcgi_supervisor,
213234 }
214235 default_server = "wsgi"
215
236
216237 def _setup_server(cls, supervisor, conf):
217238 v = sys.version.split()[0]
218239 log.info("Python version used to run this test script: %s" % v)
256277 webtest.WebCase.HTTP_CONN = HTTPSConnection
257278 return baseconf
258279 _setup_server = classmethod(_setup_server)
259
280
260281 def setup_class(cls):
261282 ''
262 #Creates a server
283 # Creates a server
263284 conf = get_tst_config()
264 supervisor_factory = cls.available_servers.get(conf.get('server', 'wsgi'))
285 supervisor_factory = cls.available_servers.get(
286 conf.get('server', 'wsgi'))
265287 if supervisor_factory is None:
266288 raise RuntimeError('Unknown server in config: %s' % conf['server'])
267289 supervisor = supervisor_factory(**conf)
268290
269 #Copied from "run_test_suite"
291 # Copied from "run_test_suite"
270292 cherrypy.config.reset()
271293 baseconf = cls._setup_server(supervisor, conf)
272294 cherrypy.config.update(baseconf)
292314 if hasattr(cls, 'setup_server'):
293315 cls.supervisor.stop()
294316 teardown_class = classmethod(teardown_class)
295
317
296318 do_gc_test = False
297
319
298320 def test_gc(self):
299321 if self.do_gc_test:
300322 self.getPage("/gc/stats")
301323 self.assertBody("Statistics:")
302324 # Tell nose to run this last in each class.
303325 # Prefer sys.maxint for Python 2.3, which didn't have float('inf')
304 test_gc.compat_co_firstlineno = getattr(sys, 'maxint', None) or float('inf')
305
326 test_gc.compat_co_firstlineno = getattr(
327 sys, 'maxint', None) or float('inf')
328
306329 def prefix(self):
307330 return self.script_name.rstrip("/")
308
331
309332 def base(self):
310333 if ((self.scheme == "http" and self.PORT == 80) or
311 (self.scheme == "https" and self.PORT == 443)):
334 (self.scheme == "https" and self.PORT == 443)):
312335 port = ""
313336 else:
314337 port = ":%s" % self.PORT
315
338
316339 return "%s://%s%s%s" % (self.scheme, self.HOST, port,
317340 self.script_name.rstrip("/"))
318
341
319342 def exit(self):
320343 sys.exit()
321
322 def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
344
345 def getPage(self, url, headers=None, method="GET", body=None,
346 protocol=None):
323347 """Open the url. Return status, headers, body."""
324348 if self.script_name:
325349 url = httputil.urljoin(self.script_name, url)
326 return webtest.WebCase.getPage(self, url, headers, method, body, protocol)
327
350 return webtest.WebCase.getPage(self, url, headers, method, body,
351 protocol)
352
328353 def skip(self, msg='skipped '):
329354 raise nose.SkipTest(msg)
330
355
331356 def assertErrorPage(self, status, message=None, pattern=''):
332357 """Compare the response body with a built in error page.
333
358
334359 The function will optionally look for the regexp pattern,
335360 within the exception embedded in the error page."""
336
361
337362 # This will never contain a traceback
338363 page = cherrypy._cperror.get_error_page(status, message=message)
339
364
340365 # First, test the response body without checking the traceback.
341366 # Stick a match-all group (.*) in to grab the traceback.
342 esc = re.escape
343 epage = esc(page)
344 epage = epage.replace(esc('<pre id="traceback"></pre>'),
345 esc('<pre id="traceback">') + '(.*)' + esc('</pre>'))
346 m = re.match(ntob(epage, self.encoding), self.body, re.DOTALL)
367 def esc(text):
368 return re.escape(ntob(text))
369 epage = re.escape(page)
370 epage = epage.replace(
371 esc('<pre id="traceback"></pre>'),
372 esc('<pre id="traceback">') + ntob('(.*)') + esc('</pre>'))
373 m = re.match(epage, self.body, re.DOTALL)
347374 if not m:
348 self._handlewebError('Error page does not match; expected:\n' + page)
375 self._handlewebError(
376 'Error page does not match; expected:\n' + page)
349377 return
350
378
351379 # Now test the pattern against the traceback
352380 if pattern is None:
353381 # Special-case None to mean that there should be *no* traceback.
359387 m.group(1))):
360388 msg = 'Error page does not contain %s in traceback'
361389 self._handlewebError(msg % repr(pattern))
362
390
363391 date_tolerance = 2
364
392
365393 def assertEqualDates(self, dt1, dt2, seconds=None):
366394 """Assert abs(dt1 - dt2) is within Y seconds."""
367395 if seconds is None:
368396 seconds = self.date_tolerance
369
397
370398 if dt1 > dt2:
371399 diff = dt1 - dt2
372400 else:
387415
388416
389417 class CPProcess(object):
390
418
391419 pid_file = os.path.join(thisdir, 'test.pid')
392420 config_file = os.path.join(thisdir, 'test.conf')
393421 config_template = """[global]
402430 """
403431 error_log = os.path.join(thisdir, 'test.error.log')
404432 access_log = os.path.join(thisdir, 'test.access.log')
405
406 def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None):
433
434 def __init__(self, wait=False, daemonize=False, ssl=False,
435 socket_host=None, socket_port=None):
407436 self.wait = wait
408437 self.daemonize = daemonize
409438 self.ssl = ssl
410439 self.host = socket_host or cherrypy.server.socket_host
411440 self.port = socket_port or cherrypy.server.socket_port
412
441
413442 def write_conf(self, extra=""):
414443 if self.ssl:
415444 serverpem = os.path.join(thisdir, 'test.pem')
419448 """ % (serverpem, serverpem)
420449 else:
421450 ssl = ""
422
451
423452 conf = self.config_template % {
424453 'host': self.host,
425454 'port': self.port,
427456 'access_log': self.access_log,
428457 'ssl': ssl,
429458 'extra': extra,
430 }
459 }
431460 f = open(self.config_file, 'wb')
432461 f.write(ntob(conf, 'utf-8'))
433462 f.close()
434
463
435464 def start(self, imports=None):
436465 """Start cherryd in a subprocess."""
437466 cherrypy._cpserver.wait_for_free_port(self.host, self.port)
438
439 args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'),
440 '-c', self.config_file, '-p', self.pid_file]
441
467
468 args = [
469 os.path.join(thisdir, '..', 'cherryd'),
470 '-c', self.config_file,
471 '-p', self.pid_file,
472 ]
473
442474 if not isinstance(imports, (list, tuple)):
443475 imports = [imports]
444476 for i in imports:
445477 if i:
446478 args.append('-i')
447479 args.append(i)
448
480
449481 if self.daemonize:
450482 args.append('-d')
451483
452484 env = os.environ.copy()
453 # Make sure we import the cherrypy package in which this module is defined.
485 # Make sure we import the cherrypy package in which this module is
486 # defined.
454487 grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
455488 if env.get('PYTHONPATH', ''):
456 env['PYTHONPATH'] = os.pathsep.join((grandparentdir, env['PYTHONPATH']))
489 env['PYTHONPATH'] = os.pathsep.join(
490 (grandparentdir, env['PYTHONPATH']))
457491 else:
458492 env['PYTHONPATH'] = grandparentdir
493 self._proc = subprocess.Popen([sys.executable] + args, env=env)
459494 if self.wait:
460 self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env)
461 else:
462 os.spawnve(os.P_NOWAIT, sys.executable, args, env)
495 self.exit_code = self._proc.wait()
496 else:
463497 cherrypy._cpserver.wait_for_occupied_port(self.host, self.port)
464
498
465499 # Give the engine a wee bit more time to finish STARTING
466500 if self.daemonize:
467501 time.sleep(2)
468502 else:
469503 time.sleep(1)
470
504
471505 def get_pid(self):
472 return int(open(self.pid_file, 'rb').read())
473
506 if self.daemonize:
507 return int(open(self.pid_file, 'rb').read())
508 return self._proc.pid
509
474510 def join(self):
475511 """Wait for the process to exit."""
512 if self.daemonize:
513 return self._join_daemon()
514 self._proc.wait()
515
516 def _join_daemon(self):
476517 try:
477518 try:
478519 # Mac, UNIX
490531 x = sys.exc_info()[1]
491532 if x.args != (10, 'No child processes'):
492533 raise
493
99 try:
1010 # On Windows, msvcrt.getch reads a single char without output.
1111 import msvcrt
12
1213 def getchar():
1314 return msvcrt.getch()
1415 except ImportError:
1516 # Unix getchr
16 import tty, termios
17 import tty
18 import termios
19
1720 def getchar():
1821 fd = sys.stdin.fileno()
1922 old_settings = termios.tcgetattr(fd)
2629
2730
2831 class LogCase(object):
32
2933 """unittest.TestCase mixin for testing log messages.
30
34
3135 logfile: a filename for the desired log. Yes, I know modes are evil,
3236 but it makes the test functions so much cleaner to set this once.
33
37
3438 lastmarker: the last marker in the log. This can be used to search for
3539 messages since the last marker.
36
40
3741 markerPrefix: a string with which to prefix log markers. This should be
3842 unique enough from normal log output to use for marker identification.
3943 """
40
44
4145 logfile = None
4246 lastmarker = None
4347 markerPrefix = ntob("test suite marker: ")
44
48
4549 def _handleLogError(self, msg, data, marker, pattern):
4650 print("")
4751 print(" ERROR: %s" % msg)
48
52
4953 if not self.interactive:
5054 raise self.failureException(msg)
51
52 p = " Show: [L]og [M]arker [P]attern; [I]gnore, [R]aise, or sys.e[X]it >> "
55
56 p = (" Show: "
57 "[L]og [M]arker [P]attern; "
58 "[I]gnore, [R]aise, or sys.e[X]it >> ")
5359 sys.stdout.write(p + ' ')
5460 # ARGH
5561 sys.stdout.flush()
8187 elif i == "X":
8288 self.exit()
8389 sys.stdout.write(p + ' ')
84
90
8591 def exit(self):
8692 sys.exit()
87
93
8894 def emptyLog(self):
8995 """Overwrite self.logfile with 0 bytes."""
9096 open(self.logfile, 'wb').write("")
91
97
9298 def markLog(self, key=None):
9399 """Insert a marker line into the log and set self.lastmarker."""
94100 if key is None:
95101 key = str(time.time())
96102 self.lastmarker = key
97
98 open(self.logfile, 'ab+').write(ntob("%s%s\n" % (self.markerPrefix, key),"utf-8"))
99
103
104 open(self.logfile, 'ab+').write(
105 ntob("%s%s\n" % (self.markerPrefix, key), "utf-8"))
106
100107 def _read_marked_region(self, marker=None):
101108 """Return lines from self.logfile in the marked region.
102
109
103110 If marker is None, self.lastmarker is used. If the log hasn't
104111 been marked (using self.markLog), the entire log will be returned.
105112 """
106 ## # Give the logger time to finish writing?
107 ## time.sleep(0.5)
108
113 # Give the logger time to finish writing?
114 # time.sleep(0.5)
115
109116 logfile = self.logfile
110117 marker = marker or self.lastmarker
111118 if marker is None:
112119 return open(logfile, 'rb').readlines()
113
120
114121 if isinstance(marker, unicodestr):
115122 marker = marker.encode('utf-8')
116123 data = []
124131 elif marker in line:
125132 in_region = True
126133 return data
127
134
128135 def assertInLog(self, line, marker=None):
129136 """Fail if the given (partial) line is not in the log.
130
137
131138 The log will be searched from the given marker to the next marker.
132139 If marker is None, self.lastmarker is used. If the log hasn't
133140 been marked (using self.markLog), the entire log will be searched.
138145 return
139146 msg = "%r not found in log" % line
140147 self._handleLogError(msg, data, marker, line)
141
148
142149 def assertNotInLog(self, line, marker=None):
143150 """Fail if the given (partial) line is in the log.
144
151
145152 The log will be searched from the given marker to the next marker.
146153 If marker is None, self.lastmarker is used. If the log hasn't
147154 been marked (using self.markLog), the entire log will be searched.
151158 if line in logline:
152159 msg = "%r found in log" % line
153160 self._handleLogError(msg, data, marker, line)
154
161
155162 def assertLog(self, sliceargs, lines, marker=None):
156163 """Fail if log.readlines()[sliceargs] is not contained in 'lines'.
157
164
158165 The log will be searched from the given marker to the next marker.
159166 If marker is None, self.lastmarker is used. If the log hasn't
160167 been marked (using self.markLog), the entire log will be searched.
168175 lines = lines.encode('utf-8')
169176 if lines not in data[sliceargs]:
170177 msg = "%r not found on log line %r" % (lines, sliceargs)
171 self._handleLogError(msg, [data[sliceargs],"--EXTRA CONTEXT--"] + data[sliceargs+1:sliceargs+6], marker, lines)
178 self._handleLogError(
179 msg,
180 [data[sliceargs], "--EXTRA CONTEXT--"] + data[
181 sliceargs + 1:sliceargs + 6],
182 marker,
183 lines)
172184 else:
173185 # Multiple args. Use __getslice__ and require lines to be list.
174186 if isinstance(lines, tuple):
176188 elif isinstance(lines, basestring):
177189 raise TypeError("The 'lines' arg must be a list when "
178190 "'sliceargs' is a tuple.")
179
191
180192 start, stop = sliceargs
181193 for line, logline in zip(lines, data[start:stop]):
182194 if isinstance(line, unicodestr):
184196 if line not in logline:
185197 msg = "%r not found in log" % line
186198 self._handleLogError(msg, data[start:stop], marker, line)
187
7979 FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
8080 """
8181
82
8283 def erase_script_name(environ, start_response):
8384 environ['SCRIPT_NAME'] = ''
8485 return cherrypy.tree(environ, start_response)
8586
87
8688 class ModFCGISupervisor(helper.LocalWSGISupervisor):
87
89
8890 httpserver_class = "cherrypy.process.servers.FlupFCGIServer"
8991 using_apache = True
9092 using_wsgi = True
9193 template = conf_fastcgi
92
94
9395 def __str__(self):
9496 return "FCGI Server on %s:%s" % (self.host, self.port)
95
97
9698 def start(self, modulename):
9799 cherrypy.server.httpserver = servers.FlupFCGIServer(
98100 application=erase_script_name, bindAddress=('127.0.0.1', 4000))
103105 # ...and our local server
104106 cherrypy.engine.start()
105107 self.sync_apps()
106
108
107109 def start_apache(self):
108110 fcgiconf = CONF_PATH
109111 if not os.path.isabs(fcgiconf):
110112 fcgiconf = os.path.join(curdir, fcgiconf)
111
113
112114 # Write the Apache conf file.
113115 f = open(fcgiconf, 'wb')
114116 try:
119121 f.write(output)
120122 finally:
121123 f.close()
122
124
123125 result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf)
124126 if result:
125127 print(result)
126
128
127129 def stop(self):
128130 """Gracefully shutdown a server that is serving forever."""
129131 read_process(APACHE_PATH, "-k stop")
130132 helper.LocalWSGISupervisor.stop(self)
131
133
132134 def sync_apps(self):
133 cherrypy.server.httpserver.fcgiserver.application = self.get_app(erase_script_name)
134
135 cherrypy.server.httpserver.fcgiserver.application = self.get_app(
136 erase_script_name)
7676 FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
7777 """
7878
79
7980 class ModFCGISupervisor(helper.LocalSupervisor):
80
81
8182 using_apache = True
8283 using_wsgi = True
8384 template = conf_fcgid
84
85
8586 def __str__(self):
8687 return "FCGI Server on %s:%s" % (self.host, self.port)
87
88
8889 def start(self, modulename):
8990 cherrypy.server.httpserver = servers.FlupFCGIServer(
9091 application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
9394 self.start_apache()
9495 # ...and our local server
9596 helper.LocalServer.start(self, modulename)
96
97
9798 def start_apache(self):
9899 fcgiconf = CONF_PATH
99100 if not os.path.isabs(fcgiconf):
100101 fcgiconf = os.path.join(curdir, fcgiconf)
101
102
102103 # Write the Apache conf file.
103104 f = open(fcgiconf, 'wb')
104105 try:
109110 f.write(output)
110111 finally:
111112 f.close()
112
113
113114 result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf)
114115 if result:
115116 print(result)
116
117
117118 def stop(self):
118119 """Gracefully shutdown a server that is serving forever."""
119120 read_process(APACHE_PATH, "-k stop")
120121 helper.LocalServer.stop(self)
121
122
122123 def sync_apps(self):
123124 cherrypy.server.httpserver.fcgiserver.application = self.get_app()
124
9090 PythonDebug On
9191 """
9292
93
9394 class ModPythonSupervisor(helper.Supervisor):
94
95
9596 using_apache = True
9697 using_wsgi = False
9798 template = None
98
99
99100 def __str__(self):
100101 return "ModPython Server on %s:%s" % (self.host, self.port)
101
102
102103 def start(self, modulename):
103104 mpconf = CONF_PATH
104105 if not os.path.isabs(mpconf):
105106 mpconf = os.path.join(curdir, mpconf)
106
107
107108 f = open(mpconf, 'wb')
108109 try:
109110 f.write(self.template %
111112 'host': self.host})
112113 finally:
113114 f.close()
114
115
115116 result = read_process(APACHE_PATH, "-k start -f %s" % mpconf)
116117 if result:
117118 print(result)
118
119
119120 def stop(self):
120121 """Gracefully shutdown a server that is serving forever."""
121122 read_process(APACHE_PATH, "-k stop")
122123
123124
124125 loaded = False
126
127
125128 def wsgisetup(req):
126129 global loaded
127130 if not loaded:
128131 loaded = True
129132 options = req.get_options()
130
133
131134 import cherrypy
132135 cherrypy.config.update({
133136 "log.error_file": os.path.join(curdir, "test.log"),
134137 "environment": "test_suite",
135138 "server.socket_host": options['socket_host'],
136 })
137
139 })
140
138141 modname = options['testmod']
139142 mod = __import__(modname, globals(), locals(), [''])
140143 mod.setup_server()
141
144
142145 cherrypy.server.unsubscribe()
143146 cherrypy.engine.start()
144147 from mod_python import apache
150153 if not loaded:
151154 loaded = True
152155 options = req.get_options()
153
156
154157 import cherrypy
155158 cherrypy.config.update({
156159 "log.error_file": os.path.join(curdir, "test.log"),
157160 "environment": "test_suite",
158161 "server.socket_host": options['socket_host'],
159 })
162 })
160163 from mod_python import apache
161164 return apache.OK
162
8888
8989
9090 class ModWSGISupervisor(helper.Supervisor):
91
9192 """Server Controller for ModWSGI and CherryPy."""
92
93
9394 using_apache = True
9495 using_wsgi = True
95 template=conf_modwsgi
96
96 template = conf_modwsgi
97
9798 def __str__(self):
9899 return "ModWSGI Server on %s:%s" % (self.host, self.port)
99
100
100101 def start(self, modulename):
101102 mpconf = CONF_PATH
102103 if not os.path.isabs(mpconf):
103104 mpconf = os.path.join(curdir, mpconf)
104
105
105106 f = open(mpconf, 'wb')
106107 try:
107108 output = (self.template %
110111 f.write(output)
111112 finally:
112113 f.close()
113
114
114115 result = read_process(APACHE_PATH, "-k start -f %s" % mpconf)
115116 if result:
116117 print(result)
117
118
118119 # Make a request so mod_wsgi starts up our app.
119120 # If we don't, concurrent initial requests will 404.
120121 cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port)
121122 webtest.openURL('/ihopetheresnodefault', port=self.port)
122123 time.sleep(1)
123
124
124125 def stop(self):
125126 """Gracefully shutdown a server that is serving forever."""
126127 read_process(APACHE_PATH, "-k stop")
127128
128129
129130 loaded = False
131
132
130133 def application(environ, start_response):
131134 import cherrypy
132135 global loaded
135138 modname = "cherrypy.test." + environ['testmod']
136139 mod = __import__(modname, globals(), locals(), [''])
137140 mod.setup_server()
138
141
139142 cherrypy.config.update({
140143 "log.error_file": os.path.join(curdir, "test.error.log"),
141144 "log.access_file": os.path.join(curdir, "test.access.log"),
142145 "environment": "test_suite",
143146 "engine.SIGHUP": None,
144147 "engine.SIGTERM": None,
145 })
148 })
146149 return cherrypy.tree(environ, start_response)
147
1414 <style type='text/css'>
1515 table { border-collapse: collapse; border: 1px solid #663333; }
1616 th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
17 td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
17 td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
1818 border: 1px solid #663333; }
1919 .warn { font-family: serif; color: #990000; }
2020 </style>
5050 // Set the content of the 'btime' cell.
5151 var currentTime = new Date();
5252 var bunixtime = Math.floor(currentTime.getTime() / 1000);
53
53
5454 var v = formattime(currentTime);
5555 v += " (Unix time: " + bunixtime + ")";
56
56
5757 var diff = Math.abs(%(serverunixtime)s - bunixtime);
5858 if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
59
59
6060 document.getElementById('btime').innerHTML = v;
61
61
6262 // Warn if response cookie expires is not close to one hour in the future.
6363 // Yes, we want this to happen when wit hit the 'Expire' link, too.
6464 var expires = Date.parse("%(expires)s") / 1000;
9494 </body></html>
9595 """
9696
97
9798 class Root(object):
98
99
99100 def page(self):
100101 changemsg = []
101102 if cherrypy.session.id != cherrypy.session.originalid:
102103 if cherrypy.session.originalid is None:
103 changemsg.append('Created new session because no session id was given.')
104 changemsg.append(
105 'Created new session because no session id was given.')
104106 if cherrypy.session.missing:
105 changemsg.append('Created new session due to missing (expired or malicious) session.')
107 changemsg.append(
108 'Created new session due to missing '
109 '(expired or malicious) session.')
106110 if cherrypy.session.regenerated:
107111 changemsg.append('Application generated a new session.')
108
112
109113 try:
110114 expires = cherrypy.response.cookie['session_id']['expires']
111115 except KeyError:
112116 expires = ''
113
117
114118 return page % {
115119 'sessionid': cherrypy.session.id,
116120 'changemsg': '<br>'.join(changemsg),
117121 'respcookie': cherrypy.response.cookie.output(),
118122 'reqcookie': cherrypy.request.cookie.output(),
119123 'sessiondata': copyitems(cherrypy.session),
120 'servertime': datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC",
124 'servertime': (
125 datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC"
126 ),
121127 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
122128 'cpversion': cherrypy.__version__,
123129 'pyversion': sys.version,
124130 'expires': expires,
125 }
126
131 }
132
127133 def index(self):
128134 # Must modify data or the session will not be saved.
129135 cherrypy.session['color'] = 'green'
130136 return self.page()
131137 index.exposed = True
132
138
133139 def expire(self):
134140 sessions.expire()
135141 return self.page()
136142 expire.exposed = True
137
143
138144 def regen(self):
139145 cherrypy.session.regenerate()
140146 # Must modify data or the session will not be saved.
147153 #'environment': 'production',
148154 'log.screen': True,
149155 'tools.sessions.on': True,
150 })
156 })
151157 cherrypy.quickstart(Root())
152
0 <html>
1 <body>
2 <h1>I couldn't find that thing you were looking for!</h1>
3 </body>
4 </html>
+0
-1
cherrypy/test/static/has space.html less more
0 Hello, world
1111
1212 def setup_server():
1313 class Root:
14
1415 def index(self):
1516 return "This is public."
1617 index.exposed = True
1718
1819 class BasicProtected:
20
1921 def index(self):
20 return "Hello %s, you've been authorized." % cherrypy.request.login
22 return "Hello %s, you've been authorized." % (
23 cherrypy.request.login)
2124 index.exposed = True
2225
2326 class BasicProtected2:
27
2428 def index(self):
25 return "Hello %s, you've been authorized." % cherrypy.request.login
29 return "Hello %s, you've been authorized." % (
30 cherrypy.request.login)
2631 index.exposed = True
2732
28 userpassdict = {'xuser' : 'xpassword'}
29 userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()}
33 userpassdict = {'xuser': 'xpassword'}
34 userhashdict = {'xuser': md5(ntob('xpassword')).hexdigest()}
3035
3136 def checkpasshash(realm, user, password):
3237 p = userhashdict.get(user)
3338 return p and p == md5(ntob(password)).hexdigest() or False
3439
35 conf = {'/basic': {'tools.auth_basic.on': True,
36 'tools.auth_basic.realm': 'wonderland',
37 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)},
38 '/basic2': {'tools.auth_basic.on': True,
39 'tools.auth_basic.realm': 'wonderland',
40 'tools.auth_basic.checkpassword': checkpasshash},
41 }
40 basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
41 conf = {
42 '/basic': {
43 'tools.auth_basic.on': True,
44 'tools.auth_basic.realm': 'wonderland',
45 'tools.auth_basic.checkpassword': basic_checkpassword_dict
46 },
47 '/basic2': {
48 'tools.auth_basic.on': True,
49 'tools.auth_basic.realm': 'wonderland',
50 'tools.auth_basic.checkpassword': checkpasshash
51 },
52 }
4253
4354 root = Root()
4455 root.basic = BasicProtected()
5768 self.assertStatus(401)
5869 self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
5970
60 self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
71 self.getPage('/basic/',
72 [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
6173 self.assertStatus(401)
6274
63 self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
75 self.getPage('/basic/',
76 [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
6477 self.assertStatus('200 OK')
6578 self.assertBody("Hello xuser, you've been authorized.")
6679
6982 self.assertStatus(401)
7083 self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
7184
72 self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
85 self.getPage('/basic2/',
86 [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
7387 self.assertStatus(401)
7488
75 self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
89 self.getPage('/basic2/',
90 [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
7691 self.assertStatus('200 OK')
7792 self.assertBody("Hello xuser, you've been authorized.")
78
77
88 from cherrypy.test import helper
99
10
1011 class DigestAuthTest(helper.CPWebCase):
1112
1213 def setup_server():
1314 class Root:
15
1416 def index(self):
1517 return "This is public."
1618 index.exposed = True
1719
1820 class DigestProtected:
21
1922 def index(self):
20 return "Hello %s, you've been authorized." % cherrypy.request.login
23 return "Hello %s, you've been authorized." % (
24 cherrypy.request.login)
2125 index.exposed = True
2226
2327 def fetch_users():
2428 return {'test': 'test'}
25
2629
2730 get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users())
2831 conf = {'/digest': {'tools.auth_digest.on': True,
3538 root.digest = DigestProtected()
3639 cherrypy.tree.mount(root, config=conf)
3740 setup_server = staticmethod(setup_server)
38
41
3942 def testPublic(self):
4043 self.getPage("/")
4144 self.assertStatus('200 OK')
5457 break
5558
5659 if value is None:
57 self._handlewebError("Digest authentification scheme was not found")
60 self._handlewebError(
61 "Digest authentification scheme was not found")
5862
5963 value = value[7:]
6064 items = value.split(', ')
6973 if 'realm' not in tokens:
7074 self._handlewebError(missing_msg % 'realm')
7175 elif tokens['realm'] != '"localhost"':
72 self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
76 self._handlewebError(bad_value_msg %
77 ('realm', '"localhost"', tokens['realm']))
7378 if 'nonce' not in tokens:
7479 self._handlewebError(missing_msg % 'nonce')
7580 else:
7782 if 'algorithm' not in tokens:
7883 self._handlewebError(missing_msg % 'algorithm')
7984 elif tokens['algorithm'] != '"MD5"':
80 self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
85 self._handlewebError(bad_value_msg %
86 ('algorithm', '"MD5"', tokens['algorithm']))
8187 if 'qop' not in tokens:
8288 self._handlewebError(missing_msg % 'qop')
8389 elif tokens['qop'] != '"auth"':
84 self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
90 self._handlewebError(bad_value_msg %
91 ('qop', '"auth"', tokens['qop']))
8592
86 get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'})
93 get_ha1 = auth_digest.get_ha1_dict_plain({'test': 'test'})
8794
8895 # Test user agent response with a wrong value for 'realm'
89 base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
96 base_auth = ('Digest username="test", '
97 'realm="wrong realm", '
98 'nonce="%s", '
99 'uri="/digest/", '
100 'algorithm=MD5, '
101 'response="%s", '
102 'qop=auth, '
103 'nc=%s, '
104 'cnonce="1522e61005789929"')
90105
91 auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
106 auth_header = base_auth % (
107 nonce, '11111111111111111111111111111111', '00000001')
92108 auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
93109 # calculate the response digest
94110 ha1 = get_ha1(auth.realm, 'test')
99115 self.assertStatus(401)
100116
101117 # Test that must pass
102 base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
118 base_auth = ('Digest username="test", '
119 'realm="localhost", '
120 'nonce="%s", '
121 'uri="/digest/", '
122 'algorithm=MD5, '
123 'response="%s", '
124 'qop=auth, '
125 'nc=%s, '
126 'cnonce="1522e61005789929"')
103127
104 auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
128 auth_header = base_auth % (
129 nonce, '11111111111111111111111111111111', '00000001')
105130 auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
106131 # calculate the response digest
107132 ha1 = get_ha1('localhost', 'test')
111136 self.getPage('/digest/', [('Authorization', auth_header)])
112137 self.assertStatus('200 OK')
113138 self.assertBody("Hello test, you've been authorized.")
114
2323
2424 for channel in b.listeners:
2525 for index, priority in enumerate([100, 50, 0, 51]):
26 b.subscribe(channel, self.get_listener(channel, index), priority)
26 b.subscribe(channel,
27 self.get_listener(channel, index), priority)
2728
2829 for channel in b.listeners:
2930 b.publish(channel)
4142 custom_listeners = ('hugh', 'louis', 'dewey')
4243 for channel in custom_listeners:
4344 for index, priority in enumerate([None, 10, 60, 40]):
44 b.subscribe(channel, self.get_listener(channel, index), priority)
45 b.subscribe(channel,
46 self.get_listener(channel, index), priority)
4547
4648 for channel in custom_listeners:
4749 b.publish(channel, 'ah so')
48 expected.extend([msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)])
50 expected.extend([msg % (i, channel, 'ah so')
51 for i in (1, 3, 0, 2)])
4952 b.publish(channel)
5053 expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
5154
7376
7477 def log(self, bus):
7578 self._log_entries = []
79
7680 def logit(msg, level):
7781 self._log_entries.append(msg)
7882 bus.subscribe('log', logit)
97101 b.start()
98102 try:
99103 # The start method MUST call all 'start' listeners.
100 self.assertEqual(set(self.responses),
101 set([msg % (i, 'start', None) for i in range(num)]))
104 self.assertEqual(
105 set(self.responses),
106 set([msg % (i, 'start', None) for i in range(num)]))
102107 # The start method MUST move the state to STARTED
103108 # (or EXITING, if errors occur)
104109 self.assertEqual(b.state, b.states.STARTED)
139144 b.graceful()
140145
141146 # The graceful method MUST call all 'graceful' listeners.
142 self.assertEqual(set(self.responses),
143 set([msg % (i, 'graceful', None) for i in range(num)]))
147 self.assertEqual(
148 set(self.responses),
149 set([msg % (i, 'graceful', None) for i in range(num)]))
144150 # The graceful method MUST log its states.
145151 self.assertLog(['Bus graceful'])
146152
164170 # The exit method MUST move the state to EXITING
165171 self.assertEqual(b.state, b.states.EXITING)
166172 # The exit method MUST log its states.
167 self.assertLog(['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
173 self.assertLog(
174 ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
168175
169176 def test_wait(self):
170177 b = wspbus.Bus()
175182
176183 for method, states in [('start', [b.states.STARTED]),
177184 ('stop', [b.states.STOPPED]),
178 ('start', [b.states.STARTING, b.states.STARTED]),
185 ('start',
186 [b.states.STARTING, b.states.STARTED]),
179187 ('exit', [b.states.EXITING]),
180188 ]:
181189 threading.Thread(target=f, args=(method,)).start()
192200 def f():
193201 time.sleep(0.2)
194202 b.exit()
203
195204 def g():
196205 time.sleep(0.4)
197206 threading.Thread(target=f).start()
203212
204213 # The block method MUST wait for the EXITING state.
205214 self.assertEqual(b.state, b.states.EXITING)
206 # The block method MUST wait for ALL non-main, non-daemon threads to finish.
215 # The block method MUST wait for ALL non-main, non-daemon threads to
216 # finish.
207217 threads = [t for t in threading.enumerate() if not get_daemon(t)]
208218 self.assertEqual(len(threads), 1)
209 # The last message will mention an indeterminable thread name; ignore it
219 # The last message will mention an indeterminable thread name; ignore
220 # it
210221 self.assertEqual(self._log_entries[:-1],
211222 ['Bus STOPPING', 'Bus STOPPED',
212223 'Bus EXITING', 'Bus EXITED',
217228 self.log(b)
218229 try:
219230 events = []
231
220232 def f(*args, **kwargs):
221233 events.append(("f", args, kwargs))
234
222235 def g():
223236 events.append("g")
224237 b.subscribe("start", g)
1111 from cherrypy._cpcompat import next, ntob, quote, xrange
1212 from cherrypy.lib import httputil
1313
14 gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
15 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
16 '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;')
17
14 gif_bytes = ntob(
15 'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
16 '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
17 '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
18 )
1819
1920
2021 from cherrypy.test import helper
2122
23
2224 class CacheTest(helper.CPWebCase):
2325
2426 def setup_server():
25
27
2628 class Root:
27
29
2830 _cp_config = {'tools.caching.on': True}
29
31
3032 def __init__(self):
3133 self.counter = 0
3234 self.control_counter = 0
3335 self.longlock = threading.Lock()
34
36
3537 def index(self):
3638 self.counter += 1
3739 msg = "visit #%s" % self.counter
3840 return msg
3941 index.exposed = True
40
42
4143 def control(self):
4244 self.control_counter += 1
4345 return "visit #%s" % self.control_counter
4446 control.exposed = True
45
47
4648 def a_gif(self):
47 cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate()
49 cherrypy.response.headers[
50 'Last-Modified'] = httputil.HTTPDate()
4851 return gif_bytes
4952 a_gif.exposed = True
50
53
5154 def long_process(self, seconds='1'):
5255 try:
5356 self.longlock.acquire()
5659 self.longlock.release()
5760 return 'success!'
5861 long_process.exposed = True
59
62
6063 def clear_cache(self, path):
6164 cherrypy._cache.store[cherrypy.request.base + path].clear()
6265 clear_cache.exposed = True
63
66
6467 class VaryHeaderCachingServer(object):
65
66 _cp_config = {'tools.caching.on': True,
68
69 _cp_config = {
70 'tools.caching.on': True,
6771 'tools.response_headers.on': True,
68 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')],
69 }
70
72 'tools.response_headers.headers': [
73 ('Vary', 'Our-Varying-Header')
74 ],
75 }
76
7177 def __init__(self):
7278 self.counter = count(1)
73
79
7480 def index(self):
7581 return "visit #%s" % next(self.counter)
7682 index.exposed = True
77
83
7884 class UnCached(object):
7985 _cp_config = {'tools.expires.on': True,
8086 'tools.expires.secs': 60,
103109 cacheable.exposed = True
104110
105111 def specific(self):
106 cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
112 cherrypy.response.headers[
113 'Etag'] = 'need_this_to_make_me_cacheable'
107114 return "I am being specific"
108115 specific.exposed = True
109116 specific._cp_config = {'tools.expires.secs': 86400}
110117
111 class Foo(object):pass
112
118 class Foo(object):
119 pass
120
113121 def wrongtype(self):
114 cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
122 cherrypy.response.headers[
123 'Etag'] = 'need_this_to_make_me_cacheable'
115124 return "Woops"
116125 wrongtype.exposed = True
117126 wrongtype._cp_config = {'tools.expires.secs': Foo()}
118
127
119128 cherrypy.tree.mount(Root())
120129 cherrypy.tree.mount(UnCached(), "/expires")
121130 cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers")
133142 age = int(self.assertHeader("Age"))
134143 self.assert_(age >= elapsed)
135144 elapsed = age
136
145
137146 # POST, PUT, DELETE should not be cached.
138147 self.getPage("/", method="POST")
139148 self.assertBody('visit #2')
140 # Because gzip is turned on, the Vary header should always Vary for content-encoding
149 # Because gzip is turned on, the Vary header should always Vary for
150 # content-encoding
141151 self.assertHeader('Vary', 'Accept-Encoding')
142152 # The previous request should have invalidated the cache,
143153 # so this request will recalc the response.
148158 self.assertBody('visit #3')
149159 self.getPage("/", method="DELETE")
150160 self.assertBody('visit #4')
151
161
152162 # The previous request should have invalidated the cache,
153163 # so this request will recalc the response.
154164 self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
155165 self.assertHeader('Content-Encoding', 'gzip')
156166 self.assertHeader('Vary')
157 self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
158
167 self.assertEqual(
168 cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
169
159170 # Now check that a second request gets the gzip header and gzipped body
160171 # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
161172 # response body was being gzipped a second time.
162173 self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
163174 self.assertHeader('Content-Encoding', 'gzip')
164 self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
165
175 self.assertEqual(
176 cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
177
166178 # Now check that a third request that doesn't accept gzip
167179 # skips the cache (because the 'Vary' header denies it).
168180 self.getPage("/", method="GET")
169181 self.assertNoHeader('Content-Encoding')
170182 self.assertBody('visit #6')
171
183
172184 def testVaryHeader(self):
173185 self.getPage("/varying_headers/")
174186 self.assertStatus("200 OK")
175187 self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
176188 self.assertBody('visit #1')
177
189
178190 # Now check that different 'Vary'-fields don't evict each other.
179191 # This test creates 2 requests with different 'Our-Varying-Header'
180192 # and then tests if the first one still exists.
181 self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')])
182 self.assertStatus("200 OK")
183 self.assertBody('visit #2')
184
185 self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')])
186 self.assertStatus("200 OK")
187 self.assertBody('visit #2')
188
193 self.getPage("/varying_headers/",
194 headers=[('Our-Varying-Header', 'request 2')])
195 self.assertStatus("200 OK")
196 self.assertBody('visit #2')
197
198 self.getPage("/varying_headers/",
199 headers=[('Our-Varying-Header', 'request 2')])
200 self.assertStatus("200 OK")
201 self.assertBody('visit #2')
202
189203 self.getPage("/varying_headers/")
190204 self.assertStatus("200 OK")
191205 self.assertBody('visit #1')
192
206
193207 def testExpiresTool(self):
194208 # test setting an expires header
195209 self.getPage("/expires/specific")
196210 self.assertStatus("200 OK")
197211 self.assertHeader("Expires")
198
212
199213 # test exceptions for bad time values
200214 self.getPage("/expires/wrongtype")
201215 self.assertStatus(500)
202216 self.assertInBody("TypeError")
203
217
204218 # static content should not have "cache prevention" headers
205219 self.getPage("/expires/index.html")
206220 self.assertStatus("200 OK")
207221 self.assertNoHeader("Pragma")
208222 self.assertNoHeader("Cache-Control")
209223 self.assertHeader("Expires")
210
224
211225 # dynamic content that sets indicators should not have
212226 # "cache prevention" headers
213227 self.getPage("/expires/cacheable")
215229 self.assertNoHeader("Pragma")
216230 self.assertNoHeader("Cache-Control")
217231 self.assertHeader("Expires")
218
232
219233 self.getPage('/expires/dynamic')
220234 self.assertBody("D-d-d-dynamic!")
221235 # the Cache-Control header should be untouched
222236 self.assertHeader("Cache-Control", "private")
223237 self.assertHeader("Expires")
224
238
225239 # configure the tool to ignore indicators and replace existing headers
226240 self.getPage("/expires/force")
227241 self.assertStatus("200 OK")
230244 if cherrypy.server.protocol_version == "HTTP/1.1":
231245 self.assertHeader("Cache-Control", "no-cache, must-revalidate")
232246 self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
233
247
234248 # static content should now have "cache prevention" headers
235249 self.getPage("/expires/index.html")
236250 self.assertStatus("200 OK")
238252 if cherrypy.server.protocol_version == "HTTP/1.1":
239253 self.assertHeader("Cache-Control", "no-cache, must-revalidate")
240254 self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
241
255
242256 # the cacheable handler should now have "cache prevention" headers
243257 self.getPage("/expires/cacheable")
244258 self.assertStatus("200 OK")
246260 if cherrypy.server.protocol_version == "HTTP/1.1":
247261 self.assertHeader("Cache-Control", "no-cache, must-revalidate")
248262 self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
249
263
250264 self.getPage('/expires/dynamic')
251265 self.assertBody("D-d-d-dynamic!")
252266 # dynamic sets Cache-Control to private but it should be
255269 if cherrypy.server.protocol_version == "HTTP/1.1":
256270 self.assertHeader("Cache-Control", "no-cache, must-revalidate")
257271 self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
258
272
259273 def testLastModified(self):
260274 self.getPage("/a.gif")
261275 self.assertStatus(200)
262276 self.assertBody(gif_bytes)
263277 lm1 = self.assertHeader("Last-Modified")
264
278
265279 # this request should get the cached copy.
266280 self.getPage("/a.gif")
267281 self.assertStatus(200)
269283 self.assertHeader("Age")
270284 lm2 = self.assertHeader("Last-Modified")
271285 self.assertEqual(lm1, lm2)
272
286
273287 # this request should match the cached copy, but raise 304.
274288 self.getPage("/a.gif", [('If-Modified-Since', lm1)])
275289 self.assertStatus(304)
276290 self.assertNoHeader("Last-Modified")
277291 if not getattr(cherrypy.server, "using_apache", False):
278292 self.assertHeader("Age")
279
293
280294 def test_antistampede(self):
281295 SECONDS = 4
282296 # We MUST make an initial synchronous request in order to create the
285299 self.getPage("/long_process?seconds=%d" % SECONDS)
286300 self.assertBody('success!')
287301 self.getPage("/clear_cache?path=" +
288 quote('/long_process?seconds=%d' % SECONDS, safe=''))
302 quote('/long_process?seconds=%d' % SECONDS, safe=''))
289303 self.assertStatus(200)
290
304
291305 start = datetime.datetime.now()
306
292307 def run():
293308 self.getPage("/long_process?seconds=%d" % SECONDS)
294309 # The response should be the same every time
302317 # Allow a second (two, for slow hosts)
303318 # for our thread/TCP overhead etc.
304319 seconds=SECONDS + 2)
305
320
306321 def test_cache_control(self):
307322 self.getPage("/control")
308323 self.assertBody('visit #1')
309324 self.getPage("/control")
310325 self.assertBody('visit #1')
311
326
312327 self.getPage("/control", headers=[('Cache-Control', 'no-cache')])
313328 self.assertBody('visit #2')
314329 self.getPage("/control")
315330 self.assertBody('visit #2')
316
331
317332 self.getPage("/control", headers=[('Pragma', 'no-cache')])
318333 self.assertBody('visit #3')
319334 self.getPage("/control")
320335 self.assertBody('visit #3')
321
336
322337 time.sleep(1)
323338 self.getPage("/control", headers=[('Cache-Control', 'max-age=0')])
324339 self.assertBody('visit #4')
325340 self.getPage("/control")
326341 self.assertBody('visit #4')
327
0 import unittest
1
2 import nose
3
4 from cherrypy import _cpcompat as compat
5
6
7 class StringTester(unittest.TestCase):
8
9 def test_ntob_non_native(self):
10 """
11 ntob should raise an Exception on unicode.
12 (Python 2 only)
13
14 See #1132 for discussion.
15 """
16 if compat.py3k:
17 raise nose.SkipTest("Only useful on Python 2")
18 self.assertRaises(Exception, compat.ntob, unicode('fight'))
00 """Tests for the CherryPy configuration system."""
11
2 import os, sys
2 import os
3 import sys
4 import unittest
5
6 import cherrypy
7 import cherrypy._cpcompat as compat
8
39 localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
410
5 from cherrypy._cpcompat import ntob, StringIO
6 import unittest
7
8 import cherrypy
911
1012 def setup_server():
1113
4042 plain._cp_config = {'request.body.attempt_charsets': ['utf-16']}
4143
4244 favicon_ico = cherrypy.tools.staticfile.handler(
43 filename=os.path.join(localDir, '../favicon.ico'))
45 filename=os.path.join(localDir, '../favicon.ico'))
4446
4547 class Foo:
4648
5658 return 'Hello world'
5759 silly.exposed = True
5860 silly._cp_config = {'response.headers.X-silly': 'sillyval'}
59
61
6062 # Test the expose and config decorators
6163 #@cherrypy.expose
6264 #@cherrypy.config(foo='this3', **{'bax': 'this4'})
7173 return str(cherrypy.request.config.get(key, "None"))
7274 index.exposed = True
7375
74
7576 def raw_namespace(key, value):
7677 if key == 'input.map':
7778 handler = cherrypy.request.handler
79
7880 def wrapper():
7981 params = cherrypy.request.params
8082 for name, coercer in list(value.items()):
8688 cherrypy.request.handler = wrapper
8789 elif key == 'output':
8890 handler = cherrypy.request.handler
91
8992 def wrapper():
9093 # 'value' is a type (like int or str).
9194 return value(handler())
100103 incr.exposed = True
101104 incr._cp_config = {'raw.input.map': {'num': int}}
102105
103 ioconf = StringIO("""
106 if not compat.py3k:
107 thing3 = "thing3: unicode('test', errors='ignore')"
108 else:
109 thing3 = ''
110
111 ioconf = compat.StringIO("""
104112 [/]
105113 neg: -1234
106114 filename: os.path.join(sys.prefix, "hello.py")
107115 thing1: cherrypy.lib.httputil.response_codes[404]
108116 thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
117 %s
109118 complex: 3+2j
110119 mul: 6*3
111120 ones: "11"
114123
115124 [/favicon.ico]
116125 tools.staticfile.filename = %r
117 """ % os.path.join(localDir, 'static/dirback.jpg'))
126 """ % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
118127
119128 root = Root()
120129 root.foo = Foo()
131140 # Client-side code #
132141
133142 from cherrypy.test import helper
143
134144
135145 class ConfigTests(helper.CPWebCase):
136146 setup_server = staticmethod(setup_server)
146156 ('/foo/', 'bax', 'None'),
147157 ('/foo/bar', 'baz', "'that2'"),
148158 ('/foo/nex', 'baz', 'that2'),
149 # If 'foo' == 'this', then the mount point '/another' leaks into '/'.
150 ('/another/','foo', 'None'),
159 # If 'foo' == 'this', then the mount point '/another' leaks into
160 # '/'.
161 ('/another/', 'foo', 'None'),
151162 ]
152163 for path, key, expected in tests:
153164 self.getPage(path + "?key=" + key)
160171 'request.show_tracebacks': True,
161172 'log.screen': False,
162173 'environment': 'test_suite',
163 'engine.autoreload_on': False,
174 'engine.autoreload.on': False,
164175 # From global config
165176 'luxuryyacht': 'throatwobblermangrove',
166177 # From Root._cp_config
170181 # From Foo.bar._cp_config
171182 'foo': 'this3',
172183 'bax': 'this4',
173 }
184 }
174185 for key, expected in expectedconf.items():
175186 self.getPage("/foo/bar?key=" + key)
176187 self.assertBody(repr(expected))
191202 self.getPage("/repr?key=thing2")
192203 from cherrypy.tutorial import thing2
193204 self.assertBody(repr(thing2))
205
206 if not compat.py3k:
207 self.getPage("/repr?key=thing3")
208 self.assertBody(repr(u'test'))
194209
195210 self.getPage("/repr?key=complex")
196211 self.assertBody("(3+2j)")
225240 self.getPage("/plain", method='POST', headers=[
226241 ('Content-Type', 'application/x-www-form-urlencoded'),
227242 ('Content-Length', '13')],
228 body=ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00'))
243 body=compat.ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00'))
229244 self.assertBody("abc")
230245
231246
247262
248263 """)
249264
250 fp = StringIO(conf)
265 fp = compat.StringIO(conf)
251266
252267 cherrypy.config.update(fp)
253268 self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir")
254 self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2")
255
269 self.assertEqual(cherrypy.config["my"]
270 ["my.dir2"], "/some/dir/my/dir/dir2")
00 """Tests for the CherryPy configuration system."""
11
2 import os, sys
2 import os
3 import sys
34 localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
45 import socket
56 import time
1112
1213 from cherrypy.test import helper
1314
15
1416 class ServerConfigTests(helper.CPWebCase):
1517
1618 def setup_server():
17
19
1820 class Root:
21
1922 def index(self):
2023 return cherrypy.request.wsgi_environ['SERVER_PORT']
2124 index.exposed = True
22
25
2326 def upload(self, file):
2427 return "Size: %s" % len(file.file.read())
2528 upload.exposed = True
26
29
2730 def tinyupload(self):
2831 return cherrypy.request.body.read()
2932 tinyupload.exposed = True
3033 tinyupload._cp_config = {'request.body.maxbytes': 100}
31
34
3235 cherrypy.tree.mount(Root())
33
36
3437 cherrypy.config.update({
3538 'server.socket_host': '0.0.0.0',
3639 'server.socket_port': 9876,
3740 'server.max_request_body_size': 200,
3841 'server.max_request_header_size': 500,
3942 'server.socket_timeout': 0.5,
40
43
4144 # Test explicit server.instance
4245 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
4346 'server.2.socket_port': 9877,
44
47
4548 # Test non-numeric <servername>
4649 # Also test default server.instance = builtin server
4750 'server.yetanother.socket_port': 9878,
48 })
51 })
4952 setup_server = staticmethod(setup_server)
50
53
5154 PORT = 9876
52
55
5356 def testBasicConfig(self):
5457 self.getPage("/")
5558 self.assertBody(str(self.PORT))
56
59
5760 def testAdditionalServers(self):
5861 if self.scheme == 'https':
5962 return self.skip("not available under ssl")
6366 self.PORT = 9878
6467 self.getPage("/")
6568 self.assertBody(str(self.PORT))
66
69
6770 def testMaxRequestSizePerHandler(self):
6871 if getattr(cherrypy.server, "using_apache", False):
6972 return self.skip("skipped due to known Apache differences... ")
70
73
7174 self.getPage('/tinyupload', method="POST",
7275 headers=[('Content-Type', 'text/plain'),
7376 ('Content-Length', '100')],
7477 body="x" * 100)
7578 self.assertStatus(200)
7679 self.assertBody("x" * 100)
77
80
7881 self.getPage('/tinyupload', method="POST",
7982 headers=[('Content-Type', 'text/plain'),
8083 ('Content-Length', '101')],
8184 body="x" * 101)
8285 self.assertStatus(413)
83
86
8487 def testMaxRequestSize(self):
8588 if getattr(cherrypy.server, "using_apache", False):
8689 return self.skip("skipped due to known Apache differences... ")
87
90
8891 for size in (500, 5000, 50000):
8992 self.getPage("/", headers=[('From', "x" * 500)])
9093 self.assertStatus(413)
91
92 # Test for http://www.cherrypy.org/ticket/421
94
95 # Test for https://bitbucket.org/cherrypy/cherrypy/issue/421
9396 # (Incorrect border condition in readline of SizeCheckWrapper).
9497 # This hangs in rev 891 and earlier.
9598 lines256 = "x" * 248
9699 self.getPage("/",
97100 headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
98101 ('From', lines256)])
99
102
100103 # Test upload
104 cd = (
105 'Content-Disposition: form-data; '
106 'name="file"; '
107 'filename="hello.txt"'
108 )
101109 body = '\r\n'.join([
102110 '--x',
103 'Content-Disposition: form-data; name="file"; filename="hello.txt"',
111 cd,
104112 'Content-Type: text/plain',
105113 '',
106114 '%s',
111119 ("Content-Length", "%s" % len(b))]
112120 self.getPage('/upload', h, "POST", b)
113121 self.assertBody('Size: %d' % partlen)
114
122
115123 b = body % ("x" * 200)
116124 h = [("Content-type", "multipart/form-data; boundary=x"),
117125 ("Content-Length", "%s" % len(b))]
118126 self.getPage('/upload', h, "POST", b)
119127 self.assertStatus(413)
120
66
77
88 import cherrypy
9 from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine
10 from cherrypy._cpcompat import ntob, urlopen, unicodestr
9 from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected
10 from cherrypy._cpcompat import BadStatusLine, ntob, urlopen, unicodestr
1111 from cherrypy.test import webtest
1212 from cherrypy import _cperror
1313
1414
1515 pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
16
1617
1718 def setup_server():
1819
9293 cherrypy.config.update({
9394 'server.max_request_body_size': 1001,
9495 'server.socket_timeout': timeout,
95 })
96 })
9697
9798
9899 from cherrypy.test import helper
100
99101
100102 class ConnectionCloseTests(helper.CPWebCase):
101103 setup_server = staticmethod(setup_server)
178180 else:
179181 self.assertHeader("Connection", "close")
180182
181 # Make another request on the same connection, which should error.
183 # Make another request on the same connection, which should
184 # error.
182185 self.assertRaises(NotConnected, self.getPage, "/")
183186
184 # Try HEAD. See http://www.cherrypy.org/ticket/864.
187 # Try HEAD. See
188 # https://bitbucket.org/cherrypy/cherrypy/issue/864.
185189 self.getPage("/stream", method='HEAD')
186190 self.assertStatus('200 OK')
187191 self.assertBody('')
219223 self.assertNoHeader("Connection", "Keep-Alive")
220224 self.assertNoHeader("Transfer-Encoding")
221225
222 # Make another request on the same connection, which should error.
226 # Make another request on the same connection, which should
227 # error.
223228 self.assertRaises(NotConnected, self.getPage, "/")
224229
225230 def test_HTTP10_KeepAlive(self):
234239 self.assertStatus('200 OK')
235240 self.assertBody(pov)
236241 # Apache, for example, may emit a Connection header even for HTTP/1.0
237 ## self.assertNoHeader("Connection")
242 # self.assertNoHeader("Connection")
238243
239244 # Test a keep-alive HTTP/1.0 request.
240245 self.persistent = True
249254 self.assertStatus('200 OK')
250255 self.assertBody(pov)
251256 # Apache, for example, may emit a Connection header even for HTTP/1.0
252 ## self.assertNoHeader("Connection")
257 # self.assertNoHeader("Connection")
253258
254259
255260 class PipelineTests(helper.CPWebCase):
360365 self.body = response.read()
361366 self.assertBody(pov)
362367
363
364368 # Make another request on the same socket,
365369 # but timeout on the headers
366370 conn.send(ntob('GET /hello HTTP/1.1'))
470474 while True:
471475 line = response.fp.readline().strip()
472476 if line:
473 self.fail("100 Continue should not output any headers. Got %r" % line)
477 self.fail(
478 "100 Continue should not output any headers. Got %r" %
479 line)
474480 else:
475481 break
476482
595601 return self.skip()
596602
597603 if (hasattr(self, 'harness') and
598 "modpython" in self.harness.__class__.__name__.lower()):
604 "modpython" in self.harness.__class__.__name__.lower()):
599605 # mod_python forbids chunked encoding
600606 return self.skip()
601607
607613
608614 # Try a normal chunked request (with extensions)
609615 body = ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n"
610 "Content-Type: application/json\r\n"
611 "\r\n")
616 "Content-Type: application/json\r\n"
617 "\r\n")
612618 conn.putrequest("POST", "/upload", skip_host=True)
613619 conn.putheader("Host", self.HOST)
614620 conn.putheader("Transfer-Encoding", "chunked")
679685 # the actual bytes in the response body.
680686 self.persistent = True
681687 conn = self.HTTP_CONN
682 conn.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5",
683 skip_host=True)
688 conn.putrequest(
689 "GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5",
690 skip_host=True)
684691 conn.putheader("Host", self.HOST)
685692 conn.endheaders()
686693 response = conn.getresponse()
691698
692699 def test_598(self):
693700 remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' %
694 (self.scheme, self.HOST, self.PORT,))
701 (self.scheme, self.HOST, self.PORT,))
695702 buf = remote_data_conn.read(512)
696703 time.sleep(timeout * 0.6)
697704 remaining = (1024 * 1024) - 512
730737 self.body = response.read()
731738 self.assertBody("HTTP requires CRLF terminators")
732739 conn.close()
733
1616
1717 from cherrypy.test import helper
1818
19
1920 class CoreRequestHandlingTest(helper.CPWebCase):
2021
2122 def setup_server():
2223 class Root:
23
24
2425 def index(self):
2526 return "hello"
2627 index.exposed = True
27
28
2829 favicon_ico = tools.staticfile.handler(filename=favicon_path)
29
30
3031 def defct(self, newct):
3132 newct = "text/%s" % newct
3233 cherrypy.config.update({'tools.response_headers.on': True,
3334 'tools.response_headers.headers':
3435 [('Content-Type', newct)]})
3536 defct.exposed = True
36
37
3738 def baseurl(self, path_info, relative=None):
3839 return cherrypy.url(path_info, relative=bool(relative))
3940 baseurl.exposed = True
40
41
4142 root = Root()
42
43
4344 if sys.version_info >= (2, 5):
4445 from cherrypy.test._test_decorators import ExposeExamples
4546 root.expose_dec = ExposeExamples()
4647
47
4848 class TestType(type):
49 """Metaclass which automatically exposes all functions in each subclass,
50 and adds an instance of the subclass as an attribute of root.
49
50 """Metaclass which automatically exposes all functions in each
51 subclass, and adds an instance of the subclass as an attribute
52 of root.
5153 """
5254 def __init__(cls, name, bases, dct):
5355 type.__init__(cls, name, bases, dct)
5658 value.exposed = True
5759 setattr(root, name.lower(), cls())
5860 Test = TestType('Test', (object, ), {})
59
60
61
6162 class URL(Test):
62
63
6364 _cp_config = {'tools.trailing_slash.on': False}
64
65
6566 def index(self, path_info, relative=None):
6667 if relative != 'server':
6768 relative = bool(relative)
6869 return cherrypy.url(path_info, relative=relative)
69
70
7071 def leaf(self, path_info, relative=None):
7172 if relative != 'server':
7273 relative = bool(relative)
7374 return cherrypy.url(path_info, relative=relative)
7475
75
7676 def log_status():
7777 Status.statuses.append(cherrypy.response.status)
78 cherrypy.tools.log_status = cherrypy.Tool('on_end_resource', log_status)
79
78 cherrypy.tools.log_status = cherrypy.Tool(
79 'on_end_resource', log_status)
8080
8181 class Status(Test):
82
82
8383 def index(self):
8484 return "normal"
85
85
8686 def blank(self):
8787 cherrypy.response.status = ""
88
88
8989 # According to RFC 2616, new status codes are OK as long as they
9090 # are between 100 and 599.
91
91
9292 # Here is an illegal code...
9393 def illegal(self):
9494 cherrypy.response.status = 781
9595 return "oops"
96
96
9797 # ...and here is an unknown but legal code.
9898 def unknown(self):
9999 cherrypy.response.status = "431 My custom error"
100100 return "funky"
101
101
102102 # Non-numeric code
103103 def bad(self):
104104 cherrypy.response.status = "error"
105105 return "bad news"
106
106
107107 statuses = []
108
108109 def on_end_resource_stage(self):
109110 return repr(self.statuses)
110111 on_end_resource_stage._cp_config = {'tools.log_status.on': True}
111112
112
113113 class Redirect(Test):
114
114
115115 class Error:
116116 _cp_config = {"tools.err_redirect.on": True,
117117 "tools.err_redirect.url": "/errpage",
118118 "tools.err_redirect.internal": False,
119119 }
120
120
121121 def index(self):
122122 raise NameError("redirect_test")
123123 index.exposed = True
124124 error = Error()
125
125
126126 def index(self):
127127 return "child"
128
128
129129 def custom(self, url, code):
130130 raise cherrypy.HTTPRedirect(url, code)
131
131
132132 def by_code(self, code):
133133 raise cherrypy.HTTPRedirect("somewhere%20else", code)
134134 by_code._cp_config = {'tools.trailing_slash.extra': True}
135
135
136136 def nomodify(self):
137137 raise cherrypy.HTTPRedirect("", 304)
138
138
139139 def proxy(self):
140140 raise cherrypy.HTTPRedirect("proxy", 305)
141
141
142142 def stringify(self):
143143 return str(cherrypy.HTTPRedirect("/"))
144
144
145145 def fragment(self, frag):
146146 raise cherrypy.HTTPRedirect("/some/url#%s" % frag)
147
147
148 def url_with_quote(self):
149 raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
150
148151 def login_redir():
149152 if not getattr(cherrypy.request, "login", None):
150153 raise cherrypy.InternalRedirect("/internalredirect/login")
151154 tools.login_redir = _cptools.Tool('before_handler', login_redir)
152
155
153156 def redir_custom():
154157 raise cherrypy.InternalRedirect("/internalredirect/custom_err")
155
158
156159 class InternalRedirect(Test):
157
160
158161 def index(self):
159162 raise cherrypy.InternalRedirect("/")
160
163
161164 def choke(self):
162165 return 3 / 0
163166 choke.exposed = True
164167 choke._cp_config = {'hooks.before_error_response': redir_custom}
165
168
166169 def relative(self, a, b):
167170 raise cherrypy.InternalRedirect("cousin?t=6")
168
171
169172 def cousin(self, t):
170173 assert cherrypy.request.prev.closed
171174 return cherrypy.request.prev.query_string
172
175
173176 def petshop(self, user_id):
174177 if user_id == "parrot":
175178 # Trade it for a slug when redirecting
176 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug')
179 raise cherrypy.InternalRedirect(
180 '/image/getImagesByUser?user_id=slug')
177181 elif user_id == "terrier":
178182 # Trade it for a fish when redirecting
179 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish')
183 raise cherrypy.InternalRedirect(
184 '/image/getImagesByUser?user_id=fish')
180185 else:
181186 # This should pass the user_id through to getImagesByUser
182187 raise cherrypy.InternalRedirect(
183188 '/image/getImagesByUser?user_id=%s' % str(user_id))
184
185 # We support Python 2.3, but the @-deco syntax would look like this:
189
190 # We support Python 2.3, but the @-deco syntax would look like
191 # this:
186192 # @tools.login_redir()
187193 def secure(self):
188194 return "Welcome!"
190196 # Since calling the tool returns the same function you pass in,
191197 # you could skip binding the return value, and just write:
192198 # tools.login_redir()(secure)
193
199
194200 def login(self):
195201 return "Please log in"
196
202
197203 def custom_err(self):
198204 return "Something went horribly wrong."
199
205
200206 def early_ir(self, arg):
201207 return "whatever"
202208 early_ir._cp_config = {'hooks.before_request_body': redir_custom}
203
204
209
205210 class Image(Test):
206
211
207212 def getImagesByUser(self, user_id):
208213 return "0 images for %s" % user_id
209214
210
211215 class Flatten(Test):
212
216
213217 def as_string(self):
214218 return "content"
215
219
216220 def as_list(self):
217221 return ["con", "tent"]
218
222
219223 def as_yield(self):
220224 yield ntob("content")
221
225
222226 def as_dblyield(self):
223227 yield self.as_yield()
224228 as_dblyield._cp_config = {'tools.flatten.on': True}
225
229
226230 def as_refyield(self):
227231 for chunk in self.as_yield():
228232 yield chunk
229
230
233
231234 class Ranges(Test):
232
235
233236 def get_ranges(self, bytes):
234237 return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
235
238
236239 def slice_file(self):
237240 path = os.path.join(os.getcwd(), os.path.dirname(__file__))
238 return static.serve_file(os.path.join(path, "static/index.html"))
239
241 return static.serve_file(
242 os.path.join(path, "static/index.html"))
240243
241244 class Cookies(Test):
242
245
243246 def single(self, name):
244247 cookie = cherrypy.request.cookie[name]
245248 # Python2's SimpleCookie.__setitem__ won't take unicode keys.
246249 cherrypy.response.cookie[str(name)] = cookie.value
247
250
248251 def multiple(self, names):
249252 for name in names:
250253 cookie = cherrypy.request.cookie[name]
251 # Python2's SimpleCookie.__setitem__ won't take unicode keys.
254 # Python2's SimpleCookie.__setitem__ won't take unicode
255 # keys.
252256 cherrypy.response.cookie[str(name)] = cookie.value
253257
254258 def append_headers(header_list, debug=False):
257261 "Extending response headers with %s" % repr(header_list),
258262 "TOOLS.APPEND_HEADERS")
259263 cherrypy.serving.response.header_list.extend(header_list)
260 cherrypy.tools.append_headers = cherrypy.Tool('on_end_resource', append_headers)
261
264 cherrypy.tools.append_headers = cherrypy.Tool(
265 'on_end_resource', append_headers)
266
262267 class MultiHeader(Test):
263
268
264269 def header_list(self):
265270 pass
266271 header_list = cherrypy.tools.append_headers(header_list=[
267272 (ntob('WWW-Authenticate'), ntob('Negotiate')),
268273 (ntob('WWW-Authenticate'), ntob('Basic realm="foo"')),
269 ])(header_list)
270
274 ])(header_list)
275
271276 def commas(self):
272 cherrypy.response.headers['WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
273
277 cherrypy.response.headers[
278 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
274279
275280 cherrypy.tree.mount(root)
276281 setup_server = staticmethod(setup_server)
277
278282
279283 def testStatus(self):
280284 self.getPage("/status/")
281285 self.assertBody('normal')
282286 self.assertStatus(200)
283
287
284288 self.getPage("/status/blank")
285289 self.assertBody('')
286290 self.assertStatus(200)
287
291
288292 self.getPage("/status/illegal")
289293 self.assertStatus(500)
290294 msg = "Illegal response status from server (781 is out of range)."
291295 self.assertErrorPage(500, msg)
292
296
293297 if not getattr(cherrypy.server, 'using_apache', False):
294298 self.getPage("/status/unknown")
295299 self.assertBody('funky')
296300 self.assertStatus(431)
297
301
298302 self.getPage("/status/bad")
299303 self.assertStatus(500)
300304 msg = "Illegal response status from server ('error' is non-numeric)."
312316 # Make sure GET params are preserved.
313317 self.getPage("/redirect?id=3")
314318 self.assertStatus(301)
315 self.assertInBody("<a href='%s/redirect/?id=3'>"
316 "%s/redirect/?id=3</a>" % (self.base(), self.base()))
317
319 self.assertMatchesBody('<a href=([\'"])%s/redirect/[?]id=3\\1>'
320 "%s/redirect/[?]id=3</a>" % (self.base(), self.base()))
321
318322 if self.prefix():
319323 # Corner case: the "trailing slash" redirect could be tricky if
320324 # we're using a virtual root and the URI is "/vroot" (no slash).
321325 self.getPage("")
322326 self.assertStatus(301)
323 self.assertInBody("<a href='%s/'>%s/</a>" %
327 self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
324328 (self.base(), self.base()))
325
329
326330 # Test that requests for NON-index methods WITH a trailing slash
327331 # get redirected to the same URI path WITHOUT a trailing slash.
328332 # Make sure GET params are preserved.
329333 self.getPage("/redirect/by_code/?code=307")
330334 self.assertStatus(301)
331 self.assertInBody("<a href='%s/redirect/by_code?code=307'>"
332 "%s/redirect/by_code?code=307</a>"
335 self.assertMatchesBody("<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
336 "%s/redirect/by_code[?]code=307</a>"
333337 % (self.base(), self.base()))
334
338
335339 # If the trailing_slash tool is off, CP should just continue
336340 # as if the slashes were correct. But it needs some help
337341 # inside cherrypy.url to form correct output.
339343 self.assertBody('%s/url/page1' % self.base())
340344 self.getPage('/url/leaf/?path_info=page1')
341345 self.assertBody('%s/url/page1' % self.base())
342
346
343347 def testRedirect(self):
344348 self.getPage("/redirect/")
345349 self.assertBody('child')
346350 self.assertStatus(200)
347
351
348352 self.getPage("/redirect/by_code?code=300")
349 self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
353 self.assertMatchesBody(
354 r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
350355 self.assertStatus(300)
351
356
352357 self.getPage("/redirect/by_code?code=301")
353 self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
358 self.assertMatchesBody(
359 r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
354360 self.assertStatus(301)
355
361
356362 self.getPage("/redirect/by_code?code=302")
357 self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
363 self.assertMatchesBody(
364 r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
358365 self.assertStatus(302)
359
366
360367 self.getPage("/redirect/by_code?code=303")
361 self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
368 self.assertMatchesBody(
369 r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
362370 self.assertStatus(303)
363
371
364372 self.getPage("/redirect/by_code?code=307")
365 self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
373 self.assertMatchesBody(
374 r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
366375 self.assertStatus(307)
367
376
368377 self.getPage("/redirect/nomodify")
369378 self.assertBody('')
370379 self.assertStatus(304)
371
380
372381 self.getPage("/redirect/proxy")
373382 self.assertBody('')
374383 self.assertStatus(305)
375
384
376385 # HTTPRedirect on error
377386 self.getPage("/redirect/error/")
378387 self.assertStatus(('302 Found', '303 See Other'))
379388 self.assertInBody('/errpage')
380
389
381390 # Make sure str(HTTPRedirect()) works.
382391 self.getPage("/redirect/stringify", protocol="HTTP/1.0")
383392 self.assertStatus(200)
386395 self.getPage("/redirect/stringify", protocol="HTTP/1.1")
387396 self.assertStatus(200)
388397 self.assertBody("(['%s/'], 303)" % self.base())
389
398
390399 # check that #fragments are handled properly
391400 # http://skrb.org/ietf/http_errata.html#location-fragments
392401 frag = "foo"
393402 self.getPage("/redirect/fragment/%s" % frag)
394 self.assertMatchesBody(r"<a href='(.*)\/some\/url\#%s'>\1\/some\/url\#%s</a>" % (frag, frag))
403 self.assertMatchesBody(
404 r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
405 frag, frag))
395406 loc = self.assertHeader('Location')
396407 assert loc.endswith("#%s" % frag)
397408 self.assertStatus(('302 Found', '303 See Other'))
398
409
399410 # check injection protection
400 # See http://www.cherrypy.org/ticket/1003
401 self.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval")
411 # See https://bitbucket.org/cherrypy/cherrypy/issue/1003
412 self.getPage(
413 "/redirect/custom?"
414 "code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval")
402415 self.assertStatus(303)
403416 loc = self.assertHeader('Location')
404417 assert 'Set-Cookie' in loc
405418 self.assertNoHeader('Set-Cookie')
406
419
420 def assertValidXHTML():
421 from xml.etree import ElementTree
422 try:
423 ElementTree.fromstring('<html><body>%s</body></html>' % self.body)
424 except ElementTree.ParseError as e:
425 self._handlewebError('automatically generated redirect '
426 'did not generate well-formed html')
427
428 # check redirects to URLs generated valid HTML - we check this
429 # by seeing if it appears as valid XHTML.
430 self.getPage("/redirect/by_code?code=303")
431 self.assertStatus(303)
432 assertValidXHTML()
433
434 # do the same with a url containing quote characters.
435 self.getPage("/redirect/url_with_quote")
436 self.assertStatus(303)
437 assertValidXHTML()
438
407439 def test_InternalRedirect(self):
408440 # InternalRedirect
409441 self.getPage("/internalredirect/")
410442 self.assertBody('hello')
411443 self.assertStatus(200)
412
444
413445 # Test passthrough
414 self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
446 self.getPage(
447 "/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
415448 self.assertBody('0 images for Sir-not-appearing-in-this-film')
416449 self.assertStatus(200)
417
450
418451 # Test args
419452 self.getPage("/internalredirect/petshop?user_id=parrot")
420453 self.assertBody('0 images for slug')
421454 self.assertStatus(200)
422
455
423456 # Test POST
424457 self.getPage("/internalredirect/petshop", method="POST",
425458 body="user_id=terrier")
426459 self.assertBody('0 images for fish')
427460 self.assertStatus(200)
428
461
429462 # Test ir before body read
430463 self.getPage("/internalredirect/early_ir", method="POST",
431464 body="arg=aha!")
432465 self.assertBody("Something went horribly wrong.")
433466 self.assertStatus(200)
434
467
435468 self.getPage("/internalredirect/secure")
436469 self.assertBody('Please log in')
437470 self.assertStatus(200)
438
471
439472 # Relative path in InternalRedirect.
440473 # Also tests request.prev.
441474 self.getPage("/internalredirect/relative?a=3&b=5")
442475 self.assertBody("a=3&b=5")
443476 self.assertStatus(200)
444
477
445478 # InternalRedirect on error
446479 self.getPage("/internalredirect/choke")
447480 self.assertStatus(200)
448481 self.assertBody("Something went horribly wrong.")
449
482
450483 def testFlatten(self):
451484 for url in ["/flatten/as_string", "/flatten/as_list",
452485 "/flatten/as_yield", "/flatten/as_dblyield",
453486 "/flatten/as_refyield"]:
454487 self.getPage(url)
455488 self.assertBody('content')
456
489
457490 def testRanges(self):
458491 self.getPage("/ranges/get_ranges?bytes=3-6")
459492 self.assertBody("[(3, 7)]")
460
493
461494 # Test multiple ranges and a suffix-byte-range-spec, for good measure.
462495 self.getPage("/ranges/get_ranges?bytes=2-4,-1")
463496 self.assertBody("[(2, 5), (7, 8)]")
464
497
465498 # Get a partial file.
466499 if cherrypy.server.protocol_version == "HTTP/1.1":
467500 self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
469502 self.assertHeader("Content-Type", "text/html;charset=utf-8")
470503 self.assertHeader("Content-Range", "bytes 2-5/14")
471504 self.assertBody("llo,")
472
505
473506 # What happens with overlapping ranges (and out of order, too)?
474507 self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')])
475508 self.assertStatus(206)
490523 "--%s--\r\n" % (boundary, boundary, boundary))
491524 self.assertBody(expected_body)
492525 self.assertHeader("Content-Length")
493
526
494527 # Test "416 Requested Range Not Satisfiable"
495528 self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')])
496529 self.assertStatus(416)
503536 self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
504537 self.assertStatus(200)
505538 self.assertBody("Hello, world\r\n")
506
539
507540 def testFavicon(self):
508541 # favicon.ico is served by staticfile.
509542 icofilename = os.path.join(localDir, "../favicon.ico")
510543 icofile = open(icofilename, "rb")
511544 data = icofile.read()
512545 icofile.close()
513
546
514547 self.getPage("/favicon.ico")
515548 self.assertBody(data)
516
549
517550 def testCookies(self):
518551 if sys.version_info >= (2, 5):
519552 header_value = lambda x: x
520553 else:
521 header_value = lambda x: x+';'
522
554 header_value = lambda x: x + ';'
555
523556 self.getPage("/cookies/single?name=First",
524557 [('Cookie', 'First=Dinsdale;')])
525558 self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
526
559
527560 self.getPage("/cookies/multiple?names=First&names=Last",
528561 [('Cookie', 'First=Dinsdale; Last=Piranha;'),
529562 ])
530563 self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
531564 self.assertHeader('Set-Cookie', header_value('Last=Piranha'))
532
533 self.getPage("/cookies/single?name=Something-With:Colon",
534 [('Cookie', 'Something-With:Colon=some-value')])
565
566 self.getPage("/cookies/single?name=Something-With%2CComma",
567 [('Cookie', 'Something-With,Comma=some-value')])
535568 self.assertStatus(400)
536
569
537570 def testDefaultContentType(self):
538571 self.getPage('/')
539572 self.assertHeader('Content-Type', 'text/html;charset=utf-8')
541574 self.getPage('/')
542575 self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
543576 self.getPage('/defct/html')
544
577
545578 def test_multiple_headers(self):
546579 self.getPage('/multiheader/header_list')
547 self.assertEqual([(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
548 [('WWW-Authenticate', 'Negotiate'),
549 ('WWW-Authenticate', 'Basic realm="foo"'),
550 ])
580 self.assertEqual(
581 [(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
582 [('WWW-Authenticate', 'Negotiate'),
583 ('WWW-Authenticate', 'Basic realm="foo"'),
584 ])
551585 self.getPage('/multiheader/commas')
552586 self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
553
587
554588 def test_cherrypy_url(self):
555589 # Input relative to current
556590 self.getPage('/url/leaf?path_info=page1')
562596 self.getPage('/url/leaf?path_info=page1',
563597 headers=[('Host', host)])
564598 self.assertBody('%s://%s/url/page1' % (self.scheme, host))
565
599
566600 # Input is 'absolute'; that is, relative to script_name
567601 self.getPage('/url/leaf?path_info=/page1')
568602 self.assertBody('%s/page1' % self.base())
569603 self.getPage('/url/?path_info=/page1')
570604 self.assertBody('%s/page1' % self.base())
571
605
572606 # Single dots
573607 self.getPage('/url/leaf?path_info=./page1')
574608 self.assertBody('%s/url/page1' % self.base())
576610 self.assertBody('%s/url/other/page1' % self.base())
577611 self.getPage('/url/?path_info=/other/./page1')
578612 self.assertBody('%s/other/page1' % self.base())
579
613
580614 # Double dots
581615 self.getPage('/url/leaf?path_info=../page1')
582616 self.assertBody('%s/page1' % self.base())
584618 self.assertBody('%s/url/page1' % self.base())
585619 self.getPage('/url/leaf?path_info=/other/../page1')
586620 self.assertBody('%s/page1' % self.base())
587
621
588622 # Output relative to current path or script_name
589623 self.getPage('/url/?path_info=page1&relative=True')
590624 self.assertBody('page1')
598632 self.assertBody('../page1')
599633 self.getPage('/url/?path_info=other/../page1&relative=True')
600634 self.assertBody('page1')
601
635
602636 # Output relative to /
603637 self.getPage('/baseurl?path_info=ab&relative=True')
604638 self.assertBody('ab')
605639 # Output relative to /
606640 self.getPage('/baseurl?path_info=/ab&relative=True')
607641 self.assertBody('ab')
608
642
609643 # absolute-path references ("server-relative")
610644 # Input relative to current
611645 self.getPage('/url/leaf?path_info=page1&relative=server')
617651 self.assertBody('/page1')
618652 self.getPage('/url/?path_info=/page1&relative=server')
619653 self.assertBody('/page1')
620
654
621655 def test_expose_decorator(self):
622656 if not sys.version_info >= (2, 5):
623657 return self.skip("skipped (Python 2.5+ only) ")
624
658
625659 # Test @expose
626660 self.getPage("/expose_dec/no_call")
627661 self.assertStatus(200)
628662 self.assertBody("Mr E. R. Bradshaw")
629
663
630664 # Test @expose()
631665 self.getPage("/expose_dec/call_empty")
632666 self.assertStatus(200)
633667 self.assertBody("Mrs. B.J. Smegma")
634
668
635669 # Test @expose("alias")
636670 self.getPage("/expose_dec/call_alias")
637671 self.assertStatus(200)
640674 self.getPage("/expose_dec/nesbitt")
641675 self.assertStatus(200)
642676 self.assertBody("Mr Nesbitt")
643
677
644678 # Test @expose(["alias1", "alias2"])
645679 self.getPage("/expose_dec/alias1")
646680 self.assertStatus(200)
652686 self.getPage("/expose_dec/andrews")
653687 self.assertStatus(200)
654688 self.assertBody("Mr Ken Andrews")
655
689
656690 # Test @expose(alias="alias")
657691 self.getPage("/expose_dec/alias3")
658692 self.assertStatus(200)
665699 def break_header():
666700 # Add a header after finalize that is invalid
667701 cherrypy.serving.response.header_list.append((2, 3))
668 cherrypy.tools.break_header = cherrypy.Tool('on_end_resource', break_header)
669
702 cherrypy.tools.break_header = cherrypy.Tool(
703 'on_end_resource', break_header)
704
670705 class Root:
706
671707 def index(self):
672708 return "hello"
673709 index.exposed = True
674
710
675711 def start_response_error(self):
676712 return "salud!"
677713 start_response_error._cp_config = {'tools.break_header.on': True}
678714 root = Root()
679
715
680716 cherrypy.tree.mount(root)
681717 setup_server = staticmethod(setup_server)
682718
683719 def test_start_response_error(self):
684720 self.getPage("/start_response_error")
685721 self.assertStatus(500)
686 self.assertInBody("TypeError: response.header_list key 2 is not a byte string.")
687
722 self.assertInBody(
723 "TypeError: response.header_list key 2 is not a byte string.")
55 script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
66
77
8
98 def setup_server():
109 class SubSubRoot:
10
1111 def index(self):
1212 return "SubSubRoot index"
1313 index.exposed = True
3030 }
3131
3232 class SubRoot:
33
3334 def index(self):
3435 return "SubRoot index"
3536 index.exposed = True
4950 '1': SubRoot(),
5051 '2': SubRoot(),
5152 }
53
5254 class Root:
55
5356 def index(self):
5457 return "index"
5558 index.exposed = True
6972 # DynamicNodeAndMethodDispatcher example.
7073 # This example exposes a fairly naive HTTP api
7174 class User(object):
75
7276 def __init__(self, id, name):
7377 self.id = id
7478 self.name = name
7579
7680 def __unicode__(self):
7781 return unicode(self.name)
82
7883 def __str__(self):
7984 return str(self.name)
8085
110115
111116 class UserInstanceNode(object):
112117 exposed = True
118
113119 def __init__(self, id):
114120 self.id = id
115121 self.user = user_lookup.get(id, None)
134140
135141 def PUT(self, name):
136142 """
137 Create a new user with the specified id, or edit it if it already exists
143 Create a new user with the specified id, or edit it if it already
144 exists
138145 """
139146 if self.user:
140147 # Edit the current user
153160 del self.user
154161 return "DELETE %d" % id
155162
156
157163 class ABHandler:
164
158165 class CustomDispatch:
166
159167 def index(self, a, b):
160168 return "custom"
161169 index.exposed = True
162
170
163171 def _cp_dispatch(self, vpath):
164172 """Make sure that if we don't pop anything from vpath,
165173 processing still works.
166174 """
167175 return self.CustomDispatch()
168
176
169177 def index(self, a, b=None):
170 body = [ 'a:' + str(a) ]
178 body = ['a:' + str(a)]
171179 if b is not None:
172180 body.append(',b:' + str(b))
173181 return ''.join(body)
174182 index.exposed = True
175
183
176184 def delete(self, a, b):
177185 return 'deleting ' + str(a) + ' and ' + str(b)
178186 delete.exposed = True
179
187
180188 class IndexOnly:
189
181190 def _cp_dispatch(self, vpath):
182 """Make sure that popping ALL of vpath still shows the index
191 """Make sure that popping ALL of vpath still shows the index
183192 handler.
184193 """
185194 while vpath:
186195 vpath.pop()
187196 return self
188
197
189198 def index(self):
190199 return "IndexOnly index"
191200 index.exposed = True
192
201
193202 class DecoratedPopArgs:
203
194204 """Test _cp_dispatch with @cherrypy.popargs."""
205
195206 def index(self):
196207 return "no params"
197208 index.exposed = True
198
209
199210 def hi(self):
200211 return "hi was not interpreted as 'a' param"
201212 hi.exposed = True
202 DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs)
203
213 DecoratedPopArgs = cherrypy.popargs(
214 'a', 'b', handler=ABHandler())(DecoratedPopArgs)
215
204216 class NonDecoratedPopArgs:
217
205218 """Test _cp_dispatch = cherrypy.popargs()"""
206
219
207220 _cp_dispatch = cherrypy.popargs('a')
208
221
209222 def index(self, a):
210223 return "index: " + str(a)
211224 index.exposed = True
212
225
213226 class ParameterizedHandler:
227
214228 """Special handler created for each request"""
215
229
216230 def __init__(self, a):
217231 self.a = a
218
232
219233 def index(self):
220234 if 'a' in cherrypy.request.params:
221 raise Exception("Parameterized handler argument ended up in request.params")
235 raise Exception(
236 "Parameterized handler argument ended up in "
237 "request.params")
222238 return self.a
223239 index.exposed = True
224
240
225241 class ParameterizedPopArgs:
242
226243 """Test cherrypy.popargs() with a function call handler"""
227 ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs)
228
244 ParameterizedPopArgs = cherrypy.popargs(
245 'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
246
229247 Root.decorated = DecoratedPopArgs()
230248 Root.undecorated = NonDecoratedPopArgs()
231249 Root.index_only = IndexOnly()
236254 md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
237255 for url in script_names:
238256 conf = {'/': {
239 'user': (url or "/").split("/")[-2],
240 },
241 '/users': {
242 'request.dispatch': md
243 },
244 }
257 'user': (url or "/").split("/")[-2],
258 },
259 '/users': {
260 'request.dispatch': md
261 },
262 }
245263 cherrypy.tree.mount(Root(), url, conf)
264
246265
247266 class DynamicObjectMappingTest(helper.CPWebCase):
248267 setup_server = staticmethod(setup_server)
353372 self.assertHeader('Allow', headers)
354373
355374 # Make sure POSTs update already existings resources
356 self.getPage("/users/%d" % id, method='POST', body="name=%s" % updatedname)
375 self.getPage("/users/%d" %
376 id, method='POST', body="name=%s" % updatedname)
357377 self.assertBody("POST %d" % id)
358378 self.assertHeader('Allow', headers)
359379
360380 # Make sure PUTs Update already existing resources.
361 self.getPage("/users/%d" % id, method='PUT', body="name=%s" % updatedname)
381 self.getPage("/users/%d" %
382 id, method='PUT', body="name=%s" % updatedname)
362383 self.assertBody("PUT %d" % id)
363384 self.assertHeader('Allow', headers)
364385
367388 self.assertBody("DELETE %d" % id)
368389 self.assertHeader('Allow', headers)
369390
370
371391 # GET acts like a container
372392 self.getPage("/users")
373393 self.assertBody("[]")
374394 self.assertHeader('Allow', 'GET, HEAD, POST')
375
395
376396 def testVpathDispatch(self):
377397 self.getPage("/decorated/")
378398 self.assertBody("no params")
379
399
380400 self.getPage("/decorated/hi")
381401 self.assertBody("hi was not interpreted as 'a' param")
382
402
383403 self.getPage("/decorated/yo/")
384404 self.assertBody("a:yo")
385
405
386406 self.getPage("/decorated/yo/there/")
387407 self.assertBody("a:yo,b:there")
388
408
389409 self.getPage("/decorated/yo/there/delete")
390410 self.assertBody("deleting yo and there")
391
411
392412 self.getPage("/decorated/yo/there/handled_by_dispatch/")
393413 self.assertBody("custom")
394
414
395415 self.getPage("/undecorated/blah/")
396416 self.assertBody("index: blah")
397
417
398418 self.getPage("/index_only/a/b/c/d/e/f/g/")
399419 self.assertBody("IndexOnly index")
400
420
401421 self.getPage("/parameter_test/argument2/")
402422 self.assertBody("argument2")
403
1717
1818 def setup_server():
1919 class Root:
20
2021 def index(self, param):
21 assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode)
22 assert param == europoundUnicode, "%r != %r" % (
23 param, europoundUnicode)
2224 yield europoundUnicode
2325 index.exposed = True
24
26
2527 def mao_zedong(self):
2628 return sing
2729 mao_zedong.exposed = True
28
30
2931 def utf8(self):
3032 return sing8
3133 utf8.exposed = True
3234 utf8._cp_config = {'tools.encode.encoding': 'utf-8'}
33
35
3436 def cookies_and_headers(self):
3537 # if the headers have non-ascii characters and a cookie has
3638 # any part which is unicode (even ascii), the response
3739 # should not fail.
3840 cherrypy.response.cookie['candy'] = 'bar'
3941 cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
40 cherrypy.response.headers['Some-Header'] = 'My d\xc3\xb6g has fleas'
42 cherrypy.response.headers[
43 'Some-Header'] = 'My d\xc3\xb6g has fleas'
4144 return 'Any content'
4245 cookies_and_headers.exposed = True
4346
4447 def reqparams(self, *args, **kwargs):
45 return ntob(', ').join([": ".join((k, v)).encode('utf8')
46 for k, v in cherrypy.request.params.items()])
48 return ntob(', ').join(
49 [": ".join((k, v)).encode('utf8')
50 for k, v in sorted(cherrypy.request.params.items())]
51 )
4752 reqparams.exposed = True
48
53
4954 def nontext(self, *args, **kwargs):
50 cherrypy.response.headers['Content-Type'] = 'application/binary'
55 cherrypy.response.headers[
56 'Content-Type'] = 'application/binary'
5157 return '\x00\x01\x02\x03'
5258 nontext.exposed = True
5359 nontext._cp_config = {'tools.encode.text_only': False,
5460 'tools.encode.add_charset': True,
5561 }
56
62
5763 class GZIP:
64
5865 def index(self):
5966 yield "Hello, world"
6067 index.exposed = True
61
68
6269 def noshow(self):
63 # Test for ticket #147, where yield showed no exceptions (content-
64 # encoding was still gzip even though traceback wasn't zipped).
70 # Test for ticket #147, where yield showed no exceptions
71 # (content-encoding was still gzip even though traceback
72 # wasn't zipped).
6573 raise IndexError()
6674 yield "Here be dragons"
6775 noshow.exposed = True
6876 # Turn encoding off so the gzip tool is the one doing the collapse.
6977 noshow._cp_config = {'tools.encode.on': False}
70
78
7179 def noshow_stream(self):
72 # Test for ticket #147, where yield showed no exceptions (content-
73 # encoding was still gzip even though traceback wasn't zipped).
80 # Test for ticket #147, where yield showed no exceptions
81 # (content-encoding was still gzip even though traceback
82 # wasn't zipped).
7483 raise IndexError()
7584 yield "Here be dragons"
7685 noshow_stream.exposed = True
7786 noshow_stream._cp_config = {'response.stream': True}
78
87
7988 class Decode:
89
8090 def extra_charset(self, *args, **kwargs):
8191 return ', '.join([": ".join((k, v))
8292 for k, v in cherrypy.request.params.items()])
8494 extra_charset._cp_config = {
8595 'tools.decode.on': True,
8696 'tools.decode.default_encoding': ['utf-16'],
87 }
88
97 }
98
8999 def force_charset(self, *args, **kwargs):
90100 return ', '.join([": ".join((k, v))
91101 for k, v in cherrypy.request.params.items()])
93103 force_charset._cp_config = {
94104 'tools.decode.on': True,
95105 'tools.decode.encoding': 'utf-16',
96 }
97
106 }
107
98108 root = Root()
99109 root.gzip = GZIP()
100110 root.decode = Decode()
105115 europoundUtf8 = europoundUnicode.encode('utf-8')
106116 self.getPage(ntob('/?param=') + europoundUtf8)
107117 self.assertBody(europoundUtf8)
108
118
109119 # Encoded utf8 query strings MUST be parsed correctly.
110120 # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
111121 self.getPage("/reqparams?q=%C2%A3")
112122 # The return value will be encoded as utf8.
113123 self.assertBody(ntob("q: \xc2\xa3"))
114
124
115125 # Query strings that are incorrectly encoded MUST raise 404.
116126 # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX
117127 self.getPage("/reqparams?q=%A3")
118128 self.assertStatus(404)
119 self.assertErrorPage(404,
129 self.assertErrorPage(
130 404,
120131 "The given query string could not be processed. Query "
121132 "strings for this resource must be encoded with 'utf8'.")
122
133
123134 def test_urlencoded_decoding(self):
124135 # Test the decoding of an application/x-www-form-urlencoded entity.
125136 europoundUtf8 = europoundUnicode.encode('utf-8')
126 body=ntob("param=") + europoundUtf8
127 self.getPage('/', method='POST',
128 headers=[("Content-Type", "application/x-www-form-urlencoded"),
129 ("Content-Length", str(len(body))),
130 ],
137 body = ntob("param=") + europoundUtf8
138 self.getPage('/',
139 method='POST',
140 headers=[
141 ("Content-Type", "application/x-www-form-urlencoded"),
142 ("Content-Length", str(len(body))),
143 ],
131144 body=body),
132145 self.assertBody(europoundUtf8)
133
146
134147 # Encoded utf8 entities MUST be parsed and decoded correctly.
135148 # Here, q is the POUND SIGN U+00A3 encoded in utf8
136149 body = ntob("q=\xc2\xa3")
137150 self.getPage('/reqparams', method='POST',
138 headers=[("Content-Type", "application/x-www-form-urlencoded"),
139 ("Content-Length", str(len(body))),
140 ],
141 body=body),
142 self.assertBody(ntob("q: \xc2\xa3"))
143
151 headers=[(
152 "Content-Type", "application/x-www-form-urlencoded"),
153 ("Content-Length", str(len(body))),
154 ],
155 body=body),
156 self.assertBody(ntob("q: \xc2\xa3"))
157
144158 # ...and in utf16, which is not in the default attempt_charsets list:
145159 body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
146 self.getPage('/reqparams', method='POST',
147 headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-16"),
148 ("Content-Length", str(len(body))),
149 ],
150 body=body),
151 self.assertBody(ntob("q: \xc2\xa3"))
152
160 self.getPage('/reqparams',
161 method='POST',
162 headers=[
163 ("Content-Type",
164 "application/x-www-form-urlencoded;charset=utf-16"),
165 ("Content-Length", str(len(body))),
166 ],
167 body=body),
168 self.assertBody(ntob("q: \xc2\xa3"))
169
153170 # Entities that are incorrectly encoded MUST raise 400.
154171 # Here, q is the POUND SIGN U+00A3 encoded in utf16, but
155172 # the Content-Type incorrectly labels it utf-8.
156173 body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
157 self.getPage('/reqparams', method='POST',
158 headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"),
159 ("Content-Length", str(len(body))),
160 ],
174 self.getPage('/reqparams',
175 method='POST',
176 headers=[
177 ("Content-Type",
178 "application/x-www-form-urlencoded;charset=utf-8"),
179 ("Content-Length", str(len(body))),
180 ],
161181 body=body),
162182 self.assertStatus(400)
163 self.assertErrorPage(400,
183 self.assertErrorPage(
184 400,
164185 "The request entity could not be decoded. The following charsets "
165186 "were attempted: ['utf-8']")
166
187
167188 def test_decode_tool(self):
168189 # An extra charset should be tried first, and succeed if it matches.
169190 # Here, we add utf-16 as a charset and pass a utf-16 body.
170191 body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
171192 self.getPage('/decode/extra_charset', method='POST',
172 headers=[("Content-Type", "application/x-www-form-urlencoded"),
173 ("Content-Length", str(len(body))),
174 ],
175 body=body),
176 self.assertBody(ntob("q: \xc2\xa3"))
177
193 headers=[(
194 "Content-Type", "application/x-www-form-urlencoded"),
195 ("Content-Length", str(len(body))),
196 ],
197 body=body),
198 self.assertBody(ntob("q: \xc2\xa3"))
199
178200 # An extra charset should be tried first, and continue to other default
179201 # charsets if it doesn't match.
180202 # Here, we add utf-16 as a charset but still pass a utf-8 body.
181203 body = ntob("q=\xc2\xa3")
182204 self.getPage('/decode/extra_charset', method='POST',
183 headers=[("Content-Type", "application/x-www-form-urlencoded"),
184 ("Content-Length", str(len(body))),
185 ],
186 body=body),
187 self.assertBody(ntob("q: \xc2\xa3"))
188
205 headers=[(
206 "Content-Type", "application/x-www-form-urlencoded"),
207 ("Content-Length", str(len(body))),
208 ],
209 body=body),
210 self.assertBody(ntob("q: \xc2\xa3"))
211
189212 # An extra charset should error if force is True and it doesn't match.
190213 # Here, we force utf-16 as a charset but still pass a utf-8 body.
191214 body = ntob("q=\xc2\xa3")
192215 self.getPage('/decode/force_charset', method='POST',
193 headers=[("Content-Type", "application/x-www-form-urlencoded"),
194 ("Content-Length", str(len(body))),
195 ],
196 body=body),
197 self.assertErrorPage(400,
216 headers=[(
217 "Content-Type", "application/x-www-form-urlencoded"),
218 ("Content-Length", str(len(body))),
219 ],
220 body=body),
221 self.assertErrorPage(
222 400,
198223 "The request entity could not be decoded. The following charsets "
199224 "were attempted: ['utf-16']")
200
225
201226 def test_multipart_decoding(self):
202227 # Test the decoding of a multipart entity when the charset (utf16) is
203228 # explicitly given.
204 body=ntob('\r\n'.join(['--X',
205 'Content-Type: text/plain;charset=utf-16',
206 'Content-Disposition: form-data; name="text"',
207 '',
208 '\xff\xfea\x00b\x00\x1c c\x00',
209 '--X',
210 'Content-Type: text/plain;charset=utf-16',
211 'Content-Disposition: form-data; name="submit"',
212 '',
213 '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
214 '--X--']))
229 body = ntob('\r\n'.join([
230 '--X',
231 'Content-Type: text/plain;charset=utf-16',
232 'Content-Disposition: form-data; name="text"',
233 '',
234 '\xff\xfea\x00b\x00\x1c c\x00',
235 '--X',
236 'Content-Type: text/plain;charset=utf-16',
237 'Content-Disposition: form-data; name="submit"',
238 '',
239 '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
240 '--X--'
241 ]))
215242 self.getPage('/reqparams', method='POST',
216 headers=[("Content-Type", "multipart/form-data;boundary=X"),
217 ("Content-Length", str(len(body))),
218 ],
219 body=body),
220 self.assertBody(ntob("text: ab\xe2\x80\x9cc, submit: Create"))
221
243 headers=[(
244 "Content-Type", "multipart/form-data;boundary=X"),
245 ("Content-Length", str(len(body))),
246 ],
247 body=body),
248 self.assertBody(ntob("submit: Create, text: ab\xe2\x80\x9cc"))
249
222250 def test_multipart_decoding_no_charset(self):
223251 # Test the decoding of a multipart entity when the charset (utf8) is
224252 # NOT explicitly given, but is in the list of charsets to attempt.
225 body=ntob('\r\n'.join(['--X',
226 'Content-Disposition: form-data; name="text"',
227 '',
228 '\xe2\x80\x9c',
229 '--X',
230 'Content-Disposition: form-data; name="submit"',
231 '',
232 'Create',
233 '--X--']))
253 body = ntob('\r\n'.join([
254 '--X',
255 'Content-Disposition: form-data; name="text"',
256 '',
257 '\xe2\x80\x9c',
258 '--X',
259 'Content-Disposition: form-data; name="submit"',
260 '',
261 'Create',
262 '--X--'
263 ]))
234264 self.getPage('/reqparams', method='POST',
235 headers=[("Content-Type", "multipart/form-data;boundary=X"),
236 ("Content-Length", str(len(body))),
237 ],
238 body=body),
239 self.assertBody(ntob("text: \xe2\x80\x9c, submit: Create"))
240
265 headers=[(
266 "Content-Type", "multipart/form-data;boundary=X"),
267 ("Content-Length", str(len(body))),
268 ],
269 body=body),
270 self.assertBody(ntob("submit: Create, text: \xe2\x80\x9c"))
271
241272 def test_multipart_decoding_no_successful_charset(self):
242273 # Test the decoding of a multipart entity when the charset (utf16) is
243274 # NOT explicitly given, and is NOT in the list of charsets to attempt.
244 body=ntob('\r\n'.join(['--X',
245 'Content-Disposition: form-data; name="text"',
246 '',
247 '\xff\xfea\x00b\x00\x1c c\x00',
248 '--X',
249 'Content-Disposition: form-data; name="submit"',
250 '',
251 '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
252 '--X--']))
275 body = ntob('\r\n'.join([
276 '--X',
277 'Content-Disposition: form-data; name="text"',
278 '',
279 '\xff\xfea\x00b\x00\x1c c\x00',
280 '--X',
281 'Content-Disposition: form-data; name="submit"',
282 '',
283 '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
284 '--X--'
285 ]))
253286 self.getPage('/reqparams', method='POST',
254 headers=[("Content-Type", "multipart/form-data;boundary=X"),
255 ("Content-Length", str(len(body))),
256 ],
287 headers=[(
288 "Content-Type", "multipart/form-data;boundary=X"),
289 ("Content-Length", str(len(body))),
290 ],
257291 body=body),
258292 self.assertStatus(400)
259 self.assertErrorPage(400,
293 self.assertErrorPage(
294 400,
260295 "The request entity could not be decoded. The following charsets "
261296 "were attempted: ['us-ascii', 'utf-8']")
262
297
263298 def test_nontext(self):
264299 self.getPage('/nontext')
265300 self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
266301 self.assertBody('\x00\x01\x02\x03')
267
302
268303 def testEncoding(self):
269304 # Default encoding should be utf-8
270305 self.getPage('/mao_zedong')
271306 self.assertBody(sing8)
272
307
273308 # Ask for utf-16.
274309 self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
275310 self.assertHeader('Content-Type', 'text/html;charset=utf-16')
276311 self.assertBody(sing16)
277
312
278313 # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
279314 # should be produced.
280315 self.getPage('/mao_zedong', [('Accept-Charset',
281316 'iso-8859-1;q=1, utf-16;q=0.5')])
282317 self.assertBody(sing16)
283
318
284319 # The "*" value should default to our default_encoding, utf-8
285320 self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
286321 self.assertBody(sing8)
287
322
288323 # Only allow iso-8859-1, which should fail and raise 406.
289324 self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
290325 self.assertStatus("406 Not Acceptable")
291326 self.assertInBody("Your client sent this Accept-Charset header: "
292327 "iso-8859-1, *;q=0. We tried these charsets: "
293328 "iso-8859-1.")
294
329
295330 # Ask for x-mac-ce, which should be unknown. See ticket #569.
296331 self.getPage('/mao_zedong', [('Accept-Charset',
297332 'us-ascii, ISO-8859-1, x-mac-ce')])
299334 self.assertInBody("Your client sent this Accept-Charset header: "
300335 "us-ascii, ISO-8859-1, x-mac-ce. We tried these "
301336 "charsets: ISO-8859-1, us-ascii, x-mac-ce.")
302
337
303338 # Test the 'encoding' arg to encode.
304339 self.getPage('/utf8')
305340 self.assertBody(sing8)
306341 self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
307342 self.assertStatus("406 Not Acceptable")
308
343
309344 def testGzip(self):
310345 zbuf = BytesIO()
311346 zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
312347 zfile.write(ntob("Hello, world"))
313348 zfile.close()
314
349
315350 self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")])
316351 self.assertInBody(zbuf.getvalue()[:3])
317352 self.assertHeader("Vary", "Accept-Encoding")
318353 self.assertHeader("Content-Encoding", "gzip")
319
354
320355 # Test when gzip is denied.
321356 self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")])
322357 self.assertHeader("Vary", "Accept-Encoding")
323358 self.assertNoHeader("Content-Encoding")
324359 self.assertBody("Hello, world")
325
360
326361 self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip;q=0")])
327362 self.assertHeader("Vary", "Accept-Encoding")
328363 self.assertNoHeader("Content-Encoding")
329364 self.assertBody("Hello, world")
330
365
331366 self.getPage('/gzip/', headers=[("Accept-Encoding", "*;q=0")])
332367 self.assertStatus(406)
333368 self.assertNoHeader("Content-Encoding")
334369 self.assertErrorPage(406, "identity, gzip")
335
370
336371 # Test for ticket #147
337372 self.getPage('/gzip/noshow', headers=[("Accept-Encoding", "gzip")])
338373 self.assertNoHeader('Content-Encoding')
339374 self.assertStatus(500)
340375 self.assertErrorPage(500, pattern="IndexError\n")
341
376
342377 # In this case, there's nothing we can do to deliver a
343378 # readable page, since 1) the gzip header is already set,
344379 # and 2) we may have already written some of the body.
345380 # The fix is to never stream yields when using gzip.
346381 if (cherrypy.server.protocol_version == "HTTP/1.0" or
347 getattr(cherrypy.server, "using_apache", False)):
382 getattr(cherrypy.server, "using_apache", False)):
348383 self.getPage('/gzip/noshow_stream',
349384 headers=[("Accept-Encoding", "gzip")])
350385 self.assertHeader('Content-Encoding', 'gzip')
359394 def test_UnicodeHeaders(self):
360395 self.getPage('/cookies_and_headers')
361396 self.assertBody('Any content')
362
66
77 def setup_server():
88 class Root:
9
910 def resource(self):
1011 return "Oh wah ta goo Siam."
1112 resource.exposed = True
12
13
1314 def fail(self, code):
1415 code = int(code)
1516 if 300 <= code <= 399:
1718 else:
1819 raise cherrypy.HTTPError(code)
1920 fail.exposed = True
20
21
2122 def unicoded(self):
2223 return ntou('I am a \u1ee4nicode string.', 'escape')
2324 unicoded.exposed = True
2425 # In Python 3, tools.encode is on by default
2526 unicoded._cp_config = {'tools.encode.on': True}
26
27
2728 conf = {'/': {'tools.etags.on': True,
2829 'tools.etags.autotags': True,
2930 }}
3031 cherrypy.tree.mount(Root(), config=conf)
3132 setup_server = staticmethod(setup_server)
32
33
3334 def test_etags(self):
3435 self.getPage("/resource")
3536 self.assertStatus('200 OK')
3637 self.assertHeader('Content-Type', 'text/html;charset=utf-8')
3738 self.assertBody('Oh wah ta goo Siam.')
3839 etag = self.assertHeader('ETag')
39
40
4041 # Test If-Match (both valid and invalid)
4142 self.getPage("/resource", headers=[('If-Match', etag)])
4243 self.assertStatus("200 OK")
4647 self.assertStatus("200 OK")
4748 self.getPage("/resource", headers=[('If-Match', "a bogus tag")])
4849 self.assertStatus("412 Precondition Failed")
49
50
5051 # Test If-None-Match (both valid and invalid)
5152 self.getPage("/resource", headers=[('If-None-Match', etag)])
5253 self.assertStatus(304)
53 self.getPage("/resource", method='POST', headers=[('If-None-Match', etag)])
54 self.getPage("/resource", method='POST',
55 headers=[('If-None-Match', etag)])
5456 self.assertStatus("412 Precondition Failed")
5557 self.getPage("/resource", headers=[('If-None-Match', "*")])
5658 self.assertStatus(304)
5759 self.getPage("/resource", headers=[('If-None-Match', "a bogus tag")])
5860 self.assertStatus("200 OK")
59
61
6062 def test_errors(self):
6163 self.getPage("/resource")
6264 self.assertStatus(200)
6365 etag = self.assertHeader('ETag')
64
66
6567 # Test raising errors in page handler
6668 self.getPage("/fail/412", headers=[('If-Match', etag)])
6769 self.assertStatus(412)
7173 self.assertStatus(412)
7274 self.getPage("/fail/304", headers=[('If-None-Match', "*")])
7375 self.assertStatus(304)
74
76
7577 def test_unicode_body(self):
7678 self.getPage("/unicoded")
7779 self.assertStatus(200)
7981 self.getPage("/unicoded", headers=[('If-Match', etag1)])
8082 self.assertStatus(200)
8183 self.assertHeader('ETag', etag1)
82
1010
1111 def encode_multipart_formdata(files):
1212 """Return (content_type, body) ready for httplib.HTTP instance.
13
13
1414 files: a sequence of (name, filename, value) tuples for multipart uploads.
1515 """
1616 BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
3030 return content_type, body
3131
3232
33
34
3533 from cherrypy.test import helper
3634
35
3736 class HTTPTests(helper.CPWebCase):
37
38 def make_connection(self):
39 if self.scheme == "https":
40 return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
41 else:
42 return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
3843
3944 def setup_server():
4045 class Root:
46
4147 def index(self, *args, **kwargs):
4248 return "Hello world!"
4349 index.exposed = True
44
50
4551 def no_body(self, *args, **kwargs):
4652 return "Hello world!"
4753 no_body.exposed = True
4854 no_body._cp_config = {'request.process_request_body': False}
49
55
5056 def post_multipart(self, file):
51 """Return a summary ("a * 65536\nb * 65536") of the uploaded file."""
57 """Return a summary ("a * 65536\nb * 65536") of the uploaded
58 file.
59 """
5260 contents = file.file.read()
5361 summary = []
5462 curchar = None
5866 count += 1
5967 else:
6068 if count:
61 if py3k: curchar = chr(curchar)
69 if py3k:
70 curchar = chr(curchar)
6271 summary.append("%s * %d" % (curchar, count))
6372 count = 1
6473 curchar = c
6574 if count:
66 if py3k: curchar = chr(curchar)
75 if py3k:
76 curchar = chr(curchar)
6777 summary.append("%s * %d" % (curchar, count))
6878 return ", ".join(summary)
6979 post_multipart.exposed = True
70
80
81 @cherrypy.expose
82 def post_filename(self, myfile):
83 '''Return the name of the file which was uploaded.'''
84 return myfile.filename
85
7186 cherrypy.tree.mount(Root())
7287 cherrypy.config.update({'server.max_request_body_size': 30000000})
7388 setup_server = staticmethod(setup_server)
74
89
7590 def test_no_content_length(self):
7691 # "The presence of a message-body in a request is signaled by the
7792 # inclusion of a Content-Length or Transfer-Encoding header field in
7893 # the request's message-headers."
79 #
94 #
8095 # Send a message with neither header and no body. Even though
8196 # the request is of method POST, this should be OK because we set
8297 # request.process_request_body to False for our handler.
83 if self.scheme == "https":
84 c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
85 else:
86 c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
98 c = self.make_connection()
8799 c.request("POST", "/no_body")
88100 response = c.getresponse()
89101 self.body = response.fp.read()
90102 self.status = str(response.status)
91103 self.assertStatus(200)
92104 self.assertBody(ntob('Hello world!'))
93
105
94106 # Now send a message that has no Content-Length, but does send a body.
95107 # Verify that CP times out the socket and responds
96108 # with 411 Length Required.
103115 self.body = response.fp.read()
104116 self.status = str(response.status)
105117 self.assertStatus(411)
106
118
107119 def test_post_multipart(self):
108120 alphabet = "abcdefghijklmnopqrstuvwxyz"
109121 # generate file contents for a large post
110122 contents = "".join([c * 65536 for c in alphabet])
111
123
112124 # encode as multipart form data
113 files=[('file', 'file.txt', contents)]
125 files = [('file', 'file.txt', contents)]
114126 content_type, body = encode_multipart_formdata(files)
115127 body = body.encode('Latin-1')
116
128
117129 # post file
118 if self.scheme == 'https':
119 c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
120 else:
121 c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
130 c = self.make_connection()
122131 c.putrequest('POST', '/post_multipart')
123132 c.putheader('Content-Type', content_type)
124133 c.putheader('Content-Length', str(len(body)))
125134 c.endheaders()
126135 c.send(body)
127
136
128137 response = c.getresponse()
129138 self.body = response.fp.read()
130139 self.status = str(response.status)
131140 self.assertStatus(200)
132141 self.assertBody(", ".join(["%s * 65536" % c for c in alphabet]))
142
143 def test_post_filename_with_commas(self):
144 '''Testing that we can handle filenames with commas. This was
145 reported as a bug in:
146 https://bitbucket.org/cherrypy/cherrypy/issue/1146/'''
147 # We'll upload a bunch of files with differing names.
148 for fname in ['boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv']:
149 files = [('myfile', fname, 'yunyeenyunyue')]
150 content_type, body = encode_multipart_formdata(files)
151 body = body.encode('Latin-1')
152
153 # post file
154 c = self.make_connection()
155 c.putrequest('POST', '/post_filename')
156 c.putheader('Content-Type', content_type)
157 c.putheader('Content-Length', str(len(body)))
158 c.endheaders()
159 c.send(body)
160
161 response = c.getresponse()
162 self.body = response.fp.read()
163 self.status = str(response.status)
164 self.assertStatus(200)
165 self.assertBody(fname)
133166
134167 def test_malformed_request_line(self):
135168 if getattr(cherrypy.server, "using_apache", False):
136169 return self.skip("skipped due to known Apache differences...")
137
170
138171 # Test missing version in Request-Line
139 if self.scheme == 'https':
140 c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
141 else:
142 c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
172 c = self.make_connection()
143173 c._output(ntob('GET /'))
144174 c._send_output()
145175 if hasattr(c, 'strict'):
153183 self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line"))
154184 c.close()
155185
186 def test_request_line_split_issue_1220(self):
187 Request_URI = "/index?intervenant-entreprise-evenement_classaction=evenement-mailremerciements&_path=intervenant-entreprise-evenement&intervenant-entreprise-evenement_action-id=19404&intervenant-entreprise-evenement_id=19404&intervenant-entreprise_id=28092"
188 self.assertEqual(len("GET %s HTTP/1.1\r\n" % Request_URI), 256)
189 self.getPage(Request_URI)
190 self.assertBody("Hello world!")
191
156192 def test_malformed_header(self):
157 if self.scheme == 'https':
158 c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
159 else:
160 c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
193 c = self.make_connection()
161194 c.putrequest('GET', '/')
162195 c.putheader('Content-Type', 'text/plain')
163 # See http://www.cherrypy.org/ticket/941
196 # See https://bitbucket.org/cherrypy/cherrypy/issue/941
164197 c._output(ntob('Re, 1.2.3.4#015#012'))
165198 c.endheaders()
166
199
167200 response = c.getresponse()
168201 self.status = str(response.status)
169202 self.assertStatus(400)
170203 self.body = response.fp.read(20)
171204 self.assertBody("Illegal header line.")
172
205
173206 def test_http_over_https(self):
174207 if self.scheme != 'https':
175208 return self.skip("skipped (not running HTTPS)... ")
176
209
177210 # Try connecting without SSL.
178211 conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
179212 conn.putrequest("GET", "/", skip_host=True)
201234 try:
202235 response.begin()
203236 self.assertEqual(response.status, 400)
204 self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line"))
237 self.assertEqual(response.fp.read(22),
238 ntob("Malformed Request-Line"))
205239 c.close()
206240 except socket.error:
207241 e = sys.exc_info()[1]
208242 # "Connection reset by peer" is also acceptable.
209243 if e.errno != errno.ECONNRESET:
210244 raise
211
33
44 from cherrypy.test import helper
55
6
67 class HTTPAuthTest(helper.CPWebCase):
78
89 def setup_server():
910 class Root:
11
1012 def index(self):
1113 return "This is public."
1214 index.exposed = True
1315
1416 class DigestProtected:
17
1518 def index(self):
16 return "Hello %s, you've been authorized." % cherrypy.request.login
19 return "Hello %s, you've been authorized." % (
20 cherrypy.request.login)
1721 index.exposed = True
1822
1923 class BasicProtected:
24
2025 def index(self):
21 return "Hello %s, you've been authorized." % cherrypy.request.login
26 return "Hello %s, you've been authorized." % (
27 cherrypy.request.login)
2228 index.exposed = True
2329
2430 class BasicProtected2:
31
2532 def index(self):
26 return "Hello %s, you've been authorized." % cherrypy.request.login
33 return "Hello %s, you've been authorized." % (
34 cherrypy.request.login)
2735 index.exposed = True
2836
2937 def fetch_users():
3139
3240 def sha_password_encrypter(password):
3341 return sha(ntob(password)).hexdigest()
34
42
3543 def fetch_password(username):
3644 return sha(ntob('test')).hexdigest()
3745
38 conf = {'/digest': {'tools.digest_auth.on': True,
39 'tools.digest_auth.realm': 'localhost',
40 'tools.digest_auth.users': fetch_users},
41 '/basic': {'tools.basic_auth.on': True,
42 'tools.basic_auth.realm': 'localhost',
43 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}},
44 '/basic2': {'tools.basic_auth.on': True,
45 'tools.basic_auth.realm': 'localhost',
46 'tools.basic_auth.users': fetch_password,
47 'tools.basic_auth.encrypt': sha_password_encrypter}}
48
46 conf = {
47 '/digest': {
48 'tools.digest_auth.on': True,
49 'tools.digest_auth.realm': 'localhost',
50 'tools.digest_auth.users': fetch_users
51 },
52 '/basic': {
53 'tools.basic_auth.on': True,
54 'tools.basic_auth.realm': 'localhost',
55 'tools.basic_auth.users': {
56 'test': md5(ntob('test')).hexdigest()
57 }
58 },
59 '/basic2': {
60 'tools.basic_auth.on': True,
61 'tools.basic_auth.realm': 'localhost',
62 'tools.basic_auth.users': fetch_password,
63 'tools.basic_auth.encrypt': sha_password_encrypter
64 }
65 }
66
4967 root = Root()
5068 root.digest = DigestProtected()
5169 root.basic = BasicProtected()
5270 root.basic2 = BasicProtected2()
5371 cherrypy.tree.mount(root, config=conf)
5472 setup_server = staticmethod(setup_server)
55
5673
5774 def testPublic(self):
5875 self.getPage("/")
6784
6885 self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')])
6986 self.assertStatus(401)
70
87
7188 self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
7289 self.assertStatus('200 OK')
7390 self.assertBody("Hello test, you've been authorized.")
7996
8097 self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')])
8198 self.assertStatus(401)
82
99
83100 self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
84101 self.assertStatus('200 OK')
85102 self.assertBody("Hello test, you've been authorized.")
87104 def testDigest(self):
88105 self.getPage("/digest/")
89106 self.assertStatus(401)
90
107
91108 value = None
92109 for k, v in self.headers:
93110 if k.lower() == "www-authenticate":
96113 break
97114
98115 if value is None:
99 self._handlewebError("Digest authentification scheme was not found")
116 self._handlewebError(
117 "Digest authentification scheme was not found")
100118
101119 value = value[7:]
102120 items = value.split(', ')
104122 for item in items:
105123 key, value = item.split('=')
106124 tokens[key.lower()] = value
107
125
108126 missing_msg = "%s is missing"
109127 bad_value_msg = "'%s' was expecting '%s' but found '%s'"
110128 nonce = None
111129 if 'realm' not in tokens:
112130 self._handlewebError(missing_msg % 'realm')
113131 elif tokens['realm'] != '"localhost"':
114 self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
132 self._handlewebError(bad_value_msg %
133 ('realm', '"localhost"', tokens['realm']))
115134 if 'nonce' not in tokens:
116135 self._handlewebError(missing_msg % 'nonce')
117136 else:
119138 if 'algorithm' not in tokens:
120139 self._handlewebError(missing_msg % 'algorithm')
121140 elif tokens['algorithm'] != '"MD5"':
122 self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
141 self._handlewebError(bad_value_msg %
142 ('algorithm', '"MD5"', tokens['algorithm']))
123143 if 'qop' not in tokens:
124144 self._handlewebError(missing_msg % 'qop')
125145 elif tokens['qop'] != '"auth"':
126 self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
146 self._handlewebError(bad_value_msg %
147 ('qop', '"auth"', tokens['qop']))
127148
128149 # Test a wrong 'realm' value
129 base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
150 base_auth = (
151 'Digest '
152 'username="test", '
153 'realm="wrong realm", '
154 'nonce="%s", '
155 'uri="/digest/", '
156 'algorithm=MD5, '
157 'response="%s", '
158 'qop=auth, '
159 'nc=%s, '
160 'cnonce="1522e61005789929"'
161 )
130162
131163 auth = base_auth % (nonce, '', '00000001')
132164 params = httpauth.parseAuthorization(auth)
133165 response = httpauth._computeDigestResponse(params, 'test')
134
166
135167 auth = base_auth % (nonce, response, '00000001')
136168 self.getPage('/digest/', [('Authorization', auth)])
137169 self.assertStatus(401)
138170
139171 # Test that must pass
140 base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
172 base_auth = (
173 'Digest '
174 'username="test", '
175 'realm="localhost", '
176 'nonce="%s", '
177 'uri="/digest/", '
178 'algorithm=MD5, '
179 'response="%s", '
180 'qop=auth, '
181 'nc=%s, '
182 'cnonce="1522e61005789929"'
183 )
141184
142185 auth = base_auth % (nonce, '', '00000001')
143186 params = httpauth.parseAuthorization(auth)
144187 response = httpauth._computeDigestResponse(params, 'test')
145
188
146189 auth = base_auth % (nonce, response, '00000001')
147190 self.getPage('/digest/', [('Authorization', auth)])
148191 self.assertStatus('200 OK')
149192 self.assertBody("Hello test, you've been authorized.")
150
44
55
66 class UtilityTests(unittest.TestCase):
7
7
88 def test_urljoin(self):
99 # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO
1010 self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/")
22
33 from cherrypy._cpcompat import json
44
5
56 class JsonTest(helper.CPWebCase):
7
68 def setup_server():
79 class Root(object):
10
811 def plain(self):
912 return 'hello'
1013 plain.exposed = True
3235 json_post.exposed = True
3336 json_post._cp_config = {'tools.json_in.on': True}
3437
38 def json_cached(self):
39 return 'hello there'
40 json_cached.exposed = True
41 json_cached._cp_config = {
42 'tools.json_out.on': True,
43 'tools.caching.on': True,
44 }
45
3546 root = Root()
3647 cherrypy.tree.mount(root)
3748 setup_server = staticmethod(setup_server)
38
49
3950 def test_json_output(self):
4051 if json is None:
4152 self.skip("json not found ")
4253 return
43
54
4455 self.getPage("/plain")
4556 self.assertBody("hello")
4657
5768 if json is None:
5869 self.skip("json not found ")
5970 return
60
71
6172 body = '[13, "c"]'
6273 headers = [('Content-Type', 'application/json'),
6374 ('Content-Length', str(len(body)))]
6475 self.getPage("/json_post", method="POST", headers=headers, body=body)
6576 self.assertBody('ok')
66
77
6778 body = '[13, "c"]'
6879 headers = [('Content-Type', 'text/plain'),
6980 ('Content-Length', str(len(body)))]
7081 self.getPage("/json_post", method="POST", headers=headers, body=body)
7182 self.assertStatus(415, 'Expected an application/json content type')
72
83
7384 body = '[13, -]'
7485 headers = [('Content-Type', 'application/json'),
7586 ('Content-Length', str(len(body)))]
7687 self.getPage("/json_post", method="POST", headers=headers, body=body)
7788 self.assertStatus(400, 'Invalid JSON document')
7889
90 def test_cached(self):
91 if json is None:
92 self.skip("json not found ")
93 return
94
95 self.getPage("/json_cached")
96 self.assertStatus(200, '"hello"')
97
98 self.getPage("/json_cached") # 2'nd time to hit cache
99 self.assertStatus(200, '"hello"')
1515
1616 def setup_server():
1717 class Root:
18
18
1919 def index(self):
2020 return "hello"
2121 index.exposed = True
22
22
2323 def uni_code(self):
2424 cherrypy.request.login = tartaros
2525 cherrypy.request.remote.name = erebos
2626 uni_code.exposed = True
27
27
2828 def slashes(self):
2929 cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
3030 slashes.exposed = True
31
31
3232 def whitespace(self):
3333 # User-Agent = "User-Agent" ":" 1*( product | comment )
3434 # comment = "(" *( ctext | quoted-pair | comment ) ")"
3737 # LWS = [CRLF] 1*( SP | HT )
3838 cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
3939 whitespace.exposed = True
40
40
4141 def as_string(self):
4242 return "content"
4343 as_string.exposed = True
44
44
4545 def as_yield(self):
4646 yield "content"
4747 as_yield.exposed = True
48
48
4949 def error(self):
5050 raise ValueError()
5151 error.exposed = True
5252 error._cp_config = {'tools.log_tracebacks.on': True}
53
53
5454 root = Root()
55
5655
5756 cherrypy.config.update({'log.error_file': error_log,
5857 'log.access_file': access_log,
6059 cherrypy.tree.mount(root)
6160
6261
62 from cherrypy.test import helper, logtest
6363
64 from cherrypy.test import helper, logtest
6564
6665 class AccessLogTests(helper.CPWebCase, logtest.LogCase):
6766 setup_server = staticmethod(setup_server)
68
67
6968 logfile = access_log
70
69
7170 def testNormalReturn(self):
7271 self.markLog()
7372 self.getPage("/as_string",
7574 ('User-Agent', 'Mozilla/5.0')])
7675 self.assertBody('content')
7776 self.assertStatus(200)
78
77
7978 intro = '%s - - [' % self.interface()
80
79
8180 self.assertLog(-1, intro)
82
81
8382 if [k for k, v in self.headers if k.lower() == 'content-length']:
8483 self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
8584 '"http://www.cherrypy.org/" "Mozilla/5.0"'
8887 self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
8988 '"http://www.cherrypy.org/" "Mozilla/5.0"'
9089 % self.prefix())
91
90
9291 def testNormalYield(self):
9392 self.markLog()
9493 self.getPage("/as_yield")
9594 self.assertBody('content')
9695 self.assertStatus(200)
97
96
9897 intro = '%s - - [' % self.interface()
99
98
10099 self.assertLog(-1, intro)
101100 if [k for k, v in self.headers if k.lower() == 'content-length']:
102101 self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
104103 else:
105104 self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
106105 % self.prefix())
107
106
108107 def testEscapedOutput(self):
109108 # Test unicode in access log pieces.
110109 self.markLog()
118117 # Test the erebos value. Included inline for your enlightenment.
119118 # Note the 'r' prefix--those backslashes are literals.
120119 self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
121
120
122121 # Test backslashes in output.
123122 self.markLog()
124123 self.getPage("/slashes")
127126 self.assertLog(-1, ntob('"GET /slashed\\path HTTP/1.1"'))
128127 else:
129128 self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
130
129
131130 # Test whitespace in output.
132131 self.markLog()
133132 self.getPage("/whitespace")
138137
139138 class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
140139 setup_server = staticmethod(setup_server)
141
140
142141 logfile = error_log
143
142
144143 def testTracebacks(self):
145144 # Test that tracebacks get written to the error log.
146145 self.markLog()
153152 self.assertLog(-3, 'raise ValueError()')
154153 finally:
155154 ignore.pop()
156
22 import cherrypy
33 from cherrypy._cpcompat import ntob, ntou, sorted
44
5
56 def setup_server():
6
7
78 class Root:
8
9
910 def multipart(self, parts):
1011 return repr(parts)
1112 multipart.exposed = True
12
13
1314 def multipart_form_data(self, **kwargs):
1415 return repr(list(sorted(kwargs.items())))
1516 multipart_form_data.exposed = True
16
17
1718 def flashupload(self, Filedata, Upload, Filename):
1819 return ("Upload: %s, Filename: %s, Filedata: %r" %
1920 (Upload, Filename, Filedata.file.read()))
2021 flashupload.exposed = True
21
22
2223 cherrypy.config.update({'server.max_request_body_size': 0})
2324 cherrypy.tree.mount(Root())
2425
2728
2829 from cherrypy.test import helper
2930
31
3032 class MultipartTest(helper.CPWebCase):
3133 setup_server = staticmethod(setup_server)
32
34
3335 def test_multipart(self):
3436 text_part = ntou("This is the text version")
35 html_part = ntou("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
37 html_part = ntou(
38 """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
3639 <html>
3740 <head>
3841 <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
5760 headers = [
5861 ('Content-Type', 'multipart/mixed; boundary=123456789'),
5962 ('Content-Length', str(len(body))),
60 ]
63 ]
6164 self.getPage('/multipart', headers, "POST", body)
6265 self.assertBody(repr([text_part, html_part]))
63
66
6467 def test_multipart_form_data(self):
65 body='\r\n'.join(['--X',
66 'Content-Disposition: form-data; name="foo"',
67 '',
68 'bar',
69 '--X',
70 # Test a param with more than one value.
71 # See http://www.cherrypy.org/ticket/1028
72 'Content-Disposition: form-data; name="baz"',
73 '',
74 '111',
75 '--X',
76 'Content-Disposition: form-data; name="baz"',
77 '',
78 '333',
79 '--X--'])
68 body = '\r\n'.join([
69 '--X',
70 'Content-Disposition: form-data; name="foo"',
71 '',
72 'bar',
73 '--X',
74 # Test a param with more than one value.
75 # See
76 # https://bitbucket.org/cherrypy/cherrypy/issue/1028
77 'Content-Disposition: form-data; name="baz"',
78 '',
79 '111',
80 '--X',
81 'Content-Disposition: form-data; name="baz"',
82 '',
83 '333',
84 '--X--'
85 ])
8086 self.getPage('/multipart_form_data', method='POST',
81 headers=[("Content-Type", "multipart/form-data;boundary=X"),
82 ("Content-Length", str(len(body))),
83 ],
87 headers=[(
88 "Content-Type", "multipart/form-data;boundary=X"),
89 ("Content-Length", str(len(body))),
90 ],
8491 body=body),
85 self.assertBody(repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
92 self.assertBody(
93 repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
8694
8795
8896 class SafeMultipartHandlingTest(helper.CPWebCase):
92100 headers = [
93101 ('Accept', 'text/*'),
94102 ('Content-Type', 'multipart/form-data; '
95 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
103 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
96104 ('User-Agent', 'Shockwave Flash'),
97105 ('Host', 'www.example.com:54583'),
98106 ('Content-Length', '499'),
99107 ('Connection', 'Keep-Alive'),
100108 ('Cache-Control', 'no-cache'),
101 ]
109 ]
102110 filedata = ntob('<?xml version="1.0" encoding="UTF-8"?>\r\n'
103111 '<projectDescription>\r\n'
104112 '</projectDescription>\r\n')
112120 'name="Filedata"; filename=".project"\r\n'
113121 'Content-Type: application/octet-stream\r\n'
114122 '\r\n')
115 + filedata +
123 + filedata +
116124 ntob('\r\n'
117 '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
118 'Content-Disposition: form-data; name="Upload"\r\n'
119 '\r\n'
120 'Submit Query\r\n'
121 # Flash apps omit the trailing \r\n on the last line:
122 '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
123 ))
125 '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
126 'Content-Disposition: form-data; name="Upload"\r\n'
127 '\r\n'
128 'Submit Query\r\n'
129 # Flash apps omit the trailing \r\n on the last line:
130 '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
131 ))
124132 self.getPage('/flashupload', headers, "POST", body)
125133 self.assertBody("Upload: Submit Query, Filename: .project, "
126134 "Filedata: %r" % filedata)
127
77
88 def setup_server():
99 class Root:
10
1011 def index(self):
1112 yield "Hello, world"
1213 index.exposed = True
1314 h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')]
1415 tools.response_headers(headers=h)(index)
15
16
1617 def other(self):
1718 return "salut"
1819 other.exposed = True
2122 'tools.response_headers.headers': [("Content-Language", "fr"),
2223 ('Content-Type', 'text/plain')],
2324 'tools.log_hooks.on': True,
24 }
25
26
25 }
26
2727 class Accept:
2828 _cp_config = {'tools.accept.on': True}
29
29
3030 def index(self):
3131 return '<a href="feed">Atom feed</a>'
3232 index.exposed = True
33
33
3434 # In Python 2.4+, we could use a decorator instead:
3535 # @tools.accept('application/atom+xml')
3636 def feed(self):
4040 </feed>"""
4141 feed.exposed = True
4242 feed._cp_config = {'tools.accept.media': 'application/atom+xml'}
43
43
4444 def select(self):
4545 # We could also write this: mtype = cherrypy.lib.accept.accept(...)
4646 mtype = tools.accept.callable(['text/html', 'text/plain'])
4949 else:
5050 return "PAGE TITLE"
5151 select.exposed = True
52
52
5353 class Referer:
54
5455 def accept(self):
5556 return "Accepted!"
5657 accept.exposed = True
5758 reject = accept
58
59
5960 class AutoVary:
61
6062 def index(self):
6163 # Read a header directly with 'get'
6264 ae = cherrypy.request.headers.get('Accept-Encoding')
7577 mtype = tools.accept.callable(['text/html', 'text/plain'])
7678 return "Hello, world!"
7779 index.exposed = True
78
80
7981 conf = {'/referer': {'tools.referer.on': True,
8082 'tools.referer.pattern': r'http://[^/]*example\.com',
8183 },
8486 },
8587 '/autovary': {'tools.autovary.on': True},
8688 }
87
89
8890 root = Root()
8991 root.referer = Referer()
9092 root.accept = Accept()
9597
9698 from cherrypy.test import helper
9799
100
98101 class ResponseHeadersTest(helper.CPWebCase):
99102 setup_server = staticmethod(setup_server)
100103
111114
112115 class RefererTest(helper.CPWebCase):
113116 setup_server = staticmethod(setup_server)
114
117
115118 def testReferer(self):
116119 self.getPage('/referer/accept')
117120 self.assertErrorPage(403, 'Forbidden Referer header.')
118
121
119122 self.getPage('/referer/accept',
120123 headers=[('Referer', 'http://www.example.com/')])
121124 self.assertStatus(200)
122125 self.assertBody('Accepted!')
123
126
124127 # Reject
125128 self.getPage('/referer/reject')
126129 self.assertStatus(200)
127130 self.assertBody('Accepted!')
128
131
129132 self.getPage('/referer/reject',
130133 headers=[('Referer', 'http://www.example.com/')])
131134 self.assertErrorPage(403, 'Forbidden Referer header.')
133136
134137 class AcceptTest(helper.CPWebCase):
135138 setup_server = staticmethod(setup_server)
136
139
137140 def test_Accept_Tool(self):
138141 # Test with no header provided
139142 self.getPage('/accept/feed')
140143 self.assertStatus(200)
141144 self.assertInBody('<title>Unknown Blog</title>')
142
145
143146 # Specify exact media type
144 self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')])
145 self.assertStatus(200)
146 self.assertInBody('<title>Unknown Blog</title>')
147
147 self.getPage('/accept/feed',
148 headers=[('Accept', 'application/atom+xml')])
149 self.assertStatus(200)
150 self.assertInBody('<title>Unknown Blog</title>')
151
148152 # Specify matching media range
149153 self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
150154 self.assertStatus(200)
151155 self.assertInBody('<title>Unknown Blog</title>')
152
156
153157 # Specify all media ranges
154158 self.getPage('/accept/feed', headers=[('Accept', '*/*')])
155159 self.assertStatus(200)
156160 self.assertInBody('<title>Unknown Blog</title>')
157
161
158162 # Specify unacceptable media types
159163 self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
160164 self.assertErrorPage(406,
161165 "Your client sent this Accept header: text/html. "
162166 "But this resource only emits these media types: "
163167 "application/atom+xml.")
164
168
165169 # Test resource where tool is 'on' but media is None (not set).
166170 self.getPage('/accept/')
167171 self.assertStatus(200)
168172 self.assertBody('<a href="feed">Atom feed</a>')
169
173
170174 def test_accept_selection(self):
171175 # Try both our expected media types
172176 self.getPage('/accept/select', [('Accept', 'text/html')])
175179 self.getPage('/accept/select', [('Accept', 'text/plain')])
176180 self.assertStatus(200)
177181 self.assertBody('PAGE TITLE')
178 self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')])
182 self.getPage('/accept/select',
183 [('Accept', 'text/plain, text/*;q=0.5')])
179184 self.assertStatus(200)
180185 self.assertBody('PAGE TITLE')
181
186
182187 # text/* and */* should prefer text/html since it comes first
183188 # in our 'media' argument to tools.accept
184189 self.getPage('/accept/select', [('Accept', 'text/*')])
187192 self.getPage('/accept/select', [('Accept', '*/*')])
188193 self.assertStatus(200)
189194 self.assertBody('<h2>Page Title</h2>')
190
195
191196 # Try unacceptable media types
192197 self.getPage('/accept/select', [('Accept', 'application/xml')])
193 self.assertErrorPage(406,
194 "Your client sent this Accept header: application/xml. "
195 "But this resource only emits these media types: "
196 "text/html, text/plain.")
198 self.assertErrorPage(
199 406,
200 "Your client sent this Accept header: application/xml. "
201 "But this resource only emits these media types: "
202 "text/html, text/plain.")
197203
198204
199205 class AutoVaryTest(helper.CPWebCase):
202208 def testAutoVary(self):
203209 self.getPage('/autovary/')
204210 self.assertHeader(
205 "Vary", 'Accept, Accept-Charset, Accept-Encoding, Host, If-Modified-Since, Range')
206
211 "Vary",
212 'Accept, Accept-Charset, Accept-Encoding, '
213 'Host, If-Modified-Since, Range'
214 )
99
1010 def setup_server():
1111 class Root:
12
1213 def index(self, name="world"):
1314 return name
1415 index.exposed = True
15
16
1617 def foobar(self):
1718 return "bar"
1819 foobar.exposed = True
19
20
2021 def default(self, *params, **kwargs):
2122 return "default:" + repr(params)
2223 default.exposed = True
23
24
2425 def other(self):
2526 return "other"
2627 other.exposed = True
27
28
2829 def extra(self, *p):
2930 return repr(p)
3031 extra.exposed = True
31
32
3233 def redirect(self):
3334 raise cherrypy.HTTPRedirect('dir1/', 302)
3435 redirect.exposed = True
35
36
3637 def notExposed(self):
3738 return "not exposed"
38
39
3940 def confvalue(self):
4041 return cherrypy.request.config.get("user")
4142 confvalue.exposed = True
42
43
4344 def redirect_via_url(self, path):
4445 raise cherrypy.HTTPRedirect(cherrypy.url(path))
4546 redirect_via_url.exposed = True
46
47
4748 def translate_html(self):
4849 return "OK"
4950 translate_html.exposed = True
50
51
5152 def mapped_func(self, ID=None):
5253 return "ID is %s" % ID
5354 mapped_func.exposed = True
5455 setattr(Root, "Von B\xfclow", mapped_func)
55
56
56
5757 class Exposing:
58
5859 def base(self):
5960 return "expose works!"
6061 cherrypy.expose(base)
6162 cherrypy.expose(base, "1")
6263 cherrypy.expose(base, "2")
63
64
6465 class ExposingNewStyle(object):
66
6567 def base(self):
6668 return "expose works!"
6769 cherrypy.expose(base)
6870 cherrypy.expose(base, "1")
6971 cherrypy.expose(base, "2")
70
71
72
7273 class Dir1:
74
7375 def index(self):
7476 return "index for dir1"
7577 index.exposed = True
76
78
7779 def myMethod(self):
78 return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info)
80 return "myMethod from dir1, path_info is:" + repr(
81 cherrypy.request.path_info)
7982 myMethod.exposed = True
8083 myMethod._cp_config = {'tools.trailing_slash.extra': True}
81
84
8285 def default(self, *params):
8386 return "default for dir1, param is:" + repr(params)
8487 default.exposed = True
8588
86
8789 class Dir2:
90
8891 def index(self):
8992 return "index for dir2, path is:" + cherrypy.request.path_info
9093 index.exposed = True
91
94
9295 def script_name(self):
9396 return cherrypy.tree.script_name()
9497 script_name.exposed = True
95
98
9699 def cherrypy_url(self):
97100 return cherrypy.url("/extra")
98101 cherrypy_url.exposed = True
99
102
100103 def posparam(self, *vpath):
101104 return "/".join(vpath)
102105 posparam.exposed = True
103
104
106
105107 class Dir3:
108
106109 def default(self):
107110 return "default for dir3, not exposed"
108
111
109112 class Dir4:
113
110114 def index(self):
111115 return "index for dir4, not exposed"
112
116
113117 class DefNoIndex:
118
114119 def default(self, *args):
115120 raise cherrypy.HTTPRedirect("contact")
116121 default.exposed = True
117
122
118123 # MethodDispatcher code
119124 class ByMethod:
120125 exposed = True
121
126
122127 def __init__(self, *things):
123128 self.things = list(things)
124
129
125130 def GET(self):
126131 return repr(self.things)
127
132
128133 def POST(self, thing):
129134 self.things.append(thing)
130
135
131136 class Collection:
132137 default = ByMethod('a', 'bit')
133
138
134139 Root.exposing = Exposing()
135140 Root.exposingnew = ExposingNewStyle()
136141 Root.dir1 = Dir1()
140145 Root.defnoindex = DefNoIndex()
141146 Root.bymethod = ByMethod('another')
142147 Root.collection = Collection()
143
148
144149 d = cherrypy.dispatch.MethodDispatcher()
145150 for url in script_names:
146151 conf = {'/': {'user': (url or "/").split("/")[-2]},
148153 '/collection': {'request.dispatch': d},
149154 }
150155 cherrypy.tree.mount(Root(), url, conf)
151
152
156
153157 class Isolated:
158
154159 def index(self):
155160 return "made it!"
156161 index.exposed = True
157
162
158163 cherrypy.tree.mount(Isolated(), "/isolated")
159
164
160165 class AnotherApp:
161
166
162167 exposed = True
163
168
164169 def GET(self):
165170 return "milk"
166
167 cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}})
171
172 cherrypy.tree.mount(AnotherApp(), "/app",
173 {'/': {'request.dispatch': d}})
168174 setup_server = staticmethod(setup_server)
169175
170
171176 def testObjectMapping(self):
172177 for url in script_names:
173178 prefix = self.script_name = url
174
179
175180 self.getPage('/')
176181 self.assertBody('world')
177
182
178183 self.getPage("/dir1/myMethod")
179 self.assertBody("myMethod from dir1, path_info is:'/dir1/myMethod'")
180
184 self.assertBody(
185 "myMethod from dir1, path_info is:'/dir1/myMethod'")
186
181187 self.getPage("/this/method/does/not/exist")
182 self.assertBody("default:('this', 'method', 'does', 'not', 'exist')")
183
188 self.assertBody(
189 "default:('this', 'method', 'does', 'not', 'exist')")
190
184191 self.getPage("/extra/too/much")
185192 self.assertBody("('too', 'much')")
186
193
187194 self.getPage("/other")
188195 self.assertBody('other')
189
196
190197 self.getPage("/notExposed")
191198 self.assertBody("default:('notExposed',)")
192
199
193200 self.getPage("/dir1/dir2/")
194201 self.assertBody('index for dir2, path is:/dir1/dir2/')
195
202
196203 # Test omitted trailing slash (should be redirected by default).
197204 self.getPage("/dir1/dir2")
198205 self.assertStatus(301)
199206 self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
200
207
201208 # Test extra trailing slash (should be redirected if configured).
202209 self.getPage("/dir1/myMethod/")
203210 self.assertStatus(301)
204211 self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
205
212
206213 # Test that default method must be exposed in order to match.
207214 self.getPage("/dir1/dir2/dir3/dir4/index")
208 self.assertBody("default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
209
215 self.assertBody(
216 "default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
217
210218 # Test *vpath when default() is defined but not index()
211219 # This also tests HTTPRedirect with default.
212220 self.getPage("/defnoindex")
214222 self.assertHeader('Location', '%s/contact' % self.base())
215223 self.getPage("/defnoindex/")
216224 self.assertStatus((302, 303))
217 self.assertHeader('Location', '%s/defnoindex/contact' % self.base())
225 self.assertHeader('Location', '%s/defnoindex/contact' %
226 self.base())
218227 self.getPage("/defnoindex/page")
219228 self.assertStatus((302, 303))
220 self.assertHeader('Location', '%s/defnoindex/contact' % self.base())
221
229 self.assertHeader('Location', '%s/defnoindex/contact' %
230 self.base())
231
222232 self.getPage("/redirect")
223233 self.assertStatus('302 Found')
224234 self.assertHeader('Location', '%s/dir1/' % self.base())
225
235
226236 if not getattr(cherrypy.server, "using_apache", False):
227 # Test that we can use URL's which aren't all valid Python identifiers
237 # Test that we can use URL's which aren't all valid Python
238 # identifiers
228239 # This should also test the %XX-unquoting of URL's.
229240 self.getPage("/Von%20B%fclow?ID=14")
230241 self.assertBody("ID is 14")
231
242
232243 # Test that %2F in the path doesn't get unquoted too early;
233244 # that is, it should not be used to separate path components.
234245 # See ticket #393.
235246 self.getPage("/page%2Fname")
236247 self.assertBody("default:('page/name',)")
237
248
238249 self.getPage("/dir1/dir2/script_name")
239250 self.assertBody(url)
240251 self.getPage("/dir1/dir2/cherrypy_url")
241252 self.assertBody("%s/extra" % self.base())
242
253
243254 # Test that configs don't overwrite each other from diferent apps
244255 self.getPage("/confvalue")
245256 self.assertBody((url or "/").split("/")[-2])
246
257
247258 self.script_name = ""
248
259
249260 # Test absoluteURI's in the Request-Line
250261 self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
251262 self.assertBody('world')
252
263
253264 self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
254265 (self.interface(), self.PORT))
255266 self.assertBody("default:('abs',)")
256
267
257268 self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
258269 self.assertBody("default:('rel',)")
259
270
260271 # Test that the "isolated" app doesn't leak url's into the root app.
261272 # If it did leak, Root.default() would answer with
262273 # "default:('isolated', 'doesnt', 'exist')".
265276 self.assertBody("made it!")
266277 self.getPage("/isolated/doesnt/exist")
267278 self.assertStatus("404 Not Found")
268
279
269280 # Make sure /foobar maps to Root.foobar and not to the app
270 # mounted at /foo. See http://www.cherrypy.org/ticket/573
281 # mounted at /foo. See
282 # https://bitbucket.org/cherrypy/cherrypy/issue/573
271283 self.getPage("/foobar")
272284 self.assertBody("bar")
273
285
274286 def test_translate(self):
275287 self.getPage("/translate_html")
276288 self.assertStatus("200 OK")
277289 self.assertBody("OK")
278
290
279291 self.getPage("/translate.html")
280292 self.assertStatus("200 OK")
281293 self.assertBody("OK")
282
294
283295 self.getPage("/translate-html")
284296 self.assertStatus("200 OK")
285297 self.assertBody("OK")
286
298
287299 def test_redir_using_url(self):
288300 for url in script_names:
289301 prefix = self.script_name = url
290
302
291303 # Test the absolute path to the parent (leading slash)
292304 self.getPage('/redirect_via_url?path=./')
293305 self.assertStatus(('302 Found', '303 See Other'))
294306 self.assertHeader('Location', '%s/' % self.base())
295
307
296308 # Test the relative path to the parent (no leading slash)
297309 self.getPage('/redirect_via_url?path=./')
298310 self.assertStatus(('302 Found', '303 See Other'))
299311 self.assertHeader('Location', '%s/' % self.base())
300
312
301313 # Test the absolute path to the parent (leading slash)
302314 self.getPage('/redirect_via_url/?path=./')
303315 self.assertStatus(('302 Found', '303 See Other'))
304316 self.assertHeader('Location', '%s/' % self.base())
305
317
306318 # Test the relative path to the parent (no leading slash)
307319 self.getPage('/redirect_via_url/?path=./')
308320 self.assertStatus(('302 Found', '303 See Other'))
309321 self.assertHeader('Location', '%s/' % self.base())
310
322
311323 def testPositionalParams(self):
312324 self.getPage("/dir1/dir2/posparam/18/24/hut/hike")
313325 self.assertBody("18/24/hut/hike")
314
326
315327 # intermediate index methods should not receive posparams;
316328 # only the "final" index method should do so.
317329 self.getPage("/dir1/dir2/5/3/sir")
318330 self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
319
331
320332 # test that extra positional args raises an 404 Not Found
321 # See http://www.cherrypy.org/ticket/733.
333 # See https://bitbucket.org/cherrypy/cherrypy/issue/733.
322334 self.getPage("/dir1/dir2/script_name/extra/stuff")
323335 self.assertStatus(404)
324
336
325337 def testExpose(self):
326338 # Test the cherrypy.expose function/decorator
327339 self.getPage("/exposing/base")
328340 self.assertBody("expose works!")
329
341
330342 self.getPage("/exposing/1")
331343 self.assertBody("expose works!")
332
344
333345 self.getPage("/exposing/2")
334346 self.assertBody("expose works!")
335
347
336348 self.getPage("/exposingnew/base")
337349 self.assertBody("expose works!")
338
350
339351 self.getPage("/exposingnew/1")
340352 self.assertBody("expose works!")
341
353
342354 self.getPage("/exposingnew/2")
343355 self.assertBody("expose works!")
344
356
345357 def testMethodDispatch(self):
346358 self.getPage("/bymethod")
347359 self.assertBody("['another']")
348360 self.assertHeader('Allow', 'GET, HEAD, POST')
349
361
350362 self.getPage("/bymethod", method="HEAD")
351363 self.assertBody("")
352364 self.assertHeader('Allow', 'GET, HEAD, POST')
353
365
354366 self.getPage("/bymethod", method="POST", body="thing=one")
355367 self.assertBody("")
356368 self.assertHeader('Allow', 'GET, HEAD, POST')
357
369
358370 self.getPage("/bymethod")
359371 self.assertBody(repr(['another', ntou('one')]))
360372 self.assertHeader('Allow', 'GET, HEAD, POST')
361
373
362374 self.getPage("/bymethod", method="PUT")
363375 self.assertErrorPage(405)
364376 self.assertHeader('Allow', 'GET, HEAD, POST')
365
377
366378 # Test default with posparams
367379 self.getPage("/collection/silly", method="POST")
368380 self.getPage("/collection", method="GET")
369381 self.assertBody("['a', 'bit', 'silly']")
370
382
371383 # Test custom dispatcher set on app root (see #737).
372384 self.getPage("/app")
373385 self.assertBody("milk")
374386
375387 def testTreeMounting(self):
376388 class Root(object):
389
377390 def hello(self):
378391 return "Hello world!"
379392 hello.exposed = True
380
381 # When mounting an application instance,
393
394 # When mounting an application instance,
382395 # we can't specify a different script name in the call to mount.
383396 a = Application(Root(), '/somewhere')
384397 self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
385
398
386399 # When mounting an application instance...
387400 a = Application(Root(), '/somewhere')
388401 # ...we MUST allow in identical script name in the call to mount...
394407 cherrypy.tree.mount(a)
395408 self.getPage('/somewhere/hello')
396409 self.assertStatus(200)
397
410
398411 # In addition, we MUST be able to create an Application using
399412 # script_name == None for access to the wsgi_environ.
400413 a = Application(Root(), script_name=None)
401414 # However, this does not apply to tree.mount
402415 self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
403
66 class ProxyTest(helper.CPWebCase):
77
88 def setup_server():
9
9
1010 # Set up site
1111 cherrypy.config.update({
1212 'tools.proxy.on': True,
1313 'tools.proxy.base': 'www.mydomain.test',
14 })
15
14 })
15
1616 # Set up application
17
17
1818 class Root:
19
19
2020 def __init__(self, sn):
2121 # Calculate a URL outside of any requests.
22 self.thisnewpage = cherrypy.url("/this/new/page", script_name=sn)
23
22 self.thisnewpage = cherrypy.url(
23 "/this/new/page", script_name=sn)
24
2425 def pageurl(self):
2526 return self.thisnewpage
2627 pageurl.exposed = True
27
28
2829 def index(self):
2930 raise cherrypy.HTTPRedirect('dummy')
3031 index.exposed = True
31
32
3233 def remoteip(self):
3334 return cherrypy.request.remote.ip
3435 remoteip.exposed = True
35
36
3637 def xhost(self):
3738 raise cherrypy.HTTPRedirect('blah')
3839 xhost.exposed = True
3940 xhost._cp_config = {'tools.proxy.local': 'X-Host',
4041 'tools.trailing_slash.extra': True,
4142 }
42
43
4344 def base(self):
4445 return cherrypy.request.base
4546 base.exposed = True
46
47
4748 def ssl(self):
4849 return cherrypy.request.base
4950 ssl.exposed = True
5051 ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'}
51
52
5253 def newurl(self):
5354 return ("Browse to <a href='%s'>this page</a>."
5455 % cherrypy.url("/this/new/page"))
5556 newurl.exposed = True
56
57
5758 for sn in script_names:
5859 cherrypy.tree.mount(Root(sn), sn)
5960 setup_server = staticmethod(setup_server)
60
61
6162 def testProxy(self):
6263 self.getPage("/")
6364 self.assertHeader('Location',
6465 "%s://www.mydomain.test%s/dummy" %
6566 (self.scheme, self.prefix()))
66
67
6768 # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
68 self.getPage("/", headers=[('X-Forwarded-Host', 'http://www.example.test')])
69 self.getPage(
70 "/", headers=[('X-Forwarded-Host', 'http://www.example.test')])
6971 self.assertHeader('Location', "http://www.example.test/dummy")
7072 self.getPage("/", headers=[('X-Forwarded-Host', 'www.example.test')])
71 self.assertHeader('Location', "%s://www.example.test/dummy" % self.scheme)
73 self.assertHeader('Location', "%s://www.example.test/dummy" %
74 self.scheme)
7275 # Test multiple X-Forwarded-Host headers
7376 self.getPage("/", headers=[
7477 ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
75 ])
78 ])
7679 self.assertHeader('Location', "http://www.example.test/dummy")
77
80
7881 # Test X-Forwarded-For (Apache2)
7982 self.getPage("/remoteip",
8083 headers=[('X-Forwarded-For', '192.168.0.20')])
8184 self.assertBody("192.168.0.20")
85 #Fix bug #1268
8286 self.getPage("/remoteip",
83 headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')])
84 self.assertBody("192.168.0.20")
85
87 headers=[
88 ('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
89 ])
90 self.assertBody("67.15.36.43")
91
8692 # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
8793 self.getPage("/xhost", headers=[('X-Host', 'www.example.test')])
88 self.assertHeader('Location', "%s://www.example.test/blah" % self.scheme)
89
94 self.assertHeader('Location', "%s://www.example.test/blah" %
95 self.scheme)
96
9097 # Test X-Forwarded-Proto (lighttpd)
9198 self.getPage("/base", headers=[('X-Forwarded-Proto', 'https')])
9299 self.assertBody("https://www.mydomain.test")
93
100
94101 # Test X-Forwarded-Ssl (webfaction?)
95102 self.getPage("/ssl", headers=[('X-Forwarded-Ssl', 'on')])
96103 self.assertBody("https://www.mydomain.test")
97
104
98105 # Test cherrypy.url()
99106 for sn in script_names:
100107 # Test the value inside requests
101108 self.getPage(sn + "/newurl")
102 self.assertBody("Browse to <a href='%s://www.mydomain.test" % self.scheme
103 + sn + "/this/new/page'>this page</a>.")
109 self.assertBody(
110 "Browse to <a href='%s://www.mydomain.test" % self.scheme
111 + sn + "/this/new/page'>this page</a>.")
104112 self.getPage(sn + "/newurl", headers=[('X-Forwarded-Host',
105113 'http://www.example.test')])
106114 self.assertBody("Browse to <a href='http://www.example.test"
107115 + sn + "/this/new/page'>this page</a>.")
108
116
109117 # Test the value outside requests
110118 port = ""
111119 if self.scheme == "http" and self.PORT != 80:
120128 % (self.scheme, host, port, sn))
121129 self.getPage(sn + "/pageurl")
122130 self.assertBody(expected)
123
124 # Test trailing slash (see http://www.cherrypy.org/ticket/562).
131
132 # Test trailing slash (see
133 # https://bitbucket.org/cherrypy/cherrypy/issue/562).
125134 self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')])
126135 self.assertHeader('Location', "%s://www.example.test/xhost"
127136 % self.scheme)
128
1414 class ReferenceTests(helper.CPWebCase):
1515
1616 def setup_server():
17
17
1818 class Root:
19
1920 def index(self, *args, **kwargs):
2021 cherrypy.request.thing = data
2122 return "Hello world!"
2223 index.exposed = True
23
24
2425 cherrypy.tree.mount(Root())
2526 setup_server = staticmethod(setup_server)
26
27
2728 def test_threadlocal_garbage(self):
2829 success = []
29
30
3031 def getpage():
3132 host = '%s:%s' % (self.interface(), self.PORT)
3233 if self.scheme == 'https':
4344 finally:
4445 c.close()
4546 success.append(True)
46
47
4748 ITERATIONS = 25
4849 ts = []
4950 for _ in range(ITERATIONS):
5051 t = threading.Thread(target=getpage)
5152 ts.append(t)
5253 t.start()
53
54
5455 for t in ts:
5556 t.join()
56
57
5758 self.assertEqual(len(success), ITERATIONS)
58
1717
1818 from cherrypy.test import helper
1919
20
2021 class RequestObjectTests(helper.CPWebCase):
2122
2223 def setup_server():
2324 class Root:
24
25
2526 def index(self):
2627 return "hello"
2728 index.exposed = True
28
29
2930 def scheme(self):
3031 return cherrypy.request.scheme
3132 scheme.exposed = True
32
33
3334 root = Root()
34
35
35
3636 class TestType(type):
37 """Metaclass which automatically exposes all functions in each subclass,
38 and adds an instance of the subclass as an attribute of root.
37 """Metaclass which automatically exposes all functions in each
38 subclass, and adds an instance of the subclass as an attribute
39 of root.
3940 """
4041 def __init__(cls, name, bases, dct):
4142 type.__init__(cls, name, bases, dct)
5152 return cherrypy.request.path_info
5253
5354 class Params(Test):
54
55
5556 def index(self, thing):
5657 return repr(thing)
57
58
5859 def ismap(self, x, y):
5960 return "Coordinates: %s, %s" % (x, y)
60
61
6162 def default(self, *args, **kwargs):
62 return "args: %s kwargs: %s" % (args, kwargs)
63 return "args: %s kwargs: %s" % (args, sorted(kwargs.items()))
6364 default._cp_config = {'request.query_string_encoding': 'latin1'}
64
6565
6666 class ParamErrorsCallable(object):
6767 exposed = True
68
6869 def __call__(self):
6970 return "data"
7071
109110 raise_type_error.exposed = True
110111
111112 def raise_type_error_with_default_param(self, x, y=None):
112 return '%d' % 'a' # throw an exception
113 return '%d' % 'a' # throw an exception
113114 raise_type_error_with_default_param.exposed = True
114115
115116 def callable_error_page(status, **kwargs):
116 return "Error %s - Well, I'm very sorry but you haven't paid!" % status
117
118
117 return "Error %s - Well, I'm very sorry but you haven't paid!" % (
118 status)
119
119120 class Error(Test):
120
121
121122 _cp_config = {'tools.log_tracebacks.on': True,
122123 }
123
124
124125 def reason_phrase(self):
125126 raise cherrypy.HTTPError("410 Gone fishin'")
126
127
127128 def custom(self, err='404'):
128 raise cherrypy.HTTPError(int(err), "No, <b>really</b>, not found!")
129 custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"),
130 'error_page.401': callable_error_page,
131 }
132
129 raise cherrypy.HTTPError(
130 int(err), "No, <b>really</b>, not found!")
131 custom._cp_config = {
132 'error_page.404': os.path.join(localDir, "static/index.html"),
133 'error_page.401': callable_error_page,
134 }
135
133136 def custom_default(self):
134 return 1 + 'a' # raise an unexpected error
135 custom_default._cp_config = {'error_page.default': callable_error_page}
136
137 return 1 + 'a' # raise an unexpected error
138 custom_default._cp_config = {
139 'error_page.default': callable_error_page}
140
137141 def noexist(self):
138142 raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
139143 noexist._cp_config = {'error_page.404': "nonexistent.html"}
140
144
141145 def page_method(self):
142146 raise ValueError()
143
147
144148 def page_yield(self):
145149 yield "howdy"
146150 raise ValueError()
147
151
148152 def page_streamed(self):
149153 yield "word up"
150154 raise ValueError()
151155 yield "very oops"
152156 page_streamed._cp_config = {"response.stream": True}
153
157
154158 def cause_err_in_finalize(self):
155159 # Since status must start with an int, this should error.
156160 cherrypy.response.status = "ZOO OK"
157 cause_err_in_finalize._cp_config = {'request.show_tracebacks': False}
158
161 cause_err_in_finalize._cp_config = {
162 'request.show_tracebacks': False}
163
159164 def rethrow(self):
160 """Test that an error raised here will be thrown out to the server."""
165 """Test that an error raised here will be thrown out to
166 the server.
167 """
161168 raise ValueError()
162169 rethrow._cp_config = {'request.throw_errors': True}
163
164
170
165171 class Expect(Test):
166
172
167173 def expectation_failed(self):
168174 expect = cherrypy.request.headers.elements("Expect")
169175 if expect and expect[0].value != '100-continue':
171177 raise cherrypy.HTTPError(417, 'Expectation Failed')
172178
173179 class Headers(Test):
174
180
175181 def default(self, headername):
176182 """Spit back out the value for the requested header."""
177183 return cherrypy.request.headers[headername]
178
184
179185 def doubledheaders(self):
180 # From http://www.cherrypy.org/ticket/165:
181 # "header field names should not be case sensitive sayes the rfc.
182 # if i set a headerfield in complete lowercase i end up with two
183 # header fields, one in lowercase, the other in mixed-case."
184
186 # From https://bitbucket.org/cherrypy/cherrypy/issue/165:
187 # "header field names should not be case sensitive sayes the
188 # rfc. if i set a headerfield in complete lowercase i end up
189 # with two header fields, one in lowercase, the other in
190 # mixed-case."
191
185192 # Set the most common headers
186193 hMap = cherrypy.response.headers
187194 hMap['content-type'] = "text/html"
191198 % (cherrypy.request.local.ip,
192199 cherrypy.request.local.port,
193200 cherrypy.request.scheme))
194
201
195202 # Set a rare header for fun
196203 hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
197
204
198205 return "double header test"
199
206
200207 def ifmatch(self):
201208 val = cherrypy.request.headers['If-Match']
202209 assert isinstance(val, unicodestr)
203210 cherrypy.response.headers['ETag'] = val
204211 return val
205
206
212
207213 class HeaderElements(Test):
208
214
209215 def get_elements(self, headername):
210216 e = cherrypy.request.headers.elements(headername)
211217 return "\n".join([unicodestr(x) for x in e])
212
213
218
214219 class Method(Test):
215
220
216221 def index(self):
217222 m = cherrypy.request.method
218223 if m in defined_http_methods or m == "CONNECT":
219224 return m
220
225
221226 if m == "LINK":
222227 raise cherrypy.HTTPError(405)
223228 else:
224229 raise cherrypy.HTTPError(501)
225
230
226231 def parameterized(self, data):
227232 return data
228
233
229234 def request_body(self):
230235 # This should be a file object (temp file),
231236 # which CP will just pipe back out if we tell it to.
232237 return cherrypy.request.body
233
238
234239 def reachable(self):
235240 return "success"
236241
237242 class Divorce:
243
238244 """HTTP Method handlers shouldn't collide with normal method names.
239 For example, a GET-handler shouldn't collide with a method named 'get'.
240
241 If you build HTTP method dispatching into CherryPy, rewrite this class
242 to use your new dispatch mechanism and make sure that:
245 For example, a GET-handler shouldn't collide with a method named
246 'get'.
247
248 If you build HTTP method dispatching into CherryPy, rewrite this
249 class to use your new dispatch mechanism and make sure that:
243250 "GET /divorce HTTP/1.1" maps to divorce.index() and
244251 "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
245252 """
246
253
247254 documents = {}
248
255
249256 def index(self):
250257 yield "<h1>Choose your document</h1>\n"
251258 yield "<ul>\n"
252259 for id, contents in self.documents.items():
253 yield (" <li><a href='/divorce/get?ID=%s'>%s</a>: %s</li>\n"
254 % (id, id, contents))
260 yield (
261 " <li><a href='/divorce/get?ID=%s'>%s</a>:"
262 " %s</li>\n" % (id, id, contents))
255263 yield "</ul>"
256264 index.exposed = True
257
265
258266 def get(self, ID):
259267 return ("Divorce document %s: %s" %
260268 (ID, self.documents.get(ID, "empty")))
262270
263271 root.divorce = Divorce()
264272
265
266273 class ThreadLocal(Test):
267
274
268275 def index(self):
269276 existing = repr(getattr(cherrypy.request, "asdf", None))
270277 cherrypy.request.asdf = "rassfrassin"
271278 return existing
272
279
273280 appconf = {
274 '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")},
275 }
281 '/method': {
282 'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")
283 },
284 }
276285 cherrypy.tree.mount(root, config=appconf)
277286 setup_server = staticmethod(setup_server)
278287
292301 def testParams(self):
293302 self.getPage("/params/?thing=a")
294303 self.assertBody(repr(ntou("a")))
295
304
296305 self.getPage("/params/?thing=a&thing=b&thing=c")
297306 self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')]))
298307
302311 self.assertInBody("Missing parameters: thing")
303312 self.getPage("/params/?thing=meeting&notathing=meeting")
304313 self.assertInBody("Unexpected query string parameters: notathing")
305
314
306315 # Test ability to turn off friendly error messages
307316 cherrypy.config.update({"request.show_mismatched_params": False})
308317 self.getPage("/params/?notathing=meeting")
314323 self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
315324 self.assertBody("args: %s kwargs: %s" %
316325 (('\xd4 \xe3', 'cheese'),
317 {'Gruy\xe8re': ntou('Bulgn\xe9ville')}))
318
326 [('Gruy\xe8re', ntou('Bulgn\xe9ville'))]))
327
319328 # Make sure that encoded = and & get parsed correctly
320 self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2")
329 self.getPage(
330 "/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2")
321331 self.assertBody("args: %s kwargs: %s" %
322332 (('code',),
323 {'url': ntou('http://cherrypy.org/index?a=1&b=2')}))
324
333 [('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
334
325335 # Test coordinates sent by <img ismap>
326336 self.getPage("/params/ismap?223,114")
327337 self.assertBody("Coordinates: 223, 114")
328
338
329339 # Test "name[key]" dict-like params
330340 self.getPage("/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz")
331341 self.assertBody("args: %s kwargs: %s" %
332342 (('dictlike',),
333 {'a[1]': ntou('1'), 'b[bar]': ntou('baz'),
334 'b': ntou('foo'), 'a[2]': ntou('2')}))
343 [('a[1]', ntou('1')), ('a[2]', ntou('2')),
344 ('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
335345
336346 def testParamErrors(self):
337347
338 # test that all of the handlers work when given
348 # test that all of the handlers work when given
339349 # the correct parameters in order to ensure that the
340350 # errors below aren't coming from some other source.
341351 for uri in (
343353 '/paramerrors/one_positional_args?param1=foo',
344354 '/paramerrors/one_positional_args/foo',
345355 '/paramerrors/one_positional_args/foo/bar/baz',
346 '/paramerrors/one_positional_args_kwargs?param1=foo&param2=bar',
347 '/paramerrors/one_positional_args_kwargs/foo?param2=bar&param3=baz',
348 '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
349 '/paramerrors/one_positional_kwargs?param1=foo&param2=bar&param3=baz',
350 '/paramerrors/one_positional_kwargs/foo?param4=foo&param2=bar&param3=baz',
356 '/paramerrors/one_positional_args_kwargs?'
357 'param1=foo&param2=bar',
358 '/paramerrors/one_positional_args_kwargs/foo?'
359 'param2=bar&param3=baz',
360 '/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
361 'param2=bar&param3=baz',
362 '/paramerrors/one_positional_kwargs?'
363 'param1=foo&param2=bar&param3=baz',
364 '/paramerrors/one_positional_kwargs/foo?'
365 'param4=foo&param2=bar&param3=baz',
351366 '/paramerrors/no_positional',
352367 '/paramerrors/no_positional_args/foo',
353368 '/paramerrors/no_positional_args/foo/bar/baz',
354369 '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
355370 '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
356 '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
371 '/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
372 'param2=bar&param3=baz',
357373 '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
358374 '/paramerrors/callable_object',
359 ):
375 ):
360376 self.getPage(uri)
361377 self.assertStatus(200)
362378
363379 # query string parameters are part of the URI, so if they are wrong
364380 # for a particular handler, the status MUST be a 404.
365381 error_msgs = [
366 'Missing parameters',
367 'Nothing matches the given URI',
368 'Multiple values for parameters',
369 'Unexpected query string parameters',
370 'Unexpected body parameters',
371 ]
382 'Missing parameters',
383 'Nothing matches the given URI',
384 'Multiple values for parameters',
385 'Unexpected query string parameters',
386 'Unexpected body parameters',
387 ]
372388 for uri, msg in (
373389 ('/paramerrors/one_positional', error_msgs[0]),
374390 ('/paramerrors/one_positional?foo=foo', error_msgs[0]),
375391 ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
376392 ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
377 ('/paramerrors/one_positional/foo?param1=foo&param2=foo', error_msgs[2]),
378 ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo', error_msgs[2]),
379 ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]),
380 ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar&param3=baz', error_msgs[2]),
381 ('/paramerrors/one_positional_kwargs/foo?param1=foo&param2=bar&param3=baz', error_msgs[2]),
393 ('/paramerrors/one_positional/foo?param1=foo&param2=foo',
394 error_msgs[2]),
395 ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
396 error_msgs[2]),
397 ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
398 error_msgs[3]),
399 ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
400 'param1=bar&param3=baz',
401 error_msgs[2]),
402 ('/paramerrors/one_positional_kwargs/foo?'
403 'param1=foo&param2=bar&param3=baz',
404 error_msgs[2]),
382405 ('/paramerrors/no_positional/boo', error_msgs[1]),
383406 ('/paramerrors/no_positional?param1=foo', error_msgs[3]),
384407 ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
385 ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]),
408 ('/paramerrors/no_positional_kwargs/boo?param1=foo',
409 error_msgs[1]),
386410 ('/paramerrors/callable_object?param1=foo', error_msgs[3]),
387411 ('/paramerrors/callable_object/boo', error_msgs[1]),
388 ):
412 ):
389413 for show_mismatched_params in (True, False):
390 cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
414 cherrypy.config.update(
415 {'request.show_mismatched_params': show_mismatched_params})
391416 self.getPage(uri)
392417 self.assertStatus(404)
393418 if show_mismatched_params:
397422
398423 # if body parameters are wrong, a 400 must be returned.
399424 for uri, body, msg in (
400 ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]),
401 ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo', error_msgs[2]),
402 ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo', error_msgs[2]),
403 ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]),
404 ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz', error_msgs[2]),
405 ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
425 ('/paramerrors/one_positional/foo',
426 'param1=foo', error_msgs[2]),
427 ('/paramerrors/one_positional/foo',
428 'param1=foo&param2=foo', error_msgs[2]),
429 ('/paramerrors/one_positional_args/foo',
430 'param1=foo&param2=foo', error_msgs[2]),
431 ('/paramerrors/one_positional_args/foo/bar/baz',
432 'param2=foo', error_msgs[4]),
433 ('/paramerrors/one_positional_args_kwargs/foo/bar/baz',
434 'param1=bar&param3=baz', error_msgs[2]),
435 ('/paramerrors/one_positional_kwargs/foo',
436 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
406437 ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
407 ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]),
438 ('/paramerrors/no_positional_args/boo',
439 'param1=foo', error_msgs[4]),
408440 ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
409 ):
441 ):
410442 for show_mismatched_params in (True, False):
411 cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
443 cherrypy.config.update(
444 {'request.show_mismatched_params': show_mismatched_params})
412445 self.getPage(uri, method='POST', body=body)
413446 self.assertStatus(400)
414447 if show_mismatched_params:
416449 else:
417450 self.assertInBody("400 Bad")
418451
419
420 # even if body parameters are wrong, if we get the uri wrong, then
452 # even if body parameters are wrong, if we get the uri wrong, then
421453 # it's a 404
422454 for uri, body, msg in (
423 ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]),
424 ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]),
425 ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]),
426 ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz', error_msgs[1]),
427 ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]),
428 ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]),
429 ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]),
430 ):
455 ('/paramerrors/one_positional?param2=foo',
456 'param1=foo', error_msgs[3]),
457 ('/paramerrors/one_positional/foo/bar',
458 'param2=foo', error_msgs[1]),
459 ('/paramerrors/one_positional_args/foo/bar?param2=foo',
460 'param3=foo', error_msgs[3]),
461 ('/paramerrors/one_positional_kwargs/foo/bar',
462 'param2=bar&param3=baz', error_msgs[1]),
463 ('/paramerrors/no_positional?param1=foo',
464 'param2=foo', error_msgs[3]),
465 ('/paramerrors/no_positional_args/boo?param2=foo',
466 'param1=foo', error_msgs[3]),
467 ('/paramerrors/callable_object?param2=bar',
468 'param1=foo', error_msgs[3]),
469 ):
431470 for show_mismatched_params in (True, False):
432 cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
471 cherrypy.config.update(
472 {'request.show_mismatched_params': show_mismatched_params})
433473 self.getPage(uri, method='POST', body=body)
434474 self.assertStatus(404)
435475 if show_mismatched_params:
443483 '/paramerrors/raise_type_error',
444484 '/paramerrors/raise_type_error_with_default_param?x=0',
445485 '/paramerrors/raise_type_error_with_default_param?x=0&y=0',
446 ):
486 ):
447487 self.getPage(uri, method='GET')
448488 self.assertStatus(500)
449489 self.assertTrue('Client Error', self.body)
452492 self.getPage("/error/missing")
453493 self.assertStatus(404)
454494 self.assertErrorPage(404, "The path '/error/missing' was not found.")
455
495
456496 ignore = helper.webtest.ignored_exceptions
457497 ignore.append(ValueError)
458498 try:
459499 valerr = '\n raise ValueError()\nValueError'
460500 self.getPage("/error/page_method")
461501 self.assertErrorPage(500, pattern=valerr)
462
502
463503 self.getPage("/error/page_yield")
464504 self.assertErrorPage(500, pattern=valerr)
465
505
466506 if (cherrypy.server.protocol_version == "HTTP/1.0" or
467 getattr(cherrypy.server, "using_apache", False)):
507 getattr(cherrypy.server, "using_apache", False)):
468508 self.getPage("/error/page_streamed")
469509 # Because this error is raised after the response body has
470510 # started, the status should not change to an error status.
475515 # The HTTP client will choke when the output is incomplete.
476516 self.assertRaises((ValueError, IncompleteRead), self.getPage,
477517 "/error/page_streamed")
478
518
479519 # No traceback should be present
480520 self.getPage("/error/cause_err_in_finalize")
481521 msg = "Illegal response status from server ('ZOO' is non-numeric)."
482522 self.assertErrorPage(500, msg, None)
483523 finally:
484524 ignore.pop()
485
525
486526 # Test HTTPError with a reason-phrase in the status arg.
487527 self.getPage('/error/reason_phrase')
488528 self.assertStatus("410 Gone fishin'")
489
529
490530 # Test custom error page for a specific error.
491531 self.getPage("/error/custom")
492532 self.assertStatus(404)
493533 self.assertBody("Hello, world\r\n" + (" " * 499))
494
534
495535 # Test custom error page for a specific error.
496536 self.getPage("/error/custom?err=401")
497537 self.assertStatus(401)
498 self.assertBody("Error 401 Unauthorized - Well, I'm very sorry but you haven't paid!")
499
538 self.assertBody(
539 "Error 401 Unauthorized - "
540 "Well, I'm very sorry but you haven't paid!")
541
500542 # Test default custom error page.
501543 self.getPage("/error/custom_default")
502544 self.assertStatus(500)
503 self.assertBody("Error 500 Internal Server Error - Well, I'm very sorry but you haven't paid!".ljust(513))
504
545 self.assertBody(
546 "Error 500 Internal Server Error - "
547 "Well, I'm very sorry but you haven't paid!".ljust(513))
548
505549 # Test error in custom error page (ticket #305).
506550 # Note that the message is escaped for HTML (ticket #310).
507551 self.getPage("/error/noexist")
508552 self.assertStatus(404)
553 if sys.version_info >= (3, 3):
554 exc_name = "FileNotFoundError"
555 else:
556 exc_name = "IOError"
509557 msg = ("No, &lt;b&gt;really&lt;/b&gt;, not found!<br />"
510558 "In addition, the custom error page failed:\n<br />"
511 "IOError: [Errno 2] No such file or directory: 'nonexistent.html'")
559 "%s: [Errno 2] "
560 "No such file or directory: 'nonexistent.html'") % (exc_name,)
512561 self.assertInBody(msg)
513
562
514563 if getattr(cherrypy.server, "using_apache", False):
515564 pass
516565 else:
517566 # Test throw_errors (ticket #186).
518567 self.getPage("/error/rethrow")
519568 self.assertInBody("raise ValueError()")
520
569
521570 def testExpect(self):
522571 e = ('Expect', '100-continue')
523572 self.getPage("/headerelements/get_elements?headername=Expect", [e])
524573 self.assertBody('100-continue')
525
574
526575 self.getPage("/expect/expectation_failed", [e])
527576 self.assertStatus(417)
528
577
529578 def testHeaderElements(self):
530579 # Accept-* header elements should be sorted, with most preferred first.
531580 h = [('Accept', 'audio/*; q=0.2, audio/basic')]
533582 self.assertStatus(200)
534583 self.assertBody("audio/basic\n"
535584 "audio/*;q=0.2")
536
537 h = [('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')]
585
586 h = [
587 ('Accept',
588 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
589 ]
538590 self.getPage("/headerelements/get_elements?headername=Accept", h)
539591 self.assertStatus(200)
540592 self.assertBody("text/x-c\n"
541593 "text/html\n"
542594 "text/x-dvi;q=0.8\n"
543595 "text/plain;q=0.5")
544
596
545597 # Test that more specific media ranges get priority.
546598 h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
547599 self.getPage("/headerelements/get_elements?headername=Accept", h)
550602 "text/html\n"
551603 "text/*\n"
552604 "*/*")
553
605
554606 # Test Accept-Charset
555607 h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
556 self.getPage("/headerelements/get_elements?headername=Accept-Charset", h)
608 self.getPage(
609 "/headerelements/get_elements?headername=Accept-Charset", h)
557610 self.assertStatus("200 OK")
558611 self.assertBody("iso-8859-5\n"
559612 "unicode-1-1;q=0.8")
560
613
561614 # Test Accept-Encoding
562615 h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
563 self.getPage("/headerelements/get_elements?headername=Accept-Encoding", h)
616 self.getPage(
617 "/headerelements/get_elements?headername=Accept-Encoding", h)
564618 self.assertStatus("200 OK")
565619 self.assertBody("gzip;q=1.0\n"
566620 "identity;q=0.5\n"
567621 "*;q=0")
568
622
569623 # Test Accept-Language
570624 h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
571 self.getPage("/headerelements/get_elements?headername=Accept-Language", h)
625 self.getPage(
626 "/headerelements/get_elements?headername=Accept-Language", h)
572627 self.assertStatus("200 OK")
573628 self.assertBody("da\n"
574629 "en-gb;q=0.8\n"
575630 "en;q=0.7")
576
577 # Test malformed header parsing. See http://www.cherrypy.org/ticket/763.
631
632 # Test malformed header parsing. See
633 # https://bitbucket.org/cherrypy/cherrypy/issue/763.
578634 self.getPage("/headerelements/get_elements?headername=Content-Type",
579635 # Note the illegal trailing ";"
580636 headers=[('Content-Type', 'text/html; charset=utf-8;')])
581637 self.assertStatus(200)
582638 self.assertBody("text/html;charset=utf-8")
583
639
584640 def test_repeated_headers(self):
585641 # Test that two request headers are collapsed into one.
586 # See http://www.cherrypy.org/ticket/542.
642 # See https://bitbucket.org/cherrypy/cherrypy/issue/542.
587643 self.getPage("/headers/Accept-Charset",
588644 headers=[("Accept-Charset", "iso-8859-5"),
589645 ("Accept-Charset", "unicode-1-1;q=0.8")])
590646 self.assertBody("iso-8859-5, unicode-1-1;q=0.8")
591
647
592648 # Tests that each header only appears once, regardless of case.
593649 self.getPage("/headers/doubledheaders")
594650 self.assertBody("double header test")
596652 for key in ['Content-Length', 'Content-Type', 'Date',
597653 'Expires', 'Location', 'Server']:
598654 self.assertEqual(hnames.count(key), 1, self.headers)
599
655
600656 def test_encoded_headers(self):
601657 # First, make sure the innards work like expected.
602 self.assertEqual(httputil.decode_TEXT(ntou("=?utf-8?q?f=C3=BCr?=")), ntou("f\xfcr"))
603
658 self.assertEqual(
659 httputil.decode_TEXT(ntou("=?utf-8?q?f=C3=BCr?=")), ntou("f\xfcr"))
660
604661 if cherrypy.server.protocol_version == "HTTP/1.1":
605662 # Test RFC-2047-encoded request and response header values
606663 u = ntou('\u212bngstr\xf6m', 'escape')
607664 c = ntou("=E2=84=ABngstr=C3=B6m")
608 self.getPage("/headers/ifmatch", [('If-Match', ntou('=?utf-8?q?%s?=') % c)])
665 self.getPage("/headers/ifmatch",
666 [('If-Match', ntou('=?utf-8?q?%s?=') % c)])
609667 # The body should be utf-8 encoded.
610668 self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m"))
611669 # But the Etag header should be RFC-2047 encoded (binary)
612670 self.assertHeader("ETag", ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?='))
613
671
614672 # Test a *LONG* RFC-2047-encoded request and response header value
615673 self.getPage("/headers/ifmatch",
616674 [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))])
617675 self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m") * 10)
618676 # Note: this is different output for Python3, but it decodes fine.
619 etag = self.assertHeader("ETag",
677 etag = self.assertHeader(
678 "ETag",
620679 '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
621680 '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
622681 '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
623682 '4oSrbmdzdHLDtm0=?=')
624683 self.assertEqual(httputil.decode_TEXT(etag), u * 10)
625
684
626685 def test_header_presence(self):
627686 # If we don't pass a Content-Type header, it should not be present
628687 # in cherrypy.request.headers
629688 self.getPage("/headers/Content-Type",
630689 headers=[])
631690 self.assertStatus(500)
632
691
633692 # If Content-Type is present in the request, it should be present in
634693 # cherrypy.request.headers
635694 self.getPage("/headers/Content-Type",
636695 headers=[("Content-type", "application/json")])
637696 self.assertBody("application/json")
638
697
639698 def test_basic_HTTPMethods(self):
640699 helper.webtest.methods_with_bodies = ("POST", "PUT", "PROPFIND")
641
700
642701 # Test that all defined HTTP methods work.
643702 for m in defined_http_methods:
644703 self.getPage("/method/", method=m)
645
704
646705 # HEAD requests should not return any body.
647706 if m == "HEAD":
648707 self.assertBody("")
651710 self.assertEqual(self.body[:5], ntob("TRACE"))
652711 else:
653712 self.assertBody(m)
654
713
655714 # Request a PUT method with a form-urlencoded body
656715 self.getPage("/method/parameterized", method="PUT",
657 body="data=on+top+of+other+things")
716 body="data=on+top+of+other+things")
658717 self.assertBody("on top of other things")
659
718
660719 # Request a PUT method with a file body
661720 b = "one thing on top of another"
662721 h = [("Content-Type", "text/plain"),
664723 self.getPage("/method/request_body", headers=h, method="PUT", body=b)
665724 self.assertStatus(200)
666725 self.assertBody(b)
667
726
668727 # Request a PUT method with a file body but no Content-Type.
669 # See http://www.cherrypy.org/ticket/790.
728 # See https://bitbucket.org/cherrypy/cherrypy/issue/790.
670729 b = ntob("one thing on top of another")
671730 self.persistent = True
672731 try:
683742 self.assertBody(b)
684743 finally:
685744 self.persistent = False
686
745
687746 # Request a PUT method with no body whatsoever (not an empty one).
688 # See http://www.cherrypy.org/ticket/650.
747 # See https://bitbucket.org/cherrypy/cherrypy/issue/650.
689748 # Provide a C-T or webtest will provide one (and a C-L) for us.
690749 h = [("Content-Type", "text/plain")]
691750 self.getPage("/method/reachable", headers=h, method="PUT")
692751 self.assertStatus(411)
693
752
694753 # Request a custom method with a request body
695754 b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
696755 '<propfind xmlns="DAV:"><prop><getlastmodified/>'
697756 '</prop></propfind>')
698757 h = [('Content-Type', 'text/xml'),
699758 ('Content-Length', str(len(b)))]
700 self.getPage("/method/request_body", headers=h, method="PROPFIND", body=b)
759 self.getPage("/method/request_body", headers=h,
760 method="PROPFIND", body=b)
701761 self.assertStatus(200)
702762 self.assertBody(b)
703
763
704764 # Request a disallowed method
705765 self.getPage("/method/", method="LINK")
706766 self.assertStatus(405)
707
767
708768 # Request an unknown method
709769 self.getPage("/method/", method="SEARCH")
710770 self.assertStatus(501)
711
771
712772 # For method dispatchers: make sure that an HTTP method doesn't
713773 # collide with a virtual path atom. If you build HTTP-method
714774 # dispatching into the core, rewrite these handlers to use
719779 self.getPage("/divorce/", method="GET")
720780 self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
721781 self.assertStatus(200)
722
782
723783 def test_CONNECT_method(self):
724784 if getattr(cherrypy.server, "using_apache", False):
725785 return self.skip("skipped due to known Apache differences... ")
726
786
727787 self.getPage("/method/", method="CONNECT")
728788 self.assertBody("CONNECT")
729
789
730790 def testEmptyThreadlocals(self):
731791 results = []
732792 for x in range(20):
733793 self.getPage("/threadlocal/")
734794 results.append(self.body)
735795 self.assertEqual(results, [ntob("None")] * 20)
736
44
55 from cherrypy.test import helper
66 import nose
7
78
89 class RoutesDispatchTest(helper.CPWebCase):
910
1516 raise nose.SkipTest('Install routes to test RoutesDispatcher code')
1617
1718 class Dummy:
19
1820 def index(self):
1921 return "I said good day!"
20
22
2123 class City:
22
24
2325 def __init__(self, name):
2426 self.name = name
2527 self.population = 10000
26
28
2729 def index(self, **kwargs):
2830 return "Welcome to %s, pop. %s" % (self.name, self.population)
29 index._cp_config = {'tools.response_headers.on': True,
30 'tools.response_headers.headers': [('Content-Language', 'en-GB')]}
31
31 index._cp_config = {
32 'tools.response_headers.on': True,
33 'tools.response_headers.headers': [
34 ('Content-Language', 'en-GB')
35 ]
36 }
37
3238 def update(self, **kwargs):
3339 self.population = kwargs['pop']
3440 return "OK"
35
41
3642 d = cherrypy.dispatch.RoutesDispatcher()
3743 d.connect(action='index', name='hounslow', route='/hounslow',
3844 controller=City('Hounslow'))
39 d.connect(name='surbiton', route='/surbiton', controller=City('Surbiton'),
40 action='index', conditions=dict(method=['GET']))
45 d.connect(
46 name='surbiton', route='/surbiton', controller=City('Surbiton'),
47 action='index', conditions=dict(method=['GET']))
4148 d.mapper.connect('/surbiton', controller='surbiton',
4249 action='update', conditions=dict(method=['POST']))
4350 d.connect('main', ':action', controller=Dummy())
44
51
4552 conf = {'/': {'request.dispatch': d}}
4653 cherrypy.tree.mount(root=None, config=conf)
4754 setup_server = staticmethod(setup_server)
5057 self.getPage("/hounslow")
5158 self.assertStatus("200 OK")
5259 self.assertBody("Welcome to Hounslow, pop. 10000")
53
60
5461 self.getPage("/foo")
5562 self.assertStatus("404 Not Found")
56
63
5764 self.getPage("/surbiton")
5865 self.assertStatus("200 OK")
5966 self.assertBody("Welcome to Surbiton, pop. 10000")
60
67
6168 self.getPage("/surbiton", method="POST", body="pop=1327")
6269 self.assertStatus("200 OK")
6370 self.assertBody("OK")
6572 self.assertStatus("200 OK")
6673 self.assertHeader("Content-Language", "en-GB")
6774 self.assertBody("Welcome to Surbiton, pop. 1327")
68
88 from cherrypy.lib import sessions
99 from cherrypy.lib.httputil import response_codes
1010
11
1112 def http_methods_allowed(methods=['GET', 'HEAD']):
1213 method = cherrypy.request.method.upper()
1314 if method not in methods:
1819
1920
2021 def setup_server():
21
22
2223 class Root:
23
24
2425 _cp_config = {'tools.sessions.on': True,
25 'tools.sessions.storage_type' : 'ram',
26 'tools.sessions.storage_path' : localDir,
26 'tools.sessions.storage_type': 'ram',
27 'tools.sessions.storage_path': localDir,
2728 'tools.sessions.timeout': (1.0 / 60),
2829 'tools.sessions.clean_freq': (1.0 / 60),
2930 }
30
31
3132 def clear(self):
3233 cherrypy.session.cache.clear()
3334 clear.exposed = True
34
35
3536 def data(self):
3637 cherrypy.session['aha'] = 'foo'
3738 return repr(cherrypy.session._data)
3839 data.exposed = True
39
40
4041 def testGen(self):
4142 counter = cherrypy.session.get('counter', 0) + 1
4243 cherrypy.session['counter'] = counter
4344 yield str(counter)
4445 testGen.exposed = True
45
46
4647 def testStr(self):
4748 counter = cherrypy.session.get('counter', 0) + 1
4849 cherrypy.session['counter'] = counter
4950 return str(counter)
5051 testStr.exposed = True
51
52
5253 def setsessiontype(self, newtype):
53 self.__class__._cp_config.update({'tools.sessions.storage_type': newtype})
54 self.__class__._cp_config.update(
55 {'tools.sessions.storage_type': newtype})
5456 if hasattr(cherrypy, "session"):
5557 del cherrypy.session
5658 cls = getattr(sessions, newtype.title() + 'Session')
6062 del cls.clean_thread
6163 setsessiontype.exposed = True
6264 setsessiontype._cp_config = {'tools.sessions.on': False}
63
65
6466 def index(self):
6567 sess = cherrypy.session
6668 c = sess.get('counter', 0) + 1
6870 sess['counter'] = c
6971 return str(c)
7072 index.exposed = True
71
73
7274 def keyin(self, key):
7375 return str(key in cherrypy.session)
7476 keyin.exposed = True
75
77
7678 def delete(self):
7779 cherrypy.session.delete()
7880 sessions.expire()
7981 return "done"
8082 delete.exposed = True
81
83
8284 def delkey(self, key):
8385 del cherrypy.session[key]
8486 return "OK"
8587 delkey.exposed = True
86
88
8789 def blah(self):
8890 return self._cp_config['tools.sessions.storage_type']
8991 blah.exposed = True
90
92
9193 def iredir(self):
9294 raise cherrypy.InternalRedirect('/blah')
9395 iredir.exposed = True
94
96
9597 def restricted(self):
9698 return cherrypy.request.method
9799 restricted.exposed = True
98100 restricted._cp_config = {'tools.allow.on': True,
99101 'tools.allow.methods': ['GET']}
100
102
101103 def regen(self):
102104 cherrypy.tools.sessions.regenerate()
103105 return "logged in"
104106 regen.exposed = True
105
107
106108 def length(self):
107109 return str(len(cherrypy.session))
108110 length.exposed = True
109
111
110112 def session_cookie(self):
111113 # Must load() to start the clean thread.
112114 cherrypy.session.load()
116118 'tools.sessions.path': '/session_cookie',
117119 'tools.sessions.name': 'temp',
118120 'tools.sessions.persistent': False}
119
121
120122 cherrypy.tree.mount(Root())
121123
122124
123125 from cherrypy.test import helper
126
124127
125128 class SessionTest(helper.CPWebCase):
126129 setup_server = staticmethod(setup_server)
127
130
128131 def tearDown(self):
129132 # Clean up sessions.
130133 for fname in os.listdir(localDir):
131134 if fname.startswith(sessions.FileSession.SESSION_PREFIX):
132135 os.unlink(os.path.join(localDir, fname))
133
136
134137 def test_0_Session(self):
135138 self.getPage('/setsessiontype/ram')
136139 self.getPage('/clear')
137
140
138141 # Test that a normal request gets the same id in the cookies.
139142 # Note: this wouldn't work if /data didn't load the session.
140143 self.getPage('/data')
142145 c = self.cookies[0]
143146 self.getPage('/data', self.cookies)
144147 self.assertEqual(self.cookies[0], c)
145
148
146149 self.getPage('/testStr')
147150 self.assertBody('1')
148151 cookie_parts = dict([p.strip().split('=')
160163 self.assertBody('2')
161164 self.getPage('/delkey?key=counter', self.cookies)
162165 self.assertStatus(200)
163
166
164167 self.getPage('/setsessiontype/file')
165168 self.getPage('/testStr')
166169 self.assertBody('1')
170173 self.assertBody('3')
171174 self.getPage('/delkey?key=counter', self.cookies)
172175 self.assertStatus(200)
173
176
174177 # Wait for the session.timeout (1 second)
175178 time.sleep(2)
176179 self.getPage('/')
177180 self.assertBody('1')
178181 self.getPage('/length', self.cookies)
179182 self.assertBody('1')
180
183
181184 # Test session __contains__
182185 self.getPage('/keyin?key=counter', self.cookies)
183186 self.assertBody("True")
184187 cookieset1 = self.cookies
185
188
186189 # Make a new session and test __len__ again
187190 self.getPage('/')
188191 self.getPage('/length', self.cookies)
189192 self.assertBody('2')
190
193
191194 # Test session delete
192195 self.getPage('/delete', self.cookies)
193196 self.assertBody("done")
194197 self.getPage('/delete', cookieset1)
195198 self.assertBody("done")
196 f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
199 f = lambda: [
200 x for x in os.listdir(localDir) if x.startswith('session-')]
197201 self.assertEqual(f(), [])
198
202
199203 # Wait for the cleanup thread to delete remaining session files
200204 self.getPage('/')
201 f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
205 f = lambda: [
206 x for x in os.listdir(localDir) if x.startswith('session-')]
202207 self.assertNotEqual(f(), [])
203208 time.sleep(2)
204209 self.assertEqual(f(), [])
205
210
206211 def test_1_Ram_Concurrency(self):
207212 self.getPage('/setsessiontype/ram')
208213 self._test_Concurrency()
209
214
210215 def test_2_File_Concurrency(self):
211216 self.getPage('/setsessiontype/file')
212217 self._test_Concurrency()
213
218
214219 def _test_Concurrency(self):
215220 client_thread_count = 5
216221 request_count = 30
217
222
218223 # Get initial cookie
219224 self.getPage("/")
220225 self.assertBody("1")
221226 cookies = self.cookies
222
227
223228 data_dict = {}
224229 errors = []
225
230
226231 def request(index):
227232 if self.scheme == 'https':
228233 c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
241246 data_dict[index] = max(data_dict[index], int(body))
242247 # Uncomment the following line to prove threads overlap.
243248 ## sys.stdout.write("%d " % index)
244
249
245250 # Start <request_count> requests from each of
246251 # <client_thread_count> concurrent clients
247252 ts = []
250255 t = threading.Thread(target=request, args=(c,))
251256 ts.append(t)
252257 t.start()
253
258
254259 for t in ts:
255260 t.join()
256
261
257262 hitcount = max(data_dict.values())
258263 expected = 1 + (client_thread_count * request_count)
259
264
260265 for e in errors:
261266 print(e)
262267 self.assertEqual(hitcount, expected)
263
268
264269 def test_3_Redirect(self):
265270 # Start a new session
266271 self.getPage('/testStr')
267272 self.getPage('/iredir', self.cookies)
268273 self.assertBody("file")
269
274
270275 def test_4_File_deletion(self):
271276 # Start a new session
272277 self.getPage('/testStr')
275280 path = os.path.join(localDir, "session-" + id)
276281 os.unlink(path)
277282 self.getPage('/testStr', self.cookies)
278
283
279284 def test_5_Error_paths(self):
280285 self.getPage('/unknown/page')
281286 self.assertErrorPage(404, "The path '/unknown/page' was not found.")
282
287
283288 # Note: this path is *not* the same as above. The above
284289 # takes a normal route through the session code; this one
285290 # skips the session code's before_handler and only calls
287292 # code has to survive calling save/close without init.
288293 self.getPage('/restricted', self.cookies, method='POST')
289294 self.assertErrorPage(405, response_codes[405][1])
290
295
291296 def test_6_regenerate(self):
292297 self.getPage('/testStr')
293298 # grab the cookie ID
296301 self.assertBody('logged in')
297302 id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
298303 self.assertNotEqual(id1, id2)
299
304
300305 self.getPage('/testStr')
301306 # grab the cookie ID
302307 id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
303308 self.getPage('/testStr',
304 headers=[('Cookie',
305 'session_id=maliciousid; '
306 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
309 headers=[
310 ('Cookie',
311 'session_id=maliciousid; '
312 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
307313 id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
308314 self.assertNotEqual(id1, id2)
309315 self.assertNotEqual(id2, 'maliciousid')
310
316
311317 def test_7_session_cookies(self):
312318 self.getPage('/setsessiontype/ram')
313319 self.getPage('/clear')
314320 self.getPage('/session_cookie')
315321 # grab the cookie ID
316 cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
322 cookie_parts = dict([p.strip().split('=')
323 for p in self.cookies[0][1].split(";")])
317324 # Assert there is no 'expires' param
318325 self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
319326 id1 = cookie_parts['temp']
320327 self.assertEqual(copykeys(sessions.RamSession.cache), [id1])
321
328
322329 # Send another request in the same "browser session".
323330 self.getPage('/session_cookie', self.cookies)
324 cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
331 cookie_parts = dict([p.strip().split('=')
332 for p in self.cookies[0][1].split(";")])
325333 # Assert there is no 'expires' param
326334 self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
327335 self.assertBody(id1)
328336 self.assertEqual(copykeys(sessions.RamSession.cache), [id1])
329
337
330338 # Simulate a browser close by just not sending the cookies
331339 self.getPage('/session_cookie')
332340 # grab the cookie ID
333 cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
341 cookie_parts = dict([p.strip().split('=')
342 for p in self.cookies[0][1].split(";")])
334343 # Assert there is no 'expires' param
335344 self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
336345 # Assert a new id has been generated...
337346 id2 = cookie_parts['temp']
338347 self.assertNotEqual(id1, id2)
339 self.assertEqual(set(sessions.RamSession.cache.keys()), set([id1, id2]))
340
348 self.assertEqual(set(sessions.RamSession.cache.keys()),
349 set([id1, id2]))
350
341351 # Wait for the session.timeout on both sessions
342352 time.sleep(2.5)
343353 cache = copykeys(sessions.RamSession.cache)
351361 import socket
352362 try:
353363 import memcache
354
364
355365 host, port = '127.0.0.1', 11211
356366 for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
357367 socket.SOCK_STREAM):
372382 except (ImportError, socket.error):
373383 class MemcachedSessionTest(helper.CPWebCase):
374384 setup_server = staticmethod(setup_server)
375
385
376386 def test(self):
377387 return self.skip("memcached not reachable ")
378388 else:
379389 class MemcachedSessionTest(helper.CPWebCase):
380390 setup_server = staticmethod(setup_server)
381
391
382392 def test_0_Session(self):
383393 self.getPage('/setsessiontype/memcached')
384
394
385395 self.getPage('/testStr')
386396 self.assertBody('1')
387397 self.getPage('/testGen', self.cookies)
393403 self.assertInBody("NotImplementedError")
394404 self.getPage('/delkey?key=counter', self.cookies)
395405 self.assertStatus(200)
396
406
397407 # Wait for the session.timeout (1 second)
398408 time.sleep(1.25)
399409 self.getPage('/')
400410 self.assertBody('1')
401
411
402412 # Test session __contains__
403413 self.getPage('/keyin?key=counter', self.cookies)
404414 self.assertBody("True")
405
415
406416 # Test session delete
407417 self.getPage('/delete', self.cookies)
408418 self.assertBody("done")
409
419
410420 def test_1_Concurrency(self):
411421 client_thread_count = 5
412422 request_count = 30
413
423
414424 # Get initial cookie
415425 self.getPage("/")
416426 self.assertBody("1")
417427 cookies = self.cookies
418
428
419429 data_dict = {}
420
430
421431 def request(index):
422432 for i in range(request_count):
423433 self.getPage("/", cookies)
426436 if not self.body.isdigit():
427437 self.fail(self.body)
428438 data_dict[index] = v = int(self.body)
429
439
430440 # Start <request_count> concurrent requests from
431441 # each of <client_thread_count> clients
432442 ts = []
435445 t = threading.Thread(target=request, args=(c,))
436446 ts.append(t)
437447 t.start()
438
448
439449 for t in ts:
440450 t.join()
441
451
442452 hitcount = max(data_dict.values())
443453 expected = 1 + (client_thread_count * request_count)
444454 self.assertEqual(hitcount, expected)
445
455
446456 def test_3_Redirect(self):
447457 # Start a new session
448458 self.getPage('/testStr')
449459 self.getPage('/iredir', self.cookies)
450460 self.assertBody("memcached")
451
461
452462 def test_5_Error_paths(self):
453463 self.getPage('/unknown/page')
454 self.assertErrorPage(404, "The path '/unknown/page' was not found.")
455
464 self.assertErrorPage(
465 404, "The path '/unknown/page' was not found.")
466
456467 # Note: this path is *not* the same as above. The above
457468 # takes a normal route through the session code; this one
458469 # skips the session code's before_handler and only calls
460471 # code has to survive calling save/close without init.
461472 self.getPage('/restricted', self.cookies, method='POST')
462473 self.assertErrorPage(405, response_codes[405][1])
463
44 class SessionAuthenticateTest(helper.CPWebCase):
55
66 def setup_server():
7
7
88 def check(username, password):
99 # Dummy check_username_and_password function
1010 if username != 'test' or password != 'password':
1111 return 'Wrong login/password'
12
12
1313 def augment_params():
1414 # A simple tool to add some things to request.params
15 # This is to check to make sure that session_auth can handle request
16 # params (ticket #780)
15 # This is to check to make sure that session_auth can handle
16 # request params (ticket #780)
1717 cherrypy.request.params["test"] = "test"
1818
19 cherrypy.tools.augment_params = cherrypy.Tool('before_handler',
20 augment_params, None, priority=30)
19 cherrypy.tools.augment_params = cherrypy.Tool(
20 'before_handler', augment_params, None, priority=30)
2121
2222 class Test:
23
24 _cp_config = {'tools.sessions.on': True,
25 'tools.session_auth.on': True,
26 'tools.session_auth.check_username_and_password': check,
27 'tools.augment_params.on': True,
28 }
29
23
24 _cp_config = {
25 'tools.sessions.on': True,
26 'tools.session_auth.on': True,
27 'tools.session_auth.check_username_and_password': check,
28 'tools.augment_params.on': True,
29 }
30
3031 def index(self, **kwargs):
3132 return "Hi %s, you are logged in" % cherrypy.request.login
3233 index.exposed = True
33
34
3435 cherrypy.tree.mount(Test())
3536 setup_server = staticmethod(setup_server)
3637
37
3838 def testSessionAuthenticate(self):
3939 # request a page and check for login form
4040 self.getPage('/')
4141 self.assertInBody('<form method="post" action="do_login">')
42
42
4343 # setup credentials
4444 login_body = 'username=test&password=password&from_page=/'
45
45
4646 # attempt a login
4747 self.getPage('/do_login', method='POST', body=login_body)
4848 self.assertStatus((302, 303))
49
49
5050 # get the page now that we are logged in
5151 self.getPage('/', self.cookies)
5252 self.assertBody('Hi test, you are logged in')
53
53
5454 # do a logout
5555 self.getPage('/do_logout', self.cookies, method='POST')
5656 self.assertStatus((302, 303))
57
57
5858 # verify we are logged out
5959 self.getPage('/', self.cookies)
6060 self.assertInBody('<form method="post" action="do_login">')
61
0 import os
1 import signal
2 import socket
3 import sys
4 import time
5 import unittest
6 import warnings
7
8 import cherrypy
9 import cherrypy.process.servers
010 from cherrypy._cpcompat import BadStatusLine, ntob
1 import os
2 import sys
3 import threading
4 import time
5
6 import cherrypy
11 from cherrypy.test import helper
12
713 engine = cherrypy.engine
814 thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
915
4248
4349 db_connection = Dependency(engine)
4450
51
4552 def setup_server():
4653 class Root:
54
4755 def index(self):
4856 return "Hello World"
4957 index.exposed = True
7482 cherrypy.config.update({
7583 'environment': 'test_suite',
7684 'engine.deadlock_poll_freq': 0.1,
77 })
85 })
7886
7987 db_connection.subscribe()
8088
81
82
8389 # ------------ Enough helpers. Time for real live test cases. ------------ #
8490
85
86 from cherrypy.test import helper
8791
8892 class ServerStateTests(helper.CPWebCase):
8993 setup_server = staticmethod(setup_server)
244248 engine.exit()
245249
246250 def test_4_Autoreload(self):
251 # If test_3 has not been executed, the server won't be stopped,
252 # so we'll have to do it.
253 if engine.state != engine.states.EXITING:
254 engine.exit()
255
247256 # Start the demo script in a new process
248 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
249 p.write_conf(
250 extra='test_case_name: "test_4_Autoreload"')
257 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
258 p.write_conf(extra='test_case_name: "test_4_Autoreload"')
251259 p.start(imports='cherrypy.test._test_states_demo')
252260 try:
253261 self.getPage("/start")
275283 p.join()
276284
277285 def test_5_Start_Error(self):
286 # If test_3 has not been executed, the server won't be stopped,
287 # so we'll have to do it.
288 if engine.state != engine.states.EXITING:
289 engine.exit()
290
278291 # If a process errors during start, it should stop the engine
279292 # and exit with a non-zero exit code.
280 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
293 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
281294 wait=True)
282295 p.write_conf(
283 extra="""starterror: True
296 extra="""starterror: True
284297 test_case_name: "test_5_Start_Error"
285298 """
286299 )
290303
291304
292305 class PluginTests(helper.CPWebCase):
306
293307 def test_daemonize(self):
294308 if os.name not in ['posix']:
295309 return self.skip("skipped (not on posix) ")
298312 # Spawn the process and wait, when this returns, the original process
299313 # is finished. If it daemonized properly, we should still be able
300314 # to access pages.
301 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
315 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
302316 wait=True, daemonize=True,
303317 socket_host='127.0.0.1',
304318 socket_port=8081)
305319 p.write_conf(
306 extra='test_case_name: "test_daemonize"')
320 extra='test_case_name: "test_daemonize"')
307321 p.start(imports='cherrypy.test._test_states_demo')
308322 try:
309323 # Just get the pid of the daemonization process.
323337
324338
325339 class SignalHandlingTests(helper.CPWebCase):
340
326341 def test_SIGHUP_tty(self):
327342 # When not daemonized, SIGHUP should shut down the server.
328343 try:
331346 return self.skip("skipped (no SIGHUP) ")
332347
333348 # Spawn the process.
334 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
349 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
335350 p.write_conf(
336 extra='test_case_name: "test_SIGHUP_tty"')
351 extra='test_case_name: "test_SIGHUP_tty"')
337352 p.start(imports='cherrypy.test._test_states_demo')
338353 # Send a SIGHUP
339354 os.kill(p.get_pid(), SIGHUP)
353368 # Spawn the process and wait, when this returns, the original process
354369 # is finished. If it daemonized properly, we should still be able
355370 # to access pages.
356 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
371 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
357372 wait=True, daemonize=True)
358373 p.write_conf(
359 extra='test_case_name: "test_SIGHUP_daemonized"')
374 extra='test_case_name: "test_SIGHUP_daemonized"')
360375 p.start(imports='cherrypy.test._test_states_demo')
361376
362377 pid = p.get_pid()
374389 self.getPage("/exit")
375390 p.join()
376391
392 def _require_signal_and_kill(self, signal_name):
393 if not hasattr(signal, signal_name):
394 self.skip("skipped (no %(signal_name)s)" % vars())
395
396 if not hasattr(os, 'kill'):
397 self.skip("skipped (no os.kill)")
398
377399 def test_SIGTERM(self):
378 # SIGTERM should shut down the server whether daemonized or not.
379 try:
380 from signal import SIGTERM
381 except ImportError:
382 return self.skip("skipped (no SIGTERM) ")
383
384 try:
385 from os import kill
386 except ImportError:
387 return self.skip("skipped (no os.kill) ")
400 "SIGTERM should shut down the server whether daemonized or not."
401 self._require_signal_and_kill('SIGTERM')
388402
389403 # Spawn a normal, undaemonized process.
390 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
404 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
391405 p.write_conf(
392 extra='test_case_name: "test_SIGTERM"')
406 extra='test_case_name: "test_SIGTERM"')
393407 p.start(imports='cherrypy.test._test_states_demo')
394408 # Send a SIGTERM
395 os.kill(p.get_pid(), SIGTERM)
409 os.kill(p.get_pid(), signal.SIGTERM)
396410 # This might hang if things aren't working right, but meh.
397411 p.join()
398412
399413 if os.name in ['posix']:
400414 # Spawn a daemonized process and test again.
401 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
415 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
402416 wait=True, daemonize=True)
403417 p.write_conf(
404 extra='test_case_name: "test_SIGTERM_2"')
418 extra='test_case_name: "test_SIGTERM_2"')
405419 p.start(imports='cherrypy.test._test_states_demo')
406420 # Send a SIGTERM
407 os.kill(p.get_pid(), SIGTERM)
421 os.kill(p.get_pid(), signal.SIGTERM)
408422 # This might hang if things aren't working right, but meh.
409423 p.join()
410424
411425 def test_signal_handler_unsubscribe(self):
412 try:
413 from signal import SIGTERM
414 except ImportError:
415 return self.skip("skipped (no SIGTERM) ")
416
417 try:
418 from os import kill
419 except ImportError:
420 return self.skip("skipped (no os.kill) ")
426 self._require_signal_and_kill('SIGTERM')
427
428 # Although Windows has `os.kill` and SIGTERM is defined, the
429 # platform does not implement signals and sending SIGTERM
430 # will result in a forced termination of the process.
431 # Therefore, this test is not suitable for Windows.
432 if os.name == 'nt':
433 self.skip("SIGTERM not available")
421434
422435 # Spawn a normal, undaemonized process.
423 p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
436 p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
424437 p.write_conf(
425438 extra="""unsubsig: True
426439 test_case_name: "test_signal_handler_unsubscribe"
427440 """)
428441 p.start(imports='cherrypy.test._test_states_demo')
429 # Send a SIGTERM
430 os.kill(p.get_pid(), SIGTERM)
442 # Ask the process to quit
443 os.kill(p.get_pid(), signal.SIGTERM)
431444 # This might hang if things aren't working right, but meh.
432445 p.join()
433446
436449 if not ntob("I am an old SIGTERM handler.") in target_line:
437450 self.fail("Old SIGTERM handler did not run.\n%r" % target_line)
438451
452
453 class WaitTests(unittest.TestCase):
454
455 def test_wait_for_occupied_port_INADDR_ANY(self):
456 """
457 Wait on INADDR_ANY should not raise IOError
458
459 In cases where the loopback interface does not exist, CherryPy cannot
460 effectively determine if a port binding to INADDR_ANY was effected.
461 In this situation, CherryPy should assume that it failed to detect
462 the binding (not that the binding failed) and only warn that it could
463 not verify it.
464 """
465 # At such a time that CherryPy can reliably determine one or more
466 # viable IP addresses of the host, this test may be removed.
467
468 # Simulate the behavior we observe when no loopback interface is
469 # present by: finding a port that's not occupied, then wait on it.
470
471 free_port = self.find_free_port()
472
473 servers = cherrypy.process.servers
474
475 def with_shorter_timeouts(func):
476 """
477 A context where occupied_port_timeout is much smaller to speed
478 test runs.
479 """
480 # When we have Python 2.5, simplify using the with_statement.
481 orig_timeout = servers.occupied_port_timeout
482 servers.occupied_port_timeout = .07
483 try:
484 func()
485 finally:
486 servers.occupied_port_timeout = orig_timeout
487
488 def do_waiting():
489 # Wait on the free port that's unbound
490 with warnings.catch_warnings(record=True) as w:
491 servers.wait_for_occupied_port('0.0.0.0', free_port)
492 self.assertEqual(len(w), 1)
493 self.assertTrue(isinstance(w[0], warnings.WarningMessage))
494 self.assertTrue(
495 'Unable to verify that the server is bound on ' in str(w[0]))
496
497 # The wait should still raise an IO error if INADDR_ANY was
498 # not supplied.
499 self.assertRaises(IOError, servers.wait_for_occupied_port,
500 '127.0.0.1', free_port)
501
502 with_shorter_timeouts(do_waiting)
503
504 def find_free_port(self):
505 "Find a free port by binding to port 0 then unbinding."
506 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
507 sock.bind(('', 0))
508 free_port = sock.getsockname()[1]
509 sock.close()
510 return free_port
0 import os
1 import sys
2
03 from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
14 from cherrypy._cpcompat import BytesIO
25
3 import os
46 curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
57 has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
68 bigfile_filepath = os.path.join(curdir, "static", "bigfile.log")
79 BIGFILE_SIZE = 1024 * 1024
8 import threading
910
1011 import cherrypy
1112 from cherrypy.lib import static
1920 open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n'))
2021 if not os.path.exists(bigfile_filepath):
2122 open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE))
22
23
2324 class Root:
24
25
2526 def bigfile(self):
2627 from cherrypy.lib import static
2728 self.f = static.serve_file(bigfile_filepath)
2829 return self.f
2930 bigfile.exposed = True
3031 bigfile._cp_config = {'response.stream': True}
31
32
3233 def tell(self):
3334 if self.f.input.closed:
3435 return ''
3536 return repr(self.f.input.tell()).rstrip('L')
3637 tell.exposed = True
37
38
3839 def fileobj(self):
3940 f = open(os.path.join(curdir, 'style.css'), 'rb')
4041 return static.serve_fileobj(f, content_type='text/css')
4142 fileobj.exposed = True
42
43
4344 def bytesio(self):
4445 f = BytesIO(ntob('Fee\nfie\nfo\nfum'))
4546 return static.serve_fileobj(f, content_type='text/plain')
4647 bytesio.exposed = True
47
48
4849 class Static:
49
50
5051 def index(self):
5152 return 'You want the Baron? You can have the Baron!'
5253 index.exposed = True
53
54
5455 def dynamic(self):
5556 return "This is a DYNAMIC page"
5657 dynamic.exposed = True
57
58
58
5959 root = Root()
6060 root.static = Static()
61
61
6262 rootconf = {
6363 '/static': {
6464 'tools.staticdir.on': True,
7979 'tools.staticdir.on': True,
8080 'request.show_tracebacks': True,
8181 },
82 '/404test': {
83 'tools.staticdir.on': True,
84 'tools.staticdir.root': curdir,
85 'tools.staticdir.dir': 'static',
86 'error_page.404': error_page_404,
8287 }
88 }
8389 rootApp = cherrypy.Application(root)
8490 rootApp.merge(rootconf)
85
91
8692 test_app_conf = {
8793 '/test': {
8894 'tools.staticdir.index': 'index.html',
8995 'tools.staticdir.on': True,
9096 'tools.staticdir.root': curdir,
9197 'tools.staticdir.dir': 'static',
92 },
93 }
98 },
99 }
94100 testApp = cherrypy.Application(Static())
95101 testApp.merge(test_app_conf)
96
102
97103 vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
98104 cherrypy.tree.graft(vhost)
99105 setup_server = staticmethod(setup_server)
100
101106
102107 def teardown_server():
103108 for f in (has_space_filepath, bigfile_filepath):
108113 pass
109114 teardown_server = staticmethod(teardown_server)
110115
111
112116 def testStatic(self):
113117 self.getPage("/static/index.html")
114118 self.assertStatus('200 OK')
115119 self.assertHeader('Content-Type', 'text/html')
116120 self.assertBody('Hello, world\r\n')
117
121
118122 # Using a staticdir.root value in a subdir...
119123 self.getPage("/docroot/index.html")
120124 self.assertStatus('200 OK')
121125 self.assertHeader('Content-Type', 'text/html')
122126 self.assertBody('Hello, world\r\n')
123
127
124128 # Check a filename with spaces in it
125129 self.getPage("/static/has%20space.html")
126130 self.assertStatus('200 OK')
127131 self.assertHeader('Content-Type', 'text/html')
128132 self.assertBody('Hello, world\r\n')
129
133
130134 self.getPage("/style.css")
131135 self.assertStatus('200 OK')
132136 self.assertHeader('Content-Type', 'text/css')
135139 # into \r\n on Windows when extracting the CherryPy tarball so
136140 # we just check the content
137141 self.assertMatchesBody('^Dummy stylesheet')
138
142
139143 def test_fallthrough(self):
140144 # Test that NotFound will then try dynamic handlers (see [878]).
141145 self.getPage("/static/dynamic")
142146 self.assertBody("This is a DYNAMIC page")
143
147
144148 # Check a directory via fall-through to dynamic handler.
145149 self.getPage("/static/")
146150 self.assertStatus('200 OK')
147151 self.assertHeader('Content-Type', 'text/html;charset=utf-8')
148152 self.assertBody('You want the Baron? You can have the Baron!')
149
153
150154 def test_index(self):
151155 # Check a directory via "staticdir.index".
152156 self.getPage("/docroot/")
157161 self.getPage("/docroot")
158162 self.assertStatus(301)
159163 self.assertHeader('Location', '%s/docroot/' % self.base())
160 self.assertMatchesBody("This resource .* <a href='%s/docroot/'>"
164 self.assertMatchesBody("This resource .* <a href=(['\"])%s/docroot/\\1>"
161165 "%s/docroot/</a>." % (self.base(), self.base()))
162
166
163167 def test_config_errors(self):
164168 # Check that we get an error if no .file or .dir
165169 self.getPage("/error/thing.html")
166170 self.assertErrorPage(500)
167 self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 "
168 "(positional )?arguments \(0 given\)"))
169
171 if sys.version_info >= (3, 3):
172 errmsg = ntob("TypeError: staticdir\(\) missing 2 "
173 "required positional arguments")
174 else:
175 errmsg = ntob("TypeError: staticdir\(\) takes at least 2 "
176 "(positional )?arguments \(0 given\)")
177 self.assertMatchesBody(errmsg)
178
170179 def test_security(self):
171180 # Test up-level security
172181 self.getPage("/static/../../test/style.css")
173182 self.assertStatus((400, 403))
174
183
175184 def test_modif(self):
176185 # Test modified-since on a reasonably-large file
177186 self.getPage("/static/dirback.jpg")
187196 self.assertNoHeader("Content-Length")
188197 self.assertNoHeader("Content-Disposition")
189198 self.assertBody("")
190
199
191200 def test_755_vhost(self):
192201 self.getPage("/test/", [('Host', 'virt.net')])
193202 self.assertStatus(200)
194203 self.getPage("/test", [('Host', 'virt.net')])
195204 self.assertStatus(301)
196205 self.assertHeader('Location', self.scheme + '://virt.net/test/')
197
206
198207 def test_serve_fileobj(self):
199208 self.getPage("/fileobj")
200209 self.assertStatus('200 OK')
201210 self.assertHeader('Content-Type', 'text/css;charset=utf-8')
202211 self.assertMatchesBody('^Dummy stylesheet')
203
212
204213 def test_serve_bytesio(self):
205214 self.getPage("/bytesio")
206215 self.assertStatus('200 OK')
207216 self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
208217 self.assertHeader('Content-Length', 14)
209218 self.assertMatchesBody('Fee\nfie\nfo\nfum')
210
219
211220 def test_file_stream(self):
212221 if cherrypy.server.protocol_version != "HTTP/1.1":
213222 return self.skip()
214
223
215224 self.PROTOCOL = "HTTP/1.1"
216
225
217226 # Make an initial request
218227 self.persistent = True
219228 conn = self.HTTP_CONN
223232 response = conn.response_class(conn.sock, method="GET")
224233 response.begin()
225234 self.assertEqual(response.status, 200)
226
235
227236 body = ntob('')
228237 remaining = BIGFILE_SIZE
229238 while remaining > 0:
232241 break
233242 body += data
234243 remaining -= len(data)
235
244
236245 if self.scheme == "https":
237246 newconn = HTTPSConnection
238247 else:
245254 tell_position = BIGFILE_SIZE
246255 else:
247256 tell_position = int(b)
248
257
249258 expected = len(body)
250259 if tell_position >= BIGFILE_SIZE:
251 # We can't exactly control how much content the server asks for.
260 # We can't exactly control how much content the server asks
261 # for.
252262 # Fudge it by only checking the first half of the reads.
253263 if expected < (BIGFILE_SIZE / 2):
254264 self.fail(
255 "The file should have advanced to position %r, but has "
256 "already advanced to the end of the file. It may not be "
257 "streamed as intended, or at the wrong chunk size (64k)" %
258 expected)
265 "The file should have advanced to position %r, but "
266 "has already advanced to the end of the file. It "
267 "may not be streamed as intended, or at the wrong "
268 "chunk size (64k)" % expected)
259269 elif tell_position < expected:
260270 self.fail(
261271 "The file should have advanced to position %r, but has "
262272 "only advanced to position %r. It may not be streamed "
263273 "as intended, or at the wrong chunk size (65536)" %
264274 (expected, tell_position))
265
275
266276 if body != ntob("x" * BIGFILE_SIZE):
267277 self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
268278 (BIGFILE_SIZE, body[:50], len(body)))
269279 conn.close()
270
280
271281 def test_file_stream_deadlock(self):
272282 if cherrypy.server.protocol_version != "HTTP/1.1":
273283 return self.skip()
274
284
275285 self.PROTOCOL = "HTTP/1.1"
276
286
277287 # Make an initial request but abort early.
278288 self.persistent = True
279289 conn = self.HTTP_CONN
289299 (65536, body[:50], len(body)))
290300 response.close()
291301 conn.close()
292
302
293303 # Make a second request, which should fetch the whole file.
294304 self.persistent = False
295305 self.getPage("/bigfile")
296306 if self.body != ntob("x" * BIGFILE_SIZE):
297307 self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
298308 (BIGFILE_SIZE, self.body[:50], len(body)))
299
309
310 def test_error_page_with_serve_file(self):
311 self.getPage("/404test/yunyeen")
312 self.assertStatus(404)
313 self.assertInBody("I couldn't find that thing")
314
315 def error_page_404(status, message, traceback, version):
316 import os.path
317 return static.serve_file(os.path.join(curdir, 'static', '404.html'),
318 content_type='text/html')
11
22 import gzip
33 import sys
4 import unittest
45 from cherrypy._cpcompat import BytesIO, copyitems, itervalues
56 from cherrypy._cpcompat import IncompleteRead, ntob, ntou, py3k, xrange
7 from cherrypy._cpcompat import bytestr, unicodestr
68 import time
79 timeout = 0.2
810 import types
2022
2123
2224 class ToolTests(helper.CPWebCase):
25
2326 def setup_server():
24
27
2528 # Put check_access in a custom toolbox with its own namespace
2629 myauthtools = cherrypy._cptools.Toolbox("myauth")
27
30
2831 def check_access(default=False):
2932 if not getattr(cherrypy.request, "userid", default):
3033 raise cherrypy.HTTPError(401)
31 myauthtools.check_access = cherrypy.Tool('before_request_body', check_access)
32
34 myauthtools.check_access = cherrypy.Tool(
35 'before_request_body', check_access)
36
3337 def numerify():
3438 def number_it(body):
3539 for chunk in body:
3741 chunk = chunk.replace(k, v)
3842 yield chunk
3943 cherrypy.response.body = number_it(cherrypy.response.body)
40
44
4145 class NumTool(cherrypy.Tool):
46
4247 def _setup(self):
4348 def makemap():
4449 m = self._merged_args().get("map", {})
4550 cherrypy.request.numerify_map = copyitems(m)
4651 cherrypy.request.hooks.attach('on_start_resource', makemap)
47
52
4853 def critical():
49 cherrypy.request.error_response = cherrypy.HTTPError(502).set_response
54 cherrypy.request.error_response = cherrypy.HTTPError(
55 502).set_response
5056 critical.failsafe = True
51
57
5258 cherrypy.request.hooks.attach('on_start_resource', critical)
5359 cherrypy.request.hooks.attach(self._point, self.callable)
54
60
5561 tools.numerify = NumTool('before_finalize', numerify)
56
62
5763 # It's not mandatory to inherit from cherrypy.Tool.
5864 class NadsatTool:
59
65
6066 def __init__(self):
6167 self.ended = {}
6268 self._name = "nadsat"
63
69
6470 def nadsat(self):
6571 def nadsat_it_up(body):
6672 for chunk in body:
6975 yield chunk
7076 cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
7177 nadsat.priority = 0
72
78
7379 def cleanup(self):
7480 # This runs after the request has been completely written out.
7581 cherrypy.response.body = [ntob("razdrez")]
7783 if id:
7884 self.ended[id] = True
7985 cleanup.failsafe = True
80
86
8187 def _setup(self):
8288 cherrypy.request.hooks.attach('before_finalize', self.nadsat)
8389 cherrypy.request.hooks.attach('on_end_request', self.cleanup)
8490 tools.nadsat = NadsatTool()
85
91
8692 def pipe_body():
8793 cherrypy.request.process_request_body = False
8894 clen = int(cherrypy.request.headers['Content-Length'])
8995 cherrypy.request.body = cherrypy.request.rfile.read(clen)
90
96
9197 # Assert that we can use a callable object instead of a function.
9298 class Rotator(object):
99
93100 def __call__(self, scale):
94101 r = cherrypy.response
95102 r.collapse_body()
98105 else:
99106 r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
100107 cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
101
108
102109 def stream_handler(next_handler, *args, **kwargs):
103110 cherrypy.response.output = o = BytesIO()
104111 try:
105112 response = next_handler(*args, **kwargs)
106 # Ignore the response and return our accumulated output instead.
113 # Ignore the response and return our accumulated output
114 # instead.
107115 return o.getvalue()
108116 finally:
109117 o.close()
110 cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(stream_handler)
111
118 cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
119 stream_handler)
120
112121 class Root:
122
113123 def index(self):
114124 return "Howdy earth!"
115125 index.exposed = True
116
126
117127 def tarfile(self):
118128 cherrypy.response.output.write(ntob('I am '))
119129 cherrypy.response.output.write(ntob('a tarfile'))
120130 tarfile.exposed = True
121131 tarfile._cp_config = {'tools.streamer.on': True}
122
132
123133 def euro(self):
124134 hooks = list(cherrypy.request.hooks['before_finalize'])
125135 hooks.sort()
131141 yield ntou("world")
132142 yield europoundUnicode
133143 euro.exposed = True
134
144
135145 # Bare hooks
136146 def pipe(self):
137147 return cherrypy.request.body
138148 pipe.exposed = True
139149 pipe._cp_config = {'hooks.before_request_body': pipe_body}
140
150
141151 # Multiple decorators; include kwargs just for fun.
142152 # Note that rotator must run before gzip.
143153 def decorated_euro(self, *vpath):
147157 decorated_euro.exposed = True
148158 decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
149159 decorated_euro = tools.rotator(scale=3)(decorated_euro)
150
160
151161 root = Root()
152
153
162
154163 class TestType(type):
155 """Metaclass which automatically exposes all functions in each subclass,
156 and adds an instance of the subclass as an attribute of root.
164 """Metaclass which automatically exposes all functions in each
165 subclass, and adds an instance of the subclass as an attribute
166 of root.
157167 """
158168 def __init__(cls, name, bases, dct):
159169 type.__init__(cls, name, bases, dct)
162172 value.exposed = True
163173 setattr(root, name.lower(), cls())
164174 Test = TestType('Test', (object,), {})
165
166
175
167176 # METHOD ONE:
168177 # Declare Tools in _cp_config
169178 class Demo(Test):
170
179
171180 _cp_config = {"tools.nadsat.on": True}
172
181
173182 def index(self, id=None):
174183 return "A good piece of cherry pie"
175
184
176185 def ended(self, id):
177186 return repr(tools.nadsat.ended[id])
178
187
179188 def err(self, id=None):
180189 raise ValueError()
181
190
182191 def errinstream(self, id=None):
183192 yield "nonconfidential"
184193 raise ValueError()
185194 yield "confidential"
186
195
187196 # METHOD TWO: decorator using Tool()
188 # We support Python 2.3, but the @-deco syntax would look like this:
197 # We support Python 2.3, but the @-deco syntax would look like
198 # this:
189199 # @tools.check_access()
190200 def restricted(self):
191201 return "Welcome!"
192202 restricted = myauthtools.check_access()(restricted)
193203 userid = restricted
194
204
195205 def err_in_onstart(self):
196206 return "success!"
197
207
198208 def stream(self, id=None):
199209 for x in xrange(100000000):
200210 yield str(x)
201211 stream._cp_config = {'response.stream': True}
202
203
212
204213 conf = {
205214 # METHOD THREE:
206215 # Declare Tools in detached config
236245 }
237246 app = cherrypy.tree.mount(root, config=conf)
238247 app.request_class.namespaces['myauth'] = myauthtools
239
248
240249 if sys.version_info >= (2, 5):
241250 from cherrypy.test import _test_decorators
242251 root.tooldecs = _test_decorators.ToolExamples()
250259 time.sleep(0.1)
251260 self.getPage("/demo/ended/1")
252261 self.assertBody("True")
253
262
254263 valerr = '\n raise ValueError()\nValueError'
255264 self.getPage("/demo/err?id=3")
256265 # If body is "razdrez", then on_end_request is being called too early.
259268 time.sleep(0.1)
260269 self.getPage("/demo/ended/3")
261270 self.assertBody("True")
262
271
263272 # If body is "razdrez", then on_end_request is being called too early.
264273 if (cherrypy.server.protocol_version == "HTTP/1.0" or
265 getattr(cherrypy.server, "using_apache", False)):
274 getattr(cherrypy.server, "using_apache", False)):
266275 self.getPage("/demo/errinstream?id=5")
267276 # Because this error is raised after the response body has
268277 # started, the status should not change to an error status.
278287 time.sleep(0.1)
279288 self.getPage("/demo/ended/5")
280289 self.assertBody("True")
281
290
282291 # Test the "__call__" technique (compile-time decorator).
283292 self.getPage("/demo/restricted")
284293 self.assertErrorPage(401)
285
294
286295 # Test compile-time decorator with kwargs from config.
287296 self.getPage("/demo/userid")
288297 self.assertBody("Welcome!")
289
298
290299 def testEndRequestOnDrop(self):
291300 old_timeout = None
292301 try:
294303 old_timeout = httpserver.timeout
295304 except (AttributeError, IndexError):
296305 return self.skip()
297
306
298307 try:
299308 httpserver.timeout = timeout
300
309
301310 # Test that on_end_request is called even if the client drops.
302311 self.persistent = True
303312 try:
319328 finally:
320329 if old_timeout is not None:
321330 httpserver.timeout = old_timeout
322
331
323332 def testGuaranteedHooks(self):
324333 # The 'critical' on_start_resource hook is 'failsafe' (guaranteed
325334 # to run even if there are failures in other on_start methods).
328337 # but our 'critical' hook should run and set the error to 502.
329338 self.getPage("/demo/err_in_onstart")
330339 self.assertErrorPage(502)
331 self.assertInBody("AttributeError: 'str' object has no attribute 'items'")
332
340 self.assertInBody(
341 "AttributeError: 'str' object has no attribute 'items'")
342
333343 def testCombinedTools(self):
334 expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8')
344 expectedResult = (ntou("Hello,world") +
345 europoundUnicode).encode('utf-8')
335346 zbuf = BytesIO()
336347 zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
337348 zfile.write(expectedResult)
338349 zfile.close()
339
340 self.getPage("/euro", headers=[("Accept-Encoding", "gzip"),
341 ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")])
350
351 self.getPage("/euro",
352 headers=[
353 ("Accept-Encoding", "gzip"),
354 ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")])
342355 self.assertInBody(zbuf.getvalue()[:3])
343
356
344357 zbuf = BytesIO()
345358 zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
346359 zfile.write(expectedResult)
347360 zfile.close()
348
361
349362 self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")])
350363 self.assertInBody(zbuf.getvalue()[:3])
351
364
352365 # This returns a different value because gzip's priority was
353366 # lowered in conf, allowing the rotator to run after gzip.
354367 # Of course, we don't want breakage in production apps,
358371 if py3k:
359372 self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
360373 else:
361 self.assertInBody(''.join([chr((ord(x) + 3) % 256) for x in zbuf.getvalue()]))
362
374 self.assertInBody(''.join([chr((ord(x) + 3) % 256)
375 for x in zbuf.getvalue()]))
376
363377 def testBareHooks(self):
364378 content = "bit of a pain in me gulliver"
365379 self.getPage("/pipe",
367381 ("Content-Type", "text/plain")],
368382 method="POST", body=content)
369383 self.assertBody(content)
370
384
371385 def testHandlerWrapperTool(self):
372386 self.getPage("/tarfile")
373387 self.assertBody("I am a tarfile")
374
388
375389 def testToolWithConfig(self):
376390 if not sys.version_info >= (2, 5):
377391 return self.skip("skipped (Python 2.5+ only)")
378
392
379393 self.getPage('/tooldecs/blah')
380394 self.assertHeader('Content-Type', 'application/data')
381
395
382396 def testWarnToolOn(self):
383397 # get
384398 try:
385 numon = cherrypy.tools.numerify.on
399 cherrypy.tools.numerify.on
386400 except AttributeError:
387401 pass
388402 else:
389403 raise AssertionError("Tool.on did not error as it should have.")
390
404
391405 # set
392406 try:
393407 cherrypy.tools.numerify.on = True
396410 else:
397411 raise AssertionError("Tool.on did not error as it should have.")
398412
413
414 class SessionAuthTest(unittest.TestCase):
415
416 def test_login_screen_returns_bytes(self):
417 """
418 login_screen must return bytes even if unicode parameters are passed.
419 Issue 1132 revealed that login_screen would return unicode if the
420 username and password were unicode.
421 """
422 sa = cherrypy.lib.cptools.SessionAuth()
423 res = sa.login_screen(None, username=unicodestr('nobody'),
424 password=unicodestr('anypass'))
425 self.assertTrue(isinstance(res, bytestr))
66 class TutorialTest(helper.CPWebCase):
77
88 def setup_server(cls):
9
9
1010 conf = cherrypy.config.copy()
11
11
1212 def load_tut_module(name):
1313 """Import or reload tutorial module as needed."""
1414 cherrypy.config.reset()
1515 cherrypy.config.update(conf)
16
16
1717 target = "cherrypy.tutorial." + name
1818 if target in sys.modules:
1919 module = reload(sys.modules[target])
2121 module = __import__(target, globals(), locals(), [''])
2222 # The above import will probably mount a new app at "".
2323 app = cherrypy.tree.apps[""]
24
24
2525 app.root.load_tut_module = load_tut_module
2626 app.root.sessions = sessions
2727 app.root.traceback_setting = traceback_setting
28
28
2929 cls.supervisor.sync_apps()
3030 load_tut_module.exposed = True
31
31
3232 def sessions():
3333 cherrypy.config.update({"tools.sessions.on": True})
3434 sessions.exposed = True
35
35
3636 def traceback_setting():
3737 return repr(cherrypy.request.show_tracebacks)
3838 traceback_setting.exposed = True
39
39
4040 class Dummy:
4141 pass
4242 root = Dummy()
4444 cherrypy.tree.mount(root)
4545 setup_server = classmethod(setup_server)
4646
47
4847 def test01HelloWorld(self):
4948 self.getPage("/load_tut_module/tut01_helloworld")
5049 self.getPage("/")
5150 self.assertBody('Hello world!')
52
51
5352 def test02ExposeMethods(self):
5453 self.getPage("/load_tut_module/tut02_expose_methods")
55 self.getPage("/showMessage")
54 self.getPage("/show_msg")
5655 self.assertBody('Hello world!')
57
56
5857 def test03GetAndPost(self):
5958 self.getPage("/load_tut_module/tut03_get_and_post")
60
59
6160 # Try different GET queries
6261 self.getPage("/greetUser?name=Bob")
6362 self.assertBody("Hey Bob, what's up?")
64
63
6564 self.getPage("/greetUser")
6665 self.assertBody('Please enter your name <a href="./">here</a>.')
67
66
6867 self.getPage("/greetUser?name=")
6968 self.assertBody('No, really, enter your name <a href="./">here</a>.')
70
69
7170 # Try the same with POST
7271 self.getPage("/greetUser", method="POST", body="name=Bob")
7372 self.assertBody("Hey Bob, what's up?")
74
73
7574 self.getPage("/greetUser", method="POST", body="name=")
7675 self.assertBody('No, really, enter your name <a href="./">here</a>.')
77
76
7877 def test04ComplexSite(self):
7978 self.getPage("/load_tut_module/tut04_complex_site")
8079 msg = '''
8180 <p>Here are some extra useful links:</p>
82
81
8382 <ul>
8483 <li><a href="http://del.icio.us">del.icio.us</a></li>
85 <li><a href="http://www.mornography.de">Hendrik's weblog</a></li>
84 <li><a href="http://www.cherrypy.org">CherryPy</a></li>
8685 </ul>
87
86
8887 <p>[<a href="../">Return to links page</a>]</p>'''
8988 self.getPage("/links/extra/")
9089 self.assertBody(msg)
91
90
9291 def test05DerivedObjects(self):
9392 self.getPage("/load_tut_module/tut05_derived_objects")
9493 msg = '''
9897 <head>
9998 <body>
10099 <h2>Another Page</h2>
101
100
102101 <p>
103102 And this is the amazing second page!
104103 </p>
105
104
106105 </body>
107106 </html>
108107 '''
108 # the tutorial has some annoying spaces in otherwise blank lines
109 msg = msg.replace('</h2>\n\n', '</h2>\n \n')
110 msg = msg.replace('</p>\n\n', '</p>\n \n')
109111 self.getPage("/another/")
110112 self.assertBody(msg)
111
113
112114 def test06DefaultMethod(self):
113115 self.getPage("/load_tut_module/tut06_default_method")
114116 self.getPage('/hendrik')
115117 self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
116118 '(<a href="./">back</a>)')
117
119
118120 def test07Sessions(self):
119121 self.getPage("/load_tut_module/tut07_sessions")
120122 self.getPage("/sessions")
121
123
122124 self.getPage('/')
123 self.assertBody("\n During your current session, you've viewed this"
124 "\n page 1 times! Your life is a patio of fun!"
125 "\n ")
126
125 self.assertBody(
126 "\n During your current session, you've viewed this"
127 "\n page 1 times! Your life is a patio of fun!"
128 "\n ")
129
127130 self.getPage('/', self.cookies)
128 self.assertBody("\n During your current session, you've viewed this"
129 "\n page 2 times! Your life is a patio of fun!"
130 "\n ")
131
131 self.assertBody(
132 "\n During your current session, you've viewed this"
133 "\n page 2 times! Your life is a patio of fun!"
134 "\n ")
135
132136 def test08GeneratorsAndYield(self):
133137 self.getPage("/load_tut_module/tut08_generators_and_yield")
134138 self.getPage('/')
135139 self.assertBody('<html><body><h2>Generators rule!</h2>'
136 '<h3>List of users:</h3>'
137 'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
138 '</body></html>')
139
140 '<h3>List of users:</h3>'
141 'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
142 '</body></html>')
143
140144 def test09Files(self):
141145 self.getPage("/load_tut_module/tut09_files")
142
146
143147 # Test upload
144148 filesize = 5
145149 h = [("Content-type", "multipart/form-data; boundary=x"),
146150 ("Content-Length", str(105 + filesize))]
147 b = '--x\n' + \
148 'Content-Disposition: form-data; name="myFile"; filename="hello.txt"\r\n' + \
149 'Content-Type: text/plain\r\n' + \
150 '\r\n' + \
151 'a' * filesize + '\n' + \
152 '--x--\n'
151 b = ('--x\n'
152 'Content-Disposition: form-data; name="myFile"; '
153 'filename="hello.txt"\r\n'
154 'Content-Type: text/plain\r\n'
155 '\r\n')
156 b += 'a' * filesize + '\n' + '--x--\n'
153157 self.getPage('/upload', h, "POST", b)
154158 self.assertBody('''<html>
155159 <body>
158162 myFile mime-type: text/plain
159163 </body>
160164 </html>''' % filesize)
161
165
162166 # Test download
163167 self.getPage('/download')
164168 self.assertStatus("200 OK")
167171 # Make sure the filename is quoted.
168172 'attachment; filename="pdf_file.pdf"')
169173 self.assertEqual(len(self.body), 85698)
170
174
171175 def test10HTTPErrors(self):
172176 self.getPage("/load_tut_module/tut10_http_errors")
173
177
174178 self.getPage("/")
175179 self.assertInBody("""<a href="toggleTracebacks">""")
176180 self.assertInBody("""<a href="/doesNotExist">""")
177181 self.assertInBody("""<a href="/error?code=403">""")
178182 self.assertInBody("""<a href="/error?code=500">""")
179183 self.assertInBody("""<a href="/messageArg">""")
180
184
181185 self.getPage("/traceback_setting")
182186 setting = self.body
183187 self.getPage("/toggleTracebacks")
184188 self.assertStatus((302, 303))
185189 self.getPage("/traceback_setting")
186190 self.assertBody(str(not eval(setting)))
187
191
188192 self.getPage("/error?code=500")
189193 self.assertStatus(500)
190194 self.assertInBody("The server encountered an unexpected condition "
191195 "which prevented it from fulfilling the request.")
192
196
193197 self.getPage("/error?code=403")
194198 self.assertStatus(403)
195199 self.assertInBody("<h2>You can't do that!</h2>")
196
200
197201 self.getPage("/messageArg")
198202 self.assertStatus(500)
199203 self.assertInBody("If you construct an HTTPError with a 'message'")
200
88
99 def setup_server():
1010 class Root:
11
1112 def index(self):
1213 return "Hello, world"
1314 index.exposed = True
14
15
1516 def dom4(self):
1617 return "Under construction"
1718 dom4.exposed = True
18
19
1920 def method(self, value):
2021 return "You sent %s" % value
2122 method.exposed = True
22
23
2324 class VHost:
25
2426 def __init__(self, sitename):
2527 self.sitename = sitename
26
28
2729 def index(self):
2830 return "Welcome to %s" % self.sitename
2931 index.exposed = True
30
32
3133 def vmethod(self, value):
3234 return "You sent %s" % value
3335 vmethod.exposed = True
34
36
3537 def url(self):
3638 return cherrypy.url("nextpage")
3739 url.exposed = True
38
40
3941 # Test static as a handler (section must NOT include vhost prefix)
40 static = cherrypy.tools.staticdir.handler(section='/static', dir=curdir)
41
42 static = cherrypy.tools.staticdir.handler(
43 section='/static', dir=curdir)
44
4245 root = Root()
4346 root.mydom2 = VHost("Domain 2")
4447 root.mydom3 = VHost("Domain 3")
4750 'www.mydom4.com': '/dom4',
4851 }
4952 cherrypy.tree.mount(root, config={
50 '/': {'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)},
53 '/': {
54 'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)
55 },
5156 # Test static in config (section must include vhost prefix)
52 '/mydom2/static2': {'tools.staticdir.on': True,
53 'tools.staticdir.root': curdir,
54 'tools.staticdir.dir': 'static',
55 'tools.staticdir.index': 'index.html',
56 },
57 })
57 '/mydom2/static2': {
58 'tools.staticdir.on': True,
59 'tools.staticdir.root': curdir,
60 'tools.staticdir.dir': 'static',
61 'tools.staticdir.index': 'index.html',
62 },
63 })
5864 setup_server = staticmethod(setup_server)
59
65
6066 def testVirtualHost(self):
6167 self.getPage("/", [('Host', 'www.mydom1.com')])
6268 self.assertBody('Hello, world')
6369 self.getPage("/mydom2/", [('Host', 'www.mydom1.com')])
6470 self.assertBody('Welcome to Domain 2')
65
71
6672 self.getPage("/", [('Host', 'www.mydom2.com')])
6773 self.assertBody('Welcome to Domain 2')
6874 self.getPage("/", [('Host', 'www.mydom3.com')])
6975 self.assertBody('Welcome to Domain 3')
7076 self.getPage("/", [('Host', 'www.mydom4.com')])
7177 self.assertBody('Under construction')
72
78
7379 # Test GET, POST, and positional params
7480 self.getPage("/method?value=root")
7581 self.assertBody("You sent root")
8086 self.assertBody("You sent dom3 POST")
8187 self.getPage("/vmethod/pos", [('Host', 'www.mydom3.com')])
8288 self.assertBody("You sent pos")
83
89
8490 # Test that cherrypy.url uses the browser url, not the virtual url
8591 self.getPage("/url", [('Host', 'www.mydom2.com')])
8692 self.assertBody("%s://www.mydom2.com/nextpage" % self.scheme)
87
93
8894 def test_VHost_plus_Static(self):
8995 # Test static as a handler
9096 self.getPage("/static/style.css", [('Host', 'www.mydom2.com')])
9197 self.assertStatus('200 OK')
9298 self.assertHeader('Content-Type', 'text/css;charset=utf-8')
93
99
94100 # Test static in config
95101 self.getPage("/static2/dirback.jpg", [('Host', 'www.mydom2.com')])
96102 self.assertStatus('200 OK')
97 self.assertHeader('Content-Type', 'image/jpeg')
98
103 self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg'])
104
99105 # Test static config with "index" arg
100106 self.getPage("/static2/", [('Host', 'www.mydom2.com')])
101107 self.assertStatus('200 OK')
103109 # Since tools.trailing_slash is on by default, this should redirect
104110 self.getPage("/static2", [('Host', 'www.mydom2.com')])
105111 self.assertStatus(301)
106
55 class WSGI_Namespace_Test(helper.CPWebCase):
66
77 def setup_server():
8
8
99 class WSGIResponse(object):
10
10
1111 def __init__(self, appresults):
1212 self.appresults = appresults
1313 self.iter = iter(appresults)
14
14
1515 def __iter__(self):
1616 return self
17
17
1818 def next(self):
1919 return self.iter.next()
20
2021 def __next__(self):
2122 return next(self.iter)
22
23
2324 def close(self):
2425 if hasattr(self.appresults, "close"):
2526 self.appresults.close()
26
27
27
2828 class ChangeCase(object):
29
29
3030 def __init__(self, app, to=None):
3131 self.app = app
3232 self.to = to
33
33
3434 def __call__(self, environ, start_response):
3535 res = self.app(environ, start_response)
36
3637 class CaseResults(WSGIResponse):
38
3739 def next(this):
3840 return getattr(this.iter.next(), self.to)()
41
3942 def __next__(this):
4043 return getattr(next(this.iter), self.to)()
4144 return CaseResults(res)
42
45
4346 class Replacer(object):
44
47
4548 def __init__(self, app, map={}):
4649 self.app = app
4750 self.map = map
48
51
4952 def __call__(self, environ, start_response):
5053 res = self.app(environ, start_response)
54
5155 class ReplaceResults(WSGIResponse):
56
5257 def next(this):
5358 line = this.iter.next()
5459 for k, v in self.map.iteritems():
5560 line = line.replace(k, v)
5661 return line
62
5763 def __next__(this):
5864 line = next(this.iter)
5965 for k, v in self.map.items():
6066 line = line.replace(k, v)
6167 return line
6268 return ReplaceResults(res)
63
69
6470 class Root(object):
65
71
6672 def index(self):
6773 return "HellO WoRlD!"
6874 index.exposed = True
69
70
75
7176 root_conf = {'wsgi.pipeline': [('replace', Replacer)],
7277 'wsgi.replace.map': {ntob('L'): ntob('X'),
7378 ntob('l'): ntob('r')},
7479 }
75
80
7681 app = cherrypy.Application(Root())
7782 app.wsgiapp.pipeline.append(('changecase', ChangeCase))
7883 app.wsgiapp.config['changecase'] = {'to': 'upper'}
7984 cherrypy.tree.mount(app, config={'/': root_conf})
8085 setup_server = staticmethod(setup_server)
8186
82
8387 def test_pipeline(self):
8488 if not cherrypy.server.httpserver:
8589 return self.skip()
86
90
8791 self.getPage("/")
8892 # If body is "HEXXO WORXD!", the middleware was applied out of order.
8993 self.assertBody("HERRO WORRD!")
90
44 class WSGI_VirtualHost_Test(helper.CPWebCase):
55
66 def setup_server():
7
7
88 class ClassOfRoot(object):
9
9
1010 def __init__(self, name):
1111 self.name = name
12
12
1313 def index(self):
1414 return "Welcome to the %s website!" % self.name
1515 index.exposed = True
16
17
16
1817 default = cherrypy.Application(None)
19
18
2019 domains = {}
2120 for year in range(1997, 2008):
2221 app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
2322 domains['www.classof%s.example' % year] = app
24
23
2524 cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
2625 setup_server = staticmethod(setup_server)
27
26
2827 def test_welcome(self):
2928 if not cherrypy.server.using_wsgi:
3029 return self.skip("skipped (not using WSGI)... ")
31
30
3231 for year in range(1997, 2008):
33 self.getPage("/", headers=[('Host', 'www.classof%s.example' % year)])
32 self.getPage(
33 "/", headers=[('Host', 'www.classof%s.example' % year)])
3434 self.assertBody("Welcome to the Class of %s website!" % year)
35
0 import sys
1
02 from cherrypy._cpcompat import ntob
13 from cherrypy.test import helper
24
68 def setup_server():
79 import os
810 curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
9
11
1012 import cherrypy
11
13
1214 def test_app(environ, start_response):
1315 status = '200 OK'
1416 response_headers = [('Content-type', 'text/plain')]
1820 keys = list(environ.keys())
1921 keys.sort()
2022 for k in keys:
21 output.append('%s: %s\n' % (k,environ[k]))
23 output.append('%s: %s\n' % (k, environ[k]))
2224 return [ntob(x, 'utf-8') for x in output]
23
25
2426 def test_empty_string_app(environ, start_response):
2527 status = '200 OK'
2628 response_headers = [('Content-type', 'text/plain')]
2729 start_response(status, response_headers)
28 return [ntob('Hello'), ntob(''), ntob(' '), ntob(''), ntob('world')]
29
30
30 return [
31 ntob('Hello'), ntob(''), ntob(' '), ntob(''), ntob('world')
32 ]
33
3134 class WSGIResponse(object):
32
35
3336 def __init__(self, appresults):
3437 self.appresults = appresults
3538 self.iter = iter(appresults)
36
39
3740 def __iter__(self):
3841 return self
39
40 def next(self):
41 return self.iter.next()
42 def __next__(self):
43 return next(self.iter)
44
42
43 if sys.version_info >= (3, 0):
44 def __next__(self):
45 return next(self.iter)
46 else:
47 def next(self):
48 return self.iter.next()
49
4550 def close(self):
4651 if hasattr(self.appresults, "close"):
4752 self.appresults.close()
48
49
53
5054 class ReversingMiddleware(object):
51
55
5256 def __init__(self, app):
5357 self.app = app
54
58
5559 def __call__(self, environ, start_response):
5660 results = app(environ, start_response)
61
5762 class Reverser(WSGIResponse):
58 def next(this):
59 line = list(this.iter.next())
60 line.reverse()
61 return "".join(line)
62 def __next__(this):
63 line = list(next(this.iter))
64 line.reverse()
65 return bytes(line)
63
64 if sys.version_info >= (3, 0):
65 def __next__(this):
66 line = list(next(this.iter))
67 line.reverse()
68 return bytes(line)
69 else:
70 def next(this):
71 line = list(this.iter.next())
72 line.reverse()
73 return "".join(line)
74
6675 return Reverser(results)
67
76
6877 class Root:
78
6979 def index(self):
7080 return ntob("I'm a regular CherryPy page handler!")
7181 index.exposed = True
72
73
82
7483 cherrypy.tree.mount(Root())
75
84
7685 cherrypy.tree.graft(test_app, '/hosted/app1')
7786 cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
78
87
7988 # Set script_name explicitly to None to signal CP that it should
8089 # be pulled from the WSGI environ each time.
8190 app = cherrypy.Application(Root(), script_name=None)
8897 def test_01_standard_app(self):
8998 self.getPage("/")
9099 self.assertBody("I'm a regular CherryPy page handler!")
91
100
92101 def test_04_pure_wsgi(self):
93102 import cherrypy
94103 if not cherrypy.server.using_wsgi:
114123 self.getPage("/hosted/app3")
115124 self.assertHeader("Content-Type", "text/plain")
116125 self.assertInBody('Hello world')
117
11 from cherrypy._cpcompat import py3k
22
33 try:
4 from xmlrpclib import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport
4 from xmlrpclib import DateTime, Fault, ProtocolError, ServerProxy
5 from xmlrpclib import SafeTransport
56 except ImportError:
6 from xmlrpc.client import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport
7 from xmlrpc.client import DateTime, Fault, ProtocolError, ServerProxy
8 from xmlrpc.client import SafeTransport
79
810 if py3k:
911 HTTPSTransport = SafeTransport
1416 socket.ssl = True
1517 else:
1618 class HTTPSTransport(SafeTransport):
17 """Subclass of SafeTransport to fix sock.recv errors (by using file)."""
18
19
20 """Subclass of SafeTransport to fix sock.recv errors (by using file).
21 """
22
1923 def request(self, host, handler, request_body, verbose=0):
2024 # issue XML-RPC request
2125 h = self.make_connection(host)
2226 if verbose:
2327 h.set_debuglevel(1)
24
28
2529 self.send_request(h, handler, request_body)
2630 self.send_host(h, host)
2731 self.send_user_agent(h)
2832 self.send_content(h, request_body)
29
33
3034 errcode, errmsg, headers = h.getreply()
3135 if errcode != 200:
3236 raise ProtocolError(host + handler, errcode, errmsg, headers)
33
37
3438 self.verbose = verbose
35
39
3640 # Here's where we differ from the superclass. It says:
3741 # try:
3842 # sock = h._conn.sock
3943 # except AttributeError:
4044 # sock = None
4145 # return self._parse_response(h.getfile(), sock)
42
46
4347 return self.parse_response(h.getfile())
4448
4549 import cherrypy
4751
4852 def setup_server():
4953 from cherrypy import _cptools
50
54
5155 class Root:
56
5257 def index(self):
5358 return "I'm a standard index!"
5459 index.exposed = True
5560
61 class XmlRpc(_cptools.XMLRPCController):
5662
57 class XmlRpc(_cptools.XMLRPCController):
58
5963 def foo(self):
6064 return "Hello world!"
6165 foo.exposed = True
62
66
6367 def return_single_item_list(self):
6468 return [42]
6569 return_single_item_list.exposed = True
66
70
6771 def return_string(self):
6872 return "here is a string"
6973 return_string.exposed = True
70
74
7175 def return_tuple(self):
7276 return ('here', 'is', 1, 'tuple')
7377 return_tuple.exposed = True
74
78
7579 def return_dict(self):
7680 return dict(a=1, b=2, c=3)
7781 return_dict.exposed = True
78
82
7983 def return_composite(self):
80 return dict(a=1,z=26), 'hi', ['welcome', 'friend']
84 return dict(a=1, z=26), 'hi', ['welcome', 'friend']
8185 return_composite.exposed = True
8286
8387 def return_int(self):
109113 cherrypy.tree.mount(root, config={'/': {
110114 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
111115 'tools.xmlrpc.allow_none': 0,
112 }})
116 }})
113117
114118
115119 from cherrypy.test import helper
116120
121
117122 class XmlRpcTest(helper.CPWebCase):
118123 setup_server = staticmethod(setup_server)
124
119125 def testXmlRpc(self):
120
126
121127 scheme = self.scheme
122128 if scheme == "https":
123129 url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
125131 else:
126132 url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
127133 proxy = ServerProxy(url)
128
134
129135 # begin the tests ...
130136 self.getPage("/xmlrpc/foo")
131137 self.assertBody("Hello world!")
132
138
133139 self.assertEqual(proxy.return_single_item_list(), [42])
134140 self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
135141 self.assertEqual(proxy.return_string(), "here is a string")
136 self.assertEqual(proxy.return_tuple(), list(('here', 'is', 1, 'tuple')))
142 self.assertEqual(proxy.return_tuple(),
143 list(('here', 'is', 1, 'tuple')))
137144 self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
138145 self.assertEqual(proxy.return_composite(),
139146 [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
143150 DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
144151 self.assertEqual(proxy.return_boolean(), True)
145152 self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
146
153
147154 # Test an error in the page handler (should raise an xmlrpclib.Fault)
148155 try:
149156 proxy.test_argument_passing({})
154161 "for *: 'dict' and 'int'"))
155162 else:
156163 self.fail("Expected xmlrpclib.Fault")
157
158 # http://www.cherrypy.org/ticket/533
164
165 # https://bitbucket.org/cherrypy/cherrypy/issue/533
159166 # if a method is not found, an xmlrpclib.Fault should be raised
160167 try:
161168 proxy.non_method()
162169 except Exception:
163170 x = sys.exc_info()[1]
164171 self.assertEqual(x.__class__, Fault)
165 self.assertEqual(x.faultString, 'method "non_method" is not supported')
172 self.assertEqual(x.faultString,
173 'method "non_method" is not supported')
166174 else:
167175 self.fail("Expected xmlrpclib.Fault")
168
176
169177 # Test returning a Fault from the page handler.
170178 try:
171179 proxy.test_returning_Fault()
175183 self.assertEqual(x.faultString, ("custom Fault response"))
176184 else:
177185 self.fail("Expected xmlrpclib.Fault")
178
1515 be of further significance to your tests).
1616 """
1717
18 import os
1918 import pprint
2019 import re
2120 import socket
2726 from unittest import *
2827 from unittest import _TextTestResult
2928
30 from cherrypy._cpcompat import basestring, ntob, py3k, HTTPConnection, HTTPSConnection, unicodestr
31
29 from cherrypy._cpcompat import basestring, ntob, py3k, HTTPConnection
30 from cherrypy._cpcompat import HTTPSConnection, unicodestr
3231
3332
3433 def interface(host):
5756
5857
5958 class TerseTestRunner(TextTestRunner):
59
6060 """A test runner class that displays results in textual form."""
6161
6262 def _makeResult(self):
7474 if failed:
7575 self.stream.write("failures=%d" % failed)
7676 if errored:
77 if failed: self.stream.write(", ")
77 if failed:
78 self.stream.write(", ")
7879 self.stream.write("errors=%d" % errored)
7980 self.stream.writeln(")")
8081 return result
110111 parts = unused_parts
111112 break
112113 except ImportError:
113 unused_parts.insert(0,parts_copy[-1])
114 unused_parts.insert(0, parts_copy[-1])
114115 del parts_copy[-1]
115116 if not parts_copy:
116117 raise
119120 for part in parts:
120121 obj = getattr(obj, part)
121122
122 if type(obj) == types.ModuleType:
123 if isinstance(obj, types.ModuleType):
123124 return self.loadTestsFromModule(obj)
124125 elif (((py3k and isinstance(obj, type))
125126 or isinstance(obj, (type, types.ClassType)))
126127 and issubclass(obj, TestCase)):
127128 return self.loadTestsFromTestCase(obj)
128 elif type(obj) == types.UnboundMethodType:
129 elif isinstance(obj, types.UnboundMethodType):
129130 if py3k:
130131 return obj.__self__.__class__(obj.__name__)
131132 else:
135136 if not isinstance(test, TestCase) and \
136137 not isinstance(test, TestSuite):
137138 raise ValueError("calling %s returned %s, "
138 "not a test" % (obj,test))
139 "not a test" % (obj, test))
139140 return test
140141 else:
141142 raise ValueError("do not know how to make test from: %s" % obj)
150151 else:
151152 # On Windows, msvcrt.getch reads a single char without output.
152153 import msvcrt
154
153155 def getchar():
154156 return msvcrt.getch()
155157 except ImportError:
156158 # Unix getchr
157 import tty, termios
159 import tty
160 import termios
161
158162 def getchar():
159163 fd = sys.stdin.fileno()
160164 old_settings = termios.tcgetattr(fd)
178182 status = None
179183 headers = None
180184 body = None
181
185
182186 encoding = 'utf-8'
183
187
184188 time = None
185189
186190 def get_conn(self, auto_open=False):
220224
221225 def _get_persistent(self):
222226 return hasattr(self.HTTP_CONN, "__class__")
227
223228 def _set_persistent(self, on):
224229 self.set_persistent(on)
225230 persistent = property(_get_persistent, _set_persistent)
231236 or '::' (IN6ADDR_ANY), this will return the proper localhost."""
232237 return interface(self.HOST)
233238
234 def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
235 """Open the url with debugging support. Return status, headers, body."""
239 def getPage(self, url, headers=None, method="GET", body=None,
240 protocol=None):
241 """Open the url with debugging support. Return status, headers, body.
242 """
236243 ServerError.on = False
237
244
238245 if isinstance(url, unicodestr):
239246 url = url.encode('utf-8')
240247 if isinstance(body, unicodestr):
241248 body = body.encode('utf-8')
242
249
243250 self.url = url
244251 self.time = None
245252 start = time.time()
266273 if not self.interactive:
267274 raise self.failureException(msg)
268275
269 p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> "
276 p = (" Show: "
277 "[B]ody [H]eaders [S]tatus [U]RL; "
278 "[I]gnore, [R]aise, or sys.e[X]it >> ")
270279 sys.stdout.write(p)
271280 sys.stdout.flush()
272281 while True:
350359 msg = '%r:%r not in headers' % (key, value)
351360 self._handlewebError(msg)
352361
362 def assertHeaderIn(self, key, values, msg=None):
363 """Fail if header indicated by key doesn't have one of the values."""
364 lowkey = key.lower()
365 for k, v in self.headers:
366 if k.lower() == lowkey:
367 matches = [value for value in values if str(value) == v]
368 if matches:
369 return matches
370
371 if msg is None:
372 msg = '%(key)r not in %(values)r' % vars()
373 self._handlewebError(msg)
374
353375 def assertHeaderItemValue(self, key, value, msg=None):
354376 """Fail if the header does not contain the specified value"""
355377 actual_value = self.assertHeader(key, msg=msg)
376398 value = value.encode(self.encoding)
377399 if value != self.body:
378400 if msg is None:
379 msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body)
401 msg = 'expected body:\n%r\n\nactual body:\n%r' % (
402 value, self.body)
380403 self._handlewebError(msg)
381404
382405 def assertInBody(self, value, msg=None):
408431
409432
410433 methods_with_bodies = ("POST", "PUT")
434
411435
412436 def cleanHeaders(headers, method, body, host, port):
413437 """Return request headers, with required headers added (if missing)."""
435459 found = True
436460 break
437461 if not found:
438 headers.append(("Content-Type", "application/x-www-form-urlencoded"))
462 headers.append(
463 ("Content-Type", "application/x-www-form-urlencoded"))
439464 headers.append(("Content-Length", str(len(body or ""))))
440465
441466 return headers
491516 return
492517 self.__class__.putheader(self, header, value)
493518 import new
494 conn.putheader = new.instancemethod(putheader, conn, conn.__class__)
519 conn.putheader = new.instancemethod(
520 putheader, conn, conn.__class__)
495521 conn.putrequest(method.upper(), url, skip_host=True)
496522 elif not py3k:
497523 conn.putrequest(method.upper(), url, skip_host=True,
499525 else:
500526 import http.client
501527 # Replace the stdlib method, which only accepts ASCII url's
528
502529 def putrequest(self, method, url):
503 if self._HTTPConnection__response and self._HTTPConnection__response.isclosed():
530 if (
531 self._HTTPConnection__response and
532 self._HTTPConnection__response.isclosed()
533 ):
504534 self._HTTPConnection__response = None
505
535
506536 if self._HTTPConnection__state == http.client._CS_IDLE:
507 self._HTTPConnection__state = http.client._CS_REQ_STARTED
537 self._HTTPConnection__state = (
538 http.client._CS_REQ_STARTED)
508539 else:
509540 raise http.client.CannotSendRequest()
510
541
511542 self._method = method
512543 if not url:
513544 url = ntob('/')
514 request = ntob(' ').join((method.encode("ASCII"), url,
515 self._http_vsn_str.encode("ASCII")))
545 request = ntob(' ').join(
546 (method.encode("ASCII"),
547 url,
548 self._http_vsn_str.encode("ASCII")))
516549 self._output(request)
517550 import types
518551 conn.putrequest = types.MethodType(putrequest, conn)
519
552
520553 conn.putrequest(method.upper(), url)
521554
522555 for key, value in headers:
523 conn.putheader(key, ntob(value, "Latin-1"))
556 conn.putheader(key, value.encode("Latin-1"))
524557 conn.endheaders()
525558
526559 if body is not None:
550583 # that each response will immediately follow each request;
551584 # for example, when handling requests via multiple threads.
552585 ignore_all = False
586
553587
554588 class ServerError(Exception):
555589 on = False
571605 print("")
572606 print("".join(traceback.format_exception(*exc)))
573607 return True
574
00
11 # This is used in test_config to test unrepr of "from A import B"
2 thing2 = object()
2 thing2 = object()
+0
-168
cherrypy/tutorial/bonus-sqlobject.py less more
0 '''
1 Bonus Tutorial: Using SQLObject
2
3 This is a silly little contacts manager application intended to
4 demonstrate how to use SQLObject from within a CherryPy2 project. It
5 also shows how to use inline Cheetah templates.
6
7 SQLObject is an Object/Relational Mapper that allows you to access
8 data stored in an RDBMS in a pythonic fashion. You create data objects
9 as Python classes and let SQLObject take care of all the nasty details.
10
11 This code depends on the latest development version (0.6+) of SQLObject.
12 You can get it from the SQLObject Subversion server. You can find all
13 necessary information at <http://www.sqlobject.org>. This code will NOT
14 work with the 0.5.x version advertised on their website!
15
16 This code also depends on a recent version of Cheetah. You can find
17 Cheetah at <http://www.cheetahtemplate.org>.
18
19 After starting this application for the first time, you will need to
20 access the /reset URI in order to create the database table and some
21 sample data. Accessing /reset again will drop and re-create the table,
22 so you may want to be careful. :-)
23
24 This application isn't supposed to be fool-proof, it's not even supposed
25 to be very GOOD. Play around with it some, browse the source code, smile.
26
27 :)
28
29 -- Hendrik Mans <hendrik@mans.de>
30 '''
31
32 import cherrypy
33 from Cheetah.Template import Template
34 from sqlobject import *
35
36 # configure your database connection here
37 __connection__ = 'mysql://root:@localhost/test'
38
39 # this is our (only) data class.
40 class Contact(SQLObject):
41 lastName = StringCol(length = 50, notNone = True)
42 firstName = StringCol(length = 50, notNone = True)
43 phone = StringCol(length = 30, notNone = True, default = '')
44 email = StringCol(length = 30, notNone = True, default = '')
45 url = StringCol(length = 100, notNone = True, default = '')
46
47
48 class ContactManager:
49 def index(self):
50 # Let's display a list of all stored contacts.
51 contacts = Contact.select()
52
53 template = Template('''
54 <h2>All Contacts</h2>
55
56 #for $contact in $contacts
57 <a href="mailto:$contact.email">$contact.lastName, $contact.firstName</a>
58 [<a href="./edit?id=$contact.id">Edit</a>]
59 [<a href="./delete?id=$contact.id">Delete</a>]
60 <br/>
61 #end for
62
63 <p>[<a href="./edit">Add new contact</a>]</p>
64 ''', [locals(), globals()])
65
66 return template.respond()
67
68 index.exposed = True
69
70
71 def edit(self, id = 0):
72 # we really want id as an integer. Since GET/POST parameters
73 # are always passed as strings, let's convert it.
74 id = int(id)
75
76 if id > 0:
77 # if an id is specified, we're editing an existing contact.
78 contact = Contact.get(id)
79 title = "Edit Contact"
80 else:
81 # if no id is specified, we're entering a new contact.
82 contact = None
83 title = "New Contact"
84
85
86 # In the following template code, please note that we use
87 # Cheetah's $getVar() construct for the form values. We have
88 # to do this because contact may be set to None (see above).
89 template = Template('''
90 <h2>$title</h2>
91
92 <form action="./store" method="POST">
93 <input type="hidden" name="id" value="$id" />
94 Last Name: <input name="lastName" value="$getVar('contact.lastName', '')" /><br/>
95 First Name: <input name="firstName" value="$getVar('contact.firstName', '')" /><br/>
96 Phone: <input name="phone" value="$getVar('contact.phone', '')" /><br/>
97 Email: <input name="email" value="$getVar('contact.email', '')" /><br/>
98 URL: <input name="url" value="$getVar('contact.url', '')" /><br/>
99 <input type="submit" value="Store" />
100 </form>
101 ''', [locals(), globals()])
102
103 return template.respond()
104
105 edit.exposed = True
106
107
108 def delete(self, id):
109 # Delete the specified contact
110 contact = Contact.get(int(id))
111 contact.destroySelf()
112 return 'Deleted. <a href="./">Return to Index</a>'
113
114 delete.exposed = True
115
116
117 def store(self, lastName, firstName, phone, email, url, id = None):
118 if id and int(id) > 0:
119 # If an id was specified, update an existing contact.
120 contact = Contact.get(int(id))
121
122 # We could set one field after another, but that would
123 # cause multiple UPDATE clauses. So we'll just do it all
124 # in a single pass through the set() method.
125 contact.set(
126 lastName = lastName,
127 firstName = firstName,
128 phone = phone,
129 email = email,
130 url = url)
131 else:
132 # Otherwise, add a new contact.
133 contact = Contact(
134 lastName = lastName,
135 firstName = firstName,
136 phone = phone,
137 email = email,
138 url = url)
139
140 return 'Stored. <a href="./">Return to Index</a>'
141
142 store.exposed = True
143
144
145 def reset(self):
146 # Drop existing table
147 Contact.dropTable(True)
148
149 # Create new table
150 Contact.createTable()
151
152 # Create some sample data
153 Contact(
154 firstName = 'Hendrik',
155 lastName = 'Mans',
156 email = 'hendrik@mans.de',
157 phone = '++49 89 12345678',
158 url = 'http://www.mornography.de')
159
160 return "reset completed!"
161
162 reset.exposed = True
163
164
165 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!")
166
167 cherrypy.quickstart(ContactManager())
66 # Import CherryPy global namespace
77 import cherrypy
88
9
910 class HelloWorld:
11
1012 """ Sample request handler class. """
1113
1214 def index(self):
66
77 import cherrypy
88
9
910 class HelloWorld:
10
11
1112 def index(self):
1213 # Let's link to another method here.
13 return 'We have an <a href="showMessage">important message</a> for you!'
14 return 'We have an <a href="show_msg">important message</a> for you!'
1415 index.exposed = True
15
16 def showMessage(self):
16
17 def show_msg(self):
1718 # Here's the important message!
1819 return "Hello world!"
19 showMessage.exposed = True
20 show_msg.exposed = True
2021
2122 import os.path
2223 tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
1717 <input type="submit" />
1818 </form>'''
1919 index.exposed = True
20
21 def greetUser(self, name = None):
20
21 def greetUser(self, name=None):
2222 # CherryPy passes all GET and POST variables as method parameters.
2323 # It doesn't make a difference where the variables come from, how
2424 # large their contents are, and so on.
2626 # You can define default parameter values as usual. In this
2727 # example, the "name" parameter defaults to None so we can check
2828 # if a name was actually specified.
29
29
3030 if name:
3131 # Greet the user!
3232 return "Hey %s, what's up?" % name
88
99
1010 class HomePage:
11
1112 def index(self):
1213 return '''
1314 <p>Hi, this is the home page! Check out the other
1415 fun stuff on this site:</p>
15
16
1617 <ul>
1718 <li><a href="/joke/">A silly joke</a></li>
1819 <li><a href="/links/">Useful links</a></li>
2122
2223
2324 class JokePage:
25
2426 def index(self):
2527 return '''
2628 <p>"In Python, how do you create a string of random
3032
3133
3234 class LinksPage:
35
3336 def __init__(self):
3437 # Request handler objects can create their own nested request
3538 # handler objects. Simply create them inside their __init__
3639 # methods!
3740 self.extra = ExtraLinksPage()
38
41
3942 def index(self):
4043 # Note the way we link to the extra links page (and back).
4144 # As you can see, this object doesn't really care about its
4346 # links exclusively.
4447 return '''
4548 <p>Here are some useful links:</p>
46
49
4750 <ul>
48 <li><a href="http://www.cherrypy.org">The CherryPy Homepage</a></li>
49 <li><a href="http://www.python.org">The Python Homepage</a></li>
51 <li>
52 <a href="http://www.cherrypy.org">The CherryPy Homepage</a>
53 </li>
54 <li>
55 <a href="http://www.python.org">The Python Homepage</a>
56 </li>
5057 </ul>
51
58
5259 <p>You can check out some extra useful
5360 links <a href="./extra/">here</a>.</p>
54
61
5562 <p>[<a href="../">Return</a>]</p>
5663 '''
5764 index.exposed = True
5865
5966
6067 class ExtraLinksPage:
68
6169 def index(self):
6270 # Note the relative link back to the Links page!
6371 return '''
6472 <p>Here are some extra useful links:</p>
65
73
6674 <ul>
6775 <li><a href="http://del.icio.us">del.icio.us</a></li>
68 <li><a href="http://www.mornography.de">Hendrik's weblog</a></li>
76 <li><a href="http://www.cherrypy.org">CherryPy</a></li>
6977 </ul>
70
78
7179 <p>[<a href="../">Return to links page</a>]</p>'''
7280 index.exposed = True
7381
94102 else:
95103 # This branch is for the test suite; you can ignore it.
96104 cherrypy.tree.mount(root, config=tutconf)
97
1212 class Page:
1313 # Store the page title in a class attribute
1414 title = 'Untitled Page'
15
15
1616 def header(self):
1717 return '''
1818 <html>
2222 <body>
2323 <h2>%s</h2>
2424 ''' % (self.title, self.title)
25
25
2626 def footer(self):
2727 return '''
2828 </body>
2929 </html>
3030 '''
31
31
3232 # Note that header and footer don't get their exposed attributes
3333 # set to True. This isn't necessary since the user isn't supposed
3434 # to call header or footer directly; instead, we'll call them from
3939 class HomePage(Page):
4040 # Different title for this page
4141 title = 'Tutorial 5'
42
42
4343 def __init__(self):
4444 # create a subpage
4545 self.another = AnotherPage()
46
46
4747 def index(self):
4848 # Note that we call the header and footer methods inherited
4949 # from the Page class!
5858
5959 class AnotherPage(Page):
6060 title = 'Another Page'
61
61
6262 def index(self):
6363 return self.header() + '''
6464 <p>
7979 else:
8080 # This branch is for the test suite; you can ignore it.
8181 cherrypy.tree.mount(HomePage(), config=tutconf)
82
1919
2020
2121 class UsersPage:
22
22
2323 def index(self):
2424 # Since this is just a stupid little example, we'll simply
2525 # display a list of links to random, made-up users. In a real
3030 <a href="./lorenzo">Lorenzo Lamas</a><br/>
3131 '''
3232 index.exposed = True
33
33
3434 def default(self, user):
3535 # Here we react depending on the virtualPath -- the part of the
3636 # path that could not be mapped to an object method. In a real
4444 out = "Lorenzo Lamas, famous actor and singer!"
4545 else:
4646 out = "Unknown user. :-("
47
47
4848 return '%s (<a href="./">back</a>)' % out
4949 default.exposed = True
5050
6060 else:
6161 # This branch is for the test suite; you can ignore it.
6262 cherrypy.tree.mount(UsersPage(), config=tutconf)
63
1111
1212
1313 class HitCounter:
14
14
1515 _cp_config = {'tools.sessions.on': True}
16
16
1717 def index(self):
1818 # Increase the silly hit counter
1919 count = cherrypy.session.get('count', 0) + 1
20
20
2121 # Store the new value in the session dictionary
2222 cherrypy.session['count'] = count
23
23
2424 # And display a silly hit count message!
2525 return '''
2626 During your current session, you've viewed this
4040 else:
4141 # This branch is for the test suite; you can ignore it.
4242 cherrypy.tree.mount(HitCounter(), config=tutconf)
43
1010
1111
1212 class GeneratorDemo:
13
13
1414 def header(self):
1515 return "<html><body><h2>Generators rule!</h2>"
16
16
1717 def footer(self):
1818 return "</body></html>"
19
19
2020 def index(self):
2121 # Let's make up a list of users for presentation purposes
2222 users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas']
23
23
2424 # Every yield line adds one part to the total result body.
2525 yield self.header()
2626 yield "<h3>List of users:</h3>"
27
27
2828 for user in users:
2929 yield "%s<br/>" % user
30
30
3131 yield self.footer()
3232 index.exposed = True
3333
4343 else:
4444 # This branch is for the test suite; you can ignore it.
4545 cherrypy.tree.mount(GeneratorDemo(), config=tutconf)
46
4848
4949
5050 class FileDemo(object):
51
51
5252 def index(self):
5353 return """
5454 <html><body>
6262 </body></html>
6363 """
6464 index.exposed = True
65
65
6666 def upload(self, myFile):
6767 out = """<html>
6868 <body>
7171 myFile mime-type: %s
7272 </body>
7373 </html>"""
74
74
7575 # Although this just counts the file length, it demonstrates
7676 # how to read large files in chunks instead of all at once.
7777 # CherryPy reads the uploaded file into a temporary file;
8282 if not data:
8383 break
8484 size += len(data)
85
85
8686 return out % (size, myFile.filename, myFile.content_type)
8787 upload.exposed = True
88
88
8989 def download(self):
9090 path = os.path.join(absDir, "pdf_file.pdf")
9191 return static.serve_file(path, "application/x-download",
1515
1616
1717 class HTTPErrorDemo(object):
18
18
1919 # Set a custom response for 403 errors.
20 _cp_config = {'error_page.403' : os.path.join(curpath, "custom_error.html")}
21
20 _cp_config = {'error_page.403':
21 os.path.join(curpath, "custom_error.html")}
22
2223 def index(self):
2324 # display some links that will result in errors
2425 tracebacks = cherrypy.request.show_tracebacks
2627 trace = 'off'
2728 else:
2829 trace = 'on'
29
30
3031 return """
3132 <html><body>
3233 <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p>
3334 <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p>
34 <p><a href="/error?code=403">Use a custom error page from a file.</a></p>
35 <p>
36 <a href="/error?code=403">
37 Use a custom error page from a file.
38 </a>
39 </p>
3540 <p>These errors are explicitly raised by the application:</p>
3641 <ul>
3742 <li><a href="/error?code=400">400</a></li>
4449 </body></html>
4550 """ % trace
4651 index.exposed = True
47
52
4853 def toggleTracebacks(self):
49 # simple function to toggle tracebacks on and off
54 # simple function to toggle tracebacks on and off
5055 tracebacks = cherrypy.request.show_tracebacks
5156 cherrypy.config.update({'request.show_tracebacks': not tracebacks})
52
57
5358 # redirect back to the index
5459 raise cherrypy.HTTPRedirect('/')
5560 toggleTracebacks.exposed = True
56
61
5762 def error(self, code):
5863 # raise an error based on the get query
59 raise cherrypy.HTTPError(status = code)
64 raise cherrypy.HTTPError(status=code)
6065 error.exposed = True
61
66
6267 def messageArg(self):
6368 message = ("If you construct an HTTPError with a 'message' "
6469 "argument, it wil be placed on the error page "
2424
2525
2626 class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
27
2728 """A wrapper for integrating Python's builtin ssl module with CherryPy."""
28
29
2930 certificate = None
3031 """The filename of the server SSL certificate."""
31
32
3233 private_key = None
3334 """The filename of the server's private key file."""
34
35
3536 def __init__(self, certificate, private_key, certificate_chain=None):
3637 if ssl is None:
3738 raise ImportError("You must install the ssl module to use HTTPS.")
3839 self.certificate = certificate
3940 self.private_key = private_key
4041 self.certificate_chain = certificate_chain
41
42
4243 def bind(self, sock):
4344 """Wrap and return the given socket."""
4445 return sock
45
46
4647 def wrap(self, sock):
4748 """Wrap and return the given socket, plus WSGI environ entries."""
4849 try:
4950 s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
50 server_side=True, certfile=self.certificate,
51 keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23)
51 server_side=True, certfile=self.certificate,
52 keyfile=self.private_key,
53 ssl_version=ssl.PROTOCOL_SSLv23)
5254 except ssl.SSLError:
5355 e = sys.exc_info()[1]
5456 if e.errno == ssl.SSL_ERROR_EOF:
6668 return None, {}
6769 raise
6870 return s, self.get_environ(s)
69
71
7072 # TODO: fill this out more with mod ssl env
7173 def get_environ(self, sock):
7274 """Create WSGI environ entries to be merged into each request."""
7678 "HTTPS": "on",
7779 'SSL_PROTOCOL': cipher[1],
7880 'SSL_CIPHER': cipher[0]
79 ## SSL_VERSION_INTERFACE string The mod_ssl program version
80 ## SSL_VERSION_LIBRARY string The OpenSSL program version
81 }
81 # SSL_VERSION_INTERFACE string The mod_ssl program version
82 # SSL_VERSION_LIBRARY string The OpenSSL program version
83 }
8284 return ssl_environ
83
85
8486 if sys.version_info >= (3, 0):
8587 def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
8688 return wsgiserver.CP_makefile(sock, mode, bufsize)
8789 else:
8890 def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
8991 return wsgiserver.CP_fileobject(sock, mode, bufsize)
90
00 """A library for integrating pyOpenSSL with CherryPy.
11
22 The OpenSSL module must be importable for SSL functionality.
3 You can obtain it from http://pyopenssl.sourceforge.net/
3 You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
44
55 To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
66 SSLAdapter. There are two ways to use SSL:
4343
4444
4545 class SSL_fileobject(wsgiserver.CP_fileobject):
46
4647 """SSL file object attached to a socket object."""
47
48
4849 ssl_timeout = 3
4950 ssl_retry = .01
50
51
5152 def _safe_call(self, is_reader, call, *args, **kwargs):
5253 """Wrap the given call with SSL error-trapping.
53
54
5455 is_reader: if False EOF errors will be raised. If True, EOF errors
5556 will return "" (to emulate normal sockets).
5657 """
6970 except SSL.SysCallError, e:
7071 if is_reader and e.args == (-1, 'Unexpected EOF'):
7172 return ""
72
73
7374 errnum = e.args[0]
7475 if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
7576 return ""
7778 except SSL.Error, e:
7879 if is_reader and e.args == (-1, 'Unexpected EOF'):
7980 return ""
80
81
8182 thirdarg = None
8283 try:
8384 thirdarg = e.args[0][0][2]
8485 except IndexError:
8586 pass
86
87
8788 if thirdarg == 'http request':
8889 # The client is talking HTTP to an HTTPS server.
8990 raise wsgiserver.NoSSLError()
90
91
9192 raise wsgiserver.FatalSSLAlert(*e.args)
9293 except:
9394 raise
94
95
9596 if time.time() - start > self.ssl_timeout:
9697 raise socket.timeout("timed out")
97
98 def recv(self, *args, **kwargs):
99 buf = []
100 r = super(SSL_fileobject, self).recv
101 while True:
102 data = self._safe_call(True, r, *args, **kwargs)
103 buf.append(data)
104 p = self._sock.pending()
105 if not p:
106 return "".join(buf)
107
98
99 def recv(self, size):
100 return self._safe_call(True, super(SSL_fileobject, self).recv, size)
101
108102 def sendall(self, *args, **kwargs):
109103 return self._safe_call(False, super(SSL_fileobject, self).sendall,
110104 *args, **kwargs)
115109
116110
117111 class SSLConnection:
112
118113 """A thread-safe wrapper for an SSL.Connection.
119
114
120115 ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
121116 """
122
117
123118 def __init__(self, *args):
124119 self._ssl_conn = SSL.Connection(*args)
125120 self._lock = threading.RLock()
126
121
127122 for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
128123 'renegotiate', 'bind', 'listen', 'connect', 'accept',
129124 'setblocking', 'fileno', 'close', 'get_cipher_list',
139134 finally:
140135 self._lock.release()
141136 """ % (f, f))
142
137
143138 def shutdown(self, *args):
144139 self._lock.acquire()
145140 try:
150145
151146
152147 class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
148
153149 """A wrapper for integrating pyOpenSSL with CherryPy."""
154
150
155151 context = None
156152 """An instance of SSL.Context."""
157
153
158154 certificate = None
159155 """The filename of the server SSL certificate."""
160
156
161157 private_key = None
162158 """The filename of the server's private key file."""
163
159
164160 certificate_chain = None
165161 """Optional. The filename of CA's intermediate certificate bundle.
166
162
167163 This is needed for cheaper "chained root" SSL certificates, and should be
168164 left as None if not required."""
169
165
170166 def __init__(self, certificate, private_key, certificate_chain=None):
171167 if SSL is None:
172168 raise ImportError("You must install pyOpenSSL to use HTTPS.")
173
169
174170 self.context = None
175171 self.certificate = certificate
176172 self.private_key = private_key
177173 self.certificate_chain = certificate_chain
178174 self._environ = None
179
175
180176 def bind(self, sock):
181177 """Wrap and return the given socket."""
182178 if self.context is None:
184180 conn = SSLConnection(self.context, sock)
185181 self._environ = self.get_environ()
186182 return conn
187
183
188184 def wrap(self, sock):
189185 """Wrap and return the given socket, plus WSGI environ entries."""
190186 return sock, self._environ.copy()
191
187
192188 def get_context(self):
193189 """Return an SSL.Context from self attributes."""
194190 # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
198194 c.load_verify_locations(self.certificate_chain)
199195 c.use_certificate_file(self.certificate)
200196 return c
201
197
202198 def get_environ(self):
203199 """Return WSGI environ entries to be merged into each request."""
204200 ssl_environ = {
205201 "HTTPS": "on",
206202 # pyOpenSSL doesn't provide access to any of these AFAICT
207 ## 'SSL_PROTOCOL': 'SSLv2',
208 ## SSL_CIPHER string The cipher specification name
209 ## SSL_VERSION_INTERFACE string The mod_ssl program version
210 ## SSL_VERSION_LIBRARY string The OpenSSL program version
211 }
212
203 # 'SSL_PROTOCOL': 'SSLv2',
204 # SSL_CIPHER string The cipher specification name
205 # SSL_VERSION_INTERFACE string The mod_ssl program version
206 # SSL_VERSION_LIBRARY string The OpenSSL program version
207 }
208
213209 if self.certificate:
214210 # Server certificate attributes
215211 cert = open(self.certificate, 'rb').read()
217213 ssl_environ.update({
218214 'SSL_SERVER_M_VERSION': cert.get_version(),
219215 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
220 ## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
221 ## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
222 })
223
216 # 'SSL_SERVER_V_START':
217 # Validity of server's certificate (start time),
218 # 'SSL_SERVER_V_END':
219 # Validity of server's certificate (end time),
220 })
221
224222 for prefix, dn in [("I", cert.get_issuer()),
225223 ("S", cert.get_subject())]:
226224 # X509Name objects don't seem to have a way to get the
227225 # complete DN string. Use str() and slice it instead,
228226 # because str(dn) == "<X509Name object '/C=US/ST=...'>"
229227 dnstr = str(dn)[18:-2]
230
228
231229 wsgikey = 'SSL_SERVER_%s_DN' % prefix
232230 ssl_environ[wsgikey] = dnstr
233
231
234232 # The DN should be of the form: /k1=v1/k2=v2, but we must allow
235233 # for any value to contain slashes itself (in a URL).
236234 while dnstr:
241239 if key and value:
242240 wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
243241 ssl_environ[wsgikey] = value
244
242
245243 return ssl_environ
246
244
247245 def makefile(self, sock, mode='r', bufsize=-1):
248246 if SSL and isinstance(sock, SSL.ConnectionType):
249247 timeout = sock.gettimeout()
252250 return f
253251 else:
254252 return wsgiserver.CP_fileobject(sock, mode, bufsize)
255
33 (without using CherryPy's application machinery)::
44
55 from cherrypy import wsgiserver
6
6
77 def my_crazy_app(environ, start_response):
88 status = '200 OK'
99 response_headers = [('Content-type','text/plain')]
1010 start_response(status, response_headers)
1111 return ['Hello world!']
12
12
1313 server = wsgiserver.CherryPyWSGIServer(
1414 ('0.0.0.0', 8070), my_crazy_app,
1515 server_name='www.cherrypy.example')
1616 server.start()
17
18 The CherryPy WSGI server can serve as many WSGI applications
17
18 The CherryPy WSGI server can serve as many WSGI applications
1919 as you want in one instance by using a WSGIPathInfoDispatcher::
20
20
2121 d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
2222 server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
23
23
2424 Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance.
2525
2626 This won't call the CherryPy engine (application side) at all, only the
8585 import rfc822
8686 import socket
8787 import sys
88 if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'):
89 socket.IPPROTO_IPV6 = 41
88 if 'win' in sys.platform and hasattr(socket, "AF_INET6"):
89 if not hasattr(socket, 'IPPROTO_IPV6'):
90 socket.IPPROTO_IPV6 = 41
91 if not hasattr(socket, 'IPV6_V6ONLY'):
92 socket.IPV6_V6ONLY = 27
9093 try:
9194 import cStringIO as StringIO
9295 except ImportError:
9396 import StringIO
9497 DEFAULT_BUFFER_SIZE = -1
9598
96 _fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
99
100 class FauxSocket(object):
101
102 """Faux socket with the minimal interface required by pypy"""
103
104 def _reuse(self):
105 pass
106
107 _fileobject_uses_str_type = isinstance(
108 socket._fileobject(FauxSocket())._rbuf, basestring)
109 del FauxSocket # this class is not longer required for anything.
97110
98111 import threading
99112 import time
100113 import traceback
114
115
101116 def format_exc(limit=None):
102117 """Like print_exc() but return a string. Backport for Python 2.3."""
103118 try:
106121 finally:
107122 etype = value = tb = None
108123
124 import operator
109125
110126 from urllib import unquote
111 from urlparse import urlparse
112127 import warnings
113128
114129 if sys.version_info >= (3, 0):
115130 bytestr = bytes
116131 unicodestr = str
117132 basestring = (bytes, str)
133
118134 def ntob(n, encoding='ISO-8859-1'):
119 """Return the given native string as a byte string in the given encoding."""
135 """Return the given native string as a byte string in the given
136 encoding.
137 """
120138 # In Python 3, the native string type is unicode
121139 return n.encode(encoding)
122140 else:
123141 bytestr = str
124142 unicodestr = unicode
125143 basestring = basestring
144
126145 def ntob(n, encoding='ISO-8859-1'):
127 """Return the given native string as a byte string in the given encoding."""
146 """Return the given native string as a byte string in the given
147 encoding.
148 """
128149 # In Python 2, the native string type is bytes. Assume it's already
129150 # in the given encoding, which for ISO-8859-1 is almost always what
130151 # was intended.
145166
146167 import errno
147168
169
148170 def plat_specific_errors(*errnames):
149171 """Return error numbers for all errors in errnames on this platform.
150
172
151173 The 'errno' module contains different global constants depending on
152174 the specific platform (OS). This function will return the list of
153175 numeric values for a given list of potential names.
169191 "ECONNABORTED", "WSAECONNABORTED",
170192 "ENETRESET", "WSAENETRESET",
171193 "EHOSTDOWN", "EHOSTUNREACH",
172 )
194 )
173195 socket_errors_to_ignore.append("timed out")
174196 socket_errors_to_ignore.append("The read operation timed out")
175197
176198 socket_errors_nonblocking = plat_specific_errors(
177199 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
178200
179 comma_separated_headers = [ntob(h) for h in
201 comma_separated_headers = [
202 ntob(h) for h in
180203 ['Accept', 'Accept-Charset', 'Accept-Encoding',
181204 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control',
182205 'Connection', 'Content-Encoding', 'Content-Language', 'Expect',
183206 'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE',
184207 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning',
185 'WWW-Authenticate']]
208 'WWW-Authenticate']
209 ]
186210
187211
188212 import logging
189 if not hasattr(logging, 'statistics'): logging.statistics = {}
213 if not hasattr(logging, 'statistics'):
214 logging.statistics = {}
190215
191216
192217 def read_headers(rfile, hdict=None):
193218 """Read headers from the given stream into the given header dict.
194
219
195220 If hdict is None, a new header dict is created. Returns the populated
196221 header dict.
197
222
198223 Headers which are repeated are folded together using a comma if their
199224 specification so dictates.
200
225
201226 This function raises ValueError when the read bytes violate the HTTP spec.
202227 You should probably return "400 Bad Request" if this happens.
203228 """
204229 if hdict is None:
205230 hdict = {}
206
231
207232 while True:
208233 line = rfile.readline()
209234 if not line:
210235 # No more data--illegal end of headers
211236 raise ValueError("Illegal end of headers.")
212
237
213238 if line == CRLF:
214239 # Normal end of headers
215240 break
216241 if not line.endswith(CRLF):
217242 raise ValueError("HTTP requires CRLF terminators")
218
243
219244 if line[0] in (SPACE, TAB):
220245 # It's a continuation line.
221246 v = line.strip()
228253 k = k.strip().title()
229254 v = v.strip()
230255 hname = k
231
256
232257 if k in comma_separated_headers:
233258 existing = hdict.get(hname)
234259 if existing:
235260 v = ", ".join((existing, v))
236261 hdict[hname] = v
237
262
238263 return hdict
239264
240265
241266 class MaxSizeExceeded(Exception):
242267 pass
243268
269
244270 class SizeCheckWrapper(object):
271
245272 """Wraps a file-like object, raising MaxSizeExceeded if too large."""
246
273
247274 def __init__(self, rfile, maxlen):
248275 self.rfile = rfile
249276 self.maxlen = maxlen
250277 self.bytes_read = 0
251
278
252279 def _check_length(self):
253280 if self.maxlen and self.bytes_read > self.maxlen:
254281 raise MaxSizeExceeded()
255
282
256283 def read(self, size=None):
257284 data = self.rfile.read(size)
258285 self.bytes_read += len(data)
259286 self._check_length()
260287 return data
261
288
262289 def readline(self, size=None):
263290 if size is not None:
264291 data = self.rfile.readline(size)
265292 self.bytes_read += len(data)
266293 self._check_length()
267294 return data
268
295
269296 # User didn't specify a size ...
270297 # We read the line in chunks to make sure it's not a 100MB line !
271298 res = []
274301 self.bytes_read += len(data)
275302 self._check_length()
276303 res.append(data)
277 # See http://www.cherrypy.org/ticket/421
278 if len(data) < 256 or data[-1:] == "\n":
304 # See https://bitbucket.org/cherrypy/cherrypy/issue/421
305 if len(data) < 256 or data[-1:] == LF:
279306 return EMPTY.join(res)
280
307
281308 def readlines(self, sizehint=0):
282309 # Shamelessly stolen from StringIO
283310 total = 0
290317 break
291318 line = self.readline()
292319 return lines
293
320
294321 def close(self):
295322 self.rfile.close()
296
323
297324 def __iter__(self):
298325 return self
299
326
300327 def __next__(self):
301328 data = next(self.rfile)
302329 self.bytes_read += len(data)
303330 self._check_length()
304331 return data
305
332
306333 def next(self):
307334 data = self.rfile.next()
308335 self.bytes_read += len(data)
311338
312339
313340 class KnownLengthRFile(object):
341
314342 """Wraps a file-like object, returning an empty string when exhausted."""
315
343
316344 def __init__(self, rfile, content_length):
317345 self.rfile = rfile
318346 self.remaining = content_length
319
347
320348 def read(self, size=None):
321349 if self.remaining == 0:
322350 return ''
324352 size = self.remaining
325353 else:
326354 size = min(size, self.remaining)
327
355
328356 data = self.rfile.read(size)
329357 self.remaining -= len(data)
330358 return data
331
359
332360 def readline(self, size=None):
333361 if self.remaining == 0:
334362 return ''
336364 size = self.remaining
337365 else:
338366 size = min(size, self.remaining)
339
367
340368 data = self.rfile.readline(size)
341369 self.remaining -= len(data)
342370 return data
343
371
344372 def readlines(self, sizehint=0):
345373 # Shamelessly stolen from StringIO
346374 total = 0
353381 break
354382 line = self.readline(sizehint)
355383 return lines
356
384
357385 def close(self):
358386 self.rfile.close()
359
387
360388 def __iter__(self):
361389 return self
362
390
363391 def __next__(self):
364392 data = next(self.rfile)
365393 self.remaining -= len(data)
367395
368396
369397 class ChunkedRFile(object):
398
370399 """Wraps a file-like object, returning an empty string when exhausted.
371
400
372401 This class is intended to provide a conforming wsgi.input value for
373402 request entities that have been encoded with the 'chunked' transfer
374403 encoding.
375404 """
376
405
377406 def __init__(self, rfile, maxlen, bufsize=8192):
378407 self.rfile = rfile
379408 self.maxlen = maxlen
381410 self.buffer = EMPTY
382411 self.bufsize = bufsize
383412 self.closed = False
384
413
385414 def _fetch(self):
386415 if self.closed:
387416 return
388
417
389418 line = self.rfile.readline()
390419 self.bytes_read += len(line)
391
420
392421 if self.maxlen and self.bytes_read > self.maxlen:
393422 raise MaxSizeExceeded("Request Entity Too Large", self.maxlen)
394
423
395424 line = line.strip().split(SEMICOLON, 1)
396
425
397426 try:
398427 chunk_size = line.pop(0)
399428 chunk_size = int(chunk_size, 16)
400429 except ValueError:
401430 raise ValueError("Bad chunked transfer size: " + repr(chunk_size))
402
431
403432 if chunk_size <= 0:
404433 self.closed = True
405434 return
406
435
407436 ## if line: chunk_extension = line[0]
408
437
409438 if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
410439 raise IOError("Request Entity Too Large")
411
440
412441 chunk = self.rfile.read(chunk_size)
413442 self.bytes_read += len(chunk)
414443 self.buffer += chunk
415
444
416445 crlf = self.rfile.read(2)
417446 if crlf != CRLF:
418447 raise ValueError(
419 "Bad chunked transfer coding (expected '\\r\\n', "
420 "got " + repr(crlf) + ")")
421
448 "Bad chunked transfer coding (expected '\\r\\n', "
449 "got " + repr(crlf) + ")")
450
422451 def read(self, size=None):
423452 data = EMPTY
424453 while True:
425454 if size and len(data) >= size:
426455 return data
427
456
428457 if not self.buffer:
429458 self._fetch()
430459 if not self.buffer:
431460 # EOF
432461 return data
433
462
434463 if size:
435464 remaining = size - len(data)
436465 data += self.buffer[:remaining]
437466 self.buffer = self.buffer[remaining:]
438467 else:
439468 data += self.buffer
440
469
441470 def readline(self, size=None):
442471 data = EMPTY
443472 while True:
444473 if size and len(data) >= size:
445474 return data
446
475
447476 if not self.buffer:
448477 self._fetch()
449478 if not self.buffer:
450479 # EOF
451480 return data
452
481
453482 newline_pos = self.buffer.find(LF)
454483 if size:
455484 if newline_pos == -1:
466495 else:
467496 data += self.buffer[:newline_pos]
468497 self.buffer = self.buffer[newline_pos:]
469
498
470499 def readlines(self, sizehint=0):
471500 # Shamelessly stolen from StringIO
472501 total = 0
479508 break
480509 line = self.readline(sizehint)
481510 return lines
482
511
483512 def read_trailer_lines(self):
484513 if not self.closed:
485514 raise ValueError(
486515 "Cannot read trailers until the request body has been read.")
487
516
488517 while True:
489518 line = self.rfile.readline()
490519 if not line:
491520 # No more data--illegal end of headers
492521 raise ValueError("Illegal end of headers.")
493
522
494523 self.bytes_read += len(line)
495524 if self.maxlen and self.bytes_read > self.maxlen:
496525 raise IOError("Request Entity Too Large")
497
526
498527 if line == CRLF:
499528 # Normal end of headers
500529 break
501530 if not line.endswith(CRLF):
502531 raise ValueError("HTTP requires CRLF terminators")
503
532
504533 yield line
505
534
506535 def close(self):
507536 self.rfile.close()
508
537
509538 def __iter__(self):
510539 # Shamelessly stolen from StringIO
511540 total = 0
519548
520549
521550 class HTTPRequest(object):
551
522552 """An HTTP Request (and response).
523
553
524554 A single HTTP connection may consist of multiple request/response pairs.
525555 """
526
556
527557 server = None
528558 """The HTTPServer object which is receiving this request."""
529
559
530560 conn = None
531561 """The HTTPConnection object on which this request connected."""
532
562
533563 inheaders = {}
534564 """A dict of request headers."""
535
565
536566 outheaders = []
537567 """A list of header tuples to write in the response."""
538
568
539569 ready = False
540570 """When True, the request has been parsed and is ready to begin generating
541571 the response. When False, signals the calling Connection that the response
542572 should not be generated and the connection should close."""
543
573
544574 close_connection = False
545575 """Signals the calling Connection that the request should close. This does
546576 not imply an error! The client and/or server may each request that the
547577 connection be closed."""
548
578
549579 chunked_write = False
550580 """If True, output will be encoded with the "chunked" transfer-coding.
551
581
552582 This value is set automatically inside send_headers."""
553
583
554584 def __init__(self, server, conn):
555 self.server= server
585 self.server = server
556586 self.conn = conn
557
587
558588 self.ready = False
559589 self.started_request = False
560590 self.scheme = ntob("http")
563593 # Use the lowest-common protocol in case read_request_line errors.
564594 self.response_protocol = 'HTTP/1.0'
565595 self.inheaders = {}
566
596
567597 self.status = ""
568598 self.outheaders = []
569599 self.sent_headers = False
570600 self.close_connection = self.__class__.close_connection
571601 self.chunked_read = False
572602 self.chunked_write = self.__class__.chunked_write
573
603
574604 def parse_request(self):
575605 """Parse the next HTTP request start-line and message-headers."""
576606 self.rfile = SizeCheckWrapper(self.conn.rfile,
578608 try:
579609 success = self.read_request_line()
580610 except MaxSizeExceeded:
581 self.simple_response("414 Request-URI Too Long",
611 self.simple_response(
612 "414 Request-URI Too Long",
582613 "The Request-URI sent with the request exceeds the maximum "
583614 "allowed bytes.")
584615 return
585616 else:
586617 if not success:
587618 return
588
619
589620 try:
590621 success = self.read_request_headers()
591622 except MaxSizeExceeded:
592 self.simple_response("413 Request Entity Too Large",
623 self.simple_response(
624 "413 Request Entity Too Large",
593625 "The headers sent with the request exceed the maximum "
594626 "allowed bytes.")
595627 return
596628 else:
597629 if not success:
598630 return
599
631
600632 self.ready = True
601
633
602634 def read_request_line(self):
603635 # HTTP/1.1 connections are persistent by default. If a client
604636 # requests a page, then idles (leaves the connection open),
608640 # (although your TCP stack might suffer for it: cf Apache's history
609641 # with FIN_WAIT_2).
610642 request_line = self.rfile.readline()
611
643
612644 # Set started_request to True so communicate() knows to send 408
613645 # from here on out.
614646 self.started_request = True
615647 if not request_line:
616648 return False
617
649
618650 if request_line == CRLF:
619651 # RFC 2616 sec 4.1: "...if the server is reading the protocol
620652 # stream at the beginning of a message and receives a CRLF
623655 request_line = self.rfile.readline()
624656 if not request_line:
625657 return False
626
658
627659 if not request_line.endswith(CRLF):
628 self.simple_response("400 Bad Request", "HTTP requires CRLF terminators")
660 self.simple_response(
661 "400 Bad Request", "HTTP requires CRLF terminators")
629662 return False
630
663
631664 try:
632665 method, uri, req_protocol = request_line.strip().split(SPACE, 2)
633666 rp = int(req_protocol[5]), int(req_protocol[7])
634667 except (ValueError, IndexError):
635668 self.simple_response("400 Bad Request", "Malformed Request-Line")
636669 return False
637
670
638671 self.uri = uri
639672 self.method = method
640
673
641674 # uri may be an abs_path (including "http://host.domain.tld");
642675 scheme, authority, path = self.parse_request_uri(uri)
643676 if NUMBER_SIGN in path:
644677 self.simple_response("400 Bad Request",
645678 "Illegal #fragment in Request-URI.")
646679 return False
647
680
648681 if scheme:
649682 self.scheme = scheme
650
683
651684 qs = EMPTY
652685 if QUESTION_MARK in path:
653686 path, qs = path.split(QUESTION_MARK, 1)
654
687
655688 # Unquote the path+params (e.g. "/this%20path" -> "/this path").
656689 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
657690 #
667700 return False
668701 path = "%2F".join(atoms)
669702 self.path = path
670
703
671704 # Note that, like wsgiref and most other HTTP servers,
672705 # we "% HEX HEX"-unquote the path but not the query string.
673706 self.qs = qs
674
707
675708 # Compare request and server HTTP protocol versions, in case our
676709 # server does not support the requested protocol. Limit our output
677710 # to min(req, server). We want the following output:
685718 # the client only understands 1.0. RFC 2616 10.5.6 says we should
686719 # only return 505 if the _major_ version is different.
687720 sp = int(self.server.protocol[5]), int(self.server.protocol[7])
688
721
689722 if sp[0] != rp[0]:
690723 self.simple_response("505 HTTP Version Not Supported")
691724 return False
697730
698731 def read_request_headers(self):
699732 """Read self.rfile into self.inheaders. Return success."""
700
733
701734 # then all the http headers
702735 try:
703736 read_headers(self.rfile, self.inheaders)
705738 ex = sys.exc_info()[1]
706739 self.simple_response("400 Bad Request", ex.args[0])
707740 return False
708
741
709742 mrbs = self.server.max_request_body_size
710743 if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs:
711 self.simple_response("413 Request Entity Too Large",
744 self.simple_response(
745 "413 Request Entity Too Large",
712746 "The entity sent with the request exceeds the maximum "
713747 "allowed bytes.")
714748 return False
715
749
716750 # Persistent connection support
717751 if self.response_protocol == "HTTP/1.1":
718752 # Both server and client are HTTP/1.1
722756 # Either the server or client (or both) are HTTP/1.0
723757 if self.inheaders.get("Connection", "") != "Keep-Alive":
724758 self.close_connection = True
725
759
726760 # Transfer-Encoding support
727761 te = None
728762 if self.response_protocol == "HTTP/1.1":
729763 te = self.inheaders.get("Transfer-Encoding")
730764 if te:
731765 te = [x.strip().lower() for x in te.split(",") if x.strip()]
732
766
733767 self.chunked_read = False
734
768
735769 if te:
736770 for enc in te:
737771 if enc == "chunked":
742776 self.simple_response("501 Unimplemented")
743777 self.close_connection = True
744778 return False
745
779
746780 # From PEP 333:
747781 # "Servers and gateways that implement HTTP 1.1 must provide
748782 # transparent support for HTTP 1.1's "expect/continue" mechanism.
762796 # but it seems like it would be a big slowdown for such a rare case.
763797 if self.inheaders.get("Expect", "") == "100-continue":
764798 # Don't use simple_response here, because it emits headers
765 # we don't want. See http://www.cherrypy.org/ticket/951
799 # we don't want. See
800 # https://bitbucket.org/cherrypy/cherrypy/issue/951
766801 msg = self.server.protocol + " 100 Continue\r\n\r\n"
767802 try:
768803 self.conn.wfile.sendall(msg)
771806 if x.args[0] not in socket_errors_to_ignore:
772807 raise
773808 return True
774
809
775810 def parse_request_uri(self, uri):
776811 """Parse a Request-URI into (scheme, authority, path).
777
812
778813 Note that Request-URI's must be one of::
779
814
780815 Request-URI = "*" | absoluteURI | abs_path | authority
781
816
782817 Therefore, a Request-URI which starts with a double forward-slash
783818 cannot be a "net_path"::
784
819
785820 net_path = "//" authority [ abs_path ]
786
821
787822 Instead, it must be interpreted as an "abs_path" with an empty first
788823 path segment::
789
824
790825 abs_path = "/" path_segments
791826 path_segments = segment *( "/" segment )
792827 segment = *pchar *( ";" param )
794829 """
795830 if uri == ASTERISK:
796831 return None, None, uri
797
832
798833 i = uri.find('://')
799834 if i > 0 and QUESTION_MARK not in uri[:i]:
800835 # An absoluteURI.
801836 # If there's a scheme (and it must be http or https), then:
802 # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
837 # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query
838 # ]]
803839 scheme, remainder = uri[:i].lower(), uri[i + 3:]
804840 authority, path = remainder.split(FORWARD_SLASH, 1)
805841 path = FORWARD_SLASH + path
806842 return scheme, authority, path
807
843
808844 if uri.startswith(FORWARD_SLASH):
809845 # An abs_path.
810846 return None, None, uri
811847 else:
812848 # An authority.
813849 return None, uri, None
814
850
815851 def respond(self):
816852 """Call the gateway and write its iterable output."""
817853 mrbs = self.server.max_request_body_size
821857 cl = int(self.inheaders.get("Content-Length", 0))
822858 if mrbs and mrbs < cl:
823859 if not self.sent_headers:
824 self.simple_response("413 Request Entity Too Large",
860 self.simple_response(
861 "413 Request Entity Too Large",
825862 "The entity sent with the request exceeds the maximum "
826863 "allowed bytes.")
827864 return
828865 self.rfile = KnownLengthRFile(self.conn.rfile, cl)
829
866
830867 self.server.gateway(self).respond()
831
868
832869 if (self.ready and not self.sent_headers):
833870 self.sent_headers = True
834871 self.send_headers()
835872 if self.chunked_write:
836873 self.conn.wfile.sendall("0\r\n\r\n")
837
874
838875 def simple_response(self, status, msg=""):
839876 """Write a simple response back to the client."""
840877 status = str(status)
842879 status + CRLF,
843880 "Content-Length: %s\r\n" % len(msg),
844881 "Content-Type: text/plain\r\n"]
845
882
846883 if status[:3] in ("413", "414"):
847884 # Request Entity Too Large / Request-URI Too Long
848885 self.close_connection = True
855892 # HTTP/1.0 had no 413/414 status nor Connection header.
856893 # Emit 400 instead and trust the message body is enough.
857894 status = "400 Bad Request"
858
895
859896 buf.append(CRLF)
860897 if msg:
861898 if isinstance(msg, unicodestr):
862899 msg = msg.encode("ISO-8859-1")
863900 buf.append(msg)
864
901
865902 try:
866903 self.conn.wfile.sendall("".join(buf))
867904 except socket.error:
868905 x = sys.exc_info()[1]
869906 if x.args[0] not in socket_errors_to_ignore:
870907 raise
871
908
872909 def write(self, chunk):
873910 """Write unbuffered data to the client."""
874911 if self.chunked_write and chunk:
876913 self.conn.wfile.sendall(EMPTY.join(buf))
877914 else:
878915 self.conn.wfile.sendall(chunk)
879
916
880917 def send_headers(self):
881918 """Assert, process, and send the HTTP response message-headers.
882
919
883920 You must set self.status, and self.outheaders before calling this.
884921 """
885922 hkeys = [key.lower() for key, value in self.outheaders]
886923 status = int(self.status[:3])
887
924
888925 if status == 413:
889926 # Request Entity Too Large. Close conn to avoid garbage.
890927 self.close_connection = True
896933 pass
897934 else:
898935 if (self.response_protocol == 'HTTP/1.1'
899 and self.method != 'HEAD'):
936 and self.method != 'HEAD'):
900937 # Use the chunked transfer-coding
901938 self.chunked_write = True
902939 self.outheaders.append(("Transfer-Encoding", "chunked"))
903940 else:
904941 # Closing the conn is the only way to determine len.
905942 self.close_connection = True
906
943
907944 if "connection" not in hkeys:
908945 if self.response_protocol == 'HTTP/1.1':
909946 # Both server and client are HTTP/1.1 or better
913950 # Server and/or client are HTTP/1.0
914951 if not self.close_connection:
915952 self.outheaders.append(("Connection", "Keep-Alive"))
916
953
917954 if (not self.close_connection) and (not self.chunked_read):
918955 # Read any remaining request body data on the socket.
919956 # "If an origin server receives a request that does not include an
930967 remaining = getattr(self.rfile, 'remaining', 0)
931968 if remaining > 0:
932969 self.rfile.read(remaining)
933
970
934971 if "date" not in hkeys:
935972 self.outheaders.append(("Date", rfc822.formatdate()))
936
973
937974 if "server" not in hkeys:
938975 self.outheaders.append(("Server", self.server.server_name))
939
976
940977 buf = [self.server.protocol + SPACE + self.status + CRLF]
941978 for k, v in self.outheaders:
942979 buf.append(k + COLON + SPACE + v + CRLF)
945982
946983
947984 class NoSSLError(Exception):
985
948986 """Exception raised when a client speaks HTTP to an HTTPS socket."""
949987 pass
950988
951989
952990 class FatalSSLAlert(Exception):
991
953992 """Exception raised when the SSL implementation signals a fatal alert."""
954993 pass
955994
956995
957996 class CP_fileobject(socket._fileobject):
997
958998 """Faux file object attached to a socket object."""
959999
9601000 def __init__(self, *args, **kwargs):
9611001 self.bytes_read = 0
9621002 self.bytes_written = 0
9631003 socket._fileobject.__init__(self, *args, **kwargs)
964
1004
9651005 def sendall(self, data):
9661006 """Sendall for non-blocking sockets."""
9671007 while data:
9911031 return data
9921032 except socket.error, e:
9931033 if (e.args[0] not in socket_errors_nonblocking
994 and e.args[0] not in socket_error_eintr):
1034 and e.args[0] not in socket_error_eintr):
9951035 raise
9961036
9971037 if not _fileobject_uses_str_type:
9981038 def read(self, size=-1):
999 # Use max, disallow tiny reads in a loop as they are very inefficient.
1000 # We never leave read() with any leftover data from a new recv() call
1001 # in our internal buffer.
1039 # Use max, disallow tiny reads in a loop as they are very
1040 # inefficient.
1041 # We never leave read() with any leftover data from a new recv()
1042 # call in our internal buffer.
10021043 rbufsize = max(self._rbufsize, self.default_bufsize)
1003 # Our use of StringIO rather than lists of string objects returned by
1004 # recv() minimizes memory usage and fragmentation that occurs when
1005 # rbufsize is large compared to the typical return value of recv().
1044 # Our use of StringIO rather than lists of string objects returned
1045 # by recv() minimizes memory usage and fragmentation that occurs
1046 # when rbufsize is large compared to the typical return value of
1047 # recv().
10061048 buf = self._rbuf
10071049 buf.seek(0, 2) # seek end
10081050 if size < 0:
10091051 # Read until EOF
1010 self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
1052 # reset _rbuf. we consume it via buf.
1053 self._rbuf = StringIO.StringIO()
10111054 while True:
10121055 data = self.recv(rbufsize)
10131056 if not data:
10181061 # Read until size bytes or EOF seen, whichever comes first
10191062 buf_len = buf.tell()
10201063 if buf_len >= size:
1021 # Already have size bytes in our buffer? Extract and return.
1064 # Already have size bytes in our buffer? Extract and
1065 # return.
10221066 buf.seek(0)
10231067 rv = buf.read(size)
10241068 self._rbuf = StringIO.StringIO()
10251069 self._rbuf.write(buf.read())
10261070 return rv
10271071
1028 self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
1072 # reset _rbuf. we consume it via buf.
1073 self._rbuf = StringIO.StringIO()
10291074 while True:
10301075 left = size - buf_len
10311076 # recv() will malloc the amount of memory given as its
10731118 # Speed up unbuffered case
10741119 buf.seek(0)
10751120 buffers = [buf.read()]
1076 self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
1121 # reset _rbuf. we consume it via buf.
1122 self._rbuf = StringIO.StringIO()
10771123 data = None
10781124 recv = self.recv
10791125 while data != "\n":
10841130 return "".join(buffers)
10851131
10861132 buf.seek(0, 2) # seek end
1087 self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
1133 # reset _rbuf. we consume it via buf.
1134 self._rbuf = StringIO.StringIO()
10881135 while True:
10891136 data = self.recv(self._rbufsize)
10901137 if not data:
10991146 buf.write(data)
11001147 return buf.getvalue()
11011148 else:
1102 # Read until size bytes or \n or EOF seen, whichever comes first
1149 # Read until size bytes or \n or EOF seen, whichever comes
1150 # first
11031151 buf.seek(0, 2) # seek end
11041152 buf_len = buf.tell()
11051153 if buf_len >= size:
11081156 self._rbuf = StringIO.StringIO()
11091157 self._rbuf.write(buf.read())
11101158 return rv
1111 self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
1159 # reset _rbuf. we consume it via buf.
1160 self._rbuf = StringIO.StringIO()
11121161 while True:
11131162 data = self.recv(self._rbufsize)
11141163 if not data:
11241173 buf.write(data[:nl])
11251174 break
11261175 else:
1127 # Shortcut. Avoid data copy through buf when returning
1128 # a substring of our first recv().
1176 # Shortcut. Avoid data copy through buf when
1177 # returning a substring of our first recv().
11291178 return data[:nl]
11301179 n = len(data)
11311180 if n == size and not buf_len:
12191268 break
12201269 return "".join(buffers)
12211270 else:
1222 # Read until size bytes or \n or EOF seen, whichever comes first
1271 # Read until size bytes or \n or EOF seen, whichever comes
1272 # first
12231273 nl = data.find('\n', 0, size)
12241274 if nl >= 0:
12251275 nl += 1
12551305
12561306
12571307 class HTTPConnection(object):
1308
12581309 """An HTTP connection (active socket).
1259
1310
12601311 server: the Server object which received this connection.
12611312 socket: the raw socket object (usually TCP) for this connection.
12621313 makefile: a fileobject class for reading from the socket.
12631314 """
1264
1315
12651316 remote_addr = None
12661317 remote_port = None
12671318 ssl_env = None
12681319 rbufsize = DEFAULT_BUFFER_SIZE
12691320 wbufsize = DEFAULT_BUFFER_SIZE
12701321 RequestHandlerClass = HTTPRequest
1271
1322
12721323 def __init__(self, server, sock, makefile=CP_fileobject):
12731324 self.server = server
12741325 self.socket = sock
1275 self.rfile = makefile(sock, "rb", self.rbufsize)
1276 self.wfile = makefile(sock, "wb", self.wbufsize)
1326 self.rfile = makefile(sock._sock, "rb", self.rbufsize)
1327 self.wfile = makefile(sock._sock, "wb", self.wbufsize)
12771328 self.requests_seen = 0
1278
1329
12791330 def communicate(self):
12801331 """Read each request and respond appropriately."""
12811332 request_seen = False
12861337 # get written to the previous request.
12871338 req = None
12881339 req = self.RequestHandlerClass(self.server, self)
1289
1340
12901341 # This order of operations should guarantee correct pipelining.
12911342 req.parse_request()
12921343 if self.server.stats['Enabled']:
12961347 # probably already made a simple_response). Return and
12971348 # let the conn close.
12981349 return
1299
1350
13001351 request_seen = True
13011352 req.respond()
13021353 if req.close_connection:
13051356 e = sys.exc_info()[1]
13061357 errnum = e.args[0]
13071358 # sadly SSL sockets return a different (longer) time out string
1308 if errnum == 'timed out' or errnum == 'The read operation timed out':
1359 if (
1360 errnum == 'timed out' or
1361 errnum == 'The read operation timed out'
1362 ):
13091363 # Don't error if we're between requests; only error
13101364 # if 1) no request has been started at all, or 2) we're
13111365 # in the middle of a request.
1312 # See http://www.cherrypy.org/ticket/853
1366 # See https://bitbucket.org/cherrypy/cherrypy/issue/853
13131367 if (not request_seen) or (req and req.started_request):
13141368 # Don't bother writing the 408 if the response
13151369 # has already started being written.
13371391 except NoSSLError:
13381392 if req and not req.sent_headers:
13391393 # Unwrap our wfile
1340 self.wfile = CP_fileobject(self.socket._sock, "wb", self.wbufsize)
1341 req.simple_response("400 Bad Request",
1394 self.wfile = CP_fileobject(
1395 self.socket._sock, "wb", self.wbufsize)
1396 req.simple_response(
1397 "400 Bad Request",
13421398 "The client sent a plain HTTP request, but "
13431399 "this server only speaks HTTPS on this port.")
13441400 self.linger = True
13511407 except FatalSSLAlert:
13521408 # Close the connection.
13531409 return
1354
1410
13551411 linger = False
1356
1412
13571413 def close(self):
13581414 """Close the socket underlying this connection."""
13591415 self.rfile.close()
1360
1416
13611417 if not self.linger:
1362 # Python's socket module does NOT call close on the kernel socket
1363 # when you call socket.close(). We do so manually here because we
1364 # want this server to send a FIN TCP segment immediately. Note this
1365 # must be called *before* calling socket.close(), because the latter
1366 # drops its reference to the kernel socket.
1418 # Python's socket module does NOT call close on the kernel
1419 # socket when you call socket.close(). We do so manually here
1420 # because we want this server to send a FIN TCP segment
1421 # immediately. Note this must be called *before* calling
1422 # socket.close(), because the latter drops its reference to
1423 # the kernel socket.
13671424 if hasattr(self.socket, '_sock'):
13681425 self.socket._sock.close()
13691426 self.socket.close()
13781435
13791436
13801437 class TrueyZero(object):
1381 """An object which equals and does math like the integer '0' but evals True."""
1438
1439 """An object which equals and does math like the integer 0 but evals True.
1440 """
1441
13821442 def __add__(self, other):
13831443 return other
1444
13841445 def __radd__(self, other):
13851446 return other
13861447 trueyzero = TrueyZero()
13881449
13891450 _SHUTDOWNREQUEST = None
13901451
1452
13911453 class WorkerThread(threading.Thread):
1454
13921455 """Thread which continuously polls a Queue for Connection objects.
1393
1456
13941457 Due to the timing issues of polling a Queue, a WorkerThread does not
13951458 check its own 'ready' flag after it has started. To stop the thread,
13961459 it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
13971460 (one for each running WorkerThread).
13981461 """
1399
1462
14001463 conn = None
14011464 """The current connection pulled off the Queue, or None."""
1402
1465
14031466 server = None
14041467 """The HTTP Server which spawned this thread, and which owns the
14051468 Queue and is placing active connections into it."""
1406
1469
14071470 ready = False
14081471 """A simple flag for the calling server to know when this thread
14091472 has begun polling the Queue."""
1410
1411
1473
14121474 def __init__(self, server):
14131475 self.ready = False
14141476 self.server = server
1415
1477
14161478 self.requests_seen = 0
14171479 self.bytes_read = 0
14181480 self.bytes_written = 0
14191481 self.start_time = None
14201482 self.work_time = 0
14211483 self.stats = {
1422 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and trueyzero or self.conn.requests_seen),
1423 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and trueyzero or self.conn.rfile.bytes_read),
1424 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and trueyzero or self.conn.wfile.bytes_written),
1425 'Work Time': lambda s: self.work_time + ((self.start_time is None) and trueyzero or time.time() - self.start_time),
1426 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6),
1427 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6),
1484 'Requests': lambda s: self.requests_seen + (
1485 (self.start_time is None) and
1486 trueyzero or
1487 self.conn.requests_seen
1488 ),
1489 'Bytes Read': lambda s: self.bytes_read + (
1490 (self.start_time is None) and
1491 trueyzero or
1492 self.conn.rfile.bytes_read
1493 ),
1494 'Bytes Written': lambda s: self.bytes_written + (
1495 (self.start_time is None) and
1496 trueyzero or
1497 self.conn.wfile.bytes_written
1498 ),
1499 'Work Time': lambda s: self.work_time + (
1500 (self.start_time is None) and
1501 trueyzero or
1502 time.time() - self.start_time
1503 ),
1504 'Read Throughput': lambda s: s['Bytes Read'](s) / (
1505 s['Work Time'](s) or 1e-6),
1506 'Write Throughput': lambda s: s['Bytes Written'](s) / (
1507 s['Work Time'](s) or 1e-6),
14281508 }
14291509 threading.Thread.__init__(self)
1430
1510
14311511 def run(self):
14321512 self.server.stats['Worker Threads'][self.getName()] = self.stats
14331513 try:
14361516 conn = self.server.requests.get()
14371517 if conn is _SHUTDOWNREQUEST:
14381518 return
1439
1519
14401520 self.conn = conn
14411521 if self.server.stats['Enabled']:
14421522 self.start_time = time.time()
14571537
14581538
14591539 class ThreadPool(object):
1540
14601541 """A Request Queue for an HTTPServer which pools threads.
1461
1542
14621543 ThreadPool objects must provide min, get(), put(obj), start()
14631544 and stop(timeout) attributes.
14641545 """
1465
1546
14661547 def __init__(self, server, min=10, max=-1):
14671548 self.server = server
14681549 self.min = min
14701551 self._threads = []
14711552 self._queue = queue.Queue()
14721553 self.get = self._queue.get
1473
1554
14741555 def start(self):
14751556 """Start the pool of threads."""
14761557 for i in range(self.min):
14811562 for worker in self._threads:
14821563 while not worker.ready:
14831564 time.sleep(.1)
1484
1565
14851566 def _get_idle(self):
14861567 """Number of worker threads which are idle. Read-only."""
14871568 return len([t for t in self._threads if t.conn is None])
14881569 idle = property(_get_idle, doc=_get_idle.__doc__)
1489
1570
14901571 def put(self, obj):
14911572 self._queue.put(obj)
14921573 if obj is _SHUTDOWNREQUEST:
14931574 return
1494
1575
14951576 def grow(self, amount):
14961577 """Spawn new worker threads (not above self.max)."""
1497 for i in range(amount):
1498 if self.max > 0 and len(self._threads) >= self.max:
1499 break
1500 worker = WorkerThread(self.server)
1501 worker.setName("CP Server " + worker.getName())
1502 self._threads.append(worker)
1503 worker.start()
1504
1578 if self.max > 0:
1579 budget = max(self.max - len(self._threads), 0)
1580 else:
1581 # self.max <= 0 indicates no maximum
1582 budget = float('inf')
1583
1584 n_new = min(amount, budget)
1585
1586 workers = [self._spawn_worker() for i in range(n_new)]
1587 while not self._all(operator.attrgetter('ready'), workers):
1588 time.sleep(.1)
1589 self._threads.extend(workers)
1590
1591 def _spawn_worker(self):
1592 worker = WorkerThread(self.server)
1593 worker.setName("CP Server " + worker.getName())
1594 worker.start()
1595 return worker
1596
1597 def _all(func, items):
1598 results = [func(item) for item in items]
1599 return reduce(operator.and_, results, True)
1600 _all = staticmethod(_all)
1601
15051602 def shrink(self, amount):
15061603 """Kill off worker threads (not below self.min)."""
15071604 # Grow/shrink the pool if necessary.
15101607 if not t.isAlive():
15111608 self._threads.remove(t)
15121609 amount -= 1
1513
1514 if amount > 0:
1515 for i in range(min(amount, len(self._threads) - self.min)):
1516 # Put a number of shutdown requests on the queue equal
1517 # to 'amount'. Once each of those is processed by a worker,
1518 # that worker will terminate and be culled from our list
1519 # in self.put.
1520 self._queue.put(_SHUTDOWNREQUEST)
1521
1610
1611 # calculate the number of threads above the minimum
1612 n_extra = max(len(self._threads) - self.min, 0)
1613
1614 # don't remove more than amount
1615 n_to_remove = min(amount, n_extra)
1616
1617 # put shutdown requests on the queue equal to the number of threads
1618 # to remove. As each request is processed by a worker, that worker
1619 # will terminate and be culled from the list.
1620 for n in range(n_to_remove):
1621 self._queue.put(_SHUTDOWNREQUEST)
1622
15221623 def stop(self, timeout=5):
15231624 # Must shut down threads here so the code that calls
15241625 # this method can know when all threads are stopped.
15251626 for worker in self._threads:
15261627 self._queue.put(_SHUTDOWNREQUEST)
1527
1628
15281629 # Don't join currentThread (when stop is called inside a request).
15291630 current = threading.currentThread()
15301631 if timeout and timeout >= 0:
15521653 worker.join()
15531654 except (AssertionError,
15541655 # Ignore repeated Ctrl-C.
1555 # See http://www.cherrypy.org/ticket/691.
1656 # See
1657 # https://bitbucket.org/cherrypy/cherrypy/issue/691.
15561658 KeyboardInterrupt):
15571659 pass
1558
1660
15591661 def _get_qsize(self):
15601662 return self._queue.qsize()
15611663 qsize = property(_get_qsize)
1562
15631664
15641665
15651666 try:
15671668 except ImportError:
15681669 try:
15691670 from ctypes import windll, WinError
1671 import ctypes.wintypes
1672 _SetHandleInformation = windll.kernel32.SetHandleInformation
1673 _SetHandleInformation.argtypes = [
1674 ctypes.wintypes.HANDLE,
1675 ctypes.wintypes.DWORD,
1676 ctypes.wintypes.DWORD,
1677 ]
1678 _SetHandleInformation.restype = ctypes.wintypes.BOOL
15701679 except ImportError:
15711680 def prevent_socket_inheritance(sock):
15721681 """Dummy function, since neither fcntl nor ctypes are available."""
15741683 else:
15751684 def prevent_socket_inheritance(sock):
15761685 """Mark the given socket fd as non-inheritable (Windows)."""
1577 if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
1686 if not _SetHandleInformation(sock.fileno(), 1, 0):
15781687 raise WinError()
15791688 else:
15801689 def prevent_socket_inheritance(sock):
15851694
15861695
15871696 class SSLAdapter(object):
1697
15881698 """Base class for SSL driver library adapters.
1589
1699
15901700 Required methods:
1591
1701
15921702 * ``wrap(sock) -> (wrapped socket, ssl environ dict)``
1593 * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object``
1703 * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
1704 socket file object``
15941705 """
1595
1706
15961707 def __init__(self, certificate, private_key, certificate_chain=None):
15971708 self.certificate = certificate
15981709 self.private_key = private_key
15991710 self.certificate_chain = certificate_chain
1600
1711
16011712 def wrap(self, sock):
16021713 raise NotImplemented
1603
1714
16041715 def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
16051716 raise NotImplemented
16061717
16071718
16081719 class HTTPServer(object):
1720
16091721 """An HTTP server."""
1610
1722
16111723 _bind_addr = "127.0.0.1"
16121724 _interrupt = None
1613
1725
16141726 gateway = None
16151727 """A Gateway instance."""
1616
1728
16171729 minthreads = None
16181730 """The minimum number of worker threads to create (default 10)."""
1619
1731
16201732 maxthreads = None
1621 """The maximum number of worker threads to create (default -1 = no limit)."""
1622
1733 """The maximum number of worker threads to create (default -1 = no limit).
1734 """
1735
16231736 server_name = None
16241737 """The name of the server; defaults to socket.gethostname()."""
1625
1738
16261739 protocol = "HTTP/1.1"
16271740 """The version string to write in the Status-Line of all HTTP responses.
1628
1741
16291742 For example, "HTTP/1.1" is the default. This also limits the supported
16301743 features used in the response."""
1631
1744
16321745 request_queue_size = 5
1633 """The 'backlog' arg to socket.listen(); max queued connections (default 5)."""
1634
1746 """The 'backlog' arg to socket.listen(); max queued connections
1747 (default 5).
1748 """
1749
16351750 shutdown_timeout = 5
1636 """The total time, in seconds, to wait for worker threads to cleanly exit."""
1637
1751 """The total time, in seconds, to wait for worker threads to cleanly exit.
1752 """
1753
16381754 timeout = 10
16391755 """The timeout in seconds for accepted connections (default 10)."""
1640
1641 version = "CherryPy/3.2.2"
1756
1757 version = "CherryPy/3.3.0"
16421758 """A version string for the HTTPServer."""
1643
1759
16441760 software = None
16451761 """The value to set for the SERVER_SOFTWARE entry in the WSGI environ.
1646
1762
16471763 If None, this defaults to ``'%s Server' % self.version``."""
1648
1764
16491765 ready = False
1650 """An internal flag which marks whether the socket is accepting connections."""
1651
1766 """An internal flag which marks whether the socket is accepting connections
1767 """
1768
16521769 max_request_header_size = 0
16531770 """The maximum size, in bytes, for request headers, or 0 for no limit."""
1654
1771
16551772 max_request_body_size = 0
16561773 """The maximum size, in bytes, for request bodies, or 0 for no limit."""
1657
1774
16581775 nodelay = True
16591776 """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
1660
1777
16611778 ConnectionClass = HTTPConnection
16621779 """The class to use for handling HTTP connections."""
1663
1780
16641781 ssl_adapter = None
16651782 """An instance of SSLAdapter (or a subclass).
1666
1783
16671784 You must have the corresponding SSL driver library installed."""
1668
1785
16691786 def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1,
16701787 server_name=None):
16711788 self.bind_addr = bind_addr
16721789 self.gateway = gateway
1673
1790
16741791 self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads)
1675
1792
16761793 if not server_name:
16771794 server_name = socket.gethostname()
16781795 self.server_name = server_name
16791796 self.clear_stats()
1680
1797
16811798 def clear_stats(self):
16821799 self._start_time = None
16831800 self._run_time = 0
16911808 'Threads': lambda s: len(getattr(self.requests, "_threads", [])),
16921809 'Threads Idle': lambda s: getattr(self.requests, "idle", None),
16931810 'Socket Errors': 0,
1694 'Requests': lambda s: (not s['Enabled']) and -1 or sum([w['Requests'](w) for w
1695 in s['Worker Threads'].values()], 0),
1696 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Read'](w) for w
1697 in s['Worker Threads'].values()], 0),
1698 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Written'](w) for w
1699 in s['Worker Threads'].values()], 0),
1700 'Work Time': lambda s: (not s['Enabled']) and -1 or sum([w['Work Time'](w) for w
1701 in s['Worker Threads'].values()], 0),
1811 'Requests': lambda s: (not s['Enabled']) and -1 or sum(
1812 [w['Requests'](w) for w in s['Worker Threads'].values()], 0),
1813 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum(
1814 [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0),
1815 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum(
1816 [w['Bytes Written'](w) for w in s['Worker Threads'].values()],
1817 0),
1818 'Work Time': lambda s: (not s['Enabled']) and -1 or sum(
1819 [w['Work Time'](w) for w in s['Worker Threads'].values()], 0),
17021820 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum(
17031821 [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
17041822 for w in s['Worker Threads'].values()], 0),
17061824 [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
17071825 for w in s['Worker Threads'].values()], 0),
17081826 'Worker Threads': {},
1709 }
1827 }
17101828 logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats
1711
1829
17121830 def runtime(self):
17131831 if self._start_time is None:
17141832 return self._run_time
17151833 else:
17161834 return self._run_time + (time.time() - self._start_time)
1717
1835
17181836 def __str__(self):
17191837 return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
17201838 self.bind_addr)
1721
1839
17221840 def _get_bind_addr(self):
17231841 return self._bind_addr
1842
17241843 def _set_bind_addr(self, value):
17251844 if isinstance(value, tuple) and value[0] in ('', None):
17261845 # Despite the socket module docs, using '' does not
17371856 "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
17381857 "to listen on all active interfaces.")
17391858 self._bind_addr = value
1740 bind_addr = property(_get_bind_addr, _set_bind_addr,
1859 bind_addr = property(
1860 _get_bind_addr,
1861 _set_bind_addr,
17411862 doc="""The interface on which to listen for connections.
1742
1863
17431864 For TCP sockets, a (host, port) tuple. Host values may be any IPv4
17441865 or IPv6 address, or any valid hostname. The string 'localhost' is a
17451866 synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
17461867 The string '0.0.0.0' is a special IPv4 entry meaning "any active
17471868 interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
17481869 IPv6. The empty string or None are not allowed.
1749
1870
17501871 For UNIX sockets, supply the filename as a string.""")
1751
1872
17521873 def start(self):
17531874 """Run the server forever."""
17541875 # We don't have to trap KeyboardInterrupt or SystemExit here,
17561877 # If you're using this server with another framework, you should
17571878 # trap those exceptions in whatever code block calls start().
17581879 self._interrupt = None
1759
1880
17601881 if self.software is None:
17611882 self.software = "%s Server" % self.version
1762
1883
17631884 # SSL backward compatibility
17641885 if (self.ssl_adapter is None and
1765 getattr(self, 'ssl_certificate', None) and
1766 getattr(self, 'ssl_private_key', None)):
1886 getattr(self, 'ssl_certificate', None) and
1887 getattr(self, 'ssl_private_key', None)):
17671888 warnings.warn(
1768 "SSL attributes are deprecated in CherryPy 3.2, and will "
1769 "be removed in CherryPy 3.3. Use an ssl_adapter attribute "
1770 "instead.",
1771 DeprecationWarning
1772 )
1889 "SSL attributes are deprecated in CherryPy 3.2, and will "
1890 "be removed in CherryPy 3.3. Use an ssl_adapter attribute "
1891 "instead.",
1892 DeprecationWarning
1893 )
17731894 try:
17741895 from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
17751896 except ImportError:
17781899 self.ssl_adapter = pyOpenSSLAdapter(
17791900 self.ssl_certificate, self.ssl_private_key,
17801901 getattr(self, 'ssl_certificate_chain', None))
1781
1902
17821903 # Select the appropriate socket
17831904 if isinstance(self.bind_addr, basestring):
17841905 # AF_UNIX socket
1785
1906
17861907 # So we can reuse the socket...
1787 try: os.unlink(self.bind_addr)
1788 except: pass
1789
1908 try:
1909 os.unlink(self.bind_addr)
1910 except:
1911 pass
1912
17901913 # So everyone can access the socket...
1791 try: os.chmod(self.bind_addr, 511) # 0777
1792 except: pass
1793
1794 info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
1914 try:
1915 os.chmod(self.bind_addr, 511) # 0777
1916 except:
1917 pass
1918
1919 info = [
1920 (socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
17951921 else:
17961922 # AF_INET or AF_INET6 socket
1797 # Get the correct address family for our host (allows IPv6 addresses)
1923 # Get the correct address family for our host (allows IPv6
1924 # addresses)
17981925 host, port = self.bind_addr
17991926 try:
1800 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
1801 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
1927 info = socket.getaddrinfo(
1928 host, port, socket.AF_UNSPEC,
1929 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
18021930 except socket.gaierror:
18031931 if ':' in self.bind_addr[0]:
18041932 info = [(socket.AF_INET6, socket.SOCK_STREAM,
18061934 else:
18071935 info = [(socket.AF_INET, socket.SOCK_STREAM,
18081936 0, "", self.bind_addr)]
1809
1937
18101938 self.socket = None
18111939 msg = "No socket could be created"
18121940 for res in info:
18131941 af, socktype, proto, canonname, sa = res
18141942 try:
18151943 self.bind(af, socktype, proto)
1816 except socket.error:
1944 except socket.error, serr:
1945 msg = "%s -- (%s: %s)" % (msg, sa, serr)
18171946 if self.socket:
18181947 self.socket.close()
18191948 self.socket = None
18211950 break
18221951 if not self.socket:
18231952 raise socket.error(msg)
1824
1953
18251954 # Timeout so KeyboardInterrupt can be caught on Win32
18261955 self.socket.settimeout(1)
18271956 self.socket.listen(self.request_queue_size)
1828
1957
18291958 # Create worker threads
18301959 self.requests.start()
1831
1960
18321961 self.ready = True
18331962 self._start_time = time.time()
18341963 while self.ready:
18391968 except:
18401969 self.error_log("Error in HTTPServer.tick", level=logging.ERROR,
18411970 traceback=True)
1842
1971
18431972 if self.interrupt:
18441973 while self.interrupt is True:
18451974 # Wait for self.stop() to complete. See _set_interrupt.
18631992 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
18641993 if self.nodelay and not isinstance(self.bind_addr, str):
18651994 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1866
1995
18671996 if self.ssl_adapter is not None:
18681997 self.socket = self.ssl_adapter.bind(self.socket)
1869
1998
18701999 # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
1871 # activate dual-stack. See http://www.cherrypy.org/ticket/871.
2000 # activate dual-stack. See
2001 # https://bitbucket.org/cherrypy/cherrypy/issue/871.
18722002 if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6
1873 and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
2003 and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
18742004 try:
1875 self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
2005 self.socket.setsockopt(
2006 socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
18762007 except (AttributeError, socket.error):
18772008 # Apparently, the socket option is not available in
18782009 # this machine's TCP stack
18792010 pass
1880
2011
18812012 self.socket.bind(self.bind_addr)
1882
2013
18832014 def tick(self):
18842015 """Accept a new connection and put it on the Queue."""
18852016 try:
18882019 self.stats['Accepts'] += 1
18892020 if not self.ready:
18902021 return
1891
2022
18922023 prevent_socket_inheritance(s)
18932024 if hasattr(s, 'settimeout'):
18942025 s.settimeout(self.timeout)
1895
2026
18962027 makefile = CP_fileobject
18972028 ssl_env = {}
18982029 # if ssl cert and key are set, we try to be a secure HTTP server
19062037 "Content-Length: %s\r\n" % len(msg),
19072038 "Content-Type: text/plain\r\n\r\n",
19082039 msg]
1909
1910 wfile = makefile(s, "wb", DEFAULT_BUFFER_SIZE)
2040
2041 wfile = makefile(s._sock, "wb", DEFAULT_BUFFER_SIZE)
19112042 try:
19122043 wfile.sendall("".join(buf))
19132044 except socket.error:
19212052 # Re-apply our timeout since we may have a new socket object
19222053 if hasattr(s, 'settimeout'):
19232054 s.settimeout(self.timeout)
1924
2055
19252056 conn = self.ConnectionClass(self, s, makefile)
1926
2057
19272058 if not isinstance(self.bind_addr, basestring):
19282059 # optional values
19292060 # Until we do DNS lookups, omit REMOTE_HOST
1930 if addr is None: # sometimes this can happen
2061 if addr is None: # sometimes this can happen
19312062 # figure out if AF_INET or AF_INET6.
19322063 if len(s.getsockname()) == 2:
19332064 # AF_INET
19372068 addr = ('::', 0)
19382069 conn.remote_addr = addr[0]
19392070 conn.remote_port = addr[1]
1940
2071
19412072 conn.ssl_env = ssl_env
1942
2073
19432074 self.requests.put(conn)
19442075 except socket.timeout:
19452076 # The only reason for the timeout in start() is so we can
19552086 # is received during the accept() call; all docs say retry
19562087 # the call, and I *think* I'm reading it right that Python
19572088 # will then go ahead and poll for and handle the signal
1958 # elsewhere. See http://www.cherrypy.org/ticket/707.
2089 # elsewhere. See
2090 # https://bitbucket.org/cherrypy/cherrypy/issue/707.
19592091 return
19602092 if x.args[0] in socket_errors_nonblocking:
1961 # Just try again. See http://www.cherrypy.org/ticket/479.
2093 # Just try again. See
2094 # https://bitbucket.org/cherrypy/cherrypy/issue/479.
19622095 return
19632096 if x.args[0] in socket_errors_to_ignore:
19642097 # Our socket was closed.
1965 # See http://www.cherrypy.org/ticket/686.
2098 # See https://bitbucket.org/cherrypy/cherrypy/issue/686.
19662099 return
19672100 raise
1968
2101
19692102 def _get_interrupt(self):
19702103 return self._interrupt
2104
19712105 def _set_interrupt(self, interrupt):
19722106 self._interrupt = True
19732107 self.stop()
19752109 interrupt = property(_get_interrupt, _set_interrupt,
19762110 doc="Set this to an Exception instance to "
19772111 "interrupt the server.")
1978
2112
19792113 def stop(self):
19802114 """Gracefully shutdown a server that is serving forever."""
19812115 self.ready = False
19822116 if self._start_time is not None:
19832117 self._run_time += (time.time() - self._start_time)
19842118 self._start_time = None
1985
2119
19862120 sock = getattr(self, "socket", None)
19872121 if sock:
19882122 if not isinstance(self.bind_addr, basestring):
19932127 x = sys.exc_info()[1]
19942128 if x.args[0] not in socket_errors_to_ignore:
19952129 # Changed to use error code and not message
1996 # See http://www.cherrypy.org/ticket/860.
2130 # See
2131 # https://bitbucket.org/cherrypy/cherrypy/issue/860.
19972132 raise
19982133 else:
19992134 # Note that we're explicitly NOT using AI_PASSIVE,
20062141 s = None
20072142 try:
20082143 s = socket.socket(af, socktype, proto)
2009 # See http://groups.google.com/group/cherrypy-users/
2010 # browse_frm/thread/bbfe5eb39c904fe0
2144 # See
2145 # http://groups.google.com/group/cherrypy-users/
2146 # browse_frm/thread/bbfe5eb39c904fe0
20112147 s.settimeout(1.0)
20122148 s.connect((host, port))
20132149 s.close()
20172153 if hasattr(sock, "close"):
20182154 sock.close()
20192155 self.socket = None
2020
2156
20212157 self.requests.stop(self.shutdown_timeout)
20222158
20232159
20242160 class Gateway(object):
2025 """A base class to interface HTTPServer with other systems, such as WSGI."""
2026
2161
2162 """A base class to interface HTTPServer with other systems, such as WSGI.
2163 """
2164
20272165 def __init__(self, req):
20282166 self.req = req
2029
2167
20302168 def respond(self):
20312169 """Process the current request. Must be overridden in a subclass."""
20322170 raise NotImplemented
20372175 ssl_adapters = {
20382176 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
20392177 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
2040 }
2178 }
2179
20412180
20422181 def get_ssl_adapter_class(name='pyopenssl'):
20432182 """Return an SSL adapter class for the given name."""
20462185 last_dot = adapter.rfind(".")
20472186 attr_name = adapter[last_dot + 1:]
20482187 mod_path = adapter[:last_dot]
2049
2188
20502189 try:
20512190 mod = sys.modules[mod_path]
20522191 if mod is None:
20542193 except KeyError:
20552194 # The last [''] is important.
20562195 mod = __import__(mod_path, globals(), locals(), [''])
2057
2196
20582197 # Let an AttributeError propagate outward.
20592198 try:
20602199 adapter = getattr(mod, attr_name)
20612200 except AttributeError:
20622201 raise AttributeError("'%s' object has no attribute '%s'"
20632202 % (mod_path, attr_name))
2064
2203
20652204 return adapter
20662205
2067 # -------------------------------- WSGI Stuff -------------------------------- #
2206 # ------------------------------- WSGI Stuff -------------------------------- #
20682207
20692208
20702209 class CherryPyWSGIServer(HTTPServer):
2210
20712211 """A subclass of HTTPServer which calls a WSGI application."""
2072
2212
20732213 wsgi_version = (1, 0)
20742214 """The version of WSGI to produce."""
2075
2215
20762216 def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
20772217 max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
20782218 self.requests = ThreadPool(self, min=numthreads or 1, max=max)
20792219 self.wsgi_app = wsgi_app
20802220 self.gateway = wsgi_gateways[self.wsgi_version]
2081
2221
20822222 self.bind_addr = bind_addr
20832223 if not server_name:
20842224 server_name = socket.gethostname()
20852225 self.server_name = server_name
20862226 self.request_queue_size = request_queue_size
2087
2227
20882228 self.timeout = timeout
20892229 self.shutdown_timeout = shutdown_timeout
20902230 self.clear_stats()
2091
2231
20922232 def _get_numthreads(self):
20932233 return self.requests.min
2234
20942235 def _set_numthreads(self, value):
20952236 self.requests.min = value
20962237 numthreads = property(_get_numthreads, _set_numthreads)
20972238
20982239
20992240 class WSGIGateway(Gateway):
2241
21002242 """A base class to interface HTTPServer with WSGI."""
2101
2243
21022244 def __init__(self, req):
21032245 self.req = req
21042246 self.started_response = False
21052247 self.env = self.get_environ()
21062248 self.remaining_bytes_out = None
2107
2249
21082250 def get_environ(self):
21092251 """Return a new environ dict targeting the given wsgi.version"""
21102252 raise NotImplemented
2111
2253
21122254 def respond(self):
21132255 """Process the current request."""
21142256 response = self.req.server.wsgi_app(self.env, self.start_response)
21272269 finally:
21282270 if hasattr(response, "close"):
21292271 response.close()
2130
2131 def start_response(self, status, headers, exc_info = None):
2272
2273 def start_response(self, status, headers, exc_info=None):
21322274 """WSGI callable to begin the HTTP response."""
21332275 # "The application may call start_response more than once,
21342276 # if and only if the exc_info argument is provided."
21362278 raise AssertionError("WSGI start_response called a second "
21372279 "time with no exc_info.")
21382280 self.started_response = True
2139
2281
21402282 # "if exc_info is provided, and the HTTP headers have already been
21412283 # sent, start_response must raise an error, and should raise the
21422284 # exc_info tuple."
21452287 raise exc_info[0], exc_info[1], exc_info[2]
21462288 finally:
21472289 exc_info = None
2148
2290
21492291 self.req.status = status
21502292 for k, v in headers:
21512293 if not isinstance(k, str):
2152 raise TypeError("WSGI response header key %r is not of type str." % k)
2294 raise TypeError(
2295 "WSGI response header key %r is not of type str." % k)
21532296 if not isinstance(v, str):
2154 raise TypeError("WSGI response header value %r is not of type str." % v)
2297 raise TypeError(
2298 "WSGI response header value %r is not of type str." % v)
21552299 if k.lower() == 'content-length':
21562300 self.remaining_bytes_out = int(v)
21572301 self.req.outheaders.extend(headers)
2158
2302
21592303 return self.write
2160
2304
21612305 def write(self, chunk):
21622306 """WSGI callable to write unbuffered data to the client.
2163
2307
21642308 This method is also used internally by start_response (to write
21652309 data from the iterable returned by the WSGI application).
21662310 """
21672311 if not self.started_response:
21682312 raise AssertionError("WSGI write called before start_response.")
2169
2313
21702314 chunklen = len(chunk)
21712315 rbo = self.remaining_bytes_out
21722316 if rbo is not None and chunklen > rbo:
21732317 if not self.req.sent_headers:
21742318 # Whew. We can send a 500 to the client.
2175 self.req.simple_response("500 Internal Server Error",
2319 self.req.simple_response(
2320 "500 Internal Server Error",
21762321 "The requested resource returned more bytes than the "
21772322 "declared Content-Length.")
21782323 else:
21792324 # Dang. We have probably already sent data. Truncate the chunk
21802325 # to fit (so the client doesn't hang) and raise an error later.
21812326 chunk = chunk[:rbo]
2182
2327
21832328 if not self.req.sent_headers:
21842329 self.req.sent_headers = True
21852330 self.req.send_headers()
2186
2331
21872332 self.req.write(chunk)
2188
2333
21892334 if rbo is not None:
21902335 rbo -= chunklen
21912336 if rbo < 0:
21942339
21952340
21962341 class WSGIGateway_10(WSGIGateway):
2342
21972343 """A Gateway class to interface HTTPServer with WSGI 1.0.x."""
2198
2344
21992345 def get_environ(self):
22002346 """Return a new environ dict targeting the given wsgi.version"""
22012347 req = self.req
22222368 'wsgi.run_once': False,
22232369 'wsgi.url_scheme': req.scheme,
22242370 'wsgi.version': (1, 0),
2225 }
2226
2371 }
2372
22272373 if isinstance(req.server.bind_addr, basestring):
22282374 # AF_UNIX. This isn't really allowed by WSGI, which doesn't
22292375 # address unix domain sockets. But it's better than nothing.
22302376 env["SERVER_PORT"] = ""
22312377 else:
22322378 env["SERVER_PORT"] = str(req.server.bind_addr[1])
2233
2379
22342380 # Request headers
22352381 for k, v in req.inheaders.iteritems():
22362382 env["HTTP_" + k.upper().replace("-", "_")] = v
2237
2383
22382384 # CONTENT_TYPE/CONTENT_LENGTH
22392385 ct = env.pop("HTTP_CONTENT_TYPE", None)
22402386 if ct is not None:
22422388 cl = env.pop("HTTP_CONTENT_LENGTH", None)
22432389 if cl is not None:
22442390 env["CONTENT_LENGTH"] = cl
2245
2391
22462392 if req.conn.ssl_env:
22472393 env.update(req.conn.ssl_env)
2248
2394
22492395 return env
22502396
22512397
22522398 class WSGIGateway_u0(WSGIGateway_10):
2399
22532400 """A Gateway class to interface HTTPServer with WSGI u.0.
2254
2255 WSGI u.0 is an experimental protocol, which uses unicode for keys and values
2256 in both Python 2 and Python 3.
2401
2402 WSGI u.0 is an experimental protocol, which uses unicode for keys and
2403 values in both Python 2 and Python 3.
22572404 """
2258
2405
22592406 def get_environ(self):
22602407 """Return a new environ dict targeting the given wsgi.version"""
22612408 req = self.req
22622409 env_10 = WSGIGateway_10.get_environ(self)
2263 env = dict([(k.decode('ISO-8859-1'), v) for k, v in env_10.iteritems()])
2410 env = dict([(k.decode('ISO-8859-1'), v)
2411 for k, v in env_10.iteritems()])
22642412 env[u'wsgi.version'] = ('u', 0)
2265
2413
22662414 # Request-URI
22672415 env.setdefault(u'wsgi.url_encoding', u'utf-8')
22682416 try:
22732421 env[u'wsgi.url_encoding'] = u'ISO-8859-1'
22742422 for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
22752423 env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding'])
2276
2424
22772425 for k, v in sorted(env.items()):
22782426 if isinstance(v, str) and k not in ('REQUEST_URI', 'wsgi.input'):
22792427 env[k] = v.decode('ISO-8859-1')
2280
2428
22812429 return env
22822430
22832431 wsgi_gateways = {
22852433 ('u', 0): WSGIGateway_u0,
22862434 }
22872435
2436
22882437 class WSGIPathInfoDispatcher(object):
2438
22892439 """A WSGI dispatcher for dispatch based on the PATH_INFO.
2290
2440
22912441 apps: a dict or list of (path_prefix, app) pairs.
22922442 """
2293
2443
22942444 def __init__(self, apps):
22952445 try:
22962446 apps = list(apps.items())
22972447 except AttributeError:
22982448 pass
2299
2449
23002450 # Sort the apps by len(path), descending
2301 apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0])))
2451 apps.sort(cmp=lambda x, y: cmp(len(x[0]), len(y[0])))
23022452 apps.reverse()
2303
2453
23042454 # The path_prefix strings must start, but not end, with a slash.
23052455 # Use "" instead of "/".
23062456 self.apps = [(p.rstrip("/"), a) for p, a in apps]
2307
2457
23082458 def __call__(self, environ, start_response):
23092459 path = environ["PATH_INFO"] or "/"
23102460 for p, app in self.apps:
23142464 environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
23152465 environ["PATH_INFO"] = path[len(p):]
23162466 return app(environ, start_response)
2317
2467
23182468 start_response('404 Not Found', [('Content-Type', 'text/plain'),
23192469 ('Content-Length', '0')])
23202470 return ['']
2321
33 (without using CherryPy's application machinery)::
44
55 from cherrypy import wsgiserver
6
6
77 def my_crazy_app(environ, start_response):
88 status = '200 OK'
99 response_headers = [('Content-type','text/plain')]
1010 start_response(status, response_headers)
1111 return ['Hello world!']
12
12
1313 server = wsgiserver.CherryPyWSGIServer(
1414 ('0.0.0.0', 8070), my_crazy_app,
1515 server_name='www.cherrypy.example')
1616 server.start()
17
18 The CherryPy WSGI server can serve as many WSGI applications
17
18 The CherryPy WSGI server can serve as many WSGI applications
1919 as you want in one instance by using a WSGIPathInfoDispatcher::
20
20
2121 d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
2222 server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
23
23
2424 Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance.
2525
2626 This won't call the CherryPy engine (application side) at all, only the
8585 import email.utils
8686 import socket
8787 import sys
88 if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'):
89 socket.IPPROTO_IPV6 = 41
90 if sys.version_info < (3,1):
88 if 'win' in sys.platform and hasattr(socket, "AF_INET6"):
89 if not hasattr(socket, 'IPPROTO_IPV6'):
90 socket.IPPROTO_IPV6 = 41
91 if not hasattr(socket, 'IPV6_V6ONLY'):
92 socket.IPV6_V6ONLY = 27
93 if sys.version_info < (3, 1):
9194 import io
9295 else:
9396 import _pyio as io
9699 import threading
97100 import time
98101 from traceback import format_exc
99 from urllib.parse import unquote
100 from urllib.parse import urlparse
101 from urllib.parse import scheme_chars
102 import warnings
103102
104103 if sys.version_info >= (3, 0):
105104 bytestr = bytes
106105 unicodestr = str
107106 basestring = (bytes, str)
107
108108 def ntob(n, encoding='ISO-8859-1'):
109 """Return the given native string as a byte string in the given encoding."""
109 """Return the given native string as a byte string in the given
110 encoding.
111 """
110112 # In Python 3, the native string type is unicode
111113 return n.encode(encoding)
112114 else:
113115 bytestr = str
114116 unicodestr = unicode
115117 basestring = basestring
118
116119 def ntob(n, encoding='ISO-8859-1'):
117 """Return the given native string as a byte string in the given encoding."""
120 """Return the given native string as a byte string in the given
121 encoding.
122 """
118123 # In Python 2, the native string type is bytes. Assume it's already
119124 # in the given encoding, which for ISO-8859-1 is almost always what
120125 # was intended.
135140
136141 import errno
137142
143
138144 def plat_specific_errors(*errnames):
139145 """Return error numbers for all errors in errnames on this platform.
140
146
141147 The 'errno' module contains different global constants depending on
142148 the specific platform (OS). This function will return the list of
143149 numeric values for a given list of potential names.
159165 "ECONNABORTED", "WSAECONNABORTED",
160166 "ENETRESET", "WSAENETRESET",
161167 "EHOSTDOWN", "EHOSTUNREACH",
162 )
168 )
163169 socket_errors_to_ignore.append("timed out")
164170 socket_errors_to_ignore.append("The read operation timed out")
165171
166172 socket_errors_nonblocking = plat_specific_errors(
167173 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
168174
169 comma_separated_headers = [ntob(h) for h in
175 comma_separated_headers = [
176 ntob(h) for h in
170177 ['Accept', 'Accept-Charset', 'Accept-Encoding',
171178 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control',
172179 'Connection', 'Content-Encoding', 'Content-Language', 'Expect',
173180 'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE',
174181 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning',
175 'WWW-Authenticate']]
182 'WWW-Authenticate']
183 ]
176184
177185
178186 import logging
179 if not hasattr(logging, 'statistics'): logging.statistics = {}
187 if not hasattr(logging, 'statistics'):
188 logging.statistics = {}
180189
181190
182191 def read_headers(rfile, hdict=None):
183192 """Read headers from the given stream into the given header dict.
184
193
185194 If hdict is None, a new header dict is created. Returns the populated
186195 header dict.
187
196
188197 Headers which are repeated are folded together using a comma if their
189198 specification so dictates.
190
199
191200 This function raises ValueError when the read bytes violate the HTTP spec.
192201 You should probably return "400 Bad Request" if this happens.
193202 """
194203 if hdict is None:
195204 hdict = {}
196
205
197206 while True:
198207 line = rfile.readline()
199208 if not line:
200209 # No more data--illegal end of headers
201210 raise ValueError("Illegal end of headers.")
202
211
203212 if line == CRLF:
204213 # Normal end of headers
205214 break
206215 if not line.endswith(CRLF):
207216 raise ValueError("HTTP requires CRLF terminators")
208
217
209218 if line[0] in (SPACE, TAB):
210219 # It's a continuation line.
211220 v = line.strip()
218227 k = k.strip().title()
219228 v = v.strip()
220229 hname = k
221
230
222231 if k in comma_separated_headers:
223232 existing = hdict.get(hname)
224233 if existing:
225234 v = b", ".join((existing, v))
226235 hdict[hname] = v
227
236
228237 return hdict
229238
230239
231240 class MaxSizeExceeded(Exception):
232241 pass
233242
243
234244 class SizeCheckWrapper(object):
245
235246 """Wraps a file-like object, raising MaxSizeExceeded if too large."""
236
247
237248 def __init__(self, rfile, maxlen):
238249 self.rfile = rfile
239250 self.maxlen = maxlen
240251 self.bytes_read = 0
241
252
242253 def _check_length(self):
243254 if self.maxlen and self.bytes_read > self.maxlen:
244255 raise MaxSizeExceeded()
245
256
246257 def read(self, size=None):
247258 data = self.rfile.read(size)
248259 self.bytes_read += len(data)
249260 self._check_length()
250261 return data
251
262
252263 def readline(self, size=None):
253264 if size is not None:
254265 data = self.rfile.readline(size)
255266 self.bytes_read += len(data)
256267 self._check_length()
257268 return data
258
269
259270 # User didn't specify a size ...
260271 # We read the line in chunks to make sure it's not a 100MB line !
261272 res = []
264275 self.bytes_read += len(data)
265276 self._check_length()
266277 res.append(data)
267 # See http://www.cherrypy.org/ticket/421
268 if len(data) < 256 or data[-1:] == "\n":
278 # See https://bitbucket.org/cherrypy/cherrypy/issue/421
279 if len(data) < 256 or data[-1:] == LF:
269280 return EMPTY.join(res)
270
281
271282 def readlines(self, sizehint=0):
272283 # Shamelessly stolen from StringIO
273284 total = 0
280291 break
281292 line = self.readline()
282293 return lines
283
294
284295 def close(self):
285296 self.rfile.close()
286
297
287298 def __iter__(self):
288299 return self
289
300
290301 def __next__(self):
291302 data = next(self.rfile)
292303 self.bytes_read += len(data)
293304 self._check_length()
294305 return data
295
306
296307 def next(self):
297308 data = self.rfile.next()
298309 self.bytes_read += len(data)
301312
302313
303314 class KnownLengthRFile(object):
315
304316 """Wraps a file-like object, returning an empty string when exhausted."""
305
317
306318 def __init__(self, rfile, content_length):
307319 self.rfile = rfile
308320 self.remaining = content_length
309
321
310322 def read(self, size=None):
311323 if self.remaining == 0:
312324 return b''
314326 size = self.remaining
315327 else:
316328 size = min(size, self.remaining)
317
329
318330 data = self.rfile.read(size)
319331 self.remaining -= len(data)
320332 return data
321
333
322334 def readline(self, size=None):
323335 if self.remaining == 0:
324336 return b''
326338 size = self.remaining
327339 else:
328340 size = min(size, self.remaining)
329
341
330342 data = self.rfile.readline(size)
331343 self.remaining -= len(data)
332344 return data
333
345
334346 def readlines(self, sizehint=0):
335347 # Shamelessly stolen from StringIO
336348 total = 0
343355 break
344356 line = self.readline(sizehint)
345357 return lines
346
358
347359 def close(self):
348360 self.rfile.close()
349
361
350362 def __iter__(self):
351363 return self
352
364
353365 def __next__(self):
354366 data = next(self.rfile)
355367 self.remaining -= len(data)
357369
358370
359371 class ChunkedRFile(object):
372
360373 """Wraps a file-like object, returning an empty string when exhausted.
361
374
362375 This class is intended to provide a conforming wsgi.input value for
363376 request entities that have been encoded with the 'chunked' transfer
364377 encoding.
365378 """
366
379
367380 def __init__(self, rfile, maxlen, bufsize=8192):
368381 self.rfile = rfile
369382 self.maxlen = maxlen
371384 self.buffer = EMPTY
372385 self.bufsize = bufsize
373386 self.closed = False
374
387
375388 def _fetch(self):
376389 if self.closed:
377390 return
378
391
379392 line = self.rfile.readline()
380393 self.bytes_read += len(line)
381
394
382395 if self.maxlen and self.bytes_read > self.maxlen:
383396 raise MaxSizeExceeded("Request Entity Too Large", self.maxlen)
384
397
385398 line = line.strip().split(SEMICOLON, 1)
386
399
387400 try:
388401 chunk_size = line.pop(0)
389402 chunk_size = int(chunk_size, 16)
390403 except ValueError:
391404 raise ValueError("Bad chunked transfer size: " + repr(chunk_size))
392
405
393406 if chunk_size <= 0:
394407 self.closed = True
395408 return
396
409
397410 ## if line: chunk_extension = line[0]
398
411
399412 if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
400413 raise IOError("Request Entity Too Large")
401
414
402415 chunk = self.rfile.read(chunk_size)
403416 self.bytes_read += len(chunk)
404417 self.buffer += chunk
405
418
406419 crlf = self.rfile.read(2)
407420 if crlf != CRLF:
408421 raise ValueError(
409 "Bad chunked transfer coding (expected '\\r\\n', "
410 "got " + repr(crlf) + ")")
411
422 "Bad chunked transfer coding (expected '\\r\\n', "
423 "got " + repr(crlf) + ")")
424
412425 def read(self, size=None):
413426 data = EMPTY
414427 while True:
415428 if size and len(data) >= size:
416429 return data
417
430
418431 if not self.buffer:
419432 self._fetch()
420433 if not self.buffer:
421434 # EOF
422435 return data
423
436
424437 if size:
425438 remaining = size - len(data)
426439 data += self.buffer[:remaining]
427440 self.buffer = self.buffer[remaining:]
428441 else:
429442 data += self.buffer
430
443
431444 def readline(self, size=None):
432445 data = EMPTY
433446 while True:
434447 if size and len(data) >= size:
435448 return data
436
449
437450 if not self.buffer:
438451 self._fetch()
439452 if not self.buffer:
440453 # EOF
441454 return data
442
455
443456 newline_pos = self.buffer.find(LF)
444457 if size:
445458 if newline_pos == -1:
456469 else:
457470 data += self.buffer[:newline_pos]
458471 self.buffer = self.buffer[newline_pos:]
459
472
460473 def readlines(self, sizehint=0):
461474 # Shamelessly stolen from StringIO
462475 total = 0
469482 break
470483 line = self.readline(sizehint)
471484 return lines
472
485
473486 def read_trailer_lines(self):
474487 if not self.closed:
475488 raise ValueError(
476489 "Cannot read trailers until the request body has been read.")
477
490
478491 while True:
479492 line = self.rfile.readline()
480493 if not line:
481494 # No more data--illegal end of headers
482495 raise ValueError("Illegal end of headers.")
483
496
484497 self.bytes_read += len(line)
485498 if self.maxlen and self.bytes_read > self.maxlen:
486499 raise IOError("Request Entity Too Large")
487
500
488501 if line == CRLF:
489502 # Normal end of headers
490503 break
491504 if not line.endswith(CRLF):
492505 raise ValueError("HTTP requires CRLF terminators")
493
506
494507 yield line
495
508
496509 def close(self):
497510 self.rfile.close()
498
511
499512 def __iter__(self):
500513 # Shamelessly stolen from StringIO
501514 total = 0
509522
510523
511524 class HTTPRequest(object):
525
512526 """An HTTP Request (and response).
513
527
514528 A single HTTP connection may consist of multiple request/response pairs.
515529 """
516
530
517531 server = None
518532 """The HTTPServer object which is receiving this request."""
519
533
520534 conn = None
521535 """The HTTPConnection object on which this request connected."""
522
536
523537 inheaders = {}
524538 """A dict of request headers."""
525
539
526540 outheaders = []
527541 """A list of header tuples to write in the response."""
528
542
529543 ready = False
530544 """When True, the request has been parsed and is ready to begin generating
531545 the response. When False, signals the calling Connection that the response
532546 should not be generated and the connection should close."""
533
547
534548 close_connection = False
535549 """Signals the calling Connection that the request should close. This does
536550 not imply an error! The client and/or server may each request that the
537551 connection be closed."""
538
552
539553 chunked_write = False
540554 """If True, output will be encoded with the "chunked" transfer-coding.
541
555
542556 This value is set automatically inside send_headers."""
543
557
544558 def __init__(self, server, conn):
545 self.server= server
559 self.server = server
546560 self.conn = conn
547
561
548562 self.ready = False
549563 self.started_request = False
550564 self.scheme = ntob("http")
553567 # Use the lowest-common protocol in case read_request_line errors.
554568 self.response_protocol = 'HTTP/1.0'
555569 self.inheaders = {}
556
570
557571 self.status = ""
558572 self.outheaders = []
559573 self.sent_headers = False
560574 self.close_connection = self.__class__.close_connection
561575 self.chunked_read = False
562576 self.chunked_write = self.__class__.chunked_write
563
577
564578 def parse_request(self):
565579 """Parse the next HTTP request start-line and message-headers."""
566580 self.rfile = SizeCheckWrapper(self.conn.rfile,
568582 try:
569583 success = self.read_request_line()
570584 except MaxSizeExceeded:
571 self.simple_response("414 Request-URI Too Long",
585 self.simple_response(
586 "414 Request-URI Too Long",
572587 "The Request-URI sent with the request exceeds the maximum "
573588 "allowed bytes.")
574589 return
575590 else:
576591 if not success:
577592 return
578
593
579594 try:
580595 success = self.read_request_headers()
581596 except MaxSizeExceeded:
582 self.simple_response("413 Request Entity Too Large",
597 self.simple_response(
598 "413 Request Entity Too Large",
583599 "The headers sent with the request exceed the maximum "
584600 "allowed bytes.")
585601 return
586602 else:
587603 if not success:
588604 return
589
605
590606 self.ready = True
591
607
592608 def read_request_line(self):
593609 # HTTP/1.1 connections are persistent by default. If a client
594610 # requests a page, then idles (leaves the connection open),
598614 # (although your TCP stack might suffer for it: cf Apache's history
599615 # with FIN_WAIT_2).
600616 request_line = self.rfile.readline()
601
617
602618 # Set started_request to True so communicate() knows to send 408
603619 # from here on out.
604620 self.started_request = True
605621 if not request_line:
606622 return False
607
623
608624 if request_line == CRLF:
609625 # RFC 2616 sec 4.1: "...if the server is reading the protocol
610626 # stream at the beginning of a message and receives a CRLF
613629 request_line = self.rfile.readline()
614630 if not request_line:
615631 return False
616
632
617633 if not request_line.endswith(CRLF):
618 self.simple_response("400 Bad Request", "HTTP requires CRLF terminators")
634 self.simple_response(
635 "400 Bad Request", "HTTP requires CRLF terminators")
619636 return False
620
637
621638 try:
622639 method, uri, req_protocol = request_line.strip().split(SPACE, 2)
623 # The [x:y] slicing is necessary for byte strings to avoid getting ord's
640 # The [x:y] slicing is necessary for byte strings to avoid getting
641 # ord's
624642 rp = int(req_protocol[5:6]), int(req_protocol[7:8])
625643 except ValueError:
626644 self.simple_response("400 Bad Request", "Malformed Request-Line")
627645 return False
628
646
629647 self.uri = uri
630648 self.method = method
631
649
632650 # uri may be an abs_path (including "http://host.domain.tld");
633651 scheme, authority, path = self.parse_request_uri(uri)
634652 if NUMBER_SIGN in path:
635653 self.simple_response("400 Bad Request",
636654 "Illegal #fragment in Request-URI.")
637655 return False
638
656
639657 if scheme:
640658 self.scheme = scheme
641
659
642660 qs = EMPTY
643661 if QUESTION_MARK in path:
644662 path, qs = path.split(QUESTION_MARK, 1)
645
663
646664 # Unquote the path+params (e.g. "/this%20path" -> "/this path").
647665 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
648666 #
658676 return False
659677 path = b"%2F".join(atoms)
660678 self.path = path
661
679
662680 # Note that, like wsgiref and most other HTTP servers,
663681 # we "% HEX HEX"-unquote the path but not the query string.
664682 self.qs = qs
665
683
666684 # Compare request and server HTTP protocol versions, in case our
667685 # server does not support the requested protocol. Limit our output
668686 # to min(req, server). We want the following output:
675693 # Notice that, in (b), the response will be "HTTP/1.1" even though
676694 # the client only understands 1.0. RFC 2616 10.5.6 says we should
677695 # only return 505 if the _major_ version is different.
678 # The [x:y] slicing is necessary for byte strings to avoid getting ord's
696 # The [x:y] slicing is necessary for byte strings to avoid getting
697 # ord's
679698 sp = int(self.server.protocol[5:6]), int(self.server.protocol[7:8])
680
699
681700 if sp[0] != rp[0]:
682701 self.simple_response("505 HTTP Version Not Supported")
683702 return False
688707
689708 def read_request_headers(self):
690709 """Read self.rfile into self.inheaders. Return success."""
691
710
692711 # then all the http headers
693712 try:
694713 read_headers(self.rfile, self.inheaders)
696715 ex = sys.exc_info()[1]
697716 self.simple_response("400 Bad Request", ex.args[0])
698717 return False
699
718
700719 mrbs = self.server.max_request_body_size
701720 if mrbs and int(self.inheaders.get(b"Content-Length", 0)) > mrbs:
702 self.simple_response("413 Request Entity Too Large",
721 self.simple_response(
722 "413 Request Entity Too Large",
703723 "The entity sent with the request exceeds the maximum "
704724 "allowed bytes.")
705725 return False
706
726
707727 # Persistent connection support
708728 if self.response_protocol == "HTTP/1.1":
709729 # Both server and client are HTTP/1.1
713733 # Either the server or client (or both) are HTTP/1.0
714734 if self.inheaders.get(b"Connection", b"") != b"Keep-Alive":
715735 self.close_connection = True
716
736
717737 # Transfer-Encoding support
718738 te = None
719739 if self.response_protocol == "HTTP/1.1":
720740 te = self.inheaders.get(b"Transfer-Encoding")
721741 if te:
722742 te = [x.strip().lower() for x in te.split(b",") if x.strip()]
723
743
724744 self.chunked_read = False
725
745
726746 if te:
727747 for enc in te:
728748 if enc == b"chunked":
733753 self.simple_response("501 Unimplemented")
734754 self.close_connection = True
735755 return False
736
756
737757 # From PEP 333:
738758 # "Servers and gateways that implement HTTP 1.1 must provide
739759 # transparent support for HTTP 1.1's "expect/continue" mechanism.
753773 # but it seems like it would be a big slowdown for such a rare case.
754774 if self.inheaders.get(b"Expect", b"") == b"100-continue":
755775 # Don't use simple_response here, because it emits headers
756 # we don't want. See http://www.cherrypy.org/ticket/951
757 msg = self.server.protocol.encode('ascii') + b" 100 Continue\r\n\r\n"
776 # we don't want. See
777 # https://bitbucket.org/cherrypy/cherrypy/issue/951
778 msg = self.server.protocol.encode(
779 'ascii') + b" 100 Continue\r\n\r\n"
758780 try:
759781 self.conn.wfile.write(msg)
760782 except socket.error:
762784 if x.args[0] not in socket_errors_to_ignore:
763785 raise
764786 return True
765
787
766788 def parse_request_uri(self, uri):
767789 """Parse a Request-URI into (scheme, authority, path).
768
790
769791 Note that Request-URI's must be one of::
770
792
771793 Request-URI = "*" | absoluteURI | abs_path | authority
772
794
773795 Therefore, a Request-URI which starts with a double forward-slash
774796 cannot be a "net_path"::
775
797
776798 net_path = "//" authority [ abs_path ]
777
799
778800 Instead, it must be interpreted as an "abs_path" with an empty first
779801 path segment::
780
802
781803 abs_path = "/" path_segments
782804 path_segments = segment *( "/" segment )
783805 segment = *pchar *( ";" param )
790812 if sep and QUESTION_MARK not in scheme:
791813 # An absoluteURI.
792814 # If there's a scheme (and it must be http or https), then:
793 # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
815 # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query
816 # ]]
794817 authority, path_a, path_b = remainder.partition(FORWARD_SLASH)
795 return scheme.lower(), authority, path_a+path_b
818 return scheme.lower(), authority, path_a + path_b
796819
797820 if uri.startswith(FORWARD_SLASH):
798821 # An abs_path.
800823 else:
801824 # An authority.
802825 return None, uri, None
803
826
804827 def unquote_bytes(self, path):
805 """takes quoted string and unquotes % encoded values"""
828 """takes quoted string and unquotes % encoded values"""
806829 res = path.split(b'%')
807
830
808831 for i in range(1, len(res)):
809832 item = res[i]
810833 try:
812835 except ValueError:
813836 raise
814837 return b''.join(res)
815
838
816839 def respond(self):
817840 """Call the gateway and write its iterable output."""
818841 mrbs = self.server.max_request_body_size
822845 cl = int(self.inheaders.get(b"Content-Length", 0))
823846 if mrbs and mrbs < cl:
824847 if not self.sent_headers:
825 self.simple_response("413 Request Entity Too Large",
826 "The entity sent with the request exceeds the maximum "
827 "allowed bytes.")
848 self.simple_response(
849 "413 Request Entity Too Large",
850 "The entity sent with the request exceeds the "
851 "maximum allowed bytes.")
828852 return
829853 self.rfile = KnownLengthRFile(self.conn.rfile, cl)
830
854
831855 self.server.gateway(self).respond()
832
856
833857 if (self.ready and not self.sent_headers):
834858 self.sent_headers = True
835859 self.send_headers()
836860 if self.chunked_write:
837861 self.conn.wfile.write(b"0\r\n\r\n")
838
862
839863 def simple_response(self, status, msg=""):
840864 """Write a simple response back to the client."""
841865 status = str(status)
843867 bytes(status, "ISO-8859-1") + CRLF,
844868 bytes("Content-Length: %s\r\n" % len(msg), "ISO-8859-1"),
845869 b"Content-Type: text/plain\r\n"]
846
870
847871 if status[:3] in ("413", "414"):
848872 # Request Entity Too Large / Request-URI Too Long
849873 self.close_connection = True
856880 # HTTP/1.0 had no 413/414 status nor Connection header.
857881 # Emit 400 instead and trust the message body is enough.
858882 status = "400 Bad Request"
859
883
860884 buf.append(CRLF)
861885 if msg:
862886 if isinstance(msg, unicodestr):
863887 msg = msg.encode("ISO-8859-1")
864888 buf.append(msg)
865
889
866890 try:
867891 self.conn.wfile.write(b"".join(buf))
868892 except socket.error:
869893 x = sys.exc_info()[1]
870894 if x.args[0] not in socket_errors_to_ignore:
871895 raise
872
896
873897 def write(self, chunk):
874898 """Write unbuffered data to the client."""
875899 if self.chunked_write and chunk:
877901 self.conn.wfile.write(EMPTY.join(buf))
878902 else:
879903 self.conn.wfile.write(chunk)
880
904
881905 def send_headers(self):
882906 """Assert, process, and send the HTTP response message-headers.
883
907
884908 You must set self.status, and self.outheaders before calling this.
885909 """
886910 hkeys = [key.lower() for key, value in self.outheaders]
887911 status = int(self.status[:3])
888
912
889913 if status == 413:
890914 # Request Entity Too Large. Close conn to avoid garbage.
891915 self.close_connection = True
897921 pass
898922 else:
899923 if (self.response_protocol == 'HTTP/1.1'
900 and self.method != b'HEAD'):
924 and self.method != b'HEAD'):
901925 # Use the chunked transfer-coding
902926 self.chunked_write = True
903927 self.outheaders.append((b"Transfer-Encoding", b"chunked"))
904928 else:
905929 # Closing the conn is the only way to determine len.
906930 self.close_connection = True
907
931
908932 if b"connection" not in hkeys:
909933 if self.response_protocol == 'HTTP/1.1':
910934 # Both server and client are HTTP/1.1 or better
914938 # Server and/or client are HTTP/1.0
915939 if not self.close_connection:
916940 self.outheaders.append((b"Connection", b"Keep-Alive"))
917
941
918942 if (not self.close_connection) and (not self.chunked_read):
919943 # Read any remaining request body data on the socket.
920944 # "If an origin server receives a request that does not include an
931955 remaining = getattr(self.rfile, 'remaining', 0)
932956 if remaining > 0:
933957 self.rfile.read(remaining)
934
958
935959 if b"date" not in hkeys:
936 self.outheaders.append(
937 (b"Date", email.utils.formatdate(usegmt=True).encode('ISO-8859-1')))
938
960 self.outheaders.append((
961 b"Date",
962 email.utils.formatdate(usegmt=True).encode('ISO-8859-1')
963 ))
964
939965 if b"server" not in hkeys:
940966 self.outheaders.append(
941967 (b"Server", self.server.server_name.encode('ISO-8859-1')))
942
943 buf = [self.server.protocol.encode('ascii') + SPACE + self.status + CRLF]
968
969 buf = [self.server.protocol.encode(
970 'ascii') + SPACE + self.status + CRLF]
944971 for k, v in self.outheaders:
945972 buf.append(k + COLON + SPACE + v + CRLF)
946973 buf.append(CRLF)
948975
949976
950977 class NoSSLError(Exception):
978
951979 """Exception raised when a client speaks HTTP to an HTTPS socket."""
952980 pass
953981
954982
955983 class FatalSSLAlert(Exception):
984
956985 """Exception raised when the SSL implementation signals a fatal alert."""
957986 pass
958987
959988
960989 class CP_BufferedWriter(io.BufferedWriter):
990
961991 """Faux file object attached to a socket object."""
962992
963993 def write(self, b):
964994 self._checkClosed()
965995 if isinstance(b, str):
966996 raise TypeError("can't write str to binary stream")
967
997
968998 with self._write_lock:
969999 self._write_buf.extend(b)
9701000 self._flush_unlocked()
9711001 return len(b)
972
1002
9731003 def _flush_unlocked(self):
9741004 self._checkClosed("flush of closed file")
9751005 while self._write_buf:
9881018 else:
9891019 return CP_BufferedWriter(socket.SocketIO(sock, mode), bufsize)
9901020
1021
9911022 class HTTPConnection(object):
1023
9921024 """An HTTP connection (active socket).
993
1025
9941026 server: the Server object which received this connection.
9951027 socket: the raw socket object (usually TCP) for this connection.
9961028 makefile: a fileobject class for reading from the socket.
9971029 """
998
1030
9991031 remote_addr = None
10001032 remote_port = None
10011033 ssl_env = None
10021034 rbufsize = DEFAULT_BUFFER_SIZE
10031035 wbufsize = DEFAULT_BUFFER_SIZE
10041036 RequestHandlerClass = HTTPRequest
1005
1037
10061038 def __init__(self, server, sock, makefile=CP_makefile):
10071039 self.server = server
10081040 self.socket = sock
10091041 self.rfile = makefile(sock, "rb", self.rbufsize)
10101042 self.wfile = makefile(sock, "wb", self.wbufsize)
10111043 self.requests_seen = 0
1012
1044
10131045 def communicate(self):
10141046 """Read each request and respond appropriately."""
10151047 request_seen = False
10201052 # get written to the previous request.
10211053 req = None
10221054 req = self.RequestHandlerClass(self.server, self)
1023
1055
10241056 # This order of operations should guarantee correct pipelining.
10251057 req.parse_request()
10261058 if self.server.stats['Enabled']:
10301062 # probably already made a simple_response). Return and
10311063 # let the conn close.
10321064 return
1033
1065
10341066 request_seen = True
10351067 req.respond()
10361068 if req.close_connection:
10391071 e = sys.exc_info()[1]
10401072 errnum = e.args[0]
10411073 # sadly SSL sockets return a different (longer) time out string
1042 if errnum == 'timed out' or errnum == 'The read operation timed out':
1074 if (
1075 errnum == 'timed out' or
1076 errnum == 'The read operation timed out'
1077 ):
10431078 # Don't error if we're between requests; only error
10441079 # if 1) no request has been started at all, or 2) we're
10451080 # in the middle of a request.
1046 # See http://www.cherrypy.org/ticket/853
1081 # See https://bitbucket.org/cherrypy/cherrypy/issue/853
10471082 if (not request_seen) or (req and req.started_request):
10481083 # Don't bother writing the 408 if the response
10491084 # has already started being written.
10711106 except NoSSLError:
10721107 if req and not req.sent_headers:
10731108 # Unwrap our wfile
1074 self.wfile = CP_makefile(self.socket._sock, "wb", self.wbufsize)
1075 req.simple_response("400 Bad Request",
1076 "The client sent a plain HTTP request, but "
1077 "this server only speaks HTTPS on this port.")
1109 self.wfile = CP_makefile(
1110 self.socket._sock, "wb", self.wbufsize)
1111 req.simple_response(
1112 "400 Bad Request",
1113 "The client sent a plain HTTP request, but this server "
1114 "only speaks HTTPS on this port.")
10781115 self.linger = True
10791116 except Exception:
10801117 e = sys.exc_info()[1]
10851122 except FatalSSLAlert:
10861123 # Close the connection.
10871124 return
1088
1125
10891126 linger = False
1090
1127
10911128 def close(self):
10921129 """Close the socket underlying this connection."""
10931130 self.rfile.close()
1094
1131
10951132 if not self.linger:
1096 # Python's socket module does NOT call close on the kernel socket
1097 # when you call socket.close(). We do so manually here because we
1098 # want this server to send a FIN TCP segment immediately. Note this
1099 # must be called *before* calling socket.close(), because the latter
1100 # drops its reference to the kernel socket.
1101 # Python 3 *probably* fixed this with socket._real_close; hard to tell.
1102 ## self.socket._sock.close()
1133 # Python's socket module does NOT call close on the kernel
1134 # socket when you call socket.close(). We do so manually here
1135 # because we want this server to send a FIN TCP segment
1136 # immediately. Note this must be called *before* calling
1137 # socket.close(), because the latter drops its reference to
1138 # the kernel socket.
1139 # Python 3 *probably* fixed this with socket._real_close;
1140 # hard to tell.
1141 # self.socket._sock.close()
11031142 self.socket.close()
11041143 else:
11051144 # On the other hand, sometimes we want to hang around for a bit
11121151
11131152
11141153 class TrueyZero(object):
1115 """An object which equals and does math like the integer '0' but evals True."""
1154
1155 """An object which equals and does math like the integer 0 but evals True.
1156 """
1157
11161158 def __add__(self, other):
11171159 return other
1160
11181161 def __radd__(self, other):
11191162 return other
11201163 trueyzero = TrueyZero()
11221165
11231166 _SHUTDOWNREQUEST = None
11241167
1168
11251169 class WorkerThread(threading.Thread):
1170
11261171 """Thread which continuously polls a Queue for Connection objects.
1127
1172
11281173 Due to the timing issues of polling a Queue, a WorkerThread does not
11291174 check its own 'ready' flag after it has started. To stop the thread,
11301175 it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
11311176 (one for each running WorkerThread).
11321177 """
1133
1178
11341179 conn = None
11351180 """The current connection pulled off the Queue, or None."""
1136
1181
11371182 server = None
11381183 """The HTTP Server which spawned this thread, and which owns the
11391184 Queue and is placing active connections into it."""
1140
1185
11411186 ready = False
11421187 """A simple flag for the calling server to know when this thread
11431188 has begun polling the Queue."""
1144
1145
1189
11461190 def __init__(self, server):
11471191 self.ready = False
11481192 self.server = server
1149
1193
11501194 self.requests_seen = 0
11511195 self.bytes_read = 0
11521196 self.bytes_written = 0
11531197 self.start_time = None
11541198 self.work_time = 0
11551199 self.stats = {
1156 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and trueyzero or self.conn.requests_seen),
1157 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and trueyzero or self.conn.rfile.bytes_read),
1158 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and trueyzero or self.conn.wfile.bytes_written),
1159 'Work Time': lambda s: self.work_time + ((self.start_time is None) and trueyzero or time.time() - self.start_time),
1160 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6),
1161 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6),
1200 'Requests': lambda s: self.requests_seen + (
1201 (self.start_time is None) and
1202 trueyzero or
1203 self.conn.requests_seen
1204 ),
1205 'Bytes Read': lambda s: self.bytes_read + (
1206 (self.start_time is None) and
1207 trueyzero or
1208 self.conn.rfile.bytes_read
1209 ),
1210 'Bytes Written': lambda s: self.bytes_written + (
1211 (self.start_time is None) and
1212 trueyzero or
1213 self.conn.wfile.bytes_written
1214 ),
1215 'Work Time': lambda s: self.work_time + (
1216 (self.start_time is None) and
1217 trueyzero or
1218 time.time() - self.start_time
1219 ),
1220 'Read Throughput': lambda s: s['Bytes Read'](s) / (
1221 s['Work Time'](s) or 1e-6),
1222 'Write Throughput': lambda s: s['Bytes Written'](s) / (
1223 s['Work Time'](s) or 1e-6),
11621224 }
11631225 threading.Thread.__init__(self)
1164
1226
11651227 def run(self):
11661228 self.server.stats['Worker Threads'][self.getName()] = self.stats
11671229 try:
11701232 conn = self.server.requests.get()
11711233 if conn is _SHUTDOWNREQUEST:
11721234 return
1173
1235
11741236 self.conn = conn
11751237 if self.server.stats['Enabled']:
11761238 self.start_time = time.time()
11911253
11921254
11931255 class ThreadPool(object):
1256
11941257 """A Request Queue for an HTTPServer which pools threads.
1195
1258
11961259 ThreadPool objects must provide min, get(), put(obj), start()
11971260 and stop(timeout) attributes.
11981261 """
1199
1262
12001263 def __init__(self, server, min=10, max=-1):
12011264 self.server = server
12021265 self.min = min
12041267 self._threads = []
12051268 self._queue = queue.Queue()
12061269 self.get = self._queue.get
1207
1270
12081271 def start(self):
12091272 """Start the pool of threads."""
12101273 for i in range(self.min):
12151278 for worker in self._threads:
12161279 while not worker.ready:
12171280 time.sleep(.1)
1218
1281
12191282 def _get_idle(self):
12201283 """Number of worker threads which are idle. Read-only."""
12211284 return len([t for t in self._threads if t.conn is None])
12221285 idle = property(_get_idle, doc=_get_idle.__doc__)
1223
1286
12241287 def put(self, obj):
12251288 self._queue.put(obj)
12261289 if obj is _SHUTDOWNREQUEST:
12271290 return
1228
1291
12291292 def grow(self, amount):
12301293 """Spawn new worker threads (not above self.max)."""
1231 for i in range(amount):
1232 if self.max > 0 and len(self._threads) >= self.max:
1233 break
1234 worker = WorkerThread(self.server)
1235 worker.setName("CP Server " + worker.getName())
1236 self._threads.append(worker)
1237 worker.start()
1238
1294 if self.max > 0:
1295 budget = max(self.max - len(self._threads), 0)
1296 else:
1297 # self.max <= 0 indicates no maximum
1298 budget = float('inf')
1299
1300 n_new = min(amount, budget)
1301
1302 workers = [self._spawn_worker() for i in range(n_new)]
1303 while not all(worker.ready for worker in workers):
1304 time.sleep(.1)
1305 self._threads.extend(workers)
1306
1307 def _spawn_worker(self):
1308 worker = WorkerThread(self.server)
1309 worker.setName("CP Server " + worker.getName())
1310 worker.start()
1311 return worker
1312
12391313 def shrink(self, amount):
12401314 """Kill off worker threads (not below self.min)."""
12411315 # Grow/shrink the pool if necessary.
12441318 if not t.isAlive():
12451319 self._threads.remove(t)
12461320 amount -= 1
1247
1248 if amount > 0:
1249 for i in range(min(amount, len(self._threads) - self.min)):
1250 # Put a number of shutdown requests on the queue equal
1251 # to 'amount'. Once each of those is processed by a worker,
1252 # that worker will terminate and be culled from our list
1253 # in self.put.
1254 self._queue.put(_SHUTDOWNREQUEST)
1255
1321
1322 # calculate the number of threads above the minimum
1323 n_extra = max(len(self._threads) - self.min, 0)
1324
1325 # don't remove more than amount
1326 n_to_remove = min(amount, n_extra)
1327
1328 # put shutdown requests on the queue equal to the number of threads
1329 # to remove. As each request is processed by a worker, that worker
1330 # will terminate and be culled from the list.
1331 for n in range(n_to_remove):
1332 self._queue.put(_SHUTDOWNREQUEST)
1333
12561334 def stop(self, timeout=5):
12571335 # Must shut down threads here so the code that calls
12581336 # this method can know when all threads are stopped.
12591337 for worker in self._threads:
12601338 self._queue.put(_SHUTDOWNREQUEST)
1261
1339
12621340 # Don't join currentThread (when stop is called inside a request).
12631341 current = threading.currentThread()
12641342 if timeout and timeout >= 0:
12861364 worker.join()
12871365 except (AssertionError,
12881366 # Ignore repeated Ctrl-C.
1289 # See http://www.cherrypy.org/ticket/691.
1367 # See
1368 # https://bitbucket.org/cherrypy/cherrypy/issue/691.
12901369 KeyboardInterrupt):
12911370 pass
1292
1371
12931372 def _get_qsize(self):
12941373 return self._queue.qsize()
12951374 qsize = property(_get_qsize)
1296
12971375
12981376
12991377 try:
13011379 except ImportError:
13021380 try:
13031381 from ctypes import windll, WinError
1382 import ctypes.wintypes
1383 _SetHandleInformation = windll.kernel32.SetHandleInformation
1384 _SetHandleInformation.argtypes = [
1385 ctypes.wintypes.HANDLE,
1386 ctypes.wintypes.DWORD,
1387 ctypes.wintypes.DWORD,
1388 ]
1389 _SetHandleInformation.restype = ctypes.wintypes.BOOL
13041390 except ImportError:
13051391 def prevent_socket_inheritance(sock):
13061392 """Dummy function, since neither fcntl nor ctypes are available."""
13081394 else:
13091395 def prevent_socket_inheritance(sock):
13101396 """Mark the given socket fd as non-inheritable (Windows)."""
1311 if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
1397 if not _SetHandleInformation(sock.fileno(), 1, 0):
13121398 raise WinError()
13131399 else:
13141400 def prevent_socket_inheritance(sock):
13191405
13201406
13211407 class SSLAdapter(object):
1408
13221409 """Base class for SSL driver library adapters.
1323
1410
13241411 Required methods:
1325
1412
13261413 * ``wrap(sock) -> (wrapped socket, ssl environ dict)``
1327 * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object``
1414 * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
1415 socket file object``
13281416 """
1329
1417
13301418 def __init__(self, certificate, private_key, certificate_chain=None):
13311419 self.certificate = certificate
13321420 self.private_key = private_key
13331421 self.certificate_chain = certificate_chain
1334
1422
13351423 def wrap(self, sock):
13361424 raise NotImplemented
1337
1425
13381426 def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
13391427 raise NotImplemented
13401428
13411429
13421430 class HTTPServer(object):
1431
13431432 """An HTTP server."""
1344
1433
13451434 _bind_addr = "127.0.0.1"
13461435 _interrupt = None
1347
1436
13481437 gateway = None
13491438 """A Gateway instance."""
1350
1439
13511440 minthreads = None
13521441 """The minimum number of worker threads to create (default 10)."""
1353
1442
13541443 maxthreads = None
1355 """The maximum number of worker threads to create (default -1 = no limit)."""
1356
1444 """The maximum number of worker threads to create (default -1 = no limit).
1445 """
1446
13571447 server_name = None
13581448 """The name of the server; defaults to socket.gethostname()."""
1359
1449
13601450 protocol = "HTTP/1.1"
13611451 """The version string to write in the Status-Line of all HTTP responses.
1362
1452
13631453 For example, "HTTP/1.1" is the default. This also limits the supported
13641454 features used in the response."""
1365
1455
13661456 request_queue_size = 5
1367 """The 'backlog' arg to socket.listen(); max queued connections (default 5)."""
1368
1457 """The 'backlog' arg to socket.listen(); max queued connections
1458 (default 5).
1459 """
1460
13691461 shutdown_timeout = 5
1370 """The total time, in seconds, to wait for worker threads to cleanly exit."""
1371
1462 """The total time, in seconds, to wait for worker threads to cleanly exit.
1463 """
1464
13721465 timeout = 10
13731466 """The timeout in seconds for accepted connections (default 10)."""
1374
1375 version = "CherryPy/3.2.2"
1467
1468 version = "CherryPy/3.3.0"
13761469 """A version string for the HTTPServer."""
1377
1470
13781471 software = None
13791472 """The value to set for the SERVER_SOFTWARE entry in the WSGI environ.
1380
1473
13811474 If None, this defaults to ``'%s Server' % self.version``."""
1382
1475
13831476 ready = False
1384 """An internal flag which marks whether the socket is accepting connections."""
1385
1477 """An internal flag which marks whether the socket is accepting
1478 connections.
1479 """
1480
13861481 max_request_header_size = 0
13871482 """The maximum size, in bytes, for request headers, or 0 for no limit."""
1388
1483
13891484 max_request_body_size = 0
13901485 """The maximum size, in bytes, for request bodies, or 0 for no limit."""
1391
1486
13921487 nodelay = True
13931488 """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
1394
1489
13951490 ConnectionClass = HTTPConnection
13961491 """The class to use for handling HTTP connections."""
1397
1492
13981493 ssl_adapter = None
13991494 """An instance of SSLAdapter (or a subclass).
1400
1495
14011496 You must have the corresponding SSL driver library installed."""
1402
1497
14031498 def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1,
14041499 server_name=None):
14051500 self.bind_addr = bind_addr
14061501 self.gateway = gateway
1407
1502
14081503 self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads)
1409
1504
14101505 if not server_name:
14111506 server_name = socket.gethostname()
14121507 self.server_name = server_name
14131508 self.clear_stats()
1414
1509
14151510 def clear_stats(self):
14161511 self._start_time = None
14171512 self._run_time = 0
14251520 'Threads': lambda s: len(getattr(self.requests, "_threads", [])),
14261521 'Threads Idle': lambda s: getattr(self.requests, "idle", None),
14271522 'Socket Errors': 0,
1428 'Requests': lambda s: (not s['Enabled']) and -1 or sum([w['Requests'](w) for w
1429 in s['Worker Threads'].values()], 0),
1430 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Read'](w) for w
1431 in s['Worker Threads'].values()], 0),
1432 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Written'](w) for w
1433 in s['Worker Threads'].values()], 0),
1434 'Work Time': lambda s: (not s['Enabled']) and -1 or sum([w['Work Time'](w) for w
1435 in s['Worker Threads'].values()], 0),
1523 'Requests': lambda s: (not s['Enabled']) and -1 or sum(
1524 [w['Requests'](w) for w in s['Worker Threads'].values()], 0),
1525 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum(
1526 [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0),
1527 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum(
1528 [w['Bytes Written'](w) for w in s['Worker Threads'].values()],
1529 0),
1530 'Work Time': lambda s: (not s['Enabled']) and -1 or sum(
1531 [w['Work Time'](w) for w in s['Worker Threads'].values()], 0),
14361532 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum(
14371533 [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
14381534 for w in s['Worker Threads'].values()], 0),
14401536 [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
14411537 for w in s['Worker Threads'].values()], 0),
14421538 'Worker Threads': {},
1443 }
1539 }
14441540 logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats
1445
1541
14461542 def runtime(self):
14471543 if self._start_time is None:
14481544 return self._run_time
14491545 else:
14501546 return self._run_time + (time.time() - self._start_time)
1451
1547
14521548 def __str__(self):
14531549 return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
14541550 self.bind_addr)
1455
1551
14561552 def _get_bind_addr(self):
14571553 return self._bind_addr
1554
14581555 def _set_bind_addr(self, value):
14591556 if isinstance(value, tuple) and value[0] in ('', None):
14601557 # Despite the socket module docs, using '' does not
14711568 "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
14721569 "to listen on all active interfaces.")
14731570 self._bind_addr = value
1474 bind_addr = property(_get_bind_addr, _set_bind_addr,
1571 bind_addr = property(
1572 _get_bind_addr,
1573 _set_bind_addr,
14751574 doc="""The interface on which to listen for connections.
1476
1575
14771576 For TCP sockets, a (host, port) tuple. Host values may be any IPv4
14781577 or IPv6 address, or any valid hostname. The string 'localhost' is a
14791578 synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
14801579 The string '0.0.0.0' is a special IPv4 entry meaning "any active
14811580 interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
14821581 IPv6. The empty string or None are not allowed.
1483
1582
14841583 For UNIX sockets, supply the filename as a string.""")
1485
1584
14861585 def start(self):
14871586 """Run the server forever."""
14881587 # We don't have to trap KeyboardInterrupt or SystemExit here,
14901589 # If you're using this server with another framework, you should
14911590 # trap those exceptions in whatever code block calls start().
14921591 self._interrupt = None
1493
1592
14941593 if self.software is None:
14951594 self.software = "%s Server" % self.version
1496
1595
14971596 # Select the appropriate socket
14981597 if isinstance(self.bind_addr, basestring):
14991598 # AF_UNIX socket
1500
1599
15011600 # So we can reuse the socket...
1502 try: os.unlink(self.bind_addr)
1503 except: pass
1504
1601 try:
1602 os.unlink(self.bind_addr)
1603 except:
1604 pass
1605
15051606 # So everyone can access the socket...
1506 try: os.chmod(self.bind_addr, 511) # 0777
1507 except: pass
1508
1509 info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
1607 try:
1608 os.chmod(self.bind_addr, 511) # 0777
1609 except:
1610 pass
1611
1612 info = [
1613 (socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
15101614 else:
15111615 # AF_INET or AF_INET6 socket
1512 # Get the correct address family for our host (allows IPv6 addresses)
1616 # Get the correct address family for our host (allows IPv6
1617 # addresses)
15131618 host, port = self.bind_addr
15141619 try:
15151620 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
1516 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
1621 socket.SOCK_STREAM, 0,
1622 socket.AI_PASSIVE)
15171623 except socket.gaierror:
15181624 if ':' in self.bind_addr[0]:
15191625 info = [(socket.AF_INET6, socket.SOCK_STREAM,
15211627 else:
15221628 info = [(socket.AF_INET, socket.SOCK_STREAM,
15231629 0, "", self.bind_addr)]
1524
1630
15251631 self.socket = None
15261632 msg = "No socket could be created"
15271633 for res in info:
15281634 af, socktype, proto, canonname, sa = res
15291635 try:
15301636 self.bind(af, socktype, proto)
1531 except socket.error:
1637 except socket.error as serr:
1638 msg = "%s -- (%s: %s)" % (msg, sa, serr)
15321639 if self.socket:
15331640 self.socket.close()
15341641 self.socket = None
15361643 break
15371644 if not self.socket:
15381645 raise socket.error(msg)
1539
1646
15401647 # Timeout so KeyboardInterrupt can be caught on Win32
15411648 self.socket.settimeout(1)
15421649 self.socket.listen(self.request_queue_size)
1543
1650
15441651 # Create worker threads
15451652 self.requests.start()
1546
1653
15471654 self.ready = True
15481655 self._start_time = time.time()
15491656 while self.ready:
15771684 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
15781685 if self.nodelay and not isinstance(self.bind_addr, str):
15791686 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1580
1687
15811688 if self.ssl_adapter is not None:
15821689 self.socket = self.ssl_adapter.bind(self.socket)
1583
1690
15841691 # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
1585 # activate dual-stack. See http://www.cherrypy.org/ticket/871.
1692 # activate dual-stack. See
1693 # https://bitbucket.org/cherrypy/cherrypy/issue/871.
15861694 if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6
1587 and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
1695 and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
15881696 try:
1589 self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
1697 self.socket.setsockopt(
1698 socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
15901699 except (AttributeError, socket.error):
15911700 # Apparently, the socket option is not available in
15921701 # this machine's TCP stack
15931702 pass
1594
1703
15951704 self.socket.bind(self.bind_addr)
1596
1705
15971706 def tick(self):
15981707 """Accept a new connection and put it on the Queue."""
15991708 try:
16021711 self.stats['Accepts'] += 1
16031712 if not self.ready:
16041713 return
1605
1714
16061715 prevent_socket_inheritance(s)
16071716 if hasattr(s, 'settimeout'):
16081717 s.settimeout(self.timeout)
1609
1718
16101719 makefile = CP_makefile
16111720 ssl_env = {}
16121721 # if ssl cert and key are set, we try to be a secure HTTP server
16201729 "Content-Length: %s\r\n" % len(msg),
16211730 "Content-Type: text/plain\r\n\r\n",
16221731 msg]
1623
1732
16241733 wfile = makefile(s, "wb", DEFAULT_BUFFER_SIZE)
16251734 try:
16261735 wfile.write("".join(buf).encode('ISO-8859-1'))
16351744 # Re-apply our timeout since we may have a new socket object
16361745 if hasattr(s, 'settimeout'):
16371746 s.settimeout(self.timeout)
1638
1747
16391748 conn = self.ConnectionClass(self, s, makefile)
1640
1749
16411750 if not isinstance(self.bind_addr, basestring):
16421751 # optional values
16431752 # Until we do DNS lookups, omit REMOTE_HOST
1644 if addr is None: # sometimes this can happen
1753 if addr is None: # sometimes this can happen
16451754 # figure out if AF_INET or AF_INET6.
16461755 if len(s.getsockname()) == 2:
16471756 # AF_INET
16511760 addr = ('::', 0)
16521761 conn.remote_addr = addr[0]
16531762 conn.remote_port = addr[1]
1654
1763
16551764 conn.ssl_env = ssl_env
1656
1765
16571766 self.requests.put(conn)
16581767 except socket.timeout:
16591768 # The only reason for the timeout in start() is so we can
16691778 # is received during the accept() call; all docs say retry
16701779 # the call, and I *think* I'm reading it right that Python
16711780 # will then go ahead and poll for and handle the signal
1672 # elsewhere. See http://www.cherrypy.org/ticket/707.
1781 # elsewhere. See
1782 # https://bitbucket.org/cherrypy/cherrypy/issue/707.
16731783 return
16741784 if x.args[0] in socket_errors_nonblocking:
1675 # Just try again. See http://www.cherrypy.org/ticket/479.
1785 # Just try again. See
1786 # https://bitbucket.org/cherrypy/cherrypy/issue/479.
16761787 return
16771788 if x.args[0] in socket_errors_to_ignore:
16781789 # Our socket was closed.
1679 # See http://www.cherrypy.org/ticket/686.
1790 # See https://bitbucket.org/cherrypy/cherrypy/issue/686.
16801791 return
16811792 raise
1682
1793
16831794 def _get_interrupt(self):
16841795 return self._interrupt
1796
16851797 def _set_interrupt(self, interrupt):
16861798 self._interrupt = True
16871799 self.stop()
16891801 interrupt = property(_get_interrupt, _set_interrupt,
16901802 doc="Set this to an Exception instance to "
16911803 "interrupt the server.")
1692
1804
16931805 def stop(self):
16941806 """Gracefully shutdown a server that is serving forever."""
16951807 self.ready = False
16961808 if self._start_time is not None:
16971809 self._run_time += (time.time() - self._start_time)
16981810 self._start_time = None
1699
1811
17001812 sock = getattr(self, "socket", None)
17011813 if sock:
17021814 if not isinstance(self.bind_addr, basestring):
17071819 x = sys.exc_info()[1]
17081820 if x.args[0] not in socket_errors_to_ignore:
17091821 # Changed to use error code and not message
1710 # See http://www.cherrypy.org/ticket/860.
1822 # See
1823 # https://bitbucket.org/cherrypy/cherrypy/issue/860.
17111824 raise
17121825 else:
17131826 # Note that we're explicitly NOT using AI_PASSIVE,
17201833 s = None
17211834 try:
17221835 s = socket.socket(af, socktype, proto)
1723 # See http://groups.google.com/group/cherrypy-users/
1724 # browse_frm/thread/bbfe5eb39c904fe0
1836 # See
1837 # http://groups.google.com/group/cherrypy-users/
1838 # browse_frm/thread/bbfe5eb39c904fe0
17251839 s.settimeout(1.0)
17261840 s.connect((host, port))
17271841 s.close()
17311845 if hasattr(sock, "close"):
17321846 sock.close()
17331847 self.socket = None
1734
1848
17351849 self.requests.stop(self.shutdown_timeout)
17361850
17371851
17381852 class Gateway(object):
1739 """A base class to interface HTTPServer with other systems, such as WSGI."""
1740
1853
1854 """A base class to interface HTTPServer with other systems, such as WSGI.
1855 """
1856
17411857 def __init__(self, req):
17421858 self.req = req
1743
1859
17441860 def respond(self):
17451861 """Process the current request. Must be overridden in a subclass."""
17461862 raise NotImplemented
17501866 # of such classes (in which case they will be lazily loaded).
17511867 ssl_adapters = {
17521868 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
1753 }
1869 }
1870
17541871
17551872 def get_ssl_adapter_class(name='builtin'):
17561873 """Return an SSL adapter class for the given name."""
17591876 last_dot = adapter.rfind(".")
17601877 attr_name = adapter[last_dot + 1:]
17611878 mod_path = adapter[:last_dot]
1762
1879
17631880 try:
17641881 mod = sys.modules[mod_path]
17651882 if mod is None:
17671884 except KeyError:
17681885 # The last [''] is important.
17691886 mod = __import__(mod_path, globals(), locals(), [''])
1770
1887
17711888 # Let an AttributeError propagate outward.
17721889 try:
17731890 adapter = getattr(mod, attr_name)
17741891 except AttributeError:
17751892 raise AttributeError("'%s' object has no attribute '%s'"
17761893 % (mod_path, attr_name))
1777
1894
17781895 return adapter
17791896
1780 # -------------------------------- WSGI Stuff -------------------------------- #
1897 # ------------------------------- WSGI Stuff -------------------------------- #
17811898
17821899
17831900 class CherryPyWSGIServer(HTTPServer):
1901
17841902 """A subclass of HTTPServer which calls a WSGI application."""
1785
1903
17861904 wsgi_version = (1, 0)
17871905 """The version of WSGI to produce."""
1788
1906
17891907 def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
17901908 max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
17911909 self.requests = ThreadPool(self, min=numthreads or 1, max=max)
17921910 self.wsgi_app = wsgi_app
17931911 self.gateway = wsgi_gateways[self.wsgi_version]
1794
1912
17951913 self.bind_addr = bind_addr
17961914 if not server_name:
17971915 server_name = socket.gethostname()
17981916 self.server_name = server_name
17991917 self.request_queue_size = request_queue_size
1800
1918
18011919 self.timeout = timeout
18021920 self.shutdown_timeout = shutdown_timeout
18031921 self.clear_stats()
1804
1922
18051923 def _get_numthreads(self):
18061924 return self.requests.min
1925
18071926 def _set_numthreads(self, value):
18081927 self.requests.min = value
18091928 numthreads = property(_get_numthreads, _set_numthreads)
18101929
18111930
18121931 class WSGIGateway(Gateway):
1932
18131933 """A base class to interface HTTPServer with WSGI."""
1814
1934
18151935 def __init__(self, req):
18161936 self.req = req
18171937 self.started_response = False
18181938 self.env = self.get_environ()
18191939 self.remaining_bytes_out = None
1820
1940
18211941 def get_environ(self):
18221942 """Return a new environ dict targeting the given wsgi.version"""
18231943 raise NotImplemented
1824
1944
18251945 def respond(self):
18261946 """Process the current request."""
18271947 response = self.req.server.wsgi_app(self.env, self.start_response)
18401960 finally:
18411961 if hasattr(response, "close"):
18421962 response.close()
1843
1844 def start_response(self, status, headers, exc_info = None):
1963
1964 def start_response(self, status, headers, exc_info=None):
18451965 """WSGI callable to begin the HTTP response."""
18461966 # "The application may call start_response more than once,
18471967 # if and only if the exc_info argument is provided."
18491969 raise AssertionError("WSGI start_response called a second "
18501970 "time with no exc_info.")
18511971 self.started_response = True
1852
1972
18531973 # "if exc_info is provided, and the HTTP headers have already been
18541974 # sent, start_response must raise an error, and should raise the
18551975 # exc_info tuple."
18691989
18701990 for k, v in headers:
18711991 if not isinstance(k, str):
1872 raise TypeError("WSGI response header key %r is not of type str." % k)
1992 raise TypeError(
1993 "WSGI response header key %r is not of type str." % k)
18731994 if not isinstance(v, str):
1874 raise TypeError("WSGI response header value %r is not of type str." % v)
1995 raise TypeError(
1996 "WSGI response header value %r is not of type str." % v)
18751997 if k.lower() == 'content-length':
18761998 self.remaining_bytes_out = int(v)
1877 self.req.outheaders.append((k.encode('ISO-8859-1'), v.encode('ISO-8859-1')))
1878
1999 self.req.outheaders.append(
2000 (k.encode('ISO-8859-1'), v.encode('ISO-8859-1')))
2001
18792002 return self.write
1880
2003
18812004 def write(self, chunk):
18822005 """WSGI callable to write unbuffered data to the client.
1883
2006
18842007 This method is also used internally by start_response (to write
18852008 data from the iterable returned by the WSGI application).
18862009 """
18872010 if not self.started_response:
18882011 raise AssertionError("WSGI write called before start_response.")
1889
2012
18902013 chunklen = len(chunk)
18912014 rbo = self.remaining_bytes_out
18922015 if rbo is not None and chunklen > rbo:
18932016 if not self.req.sent_headers:
18942017 # Whew. We can send a 500 to the client.
18952018 self.req.simple_response("500 Internal Server Error",
1896 "The requested resource returned more bytes than the "
1897 "declared Content-Length.")
2019 "The requested resource returned "
2020 "more bytes than the declared "
2021 "Content-Length.")
18982022 else:
18992023 # Dang. We have probably already sent data. Truncate the chunk
19002024 # to fit (so the client doesn't hang) and raise an error later.
19012025 chunk = chunk[:rbo]
1902
2026
19032027 if not self.req.sent_headers:
19042028 self.req.sent_headers = True
19052029 self.req.send_headers()
1906
2030
19072031 self.req.write(chunk)
1908
2032
19092033 if rbo is not None:
19102034 rbo -= chunklen
19112035 if rbo < 0:
19142038
19152039
19162040 class WSGIGateway_10(WSGIGateway):
2041
19172042 """A Gateway class to interface HTTPServer with WSGI 1.0.x."""
1918
2043
19192044 def get_environ(self):
19202045 """Return a new environ dict targeting the given wsgi.version"""
19212046 req = self.req
19292054 'REMOTE_ADDR': req.conn.remote_addr or '',
19302055 'REMOTE_PORT': str(req.conn.remote_port or ''),
19312056 'REQUEST_METHOD': req.method.decode('ISO-8859-1'),
1932 'REQUEST_URI': req.uri,
2057 'REQUEST_URI': req.uri.decode('ISO-8859-1'),
19332058 'SCRIPT_NAME': '',
19342059 'SERVER_NAME': req.server.server_name,
19352060 # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
19422067 'wsgi.run_once': False,
19432068 'wsgi.url_scheme': req.scheme.decode('ISO-8859-1'),
19442069 'wsgi.version': (1, 0),
1945 }
1946
2070 }
19472071 if isinstance(req.server.bind_addr, basestring):
19482072 # AF_UNIX. This isn't really allowed by WSGI, which doesn't
19492073 # address unix domain sockets. But it's better than nothing.
19502074 env["SERVER_PORT"] = ""
19512075 else:
19522076 env["SERVER_PORT"] = str(req.server.bind_addr[1])
1953
2077
19542078 # Request headers
19552079 for k, v in req.inheaders.items():
19562080 k = k.decode('ISO-8859-1').upper().replace("-", "_")
19572081 env["HTTP_" + k] = v.decode('ISO-8859-1')
1958
2082
19592083 # CONTENT_TYPE/CONTENT_LENGTH
19602084 ct = env.pop("HTTP_CONTENT_TYPE", None)
19612085 if ct is not None:
19632087 cl = env.pop("HTTP_CONTENT_LENGTH", None)
19642088 if cl is not None:
19652089 env["CONTENT_LENGTH"] = cl
1966
2090
19672091 if req.conn.ssl_env:
19682092 env.update(req.conn.ssl_env)
1969
2093
19702094 return env
19712095
19722096
19732097 class WSGIGateway_u0(WSGIGateway_10):
2098
19742099 """A Gateway class to interface HTTPServer with WSGI u.0.
1975
1976 WSGI u.0 is an experimental protocol, which uses unicode for keys and values
1977 in both Python 2 and Python 3.
2100
2101 WSGI u.0 is an experimental protocol, which uses unicode for keys
2102 and values in both Python 2 and Python 3.
19782103 """
1979
2104
19802105 def get_environ(self):
19812106 """Return a new environ dict targeting the given wsgi.version"""
19822107 req = self.req
19832108 env_10 = WSGIGateway_10.get_environ(self)
19842109 env = env_10.copy()
19852110 env['wsgi.version'] = ('u', 0)
1986
2111
19872112 # Request-URI
19882113 env.setdefault('wsgi.url_encoding', 'utf-8')
19892114 try:
19952120 env['wsgi.url_encoding'] = 'ISO-8859-1'
19962121 env["PATH_INFO"] = env_10["PATH_INFO"]
19972122 env["QUERY_STRING"] = env_10["QUERY_STRING"]
1998
2123
19992124 return env
20002125
20012126 wsgi_gateways = {
20032128 ('u', 0): WSGIGateway_u0,
20042129 }
20052130
2131
20062132 class WSGIPathInfoDispatcher(object):
2133
20072134 """A WSGI dispatcher for dispatch based on the PATH_INFO.
2008
2135
20092136 apps: a dict or list of (path_prefix, app) pairs.
20102137 """
2011
2138
20122139 def __init__(self, apps):
20132140 try:
20142141 apps = list(apps.items())
20152142 except AttributeError:
20162143 pass
2017
2144
20182145 # Sort the apps by len(path), descending
20192146 apps.sort()
20202147 apps.reverse()
2021
2148
20222149 # The path_prefix strings must start, but not end, with a slash.
20232150 # Use "" instead of "/".
20242151 self.apps = [(p.rstrip("/"), a) for p, a in apps]
2025
2152
20262153 def __call__(self, environ, start_response):
20272154 path = environ["PATH_INFO"] or "/"
20282155 for p, app in self.apps:
20322159 environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
20332160 environ["PATH_INFO"] = path[len(p):]
20342161 return app(environ, start_response)
2035
2162
20362163 start_response('404 Not Found', [('Content-Type', 'text/plain'),
20372164 ('Content-Length', '0')])
20382165 return ['']
2039
0 [egg_info]
1 tag_build =
2 tag_date = 0
3 tag_svn_revision = 0
4
0 [sdist]
1 formats = gztar,zip
2
3 [nosetests]
4 where = cherrypy
5 logging-filter = cherrypy
6 verbosity = 2
7 nocapture = True
8
9 [wheel]
10 universal = 1
11
12 [egg_info]
13 tag_build =
14 tag_svn_revision = 0
15 tag_date = 0
16
1313 from distutils.command.install import INSTALL_SCHEMES
1414 from distutils.command.build_py import build_py
1515 import sys
16 import os
1716 import re
1817
18
1919 class cherrypy_build_py(build_py):
20 "Custom version of build_py that selects Python-specific wsgiserver"
20 """Custom version of build_py that excludes Python-specific modules"""
21
2122 def build_module(self, module, module_file, package):
2223 python3 = sys.version_info >= (3,)
2324 if python3:
24 exclude_pattern = re.compile('wsgiserver2|ssl_pyopenssl')
25 exclude_pattern = re.compile('wsgiserver2|ssl_pyopenssl|'
26 '_cpcompat_subprocess')
2527 else:
2628 exclude_pattern = re.compile('wsgiserver3')
2729 if exclude_pattern.match(module):
28 return # skip it
30 return # skip it
2931 return build_py.build_module(self, module, module_file, package)
3032
3133
3335 # arguments for the setup command
3436 ###############################################################################
3537 name = "CherryPy"
36 version = "3.2.2"
38 version = "3.3.0"
3739 desc = "Object-Oriented HTTP framework"
3840 long_desc = "CherryPy is a pythonic, object-oriented HTTP framework"
39 classifiers=[
41 classifiers = [
4042 "Development Status :: 5 - Production/Stable",
4143 "Environment :: Web Environment",
4244 "Intended Audience :: Developers",
4345 "License :: Freely Distributable",
4446 "Operating System :: OS Independent",
47 "Framework :: CherryPy",
48 "License :: OSI Approved :: BSD License",
4549 "Programming Language :: Python",
4650 "Programming Language :: Python :: 2",
51 "Programming Language :: Python :: 2.3",
52 "Programming Language :: Python :: 2.4",
53 "Programming Language :: Python :: 2.5",
54 "Programming Language :: Python :: 2.6",
55 "Programming Language :: Python :: 2.7",
4756 "Programming Language :: Python :: 3",
57 "Programming Language :: Python :: 3.3",
58 "Programming Language :: Python :: Implementation",
59 "Programming Language :: Python :: Implementation :: CPython",
60 "Programming Language :: Python :: Implementation :: Jython",
61 "Programming Language :: Python :: Implementation :: PyPy",
4862 "Topic :: Internet :: WWW/HTTP",
4963 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
5064 "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
5367 "Topic :: Internet :: WWW/HTTP :: WSGI :: Server",
5468 "Topic :: Software Development :: Libraries :: Application Frameworks",
5569 ]
56 author="CherryPy Team"
57 author_email="team@cherrypy.org"
58 url="http://www.cherrypy.org"
59 cp_license="BSD"
60 packages=[
70 author = "CherryPy Team"
71 author_email = "team@cherrypy.org"
72 url = "http://www.cherrypy.org"
73 cp_license = "BSD"
74 packages = [
6175 "cherrypy", "cherrypy.lib",
6276 "cherrypy.tutorial", "cherrypy.test",
6377 "cherrypy.process",
6478 "cherrypy.scaffold",
6579 "cherrypy.wsgiserver",
6680 ]
67 download_url="http://download.cherrypy.org/cherrypy/3.2.2/"
68 data_files=[
81 data_files = [
6982 ('cherrypy', ['cherrypy/cherryd',
7083 'cherrypy/favicon.ico',
7184 'cherrypy/LICENSE.txt',
7487 ('cherrypy/scaffold', ['cherrypy/scaffold/example.conf',
7588 'cherrypy/scaffold/site.conf',
7689 ]),
77 ('cherrypy/scaffold/static', ['cherrypy/scaffold/static/made_with_cherrypy_small.png',
78 ]),
90 ('cherrypy/scaffold/static', [
91 'cherrypy/scaffold/static/made_with_cherrypy_small.png']),
7992 ('cherrypy/test', ['cherrypy/test/style.css',
8093 'cherrypy/test/test.pem',
8194 ]),
8295 ('cherrypy/test/static', ['cherrypy/test/static/index.html',
83 'cherrypy/test/static/dirback.jpg',]),
96 'cherrypy/test/static/dirback.jpg', ]),
8497 ('cherrypy/tutorial',
8598 [
8699 'cherrypy/tutorial/tutorial.conf',
88101 'cherrypy/tutorial/pdf_file.pdf',
89102 'cherrypy/tutorial/custom_error.html',
90103 ]
91 ),
104 ),
92105 ]
93106 scripts = ["cherrypy/cherryd"]
94107
95108 cmd_class = dict(
96 build_py = cherrypy_build_py,
109 build_py=cherrypy_build_py,
97110 )
98111
99112 if sys.version_info >= (3, 0):
107120
108121 # wininst may install data_files in Python/x.y instead of the cherrypy package.
109122 # Django's solution is at http://code.djangoproject.com/changeset/8313
110 # See also http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html
123 # See also
124 # http://mail.python.org/pipermail/distutils-sig/2004-August/004134.html
111125 if 'bdist_wininst' in sys.argv or '--format=wininst' in sys.argv:
112126 data_files = [(r'\PURELIB\%s' % path, files) for path, files in data_files]
127
113128
114129 def main():
115130 if sys.version < required_python_version:
121136 for scheme in list(INSTALL_SCHEMES.values()):
122137 scheme['data'] = scheme['purelib']
123138
124 dist = setup(
139 setup(
125140 name=name,
126141 version=version,
127142 description=desc,
132147 url=url,
133148 license=cp_license,
134149 packages=packages,
135 download_url=download_url,
136150 data_files=data_files,
137151 scripts=scripts,
138152 cmdclass=cmd_class,