mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-09-10 15:14:25 +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
|
||||
## 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:
|
||||
In your `creds.json` file you must provide your `dns.he.net` account username and password:
|
||||
|
||||
{% highlight json %}
|
||||
{
|
||||
|
@ -60,6 +57,35 @@ only available when first enabling two-factor authentication.
|
|||
}
|
||||
{% 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
|
||||
This provider does not recognize any special metadata fields unique to Hurricane Electric DNS.
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package hedns
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -70,14 +74,16 @@ const (
|
|||
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."
|
||||
|
||||
SessionFileName = ".hedns-session"
|
||||
)
|
||||
|
||||
type ApiClient struct {
|
||||
Username string
|
||||
Password string
|
||||
|
||||
TfaSecret string
|
||||
TfaValue string
|
||||
Username string
|
||||
Password string
|
||||
TfaSecret string
|
||||
TfaValue string
|
||||
SessionFilePath string
|
||||
|
||||
httpClient http.Client
|
||||
}
|
||||
|
@ -92,6 +98,7 @@ type Record struct {
|
|||
func NewProvider(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")
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
if totpValue == "" && totpSecret != "" {
|
||||
var err error
|
||||
totpValue, err = totp.GenerateCode(totpSecret, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the initial login
|
||||
client := NewApiClient(username, password, totpSecret)
|
||||
client := NewApiClient(username, password, totpSecret, totpValue, sessionFilePath)
|
||||
err := client.authenticate()
|
||||
return client, err
|
||||
}
|
||||
|
||||
func NewApiClient(username, password, tfaSecret string) *ApiClient {
|
||||
func NewApiClient(username, password, tfaSecret string, tfaValue string, sessionFilePath string) *ApiClient {
|
||||
client := ApiClient{
|
||||
Username: username,
|
||||
Password: password,
|
||||
TfaSecret: tfaSecret,
|
||||
Username: username,
|
||||
Password: password,
|
||||
TfaSecret: tfaSecret,
|
||||
TfaValue: tfaValue,
|
||||
SessionFilePath: sessionFilePath,
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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{
|
||||
"tfacode": {c.TfaValue},
|
||||
"submit": {"Submit"},
|
||||
|
@ -410,6 +419,11 @@ func (c *ApiClient) auth2FA() (authenticated bool, err error) {
|
|||
}
|
||||
|
||||
func (c *ApiClient) authenticate() error {
|
||||
|
||||
if c.SessionFilePath != "" {
|
||||
_ = c.loadSessionFile()
|
||||
}
|
||||
|
||||
authenticated, requiresTfa, err := c.authResumeSession()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -435,6 +449,10 @@ func (c *ApiClient) authenticate() error {
|
|||
|
||||
if !authenticated {
|
||||
err = fmt.Errorf("unknown authentication failure")
|
||||
} else {
|
||||
if c.SessionFilePath != "" {
|
||||
err = c.saveSessionFile()
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
@ -587,6 +605,72 @@ func (c *ApiClient) setZoneDynamicKey(rc *models.RecordConfig, key string) error
|
|||
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) {
|
||||
var ignoredErrorMessages = [...]string{
|
||||
ErrorImproperDelegation,
|
||||
|
|
Loading…
Add table
Reference in a new issue