__init__.py 18 KB

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