Repository: certbot/certbot

Issue:
Allow callers to add information to the User Agent (without replacing it entirely)
(Based on IRC discussion with @nuxwin.)

Currently, there is not a good way for people re-packaging or wrapping Certbot to influence its user agent string. They may which to do this so that stats/metrics are available to boulder.

The `--user-agent` flag allows a caller to set the user agent, but that sets the string in its entirely, losing all of the information which is included by default.

For this use case, it is desirable to both the Certbot team and the party re-packaging Certbot that the custom user agent match Certbot user agent as closely as possible: this greatly simplifies the "parsing problem" and allows data for the re-packaged version to be collected along side _or_ separately from vanilla certbot.

Supporting an additional `--user-agent-comment` flag seems like an easy way to address this.

This comment, if set, could be inserted into the main (product-level) comment, following the OS information.

This argument and `--user-agent` would be mutually exclusive.

cc @pde


Discussion:

Resolving patch:
diff --git a/certbot/cli.py b/certbot/cli.py
--- a/certbot/cli.py
+++ b/certbot/cli.py
@@ -1145,6 +1145,11 @@ def _create_subparsers(helpful):
              '(default: {0}). The flags encoded in the user agent are: '
              '--duplicate, --force-renew, --allow-subset-of-names, -n, and '
              'whether any hooks are set.'.format(sample_user_agent()))
+    helpful.add(
+        None, "--user-agent-comment", default=None, type=_user_agent_comment_type,
+        help="Add a comment to the default user agent string. May be used when repackaging Certbot "
+             "or calling it from another tool to allow additional statistical data to be collected."
+             " Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)")
     helpful.add("certonly",
                 "--csr", type=read_file,
                 help="Path to a Certificate Signing Request (CSR) in DER or PEM format."
@@ -1360,6 +1365,10 @@ def parse_preferred_challenges(pref_challs):
             "Unrecognized challenges: {0}".format(unrecognized))
     return challs
 
+def _user_agent_comment_type(value):
+    if "(" in value or ")" in value:
+        raise argparse.ArgumentTypeError("may not contain parentheses")
+    return value
 
 class _DeployHookAction(argparse.Action):
     """Action class for parsing deploy hooks."""
diff --git a/certbot/client.py b/certbot/client.py
--- a/certbot/client.py
+++ b/certbot/client.py
@@ -58,11 +58,12 @@ def determine_user_agent(config):
     # policy, talk to a core Certbot team member before making any
     # changes here.
     if config.user_agent is None:
-        ua = ("CertbotACMEClient/{0} ({1}; {2}) Authenticator/{3} Installer/{4} "
+        ua = ("CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} "
               "({5}; flags: {6}) Py/{7}")
         ua = ua.format(certbot.__version__, cli.cli_command, util.get_os_info_ua(),
                        config.authenticator, config.installer, config.verb,
-                       ua_flags(config), platform.python_version())
+                       ua_flags(config), platform.python_version(),
+                       "; " + config.user_agent_comment if config.user_agent_comment else "")
     else:
         ua = config.user_agent
     return ua


Test patch: