From d72792ff3702b81ed4260b187b0eb5da4898841c Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Wed, 13 Jun 2018 14:08:39 -0700 Subject: [PATCH] Fix unique dyn situation where zone does not match tld, and there's a deeper zone --- lemur/plugins/lemur_acme/dyn.py | 127 ++++++++++++++++++++++---------- requirements-dev.txt | 8 +- requirements-docs.txt | 15 ++-- requirements-tests.txt | 10 +-- requirements.in | 1 - requirements.txt | 7 +- 6 files changed, 110 insertions(+), 58 deletions(-) diff --git a/lemur/plugins/lemur_acme/dyn.py b/lemur/plugins/lemur_acme/dyn.py index 28832cbc..09a53884 100644 --- a/lemur/plugins/lemur_acme/dyn.py +++ b/lemur/plugins/lemur_acme/dyn.py @@ -5,11 +5,10 @@ import dns.exception import dns.name import dns.query import dns.resolver -from dyn.tm.errors import DynectCreateError +from dyn.tm.errors import DynectCreateError, DynectGetError from dyn.tm.session import DynectSession -from dyn.tm.zones import Node, Zone +from dyn.tm.zones import Node, Zone, get_all_zones from flask import current_app -from tld import get_tld def get_dynect_session(): @@ -51,19 +50,43 @@ def wait_for_dns_change(change_id, account_number=None): return +def get_zone_name(domain): + zones = get_all_zones() + + zone_name = "" + + for z in zones: + if domain.endswith(z.name): + # Find the most specific zone possible for the domain + # Ex: If fqdn is a.b.c.com, there is a zone for c.com, + # and a zone for b.c.com, we want to use b.c.com. + if z.name.count(".") > zone_name.count("."): + zone_name = z.name + if not zone_name: + raise Exception("No Dyn zone found for domain: {}".format(domain)) + return zone_name + + def create_txt_record(domain, token, account_number): get_dynect_session() - zone_name = get_tld('http://' + domain) + zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) zone = Zone(zone_name) try: - zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) - except DynectCreateError: - delete_txt_record(None, None, domain, token) - zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) - node = zone.get_node(node_name) + # Delete all stale ACME TXT records + delete_acme_txt_records(domain) + except DynectGetError as e: + if ( + "No such zone." in e.message or + "Host is not in this zone" in e.message or + "Host not found in this zone" in e.message + ): + current_app.logger.debug("Unable to delete ACME TXT records. They probably don't exist yet: {}".format(e)) + else: + raise + zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) zone.publish() current_app.logger.debug("TXT record created: {0}".format(fqdn)) change_id = (fqdn, token) @@ -76,7 +99,7 @@ def delete_txt_record(change_id, account_number, domain, token): current_app.logger.debug("delete_txt_record: No domain passed") return - zone_name = get_tld('http://' + domain) + zone_name = get_zone_name(domain) zone_parts = len(zone_name.split('.')) node_name = '.'.join(domain.split('.')[:-zone_parts]) fqdn = "{0}.{1}".format(node_name, zone_name) @@ -92,40 +115,70 @@ def delete_txt_record(change_id, account_number, domain, token): zone.publish() +def delete_acme_txt_records(domain): + get_dynect_session() + if not domain: + current_app.logger.debug("delete_acme_txt_records: No domain passed") + return + acme_challenge_string = "_acme-challenge" + if not domain.startswith(acme_challenge_string): + current_app.logger.debug( + "delete_acme_txt_records: Domain {} doesn't start with string {}. " + "Cowardly refusing to delete TXT records".format(domain, acme_challenge_string)) + return + + zone_name = get_zone_name(domain) + zone_parts = len(zone_name.split('.')) + node_name = '.'.join(domain.split('.')[:-zone_parts]) + fqdn = "{0}.{1}".format(node_name, zone_name) + + zone = Zone(zone_name) + node = Node(zone_name, fqdn) + + all_txt_records = node.get_all_records_by_type('TXT') + for txt_record in all_txt_records: + current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) + txt_record.delete() + zone.publish() + + def get_authoritative_nameserver(domain): - n = dns.name.from_text(domain) + if current_app.config.get('ACME_DYN_GET_AUTHORATATIVE_NAMESERVER'): + n = dns.name.from_text(domain) - depth = 2 - default = dns.resolver.get_default_resolver() - nameserver = default.nameservers[0] + depth = 2 + default = dns.resolver.get_default_resolver() + nameserver = default.nameservers[0] - last = False - while not last: - s = n.split(depth) + last = False + while not last: + s = n.split(depth) - last = s[0].to_unicode() == u'@' - sub = s[1] + last = s[0].to_unicode() == u'@' + sub = s[1] - query = dns.message.make_query(sub, dns.rdatatype.NS) - response = dns.query.udp(query, nameserver) + query = dns.message.make_query(sub, dns.rdatatype.NS) + response = dns.query.udp(query, nameserver) - rcode = response.rcode() - if rcode != dns.rcode.NOERROR: - if rcode == dns.rcode.NXDOMAIN: - raise Exception('%s does not exist.' % sub) + rcode = response.rcode() + if rcode != dns.rcode.NOERROR: + if rcode == dns.rcode.NXDOMAIN: + raise Exception('%s does not exist.' % sub) + else: + raise Exception('Error %s' % dns.rcode.to_text(rcode)) + + if len(response.authority) > 0: + rrset = response.authority[0] else: - raise Exception('Error %s' % dns.rcode.to_text(rcode)) + rrset = response.answer[0] - if len(response.authority) > 0: - rrset = response.authority[0] - else: - rrset = response.answer[0] + rr = rrset[0] + if rr.rdtype != dns.rdatatype.SOA: + authority = rr.target + nameserver = default.query(authority).rrset[0].to_text() - rr = rrset[0] - if rr.rdtype != dns.rdatatype.SOA: - authority = rr.target - nameserver = default.query(authority).rrset[0].to_text() + depth += 1 - depth += 1 - - return nameserver + return nameserver + else: + return "8.8.8.8" diff --git a/requirements-dev.txt b/requirements-dev.txt index 58499408..a66b38af 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,20 +11,20 @@ cfgv==1.1.0 # via pre-commit chardet==3.0.4 # via requests flake8==3.5.0 identify==1.1.0 # via pre-commit -idna==2.6 # via requests +idna==2.7 # via requests invoke==1.0.0 mccabe==0.6.1 # via flake8 -nodeenv==1.3.0 +nodeenv==1.3.1 pkginfo==1.4.2 # via twine pre-commit==1.10.2 pycodestyle==2.3.1 # via flake8 pyflakes==1.6.0 # via flake8 pyyaml==3.12 # via aspy.yaml, pre-commit requests-toolbelt==0.8.0 # via twine -requests==2.18.4 # via requests-toolbelt, twine +requests==2.19.0 # via requests-toolbelt, twine six==1.11.0 # via cfgv, pre-commit toml==0.9.4 # via pre-commit tqdm==4.23.4 # via twine twine==1.11.0 -urllib3==1.22 # via requests +urllib3==1.23 # via requests virtualenv==16.0.0 # via pre-commit diff --git a/requirements-docs.txt b/requirements-docs.txt index 8795a511..58646db7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -4,7 +4,7 @@ # # pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in # -acme==0.24.0 +acme==0.25.0 alabaster==0.7.10 # via sphinx alembic-autogenerate-enums==0.0.2 alembic==0.9.9 @@ -15,8 +15,8 @@ asyncpool==1.0 babel==2.6.0 # via sphinx bcrypt==3.1.4 blinker==1.4 -boto3==1.7.32 -botocore==1.10.32 +boto3==1.7.36 +botocore==1.10.36 certifi==2018.4.16 cffi==1.11.5 click==6.7 @@ -27,7 +27,7 @@ dnspython==1.15.0 docutils==0.14 dyn==1.8.1 flask-bcrypt==0.7.1 -flask-cors==3.0.4 +flask-cors==3.0.6 flask-mail==0.9.1 flask-migrate==2.1.1 flask-principal==0.4.0 @@ -37,7 +37,7 @@ flask-sqlalchemy==2.3.2 flask==0.12 future==0.16.0 gunicorn==19.8.1 -idna==2.6 +idna==2.7 imagesize==1.0.0 # via sphinx inflection==0.3.1 itsdangerous==0.24 @@ -54,7 +54,7 @@ mock==2.0.0 ndg-httpsclient==0.5.0 packaging==17.1 # via sphinx paramiko==2.4.1 -pbr==4.0.3 +pbr==4.0.4 pem==17.1.0 psycopg2==2.7.4 pyasn1-modules==0.2.1 @@ -65,12 +65,13 @@ pyjwt==1.6.4 pynacl==1.2.1 pyopenssl==17.2.0 pyparsing==2.2.0 # via packaging -pyrfc3339==1.0 +pyrfc3339==1.1 python-dateutil==2.7.3 python-editor==1.0.3 pytz==2018.4 pyyaml==3.12 raven[flask]==6.9.0 +requests-toolbelt==0.8.0 requests[security]==2.11.1 retrying==1.3.3 s3transfer==0.1.13 diff --git a/requirements-tests.txt b/requirements-tests.txt index efcfeac3..ccfad67f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography atomicwrites==1.1.5 # via pytest attrs==18.1.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.7.36 # via moto +boto3==1.7.37 # via moto boto==2.48.0 # via moto -botocore==1.10.36 # via boto3, moto, s3transfer +botocore==1.10.37 # via boto3, moto, s3transfer certifi==2018.4.16 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -25,7 +25,7 @@ factory-boy==2.11.1 faker==0.8.15 flask==1.0.2 # via pytest-flask freezegun==0.3.10 -idna==2.6 # via cryptography, requests +idna==2.7 # via cryptography, requests itsdangerous==0.24 # via flask jinja2==2.10 # via flask, moto jmespath==0.9.3 # via boto3, botocore @@ -49,12 +49,12 @@ python-dateutil==2.6.1 # via botocore, faker, freezegun, moto pytz==2018.4 # via moto pyyaml==3.12 # via pyaml requests-mock==1.5.0 -requests==2.18.4 # via aws-xray-sdk, docker, moto, requests-mock, responses +requests==2.19.0 # via aws-xray-sdk, docker, moto, requests-mock, responses responses==0.9.0 # via moto s3transfer==0.1.13 # via boto3 six==1.11.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, more-itertools, moto, pytest, python-dateutil, requests-mock, responses, websocket-client text-unidecode==1.2 # via faker -urllib3==1.22 # via requests +urllib3==1.23 # via requests websocket-client==0.48.0 # via docker werkzeug==0.14.1 # via flask, moto, pytest-flask wrapt==1.10.11 # via aws-xray-sdk diff --git a/requirements.in b/requirements.in index 4d287c0d..2a5051a7 100644 --- a/requirements.in +++ b/requirements.in @@ -39,5 +39,4 @@ retrying six SQLAlchemy-Utils tabulate -tld xmltodict diff --git a/requirements.txt b/requirements.txt index 712a88f6..d10c92ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile --no-index --output-file requirements.txt requirements.in # -acme==0.25.0 +acme==0.25.1 alembic-autogenerate-enums==0.0.2 alembic==0.9.9 # via flask-migrate aniso8601==3.0.0 # via flask-restful @@ -13,8 +13,8 @@ asn1crypto==0.24.0 # via cryptography asyncpool==1.0 bcrypt==3.1.4 # via flask-bcrypt, paramiko blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.7.36 -botocore==1.10.36 # via boto3, s3transfer +boto3==1.7.37 +botocore==1.10.37 # via boto3, s3transfer certifi==2018.4.16 cffi==1.11.5 # via bcrypt, cryptography, pynacl click==6.7 # via flask @@ -74,6 +74,5 @@ six==1.11.0 sqlalchemy-utils==0.33.3 sqlalchemy==1.2.8 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils tabulate==0.8.2 -tld==0.7.10 werkzeug==0.14.1 # via flask xmltodict==0.11.0