__init__.py 17 KB

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