mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-12 08:04:28 +08:00
Add option to use .hedns-session
file to store sessions between runs
This commit is contained in:
parent
fc33c1dfdd
commit
ad96d44ab1
2 changed files with 132 additions and 22 deletions
|
@ -6,10 +6,7 @@ jsId: HEDNS
|
||||||
---
|
---
|
||||||
# Hurricane Electric DNS Provider
|
# Hurricane Electric DNS Provider
|
||||||
## Configuration
|
## 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 `dns.he.net` account username and password:
|
||||||
|
|
||||||
In your `creds.json` file you must provide your INWX
|
|
||||||
username and password:
|
|
||||||
|
|
||||||
{% highlight json %}
|
{% highlight json %}
|
||||||
{
|
{
|
||||||
|
@ -60,6 +57,35 @@ only available when first enabling two-factor authentication.
|
||||||
}
|
}
|
||||||
{% endhighlight %}
|
{% 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 location where a `.hedns-session` file will be created. This can be used to allow reuse of an
|
||||||
|
existing session, between runs, without full authentication.
|
||||||
|
|
||||||
|
When this key is not present, this option is disabled by default.
|
||||||
|
|
||||||
|
**Important Notes**:
|
||||||
|
* 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 your user can access.
|
||||||
|
|
||||||
|
{% highlight json %}
|
||||||
|
{
|
||||||
|
"hedns":{
|
||||||
|
"username": "yourUsername",
|
||||||
|
"password": "yourPassword",
|
||||||
|
"totp-key": "yourTOTPSharedSecret"
|
||||||
|
"session-file-path": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endhighlight %}
|
||||||
|
|
||||||
|
|
||||||
## Metadata
|
## Metadata
|
||||||
This provider does not recognize any special metadata fields unique to Hurricane Electric DNS.
|
This provider does not recognize any special metadata fields unique to Hurricane Electric DNS.
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package hedns
|
package hedns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -70,14 +74,16 @@ const (
|
||||||
ErrorTotpTokenRequired = "You must enter the token generated by your authenticator."
|
ErrorTotpTokenRequired = "You must enter the token generated by your authenticator."
|
||||||
ErrorTotpTokenReused = "This token has already been used. You may not reuse tokens."
|
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."
|
ErrorImproperDelegation = "This zone does not appear to be properly delegated to our nameservers."
|
||||||
|
|
||||||
|
SessionFileName = ".hedns-session"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiClient struct {
|
type ApiClient struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
||||||
TfaSecret string
|
TfaSecret string
|
||||||
TfaValue string
|
TfaValue string
|
||||||
|
SessionFilePath string
|
||||||
|
|
||||||
httpClient http.Client
|
httpClient http.Client
|
||||||
}
|
}
|
||||||
|
@ -92,6 +98,7 @@ type Record struct {
|
||||||
func NewProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
func NewProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
username, password := cfg["username"], cfg["password"]
|
username, password := cfg["username"], cfg["password"]
|
||||||
totpSecret, totpValue := cfg["totp-key"], cfg["totp"]
|
totpSecret, totpValue := cfg["totp-key"], cfg["totp"]
|
||||||
|
sessionFilePath := cfg["session-file-path"]
|
||||||
|
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, fmt.Errorf("username must be provided")
|
return nil, fmt.Errorf("username must be provided")
|
||||||
|
@ -103,17 +110,27 @@ func NewProvider(cfg map[string]string, _ json.RawMessage) (providers.DNSService
|
||||||
return nil, fmt.Errorf("totp and totp-key must not be specified at the same time")
|
return nil, fmt.Errorf("totp and totp-key must not be specified at the same time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if totpValue == "" && totpSecret != "" {
|
||||||
|
var err error
|
||||||
|
totpValue, err = totp.GenerateCode(totpSecret, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the initial login
|
// Perform the initial login
|
||||||
client := NewApiClient(username, password, totpSecret)
|
client := NewApiClient(username, password, totpSecret, totpValue, sessionFilePath)
|
||||||
err := client.authenticate()
|
err := client.authenticate()
|
||||||
return client, err
|
return client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiClient(username, password, tfaSecret string) *ApiClient {
|
func NewApiClient(username, password, tfaSecret string, tfaValue string, sessionFilePath string) *ApiClient {
|
||||||
client := ApiClient{
|
client := ApiClient{
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
Password: password,
|
||||||
TfaSecret: tfaSecret,
|
TfaSecret: tfaSecret,
|
||||||
|
TfaValue: tfaValue,
|
||||||
|
SessionFilePath: sessionFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create storage for the cookies
|
// Create storage for the cookies
|
||||||
|
@ -377,14 +394,6 @@ func (c *ApiClient) authUsernameAndPassword() (authenticated bool, requiresTfa b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) auth2FA() (authenticated bool, err error) {
|
func (c *ApiClient) auth2FA() (authenticated bool, err error) {
|
||||||
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(ApiUrl, url.Values{
|
response, err := c.httpClient.PostForm(ApiUrl, url.Values{
|
||||||
"tfacode": {c.TfaValue},
|
"tfacode": {c.TfaValue},
|
||||||
"submit": {"Submit"},
|
"submit": {"Submit"},
|
||||||
|
@ -410,6 +419,11 @@ func (c *ApiClient) auth2FA() (authenticated bool, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ApiClient) authenticate() error {
|
func (c *ApiClient) authenticate() error {
|
||||||
|
|
||||||
|
if c.SessionFilePath != "" {
|
||||||
|
_ = c.loadSessionFile()
|
||||||
|
}
|
||||||
|
|
||||||
authenticated, requiresTfa, err := c.authResumeSession()
|
authenticated, requiresTfa, err := c.authResumeSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -435,6 +449,10 @@ func (c *ApiClient) authenticate() error {
|
||||||
|
|
||||||
if !authenticated {
|
if !authenticated {
|
||||||
err = fmt.Errorf("unknown authentication failure")
|
err = fmt.Errorf("unknown authentication failure")
|
||||||
|
} else {
|
||||||
|
if c.SessionFilePath != "" {
|
||||||
|
err = c.saveSessionFile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -587,6 +605,72 @@ func (c *ApiClient) setZoneDynamicKey(rc *models.RecordConfig, key string) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ApiClient) generateCredentialHash() string {
|
||||||
|
hash := sha1.New()
|
||||||
|
hash.Write([]byte(c.Username))
|
||||||
|
hash.Write([]byte(c.Password))
|
||||||
|
//hash.Write([]byte(c.TfaValue))
|
||||||
|
hash.Write([]byte(c.TfaSecret))
|
||||||
|
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ApiClient) saveSessionFile() error {
|
||||||
|
cookieDomain, err := url.Parse(ApiUrl)
|
||||||
|
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 *ApiClient) loadSessionFile() error {
|
||||||
|
cookieDomain, err := url.Parse(ApiUrl)
|
||||||
|
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 *ApiClient) parseResponseForDocumentAndErrors(response *http.Response) (document *goquery.Document, err error) {
|
func (c *ApiClient) parseResponseForDocumentAndErrors(response *http.Response) (document *goquery.Document, err error) {
|
||||||
var ignoredErrorMessages = [...]string{
|
var ignoredErrorMessages = [...]string{
|
||||||
ErrorImproperDelegation,
|
ErrorImproperDelegation,
|
||||||
|
|
Loading…
Add table
Reference in a new issue