mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
[fix] Clean madness related to DynDNS (#353)
* Add a function to check if a dyndns provider provides a domain * Add a function to check a domain availability from a dyndns provider * Use new functions in dyndns_subscribe * Replace complete madness by _dyndns_available in dyndns_update * This regex is only used in dyndns_update, no need to define it at the whole file scope level * Try to clarify management of old ipv4/ipv6 ... * Add a nice helper to get both ipv4 and ipv6... * Clarify the dyndns_update madness to get current ipv4/v6 * Remove now useless IPRouteLine * Change default values of old ipv4/v6 to None, otherwise shit gets update just because IPv6 = None * Rearrange thing a bit, move path to global variable * Copypasta typo * Dyndns zone file as a global variable * Use helper to write zone content to file * Adding some debugs/info messages * Move the domain guessing to a dedicated function... * Adding comments.. * Using subprocess check_call instead of os.system for nsupdate * Removing dump of the zone update because it's kinda duplicated from what nsupdate already does * Ignore error if old_ipvx file is non existent * Add docstring for _dyndns_available * Remove parenthesis otherwise this gonna displease Bram-sama :P * Start working on postinstall .. use _dyndns_provides to check if domain is a .nohost.me or .nohost.st * Use _dyndns_available to cleanly check for domain availability * Forget about the weird 'domain split' check... * Clean dyndns stuff in domain.py * Missing argument for string
This commit is contained in:
parent
0086c8c16a
commit
5ae558edc9
4 changed files with 198 additions and 124 deletions
|
@ -158,6 +158,7 @@
|
|||
"domains_available": "Available domains:",
|
||||
"done": "Done",
|
||||
"downloading": "Downloading...",
|
||||
"dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.",
|
||||
"dyndns_cron_installed": "The DynDNS cron job has been installed",
|
||||
"dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
|
||||
"dyndns_cron_removed": "The DynDNS cron job has been removed",
|
||||
|
@ -168,7 +169,8 @@
|
|||
"dyndns_no_domain_registered": "No domain has been registered with DynDNS",
|
||||
"dyndns_registered": "The DynDNS domain has been registered",
|
||||
"dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}",
|
||||
"dyndns_unavailable": "Unavailable DynDNS subdomain",
|
||||
"dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.",
|
||||
"dyndns_unavailable": "Domain {domain:s} is not available.",
|
||||
"executing_command": "Executing command '{command:s}'...",
|
||||
"executing_script": "Executing script '{script:s}'...",
|
||||
"extracting": "Extracting...",
|
||||
|
|
|
@ -82,28 +82,23 @@ def domain_add(auth, domain, dyndns=False):
|
|||
|
||||
# DynDNS domain
|
||||
if dyndns:
|
||||
if len(domain.split('.')) < 3:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid'))
|
||||
|
||||
# Do not allow to subscribe to multiple dyndns domains...
|
||||
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
|
||||
raise MoulinetteError(errno.EPERM,
|
||||
m18n.n('domain_dyndns_already_subscribed'))
|
||||
try:
|
||||
r = requests.get('https://dyndns.yunohost.org/domains', timeout=30)
|
||||
except requests.ConnectionError as e:
|
||||
raise MoulinetteError(errno.EHOSTUNREACH,
|
||||
m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e)))
|
||||
|
||||
from yunohost.dyndns import dyndns_subscribe
|
||||
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
|
||||
|
||||
dyndomains = json.loads(r.text)
|
||||
dyndomain = '.'.join(domain.split('.')[1:])
|
||||
if dyndomain in dyndomains:
|
||||
dyndns_subscribe(domain=domain)
|
||||
else:
|
||||
# Check that this domain can effectively be provided by
|
||||
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
|
||||
if not _dyndns_provides("dyndns.yunohost.org", domain):
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.n('domain_dyndns_root_unknown'))
|
||||
|
||||
# Actually subscribe
|
||||
dyndns_subscribe(domain=domain)
|
||||
|
||||
try:
|
||||
yunohost.certificate._certificate_install_selfsigned([domain], False)
|
||||
|
||||
|
@ -281,6 +276,25 @@ def get_public_ip(protocol=4):
|
|||
raise MoulinetteError(errno.ENETUNREACH,
|
||||
m18n.n('no_internet_connection'))
|
||||
|
||||
def get_public_ips():
|
||||
"""
|
||||
Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org
|
||||
|
||||
Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not
|
||||
found.
|
||||
"""
|
||||
|
||||
try:
|
||||
ipv4 = get_public_ip()
|
||||
except:
|
||||
ipv4 = None
|
||||
try:
|
||||
ipv6 = get_public_ip(6)
|
||||
except:
|
||||
ipv6 = None
|
||||
|
||||
return (ipv4, ipv6)
|
||||
|
||||
|
||||
def _get_maindomain():
|
||||
with open('/etc/yunohost/current_host', 'r') as f:
|
||||
|
|
|
@ -35,37 +35,72 @@ import subprocess
|
|||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.utils.log import getActionLogger
|
||||
from moulinette.utils.filesystem import read_file, write_to_file, rm
|
||||
from moulinette.utils.network import download_json
|
||||
|
||||
from yunohost.domain import get_public_ip, _get_maindomain, _build_dns_conf
|
||||
from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf
|
||||
|
||||
logger = getActionLogger('yunohost.dyndns')
|
||||
|
||||
OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
|
||||
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
|
||||
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
|
||||
|
||||
class IPRouteLine(object):
|
||||
""" Utility class to parse an ip route output line
|
||||
|
||||
The output of ip ro is variable and hard to parse completly, it would
|
||||
require a real parser, not just a regexp, so do minimal parsing here...
|
||||
|
||||
>>> a = IPRouteLine('2001:: from :: via fe80::c23f:fe:1e:cafe dev eth0 src 2000:de:beef:ca:0:fe:1e:cafe metric 0')
|
||||
>>> a.src_addr
|
||||
"2000:de:beef:ca:0:fe:1e:cafe"
|
||||
def _dyndns_provides(provider, domain):
|
||||
"""
|
||||
regexp = re.compile(
|
||||
r'(?P<unreachable>unreachable)?.*src\s+(?P<src_addr>[0-9a-f:]+).*')
|
||||
Checks if a provider provide/manage a given domain.
|
||||
|
||||
def __init__(self, line):
|
||||
self.m = self.regexp.match(line)
|
||||
if not self.m:
|
||||
raise ValueError("Not a valid ip route get line")
|
||||
Keyword arguments:
|
||||
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
|
||||
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
|
||||
|
||||
# make regexp group available as object attributes
|
||||
for k, v in self.m.groupdict().items():
|
||||
setattr(self, k, v)
|
||||
Returns:
|
||||
True if the provider provide/manages the domain. False otherwise.
|
||||
"""
|
||||
|
||||
re_dyndns_private_key = re.compile(
|
||||
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
|
||||
)
|
||||
logger.debug("Checking if %s is managed by %s ..." % (domain, provider))
|
||||
|
||||
try:
|
||||
# Dyndomains will be a list of domains supported by the provider
|
||||
# e.g. [ "nohost.me", "noho.st" ]
|
||||
dyndomains = download_json('https://%s/domains' % provider, timeout=30)
|
||||
except MoulinetteError as e:
|
||||
logger.error(str(e))
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.n('dyndns_could_not_check_provide',
|
||||
domain=domain, provider=provider))
|
||||
|
||||
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
|
||||
dyndomain = '.'.join(domain.split('.')[1:])
|
||||
|
||||
return dyndomain in dyndomains
|
||||
|
||||
|
||||
def _dyndns_available(provider, domain):
|
||||
"""
|
||||
Checks if a domain is available from a given provider.
|
||||
|
||||
Keyword arguments:
|
||||
provider -- The url of the provider, e.g. "dyndns.yunohost.org"
|
||||
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
|
||||
|
||||
Returns:
|
||||
True if the domain is avaible, False otherwise.
|
||||
"""
|
||||
logger.debug("Checking if domain %s is available on %s ..."
|
||||
% (domain, provider))
|
||||
|
||||
try:
|
||||
r = download_json('https://%s/test/%s' % (provider, domain),
|
||||
expected_status_code=None)
|
||||
except MoulinetteError as e:
|
||||
logger.error(str(e))
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.n('dyndns_could_not_check_available',
|
||||
domain=domain, provider=provider))
|
||||
|
||||
return r == u"Domain %s is available" % domain
|
||||
|
||||
|
||||
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
|
||||
|
@ -81,12 +116,16 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None
|
|||
if domain is None:
|
||||
domain = _get_maindomain()
|
||||
|
||||
# Verify if domain is provided by subscribe_host
|
||||
if not _dyndns_provides(subscribe_host, domain):
|
||||
raise MoulinetteError(errno.ENOENT,
|
||||
m18n.n('dyndns_domain_not_provided',
|
||||
domain=domain, provider=subscribe_host))
|
||||
|
||||
# Verify if domain is available
|
||||
try:
|
||||
if requests.get('https://%s/test/%s' % (subscribe_host, domain)).status_code != 200:
|
||||
raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable'))
|
||||
except requests.ConnectionError:
|
||||
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
|
||||
if not _dyndns_available(subscribe_host, domain):
|
||||
raise MoulinetteError(errno.ENOENT,
|
||||
m18n.n('dyndns_unavailable', domain=domain))
|
||||
|
||||
if key is None:
|
||||
if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0:
|
||||
|
@ -133,73 +172,40 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
|
|||
ipv6 -- IPv6 address to send
|
||||
|
||||
"""
|
||||
# IPv4
|
||||
# Get old ipv4/v6
|
||||
|
||||
old_ipv4, old_ipv6 = (None, None) # (default values)
|
||||
|
||||
if os.path.isfile(OLD_IPV4_FILE):
|
||||
old_ipv4 = read_file(OLD_IPV4_FILE).rstrip()
|
||||
|
||||
if os.path.isfile(OLD_IPV6_FILE):
|
||||
old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
|
||||
|
||||
# Get current IPv4 and IPv6
|
||||
(ipv4_, ipv6_) = get_public_ips()
|
||||
|
||||
if ipv4 is None:
|
||||
ipv4 = get_public_ip()
|
||||
ipv4 = ipv4_
|
||||
|
||||
try:
|
||||
with open('/etc/yunohost/dyndns/old_ip', 'r') as f:
|
||||
old_ip = f.readline().rstrip()
|
||||
except IOError:
|
||||
old_ip = '0.0.0.0'
|
||||
|
||||
# IPv6
|
||||
if ipv6 is None:
|
||||
try:
|
||||
ip_route_out = subprocess.check_output(
|
||||
['ip', 'route', 'get', '2000::']).split('\n')
|
||||
ipv6 = ipv6_
|
||||
|
||||
if len(ip_route_out) > 0:
|
||||
route = IPRouteLine(ip_route_out[0])
|
||||
if not route.unreachable:
|
||||
ipv6 = route.src_addr
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
# Unlikely case "ip route" does not return status 0
|
||||
# or produces unexpected output
|
||||
raise MoulinetteError(errno.EBADMSG,
|
||||
"ip route cmd error : {}".format(e))
|
||||
|
||||
if ipv6 is None:
|
||||
logger.info(m18n.n('no_ipv6_connectivity'))
|
||||
|
||||
try:
|
||||
with open('/etc/yunohost/dyndns/old_ipv6', 'r') as f:
|
||||
old_ipv6 = f.readline().rstrip()
|
||||
except IOError:
|
||||
old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||
logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
|
||||
logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
|
||||
|
||||
# no need to update
|
||||
if old_ip == ipv4 and old_ipv6 == ipv6:
|
||||
if old_ipv4 == ipv4 and old_ipv6 == ipv6:
|
||||
logger.info("No updated needed.")
|
||||
return
|
||||
else:
|
||||
logger.info("Updated needed, going on...")
|
||||
|
||||
# If domain is not given, try to guess it from keys available...
|
||||
if domain is None:
|
||||
# Retrieve the first registered domain
|
||||
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
|
||||
match = re_dyndns_private_key.match(path)
|
||||
if not match:
|
||||
continue
|
||||
_domain = match.group('domain')
|
||||
|
||||
try:
|
||||
# Check if domain is registered
|
||||
request_url = 'https://{0}/test/{1}'.format(dyn_host, _domain)
|
||||
if requests.get(request_url, timeout=30).status_code == 200:
|
||||
continue
|
||||
except requests.ConnectionError:
|
||||
raise MoulinetteError(errno.ENETUNREACH,
|
||||
m18n.n('no_internet_connection'))
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning("Correction timed out on {}, skip it".format(
|
||||
request_url))
|
||||
domain = _domain
|
||||
key = path
|
||||
break
|
||||
if not domain:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.n('dyndns_no_domain_registered'))
|
||||
|
||||
if key is None:
|
||||
(domain, key) = _guess_current_dyndns_domain(dyn_host)
|
||||
# If key is not given, pick the first file we find with the domain given
|
||||
elif key is None:
|
||||
keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain))
|
||||
|
||||
if not keys:
|
||||
|
@ -207,9 +213,12 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
|
|||
|
||||
key = keys[0]
|
||||
|
||||
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
|
||||
host = domain.split('.')[1:]
|
||||
host = '.'.join(host)
|
||||
|
||||
logger.debug("Building zone update file ...")
|
||||
|
||||
lines = [
|
||||
'server %s' % dyn_host,
|
||||
'zone %s' % host,
|
||||
|
@ -246,21 +255,27 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
|
|||
'send'
|
||||
]
|
||||
|
||||
with open('/etc/yunohost/dyndns/zone', 'w') as zone:
|
||||
zone.write('\n'.join(lines))
|
||||
# Write the actions to do to update to a file, to be able to pass it
|
||||
# to nsupdate as argument
|
||||
write_to_file(DYNDNS_ZONE, '\n'.join(lines))
|
||||
|
||||
if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) != 0:
|
||||
os.system('rm -f /etc/yunohost/dyndns/old_ip')
|
||||
os.system('rm -f /etc/yunohost/dyndns/old_ipv6')
|
||||
logger.info("Now pushing new conf to DynDNS host...")
|
||||
|
||||
try:
|
||||
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
|
||||
subprocess.check_call(command)
|
||||
except subprocess.CalledProcessError:
|
||||
rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent)
|
||||
rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent)
|
||||
raise MoulinetteError(errno.EPERM,
|
||||
m18n.n('dyndns_ip_update_failed'))
|
||||
|
||||
logger.success(m18n.n('dyndns_ip_updated'))
|
||||
with open('/etc/yunohost/dyndns/old_ip', 'w') as f:
|
||||
f.write(ipv4)
|
||||
|
||||
if ipv4 is not None:
|
||||
write_to_file(OLD_IPV4_FILE, ipv4)
|
||||
if ipv6 is not None:
|
||||
with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f:
|
||||
f.write(ipv6)
|
||||
write_to_file(OLD_IPV6_FILE, ipv6)
|
||||
|
||||
|
||||
def dyndns_installcron():
|
||||
|
@ -287,3 +302,34 @@ def dyndns_removecron():
|
|||
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
|
||||
|
||||
logger.success(m18n.n('dyndns_cron_removed'))
|
||||
|
||||
|
||||
def _guess_current_dyndns_domain(dyn_host):
|
||||
"""
|
||||
This function tries to guess which domain should be updated by
|
||||
"dyndns_update()" because there's not proper management of the current
|
||||
dyndns domain :/ (and at the moment the code doesn't support having several
|
||||
dyndns domain, which is sort of a feature so that people don't abuse the
|
||||
dynette...)
|
||||
"""
|
||||
|
||||
re_dyndns_private_key = re.compile(
|
||||
r'.*/K(?P<domain>[^\s\+]+)\.\+157.+\.private$'
|
||||
)
|
||||
|
||||
# Retrieve the first registered domain
|
||||
for path in glob.iglob('/etc/yunohost/dyndns/K*.private'):
|
||||
match = re_dyndns_private_key.match(path)
|
||||
if not match:
|
||||
continue
|
||||
_domain = match.group('domain')
|
||||
|
||||
# Verify if domain is registered (i.e., if it's available, skip
|
||||
# current domain beause that's not the one we want to update..)
|
||||
if _dyndns_available(dyn_host, _domain):
|
||||
continue
|
||||
else:
|
||||
return (_domain, path)
|
||||
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.n('dyndns_no_domain_registered'))
|
||||
|
|
|
@ -45,7 +45,7 @@ from moulinette.utils.log import getActionLogger
|
|||
from moulinette.utils.filesystem import read_json, write_to_json
|
||||
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron
|
||||
from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain
|
||||
from yunohost.dyndns import dyndns_subscribe
|
||||
from yunohost.dyndns import _dyndns_available, _dyndns_provides
|
||||
from yunohost.firewall import firewall_upnp
|
||||
from yunohost.service import service_status, service_regen_conf, service_log
|
||||
from yunohost.monitor import monitor_disk, monitor_system
|
||||
|
@ -253,29 +253,41 @@ def tools_postinstall(domain, password, ignore_dyndns=False):
|
|||
password -- YunoHost admin password
|
||||
|
||||
"""
|
||||
dyndns = not ignore_dyndns
|
||||
dyndns_provider = "dyndns.yunohost.org"
|
||||
|
||||
# Do some checks at first
|
||||
if os.path.isfile('/etc/yunohost/installed'):
|
||||
raise MoulinetteError(errno.EPERM,
|
||||
m18n.n('yunohost_already_installed'))
|
||||
if len(domain.split('.')) >= 3 and not ignore_dyndns:
|
||||
try:
|
||||
r = requests.get('https://dyndns.yunohost.org/domains')
|
||||
except requests.ConnectionError:
|
||||
pass
|
||||
else:
|
||||
dyndomains = json.loads(r.text)
|
||||
dyndomain = '.'.join(domain.split('.')[1:])
|
||||
|
||||
if dyndomain in dyndomains:
|
||||
if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200:
|
||||
dyndns = True
|
||||
else:
|
||||
raise MoulinetteError(errno.EEXIST,
|
||||
m18n.n('dyndns_unavailable'))
|
||||
else:
|
||||
if not ignore_dyndns:
|
||||
# Check if yunohost dyndns can handle the given domain
|
||||
# (i.e. is it a .nohost.me ? a .noho.st ?)
|
||||
try:
|
||||
is_nohostme_or_nohost = _dyndns_provides(dyndns_provider, domain)
|
||||
# If an exception is thrown, most likely we don't have internet
|
||||
# connectivity or something. Assume that this domain isn't manageable
|
||||
# and inform the user that we could not contact the dyndns host server.
|
||||
except:
|
||||
logger.warning(m18n.n('dyndns_provider_unreachable',
|
||||
provider=dyndns_provider))
|
||||
is_nohostme_or_nohost = False
|
||||
|
||||
# If this is a nohost.me/noho.st, actually check for availability
|
||||
if is_nohostme_or_nohost:
|
||||
# (Except if the user explicitly said he/she doesn't care about dyndns)
|
||||
if ignore_dyndns:
|
||||
dyndns = False
|
||||
# Check if the domain is available...
|
||||
elif _dyndns_available(dyndns_provider, domain):
|
||||
dyndns = True
|
||||
# If not, abort the postinstall
|
||||
else:
|
||||
raise MoulinetteError(errno.EEXIST,
|
||||
m18n.n('dyndns_unavailable',
|
||||
domain=domain))
|
||||
else:
|
||||
dyndns = False
|
||||
else:
|
||||
dyndns = False
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue