prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Add  v2 and v3 source interface addresses to metadata endpoint
  ## Description
  
  The Source Interface exposes  a `/metadata` endpoint that includes information about an instance's OS and application version, supported languages, and submission key fingerprint, which is useful for Nagios monitoring purposes among other things. Adding the SI addresses in this endpoint would a) allow FPF to monitor v3 service uptake and update Nagios checks accordingly, and b) allow end users on the v2 version of  service to verify the correct v3 address for the service (as an alternative or supplement to automatic redirection via the Alt-SVC header).
  
  Potential downside:  if an admin turns on v3 but doesn't want to advertise that they've done so, this could inadvertently expose the v3 address. 
  
  ## User Research Evidence
  
  I have none but folks seem to like the idea on Gitter.
  
  ## User Stories
  - As an FPF support team member, I'd like to be able to have v3 service information available for monitoring purposes
  - as a SecureDrop user, I'd like to be able to verify the correct v3 address corresponding to a v2 address.
  
  
  --- 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/utils.py`
  Content:
  ```
  1 import io
  2 import logging
  3 import subprocess
  4 
  5 from datetime import datetime
  6 from flask import session, current_app, abort, g
  7 from sqlalchemy import create_engine
  8 from sqlalchemy.orm import sessionmaker
  9 from threading import Thread
  10 
  11 import i18n
  12 
  13 from crypto_util import CryptoException
  14 from models import Source
  15 
  16 
  17 def logged_in():
  18     return 'logged_in' in session
  19 
  20 
  21 def valid_codename(codename):
  22     try:
  23         filesystem_id = current_app.crypto_util.hash_codename(codename)
  24     except CryptoException as e:
  25         current_app.logger.info(
  26                 "Could not compute filesystem ID for codename '{}': {}".format(
  27                     codename, e))
  28         abort(500)
  29 
  30     source = Source.query.filter_by(filesystem_id=filesystem_id).first()
  31     return source is not None
  32 
  33 
  34 def generate_unique_codename(config):
  35     """Generate random codenames until we get an unused one"""
  36     while True:
  37         codename = current_app.crypto_util.genrandomid(
  38             Source.NUM_WORDS,
  39             i18n.get_language(config))
  40 
  41         # The maximum length of a word in the wordlist is 9 letters and the
  42         # codename length is 7 words, so it is currently impossible to
  43         # generate a codename that is longer than the maximum codename length
  44         # (currently 128 characters). This code is meant to be defense in depth
  45         # to guard against potential future changes, such as modifications to
  46         # the word list or the maximum codename length.
  47         if len(codename) > Source.MAX_CODENAME_LEN:
  48             current_app.logger.warning(
  49                     "Generated a source codename that was too long, "
  50                     "skipping it. This should not happen. "
  51                     "(Codename='{}')".format(codename))
  52             continue
  53 
  54         # scrypt (slow)
  55         filesystem_id = current_app.crypto_util.hash_codename(codename)
  56 
  57         matching_sources = Source.query.filter(
  58             Source.filesystem_id == filesystem_id).all()
  59         if len(matching_sources) == 0:
  60             return codename
  61 
  62 
  63 def get_entropy_estimate():
  64     with io.open('/proc/sys/kernel/random/entropy_avail') as f:
  65         return int(f.read())
  66 
  67 
  68 def asynchronous(f):
  69     def wrapper(*args, **kwargs):
  70         thread = Thread(target=f, args=args, kwargs=kwargs)
  71         thread.start()
  72     return wrapper
  73 
  74 
  75 @asynchronous
  76 def async_genkey(crypto_util_, db_uri, filesystem_id, codename):
  77     # We pass in the `crypto_util_` so we don't have to reference `current_app`
  78     # here. The app might not have a pushed context during testing which would
  79     # cause this asynchronous function to break.
  80     crypto_util_.genkeypair(filesystem_id, codename)
  81 
  82     # Register key generation as update to the source, so sources will
  83     # filter to the top of the list in the journalist interface if a
  84     # flagged source logs in and has a key generated for them. #789
  85     session = sessionmaker(bind=create_engine(db_uri))()
  86     try:
  87         source = session.query(Source).filter(
  88             Source.filesystem_id == filesystem_id).one()
  89         source.last_updated = datetime.utcnow()
  90         session.commit()
  91     except Exception as e:
  92         logging.getLogger(__name__).error(
  93                 "async_genkey for source (filesystem_id={}): {}"
  94                 .format(filesystem_id, e))
  95     session.close()
  96 
  97 
  98 def normalize_timestamps(filesystem_id):
  99     """
  100     Update the timestamps on all of the source's submissions to match that of
  101     the latest submission. This minimizes metadata that could be useful to
  102     investigators. See #301.
  103     """
  104     sub_paths = [current_app.storage.path(filesystem_id, submission.filename)
  105                  for submission in g.source.submissions]
  106     if len(sub_paths) > 1:
  107         args = ["touch"]
  108         args.extend(sub_paths[:-1])
  109         rc = subprocess.call(args)
  110         if rc != 0:
  111             current_app.logger.warning(
  112                 "Couldn't normalize submission "
  113                 "timestamps (touch exited with %d)" %
  114                 rc)
  115
  ```
  
  Path: `securedrop/source_app/api.py`
  Content:
  ```
  1 import json
  2 import platform
  3 
  4 from flask import Blueprint, current_app, make_response
  5 
  6 import version
  7 
  8 
  9 def make_blueprint(config):
  10     view = Blueprint('api', __name__)
  11 
  12     @view.route('/metadata')
  13     def metadata():
  14         meta = {
  15             'allow_document_uploads': current_app.instance_config.allow_document_uploads,
  16             'gpg_fpr': config.JOURNALIST_KEY,
  17             'sd_version': version.__version__,
  18             'server_os': platform.linux_distribution()[1],
  19             'supported_languages': config.SUPPORTED_LOCALES
  20         }
  21         resp = make_response(json.dumps(meta))
  22         resp.headers['Content-Type'] = 'application/json'
  23         return resp
  24 
  25     return view
  26
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/securedrop/source_app/api.py b/securedrop/source_app/api.py
  --- a/securedrop/source_app/api.py
  +++ b/securedrop/source_app/api.py
  @@ -3,6 +3,8 @@
   
   from flask import Blueprint, current_app, make_response
   
  +from source_app.utils import get_sourcev2_url, get_sourcev3_url
  +
   import version
   
   
  @@ -16,7 +18,9 @@
               'gpg_fpr': config.JOURNALIST_KEY,
               'sd_version': version.__version__,
               'server_os': platform.linux_distribution()[1],
  -            'supported_languages': config.SUPPORTED_LOCALES
  +            'supported_languages': config.SUPPORTED_LOCALES,
  +            'v2_source_url': get_sourcev2_url(),
  +            'v3_source_url': get_sourcev3_url()
           }
           resp = make_response(json.dumps(meta))
           resp.headers['Content-Type'] = 'application/json'
  diff --git a/securedrop/source_app/utils.py b/securedrop/source_app/utils.py
  --- a/securedrop/source_app/utils.py
  +++ b/securedrop/source_app/utils.py
  @@ -9,6 +9,7 @@
   from threading import Thread
   
   import i18n
  +import re
   
   from crypto_util import CryptoException
   from models import Source
  @@ -112,3 +113,31 @@
                   "Couldn't normalize submission "
                   "timestamps (touch exited with %d)" %
                   rc)
  +
  +
  +def check_url_file(path, regexp):
  +    """
  +    Check that a file exists at the path given and contains a single line
  +    matching the regexp. Used for checking the source interface address
  +    files at /var/lib/securedrop/source_{v2,v3}_url.
  +    """
  +    try:
  +        f = open(path, "r")
  +        contents = f.readline().strip()
  +        f.close()
  +        if re.match(regexp, contents):
  +            return contents
  +        else:
  +            return None
  +    except IOError:
  +        return None
  +
  +
  +def get_sourcev2_url():
  +    return check_url_file("/var/lib/securedrop/source_v2_url",
  +                          r"^[a-z0-9]{16}\.onion$")
  +
  +
  +def get_sourcev3_url():
  +    return check_url_file("/var/lib/securedrop/source_v3_url",
  +                          r"^[a-z0-9]{56}\.onion$")
  
verification_info: {"golden_diff": "diff --git a/securedrop/source_app/api.py b/securedrop/source_app/api.py\n--- a/securedrop/source_app/api.py\n+++ b/securedrop/source_app/api.py\n@@ -3,6 +3,8 @@\n \n from flask import Blueprint, current_app, make_response\n \n+from source_app.utils import get_sourcev2_url, get_sourcev3_url\n+\n import version\n \n \n@@ -16,7 +18,9 @@\n             'gpg_fpr': config.JOURNALIST_KEY,\n             'sd_version': version.__version__,\n             'server_os': platform.linux_distribution()[1],\n-            'supported_languages': config.SUPPORTED_LOCALES\n+            'supported_languages': config.SUPPORTED_LOCALES,\n+            'v2_source_url': get_sourcev2_url(),\n+            'v3_source_url': get_sourcev3_url()\n         }\n         resp = make_response(json.dumps(meta))\n         resp.headers['Content-Type'] = 'application/json'\ndiff --git a/securedrop/source_app/utils.py b/securedrop/source_app/utils.py\n--- a/securedrop/source_app/utils.py\n+++ b/securedrop/source_app/utils.py\n@@ -9,6 +9,7 @@\n from threading import Thread\n \n import i18n\n+import re\n \n from crypto_util import CryptoException\n from models import Source\n@@ -112,3 +113,31 @@\n                 \"Couldn't normalize submission \"\n                 \"timestamps (touch exited with %d)\" %\n                 rc)\n+\n+\n+def check_url_file(path, regexp):\n+    \"\"\"\n+    Check that a file exists at the path given and contains a single line\n+    matching the regexp. Used for checking the source interface address\n+    files at /var/lib/securedrop/source_{v2,v3}_url.\n+    \"\"\"\n+    try:\n+        f = open(path, \"r\")\n+        contents = f.readline().strip()\n+        f.close()\n+        if re.match(regexp, contents):\n+            return contents\n+        else:\n+            return None\n+    except IOError:\n+        return None\n+\n+\n+def get_sourcev2_url():\n+    return check_url_file(\"/var/lib/securedrop/source_v2_url\",\n+                          r\"^[a-z0-9]{16}\\.onion$\")\n+\n+\n+def get_sourcev3_url():\n+    return check_url_file(\"/var/lib/securedrop/source_v3_url\",\n+                          r\"^[a-z0-9]{56}\\.onion$\")\n", "issue": "Add  v2 and v3 source interface addresses to metadata endpoint\n## Description\r\n\r\nThe Source Interface exposes  a `/metadata` endpoint that includes information about an instance's OS and application version, supported languages, and submission key fingerprint, which is useful for Nagios monitoring purposes among other things. Adding the SI addresses in this endpoint would a) allow FPF to monitor v3 service uptake and update Nagios checks accordingly, and b) allow end users on the v2 version of  service to verify the correct v3 address for the service (as an alternative or supplement to automatic redirection via the Alt-SVC header).\r\n\r\nPotential downside:  if an admin turns on v3 but doesn't want to advertise that they've done so, this could inadvertently expose the v3 address. \r\n\r\n## User Research Evidence\r\n\r\nI have none but folks seem to like the idea on Gitter.\r\n\r\n## User Stories\r\n- As an FPF support team member, I'd like to be able to have v3 service information available for monitoring purposes\r\n- as a SecureDrop user, I'd like to be able to verify the correct v3 address corresponding to a v2 address.\r\n\n", "before_files": [{"content": "import io\nimport logging\nimport subprocess\n\nfrom datetime import datetime\nfrom flask import session, current_app, abort, g\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom threading import Thread\n\nimport i18n\n\nfrom crypto_util import CryptoException\nfrom models import Source\n\n\ndef logged_in():\n    return 'logged_in' in session\n\n\ndef valid_codename(codename):\n    try:\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n    except CryptoException as e:\n        current_app.logger.info(\n                \"Could not compute filesystem ID for codename '{}': {}\".format(\n                    codename, e))\n        abort(500)\n\n    source = Source.query.filter_by(filesystem_id=filesystem_id).first()\n    return source is not None\n\n\ndef generate_unique_codename(config):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = current_app.crypto_util.genrandomid(\n            Source.NUM_WORDS,\n            i18n.get_language(config))\n\n        # The maximum length of a word in the wordlist is 9 letters and the\n        # codename length is 7 words, so it is currently impossible to\n        # generate a codename that is longer than the maximum codename length\n        # (currently 128 characters). This code is meant to be defense in depth\n        # to guard against potential future changes, such as modifications to\n        # the word list or the maximum codename length.\n        if len(codename) > Source.MAX_CODENAME_LEN:\n            current_app.logger.warning(\n                    \"Generated a source codename that was too long, \"\n                    \"skipping it. This should not happen. \"\n                    \"(Codename='{}')\".format(codename))\n            continue\n\n        # scrypt (slow)\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n\n        matching_sources = Source.query.filter(\n            Source.filesystem_id == filesystem_id).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\ndef get_entropy_estimate():\n    with io.open('/proc/sys/kernel/random/entropy_avail') as f:\n        return int(f.read())\n\n\ndef asynchronous(f):\n    def wrapper(*args, **kwargs):\n        thread = Thread(target=f, args=args, kwargs=kwargs)\n        thread.start()\n    return wrapper\n\n\n@asynchronous\ndef async_genkey(crypto_util_, db_uri, filesystem_id, codename):\n    # We pass in the `crypto_util_` so we don't have to reference `current_app`\n    # here. The app might not have a pushed context during testing which would\n    # cause this asynchronous function to break.\n    crypto_util_.genkeypair(filesystem_id, codename)\n\n    # Register key generation as update to the source, so sources will\n    # filter to the top of the list in the journalist interface if a\n    # flagged source logs in and has a key generated for them. #789\n    session = sessionmaker(bind=create_engine(db_uri))()\n    try:\n        source = session.query(Source).filter(\n            Source.filesystem_id == filesystem_id).one()\n        source.last_updated = datetime.utcnow()\n        session.commit()\n    except Exception as e:\n        logging.getLogger(__name__).error(\n                \"async_genkey for source (filesystem_id={}): {}\"\n                .format(filesystem_id, e))\n    session.close()\n\n\ndef normalize_timestamps(filesystem_id):\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 = [current_app.storage.path(filesystem_id, 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            current_app.logger.warning(\n                \"Couldn't normalize submission \"\n                \"timestamps (touch exited with %d)\" %\n                rc)\n", "path": "securedrop/source_app/utils.py"}, {"content": "import json\nimport platform\n\nfrom flask import Blueprint, current_app, make_response\n\nimport version\n\n\ndef make_blueprint(config):\n    view = Blueprint('api', __name__)\n\n    @view.route('/metadata')\n    def metadata():\n        meta = {\n            'allow_document_uploads': current_app.instance_config.allow_document_uploads,\n            'gpg_fpr': config.JOURNALIST_KEY,\n            'sd_version': version.__version__,\n            'server_os': platform.linux_distribution()[1],\n            'supported_languages': config.SUPPORTED_LOCALES\n        }\n        resp = make_response(json.dumps(meta))\n        resp.headers['Content-Type'] = 'application/json'\n        return resp\n\n    return view\n", "path": "securedrop/source_app/api.py"}], "after_files": [{"content": "import io\nimport logging\nimport subprocess\n\nfrom datetime import datetime\nfrom flask import session, current_app, abort, g\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom threading import Thread\n\nimport i18n\nimport re\n\nfrom crypto_util import CryptoException\nfrom models import Source\n\n\ndef logged_in():\n    return 'logged_in' in session\n\n\ndef valid_codename(codename):\n    try:\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n    except CryptoException as e:\n        current_app.logger.info(\n                \"Could not compute filesystem ID for codename '{}': {}\".format(\n                    codename, e))\n        abort(500)\n\n    source = Source.query.filter_by(filesystem_id=filesystem_id).first()\n    return source is not None\n\n\ndef generate_unique_codename(config):\n    \"\"\"Generate random codenames until we get an unused one\"\"\"\n    while True:\n        codename = current_app.crypto_util.genrandomid(\n            Source.NUM_WORDS,\n            i18n.get_language(config))\n\n        # The maximum length of a word in the wordlist is 9 letters and the\n        # codename length is 7 words, so it is currently impossible to\n        # generate a codename that is longer than the maximum codename length\n        # (currently 128 characters). This code is meant to be defense in depth\n        # to guard against potential future changes, such as modifications to\n        # the word list or the maximum codename length.\n        if len(codename) > Source.MAX_CODENAME_LEN:\n            current_app.logger.warning(\n                    \"Generated a source codename that was too long, \"\n                    \"skipping it. This should not happen. \"\n                    \"(Codename='{}')\".format(codename))\n            continue\n\n        # scrypt (slow)\n        filesystem_id = current_app.crypto_util.hash_codename(codename)\n\n        matching_sources = Source.query.filter(\n            Source.filesystem_id == filesystem_id).all()\n        if len(matching_sources) == 0:\n            return codename\n\n\ndef get_entropy_estimate():\n    with io.open('/proc/sys/kernel/random/entropy_avail') as f:\n        return int(f.read())\n\n\ndef asynchronous(f):\n    def wrapper(*args, **kwargs):\n        thread = Thread(target=f, args=args, kwargs=kwargs)\n        thread.start()\n    return wrapper\n\n\n@asynchronous\ndef async_genkey(crypto_util_, db_uri, filesystem_id, codename):\n    # We pass in the `crypto_util_` so we don't have to reference `current_app`\n    # here. The app might not have a pushed context during testing which would\n    # cause this asynchronous function to break.\n    crypto_util_.genkeypair(filesystem_id, codename)\n\n    # Register key generation as update to the source, so sources will\n    # filter to the top of the list in the journalist interface if a\n    # flagged source logs in and has a key generated for them. #789\n    session = sessionmaker(bind=create_engine(db_uri))()\n    try:\n        source = session.query(Source).filter(\n            Source.filesystem_id == filesystem_id).one()\n        source.last_updated = datetime.utcnow()\n        session.commit()\n    except Exception as e:\n        logging.getLogger(__name__).error(\n                \"async_genkey for source (filesystem_id={}): {}\"\n                .format(filesystem_id, e))\n    session.close()\n\n\ndef normalize_timestamps(filesystem_id):\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 = [current_app.storage.path(filesystem_id, 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            current_app.logger.warning(\n                \"Couldn't normalize submission \"\n                \"timestamps (touch exited with %d)\" %\n                rc)\n\n\ndef check_url_file(path, regexp):\n    \"\"\"\n    Check that a file exists at the path given and contains a single line\n    matching the regexp. Used for checking the source interface address\n    files at /var/lib/securedrop/source_{v2,v3}_url.\n    \"\"\"\n    try:\n        f = open(path, \"r\")\n        contents = f.readline().strip()\n        f.close()\n        if re.match(regexp, contents):\n            return contents\n        else:\n            return None\n    except IOError:\n        return None\n\n\ndef get_sourcev2_url():\n    return check_url_file(\"/var/lib/securedrop/source_v2_url\",\n                          r\"^[a-z0-9]{16}\\.onion$\")\n\n\ndef get_sourcev3_url():\n    return check_url_file(\"/var/lib/securedrop/source_v3_url\",\n                          r\"^[a-z0-9]{56}\\.onion$\")\n", "path": "securedrop/source_app/utils.py"}, {"content": "import json\nimport platform\n\nfrom flask import Blueprint, current_app, make_response\n\nfrom source_app.utils import get_sourcev2_url, get_sourcev3_url\n\nimport version\n\n\ndef make_blueprint(config):\n    view = Blueprint('api', __name__)\n\n    @view.route('/metadata')\n    def metadata():\n        meta = {\n            'allow_document_uploads': current_app.instance_config.allow_document_uploads,\n            'gpg_fpr': config.JOURNALIST_KEY,\n            'sd_version': version.__version__,\n            'server_os': platform.linux_distribution()[1],\n            'supported_languages': config.SUPPORTED_LOCALES,\n            'v2_source_url': get_sourcev2_url(),\n            'v3_source_url': get_sourcev3_url()\n        }\n        resp = make_response(json.dumps(meta))\n        resp.headers['Content-Type'] = 'application/json'\n        return resp\n\n    return view\n", "path": "securedrop/source_app/api.py"}]}