from __future__ import unicode_literals import json import logging from datetime import datetime from importlib import import_module from django.conf import settings from django.core.mail import mail_admins from django.utils.dateparse import parse_date from django.utils.timezone import localtime, make_aware, now from cspreports.models import CSPReport logger = logging.getLogger(getattr(settings, "CSP_REPORTS_LOGGER_NAME", "CSP Reports")) def process_report(request): """ Given the HTTP request of a CSP violation report, log it in the required ways. """ if not should_process_report(request): return if config.EMAIL_ADMINS: email_admins(request) if config.LOG: log_report(request) if config.SAVE: save_report(request) if config.ADDITIONAL_HANDLERS: run_additional_handlers(request) def format_report(jsn): """ Given a JSON report, return a nicely formatted (i.e. with indentation) string. This should handle invalid JSON (as the JSON comes from the browser/user). We trust that Python's json library is secure, but if the JSON is invalid then we still want to be able to display it, rather than tripping up on a ValueError. """ if isinstance(jsn, bytes): jsn = jsn.decode('utf-8') try: return json.dumps(json.loads(jsn), indent=4, sort_keys=True, separators=(',', ': ')) except ValueError: return "Invalid JSON. Raw dump is below.\n\n" + jsn def email_admins(request): user_agent = request.META.get('HTTP_USER_AGENT', '') report = format_report(request.body) message = "User agent:\n%s\n\nReport:\n%s" % (user_agent, report) mail_admins("CSP Violation Report", message) def log_report(request): func = getattr(logger, config.LOG_LEVEL) func("Content Security Policy violation: %s", format_report(request.body)) def save_report(request): message = request.body if isinstance(message, bytes): message = message.decode(request.encoding or settings.DEFAULT_CHARSET) report = CSPReport.from_message(message) report.user_agent = request.META.get('HTTP_USER_AGENT', '') report.save() def run_additional_handlers(request): for handler in get_additional_handlers(): handler(request) class Config(object): """ Configuration with defaults, each of which is overrideable in django settings. """ # Defaults, these are overridden using "CSP_REPORTS_"-prefixed versions in settings.py EMAIL_ADMINS = True LOG = True LOG_LEVEL = 'warning' SAVE = True ADDITIONAL_HANDLERS = [] FILTER_FUNCTION = None def __getattribute__(self, name): try: return getattr(settings, "%s%s" % ("CSP_REPORTS_", name)) except AttributeError: return super(Config, self).__getattribute__(name) config = Config() _additional_handlers = None _filter_function = None def get_additional_handlers(): """ Returns the actual functions from the dotted paths specified in ADDITIONAL_HANDLERS. """ global _additional_handlers if not isinstance(_additional_handlers, list): handlers = [] for name in config.ADDITIONAL_HANDLERS: function = import_from_dotted_path(name) handlers.append(function) _additional_handlers = handlers return _additional_handlers def parse_date_input(value): """Return datetime based on the user's input. @param value: User's input @type value: str @raise ValueError: If the input is not valid. @return: Datetime of the beginning of the user's date. """ try: limit = parse_date(value) except ValueError: limit = None if limit is None: raise ValueError("'{}' is not a valid date.".format(value)) limit = datetime(limit.year, limit.month, limit.day) if settings.USE_TZ: limit = make_aware(limit) return limit def get_midnight(): """Return last midnight in localtime as datetime. @return: Midnight datetime """ limit = now() if settings.USE_TZ: limit = localtime(limit) return limit.replace(hour=0, minute=0, second=0, microsecond=0) def import_from_dotted_path(name): module_name, function_name = name.rsplit('.', 1) return getattr(import_module(module_name), function_name) def should_process_report(request): if not config.FILTER_FUNCTION: return True global _filter_function if _filter_function is None: _filter_function = import_from_dotted_path(config.FILTER_FUNCTION) return _filter_function(request)