We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Seven word code name option is weaker than the recommended default
When the source generates a new code name, the default is to recommend a string that is eight words long. The source can select strings that contain more words, but the source can also select a string that has fewer words than the recommended default. Available options should ideally only be stronger than the default recommendation, not weaker.


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

{"golden_diff": "diff --git a/securedrop/source.py b/securedrop/source.py\n--- a/securedrop/source.py\n+++ b/securedrop/source.py\n@@ -132,7 +132,7 @@\n     # page, or inform them that they're logged in.\n     session.pop('logged_in', None)\n \n-    num_words = 8\n+    num_words = 7\n     if request.method == 'POST':\n         num_words = int(request.form['number-words'])\n         if num_words not in range(7, 11):\n", "issue": "Seven word code name option is weaker than the recommended default\nWhen the source generates a new code name, the default is to recommend a string that is eight words long. The source can select strings that contain more words, but the source can also select a string that has fewer words than the recommended default. Available options should ideally only be stronger than the default recommendation, not weaker.\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\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\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\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\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    return 'logged_in' in session\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    num_words = 8\n    if request.method == 'POST':\n        num_words = int(request.form['number-words'])\n        if num_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(num_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename, num_words=num_words)\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    try:\n        db_session.commit()\n    except IntegrityError as e: \n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\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.utcfromtimestamp(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    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(\"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))\n        flash('{} \"{}\".'.format(\"Thanks! We received your document\",\n                                fh.filename or '[unnamed]'), \"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.utcnow()\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"}], "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\nfrom sqlalchemy.exc import IntegrityError\n\nimport config\nimport version\nimport crypto_util\nimport store\nimport background\nimport template_filters\nfrom db import db_session, Source, Submission\nfrom jinja2 import evalcontextfilter\n\napp = Flask(__name__, template_folder=config.SOURCE_TEMPLATES_DIR)\napp.config.from_object(config.SourceInterfaceFlaskConfig)\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\napp.jinja_env.filters['datetimeformat'] = template_filters.datetimeformat\napp.jinja_env.filters['nl2br'] = evalcontextfilter(template_filters.nl2br)\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    return 'logged_in' in session\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    num_words = 7\n    if request.method == 'POST':\n        num_words = int(request.form['number-words'])\n        if num_words not in range(7, 11):\n            abort(403)\n\n    codename = generate_unique_codename(num_words)\n    session['codename'] = codename\n    return render_template('generate.html', codename=codename, num_words=num_words)\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    try:\n        db_session.commit()\n    except IntegrityError as e: \n        app.logger.error(\"Attempt to create a source with duplicate codename: %s\" % (e,))\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.utcfromtimestamp(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    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(\"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))\n        flash('{} \"{}\".'.format(\"Thanks! We received your document\",\n                                fh.filename or '[unnamed]'), \"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.utcnow()\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"}]}