Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/thirdparty/bottle/bottle.py
2992 views
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
"""
4
Bottle is a fast and simple micro-framework for small web applications. It
5
offers request dispatching (Routes) with URL parameter support, templates,
6
a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
7
template engines - all in a single file and with no dependencies other than the
8
Python Standard Library.
9
10
Homepage and documentation: http://bottlepy.org/
11
12
Copyright (c) 2009-2018, Marcel Hellkamp.
13
License: MIT (see LICENSE for details)
14
"""
15
16
from __future__ import print_function
17
import sys
18
19
__author__ = 'Marcel Hellkamp'
20
__version__ = '0.13-dev'
21
__license__ = 'MIT'
22
23
###############################################################################
24
# Command-line interface ######################################################
25
###############################################################################
26
# INFO: Some server adapters need to monkey-patch std-lib modules before they
27
# are imported. This is why some of the command-line handling is done here, but
28
# the actual call to _main() is at the end of the file.
29
30
31
def _cli_parse(args): # pragma: no coverage
32
from argparse import ArgumentParser
33
34
parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app")
35
opt = parser.add_argument
36
opt("--version", action="store_true", help="show version number.")
37
opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
38
opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
39
opt("-p", "--plugin", action="append", help="install additional plugin/s.")
40
opt("-c", "--conf", action="append", metavar="FILE",
41
help="load config values from FILE.")
42
opt("-C", "--param", action="append", metavar="NAME=VALUE",
43
help="override config values.")
44
opt("--debug", action="store_true", help="start server in debug mode.")
45
opt("--reload", action="store_true", help="auto-reload on file changes.")
46
opt('app', help='WSGI app entry point.', nargs='?')
47
48
cli_args = parser.parse_args(args[1:])
49
50
return cli_args, parser
51
52
53
def _cli_patch(cli_args): # pragma: no coverage
54
parsed_args, _ = _cli_parse(cli_args)
55
opts = parsed_args
56
if opts.server:
57
if opts.server.startswith('gevent'):
58
import gevent.monkey
59
gevent.monkey.patch_all()
60
elif opts.server.startswith('eventlet'):
61
import eventlet
62
eventlet.monkey_patch()
63
64
65
if __name__ == '__main__':
66
_cli_patch(sys.argv)
67
68
###############################################################################
69
# Imports and Python 2/3 unification ##########################################
70
###############################################################################
71
72
import base64, calendar, email.utils, functools, hmac, itertools,\
73
mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib
74
75
from types import FunctionType
76
from datetime import date as datedate, datetime, timedelta
77
from tempfile import NamedTemporaryFile
78
from traceback import format_exc, print_exc
79
from unicodedata import normalize
80
81
try:
82
from ujson import dumps as json_dumps, loads as json_lds
83
except ImportError:
84
from json import dumps as json_dumps, loads as json_lds
85
86
py = sys.version_info
87
py3k = py.major > 2
88
89
# Lots of stdlib and builtin differences.
90
if py3k:
91
import http.client as httplib
92
import _thread as thread
93
from urllib.parse import urljoin, SplitResult as UrlSplitResult
94
from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
95
urlunquote = functools.partial(urlunquote, encoding='latin1')
96
from http.cookies import SimpleCookie, Morsel, CookieError
97
from collections import defaultdict
98
from collections.abc import MutableMapping as DictMixin
99
from types import ModuleType as new_module
100
import pickle
101
from io import BytesIO
102
import configparser
103
# getfullargspec was deprecated in 3.5 and un-deprecated in 3.6
104
# getargspec was deprecated in 3.0 and removed in 3.11
105
from inspect import getfullargspec
106
def getargspec(func):
107
spec = getfullargspec(func)
108
kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs)
109
return kwargs, spec[1], spec[2], spec[3]
110
111
basestring = str
112
unicode = str
113
json_loads = lambda s: json_lds(touni(s))
114
callable = lambda x: hasattr(x, '__call__')
115
imap = map
116
117
def _raise(*a):
118
raise a[0](a[1]).with_traceback(a[2])
119
else: # 2.x
120
import httplib
121
import thread
122
from urlparse import urljoin, SplitResult as UrlSplitResult
123
from urllib import urlencode, quote as urlquote, unquote as urlunquote
124
from Cookie import SimpleCookie, Morsel, CookieError
125
from itertools import imap
126
import cPickle as pickle
127
from imp import new_module
128
from StringIO import StringIO as BytesIO
129
import ConfigParser as configparser
130
from collections import MutableMapping as DictMixin, defaultdict
131
from inspect import getargspec
132
133
unicode = unicode
134
json_loads = json_lds
135
exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
136
137
# Some helpers for string/byte handling
138
def tob(s, enc='utf8'):
139
if isinstance(s, unicode):
140
return s.encode(enc)
141
return b'' if s is None else bytes(s)
142
143
144
def touni(s, enc='utf8', err='strict'):
145
if isinstance(s, bytes):
146
return s.decode(enc, err)
147
return unicode("" if s is None else s)
148
149
150
tonat = touni if py3k else tob
151
152
153
def _stderr(*args):
154
try:
155
print(*args, file=sys.stderr)
156
except (IOError, AttributeError):
157
pass # Some environments do not allow printing (mod_wsgi)
158
159
160
# A bug in functools causes it to break if the wrapper is an instance method
161
def update_wrapper(wrapper, wrapped, *a, **ka):
162
try:
163
functools.update_wrapper(wrapper, wrapped, *a, **ka)
164
except AttributeError:
165
pass
166
167
# These helpers are used at module level and need to be defined first.
168
# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
169
170
171
def depr(major, minor, cause, fix):
172
text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\
173
"Cause: %s\n"\
174
"Fix: %s\n" % (major, minor, cause, fix)
175
if DEBUG == 'strict':
176
raise DeprecationWarning(text)
177
warnings.warn(text, DeprecationWarning, stacklevel=3)
178
return DeprecationWarning(text)
179
180
181
def makelist(data): # This is just too handy
182
if isinstance(data, (tuple, list, set, dict)):
183
return list(data)
184
elif data:
185
return [data]
186
else:
187
return []
188
189
190
class DictProperty(object):
191
""" Property that maps to a key in a local dict-like attribute. """
192
193
def __init__(self, attr, key=None, read_only=False):
194
self.attr, self.key, self.read_only = attr, key, read_only
195
196
def __call__(self, func):
197
functools.update_wrapper(self, func, updated=[])
198
self.getter, self.key = func, self.key or func.__name__
199
return self
200
201
def __get__(self, obj, cls):
202
if obj is None: return self
203
key, storage = self.key, getattr(obj, self.attr)
204
if key not in storage: storage[key] = self.getter(obj)
205
return storage[key]
206
207
def __set__(self, obj, value):
208
if self.read_only: raise AttributeError("Read-Only property.")
209
getattr(obj, self.attr)[self.key] = value
210
211
def __delete__(self, obj):
212
if self.read_only: raise AttributeError("Read-Only property.")
213
del getattr(obj, self.attr)[self.key]
214
215
216
class cached_property(object):
217
""" A property that is only computed once per instance and then replaces
218
itself with an ordinary attribute. Deleting the attribute resets the
219
property. """
220
221
def __init__(self, func):
222
update_wrapper(self, func)
223
self.func = func
224
225
def __get__(self, obj, cls):
226
if obj is None: return self
227
value = obj.__dict__[self.func.__name__] = self.func(obj)
228
return value
229
230
231
class lazy_attribute(object):
232
""" A property that caches itself to the class object. """
233
234
def __init__(self, func):
235
functools.update_wrapper(self, func, updated=[])
236
self.getter = func
237
238
def __get__(self, obj, cls):
239
value = self.getter(cls)
240
setattr(cls, self.__name__, value)
241
return value
242
243
244
###############################################################################
245
# Exceptions and Events #######################################################
246
###############################################################################
247
248
249
class BottleException(Exception):
250
""" A base class for exceptions used by bottle. """
251
pass
252
253
###############################################################################
254
# Routing ######################################################################
255
###############################################################################
256
257
258
class RouteError(BottleException):
259
""" This is a base class for all routing related exceptions """
260
261
262
class RouteReset(BottleException):
263
""" If raised by a plugin or request handler, the route is reset and all
264
plugins are re-applied. """
265
266
267
class RouterUnknownModeError(RouteError):
268
269
pass
270
271
272
class RouteSyntaxError(RouteError):
273
""" The route parser found something not supported by this router. """
274
275
276
class RouteBuildError(RouteError):
277
""" The route could not be built. """
278
279
280
def _re_flatten(p):
281
""" Turn all capturing groups in a regular expression pattern into
282
non-capturing groups. """
283
if '(' not in p:
284
return p
285
return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if
286
len(m.group(1)) % 2 else m.group(1) + '(?:', p)
287
288
289
class Router(object):
290
""" A Router is an ordered collection of route->target pairs. It is used to
291
efficiently match WSGI requests against a number of routes and return
292
the first target that satisfies the request. The target may be anything,
293
usually a string, ID or callable object. A route consists of a path-rule
294
and a HTTP method.
295
296
The path-rule is either a static path (e.g. `/contact`) or a dynamic
297
path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
298
and details on the matching order are described in docs:`routing`.
299
"""
300
301
default_pattern = '[^/]+'
302
default_filter = 're'
303
304
#: The current CPython regexp implementation does not allow more
305
#: than 99 matching groups per regular expression.
306
_MAX_GROUPS_PER_PATTERN = 99
307
308
def __init__(self, strict=False):
309
self.rules = [] # All rules in order
310
self._groups = {} # index of regexes to find them in dyna_routes
311
self.builder = {} # Data structure for the url builder
312
self.static = {} # Search structure for static routes
313
self.dyna_routes = {}
314
self.dyna_regexes = {} # Search structure for dynamic routes
315
#: If true, static routes are no longer checked first.
316
self.strict_order = strict
317
self.filters = {
318
're': lambda conf: (_re_flatten(conf or self.default_pattern),
319
None, None),
320
'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))),
321
'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))),
322
'path': lambda conf: (r'.+?', None, None)
323
}
324
325
def add_filter(self, name, func):
326
""" Add a filter. The provided function is called with the configuration
327
string as parameter and must return a (regexp, to_python, to_url) tuple.
328
The first element is a string, the last two are callables or None. """
329
self.filters[name] = func
330
331
rule_syntax = re.compile('(\\\\*)'
332
'(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'
333
'|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'
334
'(?::((?:\\\\.|[^\\\\>])+)?)?)?>))')
335
336
def _itertokens(self, rule):
337
offset, prefix = 0, ''
338
for match in self.rule_syntax.finditer(rule):
339
prefix += rule[offset:match.start()]
340
g = match.groups()
341
if g[2] is not None:
342
depr(0, 13, "Use of old route syntax.",
343
"Use <name> instead of :name in routes.")
344
if len(g[0]) % 2: # Escaped wildcard
345
prefix += match.group(0)[len(g[0]):]
346
offset = match.end()
347
continue
348
if prefix:
349
yield prefix, None, None
350
name, filtr, conf = g[4:7] if g[2] is None else g[1:4]
351
yield name, filtr or 'default', conf or None
352
offset, prefix = match.end(), ''
353
if offset <= len(rule) or prefix:
354
yield prefix + rule[offset:], None, None
355
356
def add(self, rule, method, target, name=None):
357
""" Add a new rule or replace the target for an existing rule. """
358
anons = 0 # Number of anonymous wildcards found
359
keys = [] # Names of keys
360
pattern = '' # Regular expression pattern with named groups
361
filters = [] # Lists of wildcard input filters
362
builder = [] # Data structure for the URL builder
363
is_static = True
364
365
for key, mode, conf in self._itertokens(rule):
366
if mode:
367
is_static = False
368
if mode == 'default': mode = self.default_filter
369
mask, in_filter, out_filter = self.filters[mode](conf)
370
if not key:
371
pattern += '(?:%s)' % mask
372
key = 'anon%d' % anons
373
anons += 1
374
else:
375
pattern += '(?P<%s>%s)' % (key, mask)
376
keys.append(key)
377
if in_filter: filters.append((key, in_filter))
378
builder.append((key, out_filter or str))
379
elif key:
380
pattern += re.escape(key)
381
builder.append((None, key))
382
383
self.builder[rule] = builder
384
if name: self.builder[name] = builder
385
386
if is_static and not self.strict_order:
387
self.static.setdefault(method, {})
388
self.static[method][self.build(rule)] = (target, None)
389
return
390
391
try:
392
re_pattern = re.compile('^(%s)$' % pattern)
393
re_match = re_pattern.match
394
except re.error as e:
395
raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
396
397
if filters:
398
399
def getargs(path):
400
url_args = re_match(path).groupdict()
401
for name, wildcard_filter in filters:
402
try:
403
url_args[name] = wildcard_filter(url_args[name])
404
except ValueError:
405
raise HTTPError(400, 'Path has wrong format.')
406
return url_args
407
elif re_pattern.groupindex:
408
409
def getargs(path):
410
return re_match(path).groupdict()
411
else:
412
getargs = None
413
414
flatpat = _re_flatten(pattern)
415
whole_rule = (rule, flatpat, target, getargs)
416
417
if (flatpat, method) in self._groups:
418
if DEBUG:
419
msg = 'Route <%s %s> overwrites a previously defined route'
420
warnings.warn(msg % (method, rule), RuntimeWarning)
421
self.dyna_routes[method][
422
self._groups[flatpat, method]] = whole_rule
423
else:
424
self.dyna_routes.setdefault(method, []).append(whole_rule)
425
self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1
426
427
self._compile(method)
428
429
def _compile(self, method):
430
all_rules = self.dyna_routes[method]
431
comborules = self.dyna_regexes[method] = []
432
maxgroups = self._MAX_GROUPS_PER_PATTERN
433
for x in range(0, len(all_rules), maxgroups):
434
some = all_rules[x:x + maxgroups]
435
combined = (flatpat for (_, flatpat, _, _) in some)
436
combined = '|'.join('(^%s$)' % flatpat for flatpat in combined)
437
combined = re.compile(combined).match
438
rules = [(target, getargs) for (_, _, target, getargs) in some]
439
comborules.append((combined, rules))
440
441
def build(self, _name, *anons, **query):
442
""" Build an URL by filling the wildcards in a rule. """
443
builder = self.builder.get(_name)
444
if not builder:
445
raise RouteBuildError("No route with that name.", _name)
446
try:
447
for i, value in enumerate(anons):
448
query['anon%d' % i] = value
449
url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder])
450
return url if not query else url + '?' + urlencode(query)
451
except KeyError as E:
452
raise RouteBuildError('Missing URL argument: %r' % E.args[0])
453
454
def match(self, environ):
455
""" Return a (target, url_args) tuple or raise HTTPError(400/404/405). """
456
verb = environ['REQUEST_METHOD'].upper()
457
path = environ['PATH_INFO'] or '/'
458
459
methods = ('PROXY', 'HEAD', 'GET', 'ANY') if verb == 'HEAD' else ('PROXY', verb, 'ANY')
460
461
for method in methods:
462
if method in self.static and path in self.static[method]:
463
target, getargs = self.static[method][path]
464
return target, getargs(path) if getargs else {}
465
elif method in self.dyna_regexes:
466
for combined, rules in self.dyna_regexes[method]:
467
match = combined(path)
468
if match:
469
target, getargs = rules[match.lastindex - 1]
470
return target, getargs(path) if getargs else {}
471
472
# No matching route found. Collect alternative methods for 405 response
473
allowed = set([])
474
nocheck = set(methods)
475
for method in set(self.static) - nocheck:
476
if path in self.static[method]:
477
allowed.add(method)
478
for method in set(self.dyna_regexes) - allowed - nocheck:
479
for combined, rules in self.dyna_regexes[method]:
480
match = combined(path)
481
if match:
482
allowed.add(method)
483
if allowed:
484
allow_header = ",".join(sorted(allowed))
485
raise HTTPError(405, "Method not allowed.", Allow=allow_header)
486
487
# No matching route and no alternative method found. We give up
488
raise HTTPError(404, "Not found: " + repr(path))
489
490
491
class Route(object):
492
""" This class wraps a route callback along with route specific metadata and
493
configuration and applies Plugins on demand. It is also responsible for
494
turning an URL path rule into a regular expression usable by the Router.
495
"""
496
497
def __init__(self, app, rule, method, callback,
498
name=None,
499
plugins=None,
500
skiplist=None, **config):
501
#: The application this route is installed to.
502
self.app = app
503
#: The path-rule string (e.g. ``/wiki/<page>``).
504
self.rule = rule
505
#: The HTTP method as a string (e.g. ``GET``).
506
self.method = method
507
#: The original callback with no plugins applied. Useful for introspection.
508
self.callback = callback
509
#: The name of the route (if specified) or ``None``.
510
self.name = name or None
511
#: A list of route-specific plugins (see :meth:`Bottle.route`).
512
self.plugins = plugins or []
513
#: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
514
self.skiplist = skiplist or []
515
#: Additional keyword arguments passed to the :meth:`Bottle.route`
516
#: decorator are stored in this dictionary. Used for route-specific
517
#: plugin configuration and meta-data.
518
self.config = app.config._make_overlay()
519
self.config.load_dict(config)
520
521
@cached_property
522
def call(self):
523
""" The route callback with all plugins applied. This property is
524
created on demand and then cached to speed up subsequent requests."""
525
return self._make_callback()
526
527
def reset(self):
528
""" Forget any cached values. The next time :attr:`call` is accessed,
529
all plugins are re-applied. """
530
self.__dict__.pop('call', None)
531
532
def prepare(self):
533
""" Do all on-demand work immediately (useful for debugging)."""
534
self.call
535
536
def all_plugins(self):
537
""" Yield all Plugins affecting this route. """
538
unique = set()
539
for p in reversed(self.app.plugins + self.plugins):
540
if True in self.skiplist: break
541
name = getattr(p, 'name', False)
542
if name and (name in self.skiplist or name in unique): continue
543
if p in self.skiplist or type(p) in self.skiplist: continue
544
if name: unique.add(name)
545
yield p
546
547
def _make_callback(self):
548
callback = self.callback
549
for plugin in self.all_plugins():
550
try:
551
if hasattr(plugin, 'apply'):
552
callback = plugin.apply(callback, self)
553
else:
554
callback = plugin(callback)
555
except RouteReset: # Try again with changed configuration.
556
return self._make_callback()
557
if callback is not self.callback:
558
update_wrapper(callback, self.callback)
559
return callback
560
561
def get_undecorated_callback(self):
562
""" Return the callback. If the callback is a decorated function, try to
563
recover the original function. """
564
func = self.callback
565
func = getattr(func, '__func__' if py3k else 'im_func', func)
566
closure_attr = '__closure__' if py3k else 'func_closure'
567
while hasattr(func, closure_attr) and getattr(func, closure_attr):
568
attributes = getattr(func, closure_attr)
569
func = attributes[0].cell_contents
570
571
# in case of decorators with multiple arguments
572
if not isinstance(func, FunctionType):
573
# pick first FunctionType instance from multiple arguments
574
func = filter(lambda x: isinstance(x, FunctionType),
575
map(lambda x: x.cell_contents, attributes))
576
func = list(func)[0] # py3 support
577
return func
578
579
def get_callback_args(self):
580
""" Return a list of argument names the callback (most likely) accepts
581
as keyword arguments. If the callback is a decorated function, try
582
to recover the original function before inspection. """
583
return getargspec(self.get_undecorated_callback())[0]
584
585
def get_config(self, key, default=None):
586
""" Lookup a config field and return its value, first checking the
587
route.config, then route.app.config."""
588
depr(0, 13, "Route.get_config() is deprecated.",
589
"The Route.config property already includes values from the"
590
" application config for missing keys. Access it directly.")
591
return self.config.get(key, default)
592
593
def __repr__(self):
594
cb = self.get_undecorated_callback()
595
return '<%s %s -> %s:%s>' % (self.method, self.rule, cb.__module__, cb.__name__)
596
597
###############################################################################
598
# Application Object ###########################################################
599
###############################################################################
600
601
602
class Bottle(object):
603
""" Each Bottle object represents a single, distinct web application and
604
consists of routes, callbacks, plugins, resources and configuration.
605
Instances are callable WSGI applications.
606
607
:param catchall: If true (default), handle all exceptions. Turn off to
608
let debugging middleware handle exceptions.
609
"""
610
611
@lazy_attribute
612
def _global_config(cls):
613
cfg = ConfigDict()
614
cfg.meta_set('catchall', 'validate', bool)
615
return cfg
616
617
def __init__(self, **kwargs):
618
#: A :class:`ConfigDict` for app specific configuration.
619
self.config = self._global_config._make_overlay()
620
self.config._add_change_listener(
621
functools.partial(self.trigger_hook, 'config'))
622
623
self.config.update({
624
"catchall": True
625
})
626
627
if kwargs.get('catchall') is False:
628
depr(0, 13, "Bottle(catchall) keyword argument.",
629
"The 'catchall' setting is now part of the app "
630
"configuration. Fix: `app.config['catchall'] = False`")
631
self.config['catchall'] = False
632
if kwargs.get('autojson') is False:
633
depr(0, 13, "Bottle(autojson) keyword argument.",
634
"The 'autojson' setting is now part of the app "
635
"configuration. Fix: `app.config['json.enable'] = False`")
636
self.config['json.disable'] = True
637
638
self._mounts = []
639
640
#: A :class:`ResourceManager` for application files
641
self.resources = ResourceManager()
642
643
self.routes = [] # List of installed :class:`Route` instances.
644
self.router = Router() # Maps requests to :class:`Route` instances.
645
self.error_handler = {}
646
647
# Core plugins
648
self.plugins = [] # List of installed plugins.
649
self.install(JSONPlugin())
650
self.install(TemplatePlugin())
651
652
#: If true, most exceptions are caught and returned as :exc:`HTTPError`
653
catchall = DictProperty('config', 'catchall')
654
655
__hook_names = 'before_request', 'after_request', 'app_reset', 'config'
656
__hook_reversed = {'after_request'}
657
658
@cached_property
659
def _hooks(self):
660
return dict((name, []) for name in self.__hook_names)
661
662
def add_hook(self, name, func):
663
""" Attach a callback to a hook. Three hooks are currently implemented:
664
665
before_request
666
Executed once before each request. The request context is
667
available, but no routing has happened yet.
668
after_request
669
Executed once after each request regardless of its outcome.
670
app_reset
671
Called whenever :meth:`Bottle.reset` is called.
672
"""
673
if name in self.__hook_reversed:
674
self._hooks[name].insert(0, func)
675
else:
676
self._hooks[name].append(func)
677
678
def remove_hook(self, name, func):
679
""" Remove a callback from a hook. """
680
if name in self._hooks and func in self._hooks[name]:
681
self._hooks[name].remove(func)
682
return True
683
684
def trigger_hook(self, __name, *args, **kwargs):
685
""" Trigger a hook and return a list of results. """
686
return [hook(*args, **kwargs) for hook in self._hooks[__name][:]]
687
688
def hook(self, name):
689
""" Return a decorator that attaches a callback to a hook. See
690
:meth:`add_hook` for details."""
691
692
def decorator(func):
693
self.add_hook(name, func)
694
return func
695
696
return decorator
697
698
def _mount_wsgi(self, prefix, app, **options):
699
segments = [p for p in prefix.split('/') if p]
700
if not segments:
701
raise ValueError('WSGI applications cannot be mounted to "/".')
702
path_depth = len(segments)
703
704
def mountpoint_wrapper():
705
try:
706
request.path_shift(path_depth)
707
rs = HTTPResponse([])
708
709
def start_response(status, headerlist, exc_info=None):
710
if exc_info:
711
_raise(*exc_info)
712
if py3k:
713
# Errors here mean that the mounted WSGI app did not
714
# follow PEP-3333 (which requires latin1) or used a
715
# pre-encoding other than utf8 :/
716
status = status.encode('latin1').decode('utf8')
717
headerlist = [(k, v.encode('latin1').decode('utf8'))
718
for (k, v) in headerlist]
719
rs.status = status
720
for name, value in headerlist:
721
rs.add_header(name, value)
722
return rs.body.append
723
724
body = app(request.environ, start_response)
725
rs.body = itertools.chain(rs.body, body) if rs.body else body
726
return rs
727
finally:
728
request.path_shift(-path_depth)
729
730
options.setdefault('skip', True)
731
options.setdefault('method', 'PROXY')
732
options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
733
options['callback'] = mountpoint_wrapper
734
735
self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
736
if not prefix.endswith('/'):
737
self.route('/' + '/'.join(segments), **options)
738
739
def _mount_app(self, prefix, app, **options):
740
if app in self._mounts or '_mount.app' in app.config:
741
depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.",
742
"Clone application before mounting to a different location.")
743
return self._mount_wsgi(prefix, app, **options)
744
745
if options:
746
depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.",
747
"Do not specify any route options when mounting bottle application.")
748
return self._mount_wsgi(prefix, app, **options)
749
750
if not prefix.endswith("/"):
751
depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.",
752
"Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.")
753
return self._mount_wsgi(prefix, app, **options)
754
755
self._mounts.append(app)
756
app.config['_mount.prefix'] = prefix
757
app.config['_mount.app'] = self
758
for route in app.routes:
759
route.rule = prefix + route.rule.lstrip('/')
760
self.add_route(route)
761
762
def mount(self, prefix, app, **options):
763
""" Mount an application (:class:`Bottle` or plain WSGI) to a specific
764
URL prefix. Example::
765
766
parent_app.mount('/prefix/', child_app)
767
768
:param prefix: path prefix or `mount-point`.
769
:param app: an instance of :class:`Bottle` or a WSGI application.
770
771
Plugins from the parent application are not applied to the routes
772
of the mounted child application. If you need plugins in the child
773
application, install them separately.
774
775
While it is possible to use path wildcards within the prefix path
776
(:class:`Bottle` childs only), it is highly discouraged.
777
778
The prefix path must end with a slash. If you want to access the
779
root of the child application via `/prefix` in addition to
780
`/prefix/`, consider adding a route with a 307 redirect to the
781
parent application.
782
"""
783
784
if not prefix.startswith('/'):
785
raise ValueError("Prefix must start with '/'")
786
787
if isinstance(app, Bottle):
788
return self._mount_app(prefix, app, **options)
789
else:
790
return self._mount_wsgi(prefix, app, **options)
791
792
def merge(self, routes):
793
""" Merge the routes of another :class:`Bottle` application or a list of
794
:class:`Route` objects into this application. The routes keep their
795
'owner', meaning that the :data:`Route.app` attribute is not
796
changed. """
797
if isinstance(routes, Bottle):
798
routes = routes.routes
799
for route in routes:
800
self.add_route(route)
801
802
def install(self, plugin):
803
""" Add a plugin to the list of plugins and prepare it for being
804
applied to all routes of this application. A plugin may be a simple
805
decorator or an object that implements the :class:`Plugin` API.
806
"""
807
if hasattr(plugin, 'setup'): plugin.setup(self)
808
if not callable(plugin) and not hasattr(plugin, 'apply'):
809
raise TypeError("Plugins must be callable or implement .apply()")
810
self.plugins.append(plugin)
811
self.reset()
812
return plugin
813
814
def uninstall(self, plugin):
815
""" Uninstall plugins. Pass an instance to remove a specific plugin, a type
816
object to remove all plugins that match that type, a string to remove
817
all plugins with a matching ``name`` attribute or ``True`` to remove all
818
plugins. Return the list of removed plugins. """
819
removed, remove = [], plugin
820
for i, plugin in list(enumerate(self.plugins))[::-1]:
821
if remove is True or remove is plugin or remove is type(plugin) \
822
or getattr(plugin, 'name', True) == remove:
823
removed.append(plugin)
824
del self.plugins[i]
825
if hasattr(plugin, 'close'): plugin.close()
826
if removed: self.reset()
827
return removed
828
829
def reset(self, route=None):
830
""" Reset all routes (force plugins to be re-applied) and clear all
831
caches. If an ID or route object is given, only that specific route
832
is affected. """
833
if route is None: routes = self.routes
834
elif isinstance(route, Route): routes = [route]
835
else: routes = [self.routes[route]]
836
for route in routes:
837
route.reset()
838
if DEBUG:
839
for route in routes:
840
route.prepare()
841
self.trigger_hook('app_reset')
842
843
def close(self):
844
""" Close the application and all installed plugins. """
845
for plugin in self.plugins:
846
if hasattr(plugin, 'close'): plugin.close()
847
848
def run(self, **kwargs):
849
""" Calls :func:`run` with the same parameters. """
850
run(self, **kwargs)
851
852
def match(self, environ):
853
""" Search for a matching route and return a (:class:`Route`, urlargs)
854
tuple. The second value is a dictionary with parameters extracted
855
from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
856
return self.router.match(environ)
857
858
def get_url(self, routename, **kargs):
859
""" Return a string that matches a named route """
860
scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
861
location = self.router.build(routename, **kargs).lstrip('/')
862
return urljoin(urljoin('/', scriptname), location)
863
864
def add_route(self, route):
865
""" Add a route object, but do not change the :data:`Route.app`
866
attribute."""
867
self.routes.append(route)
868
self.router.add(route.rule, route.method, route, name=route.name)
869
if DEBUG: route.prepare()
870
871
def route(self,
872
path=None,
873
method='GET',
874
callback=None,
875
name=None,
876
apply=None,
877
skip=None, **config):
878
""" A decorator to bind a function to a request URL. Example::
879
880
@app.route('/hello/<name>')
881
def hello(name):
882
return 'Hello %s' % name
883
884
The ``<name>`` part is a wildcard. See :class:`Router` for syntax
885
details.
886
887
:param path: Request path or a list of paths to listen to. If no
888
path is specified, it is automatically generated from the
889
signature of the function.
890
:param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of
891
methods to listen to. (default: `GET`)
892
:param callback: An optional shortcut to avoid the decorator
893
syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
894
:param name: The name for this route. (default: None)
895
:param apply: A decorator or plugin or a list of plugins. These are
896
applied to the route callback in addition to installed plugins.
897
:param skip: A list of plugins, plugin classes or names. Matching
898
plugins are not installed to this route. ``True`` skips all.
899
900
Any additional keyword arguments are stored as route-specific
901
configuration and passed to plugins (see :meth:`Plugin.apply`).
902
"""
903
if callable(path): path, callback = None, path
904
plugins = makelist(apply)
905
skiplist = makelist(skip)
906
907
def decorator(callback):
908
if isinstance(callback, basestring): callback = load(callback)
909
for rule in makelist(path) or yieldroutes(callback):
910
for verb in makelist(method):
911
verb = verb.upper()
912
route = Route(self, rule, verb, callback,
913
name=name,
914
plugins=plugins,
915
skiplist=skiplist, **config)
916
self.add_route(route)
917
return callback
918
919
return decorator(callback) if callback else decorator
920
921
def get(self, path=None, method='GET', **options):
922
""" Equals :meth:`route`. """
923
return self.route(path, method, **options)
924
925
def post(self, path=None, method='POST', **options):
926
""" Equals :meth:`route` with a ``POST`` method parameter. """
927
return self.route(path, method, **options)
928
929
def put(self, path=None, method='PUT', **options):
930
""" Equals :meth:`route` with a ``PUT`` method parameter. """
931
return self.route(path, method, **options)
932
933
def delete(self, path=None, method='DELETE', **options):
934
""" Equals :meth:`route` with a ``DELETE`` method parameter. """
935
return self.route(path, method, **options)
936
937
def patch(self, path=None, method='PATCH', **options):
938
""" Equals :meth:`route` with a ``PATCH`` method parameter. """
939
return self.route(path, method, **options)
940
941
def error(self, code=500, callback=None):
942
""" Register an output handler for a HTTP error code. Can
943
be used as a decorator or called directly ::
944
945
def error_handler_500(error):
946
return 'error_handler_500'
947
948
app.error(code=500, callback=error_handler_500)
949
950
@app.error(404)
951
def error_handler_404(error):
952
return 'error_handler_404'
953
954
"""
955
956
def decorator(callback):
957
if isinstance(callback, basestring): callback = load(callback)
958
self.error_handler[int(code)] = callback
959
return callback
960
961
return decorator(callback) if callback else decorator
962
963
def default_error_handler(self, res):
964
return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE')))
965
966
def _handle(self, environ):
967
path = environ['bottle.raw_path'] = environ['PATH_INFO']
968
if py3k:
969
environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore')
970
971
environ['bottle.app'] = self
972
request.bind(environ)
973
response.bind()
974
975
try:
976
while True: # Remove in 0.14 together with RouteReset
977
out = None
978
try:
979
self.trigger_hook('before_request')
980
route, args = self.router.match(environ)
981
environ['route.handle'] = route
982
environ['bottle.route'] = route
983
environ['route.url_args'] = args
984
out = route.call(**args)
985
break
986
except HTTPResponse as E:
987
out = E
988
break
989
except RouteReset:
990
depr(0, 13, "RouteReset exception deprecated",
991
"Call route.call() after route.reset() and "
992
"return the result.")
993
route.reset()
994
continue
995
finally:
996
if isinstance(out, HTTPResponse):
997
out.apply(response)
998
try:
999
self.trigger_hook('after_request')
1000
except HTTPResponse as E:
1001
out = E
1002
out.apply(response)
1003
except (KeyboardInterrupt, SystemExit, MemoryError):
1004
raise
1005
except Exception as E:
1006
if not self.catchall: raise
1007
stacktrace = format_exc()
1008
environ['wsgi.errors'].write(stacktrace)
1009
environ['wsgi.errors'].flush()
1010
environ['bottle.exc_info'] = sys.exc_info()
1011
out = HTTPError(500, "Internal Server Error", E, stacktrace)
1012
out.apply(response)
1013
1014
return out
1015
1016
def _cast(self, out, peek=None):
1017
""" Try to convert the parameter into something WSGI compatible and set
1018
correct HTTP headers when possible.
1019
Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
1020
iterable of strings and iterable of unicodes
1021
"""
1022
1023
# Empty output is done here
1024
if not out:
1025
if 'Content-Length' not in response:
1026
response['Content-Length'] = 0
1027
return []
1028
# Join lists of byte or unicode strings. Mixed lists are NOT supported
1029
if isinstance(out, (tuple, list))\
1030
and isinstance(out[0], (bytes, unicode)):
1031
out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
1032
# Encode unicode strings
1033
if isinstance(out, unicode):
1034
out = out.encode(response.charset)
1035
# Byte Strings are just returned
1036
if isinstance(out, bytes):
1037
if 'Content-Length' not in response:
1038
response['Content-Length'] = len(out)
1039
return [out]
1040
# HTTPError or HTTPException (recursive, because they may wrap anything)
1041
# TODO: Handle these explicitly in handle() or make them iterable.
1042
if isinstance(out, HTTPError):
1043
out.apply(response)
1044
out = self.error_handler.get(out.status_code,
1045
self.default_error_handler)(out)
1046
return self._cast(out)
1047
if isinstance(out, HTTPResponse):
1048
out.apply(response)
1049
return self._cast(out.body)
1050
1051
# File-like objects.
1052
if hasattr(out, 'read'):
1053
if 'wsgi.file_wrapper' in request.environ:
1054
return request.environ['wsgi.file_wrapper'](out)
1055
elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
1056
return WSGIFileWrapper(out)
1057
1058
# Handle Iterables. We peek into them to detect their inner type.
1059
try:
1060
iout = iter(out)
1061
first = next(iout)
1062
while not first:
1063
first = next(iout)
1064
except StopIteration:
1065
return self._cast('')
1066
except HTTPResponse as E:
1067
first = E
1068
except (KeyboardInterrupt, SystemExit, MemoryError):
1069
raise
1070
except Exception as error:
1071
if not self.catchall: raise
1072
first = HTTPError(500, 'Unhandled exception', error, format_exc())
1073
1074
# These are the inner types allowed in iterator or generator objects.
1075
if isinstance(first, HTTPResponse):
1076
return self._cast(first)
1077
elif isinstance(first, bytes):
1078
new_iter = itertools.chain([first], iout)
1079
elif isinstance(first, unicode):
1080
encoder = lambda x: x.encode(response.charset)
1081
new_iter = imap(encoder, itertools.chain([first], iout))
1082
else:
1083
msg = 'Unsupported response type: %s' % type(first)
1084
return self._cast(HTTPError(500, msg))
1085
if hasattr(out, 'close'):
1086
new_iter = _closeiter(new_iter, out.close)
1087
return new_iter
1088
1089
def wsgi(self, environ, start_response):
1090
""" The bottle WSGI-interface. """
1091
try:
1092
out = self._cast(self._handle(environ))
1093
# rfc2616 section 4.3
1094
if response._status_code in (100, 101, 204, 304)\
1095
or environ['REQUEST_METHOD'] == 'HEAD':
1096
if hasattr(out, 'close'): out.close()
1097
out = []
1098
exc_info = environ.get('bottle.exc_info')
1099
if exc_info is not None:
1100
del environ['bottle.exc_info']
1101
start_response(response._wsgi_status_line(), response.headerlist, exc_info)
1102
return out
1103
except (KeyboardInterrupt, SystemExit, MemoryError):
1104
raise
1105
except Exception as E:
1106
if not self.catchall: raise
1107
err = '<h1>Critical error while processing request: %s</h1>' \
1108
% html_escape(environ.get('PATH_INFO', '/'))
1109
if DEBUG:
1110
err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1111
'<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1112
% (html_escape(repr(E)), html_escape(format_exc()))
1113
environ['wsgi.errors'].write(err)
1114
environ['wsgi.errors'].flush()
1115
headers = [('Content-Type', 'text/html; charset=UTF-8')]
1116
start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
1117
return [tob(err)]
1118
1119
def __call__(self, environ, start_response):
1120
""" Each instance of :class:'Bottle' is a WSGI application. """
1121
return self.wsgi(environ, start_response)
1122
1123
def __enter__(self):
1124
""" Use this application as default for all module-level shortcuts. """
1125
default_app.push(self)
1126
return self
1127
1128
def __exit__(self, exc_type, exc_value, traceback):
1129
default_app.pop()
1130
1131
def __setattr__(self, name, value):
1132
if name in self.__dict__:
1133
raise AttributeError("Attribute %s already defined. Plugin conflict?" % name)
1134
self.__dict__[name] = value
1135
1136
1137
###############################################################################
1138
# HTTP and WSGI Tools ##########################################################
1139
###############################################################################
1140
1141
# Multipart parsing stuff
1142
1143
class StopMarkupException(BottleException):
1144
pass
1145
1146
1147
HYPHEN = tob('-')
1148
CR = tob('\r')
1149
LF = tob('\n')
1150
CRLF = CR + LF
1151
LFCRLF = LF + CR + LF
1152
HYPHENx2 = HYPHEN * 2
1153
CRLFx2 = CRLF * 2
1154
CRLF_LEN = len(CRLF)
1155
CRLFx2_LEN = len(CRLFx2)
1156
1157
MULTIPART_BOUNDARY_PATT = re.compile(r'^multipart/.+?boundary=(.+?)(;|$)')
1158
1159
class MPHeadersEaeter:
1160
end_headers_patt = re.compile(tob(r'(\r\n\r\n)|(\r(\n\r?)?)$'))
1161
1162
def __init__(self):
1163
self.headers_end_expected = None
1164
self.eat_meth = self._eat_first_crlf_or_last_hyphens
1165
self._meth_map = {
1166
CR: self._eat_lf,
1167
HYPHEN: self._eat_last_hyphen
1168
}
1169
self.stopped = False
1170
1171
def eat(self, chunk, base):
1172
pos = self.eat_meth(chunk, base)
1173
if pos is None: return
1174
if self.eat_meth != self._eat_headers:
1175
if self.stopped:
1176
raise StopMarkupException()
1177
base = pos
1178
self.eat_meth = self._eat_headers
1179
return self.eat(chunk, base)
1180
# found headers section end, reset eater
1181
self.eat_meth = self._eat_first_crlf_or_last_hyphens
1182
return pos
1183
1184
def _eat_last_hyphen(self, chunk, base):
1185
chunk_start = chunk[base: base + 2]
1186
if not chunk_start: return
1187
if chunk_start == HYPHEN:
1188
self.stopped = True
1189
return base + 1
1190
raise HTTPError(422, 'Last hyphen was expected, got (first 2 symbols slice): %s' % chunk_start)
1191
1192
def _eat_lf(self, chunk, base):
1193
chunk_start = chunk[base: base + 1]
1194
if not chunk_start: return
1195
if chunk_start == LF: return base + 1
1196
invalid_sequence = CR + chunk_start
1197
raise HTTPError(422, 'Malformed headers, found invalid sequence: %s' % invalid_sequence)
1198
1199
def _eat_first_crlf_or_last_hyphens(self, chunk, base):
1200
chunk_start = chunk[base: base + 2]
1201
if not chunk_start: return
1202
if chunk_start == CRLF: return base + 2
1203
if len(chunk_start) == 1:
1204
self.eat_meth = self._meth_map.get(chunk_start)
1205
elif chunk_start == HYPHENx2:
1206
self.stopped = True
1207
return base + 2
1208
if self.eat_meth is None:
1209
raise HTTPError(422, 'Malformed headers, invalid section start: %s' % chunk_start)
1210
1211
def _eat_headers(self, chunk, base):
1212
expected = self.headers_end_expected
1213
if expected is not None:
1214
expected_len = len(expected)
1215
chunk_start = chunk[base:expected_len]
1216
if chunk_start == expected:
1217
self.headers_end_expected = None
1218
return base + expected_len - CRLFx2_LEN
1219
chunk_start_len = len(chunk_start)
1220
if not chunk_start_len: return
1221
if chunk_start_len < expected_len:
1222
if expected.startswith(chunk_start):
1223
self.headers_end_expected = expected[chunk_start_len:]
1224
return
1225
self.headers_end_expected = None
1226
if expected == LF: # we saw CRLFCR
1227
invalid_sequence = CR + chunk_start[0:1]
1228
# NOTE we don not catch all CRLF-malformed errors, but only obvious ones
1229
# to stop doing useless work
1230
raise HTTPError(422, 'Malformed headers, found invalid sequence: %s' % invalid_sequence)
1231
else:
1232
assert expected_len >= 2 # (CR)LFCRLF or (CRLF)CRLF
1233
self.headers_end_expected = None
1234
assert self.headers_end_expected is None
1235
s = self.end_headers_patt.search(chunk, base)
1236
if s is None: return
1237
end_found = s.start(1)
1238
if end_found >= 0: return end_found
1239
end_head = s.group(2)
1240
if end_head is not None:
1241
self.headers_end_expected = CRLFx2[len(end_head):]
1242
1243
1244
class MPBodyMarkup:
1245
def __init__(self, boundary):
1246
self.markups = []
1247
self.error = None
1248
if CR in boundary:
1249
raise HTTPError(422, 'The `CR` must not be in the boundary: %s' % boundary)
1250
boundary = HYPHENx2 + boundary
1251
self.boundary = boundary
1252
token = CRLF + boundary
1253
self.tlen = len(token)
1254
self.token = token
1255
self.trest = self.trest_len = None
1256
self.abspos = 0
1257
self.abs_start_section = 0
1258
self.headers_eater = MPHeadersEaeter()
1259
self.cur_meth = self._eat_start_boundary
1260
self._eat_headers = self.headers_eater.eat
1261
self.stopped = False
1262
self.idx = idx = defaultdict(list) # 1-based indices for each token symbol
1263
for i, c in enumerate(token, start=1):
1264
idx[c].append([i, token[:i]])
1265
1266
def _match_tail(self, s, start, end):
1267
idxs = self.idx.get(s[end - 1])
1268
if idxs is None: return
1269
slen = end - start
1270
assert slen <= self.tlen
1271
for i, thead in idxs: # idxs is 1-based index
1272
search_pos = slen - i
1273
if search_pos < 0: return
1274
if s[start + search_pos:end] == thead: return i # if s_tail == token_head
1275
1276
def _iter_markup(self, chunk):
1277
if self.stopped:
1278
raise StopMarkupException()
1279
cur_meth = self.cur_meth
1280
abs_start_section = self.abs_start_section
1281
start_next_sec = 0
1282
skip_start = 0
1283
tlen = self.tlen
1284
eat_data, eat_headers = self._eat_data, self._eat_headers
1285
while True:
1286
try:
1287
end_section = cur_meth(chunk, start_next_sec)
1288
except StopMarkupException:
1289
self.stopped = True
1290
return
1291
if end_section is None: break
1292
if cur_meth == eat_headers:
1293
sec_name = 'headers'
1294
start_next_sec = end_section + CRLFx2_LEN
1295
cur_meth = eat_data
1296
skip_start = 0
1297
elif cur_meth == eat_data:
1298
sec_name = 'data'
1299
start_next_sec = end_section + tlen
1300
skip_start = CRLF_LEN
1301
cur_meth = eat_headers
1302
else:
1303
assert cur_meth == self._eat_start_boundary
1304
sec_name = 'data'
1305
start_next_sec = end_section + tlen
1306
skip_start = CRLF_LEN
1307
cur_meth = eat_headers
1308
1309
# if the body starts with a hyphen,
1310
# we will have a negative abs_end_section equal to the length of the CRLF
1311
abs_end_section = self.abspos + end_section
1312
if abs_end_section < 0:
1313
assert abs_end_section == -CRLF_LEN
1314
end_section = -self.abspos
1315
yield sec_name, (abs_start_section, self.abspos + end_section)
1316
abs_start_section = self.abspos + start_next_sec + skip_start
1317
self.abspos += len(chunk)
1318
self.cur_meth = cur_meth
1319
self.abs_start_section = abs_start_section
1320
1321
def _eat_start_boundary(self, chunk, base):
1322
if self.trest is None:
1323
chunk_start = chunk[base: base + 1]
1324
if not chunk_start: return
1325
if chunk_start == CR: return self._eat_data(chunk, base)
1326
boundary = self.boundary
1327
if chunk.startswith(boundary): return base - CRLF_LEN
1328
if chunk_start != boundary[:1]:
1329
raise HTTPError(
1330
422, 'Invalid multipart/formdata body start, expected hyphen or CR, got: %s' % chunk_start)
1331
self.trest = boundary
1332
self.trest_len = len(boundary)
1333
end_section = self._eat_data(chunk, base)
1334
if end_section is not None: return end_section
1335
1336
def _eat_data(self, chunk, base):
1337
chunk_len = len(chunk)
1338
token, tlen, trest, trest_len = self.token, self.tlen, self.trest, self.trest_len
1339
start = base
1340
match_tail = self._match_tail
1341
part = None
1342
while True:
1343
end = start + tlen
1344
if end > chunk_len:
1345
part = chunk[start:]
1346
break
1347
if trest is not None:
1348
if chunk[start:start + trest_len] == trest:
1349
data_end = start + trest_len - tlen
1350
self.trest_len = self.trest = None
1351
return data_end
1352
else:
1353
trest_len = trest = None
1354
matched_len = match_tail(chunk, start, end)
1355
if matched_len is not None:
1356
if matched_len == tlen:
1357
self.trest_len = self.trest = None
1358
return start
1359
else:
1360
trest_len, trest = tlen - matched_len, token[matched_len:]
1361
start += tlen
1362
# process the tail of the chunk
1363
if part:
1364
part_len = len(part)
1365
if trest is not None:
1366
if part_len < trest_len:
1367
if trest.startswith(part):
1368
trest_len -= part_len
1369
trest = trest[part_len:]
1370
part = None
1371
else:
1372
trest_len = trest = None
1373
else:
1374
if part.startswith(trest):
1375
data_end = start + trest_len - tlen
1376
self.trest_len = self.trest = None
1377
return data_end
1378
trest_len = trest = None
1379
1380
if part is not None:
1381
assert trest is None
1382
matched_len = match_tail(part, 0, part_len)
1383
if matched_len is not None:
1384
trest_len, trest = tlen - matched_len, token[matched_len:]
1385
self.trest_len, self.trest = trest_len, trest
1386
1387
def _parse(self, chunk):
1388
for name, start_end in self._iter_markup(chunk):
1389
self.markups.append([name, start_end])
1390
1391
def parse(self, chunk):
1392
if self.error is not None: return
1393
try:
1394
self._parse(chunk)
1395
except Exception as exc:
1396
self.error = exc
1397
1398
1399
class MPBytesIOProxy:
1400
def __init__(self, src, start, end):
1401
self._src = src
1402
self._st = start
1403
self._end = end
1404
self._pos = start
1405
1406
def tell(self):
1407
return self._pos - self._st
1408
1409
def seek(self, pos):
1410
if pos < 0: pos = 0
1411
self._pos = min(self._st + pos, self._end)
1412
1413
def read(self, sz=None):
1414
max_sz = self._end - self._pos
1415
if max_sz <= 0:
1416
return tob('')
1417
if sz is not None and sz > 0:
1418
sz = min(sz, max_sz)
1419
else:
1420
sz = max_sz
1421
self._src.seek(self._pos)
1422
self._pos += sz
1423
return self._src.read(sz)
1424
1425
def writable(self):
1426
return False
1427
1428
def fileno(self):
1429
raise OSError('Not supported')
1430
1431
def closed(self):
1432
return self._src.closed()
1433
1434
def close(self):
1435
pass
1436
1437
1438
class MPHeader:
1439
def __init__(self, name, value, options):
1440
self.name = name
1441
self.value = value
1442
self.options = options
1443
1444
1445
class MPFieldStorage:
1446
1447
_patt = re.compile(tonat('(.+?)(=(.+?))?(;|$)'))
1448
1449
def __init__(self):
1450
self.name = None
1451
self.value = None
1452
self.filename = None
1453
self.file = None
1454
self.ctype = None
1455
self.headers = {}
1456
1457
def read(self, src, headers_section, data_section, max_read):
1458
start, end = headers_section
1459
sz = end - start
1460
has_read = sz
1461
if has_read > max_read:
1462
raise HTTPError(413, 'Request entity too large')
1463
src.seek(start)
1464
headers_raw = tonat(src.read(sz))
1465
for header_raw in headers_raw.splitlines():
1466
header = self.parse_header(header_raw)
1467
self.headers[header.name] = header
1468
if header.name == 'Content-Disposition':
1469
self.name = header.options['name']
1470
self.filename = header.options.get('filename')
1471
elif header.name == 'Content-Type':
1472
self.ctype = header.value
1473
if self.name is None:
1474
raise HTTPError(422, 'Noname field found while parsing multipart/formdata body: %s' % header_raw)
1475
if self.filename is not None:
1476
self.file = MPBytesIOProxy(src, *data_section)
1477
else:
1478
start, end = data_section
1479
sz = end - start
1480
if sz:
1481
has_read += sz
1482
if has_read > max_read:
1483
raise HTTPError(413, 'Request entity too large')
1484
src.seek(start)
1485
self.value = tonat(src.read(sz))
1486
else:
1487
self.value = ''
1488
return has_read
1489
1490
@classmethod
1491
def parse_header(cls, s):
1492
htype, rest = s.split(':', 1)
1493
opt_iter = cls._patt.finditer(rest)
1494
hvalue = next(opt_iter).group(1).strip()
1495
dct = {}
1496
for it in opt_iter:
1497
k = it.group(1).strip()
1498
v = it.group(3)
1499
if v is not None:
1500
v = v.strip('"')
1501
dct[k.lower()] = v
1502
return MPHeader(name=htype, value=hvalue, options=dct)
1503
1504
@classmethod
1505
def iter_items(cls, src, markup, max_read):
1506
iter_markup = iter(markup)
1507
# check & skip empty data (body should start from empty data)
1508
null_data = next(iter_markup, None)
1509
if null_data is None: return
1510
sec_name, [start, end] = null_data
1511
assert sec_name == 'data'
1512
if end > 0:
1513
raise HTTPError(
1514
422, 'Malformed multipart/formdata, unexpected data before the first boundary at: [%d:%d]'
1515
% (start, end))
1516
headers = next(iter_markup, None)
1517
data = next(iter_markup, None)
1518
while headers:
1519
sec_name, headers_slice = headers
1520
assert sec_name == 'headers'
1521
if not data:
1522
raise HTTPError(
1523
422, 'Malformed multipart/formdata, no data found for the field at: [%d:%d]'
1524
% tuple(headers_slice))
1525
sec_name, data_slice = data
1526
assert sec_name == 'data'
1527
field = cls()
1528
has_read = field.read(src, headers_slice, data_slice, max_read=max_read)
1529
max_read -= has_read
1530
yield field
1531
headers = next(iter_markup, None)
1532
data = next(iter_markup, None)
1533
1534
1535
class BaseRequest(object):
1536
""" A wrapper for WSGI environment dictionaries that adds a lot of
1537
convenient access methods and properties. Most of them are read-only.
1538
1539
Adding new attributes to a request actually adds them to the environ
1540
dictionary (as 'bottle.request.ext.<name>'). This is the recommended
1541
way to store and access request-specific data.
1542
"""
1543
1544
__slots__ = ('environ', )
1545
1546
#: Maximum size of memory buffer for :attr:`body` in bytes.
1547
MEMFILE_MAX = 102400
1548
1549
def __init__(self, environ=None):
1550
""" Wrap a WSGI environ dictionary. """
1551
#: The wrapped WSGI environ dictionary. This is the only real attribute.
1552
#: All other attributes actually are read-only properties.
1553
self.environ = {} if environ is None else environ
1554
self.environ['bottle.request'] = self
1555
1556
@DictProperty('environ', 'bottle.app', read_only=True)
1557
def app(self):
1558
""" Bottle application handling this request. """
1559
raise RuntimeError('This request is not connected to an application.')
1560
1561
@DictProperty('environ', 'bottle.route', read_only=True)
1562
def route(self):
1563
""" The bottle :class:`Route` object that matches this request. """
1564
raise RuntimeError('This request is not connected to a route.')
1565
1566
@DictProperty('environ', 'route.url_args', read_only=True)
1567
def url_args(self):
1568
""" The arguments extracted from the URL. """
1569
raise RuntimeError('This request is not connected to a route.')
1570
1571
@property
1572
def path(self):
1573
""" The value of ``PATH_INFO`` with exactly one prefixed slash (to fix
1574
broken clients and avoid the "empty path" edge case). """
1575
return '/' + self.environ.get('PATH_INFO', '').lstrip('/')
1576
1577
@property
1578
def method(self):
1579
""" The ``REQUEST_METHOD`` value as an uppercase string. """
1580
return self.environ.get('REQUEST_METHOD', 'GET').upper()
1581
1582
@DictProperty('environ', 'bottle.request.headers', read_only=True)
1583
def headers(self):
1584
""" A :class:`WSGIHeaderDict` that provides case-insensitive access to
1585
HTTP request headers. """
1586
return WSGIHeaderDict(self.environ)
1587
1588
def get_header(self, name, default=None):
1589
""" Return the value of a request header, or a given default value. """
1590
return self.headers.get(name, default)
1591
1592
@DictProperty('environ', 'bottle.request.cookies', read_only=True)
1593
def cookies(self):
1594
""" Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT
1595
decoded. Use :meth:`get_cookie` if you expect signed cookies. """
1596
cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values()
1597
return FormsDict((c.key, c.value) for c in cookies)
1598
1599
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
1600
""" Return the content of a cookie. To read a `Signed Cookie`, the
1601
`secret` must match the one used to create the cookie (see
1602
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
1603
cookie or wrong signature), return a default value. """
1604
value = self.cookies.get(key)
1605
if secret:
1606
# See BaseResponse.set_cookie for details on signed cookies.
1607
if value and value.startswith('!') and '?' in value:
1608
sig, msg = map(tob, value[1:].split('?', 1))
1609
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
1610
if _lscmp(sig, base64.b64encode(hash)):
1611
dst = pickle.loads(base64.b64decode(msg))
1612
if dst and dst[0] == key:
1613
return dst[1]
1614
return default
1615
return value or default
1616
1617
@DictProperty('environ', 'bottle.request.query', read_only=True)
1618
def query(self):
1619
""" The :attr:`query_string` parsed into a :class:`FormsDict`. These
1620
values are sometimes called "URL arguments" or "GET parameters", but
1621
not to be confused with "URL wildcards" as they are provided by the
1622
:class:`Router`. """
1623
get = self.environ['bottle.get'] = FormsDict()
1624
pairs = _parse_qsl(self.environ.get('QUERY_STRING', ''))
1625
for key, value in pairs:
1626
get[key] = value
1627
return get
1628
1629
@DictProperty('environ', 'bottle.request.forms', read_only=True)
1630
def forms(self):
1631
""" Form values parsed from an `url-encoded` or `multipart/form-data`
1632
encoded POST or PUT request body. The result is returned as a
1633
:class:`FormsDict`. All keys and values are strings. File uploads
1634
are stored separately in :attr:`files`. """
1635
forms = FormsDict()
1636
forms.recode_unicode = self.POST.recode_unicode
1637
for name, item in self.POST.allitems():
1638
if not isinstance(item, FileUpload):
1639
forms[name] = item
1640
return forms
1641
1642
@DictProperty('environ', 'bottle.request.params', read_only=True)
1643
def params(self):
1644
""" A :class:`FormsDict` with the combined values of :attr:`query` and
1645
:attr:`forms`. File uploads are stored in :attr:`files`. """
1646
params = FormsDict()
1647
for key, value in self.query.allitems():
1648
params[key] = value
1649
for key, value in self.forms.allitems():
1650
params[key] = value
1651
return params
1652
1653
@DictProperty('environ', 'bottle.request.files', read_only=True)
1654
def files(self):
1655
""" File uploads parsed from `multipart/form-data` encoded POST or PUT
1656
request body. The values are instances of :class:`FileUpload`.
1657
1658
"""
1659
files = FormsDict()
1660
files.recode_unicode = self.POST.recode_unicode
1661
for name, item in self.POST.allitems():
1662
if isinstance(item, FileUpload):
1663
files[name] = item
1664
return files
1665
1666
@DictProperty('environ', 'bottle.request.json', read_only=True)
1667
def json(self):
1668
""" If the ``Content-Type`` header is ``application/json`` or
1669
``application/json-rpc``, this property holds the parsed content
1670
of the request body. Only requests smaller than :attr:`MEMFILE_MAX`
1671
are processed to avoid memory exhaustion.
1672
Invalid JSON raises a 400 error response.
1673
"""
1674
ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0]
1675
if ctype in ('application/json', 'application/json-rpc'):
1676
b = self._get_body_string(self.MEMFILE_MAX)
1677
if not b:
1678
return None
1679
try:
1680
return json_loads(b)
1681
except (ValueError, TypeError):
1682
raise HTTPError(400, 'Invalid JSON')
1683
return None
1684
1685
def _iter_body(self, read, bufsize):
1686
maxread = max(0, self.content_length)
1687
while maxread:
1688
part = read(min(maxread, bufsize))
1689
if not part: break
1690
yield part
1691
maxread -= len(part)
1692
1693
@staticmethod
1694
def _iter_chunked(read, bufsize):
1695
err = HTTPError(400, 'Error while parsing chunked transfer body.')
1696
rn, sem, bs = tob('\r\n'), tob(';'), tob('')
1697
while True:
1698
header = read(1)
1699
while header[-2:] != rn:
1700
c = read(1)
1701
header += c
1702
if not c: raise err
1703
if len(header) > bufsize: raise err
1704
size, _, _ = header.partition(sem)
1705
try:
1706
maxread = int(tonat(size.strip()), 16)
1707
except ValueError:
1708
raise err
1709
if maxread == 0: break
1710
buff = bs
1711
while maxread > 0:
1712
if not buff:
1713
buff = read(min(maxread, bufsize))
1714
part, buff = buff[:maxread], buff[maxread:]
1715
if not part: raise err
1716
yield part
1717
maxread -= len(part)
1718
if read(2) != rn:
1719
raise err
1720
1721
@DictProperty('environ', 'bottle.request.body', read_only=True)
1722
def _body(self):
1723
mp_markup = None
1724
mp_boundary_match = MULTIPART_BOUNDARY_PATT.match(self.environ.get('CONTENT_TYPE', ''))
1725
if mp_boundary_match is not None:
1726
mp_markup = MPBodyMarkup(tob(mp_boundary_match.group(1)))
1727
try:
1728
read_func = self.environ['wsgi.input'].read
1729
except KeyError:
1730
self.environ['wsgi.input'] = BytesIO()
1731
return self.environ['wsgi.input']
1732
body_iter = self._iter_chunked if self.chunked else self._iter_body
1733
body, body_size, is_temp_file = BytesIO(), 0, False
1734
for part in body_iter(read_func, self.MEMFILE_MAX):
1735
body.write(part)
1736
if mp_markup is not None:
1737
mp_markup.parse(part)
1738
body_size += len(part)
1739
if not is_temp_file and body_size > self.MEMFILE_MAX:
1740
body, tmp = NamedTemporaryFile(mode='w+b'), body
1741
body.write(tmp.getvalue())
1742
del tmp
1743
is_temp_file = True
1744
body.multipart_markup = mp_markup
1745
self.environ['wsgi.input'] = body
1746
body.seek(0)
1747
return body
1748
1749
def _get_body_string(self, maxread):
1750
""" Read body into a string. Raise HTTPError(413) on requests that are
1751
too large. """
1752
if self.content_length > maxread:
1753
raise HTTPError(413, 'Request entity too large')
1754
data = self.body.read(maxread + 1)
1755
if len(data) > maxread:
1756
raise HTTPError(413, 'Request entity too large')
1757
return data
1758
1759
@property
1760
def body(self):
1761
""" The HTTP request body as a seek-able file-like object. Depending on
1762
:attr:`MEMFILE_MAX`, this is either a temporary file or a
1763
:class:`io.BytesIO` instance. Accessing this property for the first
1764
time reads and replaces the ``wsgi.input`` environ variable.
1765
Subsequent accesses just do a `seek(0)` on the file object. """
1766
self._body.seek(0)
1767
return self._body
1768
1769
@property
1770
def chunked(self):
1771
""" True if Chunked transfer encoding was. """
1772
return 'chunked' in self.environ.get(
1773
'HTTP_TRANSFER_ENCODING', '').lower()
1774
1775
#: An alias for :attr:`query`.
1776
GET = query
1777
1778
@DictProperty('environ', 'bottle.request.post', read_only=True)
1779
def POST(self):
1780
""" The values of :attr:`forms` and :attr:`files` combined into a single
1781
:class:`FormsDict`. Values are either strings (form values) or
1782
instances of :class:`MPBytesIOProxy` (file uploads).
1783
"""
1784
post = FormsDict()
1785
# We default to application/x-www-form-urlencoded for everything that
1786
# is not multipart and take the fast path (also: 3.1 workaround)
1787
if not self.content_type.startswith('multipart/'):
1788
body = tonat(self._get_body_string(self.MEMFILE_MAX), 'latin1')
1789
for key, value in _parse_qsl(body):
1790
post[key] = value
1791
return post
1792
1793
if py3k:
1794
post.recode_unicode = False
1795
body = self.body
1796
markup = body.multipart_markup
1797
if markup is None:
1798
raise HTTPError(400, '`boundary` required for mutlipart content')
1799
elif markup.error is not None:
1800
raise markup.error
1801
for item in MPFieldStorage.iter_items(body, markup.markups, self.MEMFILE_MAX):
1802
if item.filename is None:
1803
post[item.name] = item.value
1804
else:
1805
post[item.name] = FileUpload(item.file, item.name,
1806
item.filename, item.headers)
1807
return post
1808
1809
@property
1810
def url(self):
1811
""" The full request URI including hostname and scheme. If your app
1812
lives behind a reverse proxy or load balancer and you get confusing
1813
results, make sure that the ``X-Forwarded-Host`` header is set
1814
correctly. """
1815
return self.urlparts.geturl()
1816
1817
@DictProperty('environ', 'bottle.request.urlparts', read_only=True)
1818
def urlparts(self):
1819
""" The :attr:`url` string as an :class:`urlparse.SplitResult` tuple.
1820
The tuple contains (scheme, host, path, query_string and fragment),
1821
but the fragment is always empty because it is not visible to the
1822
server. """
1823
env = self.environ
1824
http = env.get('HTTP_X_FORWARDED_PROTO') \
1825
or env.get('wsgi.url_scheme', 'http')
1826
host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
1827
if not host:
1828
# HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
1829
host = env.get('SERVER_NAME', '127.0.0.1')
1830
port = env.get('SERVER_PORT')
1831
if port and port != ('80' if http == 'http' else '443'):
1832
host += ':' + port
1833
path = urlquote(self.fullpath)
1834
return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
1835
1836
@property
1837
def fullpath(self):
1838
""" Request path including :attr:`script_name` (if present). """
1839
return urljoin(self.script_name, self.path.lstrip('/'))
1840
1841
@property
1842
def query_string(self):
1843
""" The raw :attr:`query` part of the URL (everything in between ``?``
1844
and ``#``) as a string. """
1845
return self.environ.get('QUERY_STRING', '')
1846
1847
@property
1848
def script_name(self):
1849
""" The initial portion of the URL's `path` that was removed by a higher
1850
level (server or routing middleware) before the application was
1851
called. This script path is returned with leading and tailing
1852
slashes. """
1853
script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
1854
return '/' + script_name + '/' if script_name else '/'
1855
1856
def path_shift(self, shift=1):
1857
""" Shift path segments from :attr:`path` to :attr:`script_name` and
1858
vice versa.
1859
1860
:param shift: The number of path segments to shift. May be negative
1861
to change the shift direction. (default: 1)
1862
"""
1863
script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift)
1864
self['SCRIPT_NAME'], self['PATH_INFO'] = script, path
1865
1866
@property
1867
def content_length(self):
1868
""" The request body length as an integer. The client is responsible to
1869
set this header. Otherwise, the real length of the body is unknown
1870
and -1 is returned. In this case, :attr:`body` will be empty. """
1871
return int(self.environ.get('CONTENT_LENGTH') or -1)
1872
1873
@property
1874
def content_type(self):
1875
""" The Content-Type header as a lowercase-string (default: empty). """
1876
return self.environ.get('CONTENT_TYPE', '').lower()
1877
1878
@property
1879
def is_xhr(self):
1880
""" True if the request was triggered by a XMLHttpRequest. This only
1881
works with JavaScript libraries that support the `X-Requested-With`
1882
header (most of the popular libraries do). """
1883
requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '')
1884
return requested_with.lower() == 'xmlhttprequest'
1885
1886
@property
1887
def is_ajax(self):
1888
""" Alias for :attr:`is_xhr`. "Ajax" is not the right term. """
1889
return self.is_xhr
1890
1891
@property
1892
def auth(self):
1893
""" HTTP authentication data as a (user, password) tuple. This
1894
implementation currently supports basic (not digest) authentication
1895
only. If the authentication happened at a higher level (e.g. in the
1896
front web-server or a middleware), the password field is None, but
1897
the user field is looked up from the ``REMOTE_USER`` environ
1898
variable. On any errors, None is returned. """
1899
basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', ''))
1900
if basic: return basic
1901
ruser = self.environ.get('REMOTE_USER')
1902
if ruser: return (ruser, None)
1903
return None
1904
1905
@property
1906
def remote_route(self):
1907
""" A list of all IPs that were involved in this request, starting with
1908
the client IP and followed by zero or more proxies. This does only
1909
work if all proxies support the ```X-Forwarded-For`` header. Note
1910
that this information can be forged by malicious clients. """
1911
proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
1912
if proxy: return [ip.strip() for ip in proxy.split(',')]
1913
remote = self.environ.get('REMOTE_ADDR')
1914
return [remote] if remote else []
1915
1916
@property
1917
def remote_addr(self):
1918
""" The client IP as a string. Note that this information can be forged
1919
by malicious clients. """
1920
route = self.remote_route
1921
return route[0] if route else None
1922
1923
def copy(self):
1924
""" Return a new :class:`Request` with a shallow :attr:`environ` copy. """
1925
return Request(self.environ.copy())
1926
1927
def get(self, value, default=None):
1928
return self.environ.get(value, default)
1929
1930
def __getitem__(self, key):
1931
return self.environ[key]
1932
1933
def __delitem__(self, key):
1934
self[key] = ""
1935
del (self.environ[key])
1936
1937
def __iter__(self):
1938
return iter(self.environ)
1939
1940
def __len__(self):
1941
return len(self.environ)
1942
1943
def keys(self):
1944
return self.environ.keys()
1945
1946
def __setitem__(self, key, value):
1947
""" Change an environ value and clear all caches that depend on it. """
1948
1949
if self.environ.get('bottle.request.readonly'):
1950
raise KeyError('The environ dictionary is read-only.')
1951
1952
self.environ[key] = value
1953
todelete = ()
1954
1955
if key == 'wsgi.input':
1956
todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
1957
elif key == 'QUERY_STRING':
1958
todelete = ('query', 'params')
1959
elif key.startswith('HTTP_'):
1960
todelete = ('headers', 'cookies')
1961
1962
for key in todelete:
1963
self.environ.pop('bottle.request.' + key, None)
1964
1965
def __repr__(self):
1966
return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)
1967
1968
def __getattr__(self, name):
1969
""" Search in self.environ for additional user defined attributes. """
1970
try:
1971
var = self.environ['bottle.request.ext.%s' % name]
1972
return var.__get__(self) if hasattr(var, '__get__') else var
1973
except KeyError:
1974
raise AttributeError('Attribute %r not defined.' % name)
1975
1976
def __setattr__(self, name, value):
1977
if name == 'environ': return object.__setattr__(self, name, value)
1978
key = 'bottle.request.ext.%s' % name
1979
if hasattr(self, name):
1980
raise AttributeError("Attribute already defined: %s" % name)
1981
self.environ[key] = value
1982
1983
def __delattr__(self, name):
1984
try:
1985
del self.environ['bottle.request.ext.%s' % name]
1986
except KeyError:
1987
raise AttributeError("Attribute not defined: %s" % name)
1988
1989
1990
def _hkey(key):
1991
if '\n' in key or '\r' in key or '\0' in key:
1992
raise ValueError("Header names must not contain control characters: %r" % key)
1993
return key.title().replace('_', '-')
1994
1995
1996
def _hval(value):
1997
value = tonat(value)
1998
if '\n' in value or '\r' in value or '\0' in value:
1999
raise ValueError("Header value must not contain control characters: %r" % value)
2000
return value
2001
2002
2003
class HeaderProperty(object):
2004
def __init__(self, name, reader=None, writer=None, default=''):
2005
self.name, self.default = name, default
2006
self.reader, self.writer = reader, writer
2007
self.__doc__ = 'Current value of the %r header.' % name.title()
2008
2009
def __get__(self, obj, _):
2010
if obj is None: return self
2011
value = obj.get_header(self.name, self.default)
2012
return self.reader(value) if self.reader else value
2013
2014
def __set__(self, obj, value):
2015
obj[self.name] = self.writer(value) if self.writer else value
2016
2017
def __delete__(self, obj):
2018
del obj[self.name]
2019
2020
2021
class BaseResponse(object):
2022
""" Storage class for a response body as well as headers and cookies.
2023
2024
This class does support dict-like case-insensitive item-access to
2025
headers, but is NOT a dict. Most notably, iterating over a response
2026
yields parts of the body and not the headers.
2027
2028
:param body: The response body as one of the supported types.
2029
:param status: Either an HTTP status code (e.g. 200) or a status line
2030
including the reason phrase (e.g. '200 OK').
2031
:param headers: A dictionary or a list of name-value pairs.
2032
2033
Additional keyword arguments are added to the list of headers.
2034
Underscores in the header name are replaced with dashes.
2035
"""
2036
2037
default_status = 200
2038
default_content_type = 'text/html; charset=UTF-8'
2039
2040
# Header denylist for specific response codes
2041
# (rfc2616 section 10.2.3 and 10.3.5)
2042
bad_headers = {
2043
204: frozenset(('Content-Type', 'Content-Length')),
2044
304: frozenset(('Allow', 'Content-Encoding', 'Content-Language',
2045
'Content-Length', 'Content-Range', 'Content-Type',
2046
'Content-Md5', 'Last-Modified'))
2047
}
2048
2049
def __init__(self, body='', status=None, headers=None, **more_headers):
2050
self._cookies = None
2051
self._headers = {}
2052
self.body = body
2053
self.status = status or self.default_status
2054
if headers:
2055
if isinstance(headers, dict):
2056
headers = headers.items()
2057
for name, value in headers:
2058
self.add_header(name, value)
2059
if more_headers:
2060
for name, value in more_headers.items():
2061
self.add_header(name, value)
2062
2063
def copy(self, cls=None):
2064
""" Returns a copy of self. """
2065
cls = cls or BaseResponse
2066
assert issubclass(cls, BaseResponse)
2067
copy = cls()
2068
copy.status = self.status
2069
copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
2070
if self._cookies:
2071
cookies = copy._cookies = SimpleCookie()
2072
for k,v in self._cookies.items():
2073
cookies[k] = v.value
2074
cookies[k].update(v) # also copy cookie attributes
2075
return copy
2076
2077
def __iter__(self):
2078
return iter(self.body)
2079
2080
def close(self):
2081
if hasattr(self.body, 'close'):
2082
self.body.close()
2083
2084
@property
2085
def status_line(self):
2086
""" The HTTP status line as a string (e.g. ``404 Not Found``)."""
2087
return self._status_line
2088
2089
@property
2090
def status_code(self):
2091
""" The HTTP status code as an integer (e.g. 404)."""
2092
return self._status_code
2093
2094
def _set_status(self, status):
2095
if isinstance(status, int):
2096
code, status = status, _HTTP_STATUS_LINES.get(status)
2097
elif ' ' in status:
2098
if '\n' in status or '\r' in status or '\0' in status:
2099
raise ValueError('Status line must not include control chars.')
2100
status = status.strip()
2101
code = int(status.split()[0])
2102
else:
2103
raise ValueError('String status line without a reason phrase.')
2104
if not 100 <= code <= 999:
2105
raise ValueError('Status code out of range.')
2106
self._status_code = code
2107
self._status_line = str(status or ('%d Unknown' % code))
2108
2109
def _get_status(self):
2110
return self._status_line
2111
2112
status = property(
2113
_get_status, _set_status, None,
2114
''' A writeable property to change the HTTP response status. It accepts
2115
either a numeric code (100-999) or a string with a custom reason
2116
phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
2117
:data:`status_code` are updated accordingly. The return value is
2118
always a status string. ''')
2119
del _get_status, _set_status
2120
2121
@property
2122
def headers(self):
2123
""" An instance of :class:`HeaderDict`, a case-insensitive dict-like
2124
view on the response headers. """
2125
hdict = HeaderDict()
2126
hdict.dict = self._headers
2127
return hdict
2128
2129
def __contains__(self, name):
2130
return _hkey(name) in self._headers
2131
2132
def __delitem__(self, name):
2133
del self._headers[_hkey(name)]
2134
2135
def __getitem__(self, name):
2136
return self._headers[_hkey(name)][-1]
2137
2138
def __setitem__(self, name, value):
2139
self._headers[_hkey(name)] = [_hval(value)]
2140
2141
def get_header(self, name, default=None):
2142
""" Return the value of a previously defined header. If there is no
2143
header with that name, return a default value. """
2144
return self._headers.get(_hkey(name), [default])[-1]
2145
2146
def set_header(self, name, value):
2147
""" Create a new response header, replacing any previously defined
2148
headers with the same name. """
2149
self._headers[_hkey(name)] = [_hval(value)]
2150
2151
def add_header(self, name, value):
2152
""" Add an additional response header, not removing duplicates. """
2153
self._headers.setdefault(_hkey(name), []).append(_hval(value))
2154
2155
def iter_headers(self):
2156
""" Yield (header, value) tuples, skipping headers that are not
2157
allowed with the current response status code. """
2158
return self.headerlist
2159
2160
def _wsgi_status_line(self):
2161
""" WSGI conform status line (latin1-encodeable) """
2162
if py3k:
2163
return self._status_line.encode('utf8').decode('latin1')
2164
return self._status_line
2165
2166
@property
2167
def headerlist(self):
2168
""" WSGI conform list of (header, value) tuples. """
2169
out = []
2170
headers = list(self._headers.items())
2171
if 'Content-Type' not in self._headers:
2172
headers.append(('Content-Type', [self.default_content_type]))
2173
if self._status_code in self.bad_headers:
2174
bad_headers = self.bad_headers[self._status_code]
2175
headers = [h for h in headers if h[0] not in bad_headers]
2176
out += [(name, val) for (name, vals) in headers for val in vals]
2177
if self._cookies:
2178
for c in self._cookies.values():
2179
out.append(('Set-Cookie', _hval(c.OutputString())))
2180
if py3k:
2181
out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out]
2182
return out
2183
2184
content_type = HeaderProperty('Content-Type')
2185
content_length = HeaderProperty('Content-Length', reader=int, default=-1)
2186
expires = HeaderProperty(
2187
'Expires',
2188
reader=lambda x: datetime.utcfromtimestamp(parse_date(x)),
2189
writer=lambda x: http_date(x))
2190
2191
@property
2192
def charset(self, default='UTF-8'):
2193
""" Return the charset specified in the content-type header (default: utf8). """
2194
if 'charset=' in self.content_type:
2195
return self.content_type.split('charset=')[-1].split(';')[0].strip()
2196
return default
2197
2198
def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
2199
""" Create a new cookie or replace an old one. If the `secret` parameter is
2200
set, create a `Signed Cookie` (described below).
2201
2202
:param name: the name of the cookie.
2203
:param value: the value of the cookie.
2204
:param secret: a signature key required for signed cookies.
2205
2206
Additionally, this method accepts all RFC 2109 attributes that are
2207
supported by :class:`cookie.Morsel`, including:
2208
2209
:param maxage: maximum age in seconds. (default: None)
2210
:param expires: a datetime object or UNIX timestamp. (default: None)
2211
:param domain: the domain that is allowed to read the cookie.
2212
(default: current domain)
2213
:param path: limits the cookie to a given path (default: current path)
2214
:param secure: limit the cookie to HTTPS connections (default: off).
2215
:param httponly: prevents client-side javascript to read this cookie
2216
(default: off, requires Python 2.6 or newer).
2217
:param samesite: Control or disable third-party use for this cookie.
2218
Possible values: `lax`, `strict` or `none` (default).
2219
2220
If neither `expires` nor `maxage` is set (default), the cookie will
2221
expire at the end of the browser session (as soon as the browser
2222
window is closed).
2223
2224
Signed cookies may store any pickle-able object and are
2225
cryptographically signed to prevent manipulation. Keep in mind that
2226
cookies are limited to 4kb in most browsers.
2227
2228
Warning: Pickle is a potentially dangerous format. If an attacker
2229
gains access to the secret key, he could forge cookies that execute
2230
code on server side if unpickled. Using pickle is discouraged and
2231
support for it will be removed in later versions of bottle.
2232
2233
Warning: Signed cookies are not encrypted (the client can still see
2234
the content) and not copy-protected (the client can restore an old
2235
cookie). The main intention is to make pickling and unpickling
2236
save, not to store secret information at client side.
2237
"""
2238
if not self._cookies:
2239
self._cookies = SimpleCookie()
2240
2241
# Monkey-patch Cookie lib to support 'SameSite' parameter
2242
# https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
2243
if py < (3, 8, 0):
2244
Morsel._reserved.setdefault('samesite', 'SameSite')
2245
2246
if secret:
2247
if not isinstance(value, basestring):
2248
depr(0, 13, "Pickling of arbitrary objects into cookies is "
2249
"deprecated.", "Only store strings in cookies. "
2250
"JSON strings are fine, too.")
2251
encoded = base64.b64encode(pickle.dumps([name, value], -1))
2252
sig = base64.b64encode(hmac.new(tob(secret), encoded,
2253
digestmod=digestmod).digest())
2254
value = touni(tob('!') + sig + tob('?') + encoded)
2255
elif not isinstance(value, basestring):
2256
raise TypeError('Secret key required for non-string cookies.')
2257
2258
# Cookie size plus options must not exceed 4kb.
2259
if len(name) + len(value) > 3800:
2260
raise ValueError('Content does not fit into a cookie.')
2261
2262
self._cookies[name] = value
2263
2264
for key, value in options.items():
2265
if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
2266
key = 'max-age'
2267
if isinstance(value, timedelta):
2268
value = value.seconds + value.days * 24 * 3600
2269
if key == 'expires':
2270
value = http_date(value)
2271
if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
2272
key, value = 'samesite', (value or "none").lower()
2273
if value not in ('lax', 'strict', 'none'):
2274
raise CookieError("Invalid value for SameSite")
2275
if key in ('secure', 'httponly') and not value:
2276
continue
2277
self._cookies[name][key] = value
2278
2279
def delete_cookie(self, key, **kwargs):
2280
""" Delete a cookie. Be sure to use the same `domain` and `path`
2281
settings as used to create the cookie. """
2282
kwargs['max_age'] = -1
2283
kwargs['expires'] = 0
2284
self.set_cookie(key, '', **kwargs)
2285
2286
def __repr__(self):
2287
out = ''
2288
for name, value in self.headerlist:
2289
out += '%s: %s\n' % (name.title(), value.strip())
2290
return out
2291
2292
2293
def _local_property():
2294
ls = threading.local()
2295
2296
def fget(_):
2297
try:
2298
return ls.var
2299
except AttributeError:
2300
raise RuntimeError("Request context not initialized.")
2301
2302
def fset(_, value):
2303
ls.var = value
2304
2305
def fdel(_):
2306
del ls.var
2307
2308
return property(fget, fset, fdel, 'Thread-local property')
2309
2310
2311
class LocalRequest(BaseRequest):
2312
""" A thread-local subclass of :class:`BaseRequest` with a different
2313
set of attributes for each thread. There is usually only one global
2314
instance of this class (:data:`request`). If accessed during a
2315
request/response cycle, this instance always refers to the *current*
2316
request (even on a multithreaded server). """
2317
bind = BaseRequest.__init__
2318
environ = _local_property()
2319
2320
2321
class LocalResponse(BaseResponse):
2322
""" A thread-local subclass of :class:`BaseResponse` with a different
2323
set of attributes for each thread. There is usually only one global
2324
instance of this class (:data:`response`). Its attributes are used
2325
to build the HTTP response at the end of the request/response cycle.
2326
"""
2327
bind = BaseResponse.__init__
2328
_status_line = _local_property()
2329
_status_code = _local_property()
2330
_cookies = _local_property()
2331
_headers = _local_property()
2332
body = _local_property()
2333
2334
2335
Request = BaseRequest
2336
Response = BaseResponse
2337
2338
2339
class HTTPResponse(Response, BottleException):
2340
def __init__(self, body='', status=None, headers=None, **more_headers):
2341
super(HTTPResponse, self).__init__(body, status, headers, **more_headers)
2342
2343
def apply(self, other):
2344
other._status_code = self._status_code
2345
other._status_line = self._status_line
2346
other._headers = self._headers
2347
other._cookies = self._cookies
2348
other.body = self.body
2349
2350
2351
class HTTPError(HTTPResponse):
2352
default_status = 500
2353
2354
def __init__(self,
2355
status=None,
2356
body=None,
2357
exception=None,
2358
traceback=None, **more_headers):
2359
self.exception = exception
2360
self.traceback = traceback
2361
super(HTTPError, self).__init__(body, status, **more_headers)
2362
2363
###############################################################################
2364
# Plugins ######################################################################
2365
###############################################################################
2366
2367
2368
class PluginError(BottleException):
2369
pass
2370
2371
2372
class JSONPlugin(object):
2373
name = 'json'
2374
api = 2
2375
2376
def __init__(self, json_dumps=json_dumps):
2377
self.json_dumps = json_dumps
2378
2379
def setup(self, app):
2380
app.config._define('json.enable', default=True, validate=bool,
2381
help="Enable or disable automatic dict->json filter.")
2382
app.config._define('json.ascii', default=False, validate=bool,
2383
help="Use only 7-bit ASCII characters in output.")
2384
app.config._define('json.indent', default=True, validate=bool,
2385
help="Add whitespace to make json more readable.")
2386
app.config._define('json.dump_func', default=None,
2387
help="If defined, use this function to transform"
2388
" dict into json. The other options no longer"
2389
" apply.")
2390
2391
def apply(self, callback, route):
2392
dumps = self.json_dumps
2393
if not self.json_dumps: return callback
2394
2395
@functools.wraps(callback)
2396
def wrapper(*a, **ka):
2397
try:
2398
rv = callback(*a, **ka)
2399
except HTTPResponse as resp:
2400
rv = resp
2401
2402
if isinstance(rv, dict):
2403
#Attempt to serialize, raises exception on failure
2404
json_response = dumps(rv)
2405
#Set content type only if serialization successful
2406
response.content_type = 'application/json'
2407
return json_response
2408
elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict):
2409
rv.body = dumps(rv.body)
2410
rv.content_type = 'application/json'
2411
return rv
2412
2413
return wrapper
2414
2415
2416
class TemplatePlugin(object):
2417
""" This plugin applies the :func:`view` decorator to all routes with a
2418
`template` config parameter. If the parameter is a tuple, the second
2419
element must be a dict with additional options (e.g. `template_engine`)
2420
or default variables for the template. """
2421
name = 'template'
2422
api = 2
2423
2424
def setup(self, app):
2425
app.tpl = self
2426
2427
def apply(self, callback, route):
2428
conf = route.config.get('template')
2429
if isinstance(conf, (tuple, list)) and len(conf) == 2:
2430
return view(conf[0], **conf[1])(callback)
2431
elif isinstance(conf, str):
2432
return view(conf)(callback)
2433
else:
2434
return callback
2435
2436
2437
#: Not a plugin, but part of the plugin API. TODO: Find a better place.
2438
class _ImportRedirect(object):
2439
def __init__(self, name, impmask):
2440
""" Create a virtual package that redirects imports (see PEP 302). """
2441
self.name = name
2442
self.impmask = impmask
2443
self.module = sys.modules.setdefault(name, new_module(name))
2444
self.module.__dict__.update({
2445
'__file__': __file__,
2446
'__path__': [],
2447
'__all__': [],
2448
'__loader__': self
2449
})
2450
sys.meta_path.append(self)
2451
2452
def find_spec(self, fullname, path, target=None):
2453
if '.' not in fullname: return
2454
if fullname.rsplit('.', 1)[0] != self.name: return
2455
from importlib.util import spec_from_loader
2456
return spec_from_loader(fullname, self)
2457
2458
def find_module(self, fullname, path=None):
2459
if '.' not in fullname: return
2460
if fullname.rsplit('.', 1)[0] != self.name: return
2461
return self
2462
2463
def load_module(self, fullname):
2464
if fullname in sys.modules: return sys.modules[fullname]
2465
modname = fullname.rsplit('.', 1)[1]
2466
realname = self.impmask % modname
2467
__import__(realname)
2468
module = sys.modules[fullname] = sys.modules[realname]
2469
setattr(self.module, modname, module)
2470
module.__loader__ = self
2471
return module
2472
2473
###############################################################################
2474
# Common Utilities #############################################################
2475
###############################################################################
2476
2477
2478
class MultiDict(DictMixin):
2479
""" This dict stores multiple values per key, but behaves exactly like a
2480
normal dict in that it returns only the newest value for any given key.
2481
There are special methods available to access the full list of values.
2482
"""
2483
2484
def __init__(self, *a, **k):
2485
self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
2486
2487
def __len__(self):
2488
return len(self.dict)
2489
2490
def __iter__(self):
2491
return iter(self.dict)
2492
2493
def __contains__(self, key):
2494
return key in self.dict
2495
2496
def __delitem__(self, key):
2497
del self.dict[key]
2498
2499
def __getitem__(self, key):
2500
return self.dict[key][-1]
2501
2502
def __setitem__(self, key, value):
2503
self.append(key, value)
2504
2505
def keys(self):
2506
return self.dict.keys()
2507
2508
if py3k:
2509
2510
def values(self):
2511
return (v[-1] for v in self.dict.values())
2512
2513
def items(self):
2514
return ((k, v[-1]) for k, v in self.dict.items())
2515
2516
def allitems(self):
2517
return ((k, v) for k, vl in self.dict.items() for v in vl)
2518
2519
iterkeys = keys
2520
itervalues = values
2521
iteritems = items
2522
iterallitems = allitems
2523
2524
else:
2525
2526
def values(self):
2527
return [v[-1] for v in self.dict.values()]
2528
2529
def items(self):
2530
return [(k, v[-1]) for k, v in self.dict.items()]
2531
2532
def iterkeys(self):
2533
return self.dict.iterkeys()
2534
2535
def itervalues(self):
2536
return (v[-1] for v in self.dict.itervalues())
2537
2538
def iteritems(self):
2539
return ((k, v[-1]) for k, v in self.dict.iteritems())
2540
2541
def iterallitems(self):
2542
return ((k, v) for k, vl in self.dict.iteritems() for v in vl)
2543
2544
def allitems(self):
2545
return [(k, v) for k, vl in self.dict.iteritems() for v in vl]
2546
2547
def get(self, key, default=None, index=-1, type=None):
2548
""" Return the most recent value for a key.
2549
2550
:param default: The default value to be returned if the key is not
2551
present or the type conversion fails.
2552
:param index: An index for the list of available values.
2553
:param type: If defined, this callable is used to cast the value
2554
into a specific type. Exception are suppressed and result in
2555
the default value to be returned.
2556
"""
2557
try:
2558
val = self.dict[key][index]
2559
return type(val) if type else val
2560
except Exception:
2561
pass
2562
return default
2563
2564
def append(self, key, value):
2565
""" Add a new value to the list of values for this key. """
2566
self.dict.setdefault(key, []).append(value)
2567
2568
def replace(self, key, value):
2569
""" Replace the list of values with a single value. """
2570
self.dict[key] = [value]
2571
2572
def getall(self, key):
2573
""" Return a (possibly empty) list of values for a key. """
2574
return self.dict.get(key) or []
2575
2576
#: Aliases for WTForms to mimic other multi-dict APIs (Django)
2577
getone = get
2578
getlist = getall
2579
2580
2581
class FormsDict(MultiDict):
2582
""" This :class:`MultiDict` subclass is used to store request form data.
2583
Additionally to the normal dict-like item access methods (which return
2584
unmodified data as native strings), this container also supports
2585
attribute-like access to its values. Attributes are automatically de-
2586
or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing
2587
attributes default to an empty string. """
2588
2589
#: Encoding used for attribute values.
2590
input_encoding = 'utf8'
2591
#: If true (default), unicode strings are first encoded with `latin1`
2592
#: and then decoded to match :attr:`input_encoding`.
2593
recode_unicode = True
2594
2595
def _fix(self, s, encoding=None):
2596
if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI
2597
return s.encode('latin1').decode(encoding or self.input_encoding)
2598
elif isinstance(s, bytes): # Python 2 WSGI
2599
return s.decode(encoding or self.input_encoding)
2600
else:
2601
return s
2602
2603
def decode(self, encoding=None):
2604
""" Returns a copy with all keys and values de- or recoded to match
2605
:attr:`input_encoding`. Some libraries (e.g. WTForms) want a
2606
unicode dictionary. """
2607
copy = FormsDict()
2608
enc = copy.input_encoding = encoding or self.input_encoding
2609
copy.recode_unicode = False
2610
for key, value in self.allitems():
2611
copy.append(self._fix(key, enc), self._fix(value, enc))
2612
return copy
2613
2614
def getunicode(self, name, default=None, encoding=None):
2615
""" Return the value as a unicode string, or the default. """
2616
try:
2617
return self._fix(self[name], encoding)
2618
except (UnicodeError, KeyError):
2619
return default
2620
2621
def __getattr__(self, name, default=unicode()):
2622
# Without this guard, pickle generates a cryptic TypeError:
2623
if name.startswith('__') and name.endswith('__'):
2624
return super(FormsDict, self).__getattr__(name)
2625
return self.getunicode(name, default=default)
2626
2627
class HeaderDict(MultiDict):
2628
""" A case-insensitive version of :class:`MultiDict` that defaults to
2629
replace the old value instead of appending it. """
2630
2631
def __init__(self, *a, **ka):
2632
self.dict = {}
2633
if a or ka: self.update(*a, **ka)
2634
2635
def __contains__(self, key):
2636
return _hkey(key) in self.dict
2637
2638
def __delitem__(self, key):
2639
del self.dict[_hkey(key)]
2640
2641
def __getitem__(self, key):
2642
return self.dict[_hkey(key)][-1]
2643
2644
def __setitem__(self, key, value):
2645
self.dict[_hkey(key)] = [_hval(value)]
2646
2647
def append(self, key, value):
2648
self.dict.setdefault(_hkey(key), []).append(_hval(value))
2649
2650
def replace(self, key, value):
2651
self.dict[_hkey(key)] = [_hval(value)]
2652
2653
def getall(self, key):
2654
return self.dict.get(_hkey(key)) or []
2655
2656
def get(self, key, default=None, index=-1):
2657
return MultiDict.get(self, _hkey(key), default, index)
2658
2659
def filter(self, names):
2660
for name in (_hkey(n) for n in names):
2661
if name in self.dict:
2662
del self.dict[name]
2663
2664
2665
class WSGIHeaderDict(DictMixin):
2666
""" This dict-like class wraps a WSGI environ dict and provides convenient
2667
access to HTTP_* fields. Keys and values are native strings
2668
(2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI
2669
environment contains non-native string values, these are de- or encoded
2670
using a lossless 'latin1' character set.
2671
2672
The API will remain stable even on changes to the relevant PEPs.
2673
Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one
2674
that uses non-native strings.)
2675
"""
2676
#: List of keys that do not have a ``HTTP_`` prefix.
2677
cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
2678
2679
def __init__(self, environ):
2680
self.environ = environ
2681
2682
def _ekey(self, key):
2683
""" Translate header field name to CGI/WSGI environ key. """
2684
key = key.replace('-', '_').upper()
2685
if key in self.cgikeys:
2686
return key
2687
return 'HTTP_' + key
2688
2689
def raw(self, key, default=None):
2690
""" Return the header value as is (may be bytes or unicode). """
2691
return self.environ.get(self._ekey(key), default)
2692
2693
def __getitem__(self, key):
2694
val = self.environ[self._ekey(key)]
2695
if py3k:
2696
if isinstance(val, unicode):
2697
val = val.encode('latin1').decode('utf8')
2698
else:
2699
val = val.decode('utf8')
2700
return val
2701
2702
def __setitem__(self, key, value):
2703
raise TypeError("%s is read-only." % self.__class__)
2704
2705
def __delitem__(self, key):
2706
raise TypeError("%s is read-only." % self.__class__)
2707
2708
def __iter__(self):
2709
for key in self.environ:
2710
if key[:5] == 'HTTP_':
2711
yield _hkey(key[5:])
2712
elif key in self.cgikeys:
2713
yield _hkey(key)
2714
2715
def keys(self):
2716
return [x for x in self]
2717
2718
def __len__(self):
2719
return len(self.keys())
2720
2721
def __contains__(self, key):
2722
return self._ekey(key) in self.environ
2723
2724
_UNSET = object()
2725
2726
class ConfigDict(dict):
2727
""" A dict-like configuration storage with additional support for
2728
namespaces, validators, meta-data, overlays and more.
2729
2730
This dict-like class is heavily optimized for read access. All read-only
2731
methods as well as item access should be as fast as the built-in dict.
2732
"""
2733
2734
__slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__')
2735
2736
def __init__(self):
2737
self._meta = {}
2738
self._change_listener = []
2739
#: Weak references of overlays that need to be kept in sync.
2740
self._overlays = []
2741
#: Config that is the source for this overlay.
2742
self._source = None
2743
#: Keys of values copied from the source (values we do not own)
2744
self._virtual_keys = set()
2745
2746
def load_module(self, path, squash=True):
2747
"""Load values from a Python module.
2748
2749
Example modue ``config.py``::
2750
2751
DEBUG = True
2752
SQLITE = {
2753
"db": ":memory:"
2754
}
2755
2756
2757
>>> c = ConfigDict()
2758
>>> c.load_module('config')
2759
{DEBUG: True, 'SQLITE.DB': 'memory'}
2760
>>> c.load_module("config", False)
2761
{'DEBUG': True, 'SQLITE': {'DB': 'memory'}}
2762
2763
:param squash: If true (default), dictionary values are assumed to
2764
represent namespaces (see :meth:`load_dict`).
2765
"""
2766
config_obj = load(path)
2767
obj = {key: getattr(config_obj, key) for key in dir(config_obj)
2768
if key.isupper()}
2769
2770
if squash:
2771
self.load_dict(obj)
2772
else:
2773
self.update(obj)
2774
return self
2775
2776
def load_config(self, filename, **options):
2777
""" Load values from an ``*.ini`` style config file.
2778
2779
A configuration file consists of sections, each led by a
2780
``[section]`` header, followed by key/value entries separated by
2781
either ``=`` or ``:``. Section names and keys are case-insensitive.
2782
Leading and trailing whitespace is removed from keys and values.
2783
Values can be omitted, in which case the key/value delimiter may
2784
also be left out. Values can also span multiple lines, as long as
2785
they are indented deeper than the first line of the value. Commands
2786
are prefixed by ``#`` or ``;`` and may only appear on their own on
2787
an otherwise empty line.
2788
2789
Both section and key names may contain dots (``.``) as namespace
2790
separators. The actual configuration parameter name is constructed
2791
by joining section name and key name together and converting to
2792
lower case.
2793
2794
The special sections ``bottle`` and ``ROOT`` refer to the root
2795
namespace and the ``DEFAULT`` section defines default values for all
2796
other sections.
2797
2798
With Python 3, extended string interpolation is enabled.
2799
2800
:param filename: The path of a config file, or a list of paths.
2801
:param options: All keyword parameters are passed to the underlying
2802
:class:`python:configparser.ConfigParser` constructor call.
2803
2804
"""
2805
options.setdefault('allow_no_value', True)
2806
if py3k:
2807
options.setdefault('interpolation',
2808
configparser.ExtendedInterpolation())
2809
conf = configparser.ConfigParser(**options)
2810
conf.read(filename)
2811
for section in conf.sections():
2812
for key in conf.options(section):
2813
value = conf.get(section, key)
2814
if section not in ('bottle', 'ROOT'):
2815
key = section + '.' + key
2816
self[key.lower()] = value
2817
return self
2818
2819
def load_dict(self, source, namespace=''):
2820
""" Load values from a dictionary structure. Nesting can be used to
2821
represent namespaces.
2822
2823
>>> c = ConfigDict()
2824
>>> c.load_dict({'some': {'namespace': {'key': 'value'} } })
2825
{'some.namespace.key': 'value'}
2826
"""
2827
for key, value in source.items():
2828
if isinstance(key, basestring):
2829
nskey = (namespace + '.' + key).strip('.')
2830
if isinstance(value, dict):
2831
self.load_dict(value, namespace=nskey)
2832
else:
2833
self[nskey] = value
2834
else:
2835
raise TypeError('Key has type %r (not a string)' % type(key))
2836
return self
2837
2838
def update(self, *a, **ka):
2839
""" If the first parameter is a string, all keys are prefixed with this
2840
namespace. Apart from that it works just as the usual dict.update().
2841
2842
>>> c = ConfigDict()
2843
>>> c.update('some.namespace', key='value')
2844
"""
2845
prefix = ''
2846
if a and isinstance(a[0], basestring):
2847
prefix = a[0].strip('.') + '.'
2848
a = a[1:]
2849
for key, value in dict(*a, **ka).items():
2850
self[prefix + key] = value
2851
2852
def setdefault(self, key, value):
2853
if key not in self:
2854
self[key] = value
2855
return self[key]
2856
2857
def __setitem__(self, key, value):
2858
if not isinstance(key, basestring):
2859
raise TypeError('Key has type %r (not a string)' % type(key))
2860
2861
self._virtual_keys.discard(key)
2862
2863
value = self.meta_get(key, 'filter', lambda x: x)(value)
2864
if key in self and self[key] is value:
2865
return
2866
2867
self._on_change(key, value)
2868
dict.__setitem__(self, key, value)
2869
2870
for overlay in self._iter_overlays():
2871
overlay._set_virtual(key, value)
2872
2873
def __delitem__(self, key):
2874
if key not in self:
2875
raise KeyError(key)
2876
if key in self._virtual_keys:
2877
raise KeyError("Virtual keys cannot be deleted: %s" % key)
2878
2879
if self._source and key in self._source:
2880
# Not virtual, but present in source -> Restore virtual value
2881
dict.__delitem__(self, key)
2882
self._set_virtual(key, self._source[key])
2883
else: # not virtual, not present in source. This is OUR value
2884
self._on_change(key, None)
2885
dict.__delitem__(self, key)
2886
for overlay in self._iter_overlays():
2887
overlay._delete_virtual(key)
2888
2889
def _set_virtual(self, key, value):
2890
""" Recursively set or update virtual keys. Do nothing if non-virtual
2891
value is present. """
2892
if key in self and key not in self._virtual_keys:
2893
return # Do nothing for non-virtual keys.
2894
2895
self._virtual_keys.add(key)
2896
if key in self and self[key] is not value:
2897
self._on_change(key, value)
2898
dict.__setitem__(self, key, value)
2899
for overlay in self._iter_overlays():
2900
overlay._set_virtual(key, value)
2901
2902
def _delete_virtual(self, key):
2903
""" Recursively delete virtual entry. Do nothing if key is not virtual.
2904
"""
2905
if key not in self._virtual_keys:
2906
return # Do nothing for non-virtual keys.
2907
2908
if key in self:
2909
self._on_change(key, None)
2910
dict.__delitem__(self, key)
2911
self._virtual_keys.discard(key)
2912
for overlay in self._iter_overlays():
2913
overlay._delete_virtual(key)
2914
2915
def _on_change(self, key, value):
2916
for cb in self._change_listener:
2917
if cb(self, key, value):
2918
return True
2919
2920
def _add_change_listener(self, func):
2921
self._change_listener.append(func)
2922
return func
2923
2924
def meta_get(self, key, metafield, default=None):
2925
""" Return the value of a meta field for a key. """
2926
return self._meta.get(key, {}).get(metafield, default)
2927
2928
def meta_set(self, key, metafield, value):
2929
""" Set the meta field for a key to a new value. """
2930
self._meta.setdefault(key, {})[metafield] = value
2931
2932
def meta_list(self, key):
2933
""" Return an iterable of meta field names defined for a key. """
2934
return self._meta.get(key, {}).keys()
2935
2936
def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET):
2937
""" (Unstable) Shortcut for plugins to define own config parameters. """
2938
if default is not _UNSET:
2939
self.setdefault(key, default)
2940
if help is not _UNSET:
2941
self.meta_set(key, 'help', help)
2942
if validate is not _UNSET:
2943
self.meta_set(key, 'validate', validate)
2944
2945
def _iter_overlays(self):
2946
for ref in self._overlays:
2947
overlay = ref()
2948
if overlay is not None:
2949
yield overlay
2950
2951
def _make_overlay(self):
2952
""" (Unstable) Create a new overlay that acts like a chained map: Values
2953
missing in the overlay are copied from the source map. Both maps
2954
share the same meta entries.
2955
2956
Entries that were copied from the source are called 'virtual'. You
2957
can not delete virtual keys, but overwrite them, which turns them
2958
into non-virtual entries. Setting keys on an overlay never affects
2959
its source, but may affect any number of child overlays.
2960
2961
Other than collections.ChainMap or most other implementations, this
2962
approach does not resolve missing keys on demand, but instead
2963
actively copies all values from the source to the overlay and keeps
2964
track of virtual and non-virtual keys internally. This removes any
2965
lookup-overhead. Read-access is as fast as a build-in dict for both
2966
virtual and non-virtual keys.
2967
2968
Changes are propagated recursively and depth-first. A failing
2969
on-change handler in an overlay stops the propagation of virtual
2970
values and may result in an partly updated tree. Take extra care
2971
here and make sure that on-change handlers never fail.
2972
2973
Used by Route.config
2974
"""
2975
# Cleanup dead references
2976
self._overlays[:] = [ref for ref in self._overlays if ref() is not None]
2977
2978
overlay = ConfigDict()
2979
overlay._meta = self._meta
2980
overlay._source = self
2981
self._overlays.append(weakref.ref(overlay))
2982
for key in self:
2983
overlay._set_virtual(key, self[key])
2984
return overlay
2985
2986
2987
2988
2989
class AppStack(list):
2990
""" A stack-like list. Calling it returns the head of the stack. """
2991
2992
def __call__(self):
2993
""" Return the current default application. """
2994
return self.default
2995
2996
def push(self, value=None):
2997
""" Add a new :class:`Bottle` instance to the stack """
2998
if not isinstance(value, Bottle):
2999
value = Bottle()
3000
self.append(value)
3001
return value
3002
new_app = push
3003
3004
@property
3005
def default(self):
3006
try:
3007
return self[-1]
3008
except IndexError:
3009
return self.push()
3010
3011
3012
class WSGIFileWrapper(object):
3013
def __init__(self, fp, buffer_size=1024 * 64):
3014
self.fp, self.buffer_size = fp, buffer_size
3015
for attr in 'fileno', 'close', 'read', 'readlines', 'tell', 'seek':
3016
if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr))
3017
3018
def __iter__(self):
3019
buff, read = self.buffer_size, self.read
3020
part = read(buff)
3021
while part:
3022
yield part
3023
part = read(buff)
3024
3025
3026
class _closeiter(object):
3027
""" This only exists to be able to attach a .close method to iterators that
3028
do not support attribute assignment (most of itertools). """
3029
3030
def __init__(self, iterator, close=None):
3031
self.iterator = iterator
3032
self.close_callbacks = makelist(close)
3033
3034
def __iter__(self):
3035
return iter(self.iterator)
3036
3037
def close(self):
3038
for func in self.close_callbacks:
3039
func()
3040
3041
3042
class ResourceManager(object):
3043
""" This class manages a list of search paths and helps to find and open
3044
application-bound resources (files).
3045
3046
:param base: default value for :meth:`add_path` calls.
3047
:param opener: callable used to open resources.
3048
:param cachemode: controls which lookups are cached. One of 'all',
3049
'found' or 'none'.
3050
"""
3051
3052
def __init__(self, base='./', opener=open, cachemode='all'):
3053
self.opener = opener
3054
self.base = base
3055
self.cachemode = cachemode
3056
3057
#: A list of search paths. See :meth:`add_path` for details.
3058
self.path = []
3059
#: A cache for resolved paths. ``res.cache.clear()`` clears the cache.
3060
self.cache = {}
3061
3062
def add_path(self, path, base=None, index=None, create=False):
3063
""" Add a new path to the list of search paths. Return False if the
3064
path does not exist.
3065
3066
:param path: The new search path. Relative paths are turned into
3067
an absolute and normalized form. If the path looks like a file
3068
(not ending in `/`), the filename is stripped off.
3069
:param base: Path used to absolutize relative search paths.
3070
Defaults to :attr:`base` which defaults to ``os.getcwd()``.
3071
:param index: Position within the list of search paths. Defaults
3072
to last index (appends to the list).
3073
3074
The `base` parameter makes it easy to reference files installed
3075
along with a python module or package::
3076
3077
res.add_path('./resources/', __file__)
3078
"""
3079
base = os.path.abspath(os.path.dirname(base or self.base))
3080
path = os.path.abspath(os.path.join(base, os.path.dirname(path)))
3081
path += os.sep
3082
if path in self.path:
3083
self.path.remove(path)
3084
if create and not os.path.isdir(path):
3085
os.makedirs(path)
3086
if index is None:
3087
self.path.append(path)
3088
else:
3089
self.path.insert(index, path)
3090
self.cache.clear()
3091
return os.path.exists(path)
3092
3093
def __iter__(self):
3094
""" Iterate over all existing files in all registered paths. """
3095
search = self.path[:]
3096
while search:
3097
path = search.pop()
3098
if not os.path.isdir(path): continue
3099
for name in os.listdir(path):
3100
full = os.path.join(path, name)
3101
if os.path.isdir(full): search.append(full)
3102
else: yield full
3103
3104
def lookup(self, name):
3105
""" Search for a resource and return an absolute file path, or `None`.
3106
3107
The :attr:`path` list is searched in order. The first match is
3108
returned. Symlinks are followed. The result is cached to speed up
3109
future lookups. """
3110
if name not in self.cache or DEBUG:
3111
for path in self.path:
3112
fpath = os.path.join(path, name)
3113
if os.path.isfile(fpath):
3114
if self.cachemode in ('all', 'found'):
3115
self.cache[name] = fpath
3116
return fpath
3117
if self.cachemode == 'all':
3118
self.cache[name] = None
3119
return self.cache[name]
3120
3121
def open(self, name, mode='r', *args, **kwargs):
3122
""" Find a resource and return a file object, or raise IOError. """
3123
fname = self.lookup(name)
3124
if not fname: raise IOError("Resource %r not found." % name)
3125
return self.opener(fname, mode=mode, *args, **kwargs)
3126
3127
3128
class FileUpload(object):
3129
def __init__(self, fileobj, name, filename, headers=None):
3130
""" Wrapper for file uploads. """
3131
#: Open file(-like) object (BytesIO buffer or temporary file)
3132
self.file = fileobj
3133
#: Name of the upload form field
3134
self.name = name
3135
#: Raw filename as sent by the client (may contain unsafe characters)
3136
self.raw_filename = filename
3137
#: A :class:`HeaderDict` with additional headers (e.g. content-type)
3138
self.headers = HeaderDict(headers) if headers else HeaderDict()
3139
3140
content_type = HeaderProperty('Content-Type')
3141
content_length = HeaderProperty('Content-Length', reader=int, default=-1)
3142
3143
def get_header(self, name, default=None):
3144
""" Return the value of a header within the multipart part. """
3145
return self.headers.get(name, default)
3146
3147
@cached_property
3148
def filename(self):
3149
""" Name of the file on the client file system, but normalized to ensure
3150
file system compatibility. An empty filename is returned as 'empty'.
3151
3152
Only ASCII letters, digits, dashes, underscores and dots are
3153
allowed in the final filename. Accents are removed, if possible.
3154
Whitespace is replaced by a single dash. Leading or tailing dots
3155
or dashes are removed. The filename is limited to 255 characters.
3156
"""
3157
fname = self.raw_filename
3158
if not isinstance(fname, unicode):
3159
fname = fname.decode('utf8', 'ignore')
3160
fname = normalize('NFKD', fname)
3161
fname = fname.encode('ASCII', 'ignore').decode('ASCII')
3162
fname = os.path.basename(fname.replace('\\', os.path.sep))
3163
fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip()
3164
fname = re.sub(r'[-\s]+', '-', fname).strip('.-')
3165
return fname[:255] or 'empty'
3166
3167
def _copy_file(self, fp, chunk_size=2 ** 16):
3168
read, write, offset = self.file.read, fp.write, self.file.tell()
3169
while 1:
3170
buf = read(chunk_size)
3171
if not buf: break
3172
write(buf)
3173
self.file.seek(offset)
3174
3175
def save(self, destination, overwrite=False, chunk_size=2 ** 16):
3176
""" Save file to disk or copy its content to an open file(-like) object.
3177
If *destination* is a directory, :attr:`filename` is added to the
3178
path. Existing files are not overwritten by default (IOError).
3179
3180
:param destination: File path, directory or file(-like) object.
3181
:param overwrite: If True, replace existing files. (default: False)
3182
:param chunk_size: Bytes to read at a time. (default: 64kb)
3183
"""
3184
if isinstance(destination, basestring): # Except file-likes here
3185
if os.path.isdir(destination):
3186
destination = os.path.join(destination, self.filename)
3187
if not overwrite and os.path.exists(destination):
3188
raise IOError('File exists.')
3189
with open(destination, 'wb') as fp:
3190
self._copy_file(fp, chunk_size)
3191
else:
3192
self._copy_file(destination, chunk_size)
3193
3194
###############################################################################
3195
# Application Helper ###########################################################
3196
###############################################################################
3197
3198
3199
def abort(code=500, text='Unknown Error.'):
3200
""" Aborts execution and causes a HTTP error. """
3201
raise HTTPError(code, text)
3202
3203
3204
def redirect(url, code=None):
3205
""" Aborts execution and causes a 303 or 302 redirect, depending on
3206
the HTTP protocol version. """
3207
if not code:
3208
code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
3209
res = response.copy(cls=HTTPResponse)
3210
res.status = code
3211
res.body = ""
3212
res.set_header('Location', urljoin(request.url, url))
3213
raise res
3214
3215
3216
def _rangeiter(fp, offset, limit, bufsize=1024 * 1024):
3217
""" Yield chunks from a range in a file. """
3218
fp.seek(offset)
3219
while limit > 0:
3220
part = fp.read(min(limit, bufsize))
3221
if not part:
3222
break
3223
limit -= len(part)
3224
yield part
3225
3226
3227
def static_file(filename, root,
3228
mimetype=True,
3229
download=False,
3230
charset='UTF-8',
3231
etag=None,
3232
headers=None):
3233
""" Open a file in a safe way and return an instance of :exc:`HTTPResponse`
3234
that can be sent back to the client.
3235
3236
:param filename: Name or path of the file to send, relative to ``root``.
3237
:param root: Root path for file lookups. Should be an absolute directory
3238
path.
3239
:param mimetype: Provide the content-type header (default: guess from
3240
file extension)
3241
:param download: If True, ask the browser to open a `Save as...` dialog
3242
instead of opening the file with the associated program. You can
3243
specify a custom filename as a string. If not specified, the
3244
original filename is used (default: False).
3245
:param charset: The charset for files with a ``text/*`` mime-type.
3246
(default: UTF-8)
3247
:param etag: Provide a pre-computed ETag header. If set to ``False``,
3248
ETag handling is disabled. (default: auto-generate ETag header)
3249
:param headers: Additional headers dict to add to the response.
3250
3251
While checking user input is always a good idea, this function provides
3252
additional protection against malicious ``filename`` parameters from
3253
breaking out of the ``root`` directory and leaking sensitive information
3254
to an attacker.
3255
3256
Read-protected files or files outside of the ``root`` directory are
3257
answered with ``403 Access Denied``. Missing files result in a
3258
``404 Not Found`` response. Conditional requests (``If-Modified-Since``,
3259
``If-None-Match``) are answered with ``304 Not Modified`` whenever
3260
possible. ``HEAD`` and ``Range`` requests (used by download managers to
3261
check or continue partial downloads) are also handled automatically.
3262
3263
"""
3264
3265
root = os.path.join(os.path.abspath(root), '')
3266
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
3267
headers = headers.copy() if headers else {}
3268
3269
if not filename.startswith(root):
3270
return HTTPError(403, "Access denied.")
3271
if not os.path.exists(filename) or not os.path.isfile(filename):
3272
return HTTPError(404, "File does not exist.")
3273
if not os.access(filename, os.R_OK):
3274
return HTTPError(403, "You do not have permission to access this file.")
3275
3276
if mimetype is True:
3277
if download and download is not True:
3278
mimetype, encoding = mimetypes.guess_type(download)
3279
else:
3280
mimetype, encoding = mimetypes.guess_type(filename)
3281
if encoding:
3282
headers['Content-Encoding'] = encoding
3283
3284
if mimetype:
3285
if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
3286
and charset and 'charset' not in mimetype:
3287
mimetype += '; charset=%s' % charset
3288
headers['Content-Type'] = mimetype
3289
3290
if download:
3291
download = os.path.basename(filename if download is True else download)
3292
headers['Content-Disposition'] = 'attachment; filename="%s"' % download
3293
3294
stats = os.stat(filename)
3295
headers['Content-Length'] = clen = stats.st_size
3296
headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime,
3297
usegmt=True)
3298
headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)
3299
3300
getenv = request.environ.get
3301
3302
if etag is None:
3303
etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
3304
clen, filename)
3305
etag = hashlib.sha1(tob(etag)).hexdigest()
3306
3307
if etag:
3308
headers['ETag'] = etag
3309
check = getenv('HTTP_IF_NONE_MATCH')
3310
if check and check == etag:
3311
return HTTPResponse(status=304, **headers)
3312
3313
ims = getenv('HTTP_IF_MODIFIED_SINCE')
3314
if ims:
3315
ims = parse_date(ims.split(";")[0].strip())
3316
if ims is not None and ims >= int(stats.st_mtime):
3317
return HTTPResponse(status=304, **headers)
3318
3319
body = '' if request.method == 'HEAD' else open(filename, 'rb')
3320
3321
headers["Accept-Ranges"] = "bytes"
3322
range_header = getenv('HTTP_RANGE')
3323
if range_header:
3324
ranges = list(parse_range_header(range_header, clen))
3325
if not ranges:
3326
return HTTPError(416, "Requested Range Not Satisfiable")
3327
offset, end = ranges[0]
3328
rlen = end - offset
3329
headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
3330
headers["Content-Length"] = str(rlen)
3331
if body: body = _closeiter(_rangeiter(body, offset, rlen), body.close)
3332
return HTTPResponse(body, status=206, **headers)
3333
return HTTPResponse(body, **headers)
3334
3335
###############################################################################
3336
# HTTP Utilities and MISC (TODO) ###############################################
3337
###############################################################################
3338
3339
3340
def debug(mode=True):
3341
""" Change the debug level.
3342
There is only one debug level supported at the moment."""
3343
global DEBUG
3344
if mode: warnings.simplefilter('default')
3345
DEBUG = bool(mode)
3346
3347
3348
def http_date(value):
3349
if isinstance(value, basestring):
3350
return value
3351
if isinstance(value, datetime):
3352
# aware datetime.datetime is converted to UTC time
3353
# naive datetime.datetime is treated as UTC time
3354
value = value.utctimetuple()
3355
elif isinstance(value, datedate):
3356
# datetime.date is naive, and is treated as UTC time
3357
value = value.timetuple()
3358
if not isinstance(value, (int, float)):
3359
# convert struct_time in UTC to UNIX timestamp
3360
value = calendar.timegm(value)
3361
return email.utils.formatdate(value, usegmt=True)
3362
3363
3364
def parse_date(ims):
3365
""" Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
3366
try:
3367
ts = email.utils.parsedate_tz(ims)
3368
return calendar.timegm(ts[:8] + (0, )) - (ts[9] or 0)
3369
except (TypeError, ValueError, IndexError, OverflowError):
3370
return None
3371
3372
3373
def parse_auth(header):
3374
""" Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
3375
try:
3376
method, data = header.split(None, 1)
3377
if method.lower() == 'basic':
3378
user, pwd = touni(base64.b64decode(tob(data))).split(':', 1)
3379
return user, pwd
3380
except (KeyError, ValueError):
3381
return None
3382
3383
3384
def parse_range_header(header, maxlen=0):
3385
""" Yield (start, end) ranges parsed from a HTTP Range header. Skip
3386
unsatisfiable ranges. The end index is non-inclusive."""
3387
if not header or header[:6] != 'bytes=': return
3388
ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r]
3389
for start, end in ranges:
3390
try:
3391
if not start: # bytes=-100 -> last 100 bytes
3392
start, end = max(0, maxlen - int(end)), maxlen
3393
elif not end: # bytes=100- -> all but the first 99 bytes
3394
start, end = int(start), maxlen
3395
else: # bytes=100-200 -> bytes 100-200 (inclusive)
3396
start, end = int(start), min(int(end) + 1, maxlen)
3397
if 0 <= start < end <= maxlen:
3398
yield start, end
3399
except ValueError:
3400
pass
3401
3402
3403
#: Header tokenizer used by _parse_http_header()
3404
_hsplit = re.compile('(?:(?:"((?:[^"\\\\]|\\\\.)*)")|([^;,=]+))([;,=]?)').findall
3405
3406
def _parse_http_header(h):
3407
""" Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values
3408
and parameters. For non-standard or broken input, this implementation may return partial results.
3409
:param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``)
3410
:return: List of (value, params) tuples. The second element is a (possibly empty) dict.
3411
"""
3412
values = []
3413
if '"' not in h: # INFO: Fast path without regexp (~2x faster)
3414
for value in h.split(','):
3415
parts = value.split(';')
3416
values.append((parts[0].strip(), {}))
3417
for attr in parts[1:]:
3418
name, value = attr.split('=', 1)
3419
values[-1][1][name.strip()] = value.strip()
3420
else:
3421
lop, key, attrs = ',', None, {}
3422
for quoted, plain, tok in _hsplit(h):
3423
value = plain.strip() if plain else quoted.replace('\\"', '"')
3424
if lop == ',':
3425
attrs = {}
3426
values.append((value, attrs))
3427
elif lop == ';':
3428
if tok == '=':
3429
key = value
3430
else:
3431
attrs[value] = ''
3432
elif lop == '=' and key:
3433
attrs[key] = value
3434
key = None
3435
lop = tok
3436
return values
3437
3438
3439
def _parse_qsl(qs):
3440
r = []
3441
for pair in qs.split('&'):
3442
if not pair: continue
3443
nv = pair.split('=', 1)
3444
if len(nv) != 2: nv.append('')
3445
key = urlunquote(nv[0].replace('+', ' '))
3446
value = urlunquote(nv[1].replace('+', ' '))
3447
r.append((key, value))
3448
return r
3449
3450
3451
def _lscmp(a, b):
3452
""" Compares two strings in a cryptographically safe way:
3453
Runtime is not affected by length of common prefix. """
3454
return not sum(0 if x == y else 1
3455
for x, y in zip(a, b)) and len(a) == len(b)
3456
3457
3458
def cookie_encode(data, key, digestmod=None):
3459
""" Encode and sign a pickle-able object. Return a (byte) string """
3460
depr(0, 13, "cookie_encode() will be removed soon.",
3461
"Do not use this API directly.")
3462
digestmod = digestmod or hashlib.sha256
3463
msg = base64.b64encode(pickle.dumps(data, -1))
3464
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
3465
return tob('!') + sig + tob('?') + msg
3466
3467
3468
def cookie_decode(data, key, digestmod=None):
3469
""" Verify and decode an encoded string. Return an object or None."""
3470
depr(0, 13, "cookie_decode() will be removed soon.",
3471
"Do not use this API directly.")
3472
data = tob(data)
3473
if cookie_is_encoded(data):
3474
sig, msg = data.split(tob('?'), 1)
3475
digestmod = digestmod or hashlib.sha256
3476
hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest()
3477
if _lscmp(sig[1:], base64.b64encode(hashed)):
3478
return pickle.loads(base64.b64decode(msg))
3479
return None
3480
3481
3482
def cookie_is_encoded(data):
3483
""" Return True if the argument looks like a encoded cookie."""
3484
depr(0, 13, "cookie_is_encoded() will be removed soon.",
3485
"Do not use this API directly.")
3486
return bool(data.startswith(tob('!')) and tob('?') in data)
3487
3488
3489
def html_escape(string):
3490
""" Escape HTML special characters ``&<>`` and quotes ``'"``. """
3491
return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\
3492
.replace('"', '&quot;').replace("'", '&#039;')
3493
3494
3495
def html_quote(string):
3496
""" Escape and quote a string to be used as an HTTP attribute."""
3497
return '"%s"' % html_escape(string).replace('\n', '&#10;')\
3498
.replace('\r', '&#13;').replace('\t', '&#9;')
3499
3500
3501
def yieldroutes(func):
3502
""" Return a generator for routes that match the signature (name, args)
3503
of the func parameter. This may yield more than one route if the function
3504
takes optional keyword arguments. The output is best described by example::
3505
3506
a() -> '/a'
3507
b(x, y) -> '/b/<x>/<y>'
3508
c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>'
3509
d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>'
3510
"""
3511
path = '/' + func.__name__.replace('__', '/').lstrip('/')
3512
spec = getargspec(func)
3513
argc = len(spec[0]) - len(spec[3] or [])
3514
path += ('/<%s>' * argc) % tuple(spec[0][:argc])
3515
yield path
3516
for arg in spec[0][argc:]:
3517
path += '/<%s>' % arg
3518
yield path
3519
3520
3521
def path_shift(script_name, path_info, shift=1):
3522
""" Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
3523
3524
:return: The modified paths.
3525
:param script_name: The SCRIPT_NAME path.
3526
:param script_name: The PATH_INFO path.
3527
:param shift: The number of path fragments to shift. May be negative to
3528
change the shift direction. (default: 1)
3529
"""
3530
if shift == 0: return script_name, path_info
3531
pathlist = path_info.strip('/').split('/')
3532
scriptlist = script_name.strip('/').split('/')
3533
if pathlist and pathlist[0] == '': pathlist = []
3534
if scriptlist and scriptlist[0] == '': scriptlist = []
3535
if 0 < shift <= len(pathlist):
3536
moved = pathlist[:shift]
3537
scriptlist = scriptlist + moved
3538
pathlist = pathlist[shift:]
3539
elif 0 > shift >= -len(scriptlist):
3540
moved = scriptlist[shift:]
3541
pathlist = moved + pathlist
3542
scriptlist = scriptlist[:shift]
3543
else:
3544
empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO'
3545
raise AssertionError("Cannot shift. Nothing left from %s" % empty)
3546
new_script_name = '/' + '/'.join(scriptlist)
3547
new_path_info = '/' + '/'.join(pathlist)
3548
if path_info.endswith('/') and pathlist: new_path_info += '/'
3549
return new_script_name, new_path_info
3550
3551
3552
def auth_basic(check, realm="private", text="Access denied"):
3553
""" Callback decorator to require HTTP auth (basic).
3554
TODO: Add route(check_auth=...) parameter. """
3555
3556
def decorator(func):
3557
3558
@functools.wraps(func)
3559
def wrapper(*a, **ka):
3560
user, password = request.auth or (None, None)
3561
if user is None or not check(user, password):
3562
err = HTTPError(401, text)
3563
err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
3564
return err
3565
return func(*a, **ka)
3566
3567
return wrapper
3568
3569
return decorator
3570
3571
# Shortcuts for common Bottle methods.
3572
# They all refer to the current default application.
3573
3574
3575
def make_default_app_wrapper(name):
3576
""" Return a callable that relays calls to the current default app. """
3577
3578
@functools.wraps(getattr(Bottle, name))
3579
def wrapper(*a, **ka):
3580
return getattr(app(), name)(*a, **ka)
3581
3582
return wrapper
3583
3584
3585
route = make_default_app_wrapper('route')
3586
get = make_default_app_wrapper('get')
3587
post = make_default_app_wrapper('post')
3588
put = make_default_app_wrapper('put')
3589
delete = make_default_app_wrapper('delete')
3590
patch = make_default_app_wrapper('patch')
3591
error = make_default_app_wrapper('error')
3592
mount = make_default_app_wrapper('mount')
3593
hook = make_default_app_wrapper('hook')
3594
install = make_default_app_wrapper('install')
3595
uninstall = make_default_app_wrapper('uninstall')
3596
url = make_default_app_wrapper('get_url')
3597
3598
###############################################################################
3599
# Server Adapter ###############################################################
3600
###############################################################################
3601
3602
# Before you edit or add a server adapter, please read:
3603
# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870
3604
# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341
3605
3606
class ServerAdapter(object):
3607
quiet = False
3608
3609
def __init__(self, host='127.0.0.1', port=8080, **options):
3610
self.options = options
3611
self.host = host
3612
self.port = int(port)
3613
3614
def run(self, handler): # pragma: no cover
3615
pass
3616
3617
def __repr__(self):
3618
args = ', '.join('%s=%s' % (k, repr(v))
3619
for k, v in self.options.items())
3620
return "%s(%s)" % (self.__class__.__name__, args)
3621
3622
3623
class CGIServer(ServerAdapter):
3624
quiet = True
3625
3626
def run(self, handler): # pragma: no cover
3627
from wsgiref.handlers import CGIHandler
3628
3629
def fixed_environ(environ, start_response):
3630
environ.setdefault('PATH_INFO', '')
3631
return handler(environ, start_response)
3632
3633
CGIHandler().run(fixed_environ)
3634
3635
3636
class FlupFCGIServer(ServerAdapter):
3637
def run(self, handler): # pragma: no cover
3638
import flup.server.fcgi
3639
self.options.setdefault('bindAddress', (self.host, self.port))
3640
flup.server.fcgi.WSGIServer(handler, **self.options).run()
3641
3642
3643
class WSGIRefServer(ServerAdapter):
3644
def run(self, app): # pragma: no cover
3645
from wsgiref.simple_server import make_server
3646
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer
3647
import socket
3648
3649
class FixedHandler(WSGIRequestHandler):
3650
def address_string(self): # Prevent reverse DNS lookups please.
3651
return self.client_address[0]
3652
3653
def log_request(*args, **kw):
3654
if not self.quiet:
3655
return WSGIRequestHandler.log_request(*args, **kw)
3656
3657
handler_cls = self.options.get('handler_class', FixedHandler)
3658
server_cls = self.options.get('server_class', WSGIServer)
3659
3660
if ':' in self.host: # Fix wsgiref for IPv6 addresses.
3661
if getattr(server_cls, 'address_family') == socket.AF_INET:
3662
3663
class server_cls(server_cls):
3664
address_family = socket.AF_INET6
3665
3666
self.srv = make_server(self.host, self.port, app, server_cls,
3667
handler_cls)
3668
self.port = self.srv.server_port # update port actual port (0 means random)
3669
try:
3670
self.srv.serve_forever()
3671
except KeyboardInterrupt:
3672
self.srv.server_close() # Prevent ResourceWarning: unclosed socket
3673
raise
3674
3675
3676
class CherryPyServer(ServerAdapter):
3677
def run(self, handler): # pragma: no cover
3678
depr(0, 13, "The wsgi server part of cherrypy was split into a new "
3679
"project called 'cheroot'.", "Use the 'cheroot' server "
3680
"adapter instead of cherrypy.")
3681
from cherrypy import wsgiserver # This will fail for CherryPy >= 9
3682
3683
self.options['bind_addr'] = (self.host, self.port)
3684
self.options['wsgi_app'] = handler
3685
3686
certfile = self.options.get('certfile')
3687
if certfile:
3688
del self.options['certfile']
3689
keyfile = self.options.get('keyfile')
3690
if keyfile:
3691
del self.options['keyfile']
3692
3693
server = wsgiserver.CherryPyWSGIServer(**self.options)
3694
if certfile:
3695
server.ssl_certificate = certfile
3696
if keyfile:
3697
server.ssl_private_key = keyfile
3698
3699
try:
3700
server.start()
3701
finally:
3702
server.stop()
3703
3704
3705
class CherootServer(ServerAdapter):
3706
def run(self, handler): # pragma: no cover
3707
from cheroot import wsgi
3708
from cheroot.ssl import builtin
3709
self.options['bind_addr'] = (self.host, self.port)
3710
self.options['wsgi_app'] = handler
3711
certfile = self.options.pop('certfile', None)
3712
keyfile = self.options.pop('keyfile', None)
3713
chainfile = self.options.pop('chainfile', None)
3714
server = wsgi.Server(**self.options)
3715
if certfile and keyfile:
3716
server.ssl_adapter = builtin.BuiltinSSLAdapter(
3717
certfile, keyfile, chainfile)
3718
try:
3719
server.start()
3720
finally:
3721
server.stop()
3722
3723
3724
class WaitressServer(ServerAdapter):
3725
def run(self, handler):
3726
from waitress import serve
3727
serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options)
3728
3729
3730
class PasteServer(ServerAdapter):
3731
def run(self, handler): # pragma: no cover
3732
from paste import httpserver
3733
from paste.translogger import TransLogger
3734
handler = TransLogger(handler, setup_console_handler=(not self.quiet))
3735
httpserver.serve(handler,
3736
host=self.host,
3737
port=str(self.port), **self.options)
3738
3739
3740
class MeinheldServer(ServerAdapter):
3741
def run(self, handler):
3742
from meinheld import server
3743
server.listen((self.host, self.port))
3744
server.run(handler)
3745
3746
3747
class FapwsServer(ServerAdapter):
3748
""" Extremely fast webserver using libev. See https://github.com/william-os4y/fapws3 """
3749
3750
def run(self, handler): # pragma: no cover
3751
depr(0, 13, "fapws3 is not maintained and support will be dropped.")
3752
import fapws._evwsgi as evwsgi
3753
from fapws import base, config
3754
port = self.port
3755
if float(config.SERVER_IDENT[-2:]) > 0.4:
3756
# fapws3 silently changed its API in 0.5
3757
port = str(port)
3758
evwsgi.start(self.host, port)
3759
# fapws3 never releases the GIL. Complain upstream. I tried. No luck.
3760
if 'BOTTLE_CHILD' in os.environ and not self.quiet:
3761
_stderr("WARNING: Auto-reloading does not work with Fapws3.")
3762
_stderr(" (Fapws3 breaks python thread support)")
3763
evwsgi.set_base_module(base)
3764
3765
def app(environ, start_response):
3766
environ['wsgi.multiprocess'] = False
3767
return handler(environ, start_response)
3768
3769
evwsgi.wsgi_cb(('', app))
3770
evwsgi.run()
3771
3772
3773
class TornadoServer(ServerAdapter):
3774
""" The super hyped asynchronous server by facebook. Untested. """
3775
3776
def run(self, handler): # pragma: no cover
3777
import tornado.wsgi, tornado.httpserver, tornado.ioloop
3778
container = tornado.wsgi.WSGIContainer(handler)
3779
server = tornado.httpserver.HTTPServer(container)
3780
server.listen(port=self.port, address=self.host)
3781
tornado.ioloop.IOLoop.instance().start()
3782
3783
3784
class AppEngineServer(ServerAdapter):
3785
""" Adapter for Google App Engine. """
3786
quiet = True
3787
3788
def run(self, handler):
3789
depr(0, 13, "AppEngineServer no longer required",
3790
"Configure your application directly in your app.yaml")
3791
from google.appengine.ext.webapp import util
3792
# A main() function in the handler script enables 'App Caching'.
3793
# Lets makes sure it is there. This _really_ improves performance.
3794
module = sys.modules.get('__main__')
3795
if module and not hasattr(module, 'main'):
3796
module.main = lambda: util.run_wsgi_app(handler)
3797
util.run_wsgi_app(handler)
3798
3799
3800
class TwistedServer(ServerAdapter):
3801
""" Untested. """
3802
3803
def run(self, handler):
3804
from twisted.web import server, wsgi
3805
from twisted.python.threadpool import ThreadPool
3806
from twisted.internet import reactor
3807
thread_pool = ThreadPool()
3808
thread_pool.start()
3809
reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
3810
factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
3811
reactor.listenTCP(self.port, factory, interface=self.host)
3812
if not reactor.running:
3813
reactor.run()
3814
3815
3816
class DieselServer(ServerAdapter):
3817
""" Untested. """
3818
3819
def run(self, handler):
3820
depr(0, 13, "Diesel is not tested or supported and will be removed.")
3821
from diesel.protocols.wsgi import WSGIApplication
3822
app = WSGIApplication(handler, port=self.port)
3823
app.run()
3824
3825
3826
class GeventServer(ServerAdapter):
3827
""" Untested. Options:
3828
3829
* See gevent.wsgi.WSGIServer() documentation for more options.
3830
"""
3831
3832
def run(self, handler):
3833
from gevent import pywsgi, local
3834
if not isinstance(threading.local(), local.local):
3835
msg = "Bottle requires gevent.monkey.patch_all() (before import)"
3836
raise RuntimeError(msg)
3837
if self.quiet:
3838
self.options['log'] = None
3839
address = (self.host, self.port)
3840
server = pywsgi.WSGIServer(address, handler, **self.options)
3841
if 'BOTTLE_CHILD' in os.environ:
3842
import signal
3843
signal.signal(signal.SIGINT, lambda s, f: server.stop())
3844
server.serve_forever()
3845
3846
3847
class GunicornServer(ServerAdapter):
3848
""" Untested. See http://gunicorn.org/configure.html for options. """
3849
3850
def run(self, handler):
3851
from gunicorn.app.base import BaseApplication
3852
3853
if self.host.startswith("unix:"):
3854
config = {'bind': self.host}
3855
else:
3856
config = {'bind': "%s:%d" % (self.host, self.port)}
3857
3858
config.update(self.options)
3859
3860
class GunicornApplication(BaseApplication):
3861
def load_config(self):
3862
for key, value in config.items():
3863
self.cfg.set(key, value)
3864
3865
def load(self):
3866
return handler
3867
3868
GunicornApplication().run()
3869
3870
3871
class EventletServer(ServerAdapter):
3872
""" Untested. Options:
3873
3874
* `backlog` adjust the eventlet backlog parameter which is the maximum
3875
number of queued connections. Should be at least 1; the maximum
3876
value is system-dependent.
3877
* `family`: (default is 2) socket family, optional. See socket
3878
documentation for available families.
3879
"""
3880
3881
def run(self, handler):
3882
from eventlet import wsgi, listen, patcher
3883
if not patcher.is_monkey_patched(os):
3884
msg = "Bottle requires eventlet.monkey_patch() (before import)"
3885
raise RuntimeError(msg)
3886
socket_args = {}
3887
for arg in ('backlog', 'family'):
3888
try:
3889
socket_args[arg] = self.options.pop(arg)
3890
except KeyError:
3891
pass
3892
address = (self.host, self.port)
3893
try:
3894
wsgi.server(listen(address, **socket_args), handler,
3895
log_output=(not self.quiet))
3896
except TypeError:
3897
# Fallback, if we have old version of eventlet
3898
wsgi.server(listen(address), handler)
3899
3900
3901
class BjoernServer(ServerAdapter):
3902
""" Fast server written in C: https://github.com/jonashaag/bjoern """
3903
3904
def run(self, handler):
3905
from bjoern import run
3906
run(handler, self.host, self.port, reuse_port=True)
3907
3908
class AsyncioServerAdapter(ServerAdapter):
3909
""" Extend ServerAdapter for adding custom event loop """
3910
def get_event_loop(self):
3911
pass
3912
3913
class AiohttpServer(AsyncioServerAdapter):
3914
""" Asynchronous HTTP client/server framework for asyncio
3915
https://pypi.python.org/pypi/aiohttp/
3916
https://pypi.org/project/aiohttp-wsgi/
3917
"""
3918
3919
def get_event_loop(self):
3920
import asyncio
3921
return asyncio.new_event_loop()
3922
3923
def run(self, handler):
3924
import asyncio
3925
from aiohttp_wsgi.wsgi import serve
3926
self.loop = self.get_event_loop()
3927
asyncio.set_event_loop(self.loop)
3928
3929
if 'BOTTLE_CHILD' in os.environ:
3930
import signal
3931
signal.signal(signal.SIGINT, lambda s, f: self.loop.stop())
3932
3933
serve(handler, host=self.host, port=self.port)
3934
3935
3936
class AiohttpUVLoopServer(AiohttpServer):
3937
"""uvloop
3938
https://github.com/MagicStack/uvloop
3939
"""
3940
def get_event_loop(self):
3941
import uvloop
3942
return uvloop.new_event_loop()
3943
3944
class AutoServer(ServerAdapter):
3945
""" Untested. """
3946
adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer,
3947
CherootServer, WSGIRefServer]
3948
3949
def run(self, handler):
3950
for sa in self.adapters:
3951
try:
3952
return sa(self.host, self.port, **self.options).run(handler)
3953
except ImportError:
3954
pass
3955
3956
3957
server_names = {
3958
'cgi': CGIServer,
3959
'flup': FlupFCGIServer,
3960
'wsgiref': WSGIRefServer,
3961
'waitress': WaitressServer,
3962
'cherrypy': CherryPyServer,
3963
'cheroot': CherootServer,
3964
'paste': PasteServer,
3965
'fapws3': FapwsServer,
3966
'tornado': TornadoServer,
3967
'gae': AppEngineServer,
3968
'twisted': TwistedServer,
3969
'diesel': DieselServer,
3970
'meinheld': MeinheldServer,
3971
'gunicorn': GunicornServer,
3972
'eventlet': EventletServer,
3973
'gevent': GeventServer,
3974
'bjoern': BjoernServer,
3975
'aiohttp': AiohttpServer,
3976
'uvloop': AiohttpUVLoopServer,
3977
'auto': AutoServer,
3978
}
3979
3980
###############################################################################
3981
# Application Control ##########################################################
3982
###############################################################################
3983
3984
3985
def load(target, **namespace):
3986
""" Import a module or fetch an object from a module.
3987
3988
* ``package.module`` returns `module` as a module object.
3989
* ``pack.mod:name`` returns the module variable `name` from `pack.mod`.
3990
* ``pack.mod:func()`` calls `pack.mod.func()` and returns the result.
3991
3992
The last form accepts not only function calls, but any type of
3993
expression. Keyword arguments passed to this function are available as
3994
local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
3995
"""
3996
module, target = target.split(":", 1) if ':' in target else (target, None)
3997
if module not in sys.modules: __import__(module)
3998
if not target: return sys.modules[module]
3999
if target.isalnum(): return getattr(sys.modules[module], target)
4000
package_name = module.split('.')[0]
4001
namespace[package_name] = sys.modules[package_name]
4002
return eval('%s.%s' % (module, target), namespace)
4003
4004
4005
def load_app(target):
4006
""" Load a bottle application from a module and make sure that the import
4007
does not affect the current default application, but returns a separate
4008
application object. See :func:`load` for the target parameter. """
4009
global NORUN
4010
NORUN, nr_old = True, NORUN
4011
tmp = default_app.push() # Create a new "default application"
4012
try:
4013
rv = load(target) # Import the target module
4014
return rv if callable(rv) else tmp
4015
finally:
4016
default_app.remove(tmp) # Remove the temporary added default application
4017
NORUN = nr_old
4018
4019
4020
_debug = debug
4021
4022
4023
def run(app=None,
4024
server='wsgiref',
4025
host='127.0.0.1',
4026
port=8080,
4027
interval=1,
4028
reloader=False,
4029
quiet=False,
4030
plugins=None,
4031
debug=None,
4032
config=None, **kargs):
4033
""" Start a server instance. This method blocks until the server terminates.
4034
4035
:param app: WSGI application or target string supported by
4036
:func:`load_app`. (default: :func:`default_app`)
4037
:param server: Server adapter to use. See :data:`server_names` keys
4038
for valid names or pass a :class:`ServerAdapter` subclass.
4039
(default: `wsgiref`)
4040
:param host: Server address to bind to. Pass ``0.0.0.0`` to listens on
4041
all interfaces including the external one. (default: 127.0.0.1)
4042
:param port: Server port to bind to. Values below 1024 require root
4043
privileges. (default: 8080)
4044
:param reloader: Start auto-reloading server? (default: False)
4045
:param interval: Auto-reloader interval in seconds (default: 1)
4046
:param quiet: Suppress output to stdout and stderr? (default: False)
4047
:param options: Options passed to the server adapter.
4048
"""
4049
if NORUN: return
4050
if reloader and not os.environ.get('BOTTLE_CHILD'):
4051
import subprocess
4052
fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
4053
environ = os.environ.copy()
4054
environ['BOTTLE_CHILD'] = 'true'
4055
environ['BOTTLE_LOCKFILE'] = lockfile
4056
args = [sys.executable] + sys.argv
4057
# If a package was loaded with `python -m`, then `sys.argv` needs to be
4058
# restored to the original value, or imports might break. See #1336
4059
if getattr(sys.modules.get('__main__'), '__package__', None):
4060
args[1:1] = ["-m", sys.modules['__main__'].__package__]
4061
4062
try:
4063
os.close(fd) # We never write to this file
4064
while os.path.exists(lockfile):
4065
p = subprocess.Popen(args, env=environ)
4066
while p.poll() is None:
4067
os.utime(lockfile, None) # Tell child we are still alive
4068
time.sleep(interval)
4069
if p.returncode == 3: # Child wants to be restarted
4070
continue
4071
sys.exit(p.returncode)
4072
except KeyboardInterrupt:
4073
pass
4074
finally:
4075
if os.path.exists(lockfile):
4076
os.unlink(lockfile)
4077
return
4078
4079
try:
4080
if debug is not None: _debug(debug)
4081
app = app or default_app()
4082
if isinstance(app, basestring):
4083
app = load_app(app)
4084
if not callable(app):
4085
raise ValueError("Application is not callable: %r" % app)
4086
4087
for plugin in plugins or []:
4088
if isinstance(plugin, basestring):
4089
plugin = load(plugin)
4090
app.install(plugin)
4091
4092
if config:
4093
app.config.update(config)
4094
4095
if server in server_names:
4096
server = server_names.get(server)
4097
if isinstance(server, basestring):
4098
server = load(server)
4099
if isinstance(server, type):
4100
server = server(host=host, port=port, **kargs)
4101
if not isinstance(server, ServerAdapter):
4102
raise ValueError("Unknown or unsupported server: %r" % server)
4103
4104
server.quiet = server.quiet or quiet
4105
if not server.quiet:
4106
_stderr("Bottle v%s server starting up (using %s)..." %
4107
(__version__, repr(server)))
4108
if server.host.startswith("unix:"):
4109
_stderr("Listening on %s" % server.host)
4110
else:
4111
_stderr("Listening on http://%s:%d/" %
4112
(server.host, server.port))
4113
_stderr("Hit Ctrl-C to quit.\n")
4114
4115
if reloader:
4116
lockfile = os.environ.get('BOTTLE_LOCKFILE')
4117
bgcheck = FileCheckerThread(lockfile, interval)
4118
with bgcheck:
4119
server.run(app)
4120
if bgcheck.status == 'reload':
4121
sys.exit(3)
4122
else:
4123
server.run(app)
4124
except KeyboardInterrupt:
4125
pass
4126
except (SystemExit, MemoryError):
4127
raise
4128
except:
4129
if not reloader: raise
4130
if not getattr(server, 'quiet', quiet):
4131
print_exc()
4132
time.sleep(interval)
4133
sys.exit(3)
4134
4135
4136
class FileCheckerThread(threading.Thread):
4137
""" Interrupt main-thread as soon as a changed module file is detected,
4138
the lockfile gets deleted or gets too old. """
4139
4140
def __init__(self, lockfile, interval):
4141
threading.Thread.__init__(self)
4142
self.daemon = True
4143
self.lockfile, self.interval = lockfile, interval
4144
#: Is one of 'reload', 'error' or 'exit'
4145
self.status = None
4146
4147
def run(self):
4148
exists = os.path.exists
4149
mtime = lambda p: os.stat(p).st_mtime
4150
files = dict()
4151
4152
for module in list(sys.modules.values()):
4153
path = getattr(module, '__file__', '') or ''
4154
if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
4155
if path and exists(path): files[path] = mtime(path)
4156
4157
while not self.status:
4158
if not exists(self.lockfile)\
4159
or mtime(self.lockfile) < time.time() - self.interval - 5:
4160
self.status = 'error'
4161
thread.interrupt_main()
4162
for path, lmtime in list(files.items()):
4163
if not exists(path) or mtime(path) > lmtime:
4164
self.status = 'reload'
4165
thread.interrupt_main()
4166
break
4167
time.sleep(self.interval)
4168
4169
def __enter__(self):
4170
self.start()
4171
4172
def __exit__(self, exc_type, *_):
4173
if not self.status: self.status = 'exit' # silent exit
4174
self.join()
4175
return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
4176
4177
###############################################################################
4178
# Template Adapters ############################################################
4179
###############################################################################
4180
4181
4182
class TemplateError(BottleException):
4183
pass
4184
4185
4186
class BaseTemplate(object):
4187
""" Base class and minimal API for template adapters """
4188
extensions = ['tpl', 'html', 'thtml', 'stpl']
4189
settings = {} #used in prepare()
4190
defaults = {} #used in render()
4191
4192
def __init__(self,
4193
source=None,
4194
name=None,
4195
lookup=None,
4196
encoding='utf8', **settings):
4197
""" Create a new template.
4198
If the source parameter (str or buffer) is missing, the name argument
4199
is used to guess a template filename. Subclasses can assume that
4200
self.source and/or self.filename are set. Both are strings.
4201
The lookup, encoding and settings parameters are stored as instance
4202
variables.
4203
The lookup parameter stores a list containing directory paths.
4204
The encoding parameter should be used to decode byte strings or files.
4205
The settings parameter contains a dict for engine-specific settings.
4206
"""
4207
self.name = name
4208
self.source = source.read() if hasattr(source, 'read') else source
4209
self.filename = source.filename if hasattr(source, 'filename') else None
4210
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
4211
self.encoding = encoding
4212
self.settings = self.settings.copy() # Copy from class variable
4213
self.settings.update(settings) # Apply
4214
if not self.source and self.name:
4215
self.filename = self.search(self.name, self.lookup)
4216
if not self.filename:
4217
raise TemplateError('Template %s not found.' % repr(name))
4218
if not self.source and not self.filename:
4219
raise TemplateError('No template specified.')
4220
self.prepare(**self.settings)
4221
4222
@classmethod
4223
def search(cls, name, lookup=None):
4224
""" Search name in all directories specified in lookup.
4225
First without, then with common extensions. Return first hit. """
4226
if not lookup:
4227
raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")
4228
4229
if os.path.isabs(name):
4230
raise depr(0, 12, "Use of absolute path for template name.",
4231
"Refer to templates with names or paths relative to the lookup path.")
4232
4233
for spath in lookup:
4234
spath = os.path.abspath(spath) + os.sep
4235
fname = os.path.abspath(os.path.join(spath, name))
4236
if not fname.startswith(spath): continue
4237
if os.path.isfile(fname): return fname
4238
for ext in cls.extensions:
4239
if os.path.isfile('%s.%s' % (fname, ext)):
4240
return '%s.%s' % (fname, ext)
4241
4242
@classmethod
4243
def global_config(cls, key, *args):
4244
""" This reads or sets the global settings stored in class.settings. """
4245
if args:
4246
cls.settings = cls.settings.copy() # Make settings local to class
4247
cls.settings[key] = args[0]
4248
else:
4249
return cls.settings[key]
4250
4251
def prepare(self, **options):
4252
""" Run preparations (parsing, caching, ...).
4253
It should be possible to call this again to refresh a template or to
4254
update settings.
4255
"""
4256
raise NotImplementedError
4257
4258
def render(self, *args, **kwargs):
4259
""" Render the template with the specified local variables and return
4260
a single byte or unicode string. If it is a byte string, the encoding
4261
must match self.encoding. This method must be thread-safe!
4262
Local variables may be provided in dictionaries (args)
4263
or directly, as keywords (kwargs).
4264
"""
4265
raise NotImplementedError
4266
4267
4268
class MakoTemplate(BaseTemplate):
4269
def prepare(self, **options):
4270
from mako.template import Template
4271
from mako.lookup import TemplateLookup
4272
options.update({'input_encoding': self.encoding})
4273
options.setdefault('format_exceptions', bool(DEBUG))
4274
lookup = TemplateLookup(directories=self.lookup, **options)
4275
if self.source:
4276
self.tpl = Template(self.source, lookup=lookup, **options)
4277
else:
4278
self.tpl = Template(uri=self.name,
4279
filename=self.filename,
4280
lookup=lookup, **options)
4281
4282
def render(self, *args, **kwargs):
4283
for dictarg in args:
4284
kwargs.update(dictarg)
4285
_defaults = self.defaults.copy()
4286
_defaults.update(kwargs)
4287
return self.tpl.render(**_defaults)
4288
4289
4290
class CheetahTemplate(BaseTemplate):
4291
def prepare(self, **options):
4292
from Cheetah.Template import Template
4293
self.context = threading.local()
4294
self.context.vars = {}
4295
options['searchList'] = [self.context.vars]
4296
if self.source:
4297
self.tpl = Template(source=self.source, **options)
4298
else:
4299
self.tpl = Template(file=self.filename, **options)
4300
4301
def render(self, *args, **kwargs):
4302
for dictarg in args:
4303
kwargs.update(dictarg)
4304
self.context.vars.update(self.defaults)
4305
self.context.vars.update(kwargs)
4306
out = str(self.tpl)
4307
self.context.vars.clear()
4308
return out
4309
4310
4311
class Jinja2Template(BaseTemplate):
4312
def prepare(self, filters=None, tests=None, globals={}, **kwargs):
4313
from jinja2 import Environment, FunctionLoader
4314
self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
4315
if filters: self.env.filters.update(filters)
4316
if tests: self.env.tests.update(tests)
4317
if globals: self.env.globals.update(globals)
4318
if self.source:
4319
self.tpl = self.env.from_string(self.source)
4320
else:
4321
self.tpl = self.env.get_template(self.name)
4322
4323
def render(self, *args, **kwargs):
4324
for dictarg in args:
4325
kwargs.update(dictarg)
4326
_defaults = self.defaults.copy()
4327
_defaults.update(kwargs)
4328
return self.tpl.render(**_defaults)
4329
4330
def loader(self, name):
4331
if name == self.filename:
4332
fname = name
4333
else:
4334
fname = self.search(name, self.lookup)
4335
if not fname: return
4336
with open(fname, "rb") as f:
4337
return (f.read().decode(self.encoding), fname, lambda: False)
4338
4339
4340
class SimpleTemplate(BaseTemplate):
4341
def prepare(self,
4342
escape_func=html_escape,
4343
noescape=False,
4344
syntax=None, **ka):
4345
self.cache = {}
4346
enc = self.encoding
4347
self._str = lambda x: touni(x, enc)
4348
self._escape = lambda x: escape_func(touni(x, enc))
4349
self.syntax = syntax
4350
if noescape:
4351
self._str, self._escape = self._escape, self._str
4352
4353
@cached_property
4354
def co(self):
4355
return compile(self.code, self.filename or '<string>', 'exec')
4356
4357
@cached_property
4358
def code(self):
4359
source = self.source
4360
if not source:
4361
with open(self.filename, 'rb') as f:
4362
source = f.read()
4363
try:
4364
source, encoding = touni(source), 'utf8'
4365
except UnicodeError:
4366
raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.')
4367
parser = StplParser(source, encoding=encoding, syntax=self.syntax)
4368
code = parser.translate()
4369
self.encoding = parser.encoding
4370
return code
4371
4372
def _rebase(self, _env, _name=None, **kwargs):
4373
_env['_rebase'] = (_name, kwargs)
4374
4375
def _include(self, _env, _name=None, **kwargs):
4376
env = _env.copy()
4377
env.update(kwargs)
4378
if _name not in self.cache:
4379
self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax)
4380
return self.cache[_name].execute(env['_stdout'], env)
4381
4382
def execute(self, _stdout, kwargs):
4383
env = self.defaults.copy()
4384
env.update(kwargs)
4385
env.update({
4386
'_stdout': _stdout,
4387
'_printlist': _stdout.extend,
4388
'include': functools.partial(self._include, env),
4389
'rebase': functools.partial(self._rebase, env),
4390
'_rebase': None,
4391
'_str': self._str,
4392
'_escape': self._escape,
4393
'get': env.get,
4394
'setdefault': env.setdefault,
4395
'defined': env.__contains__
4396
})
4397
exec(self.co, env)
4398
if env.get('_rebase'):
4399
subtpl, rargs = env.pop('_rebase')
4400
rargs['base'] = ''.join(_stdout) #copy stdout
4401
del _stdout[:] # clear stdout
4402
return self._include(env, subtpl, **rargs)
4403
return env
4404
4405
def render(self, *args, **kwargs):
4406
""" Render the template using keyword arguments as local variables. """
4407
env = {}
4408
stdout = []
4409
for dictarg in args:
4410
env.update(dictarg)
4411
env.update(kwargs)
4412
self.execute(stdout, env)
4413
return ''.join(stdout)
4414
4415
4416
class StplSyntaxError(TemplateError):
4417
pass
4418
4419
4420
class StplParser(object):
4421
""" Parser for stpl templates. """
4422
_re_cache = {} #: Cache for compiled re patterns
4423
4424
# This huge pile of voodoo magic splits python code into 8 different tokens.
4425
# We use the verbose (?x) regex mode to make this more manageable
4426
4427
_re_tok = r'''(
4428
[urbURB]*
4429
(?: ''(?!')
4430
|""(?!")
4431
|'{6}
4432
|"{6}
4433
|'(?:[^\\']|\\.)+?'
4434
|"(?:[^\\"]|\\.)+?"
4435
|'{3}(?:[^\\]|\\.|\n)+?'{3}
4436
|"{3}(?:[^\\]|\\.|\n)+?"{3}
4437
)
4438
)'''
4439
4440
_re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later
4441
4442
_re_tok += r'''
4443
# 2: Comments (until end of line, but not the newline itself)
4444
|(\#.*)
4445
4446
# 3: Open and close (4) grouping tokens
4447
|([\[\{\(])
4448
|([\]\}\)])
4449
4450
# 5,6: Keywords that start or continue a python block (only start of line)
4451
|^([\ \t]*(?:if|for|while|with|try|def|class)\b)
4452
|^([\ \t]*(?:elif|else|except|finally)\b)
4453
4454
# 7: Our special 'end' keyword (but only if it stands alone)
4455
|((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#))
4456
4457
# 8: A customizable end-of-code-block template token (only end of line)
4458
|(%(block_close)s[\ \t]*(?=\r?$))
4459
4460
# 9: And finally, a single newline. The 10th token is 'everything else'
4461
|(\r?\n)
4462
'''
4463
4464
# Match the start tokens of code areas in a template
4465
_re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))'''
4466
# Match inline statements (may contain python strings)
4467
_re_inl = r'''%%(inline_start)s((?:%s|[^'"\n])*?)%%(inline_end)s''' % _re_inl
4468
4469
# add the flag in front of the regexp to avoid Deprecation warning (see Issue #949)
4470
# verbose and dot-matches-newline mode
4471
_re_tok = '(?mx)' + _re_tok
4472
_re_inl = '(?mx)' + _re_inl
4473
4474
4475
default_syntax = '<% %> % {{ }}'
4476
4477
def __init__(self, source, syntax=None, encoding='utf8'):
4478
self.source, self.encoding = touni(source, encoding), encoding
4479
self.set_syntax(syntax or self.default_syntax)
4480
self.code_buffer, self.text_buffer = [], []
4481
self.lineno, self.offset = 1, 0
4482
self.indent, self.indent_mod = 0, 0
4483
self.paren_depth = 0
4484
4485
def get_syntax(self):
4486
""" Tokens as a space separated string (default: <% %> % {{ }}) """
4487
return self._syntax
4488
4489
def set_syntax(self, syntax):
4490
self._syntax = syntax
4491
self._tokens = syntax.split()
4492
if syntax not in self._re_cache:
4493
names = 'block_start block_close line_start inline_start inline_end'
4494
etokens = map(re.escape, self._tokens)
4495
pattern_vars = dict(zip(names.split(), etokens))
4496
patterns = (self._re_split, self._re_tok, self._re_inl)
4497
patterns = [re.compile(p % pattern_vars) for p in patterns]
4498
self._re_cache[syntax] = patterns
4499
self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax]
4500
4501
syntax = property(get_syntax, set_syntax)
4502
4503
def translate(self):
4504
if self.offset: raise RuntimeError('Parser is a one time instance.')
4505
while True:
4506
m = self.re_split.search(self.source, pos=self.offset)
4507
if m:
4508
text = self.source[self.offset:m.start()]
4509
self.text_buffer.append(text)
4510
self.offset = m.end()
4511
if m.group(1): # Escape syntax
4512
line, sep, _ = self.source[self.offset:].partition('\n')
4513
self.text_buffer.append(self.source[m.start():m.start(1)] +
4514
m.group(2) + line + sep)
4515
self.offset += len(line + sep)
4516
continue
4517
self.flush_text()
4518
self.offset += self.read_code(self.source[self.offset:],
4519
multiline=bool(m.group(4)))
4520
else:
4521
break
4522
self.text_buffer.append(self.source[self.offset:])
4523
self.flush_text()
4524
return ''.join(self.code_buffer)
4525
4526
def read_code(self, pysource, multiline):
4527
code_line, comment = '', ''
4528
offset = 0
4529
while True:
4530
m = self.re_tok.search(pysource, pos=offset)
4531
if not m:
4532
code_line += pysource[offset:]
4533
offset = len(pysource)
4534
self.write_code(code_line.strip(), comment)
4535
break
4536
code_line += pysource[offset:m.start()]
4537
offset = m.end()
4538
_str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups()
4539
if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c
4540
code_line += _blk1 or _blk2
4541
continue
4542
if _str: # Python string
4543
code_line += _str
4544
elif _com: # Python comment (up to EOL)
4545
comment = _com
4546
if multiline and _com.strip().endswith(self._tokens[1]):
4547
multiline = False # Allow end-of-block in comments
4548
elif _po: # open parenthesis
4549
self.paren_depth += 1
4550
code_line += _po
4551
elif _pc: # close parenthesis
4552
if self.paren_depth > 0:
4553
# we could check for matching parentheses here, but it's
4554
# easier to leave that to python - just check counts
4555
self.paren_depth -= 1
4556
code_line += _pc
4557
elif _blk1: # Start-block keyword (if/for/while/def/try/...)
4558
code_line = _blk1
4559
self.indent += 1
4560
self.indent_mod -= 1
4561
elif _blk2: # Continue-block keyword (else/elif/except/...)
4562
code_line = _blk2
4563
self.indent_mod -= 1
4564
elif _cend: # The end-code-block template token (usually '%>')
4565
if multiline: multiline = False
4566
else: code_line += _cend
4567
elif _end:
4568
self.indent -= 1
4569
self.indent_mod += 1
4570
else: # \n
4571
self.write_code(code_line.strip(), comment)
4572
self.lineno += 1
4573
code_line, comment, self.indent_mod = '', '', 0
4574
if not multiline:
4575
break
4576
4577
return offset
4578
4579
def flush_text(self):
4580
text = ''.join(self.text_buffer)
4581
del self.text_buffer[:]
4582
if not text: return
4583
parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent
4584
for m in self.re_inl.finditer(text):
4585
prefix, pos = text[pos:m.start()], m.end()
4586
if prefix:
4587
parts.append(nl.join(map(repr, prefix.splitlines(True))))
4588
if prefix.endswith('\n'): parts[-1] += nl
4589
parts.append(self.process_inline(m.group(1).strip()))
4590
if pos < len(text):
4591
prefix = text[pos:]
4592
lines = prefix.splitlines(True)
4593
if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3]
4594
elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4]
4595
parts.append(nl.join(map(repr, lines)))
4596
code = '_printlist((%s,))' % ', '.join(parts)
4597
self.lineno += code.count('\n') + 1
4598
self.write_code(code)
4599
4600
@staticmethod
4601
def process_inline(chunk):
4602
if chunk[0] == '!': return '_str(%s)' % chunk[1:]
4603
return '_escape(%s)' % chunk
4604
4605
def write_code(self, line, comment=''):
4606
code = ' ' * (self.indent + self.indent_mod)
4607
code += line.lstrip() + comment + '\n'
4608
self.code_buffer.append(code)
4609
4610
4611
def template(*args, **kwargs):
4612
"""
4613
Get a rendered template as a string iterator.
4614
You can use a name, a filename or a template string as first parameter.
4615
Template rendering arguments can be passed as dictionaries
4616
or directly (as keyword arguments).
4617
"""
4618
tpl = args[0] if args else None
4619
for dictarg in args[1:]:
4620
kwargs.update(dictarg)
4621
adapter = kwargs.pop('template_adapter', SimpleTemplate)
4622
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
4623
tplid = (id(lookup), tpl)
4624
if tplid not in TEMPLATES or DEBUG:
4625
settings = kwargs.pop('template_settings', {})
4626
if isinstance(tpl, adapter):
4627
TEMPLATES[tplid] = tpl
4628
if settings: TEMPLATES[tplid].prepare(**settings)
4629
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
4630
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
4631
else:
4632
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
4633
if not TEMPLATES[tplid]:
4634
abort(500, 'Template (%s) not found' % tpl)
4635
return TEMPLATES[tplid].render(kwargs)
4636
4637
4638
mako_template = functools.partial(template, template_adapter=MakoTemplate)
4639
cheetah_template = functools.partial(template,
4640
template_adapter=CheetahTemplate)
4641
jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
4642
4643
4644
def view(tpl_name, **defaults):
4645
""" Decorator: renders a template for a handler.
4646
The handler can control its behavior like that:
4647
4648
- return a dict of template vars to fill out the template
4649
- return something other than a dict and the view decorator will not
4650
process the template, but return the handler result as is.
4651
This includes returning a HTTPResponse(dict) to get,
4652
for instance, JSON with autojson or other castfilters.
4653
"""
4654
4655
def decorator(func):
4656
4657
@functools.wraps(func)
4658
def wrapper(*args, **kwargs):
4659
result = func(*args, **kwargs)
4660
if isinstance(result, (dict, DictMixin)):
4661
tplvars = defaults.copy()
4662
tplvars.update(result)
4663
return template(tpl_name, **tplvars)
4664
elif result is None:
4665
return template(tpl_name, **defaults)
4666
return result
4667
4668
return wrapper
4669
4670
return decorator
4671
4672
4673
mako_view = functools.partial(view, template_adapter=MakoTemplate)
4674
cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
4675
jinja2_view = functools.partial(view, template_adapter=Jinja2Template)
4676
4677
###############################################################################
4678
# Constants and Globals ########################################################
4679
###############################################################################
4680
4681
TEMPLATE_PATH = ['./', './views/']
4682
TEMPLATES = {}
4683
DEBUG = False
4684
NORUN = False # If set, run() does nothing. Used by load_app()
4685
4686
#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
4687
HTTP_CODES = httplib.responses.copy()
4688
HTTP_CODES[418] = "I'm a teapot" # RFC 2324
4689
HTTP_CODES[428] = "Precondition Required"
4690
HTTP_CODES[429] = "Too Many Requests"
4691
HTTP_CODES[431] = "Request Header Fields Too Large"
4692
HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725
4693
HTTP_CODES[511] = "Network Authentication Required"
4694
_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v))
4695
for (k, v) in HTTP_CODES.items())
4696
4697
#: The default template used for error pages. Override with @error()
4698
ERROR_PAGE_TEMPLATE = """
4699
%%try:
4700
%%from %s import DEBUG, request
4701
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
4702
<html>
4703
<head>
4704
<title>Error: {{e.status}}</title>
4705
<style type="text/css">
4706
html {background-color: #eee; font-family: sans-serif;}
4707
body {background-color: #fff; border: 1px solid #ddd;
4708
padding: 15px; margin: 15px;}
4709
pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
4710
</style>
4711
</head>
4712
<body>
4713
<h1>Error: {{e.status}}</h1>
4714
<p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt>
4715
caused an error:</p>
4716
<pre>{{e.body}}</pre>
4717
%%if DEBUG and e.exception:
4718
<h2>Exception:</h2>
4719
%%try:
4720
%%exc = repr(e.exception)
4721
%%except:
4722
%%exc = '<unprintable %%s object>' %% type(e.exception).__name__
4723
%%end
4724
<pre>{{exc}}</pre>
4725
%%end
4726
%%if DEBUG and e.traceback:
4727
<h2>Traceback:</h2>
4728
<pre>{{e.traceback}}</pre>
4729
%%end
4730
</body>
4731
</html>
4732
%%except ImportError:
4733
<b>ImportError:</b> Could not generate the error page. Please add bottle to
4734
the import path.
4735
%%end
4736
""" % __name__
4737
4738
#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
4739
#: request callback, this instance always refers to the *current* request
4740
#: (even on a multi-threaded server).
4741
request = LocalRequest()
4742
4743
#: A thread-safe instance of :class:`LocalResponse`. It is used to change the
4744
#: HTTP response for the *current* request.
4745
response = LocalResponse()
4746
4747
#: A thread-safe namespace. Not used by Bottle.
4748
local = threading.local()
4749
4750
# Initialize app stack (create first empty Bottle app now deferred until needed)
4751
# BC: 0.6.4 and needed for run()
4752
apps = app = default_app = AppStack()
4753
4754
#: A virtual package that redirects import statements.
4755
#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
4756
ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else
4757
__name__ + ".ext", 'bottle_%s').module
4758
4759
4760
def _main(argv): # pragma: no coverage
4761
args, parser = _cli_parse(argv)
4762
4763
def _cli_error(cli_msg):
4764
parser.print_help()
4765
_stderr('\nError: %s\n' % cli_msg)
4766
sys.exit(1)
4767
4768
if args.version:
4769
print('Bottle %s' % __version__)
4770
sys.exit(0)
4771
if not args.app:
4772
_cli_error("No application entry point specified.")
4773
4774
sys.path.insert(0, '.')
4775
sys.modules.setdefault('bottle', sys.modules['__main__'])
4776
4777
host, port = (args.bind or 'localhost'), 8080
4778
if ':' in host and host.rfind(']') < host.rfind(':'):
4779
host, port = host.rsplit(':', 1)
4780
host = host.strip('[]')
4781
4782
config = ConfigDict()
4783
4784
for cfile in args.conf or []:
4785
try:
4786
if cfile.endswith('.json'):
4787
with open(cfile, 'rb') as fp:
4788
config.load_dict(json_loads(fp.read()))
4789
else:
4790
config.load_config(cfile)
4791
except configparser.Error as parse_error:
4792
_cli_error(parse_error)
4793
except IOError:
4794
_cli_error("Unable to read config file %r" % cfile)
4795
except (UnicodeError, TypeError, ValueError) as error:
4796
_cli_error("Unable to parse config file %r: %s" % (cfile, error))
4797
4798
for cval in args.param or []:
4799
if '=' in cval:
4800
config.update((cval.split('=', 1),))
4801
else:
4802
config[cval] = True
4803
4804
run(args.app,
4805
host=host,
4806
port=int(port),
4807
server=args.server,
4808
reloader=args.reload,
4809
plugins=args.plugin,
4810
debug=args.debug,
4811
config=config)
4812
4813
4814
if __name__ == '__main__': # pragma: no coverage
4815
_main(sys.argv)
4816
4817