From 1a5f09f4cfd2b5a520ec1944d986469d5e5838a8 Mon Sep 17 00:00:00 2001 From: Hugo Peixoto Date: Mon, 3 Oct 2016 19:53:53 +0100 Subject: [PATCH] First working iteration --- letsencrypt_route53/authenticator.py | 119 +++++++++++++++++---------- setup.py | 4 +- 2 files changed, 78 insertions(+), 45 deletions(-) diff --git a/letsencrypt_route53/authenticator.py b/letsencrypt_route53/authenticator.py index 95d24b0ab..95ade5916 100644 --- a/letsencrypt_route53/authenticator.py +++ b/letsencrypt_route53/authenticator.py @@ -1,23 +1,21 @@ """Route53 Let's Encrypt authenticator plugin.""" -import os import logging -import re -import subprocess +import time -import zope.component import zope.interface import boto3 from acme import challenges -from letsencrypt import errors from letsencrypt import interfaces from letsencrypt.plugins import common logger = logging.getLogger(__name__) +TTL = 30 + class Authenticator(common.Plugin): zope.interface.implements(interfaces.IAuthenticator) zope.interface.classProvides(interfaces.IPluginFactory) @@ -44,50 +42,85 @@ class Authenticator(common.Plugin): responses.append(self._perform_single(achall)) return responses + def _find_zone(self, r53, domain): + return max( + ( + zone for zone in r53.list_hosted_zones()["HostedZones"] + if (domain+".").endswith("."+zone["Name"]) + ), + key=lambda zone: len(zone["Name"]), + ) + + def _perform_single(self, achall): # provision the TXT record, using the domain name given. Assumes the hosted zone exits, else fails the challenge - response, validation = achall.response_and_validation() r53 = boto3.client('route53') - logger.info("Doing validation for " + response.domain) - listResponse = r53.list_hosted_zones_by_name(DNSName=response.domain) - matches = listResponse.HostedZones; - if matches.size != 0: - logger.error("Route53 returned " + mathces.size + " matching hosted zones. Expected exactly one. Auth canceled.") - return None - else: - r53.change_resource_record_sets(HostedZoneId=matches[0].Id, - ChangeBatch={ - 'Comment': 'Let\'s Entcrypt Change', - 'Changes': [ - { - 'Action': 'UPSERT', - 'ResourceRecordSet': { - 'Name': achall.validation_domain_name(), - 'Type': 'TXT', - 'TTL': 300, - 'ResourceRecords': [ - { - 'Value': validation - }, - ] - } - }, - ] - }) + logger.info("Doing validation for " + achall.domain) + listResponse = r53.list_hosted_zones_by_name(DNSName=achall.domain) - if response.simple_verify( - achall.chall, achall.domain, - achall.account_key.public_key(), self.config.http01_port): - return response - else: - logger.error( - "Self-verify of challenge failed, authorization abandoned!") + try: + zone = self._find_zone(r53, achall.domain) + except ValueError as e: + logger.error("Unable to find matching Route53 zone for domain " + achall.domain) return None + response, validation = achall.response_and_validation() + self._excute_r53_action(r53, achall, zone, validation, 'UPSERT', wait_for_change=True) + + for _ in xrange(TTL*2): + if response.simple_verify( + achall.chall, + achall.domain, + achall.account_key.public_key(), + ): + break + logger.info("Waiting for DNS propagation...") + time.sleep(1) + else: + logger.error("Unable to verify domain " + achall.domain) + return None + + return response + def cleanup(self, achalls): - # pylint: disable=missing-docstring,no-self-use,unused-argument - #TODO:Cleanup recordĀ  + # pylint: disable=missing-docstring r53 = boto3.client('route53') - #for achall in achalls: - # r53.delete_object(Bucket=self.conf('s3-bucket'), Key=achall.chall.path[1:]) + for achall in achalls: + try: + zone = self._find_zone(r53, achall.domain) + except ValueError: + logger.warn("Unable to find zone for " + achall.domain + ". Skipping cleanup.") + continue + + _, validation = achall.response_and_validation() + self._excute_r53_action(r53, achall, zone, validation, 'DELETE') return None + + + def _excute_r53_action(self, r53, achall, zone, validation, action, wait_for_change=False): + response = r53.change_resource_record_sets( + HostedZoneId=zone["Id"], + ChangeBatch={ + 'Comment': 'Let\'s Encrypt ' + action, + 'Changes': [ + { + 'Action': action, + 'ResourceRecordSet': { + 'Name': achall.validation_domain_name(achall.domain), + 'Type': 'TXT', + 'TTL': TTL, + 'ResourceRecords': [ + { + 'Value': '"' + validation + '"', + }, + ], + }, + }, + ], + }, + ) + + if wait_for_change: + while r53.get_change(Id=response["ChangeInfo"]["Id"])["ChangeInfo"]["Status"] == "PENDING": + logger.info("Waiting for " + action + " to propagate...") + time.sleep(1) diff --git a/setup.py b/setup.py index 95580ea15..172791aba 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ from setuptools import find_packages version = '0.1.3' install_requires = [ - 'acme>=0.1.1', - 'letsencrypt>=0.1.1', + 'acme>=0.9.0.dev0', + 'letsencrypt>=0.9.0.dev0', 'PyOpenSSL', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? 'setuptools', # pkg_resources