diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b845ded21..16ea2c5d2 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1483,9 +1483,6 @@ dyndns: subscribe: action_help: Subscribe to a DynDNS service arguments: - --subscribe-host: - help: Dynette HTTP API to subscribe to - default: "dyndns.yunohost.org" -d: full: --domain help: Full domain to subscribe with @@ -1499,20 +1496,11 @@ dyndns: update: action_help: Update IP on DynDNS platform arguments: - --dyn-host: - help: Dynette DNS server to inform - default: "dyndns.yunohost.org" -d: full: --domain help: Full domain to update extra: pattern: *pattern_domain - -k: - full: --key - help: Public DNS key - -i: - full: --ipv4 - help: IP address to send -f: full: --force help: Force the update (for debugging only) @@ -1521,9 +1509,6 @@ dyndns: full: --dry-run help: Only display the generated zone action: store_true - -6: - full: --ipv6 - help: IPv6 address to send ### dyndns_installcron() installcron: diff --git a/locales/en.json b/locales/en.json index 2c9652b9c..81e75eb32 100644 --- a/locales/en.json +++ b/locales/en.json @@ -349,7 +349,6 @@ "dpkg_is_broken": "You cannot do this right now because dpkg/APT (the system package managers) seems to be in a broken state... You can try to solve this issue by connecting through SSH and running `sudo apt install --fix-broken` and/or `sudo dpkg --configure -a`.", "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", "dyndns_could_not_check_available": "Could not check if {domain} is available on {provider}.", - "dyndns_could_not_check_provide": "Could not check if {provider} can provide {domain}.", "dyndns_domain_not_provided": "DynDNS provider {provider} cannot provide domain {domain}.", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 8eb14a36f..0bd84ea82 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -167,15 +167,16 @@ def domain_add(operation_logger, domain, dyndns=False): # DynDNS domain if dyndns: - from yunohost.dyndns import _dyndns_provides, _guess_current_dyndns_domain + from yunohost.utils.dns import is_yunohost_dyndns_domain + from yunohost.dyndns import _guess_current_dyndns_domain # Do not allow to subscribe to multiple dyndns domains... - if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None): + if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") # 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): + if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError("domain_dyndns_root_unknown") operation_logger.start() diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index cdc293421..80099d811 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -39,7 +39,7 @@ from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import _get_maindomain from yunohost.utils.network import get_public_ip -from yunohost.utils.dns import dig +from yunohost.utils.dns import dig, is_yunohost_dyndns_domain from yunohost.log import is_unit_operation from yunohost.regenconf import regen_conf @@ -53,66 +53,36 @@ RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( r".*/K(?P[^\s\+]+)\.\+165.+\.private$" ) +DYNDNS_PROVIDER = "dyndns.yunohost.org" +DYNDNS_DNS_AUTH = ["ns0.yunohost.org", "ns1.yunohost.org"] -def _dyndns_provides(provider, domain): + +def _dyndns_available(domain): """ - Checks if a provider provide/manage a given domain. + Checks if a domain is available on dyndns.yunohost.org 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 provider provide/manages the domain. False otherwise. - """ - - 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 YunohostError( - "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 available, False otherwise. """ - logger.debug("Checking if domain %s is available on %s ..." % (domain, provider)) + logger.debug(f"Checking if domain {domain} is available on {DYNDNS_PROVIDER} ...") try: - r = download_json( - "https://%s/test/%s" % (provider, domain), expected_status_code=None - ) + r = download_json(f"https://{DYNDNS_PROVIDER}/test/{domain}", expected_status_code=None) except MoulinetteError as e: logger.error(str(e)) raise YunohostError( - "dyndns_could_not_check_available", domain=domain, provider=provider + "dyndns_could_not_check_available", domain=domain, provider=DYNDNS_PROVIDER ) - return r == "Domain %s is available" % domain + return r == f"Domain {domain} is available" @is_unit_operation() def dyndns_subscribe( - operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None + operation_logger, domain=None, key=None ): """ Subscribe to a DynDNS service @@ -120,11 +90,9 @@ def dyndns_subscribe( Keyword argument: domain -- Full domain to subscribe with key -- Public DNS key - subscribe_host -- Dynette HTTP API to subscribe to - """ - if _guess_current_dyndns_domain(subscribe_host) != (None, None): + if _guess_current_dyndns_domain() != (None, None): raise YunohostValidationError("domain_dyndns_already_subscribed") if domain is None: @@ -132,13 +100,13 @@ def dyndns_subscribe( operation_logger.related_to.append(("domain", domain)) # Verify if domain is provided by subscribe_host - if not _dyndns_provides(subscribe_host, domain): + if not is_yunohost_dyndns_domain(domain): raise YunohostValidationError( - "dyndns_domain_not_provided", domain=domain, provider=subscribe_host + "dyndns_domain_not_provided", domain=domain, provider=DYNDNS_PROVIDER ) # Verify if domain is available - if not _dyndns_available(subscribe_host, domain): + if not _dyndns_available(domain): raise YunohostValidationError("dyndns_unavailable", domain=domain) operation_logger.start() @@ -167,9 +135,9 @@ def dyndns_subscribe( # Send subscription try: + b64encoded_key = base64.b64encode(key.encode()).decode() r = requests.post( - "https://%s/key/%s?key_algo=hmac-sha512" - % (subscribe_host, base64.b64encode(key.encode()).decode()), + f"https://{DYNDNS_PROVIDER}/key/{b64encoded_key}?key_algo=hmac-sha512", data={"subdomain": domain}, timeout=30, ) @@ -205,11 +173,7 @@ def dyndns_subscribe( @is_unit_operation() def dyndns_update( operation_logger, - dyn_host="dyndns.yunohost.org", domain=None, - key=None, - ipv4=None, - ipv6=None, force=False, dry_run=False, ): @@ -218,31 +182,36 @@ def dyndns_update( Keyword argument: domain -- Full domain to update - dyn_host -- Dynette DNS server to inform - key -- Public DNS key - ipv4 -- IP address to send - ipv6 -- IPv6 address to send - """ from yunohost.dns import _build_dns_conf # If domain is not given, try to guess it from keys available... + key = None if domain is None: - (domain, key) = _guess_current_dyndns_domain(dyn_host) + (domain, key) = _guess_current_dyndns_domain() if domain is None: raise YunohostValidationError("dyndns_no_domain_registered") # If key is not given, pick the first file we find with the domain given - else: - if key is None: - keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) + elif key is None: + keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) - if not keys: - raise YunohostValidationError("dyndns_key_not_found") + if not keys: + raise YunohostValidationError("dyndns_key_not_found") - key = keys[0] + key = keys[0] + + # Get current IPv4 and IPv6 + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + if ipv4 is None and ipv6 is None: + logger.debug( + "No ipv4 nor ipv6 ?! Sounds like the server is not connected to the internet, or the ip.yunohost.org infrastructure is down somehow" + ) + return # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' host = domain.split(".")[1:] @@ -251,33 +220,32 @@ def dyndns_update( logger.debug("Building zone update file ...") lines = [ - "server %s" % dyn_host, - "zone %s" % host, + f"server {DYNDNS_PROVIDER}", + f"zone {host}", ] + auth_resolvers = [] + + for dns_auth in DYNDNS_DNS_AUTH: + for type_ in ["A", "AAAA"]: + + ok, result = dig(dns_auth, type_) + if ok == "ok" and len(result) and result[0]: + auth_resolvers.append(result[0]) + + if not auth_resolvers: + raise YunohostError( + f"Failed to resolve IPv4/IPv6 for {DYNDNS_DNS_AUTH} ?", raw_msg=True + ) + def resolve_domain(domain, rdtype): - ok, result = dig(dyn_host, "A") - dyn_host_ipv4 = result[0] if ok == "ok" and len(result) else None - if not dyn_host_ipv4: - raise YunohostError( - "Failed to resolve IPv4 for %s ?" % dyn_host, raw_msg=True - ) - - ok, result = dig(dyn_host, "AAAA") - dyn_host_ipv6 = result[0] if ok == "ok" and len(result) else None - if not dyn_host_ipv6: - raise YunohostError( - "Failed to resolve IPv6 for %s ?" % dyn_host, raw_msg=True - ) - - ok, result = dig(domain, rdtype, resolvers=[dyn_host_ipv4, dyn_host_ipv6]) + ok, result = dig(domain, rdtype, resolvers=auth_resolvers) if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": logger.debug( - "Timed-out while trying to resolve %s record for %s using %s" - % (rdtype, domain, dyn_host) + f"Timed-out while trying to resolve {rdtype} record for {domain}" ) else: return None @@ -301,25 +269,9 @@ def dyndns_update( old_ipv4 = resolve_domain(domain, "A") old_ipv6 = resolve_domain(domain, "AAAA") - # Get current IPv4 and IPv6 - ipv4_ = get_public_ip() - ipv6_ = get_public_ip(6) - - if ipv4 is None: - ipv4 = ipv4_ - - if ipv6 is None: - ipv6 = ipv6_ - logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) - if ipv4 is None and ipv6 is None: - logger.debug( - "No ipv4 nor ipv6 ?! Sounds like the server is not connected to the internet, or the ip.yunohost.org infrastructure is down somehow" - ) - return - # no need to update if (not force and not dry_run) and (old_ipv4 == ipv4 and old_ipv6 == ipv6): logger.info("No updated needed.") @@ -388,19 +340,21 @@ def dyndns_update( ) +# Legacy def dyndns_installcron(): logger.warning( "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'." ) +# Legacy def dyndns_removecron(): logger.warning( "This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'." ) -def _guess_current_dyndns_domain(dyn_host): +def _guess_current_dyndns_domain(): """ This function tries to guess which domain should be updated by "dyndns_update()" because there's not proper management of the current @@ -412,6 +366,7 @@ def _guess_current_dyndns_domain(dyn_host): # Retrieve the first registered domain paths = list(glob.iglob("/etc/yunohost/dyndns/K*.private")) for path in paths: + # MD5 is legacy ugh match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) if not match: match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path) @@ -423,7 +378,7 @@ def _guess_current_dyndns_domain(dyn_host): # current domain beause that's not the one we want to update..) # If there's only 1 such key found, then avoid doing the request # for nothing (that's very probably the one we want to find ...) - if len(paths) > 1 and _dyndns_available(dyn_host, _domain): + if len(paths) > 1 and _dyndns_available(_domain): continue else: return (_domain, path) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e89081abd..54d7ffc0e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -45,7 +45,6 @@ from yunohost.app_catalog import ( _update_apps_catalog, ) from yunohost.domain import domain_add -from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.firewall import firewall_upnp from yunohost.service import service_start, service_enable from yunohost.regenconf import regen_conf @@ -205,12 +204,12 @@ def tools_postinstall( password -- YunoHost admin password """ + from yunohost.dyndns import _dyndns_available + from yunohost.utils.dns import is_yunohost_dyndns_domain from yunohost.utils.password import assert_password_is_strong_enough from yunohost.domain import domain_main_domain import psutil - dyndns_provider = "dyndns.yunohost.org" - # Do some checks at first if os.path.isfile("/etc/yunohost/installed"): raise YunohostValidationError("yunohost_already_installed") @@ -235,33 +234,28 @@ def tools_postinstall( if not force_password: assert_password_is_strong_enough("admin", password) - 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 Exception: - 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 not ignore_dyndns and is_yunohost_dyndns_domain(domain): + # (Except if the user explicitly said he/she doesn't care about dyndns) + if ignore_dyndns: + dyndns = False + # Check if the domain is available... + else: + try: + available = _dyndns_available(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 Exception: + logger.warning( + m18n.n("dyndns_provider_unreachable", provider="dyndns.yunohost.org") + ) - # 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): + if available: dyndns = True # If not, abort the postinstall else: raise YunohostValidationError("dyndns_unavailable", domain=domain) - else: - dyndns = False else: dyndns = False