comparison 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
comparison
equal deleted inserted replaced
6089:dfbc455e2c46 6094:9f12f41504fc
3 werkzeug.debug 3 werkzeug.debug
4 ~~~~~~~~~~~~~~ 4 ~~~~~~~~~~~~~~
5 5
6 WSGI application traceback debugger. 6 WSGI application traceback debugger.
7 7
8 :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. 8 :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
9 :license: BSD, see LICENSE for more details. 9 :license: BSD, see LICENSE for more details.
10 """ 10 """
11 import os
12 import re
13 import sys
14 import uuid
15 import json
16 import time
17 import getpass
18 import hashlib
11 import mimetypes 19 import mimetypes
20 from itertools import chain
12 from os.path import join, dirname, basename, isfile 21 from os.path import join, dirname, basename, isfile
13 from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response 22 from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response
23 from werkzeug.http import parse_cookie
14 from werkzeug.debug.tbtools import get_current_traceback, render_console_html 24 from werkzeug.debug.tbtools import get_current_traceback, render_console_html
15 from werkzeug.debug.console import Console 25 from werkzeug.debug.console import Console
16 from werkzeug.security import gen_salt 26 from werkzeug.security import gen_salt
17 27 from werkzeug._internal import _log
18 28 from werkzeug._compat import text_type
29
30
31 # DEPRECATED
19 #: import this here because it once was documented as being available 32 #: import this here because it once was documented as being available
20 #: from this module. In case there are users left ... 33 #: from this module. In case there are users left ...
21 from werkzeug.debug.repr import debug_repr 34 from werkzeug.debug.repr import debug_repr # noqa
35
36
37 # A week
38 PIN_TIME = 60 * 60 * 24 * 7
39
40
41 def hash_pin(pin):
42 if isinstance(pin, text_type):
43 pin = pin.encode('utf-8', 'replace')
44 return hashlib.md5(pin + b'shittysalt').hexdigest()[:12]
45
46
47 _machine_id = None
48
49
50 def get_machine_id():
51 global _machine_id
52 rv = _machine_id
53 if rv is not None:
54 return rv
55
56 def _generate():
57 # Potential sources of secret information on linux. The machine-id
58 # is stable across boots, the boot id is not
59 for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
60 try:
61 with open(filename, 'rb') as f:
62 return f.readline().strip()
63 except IOError:
64 continue
65
66 # On OS X we can use the computer's serial number assuming that
67 # ioreg exists and can spit out that information.
68 try:
69 # Also catch import errors: subprocess may not be available, e.g.
70 # Google App Engine
71 # See https://github.com/pallets/werkzeug/issues/925
72 from subprocess import Popen, PIPE
73 dump = Popen(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2'],
74 stdout=PIPE).communicate()[0]
75 match = re.search(b'"serial-number" = <([^>]+)', dump)
76 if match is not None:
77 return match.group(1)
78 except (OSError, ImportError):
79 pass
80
81 # On Windows we can use winreg to get the machine guid
82 wr = None
83 try:
84 import winreg as wr
85 except ImportError:
86 try:
87 import _winreg as wr
88 except ImportError:
89 pass
90 if wr is not None:
91 try:
92 with wr.OpenKey(wr.HKEY_LOCAL_MACHINE,
93 'SOFTWARE\\Microsoft\\Cryptography', 0,
94 wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk:
95 return wr.QueryValueEx(rk, 'MachineGuid')[0]
96 except WindowsError:
97 pass
98
99 _machine_id = rv = _generate()
100 return rv
22 101
23 102
24 class _ConsoleFrame(object): 103 class _ConsoleFrame(object):
104
25 """Helper class so that we can reuse the frame console code for the 105 """Helper class so that we can reuse the frame console code for the
26 standalone console. 106 standalone console.
27 """ 107 """
28 108
29 def __init__(self, namespace): 109 def __init__(self, namespace):
30 self.console = Console(namespace) 110 self.console = Console(namespace)
31 self.id = 0 111 self.id = 0
32 112
33 113
114 def get_pin_and_cookie_name(app):
115 """Given an application object this returns a semi-stable 9 digit pin
116 code and a random key. The hope is that this is stable between
117 restarts to not make debugging particularly frustrating. If the pin
118 was forcefully disabled this returns `None`.
119
120 Second item in the resulting tuple is the cookie name for remembering.
121 """
122 pin = os.environ.get('WERKZEUG_DEBUG_PIN')
123 rv = None
124 num = None
125
126 # Pin was explicitly disabled
127 if pin == 'off':
128 return None, None
129
130 # Pin was provided explicitly
131 if pin is not None and pin.replace('-', '').isdigit():
132 # If there are separators in the pin, return it directly
133 if '-' in pin:
134 rv = pin
135 else:
136 num = pin
137
138 modname = getattr(app, '__module__',
139 getattr(app.__class__, '__module__'))
140
141 try:
142 # `getpass.getuser()` imports the `pwd` module,
143 # which does not exist in the Google App Engine sandbox.
144 username = getpass.getuser()
145 except ImportError:
146 username = None
147
148 mod = sys.modules.get(modname)
149
150 # This information only exists to make the cookie unique on the
151 # computer, not as a security feature.
152 probably_public_bits = [
153 username,
154 modname,
155 getattr(app, '__name__', getattr(app.__class__, '__name__')),
156 getattr(mod, '__file__', None),
157 ]
158
159 # This information is here to make it harder for an attacker to
160 # guess the cookie name. They are unlikely to be contained anywhere
161 # within the unauthenticated debug page.
162 private_bits = [
163 str(uuid.getnode()),
164 get_machine_id(),
165 ]
166
167 h = hashlib.md5()
168 for bit in chain(probably_public_bits, private_bits):
169 if not bit:
170 continue
171 if isinstance(bit, text_type):
172 bit = bit.encode('utf-8')
173 h.update(bit)
174 h.update(b'cookiesalt')
175
176 cookie_name = '__wzd' + h.hexdigest()[:20]
177
178 # If we need to generate a pin we salt it a bit more so that we don't
179 # end up with the same value and generate out 9 digits
180 if num is None:
181 h.update(b'pinsalt')
182 num = ('%09d' % int(h.hexdigest(), 16))[:9]
183
184 # Format the pincode in groups of digits for easier remembering if
185 # we don't have a result yet.
186 if rv is None:
187 for group_size in 5, 4, 3:
188 if len(num) % group_size == 0:
189 rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
190 for x in range(0, len(num), group_size))
191 break
192 else:
193 rv = num
194
195 return rv, cookie_name
196
197
34 class DebuggedApplication(object): 198 class DebuggedApplication(object):
35 """Enables debugging support for a given application:: 199 """Enables debugging support for a given application::
36 200
37 from werkzeug.debug import DebuggedApplication 201 from werkzeug.debug import DebuggedApplication
38 from myapp import app 202 from myapp import app
39 app = DebuggedApplication(app, evalex=True) 203 app = DebuggedApplication(app, evalex=True)
40 204
41 The `evalex` keyword argument allows evaluating expressions in a 205 The `evalex` keyword argument allows evaluating expressions in a
42 traceback's frame context. 206 traceback's frame context.
43 207
44 .. versionadded:: 0.7 208 .. versionadded:: 0.9
45 The `lodgeit_url` parameter was added. 209 The `lodgeit_url` parameter was deprecated.
46 210
47 :param app: the WSGI application to run debugged. 211 :param app: the WSGI application to run debugged.
48 :param evalex: enable exception evaluation feature (interactive 212 :param evalex: enable exception evaluation feature (interactive
49 debugging). This requires a non-forking server. 213 debugging). This requires a non-forking server.
50 :param request_key: The key that points to the request object in ths 214 :param request_key: The key that points to the request object in ths
55 the general purpose console. The return value 219 the general purpose console. The return value
56 is used as initial namespace. 220 is used as initial namespace.
57 :param show_hidden_frames: by default hidden traceback frames are skipped. 221 :param show_hidden_frames: by default hidden traceback frames are skipped.
58 You can show them by setting this parameter 222 You can show them by setting this parameter
59 to `True`. 223 to `True`.
60 :param lodgeit_url: the base URL of the LodgeIt instance to use for 224 :param pin_security: can be used to disable the pin based security system.
61 pasting tracebacks. 225 :param pin_logging: enables the logging of the pin system.
62 """ 226 """
63
64 # this class is public
65 __module__ = 'werkzeug'
66 227
67 def __init__(self, app, evalex=False, request_key='werkzeug.request', 228 def __init__(self, app, evalex=False, request_key='werkzeug.request',
68 console_path='/console', console_init_func=None, 229 console_path='/console', console_init_func=None,
69 show_hidden_frames=False, 230 show_hidden_frames=False, lodgeit_url=None,
70 lodgeit_url='http://paste.pocoo.org/'): 231 pin_security=True, pin_logging=True):
232 if lodgeit_url is not None:
233 from warnings import warn
234 warn(DeprecationWarning('Werkzeug now pastes into gists.'))
71 if not console_init_func: 235 if not console_init_func:
72 console_init_func = dict 236 console_init_func = None
73 self.app = app 237 self.app = app
74 self.evalex = evalex 238 self.evalex = evalex
75 self.frames = {} 239 self.frames = {}
76 self.tracebacks = {} 240 self.tracebacks = {}
77 self.request_key = request_key 241 self.request_key = request_key
78 self.console_path = console_path 242 self.console_path = console_path
79 self.console_init_func = console_init_func 243 self.console_init_func = console_init_func
80 self.show_hidden_frames = show_hidden_frames 244 self.show_hidden_frames = show_hidden_frames
81 self.lodgeit_url = lodgeit_url
82 self.secret = gen_salt(20) 245 self.secret = gen_salt(20)
246 self._failed_pin_auth = 0
247
248 self.pin_logging = pin_logging
249 if pin_security:
250 # Print out the pin for the debugger on standard out.
251 if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' and \
252 pin_logging:
253 _log('warning', ' * Debugger is active!')
254 if self.pin is None:
255 _log('warning', ' * Debugger pin disabled. '
256 'DEBUGGER UNSECURED!')
257 else:
258 _log('info', ' * Debugger pin code: %s' % self.pin)
259 else:
260 self.pin = None
261
262 def _get_pin(self):
263 if not hasattr(self, '_pin'):
264 self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
265 return self._pin
266
267 def _set_pin(self, value):
268 self._pin = value
269
270 pin = property(_get_pin, _set_pin)
271 del _get_pin, _set_pin
272
273 @property
274 def pin_cookie_name(self):
275 """The name of the pin cookie."""
276 if not hasattr(self, '_pin_cookie'):
277 self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
278 return self._pin_cookie
83 279
84 def debug_application(self, environ, start_response): 280 def debug_application(self, environ, start_response):
85 """Run the application and conserve the traceback frames.""" 281 """Run the application and conserve the traceback frames."""
86 app_iter = None 282 app_iter = None
87 try: 283 try:
91 if hasattr(app_iter, 'close'): 287 if hasattr(app_iter, 'close'):
92 app_iter.close() 288 app_iter.close()
93 except Exception: 289 except Exception:
94 if hasattr(app_iter, 'close'): 290 if hasattr(app_iter, 'close'):
95 app_iter.close() 291 app_iter.close()
96 traceback = get_current_traceback(skip=1, show_hidden_frames= 292 traceback = get_current_traceback(
97 self.show_hidden_frames, 293 skip=1, show_hidden_frames=self.show_hidden_frames,
98 ignore_system_exceptions=True) 294 ignore_system_exceptions=True)
99 for frame in traceback.frames: 295 for frame in traceback.frames:
100 self.frames[frame.id] = frame 296 self.frames[frame.id] = frame
101 self.tracebacks[traceback.id] = traceback 297 self.tracebacks[traceback.id] = traceback
102 298
103 try: 299 try:
104 start_response('500 INTERNAL SERVER ERROR', [ 300 start_response('500 INTERNAL SERVER ERROR', [
105 ('Content-Type', 'text/html; charset=utf-8') 301 ('Content-Type', 'text/html; charset=utf-8'),
302 # Disable Chrome's XSS protection, the debug
303 # output can cause false-positives.
304 ('X-XSS-Protection', '0'),
106 ]) 305 ])
107 except Exception: 306 except Exception:
108 # if we end up here there has been output but an error 307 # if we end up here there has been output but an error
109 # occurred. in that situation we can do nothing fancy any 308 # occurred. in that situation we can do nothing fancy any
110 # more, better log something into the error log and fall 309 # more, better log something into the error log and fall
112 environ['wsgi.errors'].write( 311 environ['wsgi.errors'].write(
113 'Debugging middleware caught exception in streamed ' 312 'Debugging middleware caught exception in streamed '
114 'response at a point where response headers were already ' 313 'response at a point where response headers were already '
115 'sent.\n') 314 'sent.\n')
116 else: 315 else:
316 is_trusted = bool(self.check_pin_trust(environ))
117 yield traceback.render_full(evalex=self.evalex, 317 yield traceback.render_full(evalex=self.evalex,
118 lodgeit_url=self.lodgeit_url, 318 evalex_trusted=is_trusted,
119 secret=self.secret) \ 319 secret=self.secret) \
120 .encode('utf-8', 'replace') 320 .encode('utf-8', 'replace')
121 321
122 traceback.log(environ['wsgi.errors']) 322 traceback.log(environ['wsgi.errors'])
123 323
124 def execute_command(self, request, command, frame): 324 def execute_command(self, request, command, frame):
125 """Execute a command in a console.""" 325 """Execute a command in a console."""
126 return Response(frame.console.eval(command), mimetype='text/html') 326 return Response(frame.console.eval(command), mimetype='text/html')
127 327
128 def display_console(self, request): 328 def display_console(self, request):
129 """Display a standalone shell.""" 329 """Display a standalone shell."""
130 if 0 not in self.frames: 330 if 0 not in self.frames:
131 self.frames[0] = _ConsoleFrame(self.console_init_func()) 331 if self.console_init_func is None:
132 return Response(render_console_html(secret=self.secret), 332 ns = {}
333 else:
334 ns = dict(self.console_init_func())
335 ns.setdefault('app', self.app)
336 self.frames[0] = _ConsoleFrame(ns)
337 is_trusted = bool(self.check_pin_trust(request.environ))
338 return Response(render_console_html(secret=self.secret,
339 evalex_trusted=is_trusted),
133 mimetype='text/html') 340 mimetype='text/html')
134 341
135 def paste_traceback(self, request, traceback): 342 def paste_traceback(self, request, traceback):
136 """Paste the traceback and return a JSON response.""" 343 """Paste the traceback and return a JSON response."""
137 paste_id = traceback.paste(self.lodgeit_url) 344 rv = traceback.paste()
138 return Response('{"url": "%sshow/%s/", "id": "%s"}' 345 return Response(json.dumps(rv), mimetype='application/json')
139 % (self.lodgeit_url, paste_id, paste_id),
140 mimetype='application/json')
141
142 def get_source(self, request, frame):
143 """Render the source viewer."""
144 return Response(frame.render_source(), mimetype='text/html')
145 346
146 def get_resource(self, request, filename): 347 def get_resource(self, request, filename):
147 """Return a static resource from the shared folder.""" 348 """Return a static resource from the shared folder."""
148 filename = join(dirname(__file__), 'shared', basename(filename)) 349 filename = join(dirname(__file__), 'shared', basename(filename))
149 if isfile(filename): 350 if isfile(filename):
150 mimetype = mimetypes.guess_type(filename)[0] \ 351 mimetype = mimetypes.guess_type(filename)[0] \
151 or 'application/octet-stream' 352 or 'application/octet-stream'
152 f = file(filename, 'rb') 353 f = open(filename, 'rb')
153 try: 354 try:
154 return Response(f.read(), mimetype=mimetype) 355 return Response(f.read(), mimetype=mimetype)
155 finally: 356 finally:
156 f.close() 357 f.close()
157 return Response('Not Found', status=404) 358 return Response('Not Found', status=404)
359
360 def check_pin_trust(self, environ):
361 """Checks if the request passed the pin test. This returns `True` if the
362 request is trusted on a pin/cookie basis and returns `False` if not.
363 Additionally if the cookie's stored pin hash is wrong it will return
364 `None` so that appropriate action can be taken.
365 """
366 if self.pin is None:
367 return True
368 val = parse_cookie(environ).get(self.pin_cookie_name)
369 if not val or '|' not in val:
370 return False
371 ts, pin_hash = val.split('|', 1)
372 if not ts.isdigit():
373 return False
374 if pin_hash != hash_pin(self.pin):
375 return None
376 return (time.time() - PIN_TIME) < int(ts)
377
378 def _fail_pin_auth(self):
379 time.sleep(self._failed_pin_auth > 5 and 5.0 or 0.5)
380 self._failed_pin_auth += 1
381
382 def pin_auth(self, request):
383 """Authenticates with the pin."""
384 exhausted = False
385 auth = False
386 trust = self.check_pin_trust(request.environ)
387
388 # If the trust return value is `None` it means that the cookie is
389 # set but the stored pin hash value is bad. This means that the
390 # pin was changed. In this case we count a bad auth and unset the
391 # cookie. This way it becomes harder to guess the cookie name
392 # instead of the pin as we still count up failures.
393 bad_cookie = False
394 if trust is None:
395 self._fail_pin_auth()
396 bad_cookie = True
397
398 # If we're trusted, we're authenticated.
399 elif trust:
400 auth = True
401
402 # If we failed too many times, then we're locked out.
403 elif self._failed_pin_auth > 10:
404 exhausted = True
405
406 # Otherwise go through pin based authentication
407 else:
408 entered_pin = request.args.get('pin')
409 if entered_pin.strip().replace('-', '') == \
410 self.pin.replace('-', ''):
411 self._failed_pin_auth = 0
412 auth = True
413 else:
414 self._fail_pin_auth()
415
416 rv = Response(json.dumps({
417 'auth': auth,
418 'exhausted': exhausted,
419 }), mimetype='application/json')
420 if auth:
421 rv.set_cookie(self.pin_cookie_name, '%s|%s' % (
422 int(time.time()),
423 hash_pin(self.pin)
424 ), httponly=True)
425 elif bad_cookie:
426 rv.delete_cookie(self.pin_cookie_name)
427 return rv
428
429 def log_pin_request(self):
430 """Log the pin if needed."""
431 if self.pin_logging and self.pin is not None:
432 _log('info', ' * To enable the debugger you need to '
433 'enter the security pin:')
434 _log('info', ' * Debugger pin code: %s' % self.pin)
435 return Response('')
158 436
159 def __call__(self, environ, start_response): 437 def __call__(self, environ, start_response):
160 """Dispatch the requests.""" 438 """Dispatch the requests."""
161 # important: don't ever access a function here that reads the incoming 439 # important: don't ever access a function here that reads the incoming
162 # form data! Otherwise the application won't have access to that data 440 # form data! Otherwise the application won't have access to that data
170 traceback = self.tracebacks.get(request.args.get('tb', type=int)) 448 traceback = self.tracebacks.get(request.args.get('tb', type=int))
171 frame = self.frames.get(request.args.get('frm', type=int)) 449 frame = self.frames.get(request.args.get('frm', type=int))
172 if cmd == 'resource' and arg: 450 if cmd == 'resource' and arg:
173 response = self.get_resource(request, arg) 451 response = self.get_resource(request, arg)
174 elif cmd == 'paste' and traceback is not None and \ 452 elif cmd == 'paste' and traceback is not None and \
175 secret == self.secret: 453 secret == self.secret:
176 response = self.paste_traceback(request, traceback) 454 response = self.paste_traceback(request, traceback)
177 elif cmd == 'source' and frame and self.secret == secret: 455 elif cmd == 'pinauth' and secret == self.secret:
178 response = self.get_source(request, frame) 456 response = self.pin_auth(request)
179 elif self.evalex and cmd is not None and frame is not None and \ 457 elif cmd == 'printpin' and secret == self.secret:
180 self.secret == secret: 458 response = self.log_pin_request()
459 elif self.evalex and cmd is not None and frame is not None \
460 and self.secret == secret and \
461 self.check_pin_trust(environ):
181 response = self.execute_command(request, cmd, frame) 462 response = self.execute_command(request, cmd, frame)
182 elif self.evalex and self.console_path is not None and \ 463 elif self.evalex and self.console_path is not None and \
183 request.path == self.console_path: 464 request.path == self.console_path:
184 response = self.display_console(request) 465 response = self.display_console(request)
185 return response(environ, start_response) 466 return response(environ, start_response)