Repository: certbot/certbot

Issue:
domain string in user config limited to 64 chars
I've initially posted it at the wrong place ([see here](https://github.com/letsencrypt/acme-spec/issues/232)), from where I was sent to [another wrong place](https://github.com/letsencrypt/boulder/issues/1117) – so I hope try number 3 hits the right place now:

The client accepts a config file using the `-c <filename>` parameter. I'm using this to setup things. With new domains whitelisted, I had to create a new certificate – so I've setup my config file, putting the host names in the `domain = name, altname1, altname2` line. On call, the [client crashed](http://pastebin.com/vs60WXn2):

```
Error: [('asn1 encoding routines', 'ASN1_mbstring_ncopy', 'string too long')]
```

Bad thing, as wildcards are not permitted and the cert (in this case) _must_ hold all the names. Figured I can work around this via the `-d` command-line parameter (`-d name -d altname1 -d altname2`), and the cert was created fine. Checked the `.conf` file in `/etc/letsencrypt/renewal` – and found the `domain =` line being exactly as quoted above.

**Verdict:** this limitation just applies to the `-c <config>` file, but not to the renewal config file. Work-around exists as described – but it still would be nice to see this fixed.



Discussion:
Third times a charm. You've found the right repo. This actually a known problem though. See #1180 for discussion of the issue.

Thanks for the pointer, @bmw – different error message, but similar issue. The key answer is (quoted from there):

> Setting values in the configuration file is like setting them on the command line. On the command line, you have to specify the domain flag for each domain.

I already noticed that in another context (no underscores, but dashes in e.g. `standalone-supported-challenges`). The quoted line should be placed in the head of the sample configs :)

Plase submit PR, @IzzySoft to change `example/cli.ini`?

@kuba Shall I really fork the entire repo and submit a PR just to add a comment at top of `examples/cli.ini`? If you insist I do (but I feel that a bit overkill) – otherwise feel free to copy-paste from here:

```
# This is an example of the kind of things you can do in a configuration file.
# All flags used by the client can be configured here. Run Let's Encrypt with
# "--help" to learn more about the available options.
# Note that you have to specify the options in this configuration file as they
# are shown in that help message (not as they appear in the config files located
# in /etc/letsencrypt/renewal). If at the command line you need to specify an
# option multiple times (e.g. domains using the "-d" switch), you need to do so
# here as well (e.g. multiple "domains =" lines, one per domain).
```

First 3 lines are those already present – subsequent lines I'd suggest to add.

@IzzySoft, read the rest of my comment from #1180 that you quoted. Specifying `domains` multiple times in the config file doesn't work:

> ... This is what you would have to do in the configuration file BUT this doesn't quite work. If you do something like:
> 
> ```
> domains = example1.com
> domains = example2.org
> ```
> 
> The value of `domains` is overwritten by "example2.org". I agree the use of `domains` is misleading and this limitation isn't ideal. Perhaps we could consider using a comma separated list for `--domains` in the future which would fix the problem.

@bmw Oh – I see. In that case, I'd raise the priority of fixing the issue (instead of adding a comment). Work-around with "string too long" is specifying everything by `-d` at the command line; does the job, but is not really user-friendly. I agree on "comma separated list", as that again matches the syntax of the config files in `/etc/letsencrypt/renewal/`.

Thanks for correcting my wrong assumption! I otherwise would have wondered on the next run what's wrong now :)


Resolving patch:
diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py
--- a/letsencrypt/cli.py
+++ b/letsencrypt/cli.py
@@ -106,7 +106,7 @@ def _find_domains(args, installer):
         domains = args.domains
 
     if not domains:
-        raise errors.Error("Please specify --domain, or --installer that "
+        raise errors.Error("Please specify --domains, or --installer that "
                            "will help in domain names autodiscovery")
 
     return domains
@@ -465,9 +465,9 @@ def obtaincert(args, config, plugins):
     """Authenticate & obtain cert, but do not install it."""
 
     if args.domains is not None and args.csr is not None:
-        # TODO: --csr could have a priority, when --domain is
+        # TODO: --csr could have a priority, when --domains is
         # supplied, check if CSR matches given domains?
-        return "--domain and --csr are mutually exclusive"
+        return "--domains and --csr are mutually exclusive"
 
     try:
         # installers are used in auth mode to determine domain names
@@ -668,8 +668,32 @@ def parse_args(self):
         parsed_args = self.parser.parse_args(self.args)
         parsed_args.func = self.VERBS[self.verb]
 
+        parsed_args.domains = self._parse_domains(parsed_args.domains)
         return parsed_args
 
+    def _parse_domains(self, domains):
+        """Helper function for parse_args() that parses domains from a
+        (possibly) comma separated list and returns list of unique domains.
+
+        :param domains: List of domain flags
+        :type domains: `list` of `string`
+
+        :returns: List of unique domains
+        :rtype: `list` of `string`
+
+        """
+
+        uniqd = None
+
+        if domains:
+            dlist = []
+            for domain in domains:
+                dlist.extend([d.strip() for d in domain.split(",")])
+            # Make sure we don't have duplicates
+            uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]]
+
+        return uniqd
+
     def determine_verb(self):
         """Determines the verb/subcommand provided by the user.
 
@@ -807,14 +831,15 @@ def prepare_and_parse_args(plugins, args):
         None, "-t", "--text", dest="text_mode", action="store_true",
         help="Use the text output instead of the curses UI.")
     helpful.add(None, "-m", "--email", help=config_help("email"))
-    # positional arg shadows --domain, instead of appending, and
-    # --domain is useful, because it can be stored in config
+    # positional arg shadows --domains, instead of appending, and
+    # --domains is useful, because it can be stored in config
     #for subparser in parser_run, parser_auth, parser_install:
     #    subparser.add_argument("domains", nargs="*", metavar="domain")
-    helpful.add(None, "-d", "--domain", dest="domains",
+    helpful.add(None, "-d", "--domains", dest="domains",
                 metavar="DOMAIN", action="append",
-                help="Domain names to apply. Use multiple -d flags if you want "
-                "to specify multiple domains")
+                help="Domain names to apply. For multiple domains you can use "
+                "multiple -d flags or enter a comma separated list of domains"
+                "as a parameter.")
     helpful.add(
         None, "--duplicate", dest="duplicate", action="store_true",
         help="Allow getting a certificate that duplicates an existing one")


Test patch:
diff --git a/letsencrypt-nginx/tests/boulder-integration.sh b/letsencrypt-nginx/tests/boulder-integration.sh
--- a/letsencrypt-nginx/tests/boulder-integration.sh
+++ b/letsencrypt-nginx/tests/boulder-integration.sh
@@ -18,7 +18,7 @@ letsencrypt_test_nginx () {
         "$@"
 }
 
-letsencrypt_test_nginx --domain nginx.wtf run
+letsencrypt_test_nginx --domains nginx.wtf run
 echo | openssl s_client -connect localhost:5001 \
     | openssl x509 -out $root/nginx.pem
 diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem
diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py
--- a/letsencrypt/tests/cli_test.py
+++ b/letsencrypt/tests/cli_test.py
@@ -117,7 +117,7 @@ def test_help(self):
 
     @mock.patch('letsencrypt.cli.display_ops')
     def test_installer_selection(self, mock_display_ops):
-        self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert',
+        self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',
                     '--key-path', 'key', '--chain-path', 'chain'])
         self.assertEqual(mock_display_ops.pick_installer.call_count, 1)
 
@@ -170,7 +170,7 @@ def test_plugins(self):
 
     def test_certonly_bad_args(self):
         ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
-        self.assertEqual(ret, '--domain and --csr are mutually exclusive')
+        self.assertEqual(ret, '--domains and --csr are mutually exclusive')
 
         ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
         self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
@@ -193,6 +193,27 @@ def test_check_config_sanity_domain(self):
                           self._call,
                           ['-d', '*.wildcard.tld'])
 
+    def test_parse_domains(self):
+        from letsencrypt import cli
+        plugins = disco.PluginsRegistry.find_all()
+
+        short_args = ['-d', 'example.com']
+        namespace = cli.prepare_and_parse_args(plugins, short_args)
+        self.assertEqual(namespace.domains, ['example.com'])
+
+        short_args = ['-d', 'example.com,another.net,third.org,example.com']
+        namespace = cli.prepare_and_parse_args(plugins, short_args)
+        self.assertEqual(namespace.domains, ['example.com', 'another.net',
+                                             'third.org'])
+
+        long_args = ['--domains', 'example.com']
+        namespace = cli.prepare_and_parse_args(plugins, long_args)
+        self.assertEqual(namespace.domains, ['example.com'])
+
+        long_args = ['--domains', 'example.com,another.net,example.com']
+        namespace = cli.prepare_and_parse_args(plugins, long_args)
+        self.assertEqual(namespace.domains, ['example.com', 'another.net'])
+
     @mock.patch('letsencrypt.crypto_util.notAfter')
     @mock.patch('letsencrypt.cli.zope.component.getUtility')
     def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh
--- a/tests/boulder-integration.sh
+++ b/tests/boulder-integration.sh
@@ -27,8 +27,8 @@ common() {
         "$@"
 }
 
-common --domain le1.wtf --standalone-supported-challenges tls-sni-01 auth
-common --domain le2.wtf --standalone-supported-challenges http-01 run
+common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth
+common --domains le2.wtf --standalone-supported-challenges http-01 run
 common -a manual -d le.wtf auth
 
 export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
@@ -40,7 +40,7 @@ common auth --csr "$CSR_PATH" \
 openssl x509 -in "${root}/csr/0000_cert.pem" -text
 openssl x509 -in "${root}/csr/0000_chain.pem" -text
 
-common --domain le3.wtf install \
+common --domains le3.wtf install \
        --cert-path "${root}/csr/cert.pem" \
        --key-path "${root}/csr/key.pem"