|
|
|
@ -5,26 +5,31 @@ |
|
|
|
from __future__ import absolute_import, division, print_function, \ |
|
|
|
unicode_literals |
|
|
|
|
|
|
|
import re |
|
|
|
|
|
|
|
from requests import HTTPError, Session |
|
|
|
from logging import getLogger |
|
|
|
|
|
|
|
from ..record import Create, Record |
|
|
|
from ..record import Record |
|
|
|
from .base import BaseProvider |
|
|
|
|
|
|
|
import re |
|
|
|
|
|
|
|
import sys |
|
|
|
|
|
|
|
def add_trailing_dot(s): |
|
|
|
assert s |
|
|
|
assert s[-1] != '.' |
|
|
|
return s + '.' |
|
|
|
def add_trailing_dot(value): |
|
|
|
''' |
|
|
|
Add trailing dots to values |
|
|
|
''' |
|
|
|
assert value |
|
|
|
assert value[-1] != '.' |
|
|
|
return value + '.' |
|
|
|
|
|
|
|
|
|
|
|
def remove_trailing_dot(s): |
|
|
|
assert s |
|
|
|
assert s[-1] == '.' |
|
|
|
return s[:-1] |
|
|
|
def remove_trailing_dot(value): |
|
|
|
''' |
|
|
|
Remove trailing dots from values |
|
|
|
''' |
|
|
|
assert value |
|
|
|
assert value[-1] == '.' |
|
|
|
return value[:-1] |
|
|
|
|
|
|
|
|
|
|
|
class MythicBeastsProvider(BaseProvider): |
|
|
|
@ -35,7 +40,6 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
class: octodns.provider.mythicbeasts.MythicBeastsProvider |
|
|
|
zones: |
|
|
|
my-zone: 'password' |
|
|
|
|
|
|
|
''' |
|
|
|
|
|
|
|
SUPPORTS_GEO = False |
|
|
|
@ -45,10 +49,13 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
BASE = 'https://dnsapi.mythic-beasts.com/' |
|
|
|
TIMEOUT = 15 |
|
|
|
|
|
|
|
def __init__(self, id, passwords, *args, **kwargs): |
|
|
|
self.log = getLogger('MythicBeastsProvider[{}]'.format(id)) |
|
|
|
self.log.debug('__init__: id=%s, registered zones; %s', id, passwords.keys()) |
|
|
|
super(MythicBeastsProvider, self).__init__(id, *args, **kwargs) |
|
|
|
def __init__(self, identifier, passwords, *args, **kwargs): |
|
|
|
self.log = getLogger('MythicBeastsProvider[{}]'.format(identifier)) |
|
|
|
self.log.debug( |
|
|
|
'__init__: id=%s, registered zones; %s', |
|
|
|
identifier, |
|
|
|
passwords.keys()) |
|
|
|
super(MythicBeastsProvider, self).__init__(identifier, *args, **kwargs) |
|
|
|
|
|
|
|
self._passwords = passwords |
|
|
|
sess = Session() |
|
|
|
@ -59,7 +66,10 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
|
|
|
|
url = self.BASE |
|
|
|
resp = self._sess.request(method, url, data=data, timeout=self.TIMEOUT) |
|
|
|
self.log.debug('_request: status=%d data=%s', resp.status_code, resp.text) |
|
|
|
self.log.debug( |
|
|
|
'_request: status=%d data=%s', |
|
|
|
resp.status_code, |
|
|
|
resp.text) |
|
|
|
if resp.status_code != 200: |
|
|
|
self.log.info('request failed: %s, response %s', data, resp.text) |
|
|
|
resp.raise_for_status() |
|
|
|
@ -76,26 +86,32 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
'command': 'LIST', |
|
|
|
}) |
|
|
|
|
|
|
|
def _data_for_single(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_single(_type, data): |
|
|
|
return { |
|
|
|
'type': _type, |
|
|
|
'value': data['raw_values'][0]['value'], |
|
|
|
'ttl': data['raw_values'][0]['ttl'] |
|
|
|
} |
|
|
|
|
|
|
|
def _data_for_multiple(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_multiple(_type, data): |
|
|
|
return { |
|
|
|
'type': _type, |
|
|
|
'values': [raw_values['value'] for raw_values in data['raw_values']], |
|
|
|
'ttl': max([raw_values['ttl'] for raw_values in data['raw_values']]), |
|
|
|
'values': |
|
|
|
[raw_values['value'] for raw_values in data['raw_values']], |
|
|
|
'ttl': |
|
|
|
max([raw_values['ttl'] for raw_values in data['raw_values']]), |
|
|
|
} |
|
|
|
|
|
|
|
def _data_for_MX(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_MX(_type, data): |
|
|
|
ttl = max([raw_values['ttl'] for raw_values in data['raw_values']]) |
|
|
|
values = [] |
|
|
|
|
|
|
|
for raw_value in [raw_values['value'] for raw_values in data['raw_values']]: |
|
|
|
match = re.match('^([0-9]+)\s+(\S+)$', raw_value, re.IGNORECASE) |
|
|
|
for raw_value in \ |
|
|
|
[raw_values['value'] for raw_values in data['raw_values']]: |
|
|
|
match = re.match('^([0-9]+)\\s+(\\S+)$', raw_value, re.IGNORECASE) |
|
|
|
|
|
|
|
if match is not None: |
|
|
|
exchange = match.group(2) |
|
|
|
@ -114,25 +130,38 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
'ttl': ttl, |
|
|
|
} |
|
|
|
|
|
|
|
def _data_for_CNAME(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_CNAME(_type, data): |
|
|
|
ttl = data['raw_values'][0]['ttl'] |
|
|
|
value = data['raw_values'][0]['value'] |
|
|
|
if not value.endswith('.'): |
|
|
|
value = '{}.{}'.format(value, data['zone']) |
|
|
|
|
|
|
|
return self._data_for_single(_type, {'raw_values': [ {'value': value, 'ttl': ttl} ]}) |
|
|
|
return MythicBeastsProvider._data_for_single( |
|
|
|
_type, |
|
|
|
{'raw_values': [ |
|
|
|
{'value': value, 'ttl': ttl} |
|
|
|
]}) |
|
|
|
|
|
|
|
def _data_for_ANAME(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_ANAME(_type, data): |
|
|
|
ttl = data['raw_values'][0]['ttl'] |
|
|
|
value = data['raw_values'][0]['value'] |
|
|
|
return self._data_for_single('ALIAS', {'raw_values': [ {'value': value, 'ttl': ttl} ]}) |
|
|
|
|
|
|
|
|
|
|
|
def _data_for_SRV(self, _type, data): |
|
|
|
return MythicBeastsProvider._data_for_single( |
|
|
|
'ALIAS', |
|
|
|
{'raw_values': [ |
|
|
|
{'value': value, 'ttl': ttl} |
|
|
|
]}) |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _data_for_SRV(_type, data): |
|
|
|
ttl = data['raw_values'][0]['ttl'] |
|
|
|
raw_value = data['raw_values'][0]['value'] |
|
|
|
|
|
|
|
match = re.match('^([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+(\S+)$', raw_value, re.IGNORECASE) |
|
|
|
match = re.match( |
|
|
|
'^([0-9]+)\\s+([0-9]+)\\s+([0-9]+)\\s+(\\S+)$', |
|
|
|
raw_value, |
|
|
|
re.IGNORECASE) |
|
|
|
|
|
|
|
if match is not None: |
|
|
|
target = match.group(4) |
|
|
|
@ -146,13 +175,21 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
'target': target, |
|
|
|
} |
|
|
|
|
|
|
|
return self._data_for_single('SRV', {'raw_values': [ {'value': value, 'ttl': ttl} ]}) |
|
|
|
return MythicBeastsProvider._data_for_single( |
|
|
|
'SRV', |
|
|
|
{'raw_values': [ |
|
|
|
{'value': value, 'ttl': ttl} |
|
|
|
]}) |
|
|
|
|
|
|
|
def _data_for_SSHFP(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_SSHFP(_type, data): |
|
|
|
ttl = data['raw_values'][0]['ttl'] |
|
|
|
raw_value = data['raw_values'][0]['value'] |
|
|
|
|
|
|
|
match = re.match('^([0-9]+)\s+([0-9]+)\s+(\S+)$', raw_value, re.IGNORECASE) |
|
|
|
match = re.match( |
|
|
|
'^([0-9]+)\\s+([0-9]+)\\s+(\\S+)$', |
|
|
|
raw_value, |
|
|
|
re.IGNORECASE) |
|
|
|
|
|
|
|
if match is not None: |
|
|
|
value = { |
|
|
|
@ -161,14 +198,21 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
'fingerprint': match.group(3), |
|
|
|
} |
|
|
|
|
|
|
|
return self._data_for_single('SSHFP', {'raw_values': [ {'value': value, 'ttl': ttl} ]}) |
|
|
|
|
|
|
|
return MythicBeastsProvider._data_for_single( |
|
|
|
'SSHFP', |
|
|
|
{'raw_values': [ |
|
|
|
{'value': value, 'ttl': ttl} |
|
|
|
]}) |
|
|
|
|
|
|
|
def _data_for_CAA(self, _type, data): |
|
|
|
@staticmethod |
|
|
|
def _data_for_CAA(_type, data): |
|
|
|
ttl = data['raw_values'][0]['ttl'] |
|
|
|
raw_value = data['raw_values'][0]['value'] |
|
|
|
|
|
|
|
match = re.match('^([0-9]+)\s+(issue|issuewild|iodef)\s+(\S+)$', raw_value, re.IGNORECASE) |
|
|
|
match = re.match( |
|
|
|
'^([0-9]+)\\s+(issue|issuewild|iodef)\\s+(\\S+)$', |
|
|
|
raw_value, |
|
|
|
re.IGNORECASE) |
|
|
|
|
|
|
|
if match is not None: |
|
|
|
value = { |
|
|
|
@ -176,15 +220,17 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
'tag': match.group(2), |
|
|
|
'value': match.group(3), |
|
|
|
} |
|
|
|
return self._data_for_single('CAA', {'raw_values': [ {'value': value, 'ttl': ttl} ]}) |
|
|
|
|
|
|
|
return MythicBeastsProvider._data_for_single( |
|
|
|
'CAA', |
|
|
|
{'raw_values': [ |
|
|
|
{'value': value, 'ttl': ttl} |
|
|
|
]}) |
|
|
|
|
|
|
|
_data_for_NS = _data_for_multiple |
|
|
|
_data_for_TXT = _data_for_multiple |
|
|
|
_data_for_A = _data_for_multiple |
|
|
|
_data_for_AAAA = _data_for_multiple |
|
|
|
|
|
|
|
|
|
|
|
def populate(self, zone, target=False, lenient=False): |
|
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, |
|
|
|
target, lenient) |
|
|
|
@ -192,15 +238,12 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
resp = None |
|
|
|
try: |
|
|
|
resp = self.records(zone.name) |
|
|
|
except HTTPError as e: |
|
|
|
if e.response.status_code == 401: |
|
|
|
except HTTPError as err: |
|
|
|
if err.response.status_code == 401: |
|
|
|
# Nicer error message for auth problems |
|
|
|
raise Exception('Mythic Beasts authentication problem with {}'.format(zone.name)) |
|
|
|
elif e.response.status_code == 422: |
|
|
|
# 422 means mythicbeasts doesn't know anything about the requested |
|
|
|
# domain. We'll just ignore it here and leave the zone |
|
|
|
# untouched. |
|
|
|
raise |
|
|
|
raise Exception( |
|
|
|
'Mythic Beasts authentication problem with {}'.format( |
|
|
|
zone.name)) |
|
|
|
else: |
|
|
|
# just re-throw |
|
|
|
raise |
|
|
|
@ -212,7 +255,10 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
if resp: |
|
|
|
exists = True |
|
|
|
for line in resp.content.splitlines(): |
|
|
|
match = re.match('^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$', line, re.IGNORECASE) |
|
|
|
match = re.match( |
|
|
|
'^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(.*)$', |
|
|
|
line, |
|
|
|
re.IGNORECASE) |
|
|
|
|
|
|
|
if match is not None: |
|
|
|
if match.group(1) == '@': |
|
|
|
@ -244,16 +290,18 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
data[_type][_name].get('raw_values').append( |
|
|
|
{'value': _value, 'ttl': _ttl} |
|
|
|
) |
|
|
|
except AttributeError as error: |
|
|
|
except AttributeError: |
|
|
|
self.log.debug('skipping {} as not supported', _type) |
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
for _type in data: |
|
|
|
for _name in data[_type]: |
|
|
|
data_for = getattr(self, '_data_for_{}'.format(_type)) |
|
|
|
|
|
|
|
self.log.debug('record: {}, {}'.format(_type, data[_type][_name])) |
|
|
|
self.log.debug( |
|
|
|
'record: %s,\t%s', |
|
|
|
_type, |
|
|
|
data[_type][_name]) |
|
|
|
|
|
|
|
record = Record.new( |
|
|
|
zone, |
|
|
|
@ -263,12 +311,10 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
) |
|
|
|
zone.add_record(record, lenient=lenient) |
|
|
|
|
|
|
|
|
|
|
|
self.log.debug('populate: found %s records, exists=%s', |
|
|
|
len(zone.records) - before, exists) |
|
|
|
len(zone.records) - before, exists) |
|
|
|
return exists |
|
|
|
|
|
|
|
|
|
|
|
def _compile_commands(self, action, change): |
|
|
|
commands = [] |
|
|
|
|
|
|
|
@ -280,7 +326,6 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
elif action == 'DELETE': |
|
|
|
record = change.existing |
|
|
|
|
|
|
|
zone = record.zone |
|
|
|
hostname = remove_trailing_dot(record.fqdn) |
|
|
|
ttl = record.ttl |
|
|
|
_type = record._type |
|
|
|
@ -288,13 +333,11 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
if _type == 'ALIAS': |
|
|
|
_type = 'ANAME' |
|
|
|
|
|
|
|
|
|
|
|
if hasattr(record, 'values'): |
|
|
|
values = record.values |
|
|
|
else: |
|
|
|
values = [record.value] |
|
|
|
|
|
|
|
|
|
|
|
base = '{} {} {} {}'.format(action, hostname, ttl, _type) |
|
|
|
|
|
|
|
if re.match('[A]{1,4}', _type) is not None: |
|
|
|
@ -304,25 +347,35 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
elif _type == 'SSHFP': |
|
|
|
data = values[0].data |
|
|
|
commands.append('{} {} {} {}'.format( |
|
|
|
base, data['algorithm'], data['fingerprint_type'], data['fingerprint'])) |
|
|
|
base, |
|
|
|
data['algorithm'], |
|
|
|
data['fingerprint_type'], |
|
|
|
data['fingerprint'] |
|
|
|
)) |
|
|
|
|
|
|
|
elif _type == 'SRV': |
|
|
|
data = values[0].data |
|
|
|
commands.append('{} {} {} {} {}'.format( |
|
|
|
base, data['priority'], data['weight'], data['port'], data['target'])) |
|
|
|
|
|
|
|
base, |
|
|
|
data['priority'], |
|
|
|
data['weight'], |
|
|
|
data['port'], |
|
|
|
data['target'])) |
|
|
|
|
|
|
|
elif _type == 'MX': |
|
|
|
for value in values: |
|
|
|
data = value.data |
|
|
|
commands.append('{} {} {}'.format( |
|
|
|
base, data['preference'], data['exchange'])) |
|
|
|
base, |
|
|
|
data['preference'], |
|
|
|
data['exchange'])) |
|
|
|
|
|
|
|
else: |
|
|
|
try: |
|
|
|
if getattr(self, '_data_for_{}'.format(_type)) is not None: |
|
|
|
commands.append('{} {}'.format( |
|
|
|
base, values[0])) |
|
|
|
except AttributeError as error: |
|
|
|
base, values[0])) |
|
|
|
except AttributeError: |
|
|
|
self.log.debug('skipping {} as not supported', _type) |
|
|
|
pass |
|
|
|
|
|
|
|
@ -364,11 +417,6 @@ class MythicBeastsProvider(BaseProvider): |
|
|
|
self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, |
|
|
|
len(changes)) |
|
|
|
|
|
|
|
domain_name = desired.name |
|
|
|
|
|
|
|
for change in changes: |
|
|
|
class_name = change.__class__.__name__ |
|
|
|
getattr(self, '_apply_{}'.format(class_name))(change) |
|
|
|
|
|
|
|
|
|
|
|
|