Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apache plugin wildcard support for ACMEv2 #5608

Merged
merged 6 commits into from Feb 28, 2018
Merged

Apache plugin wildcard support for ACMEv2 #5608

merged 6 commits into from Feb 28, 2018

Conversation

joohoi
Copy link
Member

@joohoi joohoi commented Feb 22, 2018

Functional example for wildcard certificate support in Apache plugin.

In deploy_cert() and enhance(), the user will be presented with a dialog to choose from the VirtualHosts that can be covered by the wildcard domain name. The (multiple) selection result will then be handled in a similar way that we previously handled a single VirtualHost that was returned by the _find_best_vhost().

Additionally the selected VirtualHosts are added to a dictionary that maps selections to a wildcard domain to be reused in the later enhance() call and not forcing the user to select the same VirtualHosts again.

The code here should be almost completely compatible with Nginx plugin as well, so when we get this merged, we can bring the changes over to Nginx plugin also.

@joohoi joohoi changed the title [WIP] Apache plugin wildcard support Apache plugin wildcard support for ACMEv2 Feb 23, 2018
@@ -1377,7 +1478,18 @@ def enhance(self, domain, enhancement, options=None):
raise errors.PluginError(
"Unsupported enhancement: {0}".format(enhancement))
try:
func(self.choose_vhost(domain), options)
if self.wildcard_domain(domain):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor this so there's less inside the try block?

for vhost in deploy_vhosts:
self._deploy_cert(vhost, cert_path, key_path,
chain_path, fullchain_path)
if domain not in self.wildcard_vhosts.keys():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think you need .keys() here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, why not do this inside choose_vhosts_wildcard?

return fnmatch.fnmatch(name, domain)


def choose_vhosts_wildcard(self, domain, create_ssl=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can probably be internal

filtered_vhosts[name] = vhost
elif name not in filtered_vhosts.keys():
# Add if not in list previously
filtered_vhosts[name] = vhost
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if there aren't any? do we want to create a new one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we do create one when appending the vhosts to the final list in the method.

vhost = self.choose_vhost(domain)
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)

def vhosts_for_wildcard(self, domain):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also can be internal?

func(self.choose_vhost(domain), options)
if self.wildcard_domain(domain):
# Handle virtualhosts configured for a wildcard domain
if domain in self.wildcard_vhosts.keys():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this if (if domain in self.wildcard_vhosts:) go inside choose_vhosts_wildcard?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah, if both this and the other thing go into the function, that cleans everything else up a bunch!

these are my thoughts as I do the nginx one, it may be clearer to look at the PR I put up for that (#5619)

@ohemorange
Copy link
Contributor

What happens if apache is authenticator and it's given a wildcard domain?

@@ -152,6 +153,9 @@ def __init__(self, *args, **kwargs):
self.assoc = dict()
# Outstanding challenges
self._chall_out = set()
# List of vhosts configured per wildcard domain on this run.
# used by deploy_cert() and enhance()
self.wildcard_vhosts = dict()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

underscores everywhere

@joohoi
Copy link
Member Author

joohoi commented Feb 27, 2018

What happens if apache is authenticator and it's given a wildcard domain?

We had a short discussion about this yesterday, but Certbot should raise an exception when Apache plugin is connecting to ACME server that doesn't allow wildcard challenges over HTTP or TLS-SNI challenges.

All the review comments should now be addressed, and this is ready for the next round.

@ohemorange ohemorange requested a review from bmw February 27, 2018 20:40
Copy link
Member

@bmw bmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great @joohoi.

Other than the minor inline comments, when testing this I created a bunch of vhosts but forgot to enable them and when I ran Certbot, it obtained the cert but Apache deploy_cert and enhance just quietly did nothing (because there were no matching vhosts). Took me a few to figure out what was going on.

Any reason not to do what we currently do for single vhosts? If there's no matching vhosts (either because one doesn't exist or the user doesn't select one), we error out telling them to create a vhost. This seems preferable to me.

@@ -300,7 +300,7 @@ def test_get_valid_domains(self):
from certbot.display.ops import get_valid_domains
all_valid = ["example.com", "second.example.com",
"also.example.com", "under_score.example.com",
"justtld", "*.wildcard.com"]
"justtld"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops. I think this was caused by me also making changes to this file in another PR, but we shouldn't remove the wildcard from the list of valid domains here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. This was removed at the early phase of the development, and is now brought back.

for vhost in vhosts:
self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)

def choose_vhosts(self, domain, cached=False, create_if_no_ssl=True):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care too much, but do we need the cached parameter? Do we ever expect there to be a case where both of the following are true:

  1. We have cached vhosts for a domain.
  2. We don't want to use them.

If this doesn't seem likely, maybe we can simplify things slightly and remove this parameter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, we shouldn't hit such scenario. This parameter is now removed.

if not vhost.ssl and create_ssl:
return_vhosts.append(self.make_vhost_ssl(vhost))
else:
return_vhosts.append(vhost)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming the only use case for calling this function with create_ssl=False is for enhance, I get not wanting create an SSL copy in this case, but I'm not sure we should ever be returning HTTP vhosts from this function. Which of our enhancements make sense when you don't have an SSL vhost for a domain?

Instead, maybe if create_ssl is False, we should remove HTTP vhosts from dialog_input before asking for user input.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a more sophisticated way to handle it, I changed the behavior accordingly. This also helps to reduce the static from the dialog in the first place.

not in scope: domain = *.wild.card, name = 1.2.wild.card
"""
if len(name.split(".")) == len(domain.split(".")):
return fnmatch.fnmatch(name, domain)
Copy link
Member

@bmw bmw Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for wildcards in ServerAlias directives too right? Very nice.

Copy link
Member Author

@joohoi joohoi Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, vhost.get_names() (which is basically what's fed to this method) returns all the names associated to the VirtualHost, both ServerNames and ServerAlias. Furthermore, I don't think wildcard names are allowed in the ServerName in the first place.

if not vhosts:
return list()
tags_list = [vhost.display_repr() for vhost in vhosts]
while True:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same can be said about the old select_vhosts function down below, but why do we need this loop? To me, it looks like it will only ever execute once.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presumed there being an undefined reason behind this, as select_vhost() function has the same behavior. It's been around since original implementation by James, introduced in 99fcb4f and now looking at the original commit, it looks like it was in place for a help selection that has since been removed. I cleaned this up from the select_vhost() too.

"File: {filename}\n"
"Addresses: {addrs}\n"
"Names: {names}\n"
"HTTPS: {https}\n\n".format(
Copy link
Member

@bmw bmw Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The trailing empty line on the last entry looks a little awkward to me:

-------------------------------------------------------------------------------
1: File: /etc/apache2/sites-enabled/bmatch.conf
Addresses: *:80
Names: geqgeqoc.example.org
HTTPS: No

2: File: /etc/apache2/sites-enabled/amatch.conf
Addresses: *:80
Names: a.example.org
HTTPS: No

3: File: /etc/apache2/sites-enabled/wildcard.conf
Addresses: *:80
Names: abc.def.example.org, *.example.org
HTTPS: No

-------------------------------------------------------------------------------

Unless you prefer it that way, can we remove it?

The way I'd personally do this is not to include the 2nd newline in this function and have the function calling display_repr iterate over each representation adding a newline to the beginning/end of each one except for the first/last one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks a lot cleaner without the extra newline, I agree. Even though handling that requires a few more lines in the code. This is now implemented.

@bmw
Copy link
Member

bmw commented Feb 28, 2018

Looks good except I don't think my comment at #5608 (review) got addressed.

To expand a little more on why I think it's important, Certbot will currently exit with a zero status and no warnings if there's no matching vhosts so it's not clear it didn't do what the user asked.

While we may want to change the behavior around deploy/enhance if we fail for a domain in the future (what's being proposed is we continue execution so we work for subsequent domains), I don't think we should make any of those changes here as it requires some UI changes to make sure the domains we failed on is 100% clear and not hidden in the scrollback in the user's terminal. Currently if we fail on one, we exit with an error and rollback changes to all which is while arguably less useful, I think make things pretty clear to the user: your request to deploy/enhance the cert failed.

@bmw bmw added this to the 0.22.0 milestone Feb 28, 2018
Copy link
Member

@bmw bmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Thanks for turning this around so quickly @joohoi.

@bmw bmw merged commit e9bc4a3 into master Feb 28, 2018
@joohoi joohoi mentioned this pull request Mar 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants