diff --git a/lib/core/settings.py b/lib/core/settings.py index 9e5f62c3b..089634b68 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six from thirdparty.six import unichr as _unichr # sqlmap version (...) -VERSION = "1.5.9.1" +VERSION = "1.5.9.2" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/thirdparty/bottle/bottle.py b/thirdparty/bottle/bottle.py index de959cd37..be42bcbcc 100644 --- a/thirdparty/bottle/bottle.py +++ b/thirdparty/bottle/bottle.py @@ -9,11 +9,11 @@ Python Standard Library. Homepage and documentation: http://bottlepy.org/ -Copyright (c) 2014, Marcel Hellkamp. +Copyright (c) 2009-2018, Marcel Hellkamp. License: MIT (see LICENSE for details) """ -from __future__ import with_statement +from __future__ import print_function import sys __author__ = 'Marcel Hellkamp' @@ -21,18 +21,18 @@ __version__ = '0.13-dev' __license__ = 'MIT' ############################################################################### -# Command-line interface ######################################################## +# Command-line interface ###################################################### ############################################################################### # INFO: Some server adapters need to monkey-patch std-lib modules before they # are imported. This is why some of the command-line handling is done here, but -# the actual call to main() is at the end of the file. +# the actual call to _main() is at the end of the file. -def _cli_parse(args): - from optparse import OptionParser - parser = OptionParser( - usage="usage: %prog [options] package.module:app") - opt = parser.add_option +def _cli_parse(args): # pragma: no coverage + from argparse import ArgumentParser + + parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app") + opt = parser.add_argument opt("--version", action="store_true", help="show version number.") opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") @@ -43,13 +43,16 @@ def _cli_parse(args): help="override config values.") opt("--debug", action="store_true", help="start server in debug mode.") opt("--reload", action="store_true", help="auto-reload on file changes.") - opts, args = parser.parse_args(args[1:]) + opt('app', help='WSGI app entry point.', nargs='?') - return opts, args, parser + cli_args = parser.parse_args(args[1:]) + + return cli_args, parser -def _cli_patch(args): - opts, _, _ = _cli_parse(args) +def _cli_patch(cli_args): # pragma: no coverage + parsed_args, _ = _cli_parse(cli_args) + opts = parsed_args if opts.server: if opts.server.startswith('gevent'): import gevent.monkey @@ -63,12 +66,11 @@ if __name__ == '__main__': _cli_patch(sys.argv) ############################################################################### -# Imports and Python 2/3 unification ########################################### +# Imports and Python 2/3 unification ########################################## ############################################################################### - -import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ - os, re, tempfile, threading, time, warnings +import base64, calendar, cgi, email.utils, functools, hmac, imp, itertools,\ + mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib from types import FunctionType from datetime import date as datedate, datetime, timedelta @@ -76,6 +78,11 @@ from tempfile import TemporaryFile from traceback import format_exc, print_exc from unicodedata import normalize +try: + from ujson import dumps as json_dumps, loads as json_lds +except ImportError: + from json import dumps as json_dumps, loads as json_lds + # inspect.getargspec was removed in Python 3.6, use # Signature-based version where we can (Python 3.3+) try: @@ -94,60 +101,32 @@ try: defaults.append(param.default) return (args, varargs, keywords, tuple(defaults) or None) except ImportError: - from inspect import getargspec - -try: - from simplejson import dumps as json_dumps, loads as json_lds -except ImportError: # pragma: no cover try: - from json import dumps as json_dumps, loads as json_lds + from inspect import getfullargspec + def getargspec(func): + spec = getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return kwargs, spec[1], spec[2], spec[3] except ImportError: - try: - from django.utils.simplejson import dumps as json_dumps, loads as json_lds - except ImportError: + from inspect import getargspec - def json_dumps(data): - raise ImportError( - "JSON support requires Python 2.6 or simplejson.") - - json_lds = json_dumps - -# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. -# It ain't pretty but it works... Sorry for the mess. py = sys.version_info -py3k = py >= (3, 0, 0) -py25 = py < (2, 6, 0) -py31 = (3, 1, 0) <= py < (3, 2, 0) - -# Workaround for the missing "as" keyword in py3k. -def _e(): - return sys.exc_info()[1] - -# Workaround for the "print is a keyword/function" Python 2/3 dilemma -# and a fallback for mod_wsgi (resticts stdout/err attribute access) -try: - _stdout, _stderr = sys.stdout.write, sys.stderr.write -except IOError: - _stdout = lambda x: sys.stdout.write(x) - _stderr = lambda x: sys.stderr.write(x) +py3k = py.major > 2 # Lots of stdlib and builtin differences. if py3k: - try: - from collections.abc import MutableMapping as DictMixin - except ImportError: - from collections import MutableMapping as DictMixin - import http.client as httplib import _thread as thread from urllib.parse import urljoin, SplitResult as UrlSplitResult from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote urlunquote = functools.partial(urlunquote, encoding='latin1') - from http.cookies import SimpleCookie + from http.cookies import SimpleCookie, Morsel, CookieError + from collections.abc import MutableMapping as DictMixin import pickle from io import BytesIO - from configparser import ConfigParser, Error as ConfigParserError + import configparser + basestring = str unicode = str json_loads = lambda s: json_lds(touni(s)) @@ -161,50 +140,37 @@ else: # 2.x import thread from urlparse import urljoin, SplitResult as UrlSplitResult from urllib import urlencode, quote as urlquote, unquote as urlunquote - from Cookie import SimpleCookie + from Cookie import SimpleCookie, Morsel, CookieError from itertools import imap import cPickle as pickle from StringIO import StringIO as BytesIO - from ConfigParser import SafeConfigParser as ConfigParser, \ - Error as ConfigParserError - if py25: - msg = "Python 2.5 support may be dropped in future versions of Bottle." - warnings.warn(msg, DeprecationWarning) - from UserDict import DictMixin - - def next(it): - return it.next() - - bytes = str - else: # 2.6, 2.7 - from collections import MutableMapping as DictMixin + import ConfigParser as configparser + from collections import MutableMapping as DictMixin unicode = unicode json_loads = json_lds - eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) - + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) # Some helpers for string/byte handling def tob(s, enc='utf8'): - return s.encode(enc) if isinstance(s, unicode) else bytes(s) + if isinstance(s, unicode): + return s.encode(enc) + return b'' if s is None else bytes(s) def touni(s, enc='utf8', err='strict'): if isinstance(s, bytes): return s.decode(enc, err) - else: - return unicode(s or ("" if s is None else s)) + return unicode("" if s is None else s) tonat = touni if py3k else tob -# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). -# 3.1 needs a workaround. -if py31: - from io import TextIOWrapper - class NCTextIOWrapper(TextIOWrapper): - def close(self): - pass # Keep wrapped buffer open. +def _stderr(*args): + try: + print(*args, file=sys.stderr) + except (IOError, AttributeError): + pass # Some environments do not allow printing (mod_wsgi) # A bug in functools causes it to break if the wrapper is an instance method @@ -218,8 +184,14 @@ def update_wrapper(wrapper, wrapped, *a, **ka): # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. -def depr(message, strict=False): - warnings.warn(message, DeprecationWarning, stacklevel=3) +def depr(major, minor, cause, fix): + text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\ + "Cause: %s\n"\ + "Fix: %s\n" % (major, minor, cause, fix) + if DEBUG == 'strict': + raise DeprecationWarning(text) + warnings.warn(text, DeprecationWarning, stacklevel=3) + return DeprecationWarning(text) def makelist(data): # This is just too handy @@ -263,7 +235,7 @@ class cached_property(object): property. """ def __init__(self, func): - self.__doc__ = getattr(func, '__doc__') + update_wrapper(self, func) self.func = func def __get__(self, obj, cls): @@ -285,7 +257,7 @@ class lazy_attribute(object): return value ############################################################################### -# Exceptions and Events ######################################################## +# Exceptions and Events ####################################################### ############################################################################### @@ -374,13 +346,16 @@ class Router(object): rule_syntax = re.compile('(\\\\*)' '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' - '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + '(?::((?:\\\\.|[^\\\\>])+)?)?)?>))') def _itertokens(self, rule): offset, prefix = 0, '' for match in self.rule_syntax.finditer(rule): prefix += rule[offset:match.start()] g = match.groups() + if g[2] is not None: + depr(0, 13, "Use of old route syntax.", + "Use instead of :name in routes.") if len(g[0]) % 2: # Escaped wildcard prefix += match.group(0)[len(g[0]):] offset = match.end() @@ -431,9 +406,8 @@ class Router(object): try: re_pattern = re.compile('^(%s)$' % pattern) re_match = re_pattern.match - except re.error: - raise RouteSyntaxError("Could not add Route: %s (%s)" % - (rule, _e())) + except re.error as e: + raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) if filters: @@ -489,18 +463,15 @@ class Router(object): query['anon%d' % i] = value url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) return url if not query else url + '?' + urlencode(query) - except KeyError: - raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) + except KeyError as E: + raise RouteBuildError('Missing URL argument: %r' % E.args[0]) def match(self, environ): """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ verb = environ['REQUEST_METHOD'].upper() path = environ['PATH_INFO'] or '/' - if verb == 'HEAD': - methods = ['PROXY', verb, 'GET', 'ANY'] - else: - methods = ['PROXY', verb, 'ANY'] + methods = ('PROXY', 'HEAD', 'GET', 'ANY') if verb == 'HEAD' else ('PROXY', verb, 'ANY') for method in methods: if method in self.static and path in self.static[method]: @@ -518,7 +489,7 @@ class Router(object): nocheck = set(methods) for method in set(self.static) - nocheck: if path in self.static[method]: - allowed.add(verb) + allowed.add(method) for method in set(self.dyna_regexes) - allowed - nocheck: for combined, rules in self.dyna_regexes[method]: match = combined(path) @@ -535,7 +506,7 @@ class Router(object): class Route(object): """ This class wraps a route callback along with route specific metadata and configuration and applies Plugins on demand. It is also responsible for - turing an URL path rule into a regular expression usable by the Router. + turning an URL path rule into a regular expression usable by the Router. """ def __init__(self, app, rule, method, callback, @@ -559,7 +530,8 @@ class Route(object): #: Additional keyword arguments passed to the :meth:`Bottle.route` #: decorator are stored in this dictionary. Used for route-specific #: plugin configuration and meta-data. - self.config = ConfigDict().load_dict(config) + self.config = app.config._make_overlay() + self.config.load_dict(config) @cached_property def call(self): @@ -574,7 +546,7 @@ class Route(object): def prepare(self): """ Do all on-demand work immediately (useful for debugging).""" - self.call() + self.call def all_plugins(self): """ Yield all Plugins affecting this route. """ @@ -597,7 +569,7 @@ class Route(object): callback = plugin(callback) except RouteReset: # Try again with changed configuration. return self._make_callback() - if not callback is self.callback: + if callback is not self.callback: update_wrapper(callback, self.callback) return callback @@ -628,13 +600,14 @@ class Route(object): def get_config(self, key, default=None): """ Lookup a config field and return its value, first checking the route.config, then route.app.config.""" - for conf in (self.config, self.app.config): - if key in conf: return conf[key] - return default + depr(0, 13, "Route.get_config() is deprecated.", + "The Route.config property already includes values from the" + " application config for missing keys. Access it directly.") + return self.config.get(key, default) def __repr__(self): cb = self.get_undecorated_callback() - return '<%s %r %r>' % (self.method, self.rule, cb) + return '<%s %s -> %s:%s>' % (self.method, self.rule, cb.__module__, cb.__name__) ############################################################################### # Application Object ########################################################### @@ -650,14 +623,34 @@ class Bottle(object): let debugging middleware handle exceptions. """ - def __init__(self, catchall=True, autojson=True): + @lazy_attribute + def _global_config(cls): + cfg = ConfigDict() + cfg.meta_set('catchall', 'validate', bool) + return cfg + + def __init__(self, **kwargs): #: A :class:`ConfigDict` for app specific configuration. - self.config = ConfigDict() - self.config._on_change = functools.partial(self.trigger_hook, 'config') - self.config.meta_set('autojson', 'validate', bool) - self.config.meta_set('catchall', 'validate', bool) - self.config['catchall'] = catchall - self.config['autojson'] = autojson + self.config = self._global_config._make_overlay() + self.config._add_change_listener( + functools.partial(self.trigger_hook, 'config')) + + self.config.update({ + "catchall": True + }) + + if kwargs.get('catchall') is False: + depr(0, 13, "Bottle(catchall) keyword argument.", + "The 'catchall' setting is now part of the app " + "configuration. Fix: `app.config['catchall'] = False`") + self.config['catchall'] = False + if kwargs.get('autojson') is False: + depr(0, 13, "Bottle(autojson) keyword argument.", + "The 'autojson' setting is now part of the app " + "configuration. Fix: `app.config['json.enable'] = False`") + self.config['json.disable'] = True + + self._mounts = [] #: A :class:`ResourceManager` for application files self.resources = ResourceManager() @@ -668,15 +661,14 @@ class Bottle(object): # Core plugins self.plugins = [] # List of installed plugins. - if self.config['autojson']: - self.install(JSONPlugin()) + self.install(JSONPlugin()) self.install(TemplatePlugin()) #: If true, most exceptions are caught and returned as :exc:`HTTPError` catchall = DictProperty('config', 'catchall') __hook_names = 'before_request', 'after_request', 'app_reset', 'config' - __hook_reversed = 'after_request' + __hook_reversed = {'after_request'} @cached_property def _hooks(self): @@ -718,21 +710,10 @@ class Bottle(object): return decorator - def mount(self, prefix, app, **options): - """ Mount an application (:class:`Bottle` or plain WSGI) to a specific - URL prefix. Example:: - - root_app.mount('/admin/', admin_app) - - :param prefix: path prefix or `mount-point`. If it ends in a slash, - that slash is mandatory. - :param app: an instance of :class:`Bottle` or a WSGI application. - - All other parameters are passed to the underlying :meth:`route` call. - """ - + def _mount_wsgi(self, prefix, app, **options): segments = [p for p in prefix.split('/') if p] - if not segments: raise ValueError('Empty path prefix.') + if not segments: + raise ValueError('WSGI applications cannot be mounted to "/".') path_depth = len(segments) def mountpoint_wrapper(): @@ -743,6 +724,13 @@ class Bottle(object): def start_response(status, headerlist, exc_info=None): if exc_info: _raise(*exc_info) + if py3k: + # Errors here mean that the mounted WSGI app did not + # follow PEP-3333 (which requires latin1) or used a + # pre-encoding other than utf8 :/ + status = status.encode('latin1').decode('utf8') + headerlist = [(k, v.encode('latin1').decode('utf8')) + for (k, v) in headerlist] rs.status = status for name, value in headerlist: rs.add_header(name, value) @@ -763,6 +751,59 @@ class Bottle(object): if not prefix.endswith('/'): self.route('/' + '/'.join(segments), **options) + def _mount_app(self, prefix, app, **options): + if app in self._mounts or '_mount.app' in app.config: + depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.", + "Clone application before mounting to a different location.") + return self._mount_wsgi(prefix, app, **options) + + if options: + depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.", + "Do not specify any route options when mounting bottle application.") + return self._mount_wsgi(prefix, app, **options) + + if not prefix.endswith("/"): + depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.", + "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.") + return self._mount_wsgi(prefix, app, **options) + + self._mounts.append(app) + app.config['_mount.prefix'] = prefix + app.config['_mount.app'] = self + for route in app.routes: + route.rule = prefix + route.rule.lstrip('/') + self.add_route(route) + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + parent_app.mount('/prefix/', child_app) + + :param prefix: path prefix or `mount-point`. + :param app: an instance of :class:`Bottle` or a WSGI application. + + Plugins from the parent application are not applied to the routes + of the mounted child application. If you need plugins in the child + application, install them separately. + + While it is possible to use path wildcards within the prefix path + (:class:`Bottle` childs only), it is highly discouraged. + + The prefix path must end with a slash. If you want to access the + root of the child application via `/prefix` in addition to + `/prefix/`, consider adding a route with a 307 redirect to the + parent application. + """ + + if not prefix.startswith('/'): + raise ValueError("Prefix must start with '/'") + + if isinstance(app, Bottle): + return self._mount_app(prefix, app, **options) + else: + return self._mount_wsgi(prefix, app, **options) + def merge(self, routes): """ Merge the routes of another :class:`Bottle` application or a list of :class:`Route` objects into this application. The routes keep their @@ -824,7 +865,7 @@ class Bottle(object): run(self, **kwargs) def match(self, environ): - """ Search for a matching route and return a (:class:`Route` , urlargs) + """ Search for a matching route and return a (:class:`Route`, urlargs) tuple. The second value is a dictionary with parameters extracted from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" return self.router.match(environ) @@ -912,56 +953,80 @@ class Bottle(object): """ Equals :meth:`route` with a ``PATCH`` method parameter. """ return self.route(path, method, **options) - def error(self, code=500): - """ Decorator: Register an output handler for a HTTP error code""" + def error(self, code=500, callback=None): + """ Register an output handler for a HTTP error code. Can + be used as a decorator or called directly :: - def wrapper(handler): - self.error_handler[int(code)] = handler - return handler + def error_handler_500(error): + return 'error_handler_500' - return wrapper + app.error(code=500, callback=error_handler_500) + + @app.error(404) + def error_handler_404(error): + return 'error_handler_404' + + """ + + def decorator(callback): + if isinstance(callback, basestring): callback = load(callback) + self.error_handler[int(code)] = callback + return callback + + return decorator(callback) if callback else decorator def default_error_handler(self, res): - return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE'))) def _handle(self, environ): path = environ['bottle.raw_path'] = environ['PATH_INFO'] if py3k: environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') - def _inner_handle(): - # Maybe pass variables as locals for better performance? - try: - route, args = self.router.match(environ) - environ['route.handle'] = route - environ['bottle.route'] = route - environ['route.url_args'] = args - return route.call(**args) - except HTTPResponse: - return _e() - except RouteReset: - route.reset() - return _inner_handle() - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception: - if not self.catchall: raise - stacktrace = format_exc() - environ['wsgi.errors'].write(stacktrace) - return HTTPError(500, "Internal Server Error", _e(), stacktrace) + environ['bottle.app'] = self + request.bind(environ) + response.bind() try: - out = None - environ['bottle.app'] = self - request.bind(environ) - response.bind() - self.trigger_hook('before_request') - out = _inner_handle() - return out; - finally: - if isinstance(out, HTTPResponse): - out.apply(response) - self.trigger_hook('after_request') + while True: # Remove in 0.14 together with RouteReset + out = None + try: + self.trigger_hook('before_request') + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + out = route.call(**args) + break + except HTTPResponse as E: + out = E + break + except RouteReset: + depr(0, 13, "RouteReset exception deprecated", + "Call route.call() after route.reset() and " + "return the result.") + route.reset() + continue + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + try: + self.trigger_hook('after_request') + except HTTPResponse as E: + out = E + out.apply(response) + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception as E: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + environ['wsgi.errors'].flush() + environ['bottle.exc_info'] = sys.exc_info() + out = HTTPError(500, "Internal Server Error", E, stacktrace) + out.apply(response) + + return out def _cast(self, out, peek=None): """ Try to convert the parameter into something WSGI compatible and set @@ -1013,13 +1078,13 @@ class Bottle(object): first = next(iout) except StopIteration: return self._cast('') - except HTTPResponse: - first = _e() + except HTTPResponse as E: + first = E except (KeyboardInterrupt, SystemExit, MemoryError): raise - except: + except Exception as error: if not self.catchall: raise - first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + first = HTTPError(500, 'Unhandled exception', error, format_exc()) # These are the inner types allowed in iterator or generator objects. if isinstance(first, HTTPResponse): @@ -1045,19 +1110,23 @@ class Bottle(object): or environ['REQUEST_METHOD'] == 'HEAD': if hasattr(out, 'close'): out.close() out = [] - start_response(response._status_line, response.headerlist) + exc_info = environ.get('bottle.exc_info') + if exc_info is not None: + del environ['bottle.exc_info'] + start_response(response._wsgi_status_line(), response.headerlist, exc_info) return out except (KeyboardInterrupt, SystemExit, MemoryError): raise - except: + except Exception as E: if not self.catchall: raise err = '

Critical error while processing request: %s

' \ % html_escape(environ.get('PATH_INFO', '/')) if DEBUG: err += '

Error:

\n
\n%s\n
\n' \ '

Traceback:

\n
\n%s\n
\n' \ - % (html_escape(repr(_e())), html_escape(format_exc())) + % (html_escape(repr(E)), html_escape(format_exc())) environ['wsgi.errors'].write(err) + environ['wsgi.errors'].flush() headers = [('Content-Type', 'text/html; charset=UTF-8')] start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) return [tob(err)] @@ -1149,15 +1218,22 @@ class BaseRequest(object): cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() return FormsDict((c.key, c.value) for c in cookies) - def get_cookie(self, key, default=None, secret=None): + def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256): """ Return the content of a cookie. To read a `Signed Cookie`, the `secret` must match the one used to create the cookie (see :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing cookie or wrong signature), return a default value. """ value = self.cookies.get(key) - if secret and value: - dec = cookie_decode(value, secret) # (key, value) tuple or None - return dec[1] if dec and dec[0] == key else default + if secret: + # See BaseResponse.set_cookie for details on signed cookies. + if value and value.startswith('!') and '?' in value: + sig, msg = map(tob, value[1:].split('?', 1)) + hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest() + if _lscmp(sig, base64.b64encode(hash)): + dst = pickle.loads(base64.b64decode(msg)) + if dst and dst[0] == key: + return dst[1] + return default return value or default @DictProperty('environ', 'bottle.request.query', read_only=True) @@ -1179,6 +1255,7 @@ class BaseRequest(object): :class:`FormsDict`. All keys and values are strings. File uploads are stored separately in :attr:`files`. """ forms = FormsDict() + forms.recode_unicode = self.POST.recode_unicode for name, item in self.POST.allitems(): if not isinstance(item, FileUpload): forms[name] = item @@ -1202,6 +1279,7 @@ class BaseRequest(object): """ files = FormsDict() + files.recode_unicode = self.POST.recode_unicode for name, item in self.POST.allitems(): if isinstance(item, FileUpload): files[name] = item @@ -1209,13 +1287,15 @@ class BaseRequest(object): @DictProperty('environ', 'bottle.request.json', read_only=True) def json(self): - """ If the ``Content-Type`` header is ``application/json``, this - property holds the parsed content of the request body. Only requests - smaller than :attr:`MEMFILE_MAX` are processed to avoid memory - exhaustion. Invalid JSON raises a 400 error response. """ + """ If the ``Content-Type`` header is ``application/json`` or + ``application/json-rpc``, this property holds the parsed content + of the request body. Only requests smaller than :attr:`MEMFILE_MAX` + are processed to avoid memory exhaustion. + Invalid JSON raises a 400 error response. + """ ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] - if ctype == 'application/json': - b = self._get_body_string() + if ctype in ('application/json', 'application/json-rpc'): + b = self._get_body_string(self.MEMFILE_MAX) if not b: return None try: @@ -1281,15 +1361,13 @@ class BaseRequest(object): body.seek(0) return body - def _get_body_string(self): - """ read body until content-length or MEMFILE_MAX into a string. Raise - HTTPError(413) on requests that are to large. """ - clen = self.content_length - if clen > self.MEMFILE_MAX: + def _get_body_string(self, maxread): + """ Read body into a string. Raise HTTPError(413) on requests that are + to large. """ + if self.content_length > maxread: raise HTTPError(413, 'Request entity too large') - if clen < 0: clen = self.MEMFILE_MAX + 1 - data = self.body.read(clen) - if len(data) > self.MEMFILE_MAX: # Fail fast + data = self.body.read(maxread + 1) + if len(data) > maxread: raise HTTPError(413, 'Request entity too large') return data @@ -1322,8 +1400,8 @@ class BaseRequest(object): # We default to application/x-www-form-urlencoded for everything that # is not multipart and take the fast path (also: 3.1 workaround) if not self.content_type.startswith('multipart/'): - pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) - for key, value in pairs: + body = tonat(self._get_body_string(self.MEMFILE_MAX), 'latin1') + for key, value in _parse_qsl(body): post[key] = value return post @@ -1331,21 +1409,19 @@ class BaseRequest(object): for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): if key in self.environ: safe_env[key] = self.environ[key] args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) - if py31: - args['fp'] = NCTextIOWrapper(args['fp'], - encoding='utf8', - newline='\n') - elif py3k: + + if py3k: args['encoding'] = 'utf8' + post.recode_unicode = False data = cgi.FieldStorage(**args) self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 data = data.list or [] for item in data: - if item.filename: + if item.filename is None: + post[item.name] = item.value + else: post[item.name] = FileUpload(item.file, item.name, item.filename, item.headers) - else: - post[item.name] = item.value return post @property @@ -1528,26 +1604,36 @@ class BaseRequest(object): except KeyError: raise AttributeError("Attribute not defined: %s" % name) -def _hkey(s): - return s.title().replace('_', '-') + +def _hkey(key): + if '\n' in key or '\r' in key or '\0' in key: + raise ValueError("Header names must not contain control characters: %r" % key) + return key.title().replace('_', '-') + + +def _hval(value): + value = tonat(value) + if '\n' in value or '\r' in value or '\0' in value: + raise ValueError("Header value must not contain control characters: %r" % value) + return value class HeaderProperty(object): - def __init__(self, name, reader=None, writer=str, default=''): + def __init__(self, name, reader=None, writer=None, default=''): self.name, self.default = name, default self.reader, self.writer = reader, writer self.__doc__ = 'Current value of the %r header.' % name.title() def __get__(self, obj, _): if obj is None: return self - value = obj.headers.get(self.name, self.default) + value = obj.get_header(self.name, self.default) return self.reader(value) if self.reader else value def __set__(self, obj, value): - obj.headers[self.name] = self.writer(value) + obj[self.name] = self.writer(value) if self.writer else value def __delete__(self, obj): - del obj.headers[self.name] + del obj[self.name] class BaseResponse(object): @@ -1572,8 +1658,8 @@ class BaseResponse(object): # Header blacklist for specific response codes # (rfc2616 section 10.2.3 and 10.3.5) bad_headers = { - 204: set(('Content-Type', )), - 304: set(('Allow', 'Content-Encoding', 'Content-Language', + 204: frozenset(('Content-Type', 'Content-Length')), + 304: frozenset(('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Range', 'Content-Type', 'Content-Md5', 'Last-Modified')) } @@ -1600,8 +1686,10 @@ class BaseResponse(object): copy.status = self.status copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) if self._cookies: - copy._cookies = SimpleCookie() - copy._cookies.load(self._cookies.output(header='')) + cookies = copy._cookies = SimpleCookie() + for k,v in self._cookies.items(): + cookies[k] = v.value + cookies[k].update(v) # also copy cookie attributes return copy def __iter__(self): @@ -1625,6 +1713,8 @@ class BaseResponse(object): if isinstance(status, int): code, status = status, _HTTP_STATUS_LINES.get(status) elif ' ' in status: + if '\n' in status or '\r' in status or '\0' in status: + raise ValueError('Status line must not include control chars.') status = status.strip() code = int(status.split()[0]) else: @@ -1664,8 +1754,7 @@ class BaseResponse(object): return self._headers[_hkey(name)][-1] def __setitem__(self, name, value): - self._headers[_hkey(name)] = [value if isinstance(value, unicode) else - str(value)] + self._headers[_hkey(name)] = [_hval(value)] def get_header(self, name, default=None): """ Return the value of a previously defined header. If there is no @@ -1675,19 +1764,23 @@ class BaseResponse(object): def set_header(self, name, value): """ Create a new response header, replacing any previously defined headers with the same name. """ - self._headers[_hkey(name)] = [value if isinstance(value, unicode) - else str(value)] + self._headers[_hkey(name)] = [_hval(value)] def add_header(self, name, value): """ Add an additional response header, not removing duplicates. """ - self._headers.setdefault(_hkey(name), []).append( - value if isinstance(value, unicode) else str(value)) + self._headers.setdefault(_hkey(name), []).append(_hval(value)) def iter_headers(self): """ Yield (header, value) tuples, skipping headers that are not allowed with the current response status code. """ return self.headerlist + def _wsgi_status_line(self): + """ WSGI conform status line (latin1-encodeable) """ + if py3k: + return self._status_line.encode('utf8').decode('latin1') + return self._status_line + @property def headerlist(self): """ WSGI conform list of (header, value) tuples. """ @@ -1701,15 +1794,13 @@ class BaseResponse(object): out += [(name, val) for (name, vals) in headers for val in vals] if self._cookies: for c in self._cookies.values(): - out.append(('Set-Cookie', c.OutputString())) + out.append(('Set-Cookie', _hval(c.OutputString()))) if py3k: - return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] - else: - return [(k, v.encode('utf8') if isinstance(v, unicode) else v) - for (k, v) in out] + out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + return out content_type = HeaderProperty('Content-Type') - content_length = HeaderProperty('Content-Length', reader=int) + content_length = HeaderProperty('Content-Length', reader=int, default=-1) expires = HeaderProperty( 'Expires', reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), @@ -1722,7 +1813,7 @@ class BaseResponse(object): return self.content_type.split('charset=')[-1].split(';')[0].strip() return default - def set_cookie(self, name, value, secret=None, **options): + def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options): """ Create a new cookie or replace an old one. If the `secret` parameter is set, create a `Signed Cookie` (described below). @@ -1733,7 +1824,7 @@ class BaseResponse(object): Additionally, this method accepts all RFC 2109 attributes that are supported by :class:`cookie.Morsel`, including: - :param max_age: maximum age in seconds. (default: None) + :param maxage: maximum age in seconds. (default: None) :param expires: a datetime object or UNIX timestamp. (default: None) :param domain: the domain that is allowed to read the cookie. (default: current domain) @@ -1741,8 +1832,10 @@ class BaseResponse(object): :param secure: limit the cookie to HTTPS connections (default: off). :param httponly: prevents client-side javascript to read this cookie (default: off, requires Python 2.6 or newer). + :param samesite: Control or disable third-party use for this cookie. + Possible values: `lax`, `strict` or `none` (default). - If neither `expires` nor `max_age` is set (default), the cookie will + If neither `expires` nor `maxage` is set (default), the cookie will expire at the end of the browser session (as soon as the browser window is closed). @@ -1750,6 +1843,11 @@ class BaseResponse(object): cryptographically signed to prevent manipulation. Keep in mind that cookies are limited to 4kb in most browsers. + Warning: Pickle is a potentially dangerous format. If an attacker + gains access to the secret key, he could forge cookies that execute + code on server side if unpickled. Using pickle is discouraged and + support for it will be removed in later versions of bottle. + Warning: Signed cookies are not encrypted (the client can still see the content) and not copy-protected (the client can restore an old cookie). The main intention is to make pickling and unpickling @@ -1758,10 +1856,22 @@ class BaseResponse(object): if not self._cookies: self._cookies = SimpleCookie() + # Monkey-patch Cookie lib to support 'SameSite' parameter + # https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 + if py < (3, 8, 0): + Morsel._reserved.setdefault('samesite', 'SameSite') + if secret: - value = touni(cookie_encode((name, value), secret)) + if not isinstance(value, basestring): + depr(0, 13, "Pickling of arbitrary objects into cookies is " + "deprecated.", "Only store strings in cookies. " + "JSON strings are fine, too.") + encoded = base64.b64encode(pickle.dumps([name, value], -1)) + sig = base64.b64encode(hmac.new(tob(secret), encoded, + digestmod=digestmod).digest()) + value = touni(tob('!') + sig + tob('?') + encoded) elif not isinstance(value, basestring): - raise TypeError('Secret key missing for non-string Cookie.') + raise TypeError('Secret key required for non-string cookies.') # Cookie size plus options must not exceed 4kb. if len(name) + len(value) > 3800: @@ -1770,18 +1880,19 @@ class BaseResponse(object): self._cookies[name] = value for key, value in options.items(): - if key == 'max_age': + if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13 + key = 'max-age' if isinstance(value, timedelta): value = value.seconds + value.days * 24 * 3600 if key == 'expires': - if isinstance(value, (datedate, datetime)): - value = value.timetuple() - elif isinstance(value, (int, float)): - value = time.gmtime(value) - value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + value = http_date(value) + if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13 + key, value = 'samesite', (value or "none").lower() + if value not in ('lax', 'strict', 'none'): + raise CookieError("Invalid value for SameSite") if key in ('secure', 'httponly') and not value: continue - self._cookies[name][key.replace('_', '-')] = value + self._cookies[name][key] = value def delete_cookie(self, key, **kwargs): """ Delete a cookie. Be sure to use the same `domain` and `path` @@ -1883,15 +1994,27 @@ class JSONPlugin(object): def __init__(self, json_dumps=json_dumps): self.json_dumps = json_dumps - def apply(self, callback, _): + def setup(self, app): + app.config._define('json.enable', default=True, validate=bool, + help="Enable or disable automatic dict->json filter.") + app.config._define('json.ascii', default=False, validate=bool, + help="Use only 7-bit ASCII characters in output.") + app.config._define('json.indent', default=True, validate=bool, + help="Add whitespace to make json more readable.") + app.config._define('json.dump_func', default=None, + help="If defined, use this function to transform" + " dict into json. The other options no longer" + " apply.") + + def apply(self, callback, route): dumps = self.json_dumps - if not dumps: return callback + if not self.json_dumps: return callback def wrapper(*a, **ka): try: rv = callback(*a, **ka) - except HTTPError: - rv = _e() + except HTTPResponse as resp: + rv = resp if isinstance(rv, dict): #Attempt to serialize, raises exception on failure @@ -2113,7 +2236,6 @@ class FormsDict(MultiDict): return super(FormsDict, self).__getattr__(name) return self.getunicode(name, default=default) - class HeaderDict(MultiDict): """ A case-insensitive version of :class:`MultiDict` that defaults to replace the old value instead of appending it. """ @@ -2132,16 +2254,13 @@ class HeaderDict(MultiDict): return self.dict[_hkey(key)][-1] def __setitem__(self, key, value): - self.dict[_hkey(key)] = [value if isinstance(value, unicode) else - str(value)] + self.dict[_hkey(key)] = [_hval(value)] def append(self, key, value): - self.dict.setdefault(_hkey(key), []).append( - value if isinstance(value, unicode) else str(value)) + self.dict.setdefault(_hkey(key), []).append(_hval(value)) def replace(self, key, value): - self.dict[_hkey(key)] = [value if isinstance(value, unicode) else - str(value)] + self.dict[_hkey(key)] = [_hval(value)] def getall(self, key): return self.dict.get(_hkey(key)) or [] @@ -2150,7 +2269,7 @@ class HeaderDict(MultiDict): return MultiDict.get(self, _hkey(key), default, index) def filter(self, names): - for name in [_hkey(n) for n in names]: + for name in (_hkey(n) for n in names): if name in self.dict: del self.dict[name] @@ -2214,48 +2333,99 @@ class WSGIHeaderDict(DictMixin): def __contains__(self, key): return self._ekey(key) in self.environ +_UNSET = object() class ConfigDict(dict): """ A dict-like configuration storage with additional support for - namespaces, validators, meta-data, on_change listeners and more. + namespaces, validators, meta-data, overlays and more. + + This dict-like class is heavily optimized for read access. All read-only + methods as well as item access should be as fast as the built-in dict. """ - __slots__ = ('_meta', '_on_change') + __slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__') def __init__(self): self._meta = {} - self._on_change = lambda name, value: None + self._change_listener = [] + #: Weak references of overlays that need to be kept in sync. + self._overlays = [] + #: Config that is the source for this overlay. + self._source = None + #: Keys of values copied from the source (values we do not own) + self._virtual_keys = set() - def load_module(self, path, squash): - """ Load values from a Python module. - :param squash: Squash nested dicts into namespaces by using - load_dict(), otherwise use update() - Example: load_config('my.app.settings', True) - Example: load_config('my.app.settings', False) + def load_module(self, path, squash=True): + """Load values from a Python module. + + Example modue ``config.py``:: + + DEBUG = True + SQLITE = { + "db": ":memory:" + } + + + >>> c = ConfigDict() + >>> c.load_module('config') + {DEBUG: True, 'SQLITE.DB': 'memory'} + >>> c.load_module("config", False) + {'DEBUG': True, 'SQLITE': {'DB': 'memory'}} + + :param squash: If true (default), dictionary values are assumed to + represent namespaces (see :meth:`load_dict`). """ - config_obj = __import__(path) - obj = dict([(key, getattr(config_obj, key)) - for key in dir(config_obj) if key.isupper()]) + config_obj = load(path) + obj = {key: getattr(config_obj, key) for key in dir(config_obj) + if key.isupper()} + if squash: self.load_dict(obj) else: self.update(obj) return self - def load_config(self, filename): + def load_config(self, filename, **options): """ Load values from an ``*.ini`` style config file. - If the config file contains sections, their names are used as - namespaces for the values within. The two special sections - ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + A configuration file consists of sections, each led by a + ``[section]`` header, followed by key/value entries separated by + either ``=`` or ``:``. Section names and keys are case-insensitive. + Leading and trailing whitespace is removed from keys and values. + Values can be omitted, in which case the key/value delimiter may + also be left out. Values can also span multiple lines, as long as + they are indented deeper than the first line of the value. Commands + are prefixed by ``#`` or ``;`` and may only appear on their own on + an otherwise empty line. + + Both section and key names may contain dots (``.``) as namespace + separators. The actual configuration parameter name is constructed + by joining section name and key name together and converting to + lower case. + + The special sections ``bottle`` and ``ROOT`` refer to the root + namespace and the ``DEFAULT`` section defines default values for all + other sections. + + With Python 3, extended string interpolation is enabled. + + :param filename: The path of a config file, or a list of paths. + :param options: All keyword parameters are passed to the underlying + :class:`python:configparser.ConfigParser` constructor call. + """ - conf = ConfigParser() + options.setdefault('allow_no_value', True) + if py3k: + options.setdefault('interpolation', + configparser.ExtendedInterpolation()) + conf = configparser.ConfigParser(**options) conf.read(filename) for section in conf.sections(): - for key, value in conf.items(section): - if section not in ('DEFAULT', 'bottle'): + for key in conf.options(section): + value = conf.get(section, key) + if section not in ('bottle', 'ROOT'): key = section + '.' + key - self[key] = value + self[key.lower()] = value return self def load_dict(self, source, namespace=''): @@ -2280,7 +2450,10 @@ class ConfigDict(dict): def update(self, *a, **ka): """ If the first parameter is a string, all keys are prefixed with this namespace. Apart from that it works just as the usual dict.update(). - Example: ``update('some.namespace', key='value')`` """ + + >>> c = ConfigDict() + >>> c.update('some.namespace', key='value') + """ prefix = '' if a and isinstance(a[0], basestring): prefix = a[0].strip('.') + '.' @@ -2296,38 +2469,141 @@ class ConfigDict(dict): def __setitem__(self, key, value): if not isinstance(key, basestring): raise TypeError('Key has type %r (not a string)' % type(key)) + + self._virtual_keys.discard(key) + value = self.meta_get(key, 'filter', lambda x: x)(value) if key in self and self[key] is value: return + self._on_change(key, value) dict.__setitem__(self, key, value) + for overlay in self._iter_overlays(): + overlay._set_virtual(key, value) + def __delitem__(self, key): - self._on_change(key, None) + if key not in self: + raise KeyError(key) + if key in self._virtual_keys: + raise KeyError("Virtual keys cannot be deleted: %s" % key) + + if self._source and key in self._source: + # Not virtual, but present in source -> Restore virtual value + dict.__delitem__(self, key) + self._set_virtual(key, self._source[key]) + else: # not virtual, not present in source. This is OUR value + self._on_change(key, None) + dict.__delitem__(self, key) + for overlay in self._iter_overlays(): + overlay._delete_virtual(key) + + def _set_virtual(self, key, value): + """ Recursively set or update virtual keys. Do nothing if non-virtual + value is present. """ + if key in self and key not in self._virtual_keys: + return # Do nothing for non-virtual keys. + + self._virtual_keys.add(key) + if key in self and self[key] is not value: + self._on_change(key, value) + dict.__setitem__(self, key, value) + for overlay in self._iter_overlays(): + overlay._set_virtual(key, value) + + def _delete_virtual(self, key): + """ Recursively delete virtual entry. Do nothing if key is not virtual. + """ + if key not in self._virtual_keys: + return # Do nothing for non-virtual keys. + + if key in self: + self._on_change(key, None) dict.__delitem__(self, key) + self._virtual_keys.discard(key) + for overlay in self._iter_overlays(): + overlay._delete_virtual(key) + + def _on_change(self, key, value): + for cb in self._change_listener: + if cb(self, key, value): + return True + + def _add_change_listener(self, func): + self._change_listener.append(func) + return func def meta_get(self, key, metafield, default=None): """ Return the value of a meta field for a key. """ return self._meta.get(key, {}).get(metafield, default) def meta_set(self, key, metafield, value): - """ Set the meta field for a key to a new value. This triggers the - on-change handler for existing keys. """ + """ Set the meta field for a key to a new value. """ self._meta.setdefault(key, {})[metafield] = value - if key in self: - self[key] = self[key] def meta_list(self, key): """ Return an iterable of meta field names defined for a key. """ return self._meta.get(key, {}).keys() + def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET): + """ (Unstable) Shortcut for plugins to define own config parameters. """ + if default is not _UNSET: + self.setdefault(key, default) + if help is not _UNSET: + self.meta_set(key, 'help', help) + if validate is not _UNSET: + self.meta_set(key, 'validate', validate) + + def _iter_overlays(self): + for ref in self._overlays: + overlay = ref() + if overlay is not None: + yield overlay + + def _make_overlay(self): + """ (Unstable) Create a new overlay that acts like a chained map: Values + missing in the overlay are copied from the source map. Both maps + share the same meta entries. + + Entries that were copied from the source are called 'virtual'. You + can not delete virtual keys, but overwrite them, which turns them + into non-virtual entries. Setting keys on an overlay never affects + its source, but may affect any number of child overlays. + + Other than collections.ChainMap or most other implementations, this + approach does not resolve missing keys on demand, but instead + actively copies all values from the source to the overlay and keeps + track of virtual and non-virtual keys internally. This removes any + lookup-overhead. Read-access is as fast as a build-in dict for both + virtual and non-virtual keys. + + Changes are propagated recursively and depth-first. A failing + on-change handler in an overlay stops the propagation of virtual + values and may result in an partly updated tree. Take extra care + here and make sure that on-change handlers never fail. + + Used by Route.config + """ + # Cleanup dead references + self._overlays[:] = [ref for ref in self._overlays if ref() is not None] + + overlay = ConfigDict() + overlay._meta = self._meta + overlay._source = self + self._overlays.append(weakref.ref(overlay)) + for key in self: + overlay._set_virtual(key, self[key]) + return overlay + + + class AppStack(list): """ A stack-like list. Calling it returns the head of the stack. """ def __call__(self): """ Return the current default application. """ - return self[-1] + return self.default def push(self, value=None): """ Add a new :class:`Bottle` instance to the stack """ @@ -2335,20 +2611,28 @@ class AppStack(list): value = Bottle() self.append(value) return value + new_app = push + + @property + def default(self): + try: + return self[-1] + except IndexError: + return self.push() class WSGIFileWrapper(object): def __init__(self, fp, buffer_size=1024 * 64): self.fp, self.buffer_size = fp, buffer_size - for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + for attr in 'fileno', 'close', 'read', 'readlines', 'tell', 'seek': if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) def __iter__(self): buff, read = self.buffer_size, self.read - while True: - part = read(buff) - if not part: return + part = read(buff) + while part: yield part + part = read(buff) class _closeiter(object): @@ -2433,7 +2717,7 @@ class ResourceManager(object): """ Search for a resource and return an absolute file path, or `None`. The :attr:`path` list is searched in order. The first match is - returend. Symlinks are followed. The result is cached to speed up + returned. Symlinks are followed. The result is cached to speed up future lookups. """ if name not in self.cache or DEBUG: for path in self.path: @@ -2468,6 +2752,10 @@ class FileUpload(object): content_type = HeaderProperty('Content-Type') content_length = HeaderProperty('Content-Length', reader=int, default=-1) + def get_header(self, name, default=None): + """ Return the value of a header within the multipart part. """ + return self.headers.get(name, default) + @cached_property def filename(self): """ Name of the file on the client file system, but normalized to ensure @@ -2537,42 +2825,61 @@ def redirect(url, code=None): raise res -def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): - """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024, close=False): + """ Yield chunks from a range in a file, optionally closing it at the end. + No chunk is bigger than maxread. """ fp.seek(offset) while bytes > 0: part = fp.read(min(bytes, maxread)) - if not part: break + if not part: + break bytes -= len(part) yield part + if close: + fp.close() def static_file(filename, root, - mimetype='auto', + mimetype=True, download=False, - charset='UTF-8'): - """ Open a file in a safe way and return :exc:`HTTPResponse` with status - code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, - ``Content-Length`` and ``Last-Modified`` headers are set if possible. - Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` - requests. + charset='UTF-8', + etag=None, + headers=None): + """ Open a file in a safe way and return an instance of :exc:`HTTPResponse` + that can be sent back to the client. - :param filename: Name or path of the file to send. + :param filename: Name or path of the file to send, relative to ``root``. :param root: Root path for file lookups. Should be an absolute directory path. - :param mimetype: Defines the content-type header (default: guess from + :param mimetype: Provide the content-type header (default: guess from file extension) :param download: If True, ask the browser to open a `Save as...` dialog instead of opening the file with the associated program. You can specify a custom filename as a string. If not specified, the original filename is used (default: False). - :param charset: The charset to use for files with a ``text/*`` - mime-type. (default: UTF-8) + :param charset: The charset for files with a ``text/*`` mime-type. + (default: UTF-8) + :param etag: Provide a pre-computed ETag header. If set to ``False``, + ETag handling is disabled. (default: auto-generate ETag header) + :param headers: Additional headers dict to add to the response. + + While checking user input is always a good idea, this function provides + additional protection against malicious ``filename`` parameters from + breaking out of the ``root`` directory and leaking sensitive information + to an attacker. + + Read-protected files or files outside of the ``root`` directory are + answered with ``403 Access Denied``. Missing files result in a + ``404 Not Found`` response. Conditional requests (``If-Modified-Since``, + ``If-None-Match``) are answered with ``304 Not Modified`` whenever + possible. ``HEAD`` and ``Range`` requests (used by download managers to + check or continue partial downloads) are also handled automatically. + """ - root = os.path.abspath(root) + os.sep + root = os.path.join(os.path.abspath(root), '') filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) - headers = dict() + headers = headers.copy() if headers else {} if not filename.startswith(root): return HTTPError(403, "Access denied.") @@ -2581,47 +2888,61 @@ def static_file(filename, root, if not os.access(filename, os.R_OK): return HTTPError(403, "You do not have permission to access this file.") - if mimetype == 'auto': - if download and download != True: + if mimetype is True: + if download and download is not True: mimetype, encoding = mimetypes.guess_type(download) else: mimetype, encoding = mimetypes.guess_type(filename) - if encoding: headers['Content-Encoding'] = encoding + if encoding: + headers['Content-Encoding'] = encoding if mimetype: - if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ + and charset and 'charset' not in mimetype: mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype if download: - download = os.path.basename(filename if download == True else download) + download = os.path.basename(filename if download is True else download) headers['Content-Disposition'] = 'attachment; filename="%s"' % download stats = os.stat(filename) headers['Content-Length'] = clen = stats.st_size - lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) - headers['Last-Modified'] = lm + headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, + usegmt=True) + headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) - ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') + getenv = request.environ.get + + if etag is None: + etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime, + clen, filename) + etag = hashlib.sha1(tob(etag)).hexdigest() + + if etag: + headers['ETag'] = etag + check = getenv('HTTP_IF_NONE_MATCH') + if check and check == etag: + return HTTPResponse(status=304, **headers) + + ims = getenv('HTTP_IF_MODIFIED_SINCE') if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): - headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", - time.gmtime()) return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else open(filename, 'rb') headers["Accept-Ranges"] = "bytes" - ranges = request.environ.get('HTTP_RANGE') - if 'HTTP_RANGE' in request.environ: - ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + range_header = getenv('HTTP_RANGE') + if range_header: + ranges = list(parse_range_header(range_header, clen)) if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) headers["Content-Length"] = str(end - offset) - if body: body = _file_iter_range(body, offset, end - offset) + if body: body = _file_iter_range(body, offset, end - offset, close=True) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers) @@ -2634,25 +2955,31 @@ def debug(mode=True): """ Change the debug level. There is only one debug level supported at the moment.""" global DEBUG - #if mode: warnings.simplefilter('default') # neutralizing already set warning filters (e.g. DeprecationWarning inside sqlmapapi.py) + if mode: warnings.simplefilter('default') DEBUG = bool(mode) def http_date(value): - if isinstance(value, (datedate, datetime)): + if isinstance(value, basestring): + return value + if isinstance(value, datetime): + # aware datetime.datetime is converted to UTC time + # naive datetime.datetime is treated as UTC time value = value.utctimetuple() - elif isinstance(value, (int, float)): - value = time.gmtime(value) - if not isinstance(value, basestring): - value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) - return value + elif isinstance(value, datedate): + # datetime.date is naive, and is treated as UTC time + value = value.timetuple() + if not isinstance(value, (int, float)): + # convert struct_time in UTC to UNIX timestamp + value = calendar.timegm(value) + return email.utils.formatdate(value, usegmt=True) def parse_date(ims): """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ try: ts = email.utils.parsedate_tz(ims) - return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone + return calendar.timegm(ts[:8] + (0, )) - (ts[9] or 0) except (TypeError, ValueError, IndexError, OverflowError): return None @@ -2687,9 +3014,45 @@ def parse_range_header(header, maxlen=0): pass +#: Header tokenizer used by _parse_http_header() +_hsplit = re.compile('(?:(?:"((?:[^"\\\\]|\\\\.)*)")|([^;,=]+))([;,=]?)').findall + +def _parse_http_header(h): + """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values + and parameters. For non-standard or broken input, this implementation may return partial results. + :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``) + :return: List of (value, params) tuples. The second element is a (possibly empty) dict. + """ + values = [] + if '"' not in h: # INFO: Fast path without regexp (~2x faster) + for value in h.split(','): + parts = value.split(';') + values.append((parts[0].strip(), {})) + for attr in parts[1:]: + name, value = attr.split('=', 1) + values[-1][1][name.strip()] = value.strip() + else: + lop, key, attrs = ',', None, {} + for quoted, plain, tok in _hsplit(h): + value = plain.strip() if plain else quoted.replace('\\"', '"') + if lop == ',': + attrs = {} + values.append((value, attrs)) + elif lop == ';': + if tok == '=': + key = value + else: + attrs[value] = '' + elif lop == '=' and key: + attrs[key] = value + key = None + lop = tok + return values + + def _parse_qsl(qs): r = [] - for pair in qs.replace(';', '&').split('&'): + for pair in qs.split('&'): if not pair: continue nv = pair.split('=', 1) if len(nv) != 2: nv.append('') @@ -2706,25 +3069,34 @@ def _lscmp(a, b): for x, y in zip(a, b)) and len(a) == len(b) -def cookie_encode(data, key): +def cookie_encode(data, key, digestmod=None): """ Encode and sign a pickle-able object. Return a (byte) string """ + depr(0, 13, "cookie_encode() will be removed soon.", + "Do not use this API directly.") + digestmod = digestmod or hashlib.sha256 msg = base64.b64encode(pickle.dumps(data, -1)) - sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) return tob('!') + sig + tob('?') + msg -def cookie_decode(data, key): +def cookie_decode(data, key, digestmod=None): """ Verify and decode an encoded string. Return an object or None.""" + depr(0, 13, "cookie_decode() will be removed soon.", + "Do not use this API directly.") data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?'), 1) - if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + digestmod = digestmod or hashlib.sha256 + hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() + if _lscmp(sig[1:], base64.b64encode(hashed)): return pickle.loads(base64.b64decode(msg)) return None def cookie_is_encoded(data): """ Return True if the argument looks like a encoded cookie.""" + depr(0, 13, "cookie_is_encoded() will be removed soon.", + "Do not use this API directly.") return bool(data.startswith(tob('!')) and tob('?') in data) @@ -2841,6 +3213,9 @@ url = make_default_app_wrapper('get_url') # Server Adapter ############################################################### ############################################################################### +# Before you edit or add a server adapter, please read: +# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870 +# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341 class ServerAdapter(object): quiet = False @@ -2854,8 +3229,8 @@ class ServerAdapter(object): pass def __repr__(self): - args = ', '.join(['%s=%s' % (k, repr(v)) - for k, v in self.options.items()]) + args = ', '.join('%s=%s' % (k, repr(v)) + for k, v in self.options.items()) return "%s(%s)" % (self.__class__.__name__, args) @@ -2914,7 +3289,11 @@ class WSGIRefServer(ServerAdapter): class CherryPyServer(ServerAdapter): def run(self, handler): # pragma: no cover - from cherrypy import wsgiserver + depr(0, 13, "The wsgi server part of cherrypy was split into a new " + "project called 'cheroot'.", "Use the 'cheroot' server " + "adapter instead of cherrypy.") + from cherrypy import wsgiserver # This will fail for CherryPy >= 9 + self.options['bind_addr'] = (self.host, self.port) self.options['wsgi_app'] = handler @@ -2937,10 +3316,29 @@ class CherryPyServer(ServerAdapter): server.stop() +class CherootServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cheroot import wsgi + from cheroot.ssl import builtin + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + certfile = self.options.pop('certfile', None) + keyfile = self.options.pop('keyfile', None) + chainfile = self.options.pop('chainfile', None) + server = wsgi.Server(**self.options) + if certfile and keyfile: + server.ssl_adapter = builtin.BuiltinSSLAdapter( + certfile, keyfile, chainfile) + try: + server.start() + finally: + server.stop() + + class WaitressServer(ServerAdapter): def run(self, handler): from waitress import serve - serve(handler, host=self.host, port=self.port, _quiet=self.quiet) + serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options) class PasteServer(ServerAdapter): @@ -2964,6 +3362,7 @@ class FapwsServer(ServerAdapter): """ Extremely fast webserver using libev. See http://www.fapws.org/ """ def run(self, handler): # pragma: no cover + depr(0, 13, "fapws3 is not maintained and support will be dropped.") import fapws._evwsgi as evwsgi from fapws import base, config port = self.port @@ -2973,8 +3372,8 @@ class FapwsServer(ServerAdapter): evwsgi.start(self.host, port) # fapws3 never releases the GIL. Complain upstream. I tried. No luck. if 'BOTTLE_CHILD' in os.environ and not self.quiet: - _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") - _stderr(" (Fapws3 breaks python thread support)\n") + _stderr("WARNING: Auto-reloading does not work with Fapws3.") + _stderr(" (Fapws3 breaks python thread support)") evwsgi.set_base_module(base) def app(environ, start_response): @@ -3001,6 +3400,8 @@ class AppEngineServer(ServerAdapter): quiet = True def run(self, handler): + depr(0, 13, "AppEngineServer no longer required", + "Configure your application directly in your app.yaml") from google.appengine.ext.webapp import util # A main() function in the handler script enables 'App Caching'. # Lets makes sure it is there. This _really_ improves performance. @@ -3030,6 +3431,7 @@ class DieselServer(ServerAdapter): """ Untested. """ def run(self, handler): + depr(0, 13, "Diesel is not tested or supported and will be removed.") from diesel.protocols.wsgi import WSGIApplication app = WSGIApplication(handler, port=self.port) app.run() @@ -3038,45 +3440,41 @@ class DieselServer(ServerAdapter): class GeventServer(ServerAdapter): """ Untested. Options: - * `fast` (default: False) uses libevent's http server, but has some - issues: No streaming, no pipelining, no SSL. * See gevent.wsgi.WSGIServer() documentation for more options. """ def run(self, handler): - from gevent import wsgi, pywsgi, local + from gevent import pywsgi, local if not isinstance(threading.local(), local.local): msg = "Bottle requires gevent.monkey.patch_all() (before import)" raise RuntimeError(msg) - if not self.options.pop('fast', None): wsgi = pywsgi - self.options['log'] = None if self.quiet else 'default' + if self.quiet: + self.options['log'] = None address = (self.host, self.port) - server = wsgi.WSGIServer(address, handler, **self.options) + server = pywsgi.WSGIServer(address, handler, **self.options) if 'BOTTLE_CHILD' in os.environ: import signal signal.signal(signal.SIGINT, lambda s, f: server.stop()) server.serve_forever() -class GeventSocketIOServer(ServerAdapter): - def run(self, handler): - from socketio import server - address = (self.host, self.port) - server.SocketIOServer(address, handler, **self.options).serve_forever() - - class GunicornServer(ServerAdapter): """ Untested. See http://gunicorn.org/configure.html for options. """ def run(self, handler): - from gunicorn.app.base import Application + from gunicorn.app.base import BaseApplication + + if self.host.startswith("unix:"): + config = {'bind': self.host} + else: + config = {'bind': "%s:%d" % (self.host, self.port)} - config = {'bind': "%s:%d" % (self.host, int(self.port))} config.update(self.options) - class GunicornApplication(Application): - def init(self, parser, opts, args): - return config + class GunicornApplication(BaseApplication): + def load_config(self): + for key, value in config.items(): + self.cfg.set(key, value) def load(self): return handler @@ -3114,57 +3512,53 @@ class EventletServer(ServerAdapter): wsgi.server(listen(address), handler) -class RocketServer(ServerAdapter): - """ Untested. """ - - def run(self, handler): - from rocket import Rocket - server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) - server.start() - - class BjoernServer(ServerAdapter): """ Fast server written in C: https://github.com/jonashaag/bjoern """ def run(self, handler): from bjoern import run - run(handler, self.host, self.port) + run(handler, self.host, self.port, reuse_port=True) +class AsyncioServerAdapter(ServerAdapter): + """ Extend ServerAdapter for adding custom event loop """ + def get_event_loop(self): + pass -class AiohttpServer(ServerAdapter): - """ Untested. - aiohttp +class AiohttpServer(AsyncioServerAdapter): + """ Asynchronous HTTP client/server framework for asyncio https://pypi.python.org/pypi/aiohttp/ + https://pypi.org/project/aiohttp-wsgi/ """ + def get_event_loop(self): + import asyncio + return asyncio.new_event_loop() + def run(self, handler): import asyncio - from aiohttp.wsgi import WSGIServerHttpProtocol - self.loop = asyncio.new_event_loop() + from aiohttp_wsgi.wsgi import serve + self.loop = self.get_event_loop() asyncio.set_event_loop(self.loop) - protocol_factory = lambda: WSGIServerHttpProtocol( - handler, - readpayload=True, - debug=(not self.quiet)) - self.loop.run_until_complete(self.loop.create_server(protocol_factory, - self.host, - self.port)) - if 'BOTTLE_CHILD' in os.environ: import signal signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) - try: - self.loop.run_forever() - except KeyboardInterrupt: - self.loop.stop() + serve(handler, host=self.host, port=self.port) +class AiohttpUVLoopServer(AiohttpServer): + """uvloop + https://github.com/MagicStack/uvloop + """ + def get_event_loop(self): + import uvloop + return uvloop.new_event_loop() + class AutoServer(ServerAdapter): """ Untested. """ adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, - WSGIRefServer] + CherootServer, WSGIRefServer] def run(self, handler): for sa in self.adapters: @@ -3180,6 +3574,7 @@ server_names = { 'wsgiref': WSGIRefServer, 'waitress': WaitressServer, 'cherrypy': CherryPyServer, + 'cheroot': CherootServer, 'paste': PasteServer, 'fapws3': FapwsServer, 'tornado': TornadoServer, @@ -3190,10 +3585,9 @@ server_names = { 'gunicorn': GunicornServer, 'eventlet': EventletServer, 'gevent': GeventServer, - 'geventSocketIO': GeventSocketIOServer, - 'rocket': RocketServer, 'bjoern': BjoernServer, 'aiohttp': AiohttpServer, + 'uvloop': AiohttpUVLoopServer, 'auto': AutoServer, } @@ -3269,22 +3663,26 @@ def run(app=None, if NORUN: return if reloader and not os.environ.get('BOTTLE_CHILD'): import subprocess - lockfile = None + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + args = [sys.executable] + sys.argv + # If a package was loaded with `python -m`, then `sys.argv` needs to be + # restored to the original value, or imports might break. See #1336 + if getattr(sys.modules.get('__main__'), '__package__', None): + args[1:1] = ["-m", sys.modules['__main__'].__package__] + try: - fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') - os.close(fd) # We only need this file to exist. We never write to it + os.close(fd) # We never write to this file while os.path.exists(lockfile): - args = [sys.executable] + sys.argv - environ = os.environ.copy() - environ['BOTTLE_CHILD'] = 'true' - environ['BOTTLE_LOCKFILE'] = lockfile p = subprocess.Popen(args, env=environ) - while p.poll() is None: # Busy wait... - os.utime(lockfile, None) # I am alive! + while p.poll() is None: + os.utime(lockfile, None) # Tell child we are still alive time.sleep(interval) - if p.poll() != 3: - if os.path.exists(lockfile): os.unlink(lockfile) - sys.exit(p.poll()) + if p.returncode == 3: # Child wants to be restarted + continue + sys.exit(p.returncode) except KeyboardInterrupt: pass finally: @@ -3319,11 +3717,14 @@ def run(app=None, server.quiet = server.quiet or quiet if not server.quiet: - _stderr("Bottle v%s server starting up (using %s)...\n" % + _stderr("Bottle v%s server starting up (using %s)..." % (__version__, repr(server))) - _stderr("Listening on http://%s:%d/\n" % - (server.host, server.port)) - _stderr("Hit Ctrl-C to quit.\n\n") + if server.host.startswith("unix:"): + _stderr("Listening on %s" % server.host) + else: + _stderr("Listening on http://%s:%d/" % + (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n") if reloader: lockfile = os.environ.get('BOTTLE_LOCKFILE') @@ -3348,7 +3749,7 @@ def run(app=None, class FileCheckerThread(threading.Thread): """ Interrupt main-thread as soon as a changed module file is detected, - the lockfile gets deleted or gets to old. """ + the lockfile gets deleted or gets too old. """ def __init__(self, lockfile, interval): threading.Thread.__init__(self) @@ -3363,7 +3764,7 @@ class FileCheckerThread(threading.Thread): files = dict() for module in list(sys.modules.values()): - path = getattr(module, '__file__', '') + path = getattr(module, '__file__', '') or '' if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] if path and exists(path): files[path] = mtime(path) @@ -3392,9 +3793,8 @@ class FileCheckerThread(threading.Thread): ############################################################################### -class TemplateError(HTTPError): - def __init__(self, message): - HTTPError.__init__(self, 500, message) +class TemplateError(BottleException): + pass class BaseTemplate(object): @@ -3438,13 +3838,11 @@ class BaseTemplate(object): """ Search name in all directories specified in lookup. First without, then with common extensions. Return first hit. """ if not lookup: - depr('The template lookup path list should not be empty.', - True) #0.12 - lookup = ['.'] + raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.") - if os.path.isabs(name) and os.path.isfile(name): - depr('Absolute template path names are deprecated.', True) #0.12 - return os.path.abspath(name) + if os.path.isabs(name): + raise depr(0, 12, "Use of absolute path for template name.", + "Refer to templates with names or paths relative to the lookup path.") for spath in lookup: spath = os.path.abspath(spath) + os.sep @@ -3534,7 +3932,7 @@ class Jinja2Template(BaseTemplate): if self.source: self.tpl = self.env.from_string(self.source) else: - self.tpl = self.env.get_template(self.filename) + self.tpl = self.env.get_template(self.name) def render(self, *args, **kwargs): for dictarg in args: @@ -3544,10 +3942,13 @@ class Jinja2Template(BaseTemplate): return self.tpl.render(**_defaults) def loader(self, name): - fname = self.search(name, self.lookup) + if name == self.filename: + fname = name + else: + fname = self.search(name, self.lookup) if not fname: return with open(fname, "rb") as f: - return f.read().decode(self.encoding) + return (f.read().decode(self.encoding), fname, lambda: False) class SimpleTemplate(BaseTemplate): @@ -3576,8 +3977,7 @@ class SimpleTemplate(BaseTemplate): try: source, encoding = touni(source), 'utf8' except UnicodeError: - depr('Template encodings other than utf8 are not supported.') #0.11 - source, encoding = touni(source, 'latin1'), 'latin1' + raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.') parser = StplParser(source, encoding=encoding, syntax=self.syntax) code = parser.translate() self.encoding = parser.encoding @@ -3590,7 +3990,7 @@ class SimpleTemplate(BaseTemplate): env = _env.copy() env.update(kwargs) if _name not in self.cache: - self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax) return self.cache[_name].execute(env['_stdout'], env) def execute(self, _stdout, kwargs): @@ -3608,7 +4008,7 @@ class SimpleTemplate(BaseTemplate): 'setdefault': env.setdefault, 'defined': env.__contains__ }) - eval(self.co, env) + exec(self.co, env) if env.get('_rebase'): subtpl, rargs = env.pop('_rebase') rargs['base'] = ''.join(_stdout) #copy stdout @@ -3628,7 +4028,6 @@ class SimpleTemplate(BaseTemplate): class StplSyntaxError(TemplateError): - pass @@ -3639,7 +4038,7 @@ class StplParser(object): # This huge pile of voodoo magic splits python code into 8 different tokens. # We use the verbose (?x) regex mode to make this more manageable - _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode + _re_tok = r'''( [urbURB]* (?: ''(?!') |""(?!") @@ -3679,7 +4078,13 @@ class StplParser(object): # Match the start tokens of code areas in a template _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' # Match inline statements (may contain python strings) - _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n])*?)%%(inline_end)s''' % _re_inl + + # add the flag in front of the regexp to avoid Deprecation warning (see Issue #949) + # verbose and dot-matches-newline mode + _re_tok = '(?mx)' + _re_tok + _re_inl = '(?mx)' + _re_inl + default_syntax = '<% %> % {{ }}' @@ -3698,7 +4103,7 @@ class StplParser(object): def set_syntax(self, syntax): self._syntax = syntax self._tokens = syntax.split() - if not syntax in self._re_cache: + if syntax not in self._re_cache: names = 'block_start block_close line_start inline_start inline_end' etokens = map(re.escape, self._tokens) pattern_vars = dict(zip(names.split(), etokens)) @@ -3764,15 +4169,18 @@ class StplParser(object): self.paren_depth -= 1 code_line += _pc elif _blk1: # Start-block keyword (if/for/while/def/try/...) - code_line, self.indent_mod = _blk1, -1 + code_line = _blk1 self.indent += 1 + self.indent_mod -= 1 elif _blk2: # Continue-block keyword (else/elif/except/...) - code_line, self.indent_mod = _blk2, -1 - elif _end: # The non-standard 'end'-keyword (ends a block) - self.indent -= 1 + code_line = _blk2 + self.indent_mod -= 1 elif _cend: # The end-code-block template token (usually '%>') if multiline: multiline = False else: code_line += _cend + elif _end: + self.indent -= 1 + self.indent_mod += 1 else: # \n self.write_code(code_line.strip(), comment) self.lineno += 1 @@ -3822,6 +4230,8 @@ def template(*args, **kwargs): or directly (as keyword arguments). """ tpl = args[0] if args else None + for dictarg in args[1:]: + kwargs.update(dictarg) adapter = kwargs.pop('template_adapter', SimpleTemplate) lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) tplid = (id(lookup), tpl) @@ -3836,8 +4246,6 @@ def template(*args, **kwargs): TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) if not TEMPLATES[tplid]: abort(500, 'Template (%s) not found' % tpl) - for dictarg in args[1:]: - kwargs.update(dictarg) return TEMPLATES[tplid].render(kwargs) @@ -3895,6 +4303,7 @@ HTTP_CODES[418] = "I'm a teapot" # RFC 2324 HTTP_CODES[428] = "Precondition Required" HTTP_CODES[429] = "Too Many Requests" HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725 HTTP_CODES[511] = "Network Authentication Required" _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) for (k, v) in HTTP_CODES.items()) @@ -3921,7 +4330,12 @@ ERROR_PAGE_TEMPLATE = """
{{e.body}}
%%if DEBUG and e.exception:

Exception:

-
{{repr(e.exception)}}
+ %%try: + %%exc = repr(e.exception) + %%except: + %%exc = '' %% type(e.exception).__name__ + %%end +
{{exc}}
%%end %%if DEBUG and e.traceback:

Traceback:

@@ -3947,10 +4361,9 @@ response = LocalResponse() #: A thread-safe namespace. Not used by Bottle. local = threading.local() -# Initialize app stack (create first empty Bottle app) +# Initialize app stack (create first empty Bottle app now deferred until needed) # BC: 0.6.4 and needed for run() -app = default_app = AppStack() -app.push() +apps = app = default_app = AppStack() #: A virtual package that redirects import statements. #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. @@ -3958,58 +4371,59 @@ ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__ + ".ext", 'bottle_%s').module +def _main(argv): # pragma: no coverage + args, parser = _cli_parse(argv) -if __name__ == '__main__': - opt, args, parser = _cli_parse(sys.argv) - - def _cli_error(msg): + def _cli_error(cli_msg): parser.print_help() - _stderr('\nError: %s\n' % msg) + _stderr('\nError: %s\n' % cli_msg) sys.exit(1) - if opt.version: - _stdout('Bottle %s\n' % __version__) + if args.version: + print('Bottle %s' % __version__) sys.exit(0) - if not args: + if not args.app: _cli_error("No application entry point specified.") sys.path.insert(0, '.') sys.modules.setdefault('bottle', sys.modules['__main__']) - host, port = (opt.bind or 'localhost'), 8080 + host, port = (args.bind or 'localhost'), 8080 if ':' in host and host.rfind(']') < host.rfind(':'): host, port = host.rsplit(':', 1) host = host.strip('[]') config = ConfigDict() - for cfile in opt.conf or []: + for cfile in args.conf or []: try: if cfile.endswith('.json'): with open(cfile, 'rb') as fp: config.load_dict(json_loads(fp.read())) else: config.load_config(cfile) - except ConfigParserError: - _cli_error(str(_e())) + except configparser.Error as parse_error: + _cli_error(parse_error) except IOError: _cli_error("Unable to read config file %r" % cfile) - except (UnicodeError, TypeError, ValueError): - _cli_error("Unable to parse config file %r: %s" % (cfile, _e())) + except (UnicodeError, TypeError, ValueError) as error: + _cli_error("Unable to parse config file %r: %s" % (cfile, error)) - for cval in opt.param or []: + for cval in args.param or []: if '=' in cval: config.update((cval.split('=', 1),)) else: config[cval] = True - run(args[0], + run(args.app, host=host, port=int(port), - server=opt.server, - reloader=opt.reload, - plugins=opt.plugin, - debug=opt.debug, + server=args.server, + reloader=args.reload, + plugins=args.plugin, + debug=args.debug, config=config) -# THE END + +if __name__ == '__main__': # pragma: no coverage + _main(sys.argv)