We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Duplicate Source Interface session triggers server error
## Description

Initiating more than one SecureDrop session in the Source Interface at a time causes a Server Error for the second session, and causes the first session to be invalidated.

## Steps to Reproduce

1. Launch the Docker-based SecureDrop development environment;
2. Visit http://localhost:8080/generate twice, in two separate private browser tabs.
3. Click "Submit" in the first tab
4. Click "Submit" in the second tab

## Expected Behavior
I can submit messages or documents.

## Actual Behavior

In the second tab, the following error message appears:

> Server error
> Sorry, the website encountered an error and was unable to complete your request.

In the first tab, subsequent page loads take me to the login screen: http://localhost:8080/login

--- 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_app/main.py`
Content:
```
1 import operator
2 import os
3 import io
4 
5 from datetime import datetime
6 from flask import (Blueprint, render_template, flash, redirect, url_for, g,
7                    session, current_app, request, Markup, abort)
8 from flask_babel import gettext
9 from sqlalchemy.exc import IntegrityError
10 
11 import store
12 
13 from db import db
14 from models import Source, Submission, Reply, get_one_or_else
15 from source_app.decorators import login_required
16 from source_app.utils import (logged_in, generate_unique_codename,
17                               async_genkey, normalize_timestamps,
18                               valid_codename, get_entropy_estimate)
19 from source_app.forms import LoginForm
20 
21 
22 def make_blueprint(config):
23     view = Blueprint('main', __name__)
24 
25     @view.route('/')
26     def index():
27         return render_template('index.html')
28 
29     @view.route('/generate', methods=('GET', 'POST'))
30     def generate():
31         if logged_in():
32             flash(gettext(
33                 "You were redirected because you are already logged in. "
34                 "If you want to create a new account, you should log out "
35                 "first."),
36                   "notification")
37             return redirect(url_for('.lookup'))
38 
39         codename = generate_unique_codename(config)
40         session['codename'] = codename
41         session['new_user'] = True
42         return render_template('generate.html', codename=codename)
43 
44     @view.route('/org-logo')
45     def select_logo():
46         if os.path.exists(os.path.join(current_app.static_folder, 'i',
47                           'custom_logo.png')):
48             return redirect(url_for('static', filename='i/custom_logo.png'))
49         else:
50             return redirect(url_for('static', filename='i/logo.png'))
51 
52     @view.route('/create', methods=['POST'])
53     def create():
54         filesystem_id = current_app.crypto_util.hash_codename(
55             session['codename'])
56 
57         source = Source(filesystem_id, current_app.crypto_util.display_id())
58         db.session.add(source)
59         try:
60             db.session.commit()
61         except IntegrityError as e:
62             db.session.rollback()
63             current_app.logger.error(
64                 "Attempt to create a source with duplicate codename: %s" %
65                 (e,))
66 
67             # Issue 2386: don't log in on duplicates
68             del session['codename']
69 
70             # Issue 4361: Delete 'logged_in' if it's in the session
71             try:
72                 del session['logged_in']
73             except KeyError:
74                 pass
75 
76             abort(500)
77         else:
78             os.mkdir(current_app.storage.path(filesystem_id))
79 
80         session['logged_in'] = True
81         return redirect(url_for('.lookup'))
82 
83     @view.route('/lookup', methods=('GET',))
84     @login_required
85     def lookup():
86         replies = []
87         source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \
88                                   .filter(Reply.deleted_by_source == False).all()  # noqa
89 
90         for reply in source_inbox:
91             reply_path = current_app.storage.path(
92                 g.filesystem_id,
93                 reply.filename,
94             )
95             try:
96                 with io.open(reply_path, "rb") as f:
97                     contents = f.read()
98                 reply_obj = current_app.crypto_util.decrypt(g.codename, contents)
99                 reply.decrypted = reply_obj
100             except UnicodeDecodeError:
101                 current_app.logger.error("Could not decode reply %s" %
102                                          reply.filename)
103             else:
104                 reply.date = datetime.utcfromtimestamp(
105                     os.stat(reply_path).st_mtime)
106                 replies.append(reply)
107 
108         # Sort the replies by date
109         replies.sort(key=operator.attrgetter('date'), reverse=True)
110 
111         # Generate a keypair to encrypt replies from the journalist
112         # Only do this if the journalist has flagged the source as one
113         # that they would like to reply to. (Issue #140.)
114         if not current_app.crypto_util.getkey(g.filesystem_id) and \
115                 g.source.flagged:
116             db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']
117             async_genkey(current_app.crypto_util,
118                          db_uri,
119                          g.filesystem_id,
120                          g.codename)
121 
122         return render_template(
123             'lookup.html',
124             allow_document_uploads=current_app.instance_config.allow_document_uploads,
125             codename=g.codename,
126             replies=replies,
127             flagged=g.source.flagged,
128             new_user=session.get('new_user', None),
129             haskey=current_app.crypto_util.getkey(
130                 g.filesystem_id))
131 
132     @view.route('/submit', methods=('POST',))
133     @login_required
134     def submit():
135         allow_document_uploads = current_app.instance_config.allow_document_uploads
136         msg = request.form['msg']
137         fh = None
138         if allow_document_uploads and 'fh' in request.files:
139             fh = request.files['fh']
140 
141         # Don't submit anything if it was an "empty" submission. #878
142         if not (msg or fh):
143             if allow_document_uploads:
144                 flash(gettext(
145                     "You must enter a message or choose a file to submit."),
146                       "error")
147             else:
148                 flash(gettext("You must enter a message."), "error")
149             return redirect(url_for('main.lookup'))
150 
151         fnames = []
152         journalist_filename = g.source.journalist_filename
153         first_submission = g.source.interaction_count == 0
154 
155         if msg:
156             g.source.interaction_count += 1
157             fnames.append(
158                 current_app.storage.save_message_submission(
159                     g.filesystem_id,
160                     g.source.interaction_count,
161                     journalist_filename,
162                     msg))
163         if fh:
164             g.source.interaction_count += 1
165             fnames.append(
166                 current_app.storage.save_file_submission(
167                     g.filesystem_id,
168                     g.source.interaction_count,
169                     journalist_filename,
170                     fh.filename,
171                     fh.stream))
172 
173         if first_submission:
174             msg = render_template('first_submission_flashed_message.html')
175             flash(Markup(msg), "success")
176 
177         else:
178             if msg and not fh:
179                 html_contents = gettext('Thanks! We received your message.')
180             elif not msg and fh:
181                 html_contents = gettext('Thanks! We received your document.')
182             else:
183                 html_contents = gettext('Thanks! We received your message and '
184                                         'document.')
185 
186             msg = render_template('next_submission_flashed_message.html',
187                                   html_contents=html_contents)
188             flash(Markup(msg), "success")
189 
190         new_submissions = []
191         for fname in fnames:
192             submission = Submission(g.source, fname)
193             db.session.add(submission)
194             new_submissions.append(submission)
195 
196         if g.source.pending:
197             g.source.pending = False
198 
199             # Generate a keypair now, if there's enough entropy (issue #303)
200             # (gpg reads 300 bytes from /dev/random)
201             entropy_avail = get_entropy_estimate()
202             if entropy_avail >= 2400:
203                 db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']
204 
205                 async_genkey(current_app.crypto_util,
206                              db_uri,
207                              g.filesystem_id,
208                              g.codename)
209                 current_app.logger.info("generating key, entropy: {}".format(
210                     entropy_avail))
211             else:
212                 current_app.logger.warn(
213                         "skipping key generation. entropy: {}".format(
214                                 entropy_avail))
215 
216         g.source.last_updated = datetime.utcnow()
217         db.session.commit()
218 
219         for sub in new_submissions:
220             store.async_add_checksum_for_file(sub)
221 
222         normalize_timestamps(g.filesystem_id)
223 
224         return redirect(url_for('main.lookup'))
225 
226     @view.route('/delete', methods=('POST',))
227     @login_required
228     def delete():
229         """This deletes the reply from the source's inbox, but preserves
230         the history for journalists such that they can view conversation
231         history.
232         """
233 
234         query = Reply.query.filter_by(
235             filename=request.form['reply_filename'],
236             source_id=g.source.id)
237         reply = get_one_or_else(query, current_app.logger, abort)
238         reply.deleted_by_source = True
239         db.session.add(reply)
240         db.session.commit()
241 
242         flash(gettext("Reply deleted"), "notification")
243         return redirect(url_for('.lookup'))
244 
245     @view.route('/delete-all', methods=('POST',))
246     @login_required
247     def batch_delete():
248         replies = Reply.query.filter(Reply.source_id == g.source.id) \
249                              .filter(Reply.deleted_by_source == False).all()  # noqa
250         if len(replies) == 0:
251             current_app.logger.error("Found no replies when at least one was "
252                                      "expected")
253             return redirect(url_for('.lookup'))
254 
255         for reply in replies:
256             reply.deleted_by_source = True
257             db.session.add(reply)
258         db.session.commit()
259 
260         flash(gettext("All replies have been deleted"), "notification")
261         return redirect(url_for('.lookup'))
262 
263     @view.route('/login', methods=('GET', 'POST'))
264     def login():
265         form = LoginForm()
266         if form.validate_on_submit():
267             codename = request.form['codename'].strip()
268             if valid_codename(codename):
269                 session.update(codename=codename, logged_in=True)
270                 return redirect(url_for('.lookup', from_login='1'))
271             else:
272                 current_app.logger.info(
273                         "Login failed for invalid codename")
274                 flash(gettext("Sorry, that is not a recognized codename."),
275                       "error")
276         return render_template('login.html', form=form)
277 
278     @view.route('/logout')
279     def logout():
280         if logged_in():
281             msg = render_template('logout_flashed_message.html')
282 
283             # Clear the session after we render the message so it's localized
284             # If a user specified a locale, save it and restore it
285             user_locale = g.locale
286             session.clear()
287             session['locale'] = user_locale
288 
289             flash(Markup(msg), "important hide-if-not-tor-browser")
290         return redirect(url_for('.index'))
291 
292     return view
293
```
--- 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_app/main.py b/securedrop/source_app/main.py
--- a/securedrop/source_app/main.py
+++ b/securedrop/source_app/main.py
@@ -2,6 +2,7 @@
 import os
 import io
 
+from base64 import urlsafe_b64encode
 from datetime import datetime
 from flask import (Blueprint, render_template, flash, redirect, url_for, g,
                    session, current_app, request, Markup, abort)
@@ -37,9 +38,17 @@
             return redirect(url_for('.lookup'))
 
         codename = generate_unique_codename(config)
-        session['codename'] = codename
+
+        # Generate a unique id for each browser tab and associate the codename with this id.
+        # This will allow retrieval of the codename displayed in the tab from which the source has
+        # clicked to proceed to /generate (ref. issue #4458)
+        tab_id = urlsafe_b64encode(os.urandom(64)).decode()
+        codenames = session.get('codenames', {})
+        codenames[tab_id] = codename
+        session['codenames'] = codenames
+
         session['new_user'] = True
-        return render_template('generate.html', codename=codename)
+        return render_template('generate.html', codename=codename, tab_id=tab_id)
 
     @view.route('/org-logo')
     def select_logo():
@@ -51,33 +60,43 @@
 
     @view.route('/create', methods=['POST'])
     def create():
-        filesystem_id = current_app.crypto_util.hash_codename(
-            session['codename'])
-
-        source = Source(filesystem_id, current_app.crypto_util.display_id())
-        db.session.add(source)
-        try:
-            db.session.commit()
-        except IntegrityError as e:
-            db.session.rollback()
-            current_app.logger.error(
-                "Attempt to create a source with duplicate codename: %s" %
-                (e,))
-
-            # Issue 2386: don't log in on duplicates
-            del session['codename']
-
-            # Issue 4361: Delete 'logged_in' if it's in the session
-            try:
-                del session['logged_in']
-            except KeyError:
-                pass
-
-            abort(500)
+        if session.get('logged_in', False):
+            flash(gettext("You are already logged in. Please verify your codename below as it " +
+                          "may differ from the one displayed on the previous page."),
+                  'notification')
         else:
-            os.mkdir(current_app.storage.path(filesystem_id))
+            tab_id = request.form['tab_id']
+            codename = session['codenames'][tab_id]
+            session['codename'] = codename
+
+            del session['codenames']
+
+            filesystem_id = current_app.crypto_util.hash_codename(codename)
+
+            source = Source(filesystem_id, current_app.crypto_util.display_id())
+            db.session.add(source)
+            try:
+                db.session.commit()
+            except IntegrityError as e:
+                db.session.rollback()
+                current_app.logger.error(
+                    "Attempt to create a source with duplicate codename: %s" %
+                    (e,))
+
+                # Issue 2386: don't log in on duplicates
+                del session['codename']
+
+                # Issue 4361: Delete 'logged_in' if it's in the session
+                try:
+                    del session['logged_in']
+                except KeyError:
+                    pass
+
+                abort(500)
+            else:
+                os.mkdir(current_app.storage.path(filesystem_id))
 
-        session['logged_in'] = True
+            session['logged_in'] = True
         return redirect(url_for('.lookup'))
 
     @view.route('/lookup', methods=('GET',))

{"golden_diff": "diff --git a/securedrop/source_app/main.py b/securedrop/source_app/main.py\n--- a/securedrop/source_app/main.py\n+++ b/securedrop/source_app/main.py\n@@ -2,6 +2,7 @@\n import os\n import io\n \n+from base64 import urlsafe_b64encode\n from datetime import datetime\n from flask import (Blueprint, render_template, flash, redirect, url_for, g,\n                    session, current_app, request, Markup, abort)\n@@ -37,9 +38,17 @@\n             return redirect(url_for('.lookup'))\n \n         codename = generate_unique_codename(config)\n-        session['codename'] = codename\n+\n+        # Generate a unique id for each browser tab and associate the codename with this id.\n+        # This will allow retrieval of the codename displayed in the tab from which the source has\n+        # clicked to proceed to /generate (ref. issue #4458)\n+        tab_id = urlsafe_b64encode(os.urandom(64)).decode()\n+        codenames = session.get('codenames', {})\n+        codenames[tab_id] = codename\n+        session['codenames'] = codenames\n+\n         session['new_user'] = True\n-        return render_template('generate.html', codename=codename)\n+        return render_template('generate.html', codename=codename, tab_id=tab_id)\n \n     @view.route('/org-logo')\n     def select_logo():\n@@ -51,33 +60,43 @@\n \n     @view.route('/create', methods=['POST'])\n     def create():\n-        filesystem_id = current_app.crypto_util.hash_codename(\n-            session['codename'])\n-\n-        source = Source(filesystem_id, current_app.crypto_util.display_id())\n-        db.session.add(source)\n-        try:\n-            db.session.commit()\n-        except IntegrityError as e:\n-            db.session.rollback()\n-            current_app.logger.error(\n-                \"Attempt to create a source with duplicate codename: %s\" %\n-                (e,))\n-\n-            # Issue 2386: don't log in on duplicates\n-            del session['codename']\n-\n-            # Issue 4361: Delete 'logged_in' if it's in the session\n-            try:\n-                del session['logged_in']\n-            except KeyError:\n-                pass\n-\n-            abort(500)\n+        if session.get('logged_in', False):\n+            flash(gettext(\"You are already logged in. Please verify your codename below as it \" +\n+                          \"may differ from the one displayed on the previous page.\"),\n+                  'notification')\n         else:\n-            os.mkdir(current_app.storage.path(filesystem_id))\n+            tab_id = request.form['tab_id']\n+            codename = session['codenames'][tab_id]\n+            session['codename'] = codename\n+\n+            del session['codenames']\n+\n+            filesystem_id = current_app.crypto_util.hash_codename(codename)\n+\n+            source = Source(filesystem_id, current_app.crypto_util.display_id())\n+            db.session.add(source)\n+            try:\n+                db.session.commit()\n+            except IntegrityError as e:\n+                db.session.rollback()\n+                current_app.logger.error(\n+                    \"Attempt to create a source with duplicate codename: %s\" %\n+                    (e,))\n+\n+                # Issue 2386: don't log in on duplicates\n+                del session['codename']\n+\n+                # Issue 4361: Delete 'logged_in' if it's in the session\n+                try:\n+                    del session['logged_in']\n+                except KeyError:\n+                    pass\n+\n+                abort(500)\n+            else:\n+                os.mkdir(current_app.storage.path(filesystem_id))\n \n-        session['logged_in'] = True\n+            session['logged_in'] = True\n         return redirect(url_for('.lookup'))\n \n     @view.route('/lookup', methods=('GET',))\n", "issue": "Duplicate Source Interface session triggers server error\n## Description\r\n\r\nInitiating more than one SecureDrop session in the Source Interface at a time causes a Server Error for the second session, and causes the first session to be invalidated.\r\n\r\n## Steps to Reproduce\r\n\r\n1. Launch the Docker-based SecureDrop development environment;\r\n2. Visit http://localhost:8080/generate twice, in two separate private browser tabs.\r\n3. Click \"Submit\" in the first tab\r\n4. Click \"Submit\" in the second tab\r\n\r\n## Expected Behavior\r\nI can submit messages or documents.\r\n\r\n## Actual Behavior\r\n\r\nIn the second tab, the following error message appears:\r\n\r\n> Server error\r\n> Sorry, the website encountered an error and was unable to complete your request.\r\n\r\nIn the first tab, subsequent page loads take me to the login screen: http://localhost:8080/login\n", "before_files": [{"content": "import operator\nimport os\nimport io\n\nfrom datetime import datetime\nfrom flask import (Blueprint, render_template, flash, redirect, url_for, g,\n                   session, current_app, request, Markup, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\n\nimport store\n\nfrom db import db\nfrom models import Source, Submission, Reply, get_one_or_else\nfrom source_app.decorators import login_required\nfrom source_app.utils import (logged_in, generate_unique_codename,\n                              async_genkey, normalize_timestamps,\n                              valid_codename, get_entropy_estimate)\nfrom source_app.forms import LoginForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/')\n    def index():\n        return render_template('index.html')\n\n    @view.route('/generate', methods=('GET', 'POST'))\n    def generate():\n        if logged_in():\n            flash(gettext(\n                \"You were redirected because you are already logged in. \"\n                \"If you want to create a new account, you should log out \"\n                \"first.\"),\n                  \"notification\")\n            return redirect(url_for('.lookup'))\n\n        codename = generate_unique_codename(config)\n        session['codename'] = codename\n        session['new_user'] = True\n        return render_template('generate.html', codename=codename)\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/create', methods=['POST'])\n    def create():\n        filesystem_id = current_app.crypto_util.hash_codename(\n            session['codename'])\n\n        source = Source(filesystem_id, current_app.crypto_util.display_id())\n        db.session.add(source)\n        try:\n            db.session.commit()\n        except IntegrityError as e:\n            db.session.rollback()\n            current_app.logger.error(\n                \"Attempt to create a source with duplicate codename: %s\" %\n                (e,))\n\n            # Issue 2386: don't log in on duplicates\n            del session['codename']\n\n            # Issue 4361: Delete 'logged_in' if it's in the session\n            try:\n                del session['logged_in']\n            except KeyError:\n                pass\n\n            abort(500)\n        else:\n            os.mkdir(current_app.storage.path(filesystem_id))\n\n        session['logged_in'] = True\n        return redirect(url_for('.lookup'))\n\n    @view.route('/lookup', methods=('GET',))\n    @login_required\n    def lookup():\n        replies = []\n        source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \\\n                                  .filter(Reply.deleted_by_source == False).all()  # noqa\n\n        for reply in source_inbox:\n            reply_path = current_app.storage.path(\n                g.filesystem_id,\n                reply.filename,\n            )\n            try:\n                with io.open(reply_path, \"rb\") as f:\n                    contents = f.read()\n                reply_obj = current_app.crypto_util.decrypt(g.codename, contents)\n                reply.decrypted = reply_obj\n            except UnicodeDecodeError:\n                current_app.logger.error(\"Could not decode reply %s\" %\n                                         reply.filename)\n            else:\n                reply.date = datetime.utcfromtimestamp(\n                    os.stat(reply_path).st_mtime)\n                replies.append(reply)\n\n        # Sort the replies by date\n        replies.sort(key=operator.attrgetter('date'), reverse=True)\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 current_app.crypto_util.getkey(g.filesystem_id) and \\\n                g.source.flagged:\n            db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n            async_genkey(current_app.crypto_util,\n                         db_uri,\n                         g.filesystem_id,\n                         g.codename)\n\n        return render_template(\n            'lookup.html',\n            allow_document_uploads=current_app.instance_config.allow_document_uploads,\n            codename=g.codename,\n            replies=replies,\n            flagged=g.source.flagged,\n            new_user=session.get('new_user', None),\n            haskey=current_app.crypto_util.getkey(\n                g.filesystem_id))\n\n    @view.route('/submit', methods=('POST',))\n    @login_required\n    def submit():\n        allow_document_uploads = current_app.instance_config.allow_document_uploads\n        msg = request.form['msg']\n        fh = None\n        if allow_document_uploads and 'fh' in request.files:\n            fh = request.files['fh']\n\n        # Don't submit anything if it was an \"empty\" submission. #878\n        if not (msg or fh):\n            if allow_document_uploads:\n                flash(gettext(\n                    \"You must enter a message or choose a file to submit.\"),\n                      \"error\")\n            else:\n                flash(gettext(\"You must enter a message.\"), \"error\")\n            return redirect(url_for('main.lookup'))\n\n        fnames = []\n        journalist_filename = g.source.journalist_filename\n        first_submission = g.source.interaction_count == 0\n\n        if msg:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_message_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    msg))\n        if fh:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_file_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    fh.filename,\n                    fh.stream))\n\n        if first_submission:\n            msg = render_template('first_submission_flashed_message.html')\n            flash(Markup(msg), \"success\")\n\n        else:\n            if msg and not fh:\n                html_contents = gettext('Thanks! We received your message.')\n            elif not msg and fh:\n                html_contents = gettext('Thanks! We received your document.')\n            else:\n                html_contents = gettext('Thanks! We received your message and '\n                                        'document.')\n\n            msg = render_template('next_submission_flashed_message.html',\n                                  html_contents=html_contents)\n            flash(Markup(msg), \"success\")\n\n        new_submissions = []\n        for fname in fnames:\n            submission = Submission(g.source, fname)\n            db.session.add(submission)\n            new_submissions.append(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            # (gpg reads 300 bytes from /dev/random)\n            entropy_avail = get_entropy_estimate()\n            if entropy_avail >= 2400:\n                db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n\n                async_genkey(current_app.crypto_util,\n                             db_uri,\n                             g.filesystem_id,\n                             g.codename)\n                current_app.logger.info(\"generating key, entropy: {}\".format(\n                    entropy_avail))\n            else:\n                current_app.logger.warn(\n                        \"skipping key generation. entropy: {}\".format(\n                                entropy_avail))\n\n        g.source.last_updated = datetime.utcnow()\n        db.session.commit()\n\n        for sub in new_submissions:\n            store.async_add_checksum_for_file(sub)\n\n        normalize_timestamps(g.filesystem_id)\n\n        return redirect(url_for('main.lookup'))\n\n    @view.route('/delete', methods=('POST',))\n    @login_required\n    def delete():\n        \"\"\"This deletes the reply from the source's inbox, but preserves\n        the history for journalists such that they can view conversation\n        history.\n        \"\"\"\n\n        query = Reply.query.filter_by(\n            filename=request.form['reply_filename'],\n            source_id=g.source.id)\n        reply = get_one_or_else(query, current_app.logger, abort)\n        reply.deleted_by_source = True\n        db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"Reply deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/delete-all', methods=('POST',))\n    @login_required\n    def batch_delete():\n        replies = Reply.query.filter(Reply.source_id == g.source.id) \\\n                             .filter(Reply.deleted_by_source == False).all()  # noqa\n        if len(replies) == 0:\n            current_app.logger.error(\"Found no replies when at least one was \"\n                                     \"expected\")\n            return redirect(url_for('.lookup'))\n\n        for reply in replies:\n            reply.deleted_by_source = True\n            db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"All replies have been deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\n        form = LoginForm()\n        if form.validate_on_submit():\n            codename = request.form['codename'].strip()\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                current_app.logger.info(\n                        \"Login failed for invalid codename\")\n                flash(gettext(\"Sorry, that is not a recognized codename.\"),\n                      \"error\")\n        return render_template('login.html', form=form)\n\n    @view.route('/logout')\n    def logout():\n        if logged_in():\n            msg = render_template('logout_flashed_message.html')\n\n            # Clear the session after we render the message so it's localized\n            # If a user specified a locale, save it and restore it\n            user_locale = g.locale\n            session.clear()\n            session['locale'] = user_locale\n\n            flash(Markup(msg), \"important hide-if-not-tor-browser\")\n        return redirect(url_for('.index'))\n\n    return view\n", "path": "securedrop/source_app/main.py"}], "after_files": [{"content": "import operator\nimport os\nimport io\n\nfrom base64 import urlsafe_b64encode\nfrom datetime import datetime\nfrom flask import (Blueprint, render_template, flash, redirect, url_for, g,\n                   session, current_app, request, Markup, abort)\nfrom flask_babel import gettext\nfrom sqlalchemy.exc import IntegrityError\n\nimport store\n\nfrom db import db\nfrom models import Source, Submission, Reply, get_one_or_else\nfrom source_app.decorators import login_required\nfrom source_app.utils import (logged_in, generate_unique_codename,\n                              async_genkey, normalize_timestamps,\n                              valid_codename, get_entropy_estimate)\nfrom source_app.forms import LoginForm\n\n\ndef make_blueprint(config):\n    view = Blueprint('main', __name__)\n\n    @view.route('/')\n    def index():\n        return render_template('index.html')\n\n    @view.route('/generate', methods=('GET', 'POST'))\n    def generate():\n        if logged_in():\n            flash(gettext(\n                \"You were redirected because you are already logged in. \"\n                \"If you want to create a new account, you should log out \"\n                \"first.\"),\n                  \"notification\")\n            return redirect(url_for('.lookup'))\n\n        codename = generate_unique_codename(config)\n\n        # Generate a unique id for each browser tab and associate the codename with this id.\n        # This will allow retrieval of the codename displayed in the tab from which the source has\n        # clicked to proceed to /generate (ref. issue #4458)\n        tab_id = urlsafe_b64encode(os.urandom(64)).decode()\n        codenames = session.get('codenames', {})\n        codenames[tab_id] = codename\n        session['codenames'] = codenames\n\n        session['new_user'] = True\n        return render_template('generate.html', codename=codename, tab_id=tab_id)\n\n    @view.route('/org-logo')\n    def select_logo():\n        if os.path.exists(os.path.join(current_app.static_folder, 'i',\n                          'custom_logo.png')):\n            return redirect(url_for('static', filename='i/custom_logo.png'))\n        else:\n            return redirect(url_for('static', filename='i/logo.png'))\n\n    @view.route('/create', methods=['POST'])\n    def create():\n        if session.get('logged_in', False):\n            flash(gettext(\"You are already logged in. Please verify your codename below as it \" +\n                          \"may differ from the one displayed on the previous page.\"),\n                  'notification')\n        else:\n            tab_id = request.form['tab_id']\n            codename = session['codenames'][tab_id]\n            session['codename'] = codename\n\n            del session['codenames']\n\n            filesystem_id = current_app.crypto_util.hash_codename(codename)\n\n            source = Source(filesystem_id, current_app.crypto_util.display_id())\n            db.session.add(source)\n            try:\n                db.session.commit()\n            except IntegrityError as e:\n                db.session.rollback()\n                current_app.logger.error(\n                    \"Attempt to create a source with duplicate codename: %s\" %\n                    (e,))\n\n                # Issue 2386: don't log in on duplicates\n                del session['codename']\n\n                # Issue 4361: Delete 'logged_in' if it's in the session\n                try:\n                    del session['logged_in']\n                except KeyError:\n                    pass\n\n                abort(500)\n            else:\n                os.mkdir(current_app.storage.path(filesystem_id))\n\n            session['logged_in'] = True\n        return redirect(url_for('.lookup'))\n\n    @view.route('/lookup', methods=('GET',))\n    @login_required\n    def lookup():\n        replies = []\n        source_inbox = Reply.query.filter(Reply.source_id == g.source.id) \\\n                                  .filter(Reply.deleted_by_source == False).all()  # noqa\n\n        for reply in source_inbox:\n            reply_path = current_app.storage.path(\n                g.filesystem_id,\n                reply.filename,\n            )\n            try:\n                with io.open(reply_path, \"rb\") as f:\n                    contents = f.read()\n                reply_obj = current_app.crypto_util.decrypt(g.codename, contents)\n                reply.decrypted = reply_obj\n            except UnicodeDecodeError:\n                current_app.logger.error(\"Could not decode reply %s\" %\n                                         reply.filename)\n            else:\n                reply.date = datetime.utcfromtimestamp(\n                    os.stat(reply_path).st_mtime)\n                replies.append(reply)\n\n        # Sort the replies by date\n        replies.sort(key=operator.attrgetter('date'), reverse=True)\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 current_app.crypto_util.getkey(g.filesystem_id) and \\\n                g.source.flagged:\n            db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n            async_genkey(current_app.crypto_util,\n                         db_uri,\n                         g.filesystem_id,\n                         g.codename)\n\n        return render_template(\n            'lookup.html',\n            allow_document_uploads=current_app.instance_config.allow_document_uploads,\n            codename=g.codename,\n            replies=replies,\n            flagged=g.source.flagged,\n            new_user=session.get('new_user', None),\n            haskey=current_app.crypto_util.getkey(\n                g.filesystem_id))\n\n    @view.route('/submit', methods=('POST',))\n    @login_required\n    def submit():\n        allow_document_uploads = current_app.instance_config.allow_document_uploads\n        msg = request.form['msg']\n        fh = None\n        if allow_document_uploads and 'fh' in request.files:\n            fh = request.files['fh']\n\n        # Don't submit anything if it was an \"empty\" submission. #878\n        if not (msg or fh):\n            if allow_document_uploads:\n                flash(gettext(\n                    \"You must enter a message or choose a file to submit.\"),\n                      \"error\")\n            else:\n                flash(gettext(\"You must enter a message.\"), \"error\")\n            return redirect(url_for('main.lookup'))\n\n        fnames = []\n        journalist_filename = g.source.journalist_filename\n        first_submission = g.source.interaction_count == 0\n\n        if msg:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_message_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    msg))\n        if fh:\n            g.source.interaction_count += 1\n            fnames.append(\n                current_app.storage.save_file_submission(\n                    g.filesystem_id,\n                    g.source.interaction_count,\n                    journalist_filename,\n                    fh.filename,\n                    fh.stream))\n\n        if first_submission:\n            msg = render_template('first_submission_flashed_message.html')\n            flash(Markup(msg), \"success\")\n\n        else:\n            if msg and not fh:\n                html_contents = gettext('Thanks! We received your message.')\n            elif not msg and fh:\n                html_contents = gettext('Thanks! We received your document.')\n            else:\n                html_contents = gettext('Thanks! We received your message and '\n                                        'document.')\n\n            msg = render_template('next_submission_flashed_message.html',\n                                  html_contents=html_contents)\n            flash(Markup(msg), \"success\")\n\n        new_submissions = []\n        for fname in fnames:\n            submission = Submission(g.source, fname)\n            db.session.add(submission)\n            new_submissions.append(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            # (gpg reads 300 bytes from /dev/random)\n            entropy_avail = get_entropy_estimate()\n            if entropy_avail >= 2400:\n                db_uri = current_app.config['SQLALCHEMY_DATABASE_URI']\n\n                async_genkey(current_app.crypto_util,\n                             db_uri,\n                             g.filesystem_id,\n                             g.codename)\n                current_app.logger.info(\"generating key, entropy: {}\".format(\n                    entropy_avail))\n            else:\n                current_app.logger.warn(\n                        \"skipping key generation. entropy: {}\".format(\n                                entropy_avail))\n\n        g.source.last_updated = datetime.utcnow()\n        db.session.commit()\n\n        for sub in new_submissions:\n            store.async_add_checksum_for_file(sub)\n\n        normalize_timestamps(g.filesystem_id)\n\n        return redirect(url_for('main.lookup'))\n\n    @view.route('/delete', methods=('POST',))\n    @login_required\n    def delete():\n        \"\"\"This deletes the reply from the source's inbox, but preserves\n        the history for journalists such that they can view conversation\n        history.\n        \"\"\"\n\n        query = Reply.query.filter_by(\n            filename=request.form['reply_filename'],\n            source_id=g.source.id)\n        reply = get_one_or_else(query, current_app.logger, abort)\n        reply.deleted_by_source = True\n        db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"Reply deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/delete-all', methods=('POST',))\n    @login_required\n    def batch_delete():\n        replies = Reply.query.filter(Reply.source_id == g.source.id) \\\n                             .filter(Reply.deleted_by_source == False).all()  # noqa\n        if len(replies) == 0:\n            current_app.logger.error(\"Found no replies when at least one was \"\n                                     \"expected\")\n            return redirect(url_for('.lookup'))\n\n        for reply in replies:\n            reply.deleted_by_source = True\n            db.session.add(reply)\n        db.session.commit()\n\n        flash(gettext(\"All replies have been deleted\"), \"notification\")\n        return redirect(url_for('.lookup'))\n\n    @view.route('/login', methods=('GET', 'POST'))\n    def login():\n        form = LoginForm()\n        if form.validate_on_submit():\n            codename = request.form['codename'].strip()\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                current_app.logger.info(\n                        \"Login failed for invalid codename\")\n                flash(gettext(\"Sorry, that is not a recognized codename.\"),\n                      \"error\")\n        return render_template('login.html', form=form)\n\n    @view.route('/logout')\n    def logout():\n        if logged_in():\n            msg = render_template('logout_flashed_message.html')\n\n            # Clear the session after we render the message so it's localized\n            # If a user specified a locale, save it and restore it\n            user_locale = g.locale\n            session.clear()\n            session['locale'] = user_locale\n\n            flash(Markup(msg), \"important hide-if-not-tor-browser\")\n        return redirect(url_for('.index'))\n\n    return view\n", "path": "securedrop/source_app/main.py"}]}