We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
codename collisions
If a codename collision is detected the application only logs a message it does not prevent that codename from being used by a the second source. While this situation should not exist under normal working conditions it is a defense in depth option that should be implemented.
mentioned by @iSec.


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

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -115,17 +115,34 @@\n     return render_template('index.html')\n \n \n+def 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'))\n def 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-    session['codename'] = crypto_util.genrandomid(number_words)\n-    session.pop('logged_in', None)\n-    # TODO: make sure this codename isn't a repeat\n-    return render_template('generate.html', codename=session['codename'])\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'])\n@@ -136,11 +153,7 @@\n     db_session.add(source)\n     db_session.commit()\n \n-    if os.path.exists(store.path(sid)):\n-        # if this happens, we're not using very secure crypto\n-        log.warning(\"Got a duplicate ID '%s'\" % sid)\n-    else:\n-        os.mkdir(store.path(sid))\n+    os.mkdir(store.path(sid))\n \n     session['logged_in'] = True\n     return redirect(url_for('lookup'))\n", "issue": "codename collisions\nIf a codename collision is detected the application only logs a message it does not prevent that codename from being used by a the second source. While this situation should not exist under normal working conditions it is a defense in depth option that should be implemented.\nmentioned by @iSec.\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\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\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\n@app.route('/generate', methods=('GET', 'POST'))\ndef generate():\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    session['codename'] = crypto_util.genrandomid(number_words)\n    session.pop('logged_in', None)\n    # TODO: make sure this codename isn't a repeat\n    return render_template('generate.html', codename=session['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    if os.path.exists(store.path(sid)):\n        # if this happens, we're not using very secure crypto\n        log.warning(\"Got a duplicate ID '%s'\" % sid)\n    else:\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    strip_metadata = True if 'notclean' in request.form else False\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(\"Thanks! We received your message.\", \"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, fh.content_type, strip_metadata))\n        flash(\"Thanks! We received your document '%s'.\"\n              % fh.filename or '[unnamed]', \"notification\")\n\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_REDIRECT_URL_WHITELIST = [\"http://tor2web.org/\",\n        \"https://www.torproject.org/download.html.en\",\n        \"https://tails.boum.org/\",\n        \"http://www.wired.com/threatlevel/2013/09/freedom-hosting-fbi/\",\n        \"http://www.theguardian.com/world/interactive/2013/oct/04/egotistical-giraffe-nsa-tor-document\",\n        \"https://addons.mozilla.org/en-US/firefox/addon/noscript/\",\n        \"http://noscript.net\"]\n\n\n@app.route('/redirect/<path:redirect_url>')\ndef redirect_hack(redirect_url):\n    # A hack to avoid referer leakage when a user clicks on an external link.\n    # TODO: Most likely will want to share this between source.py and\n    # journalist.py in the future.\n    if redirect_url not in _REDIRECT_URL_WHITELIST:\n        return 'Redirect not allowed'\n    else:\n        return render_template(\"redirect.html\", redirect_url=redirect_url)\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\nif __name__ == \"__main__\":\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\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\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.FlaskConfig)\nCsrfProtect(app)\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    strip_metadata = True if 'notclean' in request.form else False\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(\"Thanks! We received your message.\", \"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, fh.content_type, strip_metadata))\n        flash(\"Thanks! We received your document '%s'.\"\n              % fh.filename or '[unnamed]', \"notification\")\n\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_REDIRECT_URL_WHITELIST = [\"http://tor2web.org/\",\n        \"https://www.torproject.org/download.html.en\",\n        \"https://tails.boum.org/\",\n        \"http://www.wired.com/threatlevel/2013/09/freedom-hosting-fbi/\",\n        \"http://www.theguardian.com/world/interactive/2013/oct/04/egotistical-giraffe-nsa-tor-document\",\n        \"https://addons.mozilla.org/en-US/firefox/addon/noscript/\",\n        \"http://noscript.net\"]\n\n\n@app.route('/redirect/<path:redirect_url>')\ndef redirect_hack(redirect_url):\n    # A hack to avoid referer leakage when a user clicks on an external link.\n    # TODO: Most likely will want to share this between source.py and\n    # journalist.py in the future.\n    if redirect_url not in _REDIRECT_URL_WHITELIST:\n        return 'Redirect not allowed'\n    else:\n        return render_template(\"redirect.html\", redirect_url=redirect_url)\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\nif __name__ == \"__main__\":\n    # TODO make sure debug is not on in production\n    app.run(debug=True, host='0.0.0.0', port=8080)\n", "path": "securedrop/source.py"}]}