We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Codename with a ‘/‘ causes an exception (or ‘\’)
STR:
1. Visit `/login` on the source interface
2. Type "/" in the codename field and submit the form.

In development, you can see from the traceback that this raises a CryptoException. This is intentional - we intentionally check the codenames for dangerous characters and do not process codenames that contain them, since the dangerous characters will be passed to GPG and could lead to a shell injection. In prod, you would get a 500 Internal Server Error.

Instead of erroring out, we should flash a message to the user that says their codename was invalid, and redirect them back to the login page.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/source.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 from datetime import datetime
4 import uuid
5 from functools import wraps
6 import zipfile
7 from cStringIO import StringIO
8 import subprocess
9 
10 import logging
11 # This module's logger is explicitly labeled so the correct logger is used,
12 # even when this is run from the command line (e.g. during development)
13 log = logging.getLogger('source')
14 
15 from flask import (Flask, request, render_template, session, redirect, url_for,
16                    flash, abort, g, send_file)
17 from flask_wtf.csrf import CsrfProtect
18 
19 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
20 
21 import config
22 import version
23 import crypto_util
24 import store
25 import background
26 from db import db_session, Source, Submission
27 from request_that_secures_file_uploads import RequestThatSecuresFileUploads
28 
29 app = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)
30 app.request_class = RequestThatSecuresFileUploads
31 app.config.from_object(config.FlaskConfig)
32 CsrfProtect(app)
33 
34 SUBMIT_DOC_NOTIFY_STR = "Thanks! We received your document"
35 SUBMIT_MSG_NOTIFY_STR = "Thanks! We received your message"
36 SUBMIT_CODENAME_NOTIFY_STR = "Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages."
37 
38 app.jinja_env.globals['version'] = version.__version__
39 if getattr(config, 'CUSTOM_HEADER_IMAGE', None):
40     app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
41     app.jinja_env.globals['use_custom_header_image'] = True
42 else:
43     app.jinja_env.globals['header_image'] = 'logo.png'
44     app.jinja_env.globals['use_custom_header_image'] = False
45 
46 @app.template_filter('datetimeformat')
47 def _jinja2_datetimeformat(dt, fmt=None):
48     """Template filter for readable formatting of datetime.datetime"""
49     fmt = fmt or '%b %d, %Y %I:%M %p'
50     return dt.strftime(fmt)
51 
52 
53 @app.teardown_appcontext
54 def shutdown_session(exception=None):
55     """Automatically remove database sessions at the end of the request, or
56     when the application shuts down"""
57     db_session.remove()
58 
59 
60 def logged_in():
61     if 'logged_in' in session:
62         return True
63 
64 
65 def login_required(f):
66     @wraps(f)
67     def decorated_function(*args, **kwargs):
68         if not logged_in():
69             return redirect(url_for('login'))
70         return f(*args, **kwargs)
71     return decorated_function
72 
73 
74 def ignore_static(f):
75     """Only executes the wrapped function if we're not loading a static resource."""
76     @wraps(f)
77     def decorated_function(*args, **kwargs):
78         if request.path.startswith('/static'):
79             return  # don't execute the decorated function
80         return f(*args, **kwargs)
81     return decorated_function
82 
83 
84 @app.before_request
85 @ignore_static
86 def setup_g():
87     """Store commonly used values in Flask's special g object"""
88     # ignore_static here because `crypto_util.hash_codename` is scrypt (very
89     # time consuming), and we don't need to waste time running if we're just
90     # serving a static resource that won't need to access these common values.
91     if logged_in():
92         g.codename = session['codename']
93         g.sid = crypto_util.hash_codename(g.codename)
94         try:
95             g.source = Source.query.filter(Source.filesystem_id == g.sid).one()
96         except MultipleResultsFound as e:
97             app.logger.error("Found multiple Sources when one was expected: %s" % (e,))
98             abort(500)
99         except NoResultFound as e:
100             app.logger.error("Found no Sources when one was expected: %s" % (e,))
101             del session['logged_in']
102             del session['codename']
103             return redirect(url_for('index'))
104         g.loc = store.path(g.sid)
105 
106 
107 @app.before_request
108 @ignore_static
109 def check_tor2web():
110         # ignore_static here so we only flash a single message warning about Tor2Web,
111         # corresponding to the intial page load.
112     if 'X-tor2web' in request.headers:
113         flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '
114               'This <strong>does not</strong> provide anonymity. '
115               '<a href="/tor2web-warning">Why is this dangerous?</a>',
116               "banner-warning")
117 
118 
119 @app.route('/')
120 def index():
121     return render_template('index.html')
122 
123 
124 def generate_unique_codename(num_words):
125     """Generate random codenames until we get an unused one"""
126     while True:
127         codename = crypto_util.genrandomid(num_words)
128         sid = crypto_util.hash_codename(codename) # scrypt (slow)
129         matching_sources = Source.query.filter(Source.filesystem_id == sid).all()
130         if len(matching_sources) == 0:
131             return codename
132 
133 
134 @app.route('/generate', methods=('GET', 'POST'))
135 def generate():
136     # Popping this key prevents errors when a logged in user returns to /generate.
137     # TODO: is this the best experience? A logged in user will be automatically
138     # logged out if they navigate to /generate by accident, which could be
139     # confusing. It might be better to instead redirect them to the lookup
140     # page, or inform them that they're logged in.
141     session.pop('logged_in', None)
142 
143     number_words = 8
144     if request.method == 'POST':
145         number_words = int(request.form['number-words'])
146         if number_words not in range(7, 11):
147             abort(403)
148 
149     codename = generate_unique_codename(number_words)
150     session['codename'] = codename
151     return render_template('generate.html', codename=codename)
152 
153 
154 @app.route('/create', methods=['POST'])
155 def create():
156     sid = crypto_util.hash_codename(session['codename'])
157 
158     source = Source(sid, crypto_util.display_id())
159     db_session.add(source)
160     db_session.commit()
161 
162     os.mkdir(store.path(sid))
163 
164     session['logged_in'] = True
165     return redirect(url_for('lookup'))
166 
167 
168 @app.route('/lookup', methods=('GET',))
169 @login_required
170 def lookup():
171     replies = []
172     for fn in os.listdir(g.loc):
173         if fn.endswith('-reply.gpg'):
174             try:
175                 msg = crypto_util.decrypt(g.codename,
176                         file(store.path(g.sid, fn)).read()).decode("utf-8")
177             except UnicodeDecodeError:
178                 app.logger.error("Could not decode reply %s" % fn)
179             else:
180                 date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)
181                 replies.append(dict(id=fn, date=date, msg=msg))
182 
183     def async_genkey(sid, codename):
184         with app.app_context():
185             background.execute(lambda: crypto_util.genkeypair(sid, codename))
186 
187     # Generate a keypair to encrypt replies from the journalist
188     # Only do this if the journalist has flagged the source as one
189     # that they would like to reply to. (Issue #140.)
190     if not crypto_util.getkey(g.sid) and g.source.flagged:
191         async_genkey(g.sid, g.codename)
192 
193     # if this was a redirect from the login page, flash a message if there are
194     # no replies to clarify "check for replies" flow (#393)
195     if request.args.get('from_login') == '1' and len(replies) == 0:
196         flash("There are no replies at this time. You can submit more documents from this code name below.", "notification")
197 
198     return render_template('lookup.html', codename=g.codename, replies=replies,
199             flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))
200 
201 
202 def normalize_timestamps(sid):
203     """
204     Update the timestamps on all of the source's submissions to match that of
205     the latest submission. This minimizes metadata that could be useful to
206     investigators. See #301.
207     """
208     sub_paths = [ store.path(sid, submission.filename)
209                   for submission in g.source.submissions ]
210     if len(sub_paths) > 1:
211         args = ["touch"]
212         args.extend(sub_paths[:-1])
213         rc = subprocess.call(args)
214         if rc != 0:
215             app.logger.warning("Couldn't normalize submission timestamps (touch exited with %d)" % rc)
216 
217 
218 @app.route('/submit', methods=('POST',))
219 @login_required
220 def submit():
221     msg = request.form['msg']
222     fh = request.files['fh']
223 
224     fnames = []
225     journalist_filename = g.source.journalist_filename()
226 
227     if msg:
228         g.source.interaction_count += 1
229         fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,
230             journalist_filename, msg))
231         flash("{}. {}".format(SUBMIT_MSG_NOTIFY_STR,
232                               SUBMIT_CODENAME_NOTIFY_STR), "notification")
233     if fh:
234         g.source.interaction_count += 1
235         fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,
236             journalist_filename, fh.filename, fh.stream))
237         flash("{} '{}'. {}".format(SUBMIT_DOC_NOTIFY_STR,
238                                    fh.filename or '[unnamed]',
239                                    SUBMIT_CODENAME_NOTIFY_STR), "notification")
240     for fname in fnames:
241         submission = Submission(g.source, fname)
242         db_session.add(submission)
243 
244     if g.source.pending:
245         g.source.pending = False
246 
247         # Generate a keypair now, if there's enough entropy (issue #303)
248         entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())
249         if entropy_avail >= 2400:
250             crypto_util.genkeypair(g.sid, g.codename)
251 
252     g.source.last_updated = datetime.now()
253     db_session.commit()
254     normalize_timestamps(g.sid)
255 
256     return redirect(url_for('lookup'))
257 
258 
259 @app.route('/delete', methods=('POST',))
260 @login_required
261 def delete():
262     msgid = request.form['msgid']
263     assert '/' not in msgid
264     potential_files = os.listdir(g.loc)
265     if msgid not in potential_files:
266         abort(404)  # TODO are the checks necessary?
267     store.secure_unlink(store.path(g.sid, msgid))
268     flash("Reply deleted.", "notification")
269 
270     return redirect(url_for('lookup'))
271 
272 
273 def valid_codename(codename):
274     return os.path.exists(store.path(crypto_util.hash_codename(codename)))
275 
276 @app.route('/login', methods=('GET', 'POST'))
277 def login():
278     if request.method == 'POST':
279         codename = request.form['codename']
280         if valid_codename(codename):
281             session.update(codename=codename, logged_in=True)
282             return redirect(url_for('lookup', from_login='1'))
283         else:
284             flash("Sorry, that is not a recognized codename.", "error")
285     return render_template('login.html')
286 
287 
288 @app.route('/howto-disable-js')
289 def howto_disable_js():
290     return render_template("howto-disable-js.html")
291 
292 
293 @app.route('/tor2web-warning')
294 def tor2web_warning():
295     return render_template("tor2web-warning.html")
296 
297 
298 @app.route('/journalist-key')
299 def download_journalist_pubkey():
300     journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)
301     return send_file(StringIO(journalist_pubkey),
302                      mimetype="application/pgp-keys",
303                      attachment_filename=config.JOURNALIST_KEY + ".asc",
304                      as_attachment=True)
305 
306 
307 @app.route('/why-journalist-key')
308 def why_download_journalist_pubkey():
309     return render_template("why-journalist-key.html")
310 
311 
312 @app.errorhandler(404)
313 def page_not_found(error):
314     return render_template('notfound.html'), 404
315 
316 @app.errorhandler(500)
317 def internal_error(error):
318     return render_template('error.html'), 500
319 
320 def write_pidfile():
321     pid = str(os.getpid())
322     with open(config.SOURCE_PIDFILE, 'w') as fp:
323         fp.write(pid)
324 
325 if __name__ == "__main__":
326     write_pidfile()
327     # TODO make sure debug is not on in production
328     app.run(debug=True, host='0.0.0.0', port=8080)
329 
330
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/source.py b/securedrop/source.py
--- a/securedrop/source.py
+++ b/securedrop/source.py
@@ -277,11 +277,15 @@
 def login():
     if request.method == 'POST':
         codename = request.form['codename']
-        if valid_codename(codename):
-            session.update(codename=codename, logged_in=True)
-            return redirect(url_for('lookup', from_login='1'))
+        try:
+            valid = valid_codename(codename)
+        except crypto_util.CryptoException:
+            pass
         else:
-            flash("Sorry, that is not a recognized codename.", "error")
+            if valid:
+                session.update(codename=codename, logged_in=True)
+                return redirect(url_for('lookup', from_login='1'))
+        flash("Sorry, that is not a recognized codename.", "error")
     return render_template('login.html')

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -277,11 +277,15 @@\n def login():\n     if request.method == 'POST':\n         codename = request.form['codename']\n-        if valid_codename(codename):\n-            session.update(codename=codename, logged_in=True)\n-            return redirect(url_for('lookup', from_login='1'))\n+        try:\n+            valid = valid_codename(codename)\n+        except crypto_util.CryptoException:\n+            pass\n         else:\n-            flash(\"Sorry, that is not a recognized codename.\", \"error\")\n+            if valid:\n+                session.update(codename=codename, logged_in=True)\n+                return redirect(url_for('lookup', from_login='1'))\n+        flash(\"Sorry, that is not a recognized codename.\", \"error\")\n     return render_template('login.html')\n", "issue": "Codename with a \u2018/\u2018 causes an exception (or \u2018\\\u2019)\nSTR:\n1. Visit `/login` on the source interface\n2. Type \"/\" in the codename field and submit the form.\n\nIn development, you can see from the traceback that this raises a CryptoException. This is intentional - we intentionally check the codenames for dangerous characters and do not process codenames that contain them, since the dangerous characters will be passed to GPG and could lead to a shell injection. In prod, you would get a 500 Internal Server Error.\n\nInstead of erroring out, we should flash a message to the user that says their codename was invalid, and redirect them back to the login page.\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nfrom db import db_session, Source, Submission\nfrom request_that_secures_file_uploads import RequestThatSecuresFileUploads\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.request_class = RequestThatSecuresFileUploads\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\n\nSUBMIT_DOC_NOTIFY_STR = \"Thanks! We received your document\"\nSUBMIT_MSG_NOTIFY_STR = \"Thanks! We received your message\"\nSUBMIT_CODENAME_NOTIFY_STR = \"Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages.\"\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\n@app.template_filter('datetimeformat')\ndef _jinja2_datetimeformat(dt, fmt=None):\n    \"\"\"Template filter for readable formatting of datetime.datetime\"\"\"\n    fmt = fmt or '%b %d, %Y %I:%M %p'\n    return dt.strftime(fmt)\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    if 'logged_in' in session:\n        return True\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\n    number_words = 8\n    if request.method == 'POST':\n        number_words = int(request.form['number-words'])\n        if number_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(number_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename)\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    db_session.commit()\n\n    os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    def async_genkey(sid, codename):\n        with app.app_context():\n            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    # if this was a redirect from the login page, flash a message if there are\n    # no replies to clarify \"check for replies\" flow (#393)\n    if request.args.get('from_login') == '1' and len(replies) == 0:\n        flash(\"There are no replies at this time. You can submit more documents from this code name below.\", \"notification\")\n\n    return render_template('lookup.html', codename=g.codename, replies=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"{}. {}\".format(SUBMIT_MSG_NOTIFY_STR,\n                              SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream))\n        flash(\"{} '{}'. {}\".format(SUBMIT_DOC_NOTIFY_STR,\n                                   fh.filename or '[unnamed]',\n                                   SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            crypto_util.genkeypair(g.sid, g.codename)\n\n    g.source.last_updated = datetime.now()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted.\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        if valid_codename(codename):\n            session.update(codename=codename, logged_in=True)\n            return redirect(url_for('lookup', from_login='1'))\n        else:\n            flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\n", "path": "securedrop/source.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\nfrom functools import wraps\nimport zipfile\nfrom cStringIO import StringIO\nimport subprocess\n\nimport logging\n# This module's logger is explicitly labeled so the correct logger is used,\n# even when this is run from the command line (e.g. during development)\nlog = logging.getLogger('source')\n\nfrom flask import (Flask, request, render_template, session, redirect, url_for,\n                   flash, abort, g, send_file)\nfrom flask_wtf.csrf import CsrfProtect\n\nfrom sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nfrom db import db_session, Source, Submission\nfrom request_that_secures_file_uploads import RequestThatSecuresFileUploads\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.request_class = RequestThatSecuresFileUploads\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\n\nSUBMIT_DOC_NOTIFY_STR = \"Thanks! We received your document\"\nSUBMIT_MSG_NOTIFY_STR = \"Thanks! We received your message\"\nSUBMIT_CODENAME_NOTIFY_STR = \"Please remember your codename: you can use it to log back into this site to read responses from us and to submit follow-up documents and messages.\"\n\napp.jinja_env.globals['version'] = version.__version__\nif getattr(config, 'CUSTOM_HEADER_IMAGE', None):\n    app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE\n    app.jinja_env.globals['use_custom_header_image'] = True\nelse:\n    app.jinja_env.globals['header_image'] = 'logo.png'\n    app.jinja_env.globals['use_custom_header_image'] = False\n\n@app.template_filter('datetimeformat')\ndef _jinja2_datetimeformat(dt, fmt=None):\n    \"\"\"Template filter for readable formatting of datetime.datetime\"\"\"\n    fmt = fmt or '%b %d, %Y %I:%M %p'\n    return dt.strftime(fmt)\n\n\n@app.teardown_appcontext\ndef shutdown_session(exception=None):\n    \"\"\"Automatically remove database sessions at the end of the request, or\n    when the application shuts down\"\"\"\n    db_session.remove()\n\n\ndef logged_in():\n    if 'logged_in' in session:\n        return True\n\n\ndef login_required(f):\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if not logged_in():\n            return redirect(url_for('login'))\n        return f(*args, **kwargs)\n    return decorated_function\n\n\ndef ignore_static(f):\n    \"\"\"Only executes the wrapped function if we're not loading a static resource.\"\"\"\n    @wraps(f)\n    def decorated_function(*args, **kwargs):\n        if request.path.startswith('/static'):\n            return  # don't execute the decorated function\n        return f(*args, **kwargs)\n    return decorated_function\n\n\n@app.before_request\n@ignore_static\ndef setup_g():\n    \"\"\"Store commonly used values in Flask's special g object\"\"\"\n    # ignore_static here because `crypto_util.hash_codename` is scrypt (very\n    # time consuming), and we don't need to waste time running if we're just\n    # serving a static resource that won't need to access these common values.\n    if logged_in():\n        g.codename = session['codename']\n        g.sid = crypto_util.hash_codename(g.codename)\n        try:\n            g.source = Source.query.filter(Source.filesystem_id == g.sid).one()\n        except MultipleResultsFound as e:\n            app.logger.error(\"Found multiple Sources when one was expected: %s\" % (e,))\n            abort(500)\n        except NoResultFound as e:\n            app.logger.error(\"Found no Sources when one was expected: %s\" % (e,))\n            del session['logged_in']\n            del session['codename']\n            return redirect(url_for('index'))\n        g.loc = store.path(g.sid)\n\n\n@app.before_request\n@ignore_static\ndef check_tor2web():\n        # ignore_static here so we only flash a single message warning about Tor2Web,\n        # corresponding to the intial page load.\n    if 'X-tor2web' in request.headers:\n        flash('<strong>WARNING:</strong> You appear to be using Tor2Web. '\n              'This <strong>does not</strong> provide anonymity. '\n              '<a href=\"/tor2web-warning\">Why is this dangerous?</a>',\n              \"banner-warning\")\n\n\n@app.route('/')\ndef index():\n    return render_template('index.html')\n\n\ndef generate_unique_codename(num_words):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = crypto_util.genrandomid(num_words)\n        sid = crypto_util.hash_codename(codename) # scrypt (slow)\n        matching_sources = Source.query.filter(Source.filesystem_id == sid).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\n    # Popping this key prevents errors when a logged in user returns to /generate.\n    # TODO: is this the best experience? A logged in user will be automatically\n    # logged out if they navigate to /generate by accident, which could be\n    # confusing. It might be better to instead redirect them to the lookup\n    # page, or inform them that they're logged in.\n    session.pop('logged_in', None)\n\n    number_words = 8\n    if request.method == 'POST':\n        number_words = int(request.form['number-words'])\n        if number_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(number_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename)\n\n\n@app.route('/create', methods=['POST'])\ndef create():\n    sid = crypto_util.hash_codename(session['codename'])\n\n    source = Source(sid, crypto_util.display_id())\n    db_session.add(source)\n    db_session.commit()\n\n    os.mkdir(store.path(sid))\n\n    session['logged_in'] = True\n    return redirect(url_for('lookup'))\n\n\n@app.route('/lookup', methods=('GET',))\n@login_required\ndef lookup():\n    replies = []\n    for fn in os.listdir(g.loc):\n        if fn.endswith('-reply.gpg'):\n            try:\n                msg = crypto_util.decrypt(g.codename,\n                        file(store.path(g.sid, fn)).read()).decode(\"utf-8\")\n            except UnicodeDecodeError:\n                app.logger.error(\"Could not decode reply %s\" % fn)\n            else:\n                date = datetime.fromtimestamp(os.stat(store.path(g.sid, fn)).st_mtime)\n                replies.append(dict(id=fn, date=date, msg=msg))\n\n    def async_genkey(sid, codename):\n        with app.app_context():\n            background.execute(lambda: crypto_util.genkeypair(sid, codename))\n\n    # Generate a keypair to encrypt replies from the journalist\n    # Only do this if the journalist has flagged the source as one\n    # that they would like to reply to. (Issue #140.)\n    if not crypto_util.getkey(g.sid) and g.source.flagged:\n        async_genkey(g.sid, g.codename)\n\n    # if this was a redirect from the login page, flash a message if there are\n    # no replies to clarify \"check for replies\" flow (#393)\n    if request.args.get('from_login') == '1' and len(replies) == 0:\n        flash(\"There are no replies at this time. You can submit more documents from this code name below.\", \"notification\")\n\n    return render_template('lookup.html', codename=g.codename, replies=replies,\n            flagged=g.source.flagged, haskey=crypto_util.getkey(g.sid))\n\n\ndef normalize_timestamps(sid):\n    \"\"\"\n    Update the timestamps on all of the source's submissions to match that of\n    the latest submission. This minimizes metadata that could be useful to\n    investigators. See #301.\n    \"\"\"\n    sub_paths = [ store.path(sid, submission.filename)\n                  for submission in g.source.submissions ]\n    if len(sub_paths) > 1:\n        args = [\"touch\"]\n        args.extend(sub_paths[:-1])\n        rc = subprocess.call(args)\n        if rc != 0:\n            app.logger.warning(\"Couldn't normalize submission timestamps (touch exited with %d)\" % rc)\n\n\n@app.route('/submit', methods=('POST',))\n@login_required\ndef submit():\n    msg = request.form['msg']\n    fh = request.files['fh']\n\n    fnames = []\n    journalist_filename = g.source.journalist_filename()\n\n    if msg:\n        g.source.interaction_count += 1\n        fnames.append(store.save_message_submission(g.sid, g.source.interaction_count,\n            journalist_filename, msg))\n        flash(\"{}. {}\".format(SUBMIT_MSG_NOTIFY_STR,\n                              SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\n    if fh:\n        g.source.interaction_count += 1\n        fnames.append(store.save_file_submission(g.sid, g.source.interaction_count,\n            journalist_filename, fh.filename, fh.stream))\n        flash(\"{} '{}'. {}\".format(SUBMIT_DOC_NOTIFY_STR,\n                                   fh.filename or '[unnamed]',\n                                   SUBMIT_CODENAME_NOTIFY_STR), \"notification\")\n    for fname in fnames:\n        submission = Submission(g.source, fname)\n        db_session.add(submission)\n\n    if g.source.pending:\n        g.source.pending = False\n\n        # Generate a keypair now, if there's enough entropy (issue #303)\n        entropy_avail = int(open('/proc/sys/kernel/random/entropy_avail').read())\n        if entropy_avail >= 2400:\n            crypto_util.genkeypair(g.sid, g.codename)\n\n    g.source.last_updated = datetime.now()\n    db_session.commit()\n    normalize_timestamps(g.sid)\n\n    return redirect(url_for('lookup'))\n\n\n@app.route('/delete', methods=('POST',))\n@login_required\ndef delete():\n    msgid = request.form['msgid']\n    assert '/' not in msgid\n    potential_files = os.listdir(g.loc)\n    if msgid not in potential_files:\n        abort(404)  # TODO are the checks necessary?\n    store.secure_unlink(store.path(g.sid, msgid))\n    flash(\"Reply deleted.\", \"notification\")\n\n    return redirect(url_for('lookup'))\n\n\ndef valid_codename(codename):\n    return os.path.exists(store.path(crypto_util.hash_codename(codename)))\n\n@app.route('/login', methods=('GET', 'POST'))\ndef login():\n    if request.method == 'POST':\n        codename = request.form['codename']\n        try:\n            valid = valid_codename(codename)\n        except crypto_util.CryptoException:\n            pass\n        else:\n            if valid:\n                session.update(codename=codename, logged_in=True)\n                return redirect(url_for('lookup', from_login='1'))\n        flash(\"Sorry, that is not a recognized codename.\", \"error\")\n    return render_template('login.html')\n\n\n@app.route('/howto-disable-js')\ndef howto_disable_js():\n    return render_template(\"howto-disable-js.html\")\n\n\n@app.route('/tor2web-warning')\ndef tor2web_warning():\n    return render_template(\"tor2web-warning.html\")\n\n\n@app.route('/journalist-key')\ndef download_journalist_pubkey():\n    journalist_pubkey = crypto_util.gpg.export_keys(config.JOURNALIST_KEY)\n    return send_file(StringIO(journalist_pubkey),\n                     mimetype=\"application/pgp-keys\",\n                     attachment_filename=config.JOURNALIST_KEY + \".asc\",\n                     as_attachment=True)\n\n\n@app.route('/why-journalist-key')\ndef why_download_journalist_pubkey():\n    return render_template(\"why-journalist-key.html\")\n\n\n@app.errorhandler(404)\ndef page_not_found(error):\n    return render_template('notfound.html'), 404\n\n@app.errorhandler(500)\ndef internal_error(error):\n    return render_template('error.html'), 500\n\ndef write_pidfile():\n    pid = str(os.getpid())\n    with open(config.SOURCE_PIDFILE, 'w') as fp:\n        fp.write(pid)\n\nif __name__ == \"__main__\":\n    write_pidfile()\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n\n", "path": "securedrop/source.py"}]}