Repository: AnalogJ/lexicon

Issue:
TLDExtract Private Domains for Dynamic DNS Providers
Hello all,

I'm currently putting together the plugin for dynu.com (listed in NYI proiders).
As Dynu also acts as Dynamic DNS provider with several toplevel domains as base for the dynamic domain (`yourhost.dynu.net`, `yourhost.freeddns.org`..., also, wildcards), I had some trouble putting together the plugin.

As an example, I'm making up `mydomain.dynu.net` as my target dynamic dns hostname.

Now, the `tldextract` package used to determine the part of the domain that belongs to the toplevel will spit out `net` as the toplevel, `dynu` as the TLD and then drops `mydomain` in further processing as seen [in client.py](../blob/master/lexicon/client.py#L43).

In turn, finding the right domain from the list of dns entries in `_authenticate` is not possible by default (as `self.domain` is set to `dynu.net`).

I discovered two workarounds for this:

1. use `--delegated "mydomain.dynu.net"` to explicitly target the subdomain
2. change the code [in client.py](../blob/master/lexicon/client.py#L41) to this:

```python
extract = tldextract.TLDExtract(include_psl_private_domains=True)

# Process domain, strip subdomain
domain_parts = extract(
    self.config.resolve('lexicon:domain'))
runtime_config['domain'] = '{0}.{1}'.format(
    domain_parts.domain, domain_parts.suffix)
```

The latter is taken from [the tldextract README](https://github.com/john-kurkowski/tldextract#public-vs-private-domains).
And because Dynu probably isn't the only Dynamic DNS provider using subdomains for their users, I guess this should be the default solution.
There's a catch however that is still in ongoing development [tldextract#144](https://github.com/john-kurkowski/tldextract/pull/144):
The list of TLDs is cached on first load of the extension, so if the config is not set to `include_psl_private_domains` before the package is first initialized, it won't work. So either an update has to be triggered manually, or, lexicon should be installed and used from a virtualenv in the first place.

Since I'm already making use of method 2 in my dev enviroment, I could open a PR right away, but I'm not 100% sure on side effects for other plugins, hence my hesitation.

Thanks and best,
Chris

edit// whitespace in codeblock, typos, grammar


Discussion:

Resolving patch:
diff --git a/lexicon/client.py b/lexicon/client.py
--- a/lexicon/client.py
+++ b/lexicon/client.py
@@ -1,6 +1,7 @@
 """Main module of Lexicon. Defines the Client class, that holds all Lexicon logic."""
 from __future__ import absolute_import
 import importlib
+import os
 
 import tldextract
 
@@ -10,6 +11,9 @@
     legacy_config_resolver, non_interactive_config_resolver,
 )
 
+TLDEXTRACT_CACHE_FILE_DEFAULT = os.path.join('~', '.lexicon_tld_set')
+TLDEXTRACT_CACHE_FILE = os.path.expanduser(os.environ.get("LEXICON_TLDEXTRACT_CACHE",
+                                                          TLDEXTRACT_CACHE_FILE_DEFAULT))
 
 class ProviderNotAvailableError(Exception):
     """
@@ -38,7 +42,9 @@ def __init__(self, config=None):
         runtime_config = {}
 
         # Process domain, strip subdomain
-        domain_parts = tldextract.extract(
+        domain_extractor = tldextract.TLDExtract(cache_file=TLDEXTRACT_CACHE_FILE,
+                                                 include_psl_private_domains=True)
+        domain_parts = domain_extractor(
             self.config.resolve('lexicon:domain'))
         runtime_config['domain'] = '{0}.{1}'.format(
             domain_parts.domain, domain_parts.suffix)


Test patch: