From 9d4c60e67a962cbe29312647155a9eef9bd0bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Noack?= Date: Thu, 18 Jan 2024 21:51:57 +0100 Subject: [PATCH] inwx: wait before generating new TOTP TANs (#2084) Co-authored-by: Fernandez Ludovic --- providers/dns/inwx/inwx.go | 28 +++++++++++++++++++-- providers/dns/inwx/inwx_test.go | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go index ab134b0c..ee569c92 100644 --- a/providers/dns/inwx/inwx.go +++ b/providers/dns/inwx/inwx.go @@ -51,8 +51,9 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - config *Config - client *goinwx.Client + config *Config + client *goinwx.Client + previousUnlock time.Time } // NewDNSProvider returns a DNSProvider instance configured for Dyn DNS. @@ -202,10 +203,33 @@ func (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error { return errors.New("two-factor authentication but no shared secret is given") } + // INWX forbids re-authentication with a previously used TAN. + // To avoid using the same TAN twice, we wait until the next TOTP period. + sleep := d.computeSleep(time.Now()) + if sleep != 0 { + log.Infof("inwx: waiting %s for next TOTP token", sleep) + time.Sleep(sleep) + } + tan, err := totp.GenerateCode(d.config.SharedSecret, time.Now()) if err != nil { return err } + d.previousUnlock = time.Now() + return d.client.Account.Unlock(tan) } + +func (d *DNSProvider) computeSleep(now time.Time) time.Duration { + if d.previousUnlock.IsZero() { + return 0 * time.Second + } + + endPeriod := d.previousUnlock.Add(30 * time.Second) + if endPeriod.After(now) { + return endPeriod.Sub(now) + } + + return 0 * time.Second +} diff --git a/providers/dns/inwx/inwx_test.go b/providers/dns/inwx/inwx_test.go index 14dbd434..8053c879 100644 --- a/providers/dns/inwx/inwx_test.go +++ b/providers/dns/inwx/inwx_test.go @@ -2,8 +2,10 @@ package inwx import ( "testing" + "time" "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -141,3 +143,45 @@ func TestLivePresentAndCleanup(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func Test_computeSleep(t *testing.T) { + testCases := []struct { + desc string + previous string + expected time.Duration + }{ + { + desc: "after 30s", + previous: "2024-01-01T06:29:20Z", + expected: 0 * time.Second, + }, + { + desc: "0s", + previous: "2024-01-01T06:29:30Z", + expected: 0 * time.Second, + }, + { + desc: "before 30s", + previous: "2024-01-01T06:29:50Z", // 10 s + expected: 20 * time.Second, + }, + } + + now, err := time.Parse(time.RFC3339, "2024-01-01T06:30:00Z") + require.NoError(t, err) + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + previous, err := time.Parse(time.RFC3339, test.previous) + require.NoError(t, err) + + d := &DNSProvider{previousUnlock: previous} + + sleep := d.computeSleep(now) + assert.Equal(t, test.expected, sleep) + }) + } +}