mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
NEW PROVIDER: HEDNS: Hurricane Electric DNS (dns.he.net) (#822)
* Add initial dns.he.net provider support
* Update to new IncrementalDiff interface
* Fix ListZones output for `all` query on `get-zones`
* Refactor authentication code for 2FA with better error checking
* Fix integration test and refactor zone record retrieval
* Add option to use `.hedns-session` file to store sessions between runs
* Add comment on `session-file-path`
* Add integration test for TXT records longer than 255 characters
* Add additional checks for expected responses, and better 2FA error checking
* Minor documentation changes
* Revert "Add integration test for TXT records longer than 255 characters"
This reverts commit 657272db
* Add note on provider fragility due to parsing the web-interface
* Resolve go lint issues
* Clarify security warnings in documentation
This commit is contained in:
committed by
GitHub
parent
443c187dda
commit
74dd34443a
1
OWNERS
1
OWNERS
@ -9,6 +9,7 @@ providers/digitalocean @Deraen
|
|||||||
providers/dnsimple @aeden
|
providers/dnsimple @aeden
|
||||||
providers/gandi_v5 @TomOnTime
|
providers/gandi_v5 @TomOnTime
|
||||||
# providers/gcloud
|
# providers/gcloud
|
||||||
|
providers/hedns @rblenkinsopp
|
||||||
providers/hexonet @papakai
|
providers/hexonet @papakai
|
||||||
providers/internetbs @pragmaton
|
providers/internetbs @pragmaton
|
||||||
providers/inwx @svenpeter42
|
providers/inwx @svenpeter42
|
||||||
|
@ -27,6 +27,7 @@ Currently supported DNS providers:
|
|||||||
- Exoscale
|
- Exoscale
|
||||||
- Gandi
|
- Gandi
|
||||||
- Google DNS
|
- Google DNS
|
||||||
|
- Hurricane Electric DNS
|
||||||
- HEXONET
|
- HEXONET
|
||||||
- Internet.bs
|
- Internet.bs
|
||||||
- INWX
|
- INWX
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<th class="rotate"><div><span>EXOSCALE</span></div></th>
|
<th class="rotate"><div><span>EXOSCALE</span></div></th>
|
||||||
<th class="rotate"><div><span>GANDI_V5</span></div></th>
|
<th class="rotate"><div><span>GANDI_V5</span></div></th>
|
||||||
<th class="rotate"><div><span>GCLOUD</span></div></th>
|
<th class="rotate"><div><span>GCLOUD</span></div></th>
|
||||||
|
<th class="rotate"><div><span>HEDNS</span></div></th>
|
||||||
<th class="rotate"><div><span>HEXONET</span></div></th>
|
<th class="rotate"><div><span>HEXONET</span></div></th>
|
||||||
<th class="rotate"><div><span>INTERNETBS</span></div></th>
|
<th class="rotate"><div><span>INTERNETBS</span></div></th>
|
||||||
<th class="rotate"><div><span>INWX</span></div></th>
|
<th class="rotate"><div><span>INWX</span></div></th>
|
||||||
@ -74,6 +75,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="danger">
|
||||||
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Actively maintained provider module.">
|
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Actively maintained provider module.">
|
||||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -161,6 +165,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="danger">
|
<td class="danger">
|
||||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -242,6 +249,9 @@
|
|||||||
<td class="danger">
|
<td class="danger">
|
||||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="danger">
|
||||||
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -318,6 +328,9 @@
|
|||||||
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
|
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
|
||||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -374,6 +387,9 @@
|
|||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td class="danger">
|
||||||
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="Supported by INWX but not implemented yet.">
|
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="Supported by INWX but not implemented yet.">
|
||||||
@ -435,6 +451,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
@ -503,6 +522,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="PTR records with empty targets are not supported">
|
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="PTR records with empty targets are not supported">
|
||||||
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
||||||
@ -560,6 +582,9 @@
|
|||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
@ -616,6 +641,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="SRV records with empty targets are not supported">
|
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="SRV records with empty targets are not supported">
|
||||||
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -688,6 +716,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
@ -744,6 +775,9 @@
|
|||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td class="danger">
|
||||||
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -801,6 +835,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="INWX only supports a single entry for TXT records">
|
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="INWX only supports a single entry for TXT records">
|
||||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||||
@ -840,6 +877,7 @@
|
|||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
|
<td class="danger" data-toggle="tooltip" data-container="body" data-placement="top" title="Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us.">
|
||||||
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa has-tooltip fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -878,6 +916,7 @@
|
|||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="danger">
|
<td class="danger">
|
||||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -916,6 +955,9 @@
|
|||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
|
<td class="danger">
|
||||||
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="DS records are only supported at the apex and require a different API call that hasn't been implemented yet.">
|
<td class="info" data-toggle="tooltip" data-container="body" data-placement="top" title="DS records are only supported at the apex and require a different API call that hasn't been implemented yet.">
|
||||||
@ -971,6 +1013,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td><i class="fa fa-minus dim"></i></td>
|
<td><i class="fa fa-minus dim"></i></td>
|
||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
@ -1045,6 +1090,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="danger">
|
<td class="danger">
|
||||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -1138,6 +1186,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="danger">
|
<td class="danger">
|
||||||
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
<i class="fa fa-times text-danger" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -1210,6 +1261,9 @@
|
|||||||
<td class="success">
|
<td class="success">
|
||||||
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="success">
|
||||||
|
<i class="fa fa-check text-success" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
<td class="info">
|
<td class="info">
|
||||||
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
|
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
|
||||||
</td>
|
</td>
|
||||||
|
108
docs/_providers/hedns.md
Normal file
108
docs/_providers/hedns.md
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
name: Hurricane Electric DNS
|
||||||
|
title: Hurricane Electric DNS Provider
|
||||||
|
layout: default
|
||||||
|
jsId: HEDNS
|
||||||
|
---
|
||||||
|
# Hurricane Electric DNS Provider
|
||||||
|
|
||||||
|
## Important Note
|
||||||
|
Hurricane Electric does not currently expose an official JSON or XML API, and as such, this provider interacts directly
|
||||||
|
with the web interface. Because there is no officially supported API, this provider may cease to function if Hurricane
|
||||||
|
Electric changes their interface, and you should be willing to accept this possibility before relying on this provider.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
In your `creds.json` file you must provide your `dns.he.net` account username and password. These are the same username
|
||||||
|
and password used to login to the [web interface]([https://dns.he.net]).
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"hedns":{
|
||||||
|
"username": "yourUsername",
|
||||||
|
"password": "yourPassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
### Two factor authentication
|
||||||
|
|
||||||
|
If two-factor authentication has been enabled on your account you will also need to provide a valid TOTP code.
|
||||||
|
This can also be done via an environment variable:
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"hedns":{
|
||||||
|
"username": "yourUsername",
|
||||||
|
"password": "yourPassword",
|
||||||
|
"totp": "$HEDNS_TOTP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
and then you can run
|
||||||
|
|
||||||
|
{% highlight bash %}
|
||||||
|
$ HEDNS_TOTP=12345 dnscontrol preview
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
It is also possible to directly provide the shared TOTP secret using the key "totp-key" in `creds.json`. This secret is
|
||||||
|
only available when first enabling two-factor authentication.
|
||||||
|
|
||||||
|
**Security Warning**:
|
||||||
|
* Anyone with access to this `creds.json` file will have *full* access to your Hurricane Electric account and will be
|
||||||
|
able to modify and delete your DNS entries
|
||||||
|
* Storing the shared secret together with the password weakens two factor authentication because both factors are stored
|
||||||
|
in a single place.
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"hedns":{
|
||||||
|
"username": "yourUsername",
|
||||||
|
"password": "yourPassword",
|
||||||
|
"totp-key": "yourTOTPSharedSecret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
### Persistent Sessions
|
||||||
|
|
||||||
|
Normally this provider will refresh authentication with each run of dnscontrol. This can lead to issues when using
|
||||||
|
two-factor authentication if two runs occur within the time period of a single TOTP token (30 seconds), as reusing the
|
||||||
|
same token is explicitly disallowed by RFC 6238 (TOTP).
|
||||||
|
|
||||||
|
To work around this limitation, if multiple requests need to be made, the option `"session-file-path"` can be set in
|
||||||
|
`creds.json`, which is the directory where a `.hedns-session` file will be created. This can be used to allow reuse of an
|
||||||
|
existing session between runs, without the need to re-authenticate.
|
||||||
|
|
||||||
|
This option is disabled by default when this key is not present,
|
||||||
|
|
||||||
|
**Security Warning**:
|
||||||
|
* Anyone with access to this `.hedns-session` file will be able to use the existing session (until it expires) and have
|
||||||
|
*full* access to your Hurrican Electric account and will be able to modify and delete your DNS entries.
|
||||||
|
* It should be stored in a location only trusted users can access.
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"hedns":{
|
||||||
|
"username": "yourUsername",
|
||||||
|
"password": "yourPassword",
|
||||||
|
"totp-key": "yourTOTPSharedSecret"
|
||||||
|
"session-file-path": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
This provider does not recognize any special metadata fields unique to Hurricane Electric DNS.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Example Javascript:
|
||||||
|
|
||||||
|
{% highlight js %}
|
||||||
|
var DNSIMPLE = NewDnsProvider("hedns", "HEDNS");
|
||||||
|
|
||||||
|
D("example.tld", REG_DNSIMPLE, DnsProvider(HEDNS),
|
||||||
|
A("test","1.2.3.4")
|
||||||
|
);
|
||||||
|
{% endhighlight %}
|
@ -78,6 +78,7 @@ Maintainers of contributed providers:
|
|||||||
* `DNSIMPLE` @aeden
|
* `DNSIMPLE` @aeden
|
||||||
* `EXOSCALE` @pierre-emmanuelJ
|
* `EXOSCALE` @pierre-emmanuelJ
|
||||||
* `GANDI_V5` @TomOnTime
|
* `GANDI_V5` @TomOnTime
|
||||||
|
* `HEDNS` @rblenkinsopp
|
||||||
* `HEXONET` @papakai
|
* `HEXONET` @papakai
|
||||||
* `INTERNETBS` @pragmaton
|
* `INTERNETBS` @pragmaton
|
||||||
* `INWX` @svenpeter42
|
* `INWX` @svenpeter42
|
||||||
|
5
go.mod
5
go.mod
@ -7,8 +7,10 @@ require (
|
|||||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.0
|
github.com/Azure/go-autorest/autorest/azure/auth v0.5.0
|
||||||
github.com/Azure/go-autorest/autorest/to v0.4.0
|
github.com/Azure/go-autorest/autorest/to v0.4.0
|
||||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55
|
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1
|
||||||
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f
|
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||||
|
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||||
github.com/aws/aws-sdk-go v1.32.10
|
github.com/aws/aws-sdk-go v1.32.10
|
||||||
github.com/billputer/go-namecheap v0.0.0-20170915210158-0c7adb0710f8
|
github.com/billputer/go-namecheap v0.0.0-20170915210158-0c7adb0710f8
|
||||||
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
|
||||||
@ -47,8 +49,7 @@ require (
|
|||||||
github.com/tiramiseb/go-gandi v0.0.0-20200313161345-6b74caa58663
|
github.com/tiramiseb/go-gandi v0.0.0-20200313161345-6b74caa58663
|
||||||
github.com/urfave/cli/v2 v2.2.0
|
github.com/urfave/cli/v2 v2.2.0
|
||||||
github.com/vultr/govultr v0.2.0
|
github.com/vultr/govultr v0.2.0
|
||||||
golang.org/x/mod v0.3.0 // indirect
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 // indirect
|
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 // indirect
|
||||||
google.golang.org/api v0.28.0
|
google.golang.org/api v0.28.0
|
||||||
|
15
go.sum
15
go.sum
@ -49,11 +49,17 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55 h1:jbGlDKdzAZ92NzK65hUP98ri0/r50vVVvmZsFP/nIqo=
|
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55 h1:jbGlDKdzAZ92NzK65hUP98ri0/r50vVVvmZsFP/nIqo=
|
||||||
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
|
github.com/DisposaBoy/JsonConfigReader v0.0.0-20171218180944-5ea4d0ddac55/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f h1:MXp+2PP1RxWWoE3qmOecVblerzKCryXkFXq9er+EDr8=
|
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f h1:MXp+2PP1RxWWoE3qmOecVblerzKCryXkFXq9er+EDr8=
|
||||||
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f/go.mod h1:FiuynIwe98RFhWI8nZ0dnsldPVsBy9rHH1hn2WYwme4=
|
github.com/TomOnTime/utfutil v0.0.0-20200626160131-0b0178852c8f/go.mod h1:FiuynIwe98RFhWI8nZ0dnsldPVsBy9rHH1hn2WYwme4=
|
||||||
github.com/alecthomas/kong v0.2.2/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
github.com/alecthomas/kong v0.2.2/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
|
||||||
|
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/aws/aws-sdk-go v1.32.10 h1:cEJTxGcBGlsM2tN36MZQKhlK93O9HrnaRs+lq2f0zN8=
|
github.com/aws/aws-sdk-go v1.32.10 h1:cEJTxGcBGlsM2tN36MZQKhlK93O9HrnaRs+lq2f0zN8=
|
||||||
@ -193,8 +199,6 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
|
|||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
|
github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU=
|
||||||
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
|
||||||
github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=
|
github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8=
|
||||||
@ -307,7 +311,6 @@ github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2
|
|||||||
github.com/vultr/govultr v0.2.0 h1:CZSNNCk+PHz9hzmfH2PFGkDgc3qNetwZqtcaqL8shlg=
|
github.com/vultr/govultr v0.2.0 h1:CZSNNCk+PHz9hzmfH2PFGkDgc3qNetwZqtcaqL8shlg=
|
||||||
github.com/vultr/govultr v0.2.0/go.mod h1:glSLa57Jdj5s860EEc6+DEBbb/t3aUOKnB4gVPmDVlQ=
|
github.com/vultr/govultr v0.2.0/go.mod h1:glSLa57Jdj5s860EEc6+DEBbb/t3aUOKnB4gVPmDVlQ=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||||
@ -356,6 +359,7 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
|||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -379,6 +383,8 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7
|
|||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
|
||||||
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||||
@ -396,6 +402,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -468,8 +475,6 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
golang.org/x/tools v0.0.0-20200626032829-bcbc01e07a20 h1:q+ysxVHVQNTVHgzwjuk4ApAILRbfOLARfnEaqCIBR6A=
|
|
||||||
golang.org/x/tools v0.0.0-20200626032829-bcbc01e07a20/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 h1:8Kg+JssU1jBZs8GIrL5pl4nVyaqyyhdmHAR4D1zGErg=
|
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 h1:8Kg+JssU1jBZs8GIrL5pl4nVyaqyyhdmHAR4D1zGErg=
|
||||||
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
@ -438,7 +438,7 @@ func ignoreName(name string) *rec {
|
|||||||
|
|
||||||
func ignoreTarget(name string, typ string) *rec {
|
func ignoreTarget(name string, typ string) *rec {
|
||||||
r := &rec{
|
r := &rec{
|
||||||
Type: "IGNORE_TARGET",
|
Type: "IGNORE_TARGET",
|
||||||
Target: typ,
|
Target: typ,
|
||||||
}
|
}
|
||||||
r.SetLabel(name, "**current-domain**")
|
r.SetLabel(name, "**current-domain**")
|
||||||
@ -509,16 +509,16 @@ func tc(desc string, recs ...*rec) *TestCase {
|
|||||||
} else if r.Type == "IGNORE_TARGET" {
|
} else if r.Type == "IGNORE_TARGET" {
|
||||||
ignoredTargets = append(ignoredTargets, &models.IgnoreTarget{
|
ignoredTargets = append(ignoredTargets, &models.IgnoreTarget{
|
||||||
Pattern: r.GetLabel(),
|
Pattern: r.GetLabel(),
|
||||||
Type: r.Target,
|
Type: r.Target,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
records = append(records, r)
|
records = append(records, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &TestCase{
|
return &TestCase{
|
||||||
Desc: desc,
|
Desc: desc,
|
||||||
Records: records,
|
Records: records,
|
||||||
IgnoredNames: ignoredNames,
|
IgnoredNames: ignoredNames,
|
||||||
IgnoredTargets: ignoredTargets,
|
IgnoredTargets: ignoredTargets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -614,6 +614,10 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
tc("Delete one", a("@", "1.2.3.4").ttl(500), a("www", "5.6.7.8").ttl(400)),
|
tc("Delete one", a("@", "1.2.3.4").ttl(500), a("www", "5.6.7.8").ttl(400)),
|
||||||
tc("Add back and change ttl", a("www", "5.6.7.8").ttl(700), a("www", "1.2.3.4").ttl(700)),
|
tc("Add back and change ttl", a("www", "5.6.7.8").ttl(700), a("www", "1.2.3.4").ttl(700)),
|
||||||
tc("Change targets and ttls", a("www", "1.1.1.1"), a("www", "2.2.2.2")),
|
tc("Change targets and ttls", a("www", "1.1.1.1"), a("www", "2.2.2.2")),
|
||||||
|
),
|
||||||
|
|
||||||
|
testgroup("WildcardACD",
|
||||||
|
not("HEDNS"), // Not supported by dns.he.net due to abuse
|
||||||
tc("Create wildcard", a("*", "1.2.3.4"), a("www", "1.1.1.1")),
|
tc("Create wildcard", a("*", "1.2.3.4"), a("www", "1.1.1.1")),
|
||||||
tc("Delete wildcard", a("www", "1.1.1.1")),
|
tc("Delete wildcard", a("www", "1.1.1.1")),
|
||||||
),
|
),
|
||||||
@ -641,7 +645,7 @@ func makeTests(t *testing.T) []*TestGroup {
|
|||||||
),
|
),
|
||||||
|
|
||||||
testgroup("Null MX",
|
testgroup("Null MX",
|
||||||
not("AZURE_DNS", "GANDI_V5", "INWX", "NAMEDOTCOM", "DIGITALOCEAN", "NETCUP", "DNSIMPLE"), // These providers don't support RFC 7505
|
not("AZURE_DNS", "GANDI_V5", "INWX", "NAMEDOTCOM", "DIGITALOCEAN", "NETCUP", "DNSIMPLE", "HEDNS"), // These providers don't support RFC 7505
|
||||||
tc("Null MX", mx("@", 0, ".")),
|
tc("Null MX", mx("@", 0, ".")),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -62,6 +62,13 @@
|
|||||||
"project_id": "$GCLOUD_PROJECT",
|
"project_id": "$GCLOUD_PROJECT",
|
||||||
"type": "$GCLOUD_TYPE"
|
"type": "$GCLOUD_TYPE"
|
||||||
},
|
},
|
||||||
|
"HEDNS": {
|
||||||
|
"username": "$HEDNS_USERNAME",
|
||||||
|
"password": "$HEDNS_PASSWORD",
|
||||||
|
"totp-key": "$HEDNS_TOTP_SECRET",
|
||||||
|
"session-file-path": ".",
|
||||||
|
"domain": "$HEDNS_DOMAIN"
|
||||||
|
},
|
||||||
"HEXONET": {
|
"HEXONET": {
|
||||||
"apientity": "$HEXONET_ENTITY",
|
"apientity": "$HEXONET_ENTITY",
|
||||||
"apilogin": "$HEXONET_UID",
|
"apilogin": "$HEXONET_UID",
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/exoscale"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/exoscale"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/gandi_v5"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/gandi_v5"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/gcloud"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/gcloud"
|
||||||
|
_ "github.com/StackExchange/dnscontrol/v3/providers/hedns"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/hexonet"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/hexonet"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/internetbs"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/internetbs"
|
||||||
_ "github.com/StackExchange/dnscontrol/v3/providers/inwx"
|
_ "github.com/StackExchange/dnscontrol/v3/providers/inwx"
|
||||||
|
742
providers/hedns/hednsProvider.go
Normal file
742
providers/hedns/hednsProvider.go
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
package hedns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
||||||
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hurricane Electric DNS provider (dns.he.net)
|
||||||
|
|
||||||
|
Info required in `creds.json`:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
|
||||||
|
Either of the following settings is required when two factor authentication is enabled:
|
||||||
|
- totp (TOTP code if 2FA is enabled; best specified as an env variable)
|
||||||
|
- totp-key (shared TOTP secret used to generate a valid TOTP code; not recommended since
|
||||||
|
this effectively defeats the purpose of two factor authentication by storing
|
||||||
|
both factors at the same place)
|
||||||
|
|
||||||
|
Additionally
|
||||||
|
- session-file-path (Path where a '.hedns-session' file will be created to allow a
|
||||||
|
session to persist between executions)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var features = providers.DocumentationNotes{
|
||||||
|
providers.CanUseAlias: providers.Can(),
|
||||||
|
providers.CanUseCAA: providers.Can(),
|
||||||
|
providers.CanUseNAPTR: providers.Can(),
|
||||||
|
providers.CanUseDS: providers.Cannot(),
|
||||||
|
providers.CanUseDSForChildren: providers.Cannot(),
|
||||||
|
providers.CanUsePTR: providers.Can(),
|
||||||
|
providers.CanUseSSHFP: providers.Can(),
|
||||||
|
providers.CanUseSRV: providers.Can(),
|
||||||
|
providers.CanUseTLSA: providers.Cannot(),
|
||||||
|
providers.CanUseTXTMulti: providers.Can(),
|
||||||
|
providers.CanAutoDNSSEC: providers.Cannot(),
|
||||||
|
providers.DocCreateDomains: providers.Can(),
|
||||||
|
providers.DocDualHost: providers.Can(),
|
||||||
|
providers.DocOfficiallySupported: providers.Cannot(),
|
||||||
|
providers.CanGetZones: providers.Can(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
providers.RegisterDomainServiceProviderType("HEDNS", newHDNSProvider, features)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultNameservers = []string{
|
||||||
|
"ns1.he.net",
|
||||||
|
"ns2.he.net",
|
||||||
|
"ns3.he.net",
|
||||||
|
"ns4.he.net",
|
||||||
|
"ns5.he.net",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiEndpoint = "https://dns.he.net/"
|
||||||
|
sessionFileName = ".hedns-session"
|
||||||
|
|
||||||
|
errorInvalidCredentials = "Incorrect"
|
||||||
|
errorInvalidTotpToken = "The token supplied is invalid."
|
||||||
|
errorTotpTokenRequired = "You must enter the token generated by your authenticator."
|
||||||
|
errorTotpTokenReused = "This token has already been used. You may not reuse tokens."
|
||||||
|
errorImproperDelegation = "This zone does not appear to be properly delegated to our nameservers."
|
||||||
|
)
|
||||||
|
|
||||||
|
// HDNSProvider stores login credentials and represents and API connection
|
||||||
|
type HDNSProvider struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
TfaSecret string
|
||||||
|
TfaValue string
|
||||||
|
SessionFilePath string
|
||||||
|
|
||||||
|
httpClient http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record stores the HDNS specific zone and record IDs
|
||||||
|
type Record struct {
|
||||||
|
RecordName string
|
||||||
|
RecordID uint64
|
||||||
|
ZoneName string
|
||||||
|
ZoneID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHDNSProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
|
username, password := cfg["username"], cfg["password"]
|
||||||
|
totpSecret, totpValue := cfg["totp-key"], cfg["totp"]
|
||||||
|
sessionFilePath := cfg["session-file-path"]
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
|
return nil, fmt.Errorf("username must be provided")
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
return nil, fmt.Errorf("password must be provided")
|
||||||
|
}
|
||||||
|
if totpSecret != "" && totpValue != "" {
|
||||||
|
return nil, fmt.Errorf("totp and totp-key must not be specified at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the initial login
|
||||||
|
client := &HDNSProvider{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
TfaSecret: totpSecret,
|
||||||
|
TfaValue: totpValue,
|
||||||
|
SessionFilePath: sessionFilePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create storage for the cookies
|
||||||
|
cookieJar, _ := cookiejar.New(nil)
|
||||||
|
client.httpClient = http.Client{Jar: cookieJar}
|
||||||
|
|
||||||
|
err := client.authenticate()
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListZones list all zones on this provider.
|
||||||
|
func (c *HDNSProvider) ListZones() ([]string, error) {
|
||||||
|
domainsMap, err := c.listDomains()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := make([]string, 0, len(domainsMap))
|
||||||
|
for domain := range domainsMap {
|
||||||
|
domains = append(domains, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the order is deterministic
|
||||||
|
sort.Strings(domains)
|
||||||
|
|
||||||
|
return domains, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureDomainExists creates the domain if it does not exist.
|
||||||
|
func (c *HDNSProvider) EnsureDomainExists(domain string) error {
|
||||||
|
domains, err := c.ListZones()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range domains {
|
||||||
|
if d == domain {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.createDomain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameservers returns the default HDNS nameservers.
|
||||||
|
func (c *HDNSProvider) GetNameservers(_ string) ([]*models.Nameserver, error) {
|
||||||
|
return models.ToNameservers(defaultNameservers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainCorrections returns a list of corrections for the domain.
|
||||||
|
func (c *HDNSProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||||
|
var corrections []*models.Correction
|
||||||
|
|
||||||
|
err := dc.Punycode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := c.GetZoneRecords(dc.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the SOA record to get the ZoneID, then remove it from the list.
|
||||||
|
zoneID := uint64(0)
|
||||||
|
var prunedRecords models.Records
|
||||||
|
for _, r := range records {
|
||||||
|
if r.Type == "SOA" {
|
||||||
|
zoneID = r.Original.(Record).ZoneID
|
||||||
|
} else {
|
||||||
|
prunedRecords = append(prunedRecords, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
models.PostProcessRecords(prunedRecords)
|
||||||
|
|
||||||
|
differ := diff.New(dc)
|
||||||
|
_, toCreate, toDelete, toModify, err := differ.IncrementalDiff(prunedRecords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, del := range toDelete {
|
||||||
|
record := del.Existing
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: del.String(),
|
||||||
|
F: func() error { return c.deleteZoneRecord(record) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cre := range toCreate {
|
||||||
|
record := cre.Desired
|
||||||
|
record.Original = Record{
|
||||||
|
ZoneName: dc.Name,
|
||||||
|
ZoneID: zoneID,
|
||||||
|
RecordName: cre.Desired.Name,
|
||||||
|
}
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: cre.String(),
|
||||||
|
F: func() error { return c.editZoneRecord(record, true) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mod := range toModify {
|
||||||
|
record := mod.Desired
|
||||||
|
record.Original = Record{
|
||||||
|
ZoneName: dc.Name,
|
||||||
|
ZoneID: zoneID,
|
||||||
|
RecordID: mod.Existing.Original.(Record).RecordID,
|
||||||
|
RecordName: mod.Desired.Name,
|
||||||
|
}
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: mod.String(),
|
||||||
|
F: func() error { return c.editZoneRecord(record, false) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return corrections, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetZoneRecords returns all the records for the given domain
|
||||||
|
func (c *HDNSProvider) GetZoneRecords(domain string) (models.Records, error) {
|
||||||
|
var zoneRecords []*models.RecordConfig
|
||||||
|
|
||||||
|
// Get Domain ID
|
||||||
|
domains, err := c.listDomains()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domainID, domainExists := domains[domain]
|
||||||
|
if !domainExists {
|
||||||
|
return nil, fmt.Errorf("domain %s does not exist", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryURL, _ := url.Parse(apiEndpoint)
|
||||||
|
q := queryURL.Query()
|
||||||
|
q.Add("hosted_dns_zoneid", strconv.FormatUint(domainID, 10))
|
||||||
|
q.Add("menu", "edit_zone")
|
||||||
|
q.Add("hosted_dns_editzone", "")
|
||||||
|
queryURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
response, err := c.httpClient.Get(queryURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Parse the HTML response
|
||||||
|
document, err := goquery.NewDocumentFromReader(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we can find the zone records
|
||||||
|
if document.Find("#dns_main_content").Size() == 0 {
|
||||||
|
return nil, fmt.Errorf("zone records listing failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all the domain records
|
||||||
|
recordSelector := "tr.dns_tr, tr.dns_tr_dynamic, tr.dns_tr_locked"
|
||||||
|
document.Find(recordSelector).EachWithBreak(func(index int, element *goquery.Selection) bool {
|
||||||
|
parser := elementParser{}
|
||||||
|
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
Type: parser.parseStringAttr(element.Find("td > .rrlabel"), "data"),
|
||||||
|
TTL: uint32(parser.parseIntElement(element.Find("td:nth-child(5)"))),
|
||||||
|
Original: Record{
|
||||||
|
ZoneName: domain,
|
||||||
|
ZoneID: domainID,
|
||||||
|
RecordName: parser.parseStringElement(element.Find(".dns_view")),
|
||||||
|
RecordID: parser.parseIntAttr(element, "id"),
|
||||||
|
},
|
||||||
|
Target: parser.parseStringAttr(element.Find("td:nth-child(7)"), "data"),
|
||||||
|
}
|
||||||
|
|
||||||
|
priority := parser.parseIntElement(element.Find("td:nth-child(6)"))
|
||||||
|
if parser.err != nil {
|
||||||
|
err = parser.err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore record types that dnscontrol does not support
|
||||||
|
if rc.Type == "HINFO" || rc.Type == "AFSDB" || rc.Type == "RP" || rc.Type == "LOC" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.SetLabelFromFQDN(rc.Original.(Record).RecordName, domain)
|
||||||
|
|
||||||
|
// dns.he.net omits the trailing "." on the hostnames for certain record types
|
||||||
|
if rc.Type == "CNAME" || rc.Type == "MX" || rc.Type == "NS" || rc.Type == "PTR" {
|
||||||
|
rc.Target += "."
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rc.Type {
|
||||||
|
case "ALIAS":
|
||||||
|
err = rc.SetTarget(rc.Target)
|
||||||
|
case "MX":
|
||||||
|
err = rc.SetTargetMX(uint16(priority), rc.Target)
|
||||||
|
case "SRV":
|
||||||
|
err = rc.SetTargetSRVPriorityString(uint16(priority), rc.Target)
|
||||||
|
case "SPF":
|
||||||
|
// Convert to TXT record as SPF is deprecated
|
||||||
|
rc.Type = "TXT"
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
err = rc.PopulateFromString(rc.Type, rc.Target, domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneRecords = append(zoneRecords, rc)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return zoneRecords, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) authResumeSession() (authenticated bool, requiresTfa bool, err error) {
|
||||||
|
response, err := c.httpClient.Get(apiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
document, err := c.parseResponseForDocumentAndErrors(response)
|
||||||
|
if err != nil {
|
||||||
|
// Deal with the edge case where we have attempted to use the same authentication token more than two times
|
||||||
|
if err.Error() == errorTotpTokenRequired {
|
||||||
|
return false, true, nil
|
||||||
|
}
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the presence of the login button or the TFA input
|
||||||
|
authenticated = document.Find("#_tlogout").Size() > 0
|
||||||
|
requiresTfa = document.Find("input#tfacode").Size() > 0
|
||||||
|
|
||||||
|
return authenticated, requiresTfa, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) authUsernameAndPassword() (authenticated bool, requiresTfa bool, err error) {
|
||||||
|
// Login with username and password
|
||||||
|
response, err := c.httpClient.PostForm(apiEndpoint, url.Values{
|
||||||
|
"email": {c.Username},
|
||||||
|
"pass": {c.Password},
|
||||||
|
"submit": {"Login!"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
document, err := c.parseResponseForDocumentAndErrors(response)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == errorInvalidCredentials {
|
||||||
|
err = fmt.Errorf("authentication failed with incorrect username or password")
|
||||||
|
}
|
||||||
|
if err.Error() == errorTotpTokenRequired {
|
||||||
|
return false, true, nil
|
||||||
|
}
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated = document.Find("#_tlogout").Size() > 0
|
||||||
|
requiresTfa = document.Find("input#tfacode").Size() > 0
|
||||||
|
|
||||||
|
// Completed and 2FA is not required
|
||||||
|
return authenticated, requiresTfa, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) auth2FA() (authenticated bool, err error) {
|
||||||
|
|
||||||
|
if c.TfaValue == "" && c.TfaSecret == "" {
|
||||||
|
return false, fmt.Errorf("account requires two-factor authentication but neither totp or totp-key were provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TfaValue == "" && c.TfaSecret != "" {
|
||||||
|
var err error
|
||||||
|
c.TfaValue, err = totp.GenerateCode(c.TfaSecret, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.PostForm(apiEndpoint, url.Values{
|
||||||
|
"tfacode": {c.TfaValue},
|
||||||
|
"submit": {"Submit"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
document, err := c.parseResponseForDocumentAndErrors(response)
|
||||||
|
if err != nil {
|
||||||
|
switch err.Error() {
|
||||||
|
case errorInvalidTotpToken:
|
||||||
|
err = fmt.Errorf("invalid TOTP token value")
|
||||||
|
case errorTotpTokenReused:
|
||||||
|
err = fmt.Errorf("TOTP token was reused within its period (30 seconds)")
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
authenticated = document.Find("#_tlogout").Size() > 0
|
||||||
|
|
||||||
|
return authenticated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) authenticate() error {
|
||||||
|
|
||||||
|
if c.SessionFilePath != "" {
|
||||||
|
_ = c.loadSessionFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated, requiresTfa, err := c.authResumeSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authenticated {
|
||||||
|
// Only perform username and password login if two-factor authentication is not required at this stage
|
||||||
|
if !requiresTfa {
|
||||||
|
authenticated, requiresTfa, err = c.authUsernameAndPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only perform two-factor authentication if required
|
||||||
|
if requiresTfa {
|
||||||
|
authenticated, err = c.auth2FA()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authenticated {
|
||||||
|
err = fmt.Errorf("unknown authentication failure")
|
||||||
|
} else {
|
||||||
|
if c.SessionFilePath != "" {
|
||||||
|
err = c.saveSessionFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) listDomains() (map[string]uint64, error) {
|
||||||
|
response, err := c.httpClient.Get(apiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
document, err := goquery.NewDocumentFromReader(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we can list domains
|
||||||
|
if document.Find("#domains_table").Size() == 0 {
|
||||||
|
return nil, fmt.Errorf("domain listing failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all the forward & reverse domains
|
||||||
|
domains := make(map[string]uint64)
|
||||||
|
recordsSelector := strings.Join([]string{
|
||||||
|
"#domains_table > tbody > tr > td:last-child > img", // Forward records
|
||||||
|
"#tabs-advanced .generic_table > tbody > tr > td:last-child > img", // Reverse records
|
||||||
|
}, ", ")
|
||||||
|
|
||||||
|
document.Find(recordsSelector).EachWithBreak(func(index int, element *goquery.Selection) bool {
|
||||||
|
domainID, idExists := element.Attr("value")
|
||||||
|
domainName, nameExists := element.Attr("name")
|
||||||
|
if idExists && nameExists {
|
||||||
|
domains[domainName], err = strconv.ParseUint(domainID, 10, 64)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return domains, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) createDomain(domain string) error {
|
||||||
|
values := url.Values{
|
||||||
|
"action": {"add_zone"},
|
||||||
|
"retmain": {"0"},
|
||||||
|
"add_domain": {domain},
|
||||||
|
"submit": {"Add Domain!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.PostForm(apiEndpoint, values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
_, err = c.parseResponseForDocumentAndErrors(response)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) editZoneRecord(rc *models.RecordConfig, create bool) error {
|
||||||
|
values := url.Values{
|
||||||
|
"account": {},
|
||||||
|
"menu": {"edit_zone"},
|
||||||
|
"hosted_dns_zoneid": {strconv.FormatUint(rc.Original.(Record).ZoneID, 10)},
|
||||||
|
"hosted_dns_editzone": {"1"},
|
||||||
|
"TTL": {strconv.FormatUint(uint64(rc.TTL), 10)},
|
||||||
|
"Name": {rc.Name},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the correct mode and deal with the quirks
|
||||||
|
if create {
|
||||||
|
values.Set("Type", rc.Type)
|
||||||
|
values.Set("hosted_dns_editrecord", "Submit")
|
||||||
|
values.Set("hosted_dns_recordid", "")
|
||||||
|
} else {
|
||||||
|
values.Set("Type", strings.ToLower(rc.Type)) // Lowercase on update
|
||||||
|
values.Set("hosted_dns_editrecord", "Update")
|
||||||
|
values.Set("hosted_dns_recordid", strconv.FormatUint(rc.Original.(Record).RecordID, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle priorities
|
||||||
|
if create {
|
||||||
|
values.Set("Priority", "")
|
||||||
|
} else {
|
||||||
|
values.Set("Priority", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work out the content
|
||||||
|
switch rc.Type {
|
||||||
|
case "MX":
|
||||||
|
values.Set("Priority", strconv.FormatUint(uint64(rc.MxPreference), 10))
|
||||||
|
values.Set("Content", rc.Target)
|
||||||
|
case "SRV":
|
||||||
|
values.Del("Content")
|
||||||
|
values.Set("Target", rc.Target)
|
||||||
|
values.Set("Priority", strconv.FormatUint(uint64(rc.SrvPriority), 10))
|
||||||
|
values.Set("Weight", strconv.FormatUint(uint64(rc.SrvWeight), 10))
|
||||||
|
values.Set("Port", strconv.FormatUint(uint64(rc.SrvPort), 10))
|
||||||
|
default:
|
||||||
|
values.Set("Content", rc.GetTargetCombined())
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.PostForm(apiEndpoint, values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
_, err = c.parseResponseForDocumentAndErrors(response)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) deleteZoneRecord(rc *models.RecordConfig) error {
|
||||||
|
values := url.Values{
|
||||||
|
"menu": {"edit_zone"},
|
||||||
|
"hosted_dns_zoneid": {strconv.FormatUint(rc.Original.(Record).ZoneID, 10)},
|
||||||
|
"hosted_dns_recordid": {strconv.FormatUint(rc.Original.(Record).RecordID, 10)},
|
||||||
|
"hosted_dns_editzone": {"1"},
|
||||||
|
"hosted_dns_delrecord": {"1"},
|
||||||
|
"hosted_dns_delconfirm": {"delete"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.httpClient.PostForm(apiEndpoint, values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
_, err = c.parseResponseForDocumentAndErrors(response)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) generateCredentialHash() string {
|
||||||
|
hash := sha1.New()
|
||||||
|
hash.Write([]byte(c.Username))
|
||||||
|
hash.Write([]byte(c.Password))
|
||||||
|
hash.Write([]byte(c.TfaSecret))
|
||||||
|
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) saveSessionFile() error {
|
||||||
|
cookieDomain, err := url.Parse(apiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the credential hash on the first lines
|
||||||
|
entries := []string{
|
||||||
|
c.generateCredentialHash(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cookie := range c.httpClient.Jar.Cookies(cookieDomain) {
|
||||||
|
entries = append(entries, strings.Join([]string{cookie.Name, cookie.Value}, "="))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Join(c.SessionFilePath, sessionFileName)
|
||||||
|
err = ioutil.WriteFile(fileName, []byte(strings.Join(entries, "\n")), 0600)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) loadSessionFile() error {
|
||||||
|
cookieDomain, err := url.Parse(apiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := path.Join(c.SessionFilePath, sessionFileName)
|
||||||
|
bytes, err := ioutil.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// Skip loading the session.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies []*http.Cookie
|
||||||
|
for i, entry := range strings.Split(string(bytes), "\n") {
|
||||||
|
if i == 0 {
|
||||||
|
if entry != c.generateCredentialHash() {
|
||||||
|
return fmt.Errorf("invalid credential hash in session file")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kv := strings.Split(entry, "=")
|
||||||
|
if len(kv) == 2 {
|
||||||
|
cookies = append(cookies, &http.Cookie{
|
||||||
|
Name: kv[0],
|
||||||
|
Value: kv[1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.httpClient.Jar.SetCookies(cookieDomain, cookies)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HDNSProvider) parseResponseForDocumentAndErrors(response *http.Response) (document *goquery.Document, err error) {
|
||||||
|
var ignoredErrorMessages = [...]string{
|
||||||
|
errorImproperDelegation,
|
||||||
|
}
|
||||||
|
|
||||||
|
document, err = goquery.NewDocumentFromReader(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any errors ignoring irrelevant errors
|
||||||
|
document.Find("div#dns_err").EachWithBreak(func(index int, element *goquery.Selection) bool {
|
||||||
|
errorMessage := element.Text()
|
||||||
|
for _, ignoredMessage := range ignoredErrorMessages {
|
||||||
|
if strings.Contains(errorMessage, ignoredMessage) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = fmt.Errorf(element.Text())
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return document, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type elementParser struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *elementParser) parseStringAttr(element *goquery.Selection, attr string) (result string) {
|
||||||
|
if p.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, exists := element.Attr(attr)
|
||||||
|
if !exists {
|
||||||
|
p.err = fmt.Errorf("could not locate attribute %s", attr)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *elementParser) parseIntAttr(element *goquery.Selection, attr string) (result uint64) {
|
||||||
|
if p.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if value, exists := element.Attr(attr); exists {
|
||||||
|
result, p.err = strconv.ParseUint(value, 10, 64)
|
||||||
|
} else {
|
||||||
|
p.err = fmt.Errorf("could not locate attribute %s", attr)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *elementParser) parseStringElement(element *goquery.Selection) (result string) {
|
||||||
|
if p.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return element.Text()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *elementParser) parseIntElement(element *goquery.Selection) (result uint64) {
|
||||||
|
if p.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case to deal with Priority
|
||||||
|
if element.Text() == "-" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, p.err = strconv.ParseUint(element.Text(), 10, 64)
|
||||||
|
return result
|
||||||
|
}
|
Reference in New Issue
Block a user