debug.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import functools
  2. import re
  3. import sys
  4. import types
  5. from contextlib import suppress
  6. from pathlib import Path
  7. from django.conf import settings
  8. from django.http import HttpResponse, HttpResponseNotFound
  9. from django.template import Context, Engine, TemplateDoesNotExist
  10. from django.template.defaultfilters import force_escape, pprint
  11. from django.urls import Resolver404, resolve
  12. from django.utils import timezone
  13. from django.utils.datastructures import MultiValueDict
  14. from django.utils.encoding import force_text
  15. from django.utils.module_loading import import_string
  16. from django.utils.version import get_docs_version
  17. # Minimal Django templates engine to render the error templates
  18. # regardless of the project's TEMPLATES setting. Templates are
  19. # read directly from the filesystem so that the error handler
  20. # works even if the template loader is broken.
  21. DEBUG_ENGINE = Engine(
  22. debug=True,
  23. libraries={'i18n': 'django.templatetags.i18n'},
  24. )
  25. HIDDEN_SETTINGS = re.compile('API|TOKEN|KEY|SECRET|PASS|SIGNATURE', flags=re.IGNORECASE)
  26. CLEANSED_SUBSTITUTE = '********************'
  27. CURRENT_DIR = Path(__file__).parent
  28. class CallableSettingWrapper:
  29. """
  30. Object to wrap callable appearing in settings.
  31. * Not to call in the debug page (#21345).
  32. * Not to break the debug page if the callable forbidding to set attributes
  33. (#23070).
  34. """
  35. def __init__(self, callable_setting):
  36. self._wrapped = callable_setting
  37. def __repr__(self):
  38. return repr(self._wrapped)
  39. def cleanse_setting(key, value):
  40. """
  41. Cleanse an individual setting key/value of sensitive content. If the value
  42. is a dictionary, recursively cleanse the keys in that dictionary.
  43. """
  44. try:
  45. if HIDDEN_SETTINGS.search(key):
  46. cleansed = CLEANSED_SUBSTITUTE
  47. else:
  48. if isinstance(value, dict):
  49. cleansed = {k: cleanse_setting(k, v) for k, v in value.items()}
  50. else:
  51. cleansed = value
  52. except TypeError:
  53. # If the key isn't regex-able, just return as-is.
  54. cleansed = value
  55. if callable(cleansed):
  56. # For fixing #21345 and #23070
  57. cleansed = CallableSettingWrapper(cleansed)
  58. return cleansed
  59. def get_safe_settings():
  60. """
  61. Return a dictionary of the settings module with values of sensitive
  62. settings replaced with stars (*********).
  63. """
  64. settings_dict = {}
  65. for k in dir(settings):
  66. if k.isupper():
  67. settings_dict[k] = cleanse_setting(k, getattr(settings, k))
  68. return settings_dict
  69. def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
  70. """
  71. Create a technical server error response. The last three arguments are
  72. the values returned from sys.exc_info() and friends.
  73. """
  74. reporter = ExceptionReporter(request, exc_type, exc_value, tb)
  75. if request.is_ajax():
  76. text = reporter.get_traceback_text()
  77. return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8')
  78. else:
  79. html = reporter.get_traceback_html()
  80. return HttpResponse(html, status=status_code, content_type='text/html')
  81. @functools.lru_cache()
  82. def get_default_exception_reporter_filter():
  83. # Instantiate the default filter for the first time and cache it.
  84. return import_string(settings.DEFAULT_EXCEPTION_REPORTER_FILTER)()
  85. def get_exception_reporter_filter(request):
  86. default_filter = get_default_exception_reporter_filter()
  87. return getattr(request, 'exception_reporter_filter', default_filter)
  88. class ExceptionReporterFilter:
  89. """
  90. Base for all exception reporter filter classes. All overridable hooks
  91. contain lenient default behaviors.
  92. """
  93. def get_post_parameters(self, request):
  94. if request is None:
  95. return {}
  96. else:
  97. return request.POST
  98. def get_traceback_frame_variables(self, request, tb_frame):
  99. return list(tb_frame.f_locals.items())
  100. class SafeExceptionReporterFilter(ExceptionReporterFilter):
  101. """
  102. Use annotations made by the sensitive_post_parameters and
  103. sensitive_variables decorators to filter out sensitive information.
  104. """
  105. def is_active(self, request):
  106. """
  107. This filter is to add safety in production environments (i.e. DEBUG
  108. is False). If DEBUG is True then your site is not safe anyway.
  109. This hook is provided as a convenience to easily activate or
  110. deactivate the filter on a per request basis.
  111. """
  112. return settings.DEBUG is False
  113. def get_cleansed_multivaluedict(self, request, multivaluedict):
  114. """
  115. Replace the keys in a MultiValueDict marked as sensitive with stars.
  116. This mitigates leaking sensitive POST parameters if something like
  117. request.POST['nonexistent_key'] throws an exception (#21098).
  118. """
  119. sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
  120. if self.is_active(request) and sensitive_post_parameters:
  121. multivaluedict = multivaluedict.copy()
  122. for param in sensitive_post_parameters:
  123. if param in multivaluedict:
  124. multivaluedict[param] = CLEANSED_SUBSTITUTE
  125. return multivaluedict
  126. def get_post_parameters(self, request):
  127. """
  128. Replace the values of POST parameters marked as sensitive with
  129. stars (*********).
  130. """
  131. if request is None:
  132. return {}
  133. else:
  134. sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
  135. if self.is_active(request) and sensitive_post_parameters:
  136. cleansed = request.POST.copy()
  137. if sensitive_post_parameters == '__ALL__':
  138. # Cleanse all parameters.
  139. for k, v in cleansed.items():
  140. cleansed[k] = CLEANSED_SUBSTITUTE
  141. return cleansed
  142. else:
  143. # Cleanse only the specified parameters.
  144. for param in sensitive_post_parameters:
  145. if param in cleansed:
  146. cleansed[param] = CLEANSED_SUBSTITUTE
  147. return cleansed
  148. else:
  149. return request.POST
  150. def cleanse_special_types(self, request, value):
  151. try:
  152. # If value is lazy or a complex object of another kind, this check
  153. # might raise an exception. isinstance checks that lazy
  154. # MultiValueDicts will have a return value.
  155. is_multivalue_dict = isinstance(value, MultiValueDict)
  156. except Exception as e:
  157. return '{!r} while evaluating {!r}'.format(e, value)
  158. if is_multivalue_dict:
  159. # Cleanse MultiValueDicts (request.POST is the one we usually care about)
  160. value = self.get_cleansed_multivaluedict(request, value)
  161. return value
  162. def get_traceback_frame_variables(self, request, tb_frame):
  163. """
  164. Replace the values of variables marked as sensitive with
  165. stars (*********).
  166. """
  167. # Loop through the frame's callers to see if the sensitive_variables
  168. # decorator was used.
  169. current_frame = tb_frame.f_back
  170. sensitive_variables = None
  171. while current_frame is not None:
  172. if (current_frame.f_code.co_name == 'sensitive_variables_wrapper' and
  173. 'sensitive_variables_wrapper' in current_frame.f_locals):
  174. # The sensitive_variables decorator was used, so we take note
  175. # of the sensitive variables' names.
  176. wrapper = current_frame.f_locals['sensitive_variables_wrapper']
  177. sensitive_variables = getattr(wrapper, 'sensitive_variables', None)
  178. break
  179. current_frame = current_frame.f_back
  180. cleansed = {}
  181. if self.is_active(request) and sensitive_variables:
  182. if sensitive_variables == '__ALL__':
  183. # Cleanse all variables
  184. for name, value in tb_frame.f_locals.items():
  185. cleansed[name] = CLEANSED_SUBSTITUTE
  186. else:
  187. # Cleanse specified variables
  188. for name, value in tb_frame.f_locals.items():
  189. if name in sensitive_variables:
  190. value = CLEANSED_SUBSTITUTE
  191. else:
  192. value = self.cleanse_special_types(request, value)
  193. cleansed[name] = value
  194. else:
  195. # Potentially cleanse the request and any MultiValueDicts if they
  196. # are one of the frame variables.
  197. for name, value in tb_frame.f_locals.items():
  198. cleansed[name] = self.cleanse_special_types(request, value)
  199. if (tb_frame.f_code.co_name == 'sensitive_variables_wrapper' and
  200. 'sensitive_variables_wrapper' in tb_frame.f_locals):
  201. # For good measure, obfuscate the decorated function's arguments in
  202. # the sensitive_variables decorator's frame, in case the variables
  203. # associated with those arguments were meant to be obfuscated from
  204. # the decorated function's frame.
  205. cleansed['func_args'] = CLEANSED_SUBSTITUTE
  206. cleansed['func_kwargs'] = CLEANSED_SUBSTITUTE
  207. return cleansed.items()
  208. class ExceptionReporter:
  209. """Organize and coordinate reporting on exceptions."""
  210. def __init__(self, request, exc_type, exc_value, tb, is_email=False):
  211. self.request = request
  212. self.filter = get_exception_reporter_filter(self.request)
  213. self.exc_type = exc_type
  214. self.exc_value = exc_value
  215. self.tb = tb
  216. self.is_email = is_email
  217. self.template_info = getattr(self.exc_value, 'template_debug', None)
  218. self.template_does_not_exist = False
  219. self.postmortem = None
  220. def get_traceback_data(self):
  221. """Return a dictionary containing traceback information."""
  222. if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
  223. self.template_does_not_exist = True
  224. self.postmortem = self.exc_value.chain or [self.exc_value]
  225. frames = self.get_traceback_frames()
  226. for i, frame in enumerate(frames):
  227. if 'vars' in frame:
  228. frame_vars = []
  229. for k, v in frame['vars']:
  230. v = pprint(v)
  231. # Trim large blobs of data
  232. if len(v) > 4096:
  233. v = '%s... <trimmed %d bytes string>' % (v[0:4096], len(v))
  234. frame_vars.append((k, force_escape(v)))
  235. frame['vars'] = frame_vars
  236. frames[i] = frame
  237. unicode_hint = ''
  238. if self.exc_type and issubclass(self.exc_type, UnicodeError):
  239. start = getattr(self.exc_value, 'start', None)
  240. end = getattr(self.exc_value, 'end', None)
  241. if start is not None and end is not None:
  242. unicode_str = self.exc_value.args[1]
  243. unicode_hint = force_text(
  244. unicode_str[max(start - 5, 0):min(end + 5, len(unicode_str))],
  245. 'ascii', errors='replace'
  246. )
  247. from django import get_version
  248. if self.request is None:
  249. user_str = None
  250. else:
  251. try:
  252. user_str = str(self.request.user)
  253. except Exception:
  254. # request.user may raise OperationalError if the database is
  255. # unavailable, for example.
  256. user_str = '[unable to retrieve the current user]'
  257. c = {
  258. 'is_email': self.is_email,
  259. 'unicode_hint': unicode_hint,
  260. 'frames': frames,
  261. 'request': self.request,
  262. 'user_str': user_str,
  263. 'filtered_POST_items': list(self.filter.get_post_parameters(self.request).items()),
  264. 'settings': get_safe_settings(),
  265. 'sys_executable': sys.executable,
  266. 'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
  267. 'server_time': timezone.now(),
  268. 'django_version_info': get_version(),
  269. 'sys_path': sys.path,
  270. 'template_info': self.template_info,
  271. 'template_does_not_exist': self.template_does_not_exist,
  272. 'postmortem': self.postmortem,
  273. }
  274. if self.request is not None:
  275. c['request_GET_items'] = self.request.GET.items()
  276. c['request_FILES_items'] = self.request.FILES.items()
  277. c['request_COOKIES_items'] = self.request.COOKIES.items()
  278. # Check whether exception info is available
  279. if self.exc_type:
  280. c['exception_type'] = self.exc_type.__name__
  281. if self.exc_value:
  282. c['exception_value'] = str(self.exc_value)
  283. if frames:
  284. c['lastframe'] = frames[-1]
  285. return c
  286. def get_traceback_html(self):
  287. """Return HTML version of debug 500 HTTP error page."""
  288. with Path(CURRENT_DIR, 'templates', 'technical_500.html').open() as fh:
  289. t = DEBUG_ENGINE.from_string(fh.read())
  290. c = Context(self.get_traceback_data(), use_l10n=False)
  291. return t.render(c)
  292. def get_traceback_text(self):
  293. """Return plain text version of debug 500 HTTP error page."""
  294. with Path(CURRENT_DIR, 'templates', 'technical_500.txt').open() as fh:
  295. t = DEBUG_ENGINE.from_string(fh.read())
  296. c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
  297. return t.render(c)
  298. def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None):
  299. """
  300. Return context_lines before and after lineno from file.
  301. Return (pre_context_lineno, pre_context, context_line, post_context).
  302. """
  303. source = None
  304. if loader is not None and hasattr(loader, "get_source"):
  305. with suppress(ImportError):
  306. source = loader.get_source(module_name)
  307. if source is not None:
  308. source = source.splitlines()
  309. if source is None:
  310. with suppress(OSError, IOError):
  311. with open(filename, 'rb') as fp:
  312. source = fp.read().splitlines()
  313. if source is None:
  314. return None, [], None, []
  315. # If we just read the source from a file, or if the loader did not
  316. # apply tokenize.detect_encoding to decode the source into a
  317. # string, then we should do that ourselves.
  318. if isinstance(source[0], bytes):
  319. encoding = 'ascii'
  320. for line in source[:2]:
  321. # File coding may be specified. Match pattern from PEP-263
  322. # (http://www.python.org/dev/peps/pep-0263/)
  323. match = re.search(br'coding[:=]\s*([-\w.]+)', line)
  324. if match:
  325. encoding = match.group(1).decode('ascii')
  326. break
  327. source = [str(sline, encoding, 'replace') for sline in source]
  328. lower_bound = max(0, lineno - context_lines)
  329. upper_bound = lineno + context_lines
  330. pre_context = source[lower_bound:lineno]
  331. context_line = source[lineno]
  332. post_context = source[lineno + 1:upper_bound]
  333. return lower_bound, pre_context, context_line, post_context
  334. def get_traceback_frames(self):
  335. def explicit_or_implicit_cause(exc_value):
  336. explicit = getattr(exc_value, '__cause__', None)
  337. implicit = getattr(exc_value, '__context__', None)
  338. return explicit or implicit
  339. # Get the exception and all its causes
  340. exceptions = []
  341. exc_value = self.exc_value
  342. while exc_value:
  343. exceptions.append(exc_value)
  344. exc_value = explicit_or_implicit_cause(exc_value)
  345. frames = []
  346. # No exceptions were supplied to ExceptionReporter
  347. if not exceptions:
  348. return frames
  349. # In case there's just one exception, take the traceback from self.tb
  350. exc_value = exceptions.pop()
  351. tb = self.tb if not exceptions else exc_value.__traceback__
  352. while tb is not None:
  353. # Support for __traceback_hide__ which is used by a few libraries
  354. # to hide internal frames.
  355. if tb.tb_frame.f_locals.get('__traceback_hide__'):
  356. tb = tb.tb_next
  357. continue
  358. filename = tb.tb_frame.f_code.co_filename
  359. function = tb.tb_frame.f_code.co_name
  360. lineno = tb.tb_lineno - 1
  361. loader = tb.tb_frame.f_globals.get('__loader__')
  362. module_name = tb.tb_frame.f_globals.get('__name__') or ''
  363. pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(
  364. filename, lineno, 7, loader, module_name,
  365. )
  366. if pre_context_lineno is not None:
  367. frames.append({
  368. 'exc_cause': explicit_or_implicit_cause(exc_value),
  369. 'exc_cause_explicit': getattr(exc_value, '__cause__', True),
  370. 'tb': tb,
  371. 'type': 'django' if module_name.startswith('django.') else 'user',
  372. 'filename': filename,
  373. 'function': function,
  374. 'lineno': lineno + 1,
  375. 'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
  376. 'id': id(tb),
  377. 'pre_context': pre_context,
  378. 'context_line': context_line,
  379. 'post_context': post_context,
  380. 'pre_context_lineno': pre_context_lineno + 1,
  381. })
  382. # If the traceback for current exception is consumed, try the
  383. # other exception.
  384. if not tb.tb_next and exceptions:
  385. exc_value = exceptions.pop()
  386. tb = exc_value.__traceback__
  387. else:
  388. tb = tb.tb_next
  389. return frames
  390. def technical_404_response(request, exception):
  391. """Create a technical 404 error response. `exception` is the Http404."""
  392. try:
  393. error_url = exception.args[0]['path']
  394. except (IndexError, TypeError, KeyError):
  395. error_url = request.path_info[1:] # Trim leading slash
  396. try:
  397. tried = exception.args[0]['tried']
  398. except (IndexError, TypeError, KeyError):
  399. tried = []
  400. else:
  401. if (not tried or ( # empty URLconf
  402. request.path == '/' and
  403. len(tried) == 1 and # default URLconf
  404. len(tried[0]) == 1 and
  405. getattr(tried[0][0], 'app_name', '') == getattr(tried[0][0], 'namespace', '') == 'admin'
  406. )):
  407. return default_urlconf(request)
  408. urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
  409. if isinstance(urlconf, types.ModuleType):
  410. urlconf = urlconf.__name__
  411. caller = ''
  412. try:
  413. resolver_match = resolve(request.path)
  414. except Resolver404:
  415. pass
  416. else:
  417. obj = resolver_match.func
  418. if hasattr(obj, '__name__'):
  419. caller = obj.__name__
  420. elif hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
  421. caller = obj.__class__.__name__
  422. if hasattr(obj, '__module__'):
  423. module = obj.__module__
  424. caller = '%s.%s' % (module, caller)
  425. with Path(CURRENT_DIR, 'templates', 'technical_404.html').open() as fh:
  426. t = DEBUG_ENGINE.from_string(fh.read())
  427. c = Context({
  428. 'urlconf': urlconf,
  429. 'root_urlconf': settings.ROOT_URLCONF,
  430. 'request_path': error_url,
  431. 'urlpatterns': tried,
  432. 'reason': str(exception),
  433. 'request': request,
  434. 'settings': get_safe_settings(),
  435. 'raising_view_name': caller,
  436. })
  437. return HttpResponseNotFound(t.render(c), content_type='text/html')
  438. def default_urlconf(request):
  439. """Create an empty URLconf 404 error response."""
  440. with Path(CURRENT_DIR, 'templates', 'default_urlconf.html').open() as fh:
  441. t = DEBUG_ENGINE.from_string(fh.read())
  442. c = Context({
  443. 'version': get_docs_version(),
  444. })
  445. return HttpResponse(t.render(c), content_type='text/html')