diff --git a/rophako/app.py b/rophako/app.py index 6bef520..597c281 100644 --- a/rophako/app.py +++ b/rophako/app.py @@ -29,6 +29,7 @@ Config.load_plugins() from rophako import __version__ from rophako.plugin import load_plugin +from rophako.log import logger import rophako.model.tracking as Tracking import rophako.utils @@ -156,6 +157,16 @@ def index(): return catchall("index") +@app.errorhandler(Exception) +def catch_exception(error): + """Catch unexpected Python exceptions and e-mail them out.""" + logger.error("INTERNAL SERVER ERROR: {}".format(str(error))) + + # E-mail it out. + rophako.utils.handle_exception(error) + return rophako.utils.template("errors/500.html") + + @app.errorhandler(404) def not_found(error): return render_template('errors/404.html', **g.info), 404 diff --git a/rophako/jsondb.py b/rophako/jsondb.py index 3650f43..26c296a 100644 --- a/rophako/jsondb.py +++ b/rophako/jsondb.py @@ -13,6 +13,7 @@ import json import time from rophako.settings import Config +from rophako.utils import handle_exception from rophako.log import logger redis_client = None @@ -137,7 +138,10 @@ def read_json(path): data = json.loads(text) except: logger.error("Couldn't decode JSON data from {}".format(path)) - logger.error(text) + handle_exception(Exception("Couldn't decode JSON from {}\n{}".format( + path, + text, + ))) data = None return data diff --git a/rophako/utils.py b/rophako/utils.py index e74a808..82b0472 100644 --- a/rophako/utils.py +++ b/rophako/utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from flask import g, session, request, render_template, flash, redirect, url_for +from flask import (g, session, request, render_template, flash, redirect, + url_for, current_app) from functools import wraps import codecs import uuid @@ -12,6 +13,7 @@ import smtplib import markdown import json import urlparse +import traceback from rophako.log import logger from rophako.settings import Config @@ -192,6 +194,61 @@ def send_email(to, subject, message, sender=None, reply_to=None): server.quit() +def handle_exception(error): + """Send an e-mail to the site admin when an exception occurs.""" + if current_app.config.get("DEBUG"): + print traceback.format_exc() + raise + + import rophako.jsondb as JsonDB + + # Don't spam too many e-mails in a short time frame. + cache = JsonDB.get_cache("exception_catcher") + if cache: + last_exception = int(cache) + if int(time.time()) - last_exception < 120: + # Only one e-mail per 2 minutes, minimum + logger.error("RAPID EXCEPTIONS, DROPPING") + return + JsonDB.set_cache("exception_catcher", int(time.time())) + + username = "anonymous" + try: + if "username" in g.info["session"]: + username = g.info["session"]["username"] + except: + pass + + # Get the timestamp. + timestamp = time.ctime(time.time()) + + # Exception's traceback. + error = str(error.__class__.__name__) + ": " + str(error) + stacktrace = error + "\n\n" \ + + "==== Start Traceback ====\n" \ + + traceback.format_exc() \ + + "==== End Traceback ====\n" + + # Construct the subject and message + subject = "Internal Server Error on {} - {} - {}".format( + Config.site.site_name, + username, + timestamp, + ) + message = "{} has experienced an exception on the route: {}".format( + username, + request.path, + ) + message += "\n\n" + stacktrace + + # Send the e-mail. + send_email( + to=Config.site.notify_address, + subject=subject, + message=message, + ) + + def generate_csrf_token(): """Generator for CSRF tokens.""" if "_csrf" not in session: diff --git a/rophako/www/errors/500.html b/rophako/www/errors/500.html new file mode 100644 index 0000000..b1b7c27 --- /dev/null +++ b/rophako/www/errors/500.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block title %}Internal Server Error{% endblock %} +{% block content %} + +