diff MoinMoin/support/werkzeug/debug/__init__.py @ 6094:9f12f41504fc

upgrade werkzeug from 0.8.3 to 0.11.11 no other changes, does not work like this, see next commit.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Mon, 05 Sep 2016 23:25:59 +0200
parents 8de563c487be
children 7f12cf241d5e
line wrap: on
line diff
--- a/MoinMoin/support/werkzeug/debug/__init__.py	Fri Jan 09 20:17:10 2015 +0100
+++ b/MoinMoin/support/werkzeug/debug/__init__.py	Mon Sep 05 23:25:59 2016 +0200
@@ -5,23 +5,103 @@
 
     WSGI application traceback debugger.
 
-    :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
+    :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
+import os
+import re
+import sys
+import uuid
+import json
+import time
+import getpass
+import hashlib
 import mimetypes
+from itertools import chain
 from os.path import join, dirname, basename, isfile
 from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response
+from werkzeug.http import parse_cookie
 from werkzeug.debug.tbtools import get_current_traceback, render_console_html
 from werkzeug.debug.console import Console
 from werkzeug.security import gen_salt
+from werkzeug._internal import _log
+from werkzeug._compat import text_type
 
 
+# DEPRECATED
 #: import this here because it once was documented as being available
 #: from this module.  In case there are users left ...
-from werkzeug.debug.repr import debug_repr
+from werkzeug.debug.repr import debug_repr  # noqa
+
+
+# A week
+PIN_TIME = 60 * 60 * 24 * 7
+
+
+def hash_pin(pin):
+    if isinstance(pin, text_type):
+        pin = pin.encode('utf-8', 'replace')
+    return hashlib.md5(pin + b'shittysalt').hexdigest()[:12]
+
+
+_machine_id = None
+
+
+def get_machine_id():
+    global _machine_id
+    rv = _machine_id
+    if rv is not None:
+        return rv
+
+    def _generate():
+        # Potential sources of secret information on linux.  The machine-id
+        # is stable across boots, the boot id is not
+        for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
+            try:
+                with open(filename, 'rb') as f:
+                    return f.readline().strip()
+            except IOError:
+                continue
+
+        # On OS X we can use the computer's serial number assuming that
+        # ioreg exists and can spit out that information.
+        try:
+            # Also catch import errors: subprocess may not be available, e.g.
+            # Google App Engine
+            # See https://github.com/pallets/werkzeug/issues/925
+            from subprocess import Popen, PIPE
+            dump = Popen(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2'],
+                         stdout=PIPE).communicate()[0]
+            match = re.search(b'"serial-number" = <([^>]+)', dump)
+            if match is not None:
+                return match.group(1)
+        except (OSError, ImportError):
+            pass
+
+        # On Windows we can use winreg to get the machine guid
+        wr = None
+        try:
+            import winreg as wr
+        except ImportError:
+            try:
+                import _winreg as wr
+            except ImportError:
+                pass
+        if wr is not None:
+            try:
+                with wr.OpenKey(wr.HKEY_LOCAL_MACHINE,
+                                'SOFTWARE\\Microsoft\\Cryptography', 0,
+                                wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk:
+                    return wr.QueryValueEx(rk, 'MachineGuid')[0]
+            except WindowsError:
+                pass
+
+    _machine_id = rv = _generate()
+    return rv
 
 
 class _ConsoleFrame(object):
+
     """Helper class so that we can reuse the frame console code for the
     standalone console.
     """
@@ -31,6 +111,90 @@
         self.id = 0
 
 
+def get_pin_and_cookie_name(app):
+    """Given an application object this returns a semi-stable 9 digit pin
+    code and a random key.  The hope is that this is stable between
+    restarts to not make debugging particularly frustrating.  If the pin
+    was forcefully disabled this returns `None`.
+
+    Second item in the resulting tuple is the cookie name for remembering.
+    """
+    pin = os.environ.get('WERKZEUG_DEBUG_PIN')
+    rv = None
+    num = None
+
+    # Pin was explicitly disabled
+    if pin == 'off':
+        return None, None
+
+    # Pin was provided explicitly
+    if pin is not None and pin.replace('-', '').isdigit():
+        # If there are separators in the pin, return it directly
+        if '-' in pin:
+            rv = pin
+        else:
+            num = pin
+
+    modname = getattr(app, '__module__',
+                      getattr(app.__class__, '__module__'))
+
+    try:
+        # `getpass.getuser()` imports the `pwd` module,
+        # which does not exist in the Google App Engine sandbox.
+        username = getpass.getuser()
+    except ImportError:
+        username = None
+
+    mod = sys.modules.get(modname)
+
+    # This information only exists to make the cookie unique on the
+    # computer, not as a security feature.
+    probably_public_bits = [
+        username,
+        modname,
+        getattr(app, '__name__', getattr(app.__class__, '__name__')),
+        getattr(mod, '__file__', None),
+    ]
+
+    # This information is here to make it harder for an attacker to
+    # guess the cookie name.  They are unlikely to be contained anywhere
+    # within the unauthenticated debug page.
+    private_bits = [
+        str(uuid.getnode()),
+        get_machine_id(),
+    ]
+
+    h = hashlib.md5()
+    for bit in chain(probably_public_bits, private_bits):
+        if not bit:
+            continue
+        if isinstance(bit, text_type):
+            bit = bit.encode('utf-8')
+        h.update(bit)
+    h.update(b'cookiesalt')
+
+    cookie_name = '__wzd' + h.hexdigest()[:20]
+
+    # If we need to generate a pin we salt it a bit more so that we don't
+    # end up with the same value and generate out 9 digits
+    if num is None:
+        h.update(b'pinsalt')
+        num = ('%09d' % int(h.hexdigest(), 16))[:9]
+
+    # Format the pincode in groups of digits for easier remembering if
+    # we don't have a result yet.
+    if rv is None:
+        for group_size in 5, 4, 3:
+            if len(num) % group_size == 0:
+                rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
+                              for x in range(0, len(num), group_size))
+                break
+        else:
+            rv = num
+
+    return rv, cookie_name
+
+
 class DebuggedApplication(object):
     """Enables debugging support for a given application::
 
@@ -41,8 +205,8 @@
     The `evalex` keyword argument allows evaluating expressions in a
     traceback's frame context.
 
-    .. versionadded:: 0.7
-       The `lodgeit_url` parameter was added.
+    .. versionadded:: 0.9
+       The `lodgeit_url` parameter was deprecated.
 
     :param app: the WSGI application to run debugged.
     :param evalex: enable exception evaluation feature (interactive
@@ -57,19 +221,19 @@
     :param show_hidden_frames: by default hidden traceback frames are skipped.
                                You can show them by setting this parameter
                                to `True`.
-    :param lodgeit_url: the base URL of the LodgeIt instance to use for
-                        pasting tracebacks.
+    :param pin_security: can be used to disable the pin based security system.
+    :param pin_logging: enables the logging of the pin system.
     """
 
-    # this class is public
-    __module__ = 'werkzeug'
-
     def __init__(self, app, evalex=False, request_key='werkzeug.request',
                  console_path='/console', console_init_func=None,
-                 show_hidden_frames=False,
-                 lodgeit_url='http://paste.pocoo.org/'):
+                 show_hidden_frames=False, lodgeit_url=None,
+                 pin_security=True, pin_logging=True):
+        if lodgeit_url is not None:
+            from warnings import warn
+            warn(DeprecationWarning('Werkzeug now pastes into gists.'))
         if not console_init_func:
-            console_init_func = dict
+            console_init_func = None
         self.app = app
         self.evalex = evalex
         self.frames = {}
@@ -78,8 +242,40 @@
         self.console_path = console_path
         self.console_init_func = console_init_func
         self.show_hidden_frames = show_hidden_frames
-        self.lodgeit_url = lodgeit_url
         self.secret = gen_salt(20)
+        self._failed_pin_auth = 0
+
+        self.pin_logging = pin_logging
+        if pin_security:
+            # Print out the pin for the debugger on standard out.
+            if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' and \
+               pin_logging:
+                _log('warning', ' * Debugger is active!')
+                if self.pin is None:
+                    _log('warning', ' * Debugger pin disabled.  '
+                         'DEBUGGER UNSECURED!')
+                else:
+                    _log('info', ' * Debugger pin code: %s' % self.pin)
+        else:
+            self.pin = None
+
+    def _get_pin(self):
+        if not hasattr(self, '_pin'):
+            self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
+        return self._pin
+
+    def _set_pin(self, value):
+        self._pin = value
+
+    pin = property(_get_pin, _set_pin)
+    del _get_pin, _set_pin
+
+    @property
+    def pin_cookie_name(self):
+        """The name of the pin cookie."""
+        if not hasattr(self, '_pin_cookie'):
+            self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
+        return self._pin_cookie
 
     def debug_application(self, environ, start_response):
         """Run the application and conserve the traceback frames."""
@@ -93,16 +289,19 @@
         except Exception:
             if hasattr(app_iter, 'close'):
                 app_iter.close()
-            traceback = get_current_traceback(skip=1, show_hidden_frames=
-                                              self.show_hidden_frames,
-                                              ignore_system_exceptions=True)
+            traceback = get_current_traceback(
+                skip=1, show_hidden_frames=self.show_hidden_frames,
+                ignore_system_exceptions=True)
             for frame in traceback.frames:
                 self.frames[frame.id] = frame
             self.tracebacks[traceback.id] = traceback
 
             try:
                 start_response('500 INTERNAL SERVER ERROR', [
-                    ('Content-Type', 'text/html; charset=utf-8')
+                    ('Content-Type', 'text/html; charset=utf-8'),
+                    # Disable Chrome's XSS protection, the debug
+                    # output can cause false-positives.
+                    ('X-XSS-Protection', '0'),
                 ])
             except Exception:
                 # if we end up here there has been output but an error
@@ -114,10 +313,11 @@
                     'response at a point where response headers were already '
                     'sent.\n')
             else:
+                is_trusted = bool(self.check_pin_trust(environ))
                 yield traceback.render_full(evalex=self.evalex,
-                                            lodgeit_url=self.lodgeit_url,
+                                            evalex_trusted=is_trusted,
                                             secret=self.secret) \
-                               .encode('utf-8', 'replace')
+                    .encode('utf-8', 'replace')
 
             traceback.log(environ['wsgi.errors'])
 
@@ -128,20 +328,21 @@
     def display_console(self, request):
         """Display a standalone shell."""
         if 0 not in self.frames:
-            self.frames[0] = _ConsoleFrame(self.console_init_func())
-        return Response(render_console_html(secret=self.secret),
+            if self.console_init_func is None:
+                ns = {}
+            else:
+                ns = dict(self.console_init_func())
+            ns.setdefault('app', self.app)
+            self.frames[0] = _ConsoleFrame(ns)
+        is_trusted = bool(self.check_pin_trust(request.environ))
+        return Response(render_console_html(secret=self.secret,
+                                            evalex_trusted=is_trusted),
                         mimetype='text/html')
 
     def paste_traceback(self, request, traceback):
         """Paste the traceback and return a JSON response."""
-        paste_id = traceback.paste(self.lodgeit_url)
-        return Response('{"url": "%sshow/%s/", "id": "%s"}'
-                        % (self.lodgeit_url, paste_id, paste_id),
-                        mimetype='application/json')
-
-    def get_source(self, request, frame):
-        """Render the source viewer."""
-        return Response(frame.render_source(), mimetype='text/html')
+        rv = traceback.paste()
+        return Response(json.dumps(rv), mimetype='application/json')
 
     def get_resource(self, request, filename):
         """Return a static resource from the shared folder."""
@@ -149,13 +350,90 @@
         if isfile(filename):
             mimetype = mimetypes.guess_type(filename)[0] \
                 or 'application/octet-stream'
-            f = file(filename, 'rb')
+            f = open(filename, 'rb')
             try:
                 return Response(f.read(), mimetype=mimetype)
             finally:
                 f.close()
         return Response('Not Found', status=404)
 
+    def check_pin_trust(self, environ):
+        """Checks if the request passed the pin test.  This returns `True` if the
+        request is trusted on a pin/cookie basis and returns `False` if not.
+        Additionally if the cookie's stored pin hash is wrong it will return
+        `None` so that appropriate action can be taken.
+        """
+        if self.pin is None:
+            return True
+        val = parse_cookie(environ).get(self.pin_cookie_name)
+        if not val or '|' not in val:
+            return False
+        ts, pin_hash = val.split('|', 1)
+        if not ts.isdigit():
+            return False
+        if pin_hash != hash_pin(self.pin):
+            return None
+        return (time.time() - PIN_TIME) < int(ts)
+
+    def _fail_pin_auth(self):
+        time.sleep(self._failed_pin_auth > 5 and 5.0 or 0.5)
+        self._failed_pin_auth += 1
+
+    def pin_auth(self, request):
+        """Authenticates with the pin."""
+        exhausted = False
+        auth = False
+        trust = self.check_pin_trust(request.environ)
+
+        # If the trust return value is `None` it means that the cookie is
+        # set but the stored pin hash value is bad.  This means that the
+        # pin was changed.  In this case we count a bad auth and unset the
+        # cookie.  This way it becomes harder to guess the cookie name
+        # instead of the pin as we still count up failures.
+        bad_cookie = False
+        if trust is None:
+            self._fail_pin_auth()
+            bad_cookie = True
+
+        # If we're trusted, we're authenticated.
+        elif trust:
+            auth = True
+
+        # If we failed too many times, then we're locked out.
+        elif self._failed_pin_auth > 10:
+            exhausted = True
+
+        # Otherwise go through pin based authentication
+        else:
+            entered_pin = request.args.get('pin')
+            if entered_pin.strip().replace('-', '') == \
+               self.pin.replace('-', ''):
+                self._failed_pin_auth = 0
+                auth = True
+            else:
+                self._fail_pin_auth()
+
+        rv = Response(json.dumps({
+            'auth': auth,
+            'exhausted': exhausted,
+        }), mimetype='application/json')
+        if auth:
+            rv.set_cookie(self.pin_cookie_name, '%s|%s' % (
+                int(time.time()),
+                hash_pin(self.pin)
+            ), httponly=True)
+        elif bad_cookie:
+            rv.delete_cookie(self.pin_cookie_name)
+        return rv
+
+    def log_pin_request(self):
+        """Log the pin if needed."""
+        if self.pin_logging and self.pin is not None:
+            _log('info', ' * To enable the debugger you need to '
+                 'enter the security pin:')
+            _log('info', ' * Debugger pin code: %s' % self.pin)
+        return Response('')
+
     def __call__(self, environ, start_response):
         """Dispatch the requests."""
         # important: don't ever access a function here that reads the incoming
@@ -172,14 +450,17 @@
             if cmd == 'resource' and arg:
                 response = self.get_resource(request, arg)
             elif cmd == 'paste' and traceback is not None and \
-                 secret == self.secret:
+                    secret == self.secret:
                 response = self.paste_traceback(request, traceback)
-            elif cmd == 'source' and frame and self.secret == secret:
-                response = self.get_source(request, frame)
-            elif self.evalex and cmd is not None and frame is not None and \
-                 self.secret == secret:
+            elif cmd == 'pinauth' and secret == self.secret:
+                response = self.pin_auth(request)
+            elif cmd == 'printpin' and secret == self.secret:
+                response = self.log_pin_request()
+            elif self.evalex and cmd is not None and frame is not None \
+                    and self.secret == secret and \
+                    self.check_pin_trust(environ):
                 response = self.execute_command(request, cmd, frame)
         elif self.evalex and self.console_path is not None and \
-           request.path == self.console_path:
+                request.path == self.console_path:
             response = self.display_console(request)
         return response(environ, start_response)