Add initial dns.he.net provider support

This commit is contained in:
Robert Blenkinsopp 2020-08-21 14:12:11 +01:00
parent 59747a96f0
commit d2ded53119
11 changed files with 773 additions and 8 deletions

1
OWNERS
View file

@ -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

View file

@ -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

View file

@ -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&#39;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&#39;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>

75
docs/_providers/hedns.md Normal file
View file

@ -0,0 +1,75 @@
---
name: Hurricane Electric DNS
title: Hurricane Electric DNS Provider
layout: default
jsId: HEDNS
---
# Hurricane Electric DNS Provider
## Configuration
In your `creds.json` file you must provide your `dns.he.net` account username and password, along wit
In your `creds.json` file you must provide your INWX
username and password:
{% 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.
**Important Notes**:
* Anyone with access to this `creds.json` file will have *full* access to your Hurrican 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 %}
## 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 %}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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, ".")),
), ),

View file

@ -62,6 +62,12 @@
"project_id": "$GCLOUD_PROJECT", "project_id": "$GCLOUD_PROJECT",
"type": "$GCLOUD_TYPE" "type": "$GCLOUD_TYPE"
}, },
"HEDNS": {
"username": "$HEDNS_USERNAME",
"password": "$HEDNS_PASSWORD",
"totp-secret": "$HEDNS_TOTP_SECRET",
"domain": "$HEDNS_DOMAIN"
},
"HEXONET": { "HEXONET": {
"apientity": "$HEXONET_ENTITY", "apientity": "$HEXONET_ENTITY",
"apilogin": "$HEXONET_UID", "apilogin": "$HEXONET_UID",

View file

@ -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"

View file

@ -0,0 +1,616 @@
package hedns
import (
"encoding/json"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"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)
*/
const ApiUrl = "https://dns.he.net/"
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", NewProvider, features)
}
var defaultNameservers = []string{
"ns1.he.net",
"ns2.he.net",
"ns3.he.net",
"ns4.he.net",
"ns5.he.net",
}
type ApiClient struct {
Username string
Password string
TfaSecret string
TfaValue string
httpClient http.Client
}
type Record struct {
RecordName string
RecordId uint64
ZoneName string
ZoneId uint64
}
func NewProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
username, password := cfg["username"], cfg["password"]
totpSecret, totpValue := cfg["totp-key"], cfg["totp"]
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 := NewApiClient(username, password, totpSecret)
err := client.authenticate()
return client, err
}
func NewApiClient(username, password, tfaSecret string) *ApiClient {
client := ApiClient{
Username: username,
Password: password,
TfaSecret: tfaSecret,
}
// Create storage for the cookies
cookieJar, _ := cookiejar.New(nil)
client.httpClient = http.Client{Jar: cookieJar}
return &client
}
func (c *ApiClient) ListZones() ([]string, error) {
domainsMap, err := c.listDomains()
if err != nil {
return nil, err
}
// Get the list of the domains
domains := make([]string, 0, len(domainsMap))
for _, key := range domains {
domains = append(domains, key)
}
return domains, err
}
func (c *ApiClient) 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)
}
func (c *ApiClient) GetNameservers(_ string) ([]*models.Nameserver, error) {
return models.ToNameservers(defaultNameservers)
}
func (c *ApiClient) 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 := differ.IncrementalDiff(prunedRecords)
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
}
func (c *ApiClient) 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(ApiUrl)
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()
document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
return nil, err
}
// 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{}
recordId := parser.parseIntAttr(element, "id")
recordName := parser.parseStringElement(element.Find(".dns_view"))
recordType := parser.parseStringAttr(element.Find("td > .rrlabel"), "data")
recordData := parser.parseStringAttr(element.Find("td:nth-child(7)"), "data")
recordPriority := parser.parseIntElement(element.Find("td:nth-child(6)"))
recordTtl := parser.parseIntElement(element.Find("td:nth-child(5)"))
if parser.err != nil {
err = parser.err
return false
}
// Ignore record types that dnscontrol does not support
if recordType == "HINFO" || recordType == "AFSDB" || recordType == "RP" || recordType == "LOC" {
return true
}
rc := &models.RecordConfig{
Type: recordType,
TTL: uint32(recordTtl),
Original: Record{
ZoneName: domain,
ZoneId: domainId,
RecordName: recordName,
RecordId: recordId,
},
}
rc.SetLabelFromFQDN(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" {
recordData += "."
}
switch rc.Type {
case "ALIAS":
err = rc.SetTarget(recordData)
case "MX":
err = rc.SetTargetMX(uint16(recordPriority), recordData)
case "SRV":
err = rc.SetTargetSRVPriorityString(uint16(recordPriority), recordData)
case "SPF":
// Convert to TXT record as SPF is deprecated
rc.Type = "TXT"
fallthrough
default:
err = rc.PopulateFromString(rc.Type, recordData, domain)
}
if err != nil {
return false
}
zoneRecords = append(zoneRecords, rc)
return true
})
return zoneRecords, err
}
func (c *ApiClient) checkResponseForErrors(response *http.Response) error {
document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
return err
}
var ignoredErrorMessages = [...]string{
"This zone does not appear to be properly delegated to our nameservers.",
}
// 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 err
}
func (c *ApiClient) authGetCookie() error {
response, err := c.httpClient.Get(ApiUrl)
if err != nil {
return err
}
defer response.Body.Close()
return err
}
func (c *ApiClient) authUsernameAndPassword() (requiresTfa bool, err error) {
// Login with username and password
response, err := c.httpClient.PostForm(ApiUrl, url.Values{
"email": {c.Username},
"pass": {c.Password},
"submit": {"Login!"},
})
if err != nil {
return false, err
}
defer response.Body.Close()
if err = c.checkResponseForErrors(response); err != nil {
return false, err
}
// Check to see if a 2FA code is required.
document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
return false, err
}
if document.Find("input#tfacode").Size() > 0 {
return true, nil
}
// Completed and 2FA is not required
return false, err
}
func (c *ApiClient) auth2FA() error {
if c.TfaValue == "" && c.TfaSecret != "" {
var err error
c.TfaValue, err = totp.GenerateCode(c.TfaSecret, time.Now())
if err != nil {
return err
}
}
response, err := c.httpClient.PostForm(ApiUrl, url.Values{
"tfacode": {c.TfaValue},
"submit": {"Submit"},
})
if err != nil {
return err
}
defer response.Body.Close()
err = c.checkResponseForErrors(response)
return err
}
func (c *ApiClient) authenticate() error {
if err := c.authGetCookie(); err != nil {
return err
}
requiresTfa, err := c.authUsernameAndPassword()
if err != nil {
return err
}
if requiresTfa {
err = c.auth2FA()
if err != nil {
return err
}
}
return err
}
func (c *ApiClient) listDomains() (map[string]uint64, error) {
response, err := c.httpClient.Get(ApiUrl)
if err != nil {
return nil, err
}
defer response.Body.Close()
document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
return nil, err
}
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
}, ", ")
// Find all the forward & reverse domains
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 *ApiClient) createDomain(domain string) error {
values := url.Values{
"action": {"add_zone"},
"retmain": {"0"},
"add_domain": {domain},
"submit": {"Add Domain!"},
}
response, err := c.httpClient.PostForm(ApiUrl, values)
if err != nil {
return err
}
defer response.Body.Close()
err = c.checkResponseForErrors(response)
return err
}
func (c *ApiClient) 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(ApiUrl, values)
if err != nil {
return err
}
defer response.Body.Close()
err = c.checkResponseForErrors(response)
return err
}
func (c *ApiClient) 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(ApiUrl, values)
if err != nil {
return err
}
defer response.Body.Close()
err = c.checkResponseForErrors(response)
return err
}
func (c *ApiClient) setZoneDynamicKey(rc *models.RecordConfig, key string) error {
values := url.Values{
"account": {},
"menu": {"edit_zone"},
"hosted_dns_zoneid": {strconv.FormatUint(rc.Original.(Record).ZoneId, 10)},
"hosted_dns_recordid": {},
"hosted_dns_editzone": {"1"},
"Name": {rc.Original.(Record).RecordName},
"Key": {key},
"Key2": {key},
"generate_key": {"Submit"},
}
response, err := c.httpClient.PostForm(ApiUrl, values)
if err != nil {
return err
}
defer response.Body.Close()
err = c.checkResponseForErrors(response)
return 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
}