diff --git a/go.mod b/go.mod index 36340a9..949affa 100644 --- a/go.mod +++ b/go.mod @@ -4,91 +4,111 @@ go 1.20 require ( cloud.google.com/go/compute/metadata v0.2.3 - github.com/Azure/azure-sdk-for-go v32.4.0+incompatible + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 github.com/Azure/go-autorest/autorest v0.11.24 - github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 - github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 + github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 - github.com/aws/aws-sdk-go v1.39.0 - github.com/cenkalti/backoff/v4 v4.2.0 + github.com/aws/aws-sdk-go-v2 v1.19.0 + github.com/aws/aws-sdk-go-v2/config v1.18.28 + github.com/aws/aws-sdk-go-v2/credentials v1.13.27 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 + github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 + github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 + github.com/cenkalti/backoff/v4 v4.2.1 github.com/civo/civogo v0.3.11 - github.com/cloudflare/cloudflare-go v0.49.0 + github.com/cloudflare/cloudflare-go v0.70.0 github.com/cpu/goacmedns v0.1.1 - github.com/dnsimple/dnsimple-go v0.71.1 + github.com/dnsimple/dnsimple-go v1.2.0 github.com/entrustcorporation/entrust v0.0.0-20230314134457-b6b1cf0dd3bb - github.com/exoscale/egoscale v0.90.0 - github.com/go-acme/lego/v4 v4.11.0 + github.com/exoscale/egoscale v0.100.1 + github.com/go-acme/lego/v4 v4.14.2 github.com/go-jose/go-jose/v3 v3.0.0 github.com/google/go-querystring v1.1.0 github.com/gophercloud/gophercloud v1.0.0 github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae - github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/hashicorp/go-retryablehttp v0.7.4 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df github.com/infobloxopen/infoblox-go-client v1.1.1 github.com/labbsr0x/bindman-dns-webhook v1.0.2 - github.com/linode/linodego v1.9.1 + github.com/linode/linodego v1.17.2 github.com/liquidweb/liquidweb-go v1.6.3 - github.com/miekg/dns v1.1.50 - github.com/mimuret/golang-iij-dpf v0.7.1 + github.com/miekg/dns v1.1.55 + github.com/mimuret/golang-iij-dpf v0.9.1 github.com/mitchellh/mapstructure v1.5.0 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 github.com/nrdcg/auroradns v1.1.0 - github.com/nrdcg/desec v0.6.0 + github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 + github.com/nrdcg/desec v0.7.0 github.com/nrdcg/dnspod-go v0.4.0 github.com/nrdcg/freemyip v0.2.0 - github.com/nrdcg/goinwx v0.8.1 + github.com/nrdcg/goinwx v0.8.2 github.com/nrdcg/namesilo v0.2.1 github.com/nrdcg/nodion v0.1.0 - github.com/nrdcg/porkbun v0.1.1 + github.com/nrdcg/porkbun v0.2.0 + github.com/nzdjb/go-metaname v1.0.0 github.com/oracle/oci-go-sdk v24.3.0+incompatible - github.com/ovh/go-ovh v1.1.0 - github.com/pquerna/otp v1.3.0 - github.com/sacloud/api-client-go v0.2.1 - github.com/sacloud/iaas-api-go v1.3.2 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 - github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 - github.com/sirupsen/logrus v1.9.0 - github.com/softlayer/softlayer-go v1.0.6 - github.com/stretchr/testify v1.8.1 + github.com/ovh/go-ovh v1.4.2 + github.com/pquerna/otp v1.4.0 + github.com/sacloud/api-client-go v0.2.8 + github.com/sacloud/iaas-api-go v1.11.1 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 + github.com/sirupsen/logrus v1.9.3 + github.com/softlayer/softlayer-go v1.1.2 + github.com/stretchr/testify v1.8.4 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 - github.com/transip/gotransip/v6 v6.17.0 - github.com/ultradns/ultradns-go-sdk v1.4.0-20221107152238-f3f1d1d + github.com/transip/gotransip/v6 v6.20.0 + github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c github.com/vinyldns/go-vinyldns v0.9.16 github.com/vultr/govultr/v2 v2.17.2 - github.com/weppos/publicsuffix-go v0.30.0 + github.com/weppos/publicsuffix-go v0.30.1 github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 - golang.org/x/net v0.8.0 - golang.org/x/oauth2 v0.6.0 + golang.org/x/net v0.12.0 + golang.org/x/oauth2 v0.9.0 golang.org/x/time v0.3.0 google.golang.org/api v0.111.0 - gopkg.in/ns1/ns1-go.v2 v2.6.5 + gopkg.in/ns1/ns1-go.v2 v2.7.6 gopkg.in/yaml.v2 v2.4.0 ) require ( cloud.google.com/go/compute v1.18.0 // indirect + github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect + github.com/aws/smithy-go v1.13.5 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.9.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/structs v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect - github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/go-resty/resty/v2 v2.7.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.3.0 // indirect @@ -100,34 +120,36 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect - github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b // indirect + github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/labbsr0x/goh v1.0.1 // indirect github.com/liquidweb/go-lwApi v0.0.5 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sacloud/go-http v0.1.2 // indirect - github.com/sacloud/packages-go v0.0.5 // indirect + github.com/sacloud/go-http v0.1.6 // indirect + github.com/sacloud/packages-go v0.0.9 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/spf13/cast v1.3.1 // indirect github.com/stretchr/objx v0.5.0 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.10.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f1d6c31..00c675f 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,7 @@ cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -17,16 +18,30 @@ cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v32.4.0+incompatible h1:1JP8SKfroEakYiQU2ZyPDosh8w2Tg9UopKt88VyQPt4= -github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= +github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 h1:8kDqDngH+DmVBiCtIjCFTGa7MBnsIOkF9IccInFEbjk= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 h1:8iR6OLffWWorFdzL2JFCab5xpD8VKEE2DUBBl+HNTDY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0/go.mod h1:copqlcjMWc/wgQ1N2fzsJFQxDdqKGg1EQt8T5wJMOGE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.1.2 h1:mLY+pNLjCUeKhgnAJWAKhEUQM+RJQo2H1fuGSw1Ky1E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 h1:rR8ZW79lE/ppfXTfiYSnMFv5EzmVuY4pfZWIkscIJ64= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0/go.mod h1:y2zXtLSMM/X5Mfawq0lOftpWn3f4V6OCsRdINsvWBPI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= -github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= @@ -35,19 +50,20 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPu github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1 h1:5BIsppVPdWJA29Yb5cYawQYeh5geN413WxAgBZvEtdA= -github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.1/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= +github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 h1:J45/QHgrzUdqe/Vco/Vxk0wRvdS2nKUxmf/zLgvfass= @@ -58,8 +74,34 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 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/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo= -github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k= +github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw= +github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A= +github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA= +github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco= +github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U= +github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -67,10 +109,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -80,8 +123,9 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/civo/civogo v0.3.11 h1:mON/fyrV946Sbk6paRtOSGsN+asCgCmHCgArf5xmGxM= github.com/civo/civogo v0.3.11/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cloudflare-go v0.49.0 h1:KqJYk/YQ5ZhmyYz1oa4kGDskfF1gVuZfqesaJ/XDLto= -github.com/cloudflare/cloudflare-go v0.49.0/go.mod h1:h0QgcIZ3qEXwFiwfBO8sQxjVdYsLX+PfD7NFEnANaKg= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/cloudflare-go v0.70.0 h1:4opGbUygM8DjirUuaz23jn3akuAcnOCEx+0nQtQEcFo= +github.com/cloudflare/cloudflare-go v0.70.0/go.mod h1:VW6GuazkaZ4xEDkFt24lkXQUsE8q7BiGqDniC2s8WEM= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -107,8 +151,9 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/dnsimple/dnsimple-go v0.71.1 h1:1hGoBA3CIjpjZj5DM3081xfxr4e2jYmYnkO2VuBF8Qc= -github.com/dnsimple/dnsimple-go v0.71.1/go.mod h1:F9WHww9cC76hrnwGFfAfrqdW99j3MOYasQcIwTS/aUk= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM= +github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U= github.com/entrustcorporation/entrust v0.0.0-20230314134457-b6b1cf0dd3bb h1:xQBHq0AL4UXflRL7EH/t4FrVygprNA0C0X+BqWjK9i0= github.com/entrustcorporation/entrust v0.0.0-20230314134457-b6b1cf0dd3bb/go.mod h1:NyddGpZcZoMDrK0AfWPiFn74sGdwBfyjK3nqZYb/Q6w= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -118,8 +163,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/exoscale/egoscale v0.90.0 h1:DZBXVU3iHqu5Ju5lQ5jWVlPo0IpI98SUo8Aa1UQVrmo= -github.com/exoscale/egoscale v0.90.0/go.mod h1:wyXE5zrnFynMXA0jMhwQqSe24CfUhmBk2WI5wFZcq6Y= +github.com/exoscale/egoscale v0.100.1 h1:iXsV1Ei7daqe/6FYSCSDyrFs1iUG1l1X9qNh2uMw6z0= +github.com/exoscale/egoscale v0.100.1/go.mod h1:BAb9p4rmyU+Wl400CJZO5270H2sXtdsZjLcm5xMKkz4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -127,15 +172,15 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-acme/lego/v4 v4.11.0 h1:oIPoU7zBJoTfoVrbqk62+/2NsGCSgCVK1JtZSZZ28SU= -github.com/go-acme/lego/v4 v4.11.0/go.mod h1:dENL0J3/WughN2NLy0T35otK5k1EWCmXTwCw0+X5ZaE= +github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHGM= +github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= @@ -155,8 +200,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= -github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA= @@ -168,8 +213,9 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= @@ -204,9 +250,11 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= +github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -255,9 +303,8 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -279,9 +326,8 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhK github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU= github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= -github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -301,8 +347,8 @@ github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcM github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc= -github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= +github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -314,6 +360,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labbsr0x/bindman-dns-webhook v1.0.2 h1:I7ITbmQPAVwrDdhd6dHKi+MYJTJqPCK0jE6YNBAevnk= github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= github.com/labbsr0x/goh v1.0.1 h1:97aBJkDjpyBZGPbQuOK5/gHcSFbcr5aRsq3RSRJFpPk= @@ -329,8 +377,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++ github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/linode/linodego v1.9.1 h1:29UpEPpYcGFnbwiJW8mbk/bjBZpgd/pv68io2IKTo34= -github.com/linode/linodego v1.9.1/go.mod h1:h6AuFR/JpqwwM/vkj7s8KV3iGN8/jxn+zc437F8SZ8w= +github.com/linode/linodego v1.17.2 h1:b32dj4662PGG5P9qVa6nBezccWdqgukndlMIuPGq1CQ= +github.com/linode/linodego v1.17.2/go.mod h1:C2iyT3Vg2O2sPxkWka4XAQ5WSUtm5LmTZ3Adw43Ra7Q= github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA= github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs= @@ -354,18 +402,19 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/mimuret/golang-iij-dpf v0.7.1 h1:MHEZKx6gNGTvq1+3PYUNfTZ/qtGNNK4+zo+0Rdo4jY4= -github.com/mimuret/golang-iij-dpf v0.7.1/go.mod h1:IXWYcQVIHYzuM+W7kDWX0mseHDfUoqMuarxMXHVTir0= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34= +github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -393,23 +442,27 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo= github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk= -github.com/nrdcg/desec v0.6.0 h1:kZ9JtsYEW3LNfuPIM+2tXoxoQlF9koWfQTWTQsA7Sr8= -github.com/nrdcg/desec v0.6.0/go.mod h1:wybWg5cRrNmtXLYpUCPCLvz4jfFNEGZQEnoUiX9WqcY= +github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 h1:qpB3wZR4+MPK92cTC9zZPnndkJgDgPvQqPUAgVc1NXU= +github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9/go.mod h1:HUoHXDrFvidN1NK9Wb/mZKNOfDNutKkzF2Pg71M9hHA= +github.com/nrdcg/desec v0.7.0 h1:iuGhi4pstF3+vJWwt292Oqe2+AsSPKDynQna/eu1fDs= +github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= github.com/nrdcg/freemyip v0.2.0 h1:/GscavT4GVqAY13HExl5UyoB4wlchv6Cg5NYDGsUoJ8= github.com/nrdcg/freemyip v0.2.0/go.mod h1:HjF0Yz0lSb37HD2ihIyGz9esyGcxbCrrGFLPpKevbx4= -github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU= -github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= +github.com/nrdcg/goinwx v0.8.2 h1:RmjiHlEA+lzi3toXyPSaE6hWnBQ0+G+1u7w8C6Fpp4g= +github.com/nrdcg/goinwx v0.8.2/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= -github.com/nrdcg/porkbun v0.1.1 h1:gxVzQYfFUGXhnBax/aVugoE3OIBAdHgrJgyMPyY5Sjo= -github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA= +github.com/nrdcg/porkbun v0.2.0 h1:ghaqPtIKcffba99epWFkK3VWf6TKJT9WMXMgaTqv95Y= +github.com/nrdcg/porkbun v0.2.0/go.mod h1:i0uLMn9ItFsLsSQIAeEu1wQ9/+6EvX1eQw15hulMMRw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9Hg= +github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -425,13 +478,15 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU= github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= -github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk= -github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA= +github.com/ovh/go-ovh v1.4.2 h1:ub4jVK6ERbiBTo4y5wbLCjeKCjGY+K36e7BviW+MaAU= +github.com/ovh/go-ovh v1.4.2/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -442,8 +497,8 @@ github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= -github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -468,32 +523,32 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sacloud/api-client-go v0.2.1 h1:jl02ZG6cM+mcH4eDYg0cxCFFuTOVTOjUCLYL4UbP09U= -github.com/sacloud/api-client-go v0.2.1/go.mod h1:8fmYy5OpT3W8ltV5ZxF8evultNwKpduGN4YKmU9Af7w= -github.com/sacloud/go-http v0.1.2 h1:a84HkeDHxDD1vIA6HiOT72a3fwwJueZBwuGP6zVtEJU= -github.com/sacloud/go-http v0.1.2/go.mod h1:gvWaT8LFBFnSBFVrznOQXC62uad46bHZQM8w+xoH3eE= -github.com/sacloud/iaas-api-go v1.3.2 h1:03obrdVdv/bGHK9p6CV7Uzg+ot2gLsddUMevm9DDZqQ= -github.com/sacloud/iaas-api-go v1.3.2/go.mod h1:CoqpRYBG2NRB5xfqTfZNyh2lVLKyLkE/HV9ISqmbhGc= -github.com/sacloud/packages-go v0.0.5 h1:NXTQNyyp/3ugM4CANtLBJLejFESzfWu4GPUURN4NJrA= -github.com/sacloud/packages-go v0.0.5/go.mod h1:XWMBSNHT9YKY3lCh6yJsx1o1RRQQGpuhNqJA6bSHdD4= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= +github.com/sacloud/api-client-go v0.2.8 h1:tIY6PZNBX900K66TqEPa4d6UIbedUczfCBnPJkzi8kw= +github.com/sacloud/api-client-go v0.2.8/go.mod h1:0CV/kWNYlS1hCNdnk6Wx7Wdg8DPFCnv0zOIzdXjeAeY= +github.com/sacloud/go-http v0.1.6 h1:lJGXDt9xrxJiDszRPaN9NIP8MVj10YKMzmnyzdSfI8w= +github.com/sacloud/go-http v0.1.6/go.mod h1:oLAHoDJRkptf8sq4fE8oERLkdCh0kJWfWu+paoJY7I0= +github.com/sacloud/iaas-api-go v1.11.1 h1:2MsFZ4H1uRdRVx2nVXuERWQ3swoFc3XreIV5hJ3Nsws= +github.com/sacloud/iaas-api-go v1.11.1/go.mod h1:uBDSa06F/V0OnoR66jGdbH0PVnCJw+NeE9RVbVgMfss= +github.com/sacloud/packages-go v0.0.9 h1:GbinkBLC/eirFhHpLjoDW6JV7+95Rnd2d8RWj7Afeks= +github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIWIk1OA7x9j2Q= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 h1:1WuWJu7/e8SqK+uQl7lfk/N/oMZTL2NE/TJsNKRNMc4= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c= -github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 h1:hp2CYQUINdZMHdvTdXtPOY2ainKl4IoMcpAXEf2xj3Q= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= -github.com/softlayer/softlayer-go v1.0.6 h1:wMyWmnTm0y3iNwwUJLacgSpMjxAW42MaVqWW4CwYb3c= -github.com/softlayer/softlayer-go v1.0.6/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qyx2MNvYyHof7ik4= +github.com/softlayer/softlayer-go v1.1.2 h1:rUSSGCyaxymvTOsaFjwr+cGxA8muw3xg2LSrIMNcN/c= +github.com/softlayer/softlayer-go v1.1.2/go.mod h1:hvAbzGH4LRXA6yXY8BNx99yoqZ7urfDdtl9mvBf0G+g= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -523,32 +578,31 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz27tVi2r70JYnm5y0Zk8w0Qzsx+vfUw3oqSyrEfP8= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/transip/gotransip/v6 v6.17.0 h1:2RCyqYqz5+Ej8z96EyE4sf6tQrrfEBaFDO0LliSl6+8= -github.com/transip/gotransip/v6 v6.17.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= +github.com/transip/gotransip/v6 v6.20.0 h1:AuvwyOZ51f2brzMbTqlRy/wmaM3kF7Vx5Wds8xcDflY= +github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= -github.com/ultradns/ultradns-go-sdk v1.4.0-20221107152238-f3f1d1d h1:pLMpEtrkiaeA2NY6CzA2+K75YnY6c5ka02SbxQ9YgSo= -github.com/ultradns/ultradns-go-sdk v1.4.0-20221107152238-f3f1d1d/go.mod h1:IgdoVzrGYzq4H4IGI0DAVnM3CbcuQDSxEP4s/j6cztI= +github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c h1:mKnW6IGLw7uXu6DL6RitufZWcXS6hCnauXRUFof7rKM= +github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c/go.mod h1:F4UyVEmq4/m5lAmx+GccrxyRCXmnBjzUL09JLTQFp94= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ= github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= -github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/weppos/publicsuffix-go v0.30.0 h1:QHPZ2GRu/YE7cvejH9iyavPOkVCB4dNxp2ZvtT+vQLY= -github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY= -github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE= +github.com/weppos/publicsuffix-go v0.30.1 h1:8q+QwBS1MY56Zjfk/50ycu33NN8aa1iCCEQwo/71Oos= +github.com/weppos/publicsuffix-go v0.30.1/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -557,7 +611,6 @@ github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f h1:cG+ehP github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 h1:2wzke3JH7OtN20WsNDZx2VH/TCmsbqtDEbXzjF+i05E= github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997/go.mod h1:2CHKs/YGbCcNn/BPaCkEBwKz/FNCELi+MLILjR9RaTA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -591,8 +644,9 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -611,13 +665,13 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -634,9 +688,6 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -646,18 +697,22 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= +golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -668,7 +723,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -705,27 +761,33 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -735,8 +797,10 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -762,14 +826,14 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -826,6 +890,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -841,13 +906,12 @@ gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0= gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ns1/ns1-go.v2 v2.6.5 h1:nzf3RXP4TEZLeZl7q9t6eav4htlNlWuYX+pXVUitlf0= -gopkg.in/ns1/ns1-go.v2 v2.6.5/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA= +gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/providers/allinkl/internal/types_api.go b/providers/allinkl/internal/types_api.go index 9207dc1..145163c 100644 --- a/providers/allinkl/internal/types_api.go +++ b/providers/allinkl/internal/types_api.go @@ -54,7 +54,7 @@ type DNSRequest struct { // --- type GetDNSSettingsAPIResponse struct { - Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` + Response GetDNSSettingsResponse `json:"Response" mapstructure:"Response"` } type GetDNSSettingsResponse struct { diff --git a/providers/arvancloud/arvancloud.go b/providers/arvancloud/arvancloud.go index 2e2eca8..214d4c8 100644 --- a/providers/arvancloud/arvancloud.go +++ b/providers/arvancloud/arvancloud.go @@ -99,8 +99,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { }, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/autodns/autodns.go b/providers/autodns/autodns.go index 65568b9..660cf6b 100644 --- a/providers/autodns/autodns.go +++ b/providers/autodns/autodns.go @@ -122,8 +122,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Value: info.Value, }} - // TODO(ldez) replace domain by FQDN to follow CNAME. - _, err := d.client.AddTxtRecords(context.Background(), domain, records) + _, err := d.client.AddTxtRecords(context.Background(), info.EffectiveFQDN, records) if err != nil { return fmt.Errorf("autodns: %w", err) } @@ -142,8 +141,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { Value: info.Value, }} - // TODO(ldez) replace domain by FQDN to follow CNAME. - if err := d.client.RemoveTXTRecords(context.Background(), domain, records); err != nil { + if err := d.client.RemoveTXTRecords(context.Background(), info.EffectiveFQDN, records); err != nil { return fmt.Errorf("autodns: %w", err) } diff --git a/providers/azure/azure.go b/providers/azure/azure.go index 502abc2..f651949 100644 --- a/providers/azure/azure.go +++ b/providers/azure/azure.go @@ -84,6 +84,7 @@ type DNSProvider struct { // If the credentials are _not_ set via the environment, // then it will attempt to get a bearer token via the instance metadata service. // see: https://github.com/Azure/go-autorest/blob/v10.14.0/autorest/azure/auth/auth.go#L38-L42 +// Deprecated: use azuredns instead. func NewDNSProvider() (*DNSProvider, error) { config := NewDefaultConfig() @@ -118,6 +119,7 @@ func NewDNSProvider() (*DNSProvider, error) { } // NewDNSProviderConfig return a DNSProvider instance configured for Azure. +// Deprecated: use azuredns instead. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("azure: the configuration of the DNS provider is nil") @@ -202,7 +204,7 @@ func getAuthorizer(config *Config) (autorest.Authorizer, error) { return auth.NewAuthorizerFromEnvironment() } -// Fetches metadata from environment or he instance metadata service. +// Fetches metadata from environment or the instance metadata service. // borrowed from https://github.com/Microsoft/azureimds/blob/master/imdssample.go func getMetadata(config *Config, field string) (string, error) { metadataEndpoint := config.MetadataEndpoint diff --git a/providers/azure/azure.toml b/providers/azure/azure.toml index 164512e..c4e3b67 100644 --- a/providers/azure/azure.toml +++ b/providers/azure/azure.toml @@ -1,4 +1,4 @@ -Name = "Azure" +Name = "Azure (deprecated)" Description = '''''' URL = "https://azure.microsoft.com/services/dns/" Code = "azure" diff --git a/providers/azure/private.go b/providers/azure/private.go index f936707..624897a 100644 --- a/providers/azure/private.go +++ b/providers/azure/private.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" + "github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/entrustcorporation/dv/dns01" diff --git a/providers/azure/public.go b/providers/azure/public.go index 4efcd0b..b5c4375 100644 --- a/providers/azure/public.go +++ b/providers/azure/public.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns" + "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/to" "github.com/entrustcorporation/dv/dns01" diff --git a/providers/azuredns/azuredns.go b/providers/azuredns/azuredns.go new file mode 100644 index 0000000..af5b828 --- /dev/null +++ b/providers/azuredns/azuredns.go @@ -0,0 +1,226 @@ +// Package azuredns implements a DNS provider for solving the DNS-01 challenge using azure DNS. +// Azure doesn't like trailing dots on domain names, most of the acme code does. +package azuredns + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/platform/config/env" +) + +// Environment variables names. +const ( + envNamespace = "AZURE_" + + EnvEnvironment = envNamespace + "ENVIRONMENT" + EnvSubscriptionID = envNamespace + "SUBSCRIPTION_ID" + EnvResourceGroup = envNamespace + "RESOURCE_GROUP" + EnvZoneName = envNamespace + "ZONE_NAME" + EnvPrivateZone = envNamespace + "PRIVATE_ZONE" + + EnvTenantID = envNamespace + "TENANT_ID" + EnvClientID = envNamespace + "CLIENT_ID" + EnvClientSecret = envNamespace + "CLIENT_SECRET" + + EnvAuthMethod = envNamespace + "AUTH_METHOD" + EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + SubscriptionID string + ResourceGroup string + PrivateZone bool + + Environment cloud.Configuration + + // optional if using default Azure credentials + ClientID string + ClientSecret string + TenantID string + + AuthMethod string + AuthMSITimeout time.Duration + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 60), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), + Environment: cloud.AzurePublic, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + provider challenge.ProviderTimeout +} + +// NewDNSProvider returns a DNSProvider instance configured for azuredns. +func NewDNSProvider() (*DNSProvider, error) { + config := NewDefaultConfig() + + environmentName := env.GetOrFile(EnvEnvironment) + if environmentName != "" { + switch environmentName { + case "china": + config.Environment = cloud.AzureChina + case "public": + config.Environment = cloud.AzurePublic + case "usgovernment": + config.Environment = cloud.AzureGovernment + default: + return nil, fmt.Errorf("azuredns: unknown environment %s", environmentName) + } + } else { + config.Environment = cloud.AzurePublic + } + + config.SubscriptionID = env.GetOrFile(EnvSubscriptionID) + config.ResourceGroup = env.GetOrFile(EnvResourceGroup) + config.PrivateZone = env.GetOrDefaultBool(EnvPrivateZone, false) + + config.ClientID = env.GetOrFile(EnvClientID) + config.ClientSecret = env.GetOrFile(EnvClientSecret) + config.TenantID = env.GetOrFile(EnvTenantID) + + config.AuthMethod = env.GetOrFile(EnvAuthMethod) + config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Azure. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("azuredns: the configuration of the DNS provider is nil") + } + + credentials, err := getCredentials(config) + if err != nil { + return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err) + } + + if config.SubscriptionID == "" { + return nil, errors.New("azuredns: SubscriptionID is missing") + } + + if config.ResourceGroup == "" { + return nil, errors.New("azuredns: ResourceGroup is missing") + } + + var dnsProvider challenge.ProviderTimeout + if config.PrivateZone { + dnsProvider, err = NewDNSProviderPrivate(config, credentials) + if err != nil { + return nil, fmt.Errorf("azuredns: %w", err) + } + } else { + dnsProvider, err = NewDNSProviderPublic(config, credentials) + if err != nil { + return nil, fmt.Errorf("azuredns: %w", err) + } + } + + return &DNSProvider{provider: dnsProvider}, nil +} + +func getCredentials(config *Config) (azcore.TokenCredential, error) { + clientOptions := azcore.ClientOptions{Cloud: config.Environment} + + switch strings.ToLower(config.AuthMethod) { + case "env": + if config.ClientID != "" && config.ClientSecret != "" && config.TenantID != "" { + return azidentity.NewClientSecretCredential(config.TenantID, config.ClientID, config.ClientSecret, + &azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions}) + } + + return azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{ClientOptions: clientOptions}) + + case "wli": + return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: clientOptions}) + + case "msi": + cred, err := azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions}) + if err != nil { + return nil, err + } + + return &timeoutTokenCredential{cred: cred, timeout: config.AuthMSITimeout}, nil + + case "cli": + return azidentity.NewAzureCLICredential(nil) + + default: + return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions}) + } +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.provider.Timeout() +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + return d.provider.Present(domain, token, keyAuth) +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + return d.provider.CleanUp(domain, token, keyAuth) +} + +// timeoutTokenCredential wraps a TokenCredential to add a timeout. +type timeoutTokenCredential struct { + cred azcore.TokenCredential + timeout time.Duration +} + +// GetToken implements the azcore.TokenCredential interface. +func (w *timeoutTokenCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + if w.timeout <= 0 { + return w.cred.GetToken(ctx, opts) + } + + ctxTimeout, cancel := context.WithTimeout(ctx, w.timeout) + defer cancel() + + tk, err := w.cred.GetToken(ctxTimeout, opts) + if ce := ctxTimeout.Err(); errors.Is(ce, context.DeadlineExceeded) { + return tk, azidentity.NewCredentialUnavailableError("managed identity timed out") + } + + w.timeout = 0 + + return tk, err +} + +func deref[T string | int | int32 | int64](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/azuredns/azuredns.toml b/providers/azuredns/azuredns.toml new file mode 100644 index 0000000..0aea2bd --- /dev/null +++ b/providers/azuredns/azuredns.toml @@ -0,0 +1,181 @@ +Name = "Azure DNS" +Description = '''''' +URL = "https://azure.microsoft.com/services/dns/" +Code = "azuredns" +Since = "v4.13.0" + +Example = ''' +### Using client secret + +AZURE_CLIENT_ID= \ +AZURE_TENANT_ID= \ +AZURE_CLIENT_SECRET= \ +lego --domains example.com --email your_example@email.com --dns azuredns run + +### Using client certificate + +AZURE_CLIENT_ID= \ +AZURE_TENANT_ID= \ +AZURE_CLIENT_CERTIFICATE_PATH= \ +lego --domains example.com --email your_example@email.com --dns azuredns run + +### Using Azure CLI + +az login \ +lego --domains example.com --email your_example@email.com --dns azuredns run + +### Using Managed Identity (Azure VM) + +AZURE_TENANT_ID= \ +AZURE_SUBSCRIPTION_ID= \ +AZURE_RESOURCE_GROUP= \ +lego --domains example.com --email your_example@email.com --dns azuredns run + +### Using Managed Identity (Azure Arc) + +AZURE_TENANT_ID= \ +AZURE_SUBSCRIPTION_ID= \ +AZURE_RESOURCE_GROUP= \ +IMDS_ENDPOINT=http://localhost:40342 \ +IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token \ +lego --domains example.com --email your_example@email.com --dns azuredns run + +''' + +Additional = ''' +## Description + +Several authentication methods can be used to authenticate against Azure DNS API. + +### Default Azure Credentials (default option) + +Default Azure Credentials automatically detects in the following locations and prioritized in the following order: + +1. Environment variables for client secret: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET` +2. Environment variables for client certificate: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_CERTIFICATE_PATH` +3. Workload identity for resources hosted in Azure environment (see below) +4. Shared credentials (defaults to `~/.azure` folder), used by Azure CLI + +Link: +- [Azure Authentication](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication) + +### Environment variables + +#### Client secret + +The Azure Credentials can be configured using the following environment variables: +* AZURE_CLIENT_ID = "Client ID" +* AZURE_CLIENT_SECRET = "Client secret" +* AZURE_TENANT_ID = "Tenant ID" + +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`. + +#### Client certificate + +The Azure Credentials can be configured using the following environment variables: +* AZURE_CLIENT_ID = "Client ID" +* AZURE_CLIENT_CERTIFICATE_PATH = "Client certificate path" +* AZURE_TENANT_ID = "Tenant ID" + +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `env`. + +### Workload identity + +Workload identity allows workloads running Azure Kubernetes Services (AKS) clusters to authenticate as an Azure AD application identity using federated credentials. + +This must be configured in kubernetes workload deployment in one hand and on the Azure AD application registration in the other hand. + +Here is a summary of the steps to follow to use it : +* create a `ServiceAccount` resource, add following annotations to reference the targeted Azure AD application registration : `azure.workload.identity/client-id` and `azure.workload.identity/tenant-id`. +* on the `Deployment` resource you must reference the previous `ServiceAccount` and add the following label : `azure.workload.identity/use: "true"`. +* create a fedreated credentials of type `Kubernetes accessing Azure resources`, add the cluster issuer URL and add the namespace and name of your kubernetes service account. + +Link : +- [Azure AD Workload identity](https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html) + +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `wli`. + +### Azure Managed Identity + +#### Azure Managed Identity (with Azure workload) + +The Azure Managed Identity service allows linking Azure AD identities to Azure resources, without needing to manually manage client IDs and secrets. + +Workloads with a Managed Identity can manage their own certificates, with permissions on specific domain names set using IAM assignments. +For this to work, the Managed Identity requires the **Reader** role on the target DNS Zone, +and the **DNS Zone Contributor** on the relevant `_acme-challenge` TXT records. + +For example, to allow a Managed Identity to create a certificate for "fw01.lab.example.com", using Azure CLI: + +```bash +export AZURE_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000" +export AZURE_RESOURCE_GROUP="rg1" +export SERVICE_PRINCIPAL_ID="00000000-0000-0000-0000-000000000000" + +export AZURE_DNS_ZONE="lab.example.com" +export AZ_HOSTNAME="fw01" +export AZ_RECORD_SET="_acme-challenge.${AZ_HOSTNAME}" + +az role assignment create \ +--assignee "${SERVICE_PRINCIPAL_ID}" \ +--role "Reader" \ +--scope "/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}" + +az role assignment create \ +--assignee "${SERVICE_PRINCIPAL_ID}" \ +--role "DNS Zone Contributor" \ +--scope "/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}/providers/Microsoft.Network/dnszones/${AZURE_DNS_ZONE}/TXT/${AZ_RECORD_SET}" +``` + +A timeout wrapper is configured for this authentication method. +The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`. +The default timeout is 2 seconds. +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`. + +#### Azure Managed Identity (with Azure Arc) + +The Azure Arc agent provides the ability to use a Managed Identity on resources hosted outside of Azure +(such as on-prem virtual machines, or VMs in another cloud provider). + +While the upstream `azidentity` SDK will try to automatically identify and use the Azure Arc metadata service, +if you get `azuredns: DefaultAzureCredential: failed to acquire a token.` error messages, +you may need to set the environment variables: +* `IMDS_ENDPOINT=http://localhost:40342` +* `IDENTITY_ENDPOINT=http://localhost:40342/metadata/identity/oauth2/token` + +A timeout wrapper is configured for this authentication method. +The duraction can be configured by setting the `AZURE_AUTH_MSI_TIMEOUT`. +The default timeout is 2 seconds. +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `msi`. + +### Azure CLI + +The Azure CLI is a command-line tool provided by Microsoft to interact with Azure resources. +It provides an easy way to authenticate by simply running `az login` command. +The generated token will be cached by default in the `~/.azure` folder. + +This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`. + +''' + +[Configuration] + [Configuration.Credentials] + AZURE_CLIENT_ID = "Client ID" + AZURE_CLIENT_SECRET = "Client secret" + AZURE_TENANT_ID = "Tenant ID" + AZURE_CLIENT_CERTIFICATE_PATH = "Client certificate path" + AZURE_SUBSCRIPTION_ID = "DNS zone subscription ID" + AZURE_RESOURCE_GROUP = "DNS zone resource group" + [Configuration.Additional] + AZURE_ENVIRONMENT = "Azure environment, one of: public, usgovernment, and china" + AZURE_PRIVATE_ZONE = "Set to true to use Azure Private DNS Zones and not public" + AZURE_ZONE_NAME = "Zone name to use inside Azure DNS service to add the TXT record in" + AZURE_AUTH_METHOD = "Specify which authentication method to use" + AZURE_AUTH_MSI_TIMEOUT = "Managed Identity timeout duration" + AZURE_TTL = "The TTL of the TXT record used for the DNS challenge" + AZURE_POLLING_INTERVAL = "Time between DNS propagation check" + AZURE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + +[Links] + API = "https://docs.microsoft.com/en-us/go/azure/" + GoClient = "https://github.com/Azure/azure-sdk-for-go" diff --git a/providers/azuredns/azuredns_test.go b/providers/azuredns/azuredns_test.go new file mode 100644 index 0000000..cf15055 --- /dev/null +++ b/providers/azuredns/azuredns_test.go @@ -0,0 +1,168 @@ +package azuredns + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvEnvironment, + EnvSubscriptionID, + EnvResourceGroup). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvEnvironment: "", + EnvSubscriptionID: "A", + EnvResourceGroup: "B", + }, + }, + { + desc: "unknown environment", + envVars: map[string]string{ + EnvEnvironment: "test", + EnvSubscriptionID: "A", + EnvResourceGroup: "B", + }, + expected: "azuredns: unknown environment test", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected != "" { + require.EqualError(t, err, test.expected) + return + } + + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.provider) + + assert.IsType(t, p.provider, new(DNSProviderPublic)) + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + subscriptionID string + resourceGroup string + privateZone bool + handler func(w http.ResponseWriter, r *http.Request) + expected string + }{ + { + desc: "success (public)", + subscriptionID: "A", + resourceGroup: "B", + privateZone: false, + }, + { + desc: "success (private)", + subscriptionID: "A", + resourceGroup: "B", + privateZone: true, + }, + { + desc: "SubscriptionID missing", + subscriptionID: "", + resourceGroup: "", + expected: "azuredns: SubscriptionID is missing", + }, + { + desc: "ResourceGroup missing", + subscriptionID: "A", + resourceGroup: "", + expected: "azuredns: ResourceGroup is missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.SubscriptionID = test.subscriptionID + config.ResourceGroup = test.resourceGroup + config.PrivateZone = test.privateZone + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + if test.handler == nil { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) + } else { + mux.HandleFunc("/", test.handler) + } + + p, err := NewDNSProviderConfig(config) + + if test.expected != "" { + require.EqualError(t, err, test.expected) + return + } + + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.provider) + + if test.privateZone { + assert.IsType(t, p.provider, new(DNSProviderPrivate)) + } else { + assert.IsType(t, p.provider, new(DNSProviderPublic)) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/azuredns/private.go b/providers/azuredns/private.go new file mode 100644 index 0000000..5317d64 --- /dev/null +++ b/providers/azuredns/private.go @@ -0,0 +1,154 @@ +package azuredns + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +// DNSProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS. +type DNSProviderPrivate struct { + config *Config + zoneClient *armprivatedns.PrivateZonesClient + recordClient *armprivatedns.RecordSetsClient +} + +// NewDNSProviderPrivate creates a DNSProviderPrivate structure with initialized Azure clients. +func NewDNSProviderPrivate(config *Config, credentials azcore.TokenCredential) (*DNSProviderPrivate, error) { + options := arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Cloud: config.Environment, + }, + } + + zoneClient, err := armprivatedns.NewPrivateZonesClient(config.SubscriptionID, credentials, &options) + if err != nil { + return nil, err + } + + recordClient, err := armprivatedns.NewRecordSetsClient(config.SubscriptionID, credentials, &options) + if err != nil { + return nil, err + } + + return &DNSProviderPrivate{ + config: config, + zoneClient: zoneClient, + recordClient: recordClient, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProviderPrivate) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProviderPrivate) Present(domain, _, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + // Get existing record set + rset, err := d.recordClient.Get(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, nil) + if err != nil { + var respErr *azcore.ResponseError + if !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("azuredns: %w", err) + } + } + + // Construct unique TXT records using map + uniqRecords := map[string]struct{}{info.Value: {}} + if rset.RecordSet.Properties != nil && rset.RecordSet.Properties.TxtRecords != nil { + for _, txtRecord := range rset.RecordSet.Properties.TxtRecords { + // Assume Value doesn't contain multiple strings + if len(txtRecord.Value) > 0 { + uniqRecords[deref(txtRecord.Value[0])] = struct{}{} + } + } + } + + var txtRecords []*armprivatedns.TxtRecord + for txt := range uniqRecords { + txtRecord := txt + txtRecords = append(txtRecords, &armprivatedns.TxtRecord{Value: []*string{&txtRecord}}) + } + + ttlInt64 := int64(d.config.TTL) + rec := armprivatedns.RecordSet{ + Name: &subDomain, + Properties: &armprivatedns.RecordSetProperties{ + TTL: &ttlInt64, + TxtRecords: txtRecords, + }, + } + + _, err = d.recordClient.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, rec, nil) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + _, err = d.recordClient.Delete(ctx, d.config.ResourceGroup, zone, armprivatedns.RecordTypeTXT, subDomain, nil) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + return nil +} + +// Checks that azure has a zone for this domain name. +func (d *DNSProviderPrivate) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { + if zone := env.GetOrFile(EnvZoneName); zone != "" { + return zone, nil + } + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", err + } + + zone, err := d.zoneClient.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone), nil) + if err != nil { + return "", err + } + + // zone.Name shouldn't have a trailing dot(.) + return dns01.UnFqdn(deref(zone.Name)), nil +} diff --git a/providers/azuredns/public.go b/providers/azuredns/public.go new file mode 100644 index 0000000..bcbdf2e --- /dev/null +++ b/providers/azuredns/public.go @@ -0,0 +1,154 @@ +package azuredns + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" +) + +// DNSProviderPublic implements the challenge.Provider interface for Azure Public Zone DNS. +type DNSProviderPublic struct { + config *Config + zoneClient *armdns.ZonesClient + recordClient *armdns.RecordSetsClient +} + +// NewDNSProviderPublic creates a DNSProviderPublic structure with intialised Azure clients. +func NewDNSProviderPublic(config *Config, credentials azcore.TokenCredential) (*DNSProviderPublic, error) { + options := arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Cloud: config.Environment, + }, + } + + zoneClient, err := armdns.NewZonesClient(config.SubscriptionID, credentials, &options) + if err != nil { + return nil, err + } + + recordClient, err := armdns.NewRecordSetsClient(config.SubscriptionID, credentials, &options) + if err != nil { + return nil, err + } + + return &DNSProviderPublic{ + config: config, + zoneClient: zoneClient, + recordClient: recordClient, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProviderPublic) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProviderPublic) Present(domain, _, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + // Get existing record set + rset, err := d.recordClient.Get(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, nil) + if err != nil { + var respErr *azcore.ResponseError + if !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound { + return fmt.Errorf("azuredns: %w", err) + } + } + + // Construct unique TXT records using map + uniqRecords := map[string]struct{}{info.Value: {}} + if rset.RecordSet.Properties != nil && rset.RecordSet.Properties.TxtRecords != nil { + for _, txtRecord := range rset.RecordSet.Properties.TxtRecords { + // Assume Value doesn't contain multiple strings + if len(txtRecord.Value) > 0 { + uniqRecords[deref(txtRecord.Value[0])] = struct{}{} + } + } + } + + var txtRecords []*armdns.TxtRecord + for txt := range uniqRecords { + txtRecord := txt + txtRecords = append(txtRecords, &armdns.TxtRecord{Value: []*string{&txtRecord}}) + } + + ttlInt64 := int64(d.config.TTL) + rec := armdns.RecordSet{ + Name: &subDomain, + Properties: &armdns.RecordSetProperties{ + TTL: &ttlInt64, + TxtRecords: txtRecords, + }, + } + + _, err = d.recordClient.CreateOrUpdate(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, rec, nil) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProviderPublic) CleanUp(domain, _, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + zone, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + _, err = d.recordClient.Delete(ctx, d.config.ResourceGroup, zone, subDomain, armdns.RecordTypeTXT, nil) + if err != nil { + return fmt.Errorf("azuredns: %w", err) + } + + return nil +} + +// Checks that azure has a zone for this domain name. +func (d *DNSProviderPublic) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { + if zone := env.GetOrFile(EnvZoneName); zone != "" { + return zone, nil + } + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", err + } + + zone, err := d.zoneClient.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone), nil) + if err != nil { + return "", err + } + + // zone.Name shouldn't have a trailing dot(.) + return dns01.UnFqdn(deref(zone.Name)), nil +} diff --git a/providers/bluecat/internal/client.go b/providers/bluecat/internal/client.go index 7568ba5..b92bd06 100644 --- a/providers/bluecat/internal/client.go +++ b/providers/bluecat/internal/client.go @@ -200,7 +200,7 @@ func (c *Client) LookupViewID(ctx context.Context, configName, viewName string) return view.ID, nil } -// LookupParentZoneID Return the entityId of the parent zone by recursing from the root view. +// LookupParentZoneID returns the entityId of the parent zone by iterating through the root labels. // Also return the simple name of the host. func (c *Client) LookupParentZoneID(ctx context.Context, viewID uint, fqdn string) (uint, string, error) { if fqdn == "" { diff --git a/providers/bunny/bunny.go b/providers/bunny/bunny.go index 8719ea3..06a3ec7 100644 --- a/providers/bunny/bunny.go +++ b/providers/bunny/bunny.go @@ -9,7 +9,8 @@ import ( "github.com/entrustcorporation/dv/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/simplesurance/bunny-go" + "github.com/miekg/dns" + "github.com/nrdcg/bunny-go" ) const minTTL = 60 @@ -81,8 +82,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, client: client}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } @@ -190,7 +191,28 @@ func getZone(fqdn string) (string, error) { return "", err } - return dns01.UnFqdn(authZone), nil + zone, _, err := splitDomain(dns01.UnFqdn(authZone)) + if err != nil { + return "", err + } + + return zone, nil +} + +func splitDomain(full string) (string, string, error) { + split := dns.Split(full) + if len(split) < 2 { + return "", "", fmt.Errorf("unsupported domain: %s", full) + } + + if len(split) == 2 { + return full, "", nil + } + + domain := full[split[len(split)-2]:] + subDomain := full[:split[len(split)-2]-1] + + return domain, subDomain, nil } func pointer[T string | int | int32 | int64](v T) *T { return &v } diff --git a/providers/bunny/bunny.toml b/providers/bunny/bunny.toml index 3290786..93ccfad 100644 --- a/providers/bunny/bunny.toml +++ b/providers/bunny/bunny.toml @@ -19,4 +19,4 @@ lego --email you@example.com --dns bunny --domains my.example.org run [Links] API = "https://docs.bunny.net/reference/dnszonepublic_index" - bunny-go = "https://github.com/simplesurance/bunny-go" + bunny-go = "https://github.com/nrdcg/bunny-go" diff --git a/providers/bunny/bunny_test.go b/providers/bunny/bunny_test.go index e5724bc..744febe 100644 --- a/providers/bunny/bunny_test.go +++ b/providers/bunny/bunny_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -123,3 +124,83 @@ func TestLiveCleanUp(t *testing.T) { err = provider.CleanUp(envTest.GetDomain(), "", "123d==") require.NoError(t, err) } + +func Test_splitDomain(t *testing.T) { + type expected struct { + root string + sub string + requireErr require.ErrorAssertionFunc + } + + testCases := []struct { + desc string + domain string + expected expected + }{ + { + desc: "empty", + domain: "", + expected: expected{ + requireErr: require.Error, + }, + }, + { + desc: "2 levels", + domain: "example.com", + expected: expected{ + root: "example.com", + sub: "", + requireErr: require.NoError, + }, + }, + { + desc: "3 levels", + domain: "_acme-challenge.example.com", + expected: expected{ + root: "example.com", + sub: "_acme-challenge", + requireErr: require.NoError, + }, + }, + { + desc: "4 levels", + domain: "_acme-challenge.sub.example.com", + expected: expected{ + root: "example.com", + sub: "_acme-challenge.sub", + requireErr: require.NoError, + }, + }, + { + desc: "5 levels", + domain: "_acme-challenge.my.sub.example.com", + expected: expected{ + root: "example.com", + sub: "_acme-challenge.my.sub", + requireErr: require.NoError, + }, + }, + { + desc: "6 levels", + domain: "_acme-challenge.my.sub.sub.example.com", + expected: expected{ + root: "example.com", + sub: "_acme-challenge.my.sub.sub", + requireErr: require.NoError, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + root, sub, err := splitDomain(test.domain) + test.expected.requireErr(t, err) + + assert.Equal(t, test.expected.root, root) + assert.Equal(t, test.expected.sub, sub) + }) + } +} diff --git a/providers/cloudflare/cloudflare.go b/providers/cloudflare/cloudflare.go index 253890b..eb98041 100644 --- a/providers/cloudflare/cloudflare.go +++ b/providers/cloudflare/cloudflare.go @@ -63,7 +63,7 @@ type DNSProvider struct { // For a more paranoid setup, provide CLOUDFLARE_DNS_API_TOKEN and CLOUDFLARE_ZONE_API_TOKEN. // // The email and API key should be avoided, if possible. -// Instead setup a API token with both Zone:Read and DNS:Edit permission, and pass the CLOUDFLARE_DNS_API_TOKEN environment variable. +// Instead, set up an API token with both Zone:Read and DNS:Edit permission, and pass the CLOUDFLARE_DNS_API_TOKEN environment variable. // You can split the Zone:Read and DNS:Edit permissions across multiple API tokens: // in this case pass both CLOUDFLARE_ZONE_API_TOKEN and CLOUDFLARE_DNS_API_TOKEN accordingly. func NewDNSProvider() (*DNSProvider, error) { @@ -134,7 +134,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err) } - dnsRecord := cloudflare.DNSRecord{ + dnsRecord := cloudflare.CreateDNSRecordParams{ Type: "TXT", Name: dns01.UnFqdn(info.EffectiveFQDN), Content: info.Value, @@ -146,15 +146,11 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("cloudflare: failed to create TXT record: %w", err) } - if !response.Success { - return fmt.Errorf("cloudflare: failed to create TXT record: %+v %+v", response.Errors, response.Messages) - } - d.recordIDsMu.Lock() - d.recordIDs[token] = response.Result.ID + d.recordIDs[token] = response.ID d.recordIDsMu.Unlock() - log.Infof("cloudflare: new record for %s, ID %s", domain, response.Result.ID) + log.Infof("cloudflare: new record for %s, ID %s", domain, response.ID) return nil } diff --git a/providers/cloudflare/cloudflare.toml b/providers/cloudflare/cloudflare.toml index 9852a2f..514790b 100644 --- a/providers/cloudflare/cloudflare.toml +++ b/providers/cloudflare/cloudflare.toml @@ -33,7 +33,7 @@ very specific access can be granted to your resources at Cloudflare. See this [Cloudflare announcement](https://blog.cloudflare.com/api-tokens-general-availability/) for details. The main resources Lego cares for are the DNS entries for your Zones. -It also need to resolve a domain name to an internal Zone ID in order to manipulate DNS entries. +It also needs to resolve a domain name to an internal Zone ID in order to manipulate DNS entries. Hence, you should create an API token with the following permissions: diff --git a/providers/cloudflare/wrapper.go b/providers/cloudflare/wrapper.go index 79cc41d..c92da5b 100644 --- a/providers/cloudflare/wrapper.go +++ b/providers/cloudflare/wrapper.go @@ -59,16 +59,16 @@ func newClient(config *Config) (*metaClient, error) { }, nil } -func (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) { - return m.clientEdit.CreateDNSRecord(ctx, zoneID, rr) +func (m *metaClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) { + return m.clientEdit.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), rr) } -func (m *metaClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) { - return m.clientEdit.DNSRecords(ctx, zoneID, rr) +func (m *metaClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) { + return m.clientEdit.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), rr) } func (m *metaClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error { - return m.clientEdit.DeleteDNSRecord(ctx, zoneID, recordID) + return m.clientEdit.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), recordID) } func (m *metaClient) ZoneIDByName(fdqn string) (string, error) { diff --git a/providers/cloudru/cloudru.go b/providers/cloudru/cloudru.go new file mode 100644 index 0000000..ad9db6e --- /dev/null +++ b/providers/cloudru/cloudru.go @@ -0,0 +1,200 @@ +// Package cloudru implements a DNS provider for solving the DNS-01 challenge using cloud.ru DNS. +package cloudru + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/cloudru/internal" +) + +// Environment variables names. +const ( + envNamespace = "CLOUDRU_" + + EnvServiceInstanceID = envNamespace + "SERVICE_INSTANCE_ID" + EnvKeyID = envNamespace + "KEY_ID" + EnvSecret = envNamespace + "SECRET" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ServiceInstanceID string + KeyID string + Secret string + + PropagationTimeout time.Duration + PollingInterval time.Duration + SequenceInterval time.Duration + HTTPClient *http.Client + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +type DNSProvider struct { + config *Config + client *internal.Client + records map[string]*internal.Record + recordsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for cloud.ru. +// Credentials must be passed in the environment variables: +// CLOUDRU_SERVICE_INSTANCE_ID, CLOUDRU_KEY_ID, and CLOUDRU_SECRET. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvServiceInstanceID, EnvKeyID, EnvSecret) + if err != nil { + return nil, fmt.Errorf("cloudru: %w", err) + } + + config := NewDefaultConfig() + config.ServiceInstanceID = values[EnvServiceInstanceID] + config.KeyID = values[EnvKeyID] + config.Secret = values[EnvSecret] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for cloud.ru. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("cloudru: the configuration of the DNS provider is nil") + } + + if config.ServiceInstanceID == "" || config.KeyID == "" || config.Secret == "" { + return nil, errors.New("cloudru: some credentials information are missing") + } + + client := internal.NewClient(config.KeyID, config.Secret) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + records: make(map[string]*internal.Record), + }, nil +} + +// Present creates a TXT record using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("cloudru: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + authZone = dns01.UnFqdn(authZone) + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("cloudru: %w", err) + } + + zone, err := d.getZoneInformationByName(ctx, d.config.ServiceInstanceID, authZone) + if err != nil { + return fmt.Errorf("cloudru: could not find zone information (ServiceInstanceID: %s, zone: %s): %w", d.config.ServiceInstanceID, authZone, err) + } + + record := internal.Record{ + Name: info.EffectiveFQDN, + Type: "TXT", + Values: []string{info.Value}, + TTL: strconv.Itoa(d.config.TTL), + } + + newRecord, err := d.client.CreateRecord(ctx, zone.ID, record) + if err != nil { + return fmt.Errorf("cloudru: could not create record: %w", err) + } + + d.recordsMu.Lock() + d.records[token] = newRecord + d.recordsMu.Unlock() + + return nil +} + +// CleanUp removes a given record that was generated by Present. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + d.recordsMu.Lock() + record, ok := d.records[token] + d.recordsMu.Unlock() + + if !ok { + return fmt.Errorf("cloudru: unknown recordID for %q", info.EffectiveFQDN) + } + + ctx, err := d.client.CreateAuthenticatedContext(context.Background()) + if err != nil { + return fmt.Errorf("cloudru: %w", err) + } + + err = d.client.DeleteRecord(ctx, record.ZoneID, record.Name, "TXT") + if err != nil { + return fmt.Errorf("cloudru: %w", err) + } + + d.recordsMu.Lock() + delete(d.records, token) + d.recordsMu.Unlock() + + return nil +} + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getZoneInformationByName(ctx context.Context, parentID, name string) (internal.Zone, error) { + zs, err := d.client.GetZones(ctx, parentID) + if err != nil { + return internal.Zone{}, err + } + + for _, element := range zs { + if element.Name == name { + return element, nil + } + } + + return internal.Zone{}, errors.New("could not find Zone record") +} diff --git a/providers/cloudru/cloudru.toml b/providers/cloudru/cloudru.toml new file mode 100644 index 0000000..19faf8d --- /dev/null +++ b/providers/cloudru/cloudru.toml @@ -0,0 +1,27 @@ +Name = "Cloud.ru" +Description = '''''' +URL = "https://cloud.ru" +Code = "cloudru" +Since = "v4.14.0" + +Example = ''' +CLOUDRU_SERVICE_INSTANCE_ID=ppp \ +CLOUDRU_KEY_ID=xxx \ +CLOUDRU_SECRET=yyy \ +lego --email you@example.com --dns cloudru --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + CLOUDRU_SERVICE_INSTANCE_ID = "Service Instance ID (parentId)" + CLOUDRU_KEY_ID = "Key ID (login)" + CLOUDRU_SECRET = "Key Secret" + [Configuration.Additional] + CLOUDRU_POLLING_INTERVAL = "Time between DNS propagation check" + CLOUDRU_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + CLOUDRU_TTL = "The TTL of the TXT record used for the DNS challenge" + CLOUDRU_HTTP_TIMEOUT = "API request timeout" + CLOUDRU_SEQUENCE_INTERVAL = "Time between sequential requests" + +[Links] + API = "https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref.html" diff --git a/providers/cloudru/cloudru_test.go b/providers/cloudru/cloudru_test.go new file mode 100644 index 0000000..88addde --- /dev/null +++ b/providers/cloudru/cloudru_test.go @@ -0,0 +1,176 @@ +package cloudru + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvServiceInstanceID, + EnvKeyID, + EnvSecret). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvServiceInstanceID: "123", + EnvKeyID: "user", + EnvSecret: "secret", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID,CLOUDRU_KEY_ID,CLOUDRU_SECRET", + }, + { + desc: "missing service instance ID", + envVars: map[string]string{ + EnvServiceInstanceID: "", + EnvKeyID: "user", + EnvSecret: "secret", + }, + expected: "cloudru: some credentials information are missing: CLOUDRU_SERVICE_INSTANCE_ID", + }, + { + desc: "missing key ID", + envVars: map[string]string{ + EnvServiceInstanceID: "123", + EnvKeyID: "", + EnvSecret: "secret", + }, + expected: "cloudru: some credentials information are missing: CLOUDRU_KEY_ID", + }, + { + desc: "missing secret", + envVars: map[string]string{ + EnvServiceInstanceID: "123", + EnvKeyID: "user", + EnvSecret: "", + }, + expected: "cloudru: some credentials information are missing: CLOUDRU_SECRET", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + serviceInstanceID string + keyID string + secret string + expected string + }{ + { + desc: "success", + serviceInstanceID: "123", + keyID: "user", + secret: "secret", + }, + { + desc: "missing credentials", + expected: "cloudru: some credentials information are missing", + }, + { + desc: "missing service instance ID", + serviceInstanceID: "", + keyID: "user", + secret: "secret", + expected: "cloudru: some credentials information are missing", + }, + { + desc: "missing key ID", + serviceInstanceID: "123", + keyID: "", + secret: "secret", + expected: "cloudru: some credentials information are missing", + }, + { + desc: "missing secret", + serviceInstanceID: "123", + keyID: "user", + secret: "", + expected: "cloudru: some credentials information are missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.ServiceInstanceID = test.serviceInstanceID + config.KeyID = test.keyID + config.Secret = test.secret + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(2 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/cloudru/internal/client.go b/providers/cloudru/internal/client.go new file mode 100644 index 0000000..7909107 --- /dev/null +++ b/providers/cloudru/internal/client.go @@ -0,0 +1,174 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" +) + +// Default API endpoints. +const ( + APIBaseURL = "https://console.sbercloud.ru/api/clouddns/v1" + AuthBaseURL = "https://auth.iam.sbercloud.ru/auth/system/openid/token" +) + +// Client the Cloud.ru API client. +type Client struct { + keyID string + secret string + + APIEndpoint *url.URL + AuthEndpoint *url.URL + HTTPClient *http.Client + + token *Token + muToken sync.Mutex +} + +// NewClient Creates a new Client. +func NewClient(login, secret string) *Client { + apiEndpoint, _ := url.Parse(APIBaseURL) + authEndpoint, _ := url.Parse(AuthBaseURL) + + return &Client{ + keyID: login, + secret: secret, + APIEndpoint: apiEndpoint, + AuthEndpoint: authEndpoint, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +func (c *Client) GetZones(ctx context.Context, parentID string) ([]Zone, error) { + endpoint := c.APIEndpoint.JoinPath("zones") + + query := endpoint.Query() + query.Set("parentId", parentID) + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var zones APIResponse[Zone] + err = c.do(req, &zones) + if err != nil { + return nil, err + } + + return zones.Items, nil +} + +func (c *Client) GetRecords(ctx context.Context, zoneID string) ([]Record, error) { + endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var records APIResponse[Record] + err = c.do(req, &records) + if err != nil { + return nil, err + } + + return records.Items, nil +} + +func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) { + endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return nil, err + } + + var result Record + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (c *Client) DeleteRecord(ctx context.Context, zoneID, name, recordType string) error { + endpoint := c.APIEndpoint.JoinPath("zones", zoneID, "records", name, recordType) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c *Client) do(req *http.Request, result any) error { + tok := getToken(req.Context()) + if tok != nil { + req.Header.Set("Authorization", "Bearer "+tok.AccessToken) + } else { + return fmt.Errorf("not logged in") + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + if result == nil { + return nil + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} diff --git a/providers/cloudru/internal/client_test.go b/providers/cloudru/internal/client_test.go new file mode 100644 index 0000000..d96183d --- /dev/null +++ b/providers/cloudru/internal/client_test.go @@ -0,0 +1,159 @@ +package internal + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, handler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.APIEndpoint, _ = url.Parse(server.URL) + client.token = &Token{ + AccessToken: "secret", + ExpiresIn: 60, + TokenType: "Bearer", + Deadline: time.Now().Add(1 * time.Minute), + } + + return client +} + +func writeFixtureHandler(method, filename string) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + defer func() { _ = file.Close() }() + + _, _ = io.Copy(rw, file) + } +} + +func TestClient_GetZones(t *testing.T) { + client := setupTest(t, "/zones", writeFixtureHandler(http.MethodGet, "zones.json")) + + ctx := mockContext() + + zones, err := client.GetZones(ctx, "xxx") + require.NoError(t, err) + + expected := []Zone{ + { + ID: "59556fcd-95ff-451f-b49b-9732f21f944a", + ParentID: "2d7b6194-2b83-4f71-86fd-a1e727e347b2", + Name: "example.com", + Valid: true, + Delegated: true, + CreatedAt: time.Date(2023, 7, 23, 8, 12, 41, 0, time.UTC), + UpdatedAt: time.Date(2023, 7, 24, 5, 50, 28, 0, time.UTC), + }, + } + assert.Equal(t, expected, zones) +} + +func TestClient_GetRecords(t *testing.T) { + client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodGet, "records.json")) + + ctx := mockContext() + + records, err := client.GetRecords(ctx, "zzz") + require.NoError(t, err) + + expected := []Record{ + { + ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a", + Name: "example.com.", + Type: "SOA", + Values: []string{ + "cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600", + }, + TTL: "3600", + Enables: true, + }, + { + ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a", + Name: "example.com.", + Type: "NS", + Values: []string{ + "cdns-ns01.sbercloud.ru.", + "cdns-ns02.sbercloud.ru.", + }, + TTL: "3600", + Enables: true, + }, + { + ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a", + Name: "www.example.com.", + Type: "A", + Values: []string{ + "8.8.8.8", + }, + TTL: "3600", + Enables: true, + }, + } + assert.Equal(t, expected, records) +} + +func TestClient_CreateRecord(t *testing.T) { + client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodPost, "record.json")) + + ctx := mockContext() + + recordReq := Record{ + Name: "www.example.com.", + Type: "TXT", + Values: []string{"text"}, + TTL: "3600", + } + + record, err := client.CreateRecord(ctx, "zzz", recordReq) + require.NoError(t, err) + + expected := &Record{ + ZoneID: "59556fcd-95ff-451f-b49b-9732f21f944a", + Name: "www.example.com.", + Type: "TXT", + Values: []string{ + "txt", + }, + TTL: "3600", + Enables: true, + } + assert.Equal(t, expected, record) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := setupTest(t, "/zones/zzz/records/example.com/TXT", writeFixtureHandler(http.MethodDelete, "record.json")) + + ctx := mockContext() + + err := client.DeleteRecord(ctx, "zzz", "example.com", "TXT") + require.NoError(t, err) +} diff --git a/providers/cloudru/internal/fixtures/auth-error.json b/providers/cloudru/internal/fixtures/auth-error.json new file mode 100644 index 0000000..31cf629 --- /dev/null +++ b/providers/cloudru/internal/fixtures/auth-error.json @@ -0,0 +1,4 @@ +{ + "error": "invalid_client", + "error_description": "client not found" +} diff --git a/providers/cloudru/internal/fixtures/auth.json b/providers/cloudru/internal/fixtures/auth.json new file mode 100644 index 0000000..e803665 --- /dev/null +++ b/providers/cloudru/internal/fixtures/auth.json @@ -0,0 +1,8 @@ +{ + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiJlYzk0ZWJhNC03NzU2LTRjNjQtYmNmMC0zMzYxODIwNWM5ODkiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJCZWFyZXIifQ.hhPr-Xr_NbyRwrqGoqeepthWfpfmD47RjzHUwo2lVPkeMiL8AMWzDPRxs-8gns9eTSHZCoAH0RjyrBnTaOrztInM72h8_rIIFr0MMPIIqrUkp2id_alya9eoiSWg_69PzNZ2CKWJDylL8o4Vi9_cSBYp-6H1xNcOAvO4a9xkNCoGGiogjHWNFq64qnS_P6fYY-pl9leuprCeq1GAKPODevHwzmc4gkEZIj_15SUh_ofJRJICgyLmkELQ8a0wDGYmZcdNKiGQDpd7rHaGrOvO1k8IJHfgs5aCMyuHXybTg6AMlodpYs8MBdk6K_VFY-cxSRB8ocq_Q7Hgt9qaRADg2Q", + "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImEyMjM3ZDhhLWQ0ZDQtNDA5Yi04ZTMxLWM3NGJhYTZhM2NjYiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaWFtIl0sImF1dGhfdGltZSI6MTY5MDA1Mzg1MiwiYXpwIjoiOWE0M2I0OTM1ZDRhMDc5NmRkYjE0Mjk0NjUxYjk2NzciLCJlbWFpbCI6ImxlZ29AOTlmN2I5NzItZmZlYS00OTkyLTgyN2EtY2M4MDYzOTg1MmNhLmlhbS5zYmVyY2xvdWQucnUiLCJleHAiOjE2OTAwNTc0NTIsImlhdCI6MTY5MDA1Mzg1MiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmlhbS5zYmVyY2xvdWQucnUvYXV0aC9zeXN0ZW0iLCJqdGkiOiIxNDRmMDRlNS1jYjZkLTQ2NTktODJhMi0yMmE5MDQwNGZlZjAiLCJuYmYiOjE2OTAwNTM4NTIsIm5vbmNlIjoiIiwicmVhbG1fYWNjZXNzIjpudWxsLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJvbGVzIiwic3ViIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX2lkIjoiZmVhMzRmYTUtZmE5ZS00OTdkLTk5MDAtNjdhYWJjOWNkZWJjIiwic3ViX3R5cGUiOiJzZXJ2aWNlX2FjY291bnQiLCJ0eXAiOiJJRCJ9.oW9w9X2EBozdY7JTnL6WBPE114BM52ZOaWLkXamJvUOks_F4fRxw5lJIN-LkTwMZ9jE3PsBV2_SueCL5Ry2ISiEXaZeoQ_FPnSkz-CMFDP6Ph2erOvEWQInTIPA6h-ToIhYMZR8lc_kPOmar2mTT8b043FZ6zFDf28PJCCo8snCgA_tIO7R0fNJYT7Hr-UR7LSrE-Sjz7lsgttyDEPH1P4yPm4ZzRLYLcR240p1iGKG9yxtl8IL6uxseS4pUddimaH6jFPhMFLH44PV4O_-uYs74erjoPiroCHiaWQIdDR5GZDoPCbYXQa0knh9hnK1pX6fO-krHeT3RtfuFf5609A", + "expires_in": 3600, + "not-before-policy": 0, + "scope": "openid profile email roles", + "token_type": "Bearer" +} diff --git a/providers/cloudru/internal/fixtures/record.json b/providers/cloudru/internal/fixtures/record.json new file mode 100644 index 0000000..05dd340 --- /dev/null +++ b/providers/cloudru/internal/fixtures/record.json @@ -0,0 +1,11 @@ +{ + "zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a", + "name": "www.example.com.", + "type": "TXT", + "values": [ + "txt" + ], + "ttl": "3600", + "enables": true, + "readonly": false +} diff --git a/providers/cloudru/internal/fixtures/records.json b/providers/cloudru/internal/fixtures/records.json new file mode 100644 index 0000000..93cdd06 --- /dev/null +++ b/providers/cloudru/internal/fixtures/records.json @@ -0,0 +1,38 @@ +{ + "items": [ + { + "zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a", + "name": "example.com.", + "type": "SOA", + "values": [ + "cdns-ns01.sbercloud.ru. mail.sbercloud.ru 1 120 3600 604800 3600" + ], + "ttl": "3600", + "enables": true, + "readonly": true + }, + { + "zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a", + "name": "example.com.", + "type": "NS", + "values": [ + "cdns-ns01.sbercloud.ru.", + "cdns-ns02.sbercloud.ru." + ], + "ttl": "3600", + "enables": true, + "readonly": true + }, + { + "zone_id": "59556fcd-95ff-451f-b49b-9732f21f944a", + "name": "www.example.com.", + "type": "A", + "values": [ + "8.8.8.8" + ], + "ttl": "3600", + "enables": true, + "readonly": false + } + ] +} diff --git a/providers/cloudru/internal/fixtures/zones.json b/providers/cloudru/internal/fixtures/zones.json new file mode 100644 index 0000000..762db9f --- /dev/null +++ b/providers/cloudru/internal/fixtures/zones.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "id": "59556fcd-95ff-451f-b49b-9732f21f944a", + "parent_id": "2d7b6194-2b83-4f71-86fd-a1e727e347b2", + "name": "example.com", + "valid": true, + "validation_text": "sbc-verification: 5c86c962-7ee2-4983-b39b-1d9461959d8b", + "delegated": true, + "created_at": "2023-07-23T08:12:41.000000Z", + "updated_at": "2023-07-24T05:50:28.000000Z" + } + ] +} diff --git a/providers/cloudru/internal/identity.go b/providers/cloudru/internal/identity.go new file mode 100644 index 0000000..49f3b44 --- /dev/null +++ b/providers/cloudru/internal/identity.go @@ -0,0 +1,106 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" +) + +type token string + +const tokenKey token = "token" + +// obtainToken Logs into cloud.ru and acquires a bearer token for use in future API calls. +// https://cloud.ru/ru/docs/clouddns/ug/topics/api-ref_authentication.html +func (c *Client) obtainToken(ctx context.Context) (*Token, error) { + data := make(url.Values) + data.Set("grant_type", "access_key") + data.Set("client_id", c.keyID) + data.Set("client_secret", c.secret) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.AuthEndpoint.String(), strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, parseError(req, resp) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + tok := Token{} + err = json.Unmarshal(raw, &tok) + if err != nil { + return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + if !strings.EqualFold(tok.TokenType, "Bearer") { + return nil, fmt.Errorf("received unexpected token type: %s", tok.TokenType) + } + + tok.Deadline = time.Now().Add(time.Duration(tok.ExpiresIn) * time.Second) + + return &tok, nil +} + +func (c *Client) CreateAuthenticatedContext(ctx context.Context) (context.Context, error) { + c.muToken.Lock() + defer c.muToken.Unlock() + + if c.token != nil && time.Now().Before(c.token.Deadline) { + // Already authenticated, stop now + return context.WithValue(ctx, tokenKey, c.token), nil + } + + tok, err := c.obtainToken(ctx) + if err != nil { + return nil, err + } + + return context.WithValue(ctx, tokenKey, tok), nil +} + +func parseError(req *http.Request, resp *http.Response) error { + if resp.StatusCode < 400 || resp.StatusCode > 499 { + return errutils.NewUnexpectedResponseStatusCodeError(req, resp) + } + + raw, _ := io.ReadAll(resp.Body) + + errResp := &authResponseError{} + err := json.Unmarshal(raw, errResp) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("%d: %w", resp.StatusCode, errResp) +} + +func getToken(ctx context.Context) *Token { + tok, ok := ctx.Value(tokenKey).(*Token) + if !ok { + return nil + } + + return tok +} diff --git a/providers/cloudru/internal/identity_test.go b/providers/cloudru/internal/identity_test.go new file mode 100644 index 0000000..7329e7f --- /dev/null +++ b/providers/cloudru/internal/identity_test.go @@ -0,0 +1,92 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockContext() context.Context { + return context.WithValue(context.Background(), tokenKey, &Token{AccessToken: "xxx"}) +} + +func tokenHandler(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed) + return + } + + err := req.ParseForm() + if err != nil { + http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + grantType := req.Form.Get("grant_type") + clientID := req.Form.Get("client_id") + clientSecret := req.Form.Get("client_secret") + + if clientID != "user" || clientSecret != "secret" || grantType != "access_key" { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + _ = json.NewEncoder(rw).Encode(Token{ + AccessToken: "xxx", + TokenID: "yyy", + ExpiresIn: 666, + TokenType: "Bearer", + Scope: "openid profile email roles", + }) +} + +func TestClient_obtainToken(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.AuthEndpoint, _ = url.Parse(server.URL) + + assert.Nil(t, client.token) + + tok, err := client.obtainToken(context.Background()) + require.NoError(t, err) + + assert.NotNil(t, tok) + assert.NotZero(t, tok.Deadline) + assert.Equal(t, "xxx", tok.AccessToken) +} + +func TestClient_CreateAuthenticatedContext(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc("/", tokenHandler) + + client := NewClient("user", "secret") + client.HTTPClient = server.Client() + client.AuthEndpoint, _ = url.Parse(server.URL) + + assert.Nil(t, client.token) + + ctx, err := client.CreateAuthenticatedContext(context.Background()) + require.NoError(t, err) + + tok := getToken(ctx) + + assert.NotNil(t, tok) + assert.NotZero(t, tok.Deadline) + assert.Equal(t, "xxx", tok.AccessToken) +} diff --git a/providers/cloudru/internal/types.go b/providers/cloudru/internal/types.go new file mode 100644 index 0000000..d233c73 --- /dev/null +++ b/providers/cloudru/internal/types.go @@ -0,0 +1,53 @@ +package internal + +import ( + "fmt" + "time" +) + +type Token struct { + // The bearer token for use in API requests + AccessToken string `json:"access_token"` + TokenID string `json:"id_token"` + TokenType string `json:"token_type"` + // Number in seconds before the expiration + ExpiresIn int `json:"expires_in"` + NotBeforePolicy int `json:"not-before-policy"` + Scope string `json:"scope"` + + Deadline time.Time `json:"-"` +} + +type authResponseError struct { + ErrorMsg string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +func (a authResponseError) Error() string { + return fmt.Sprintf("%s: %s", a.ErrorMsg, a.ErrorDescription) +} + +type APIResponse[T any] struct { + Items []T `json:"items"` +} + +type Zone struct { + ID string `json:"id,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Name string `json:"name,omitempty"` + Valid bool `json:"valid,omitempty"` + ValidationText string `json:"validationText,omitempty"` + Delegated bool `json:"delegated,omitempty"` + LastCheck time.Time `json:"lastCheck,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + +type Record struct { + ZoneID string `json:"zone_id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Values []string `json:"values,omitempty"` + TTL string `json:"ttl,omitempty"` + Enables bool `json:"enables,omitempty"` +} diff --git a/providers/conoha/conoha.go b/providers/conoha/conoha.go index 5725f8c..770d35d 100644 --- a/providers/conoha/conoha.go +++ b/providers/conoha/conoha.go @@ -105,7 +105,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { tokens, err := identifier.GetToken(context.TODO(), auth) if err != nil { - return nil, fmt.Errorf("conoha: failed to login: %w", err) + return nil, fmt.Errorf("conoha: failed to log in: %w", err) } client, err := internal.NewClient(config.Region, tokens.Access.Token.ID) diff --git a/providers/conoha/conoha_test.go b/providers/conoha/conoha_test.go index 8d8197f..9db5ba7 100644 --- a/providers/conoha/conoha_test.go +++ b/providers/conoha/conoha_test.go @@ -29,7 +29,7 @@ func TestNewDNSProvider(t *testing.T) { EnvAPIUsername: "api_username", EnvAPIPassword: "api_password", }, - expected: `conoha: failed to login: unexpected status code: [status code: 401] body: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, + expected: `conoha: failed to log in: unexpected status code: [status code: 401] body: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, }, { desc: "missing credentials", @@ -99,7 +99,7 @@ func TestNewDNSProviderConfig(t *testing.T) { }{ { desc: "complete credentials, but login failed", - expected: `conoha: failed to login: unexpected status code: [status code: 401] body: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, + expected: `conoha: failed to log in: unexpected status code: [status code: 401] body: {"unauthorized":{"message":"Invalid user: api_username","code":401}}`, tenant: "tenant_id", username: "api_username", password: "api_password", diff --git a/providers/constellix/internal/auth.go b/providers/constellix/internal/auth.go index 8be4f84..1a13601 100644 --- a/providers/constellix/internal/auth.go +++ b/providers/constellix/internal/auth.go @@ -23,7 +23,7 @@ type TokenTransport struct { Transport http.RoundTripper } -// NewTokenTransport Creates a HTTP transport for API authentication. +// NewTokenTransport Creates an HTTP transport for API authentication. func NewTokenTransport(apiKey, secretKey string) (*TokenTransport, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") diff --git a/providers/designate/designate.toml b/providers/designate/designate.toml index b885999..55a1cd3 100644 --- a/providers/designate/designate.toml +++ b/providers/designate/designate.toml @@ -43,6 +43,10 @@ For more information, you can read about the different methods of authentication - [Keystone username/password](https://docs.openstack.org/keystone/latest/user/supported_clients.html) - [Keystone application credentials](https://docs.openstack.org/keystone/latest/user/application_credentials.html) + +Public cloud providers with support for Designate: + +- [Fuga Cloud](https://fuga.cloud/) ''' [Configuration] @@ -65,4 +69,4 @@ For more information, you can read about the different methods of authentication [Links] API = "https://docs.openstack.org/designate/latest/" - GoClient = "https://godoc.org/github.com/gophercloud/gophercloud/openstack/dns/v2" + GoClient = "https://pkg.go.dev/github.com/gophercloud/gophercloud/openstack/dns/v2" diff --git a/providers/dns_providers.go b/providers/dns_providers.go index 0e43065..8172e19 100644 --- a/providers/dns_providers.go +++ b/providers/dns_providers.go @@ -12,6 +12,7 @@ import ( "github.com/entrustcorporation/dv/providers/auroradns" "github.com/entrustcorporation/dv/providers/autodns" "github.com/entrustcorporation/dv/providers/azure" + "github.com/entrustcorporation/dv/providers/azuredns" "github.com/entrustcorporation/dv/providers/bindman" "github.com/entrustcorporation/dv/providers/bluecat" "github.com/entrustcorporation/dv/providers/brandit" @@ -21,6 +22,7 @@ import ( "github.com/entrustcorporation/dv/providers/clouddns" "github.com/entrustcorporation/dv/providers/cloudflare" "github.com/entrustcorporation/dv/providers/cloudns" + "github.com/entrustcorporation/dv/providers/cloudru" "github.com/entrustcorporation/dv/providers/cloudxns" "github.com/entrustcorporation/dv/providers/conoha" "github.com/entrustcorporation/dv/providers/constellix" @@ -40,6 +42,7 @@ import ( "github.com/entrustcorporation/dv/providers/dynu" "github.com/entrustcorporation/dv/providers/easydns" "github.com/entrustcorporation/dv/providers/edgedns" + "github.com/entrustcorporation/dv/providers/efficientip" "github.com/entrustcorporation/dv/providers/epik" "github.com/entrustcorporation/dv/providers/exec" "github.com/entrustcorporation/dv/providers/exoscale" @@ -65,6 +68,7 @@ import ( "github.com/entrustcorporation/dv/providers/internetbs" "github.com/entrustcorporation/dv/providers/inwx" "github.com/entrustcorporation/dv/providers/ionos" + "github.com/entrustcorporation/dv/providers/ipv64" "github.com/entrustcorporation/dv/providers/iwantmyname" "github.com/entrustcorporation/dv/providers/joker" "github.com/entrustcorporation/dv/providers/liara" @@ -73,6 +77,7 @@ import ( "github.com/entrustcorporation/dv/providers/liquidweb" "github.com/entrustcorporation/dv/providers/loopia" "github.com/entrustcorporation/dv/providers/luadns" + "github.com/entrustcorporation/dv/providers/metaname" "github.com/entrustcorporation/dv/providers/mydnsjp" "github.com/entrustcorporation/dv/providers/mythicbeasts" "github.com/entrustcorporation/dv/providers/namecheap" @@ -93,6 +98,7 @@ import ( "github.com/entrustcorporation/dv/providers/plesk" "github.com/entrustcorporation/dv/providers/porkbun" "github.com/entrustcorporation/dv/providers/rackspace" + "github.com/entrustcorporation/dv/providers/rcodezero" "github.com/entrustcorporation/dv/providers/regru" "github.com/entrustcorporation/dv/providers/rfc2136" "github.com/entrustcorporation/dv/providers/rimuhosting" @@ -119,6 +125,7 @@ import ( "github.com/entrustcorporation/dv/providers/websupport" "github.com/entrustcorporation/dv/providers/wedos" "github.com/entrustcorporation/dv/providers/yandex" + "github.com/entrustcorporation/dv/providers/yandex360" "github.com/entrustcorporation/dv/providers/yandexcloud" "github.com/entrustcorporation/dv/providers/zoneee" "github.com/entrustcorporation/dv/providers/zonomi" @@ -137,6 +144,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return arvancloud.NewDNSProvider() case "azure": return azure.NewDNSProvider() + case "azuredns": + return azuredns.NewDNSProvider() case "auroradns": return auroradns.NewDNSProvider() case "autodns": @@ -159,6 +168,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return cloudflare.NewDNSProvider() case "cloudns": return cloudns.NewDNSProvider() + case "cloudru": + return cloudru.NewDNSProvider() case "cloudxns": return cloudxns.NewDNSProvider() case "conoha": @@ -197,6 +208,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return easydns.NewDNSProvider() case "edgedns", "fastdns": // "fastdns" is for compatibility with v3, must be dropped in v5 return edgedns.NewDNSProvider() + case "efficientip": + return efficientip.NewDNSProvider() case "epik": return epik.NewDNSProvider() case "exec": @@ -247,6 +260,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return inwx.NewDNSProvider() case "ionos": return ionos.NewDNSProvider() + case "ipv64": + return ipv64.NewDNSProvider() case "iwantmyname": return iwantmyname.NewDNSProvider() case "joker": @@ -265,6 +280,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return loopia.NewDNSProvider() case "manual": return dns01.NewDNSProviderManual() + case "metaname": + return metaname.NewDNSProvider() case "mydnsjp": return mydnsjp.NewDNSProvider() case "mythicbeasts": @@ -305,6 +322,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return porkbun.NewDNSProvider() case "rackspace": return rackspace.NewDNSProvider() + case "rcodezero": + return rcodezero.NewDNSProvider() case "regru": return regru.NewDNSProvider() case "rfc2136": @@ -357,6 +376,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return wedos.NewDNSProvider() case "yandex": return yandex.NewDNSProvider() + case "yandex360": + return yandex360.NewDNSProvider() case "yandexcloud": return yandexcloud.NewDNSProvider() case "zoneee": diff --git a/providers/dnsimple/dnsimple.toml b/providers/dnsimple/dnsimple.toml index e3f6f50..0dd8f06 100644 --- a/providers/dnsimple/dnsimple.toml +++ b/providers/dnsimple/dnsimple.toml @@ -16,7 +16,7 @@ Additional = ''' if `DNSIMPLE_BASE_URL` is not defined or empty, the production URL is used by default. While you can manage DNS records in the [DNSimple Sandbox environment](https://developer.dnsimple.com/sandbox/), -DNS records will not resolve and you will not be able to satisfy the ACME DNS challenge. +DNS records will not resolve, and you will not be able to satisfy the ACME DNS challenge. To authenticate you need to provide a valid API token. HTTP Basic Authentication is intentionally not supported. @@ -24,7 +24,7 @@ HTTP Basic Authentication is intentionally not supported. ### API tokens You can [generate a new API token](https://support.dnsimple.com/articles/api-access-token/) from your account page. -Only Account API tokens are supported, if you try to use an User API token you will receive an error message. +Only Account API tokens are supported, if you try to use a User API token you will receive an error message. ''' [Configuration] diff --git a/providers/dnsmadeeasy/internal/client.go b/providers/dnsmadeeasy/internal/client.go index fe3d364..42fa2da 100644 --- a/providers/dnsmadeeasy/internal/client.go +++ b/providers/dnsmadeeasy/internal/client.go @@ -84,6 +84,7 @@ func (c *Client) GetRecords(ctx context.Context, domain *Domain, recordName, rec query := endpoint.Query() query.Set("recordName", recordName) query.Set("type", recordType) + endpoint.RawQuery = query.Encode() req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { @@ -113,7 +114,7 @@ func (c *Client) CreateRecord(ctx context.Context, domain *Domain, record *Recor // DeleteRecord deletes a TXT records. func (c *Client) DeleteRecord(ctx context.Context, record Record) error { - endpoint := c.BaseURL.JoinPath("/dns/managed", strconv.Itoa(record.SourceID), "records", strconv.Itoa(record.ID)) + endpoint := c.BaseURL.JoinPath("dns", "managed", strconv.Itoa(record.SourceID), "records", strconv.Itoa(record.ID)) req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { diff --git a/providers/domeneshop/internal/client.go b/providers/domeneshop/internal/client.go index ac85bd9..721352a 100644 --- a/providers/domeneshop/internal/client.go +++ b/providers/domeneshop/internal/client.go @@ -17,7 +17,7 @@ import ( const defaultBaseURL string = "https://api.domeneshop.no/v0" // Client implements a very simple wrapper around the Domeneshop API. -// For now it will only deal with adding and removing TXT records, as required by ACME providers. +// For now, it will only deal with adding and removing TXT records, as required by ACME providers. // https://api.domeneshop.no/docs/ type Client struct { apiToken string diff --git a/providers/dynu/internal/auth.go b/providers/dynu/internal/auth.go index 72d15a8..7a21a10 100644 --- a/providers/dynu/internal/auth.go +++ b/providers/dynu/internal/auth.go @@ -16,7 +16,7 @@ type TokenTransport struct { Transport http.RoundTripper } -// NewTokenTransport Creates a HTTP transport for API authentication. +// NewTokenTransport Creates an HTTP transport for API authentication. func NewTokenTransport(apiKey string) (*TokenTransport, error) { if apiKey == "" { return nil, errors.New("credentials missing: API key") diff --git a/providers/dynu/internal/client.go b/providers/dynu/internal/client.go index 77ec705..7543bfb 100644 --- a/providers/dynu/internal/client.go +++ b/providers/dynu/internal/client.go @@ -110,7 +110,7 @@ func (c Client) GetRootDomain(ctx context.Context, hostname string) (*DNSHostnam return &apiResp, nil } -// doRetry the API is really unstable so we need to retry on EOF. +// doRetry the API is really unstable, so we need to retry on EOF. func (c Client) doRetry(ctx context.Context, method, uri string, body []byte, result any) error { operation := func() error { return c.do(ctx, method, uri, body, result) diff --git a/providers/edgedns/edgedns.go b/providers/edgedns/edgedns.go index cb89517..55596e2 100644 --- a/providers/edgedns/edgedns.go +++ b/providers/edgedns/edgedns.go @@ -62,7 +62,7 @@ type DNSProvider struct { } // NewDNSProvider returns a DNSProvider instance configured for Akamai EdgeDNS: -// Akamai credentials are automatically detected in the following locations and prioritized in the following order: +// Akamai's credentials are automatically detected in the following locations and prioritized in the following order: // // 1. Section-specific environment variables `AKAMAI_{SECTION}_HOST`, `AKAMAI_{SECTION}_ACCESS_TOKEN`, `AKAMAI_{SECTION}_CLIENT_TOKEN`, `AKAMAI_{SECTION}_CLIENT_SECRET` where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION` // 2. If `AKAMAI_EDGERC_SECTION` is not defined or is set to `default`: Environment variables `AKAMAI_HOST`, `AKAMAI_ACCESS_TOKEN`, `AKAMAI_CLIENT_TOKEN`, `AKAMAI_CLIENT_SECRET` diff --git a/providers/edgedns/edgedns.toml b/providers/edgedns/edgedns.toml index 7929e03..dffa6e4 100644 --- a/providers/edgedns/edgedns.toml +++ b/providers/edgedns/edgedns.toml @@ -15,7 +15,7 @@ lego --email you@example.com --dns edgedns --domains my.example.org run ''' Additional = ''' -Akamai credentials are automatically detected in the following locations and prioritized in the following order: +Akamai's credentials are automatically detected in the following locations and prioritized in the following order: 1. Section-specific environment variables (where `{SECTION}` is specified using `AKAMAI_EDGERC_SECTION`): - `AKAMAI_{SECTION}_HOST` @@ -40,7 +40,7 @@ See also: - [Setting up Akamai credentials](https://developer.akamai.com/api/getting-started) - [.edgerc Format](https://developer.akamai.com/legacy/introduction/Conf_Client.html#edgercformat) - [API Client Authentication](https://developer.akamai.com/legacy/introduction/Client_Auth.html) -- [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/edgegrid/config.go#L118) +- [Config from Env](https://github.com/akamai/AkamaiOPEN-edgegrid-golang/blob/master/pkg/edgegrid/config.go#L118) ''' [Configuration] diff --git a/providers/efficientip/efficientip.go b/providers/efficientip/efficientip.go new file mode 100644 index 0000000..7d9c30e --- /dev/null +++ b/providers/efficientip/efficientip.go @@ -0,0 +1,152 @@ +// Package efficientip implements a DNS provider for solving the DNS-01 challenge using Efficient IP. +package efficientip + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/efficientip/internal" +) + +// Environment variables names. +const ( + envNamespace = "EFFICIENTIP_" + + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + EnvHostname = envNamespace + "HOSTNAME" + EnvDNSName = envNamespace + "DNS_NAME" + EnvViewName = envNamespace + "VIEW_NAME" + + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + Password string + Hostname string + DNSName string + ViewName string + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a new DNS provider +// using environment variable EFFICIENTIP_API_KEY for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvPassword, EnvHostname, EnvDNSName) + if err != nil { + return nil, fmt.Errorf("efficientip: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.Hostname = values[EnvHostname] + config.DNSName = values[EnvDNSName] + config.ViewName = env.GetOrDefaultString(EnvViewName, "") + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Efficient IP. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("efficientip: the configuration of the DNS provider is nil") + } + + if config.Username == "" { + return nil, errors.New("efficientip: missing username") + } + if config.Password == "" { + return nil, errors.New("efficientip: missing password") + } + if config.Hostname == "" { + return nil, errors.New("efficientip: missing hostname") + } + if config.DNSName == "" { + return nil, errors.New("efficientip: missing dnsname") + } + + client := internal.NewClient(config.Hostname, config.Username, config.Password) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + r := internal.ResourceRecord{ + RRName: dns01.UnFqdn(info.EffectiveFQDN), + RRType: "TXT", + Value1: info.Value, + DNSName: d.config.DNSName, + DNSViewName: d.config.ViewName, + } + + _, err := d.client.AddRecord(ctx, r) + if err != nil { + return fmt.Errorf("efficientip: add record: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + params := internal.DeleteInputParameters{ + RRName: dns01.UnFqdn(info.EffectiveFQDN), + RRType: "TXT", + RRValue1: info.Value, + DNSName: d.config.DNSName, + DNSViewName: d.config.ViewName, + } + + _, err := d.client.DeleteRecord(ctx, params) + if err != nil { + return fmt.Errorf("efficientip: delete record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/efficientip/efficientip.toml b/providers/efficientip/efficientip.toml new file mode 100644 index 0000000..278701e --- /dev/null +++ b/providers/efficientip/efficientip.toml @@ -0,0 +1,26 @@ +Name = "Efficient IP" +Description = '''''' +URL = "https://efficientip.com/" +Code = "efficientip" +Since = "v4.13.0" + +Example = ''' +EFFICIENTIP_USERNAME="user" \ +EFFICIENTIP_PASSWORD="secret" \ +EFFICIENTIP_HOSTNAME="ipam.example.org" \ +EFFICIENTIP_DNS_NAME="dns.smart" \ +lego --email you@example.com --dns efficientip --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + EFFICIENTIP_USERNAME = "Username" + EFFICIENTIP_PASSWORD = "Password" + EFFICIENTIP_HOSTNAME = "Hostname (ex: foo.example.com)" + EFFICIENTIP_DNS_NAME = "DNS name (ex: dns.smart)" + [Configuration.Additional] + EFFICIENTIP_VIEW_NAME = "View name (ex: external)" + EFFICIENTIP_POLLING_INTERVAL = "Time between DNS propagation check" + EFFICIENTIP_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + EFFICIENTIP_TTL = "The TTL of the TXT record used for the DNS challenge" + EFFICIENTIP_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/efficientip/efficientip_test.go b/providers/efficientip/efficientip_test.go new file mode 100644 index 0000000..3ee2da7 --- /dev/null +++ b/providers/efficientip/efficientip_test.go @@ -0,0 +1,201 @@ +package efficientip + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvUsername, + EnvPassword, + EnvHostname, + EnvDNSName, + EnvViewName, +).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "secret", + EnvHostname: "example.com", + EnvDNSName: "dns.smart", + }, + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvUsername: "", + EnvPassword: "secret", + EnvHostname: "example.com", + EnvDNSName: "dns.smart", + }, + expected: "efficientip: some credentials information are missing: EFFICIENTIP_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "", + EnvHostname: "example.com", + EnvDNSName: "dns.smart", + }, + expected: "efficientip: some credentials information are missing: EFFICIENTIP_PASSWORD", + }, + { + desc: "missing hostname", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "secret", + EnvHostname: "", + EnvDNSName: "dns.smart", + }, + expected: "efficientip: some credentials information are missing: EFFICIENTIP_HOSTNAME", + }, + { + desc: "missing DNS name", + envVars: map[string]string{ + EnvUsername: "user", + EnvPassword: "secret", + EnvHostname: "example.com", + EnvDNSName: "", + }, + expected: "efficientip: some credentials information are missing: EFFICIENTIP_DNS_NAME", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "efficientip: some credentials information are missing: EFFICIENTIP_USERNAME,EFFICIENTIP_PASSWORD,EFFICIENTIP_HOSTNAME,EFFICIENTIP_DNS_NAME", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + password string + hostname string + dnsName string + expected string + }{ + { + desc: "success", + username: "user", + password: "secret", + hostname: "example.com", + dnsName: "dns.smart", + }, + { + desc: "missing username", + password: "secret", + hostname: "example.com", + dnsName: "dns.smart", + expected: "efficientip: missing username", + }, + { + desc: "missing password", + username: "user", + hostname: "example.com", + dnsName: "dns.smart", + expected: "efficientip: missing password", + }, + { + desc: "missing hostname", + username: "user", + password: "secret", + dnsName: "dns.smart", + expected: "efficientip: missing hostname", + }, + { + desc: "missing dnsName", + username: "user", + password: "secret", + hostname: "example.com", + expected: "efficientip: missing dnsname", + }, + { + desc: "missing all", + expected: "efficientip: missing username", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + + config.Username = test.username + config.Password = test.password + config.Hostname = test.hostname + config.DNSName = test.dnsName + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/efficientip/internal/client.go b/providers/efficientip/internal/client.go new file mode 100644 index 0000000..38e8151 --- /dev/null +++ b/providers/efficientip/internal/client.go @@ -0,0 +1,209 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" + querystring "github.com/google/go-querystring/query" +) + +type Client struct { + baseURL *url.URL + HTTPClient *http.Client + + username string + password string +} + +func NewClient(hostname string, username string, password string) *Client { + baseURL, _ := url.Parse(fmt.Sprintf("https://%s/rest/", hostname)) + + return &Client{ + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + baseURL: baseURL, + username: username, + password: password, + } +} + +func (c Client) ListRecords(ctx context.Context) ([]ResourceRecord, error) { + endpoint := c.baseURL.JoinPath("dns_rr_list") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []ResourceRecord + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (c Client) GetRecord(ctx context.Context, id string) (*ResourceRecord, error) { + endpoint := c.baseURL.JoinPath("dns_rr_info") + + query := endpoint.Query() + query.Set("rr_id", id) + endpoint.RawQuery = query.Encode() + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + var result []ResourceRecord + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + if len(result) == 0 { + return nil, nil + } + + return &result[0], nil +} + +func (c Client) AddRecord(ctx context.Context, record ResourceRecord) (*BaseOutput, error) { + endpoint := c.baseURL.JoinPath("dns_rr_add") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return nil, err + } + + var result []BaseOutput + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + if len(result) == 0 { + return nil, nil + } + + return &result[0], nil +} + +func (c Client) DeleteRecord(ctx context.Context, params DeleteInputParameters) (*BaseOutput, error) { + endpoint := c.baseURL.JoinPath("dns_rr_delete") + + // (rr_id || (rr_name && (dns_id || dns_name || hostaddr))) + + v, err := querystring.Values(params) + if err != nil { + return nil, fmt.Errorf("query parameters: %w", err) + } + endpoint.RawQuery = v.Encode() + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return nil, err + } + + var result []BaseOutput + + err = c.do(req, &result) + if err != nil { + return nil, err + } + + if len(result) == 0 { + return nil, nil + } + + return &result[0], nil +} + +func (c Client) do(req *http.Request, result any) error { + req.SetBasicAuth(c.username, c.password) + req.Header.Set("cache-control", "no-cache") + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + switch req.Method { + case http.MethodPost: + if resp.StatusCode != http.StatusCreated { + return parseError(req, resp) + } + default: + if resp.StatusCode == http.StatusNoContent { + return nil + } + + if resp.StatusCode != http.StatusOK { + return parseError(req, resp) + } + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var response APIError + err := json.Unmarshal(raw, &response) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("[status code %d] %w", resp.StatusCode, response) +} diff --git a/providers/efficientip/internal/client_test.go b/providers/efficientip/internal/client_test.go new file mode 100644 index 0000000..a766c90 --- /dev/null +++ b/providers/efficientip/internal/client_test.go @@ -0,0 +1,427 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + username, password, ok := req.BasicAuth() + if !ok { + http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if username != "user" { + http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized) + return + } + + if password != "secret" { + http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + srvURL, _ := url.Parse(server.URL) + + client := NewClient(srvURL.Host, "user", "secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func TestListRecords(t *testing.T) { + client := setupTest(t, http.MethodGet, "/dns_rr_list", http.StatusOK, "dns_rr_list.json") + + ctx := context.Background() + + records, err := client.ListRecords(ctx) + require.NoError(t, err) + + expected := []ResourceRecord{ + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "test1", + RRAuthGsstsig: "0", + RRFullName: "test.lego.example.com", + RRFullNameUTF: "test.lego.example.com", + RRGlue: "test", + RRGlueID: "21", + RRID: "239", + RRNameID: "26", + RRType: "TXT", + RRTypeID: "6", + RRValueID: "274", + TTL: "3600", + Value1: "test1", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "test2", + RRAuthGsstsig: "0", + RRFullName: "test.lego.example.com", + RRFullNameUTF: "test.lego.example.com", + RRGlue: "test", + RRGlueID: "21", + RRID: "241", + RRNameID: "26", + RRType: "TXT", + RRTypeID: "6", + RRValueID: "275", + TTL: "3600", + Value1: "test2", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "test1", + RRAuthGsstsig: "0", + RRFullName: "lego.example.com", + RRFullNameUTF: "lego.example.com", + RRGlue: ".", + RRGlueID: "3", + RRID: "245", + RRNameID: "21", + RRType: "TXT", + RRTypeID: "6", + RRValueID: "274", + TTL: "3600", + Value1: "test1", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "test2", + RRAuthGsstsig: "0", + RRFullName: "lego.example.com", + RRFullNameUTF: "lego.example.com", + RRGlue: ".", + RRGlueID: "3", + RRID: "247", + RRNameID: "21", + RRType: "TXT", + RRTypeID: "6", + RRValueID: "275", + TTL: "3600", + Value1: "test2", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "dns.smart, root@lego.example.com, 2023062719, 1200, 600, 1209600, 3600", + RRAuthGsstsig: "0", + RRFullName: "lego.example.com", + RRFullNameUTF: "lego.example.com", + RRGlue: ".", + RRGlueID: "3", + RRID: "201", + RRNameID: "21", + RRType: "SOA", + RRTypeID: "2", + RRValueID: "282", + TTL: "3600", + Value1: "dns.smart", + Value2: "root@lego.example.com", + Value3: "2023062719", + Value4: "1200", + Value5: "600", + Value6: "1209600", + Value7: "3600", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "dns.smart", + RRAuthGsstsig: "0", + RRFullName: "lego.example.com", + RRFullNameUTF: "lego.example.com", + RRGlue: ".", + RRGlueID: "3", + RRID: "200", + RRNameID: "21", + RRType: "NS", + RRTypeID: "1", + RRValueID: "10", + TTL: "3600", + Value1: "dns.smart", + VDNSParentID: "0", + VDNSParentName: "#", + }, + { + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "127.0.0.1", + RRAuthGsstsig: "0", + RRFullName: "loopback.lego.example.com", + RRFullNameUTF: "loopback.lego.example.com", + RRGlue: "loopback", + RRGlueID: "17", + RRID: "208", + RRNameID: "22", + RRType: "A", + RRTypeID: "3", + RRValueID: "237", + RRValueIP4Addr: "7f000001", + RRValueIPAddr: "7f000001", + TTL: "3600", + Value1: "127.0.0.1", + VDNSParentID: "0", + VDNSParentName: "#", + }, + } + + assert.Equal(t, expected, records) +} + +func TestGetRecord(t *testing.T) { + client := setupTest(t, http.MethodGet, "/dns_rr_info", http.StatusOK, "dns_rr_info.json") + + ctx := context.Background() + + record, err := client.GetRecord(ctx, "239") + require.NoError(t, err) + + expected := &ResourceRecord{ + ErrorCode: "0", + DelayedCreateTime: "0", + DelayedDeleteTime: "0", + DelayedTime: "0", + DNSCloud: "0", + DNSID: "3", + DNSName: "dns.smart", + DNSType: "vdns", + DNSViewID: "0", + DNSViewName: "#", + DNSZoneID: "9", + DNSZoneIsReverse: "0", + DNSZoneIsRpz: "0", + DNSZoneName: "lego.example.com", + DNSZoneNameUTF: "lego.example.com", + DNSZoneSiteName: "#", + DNSZoneSortZone: "lego.example.com", + DNSZoneType: "master", + RRAllValue: "test1", + RRAuthGsstsig: "0", + RRFullName: "test.lego.example.com", + RRFullNameUTF: "test.lego.example.com", + RRGlue: "test", + RRGlueID: "21", + RRID: "239", + RRNameID: "26", + RRType: "TXT", + RRTypeID: "6", + RRValueID: "274", + TTL: "3600", + Value1: "test1", + VDNSParentID: "0", + VDNSParentName: "#", + } + + assert.Equal(t, expected, record) +} + +func TestAddRecord(t *testing.T) { + client := setupTest(t, http.MethodPost, "/dns_rr_add", http.StatusCreated, "dns_rr_add.json") + + ctx := context.Background() + + r := ResourceRecord{ + RRName: "test.example.com", + RRType: "TXT", + Value1: "test", + DNSName: "dns.smart", + DNSViewName: "external", + } + + resp, err := client.AddRecord(ctx, r) + require.NoError(t, err) + + expected := &BaseOutput{RetOID: "239"} + + assert.Equal(t, expected, resp) +} + +func TestDeleteRecord(t *testing.T) { + client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusOK, "dns_rr_delete.json") + + ctx := context.Background() + + resp, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"}) + require.NoError(t, err) + + expected := &BaseOutput{RetOID: "251"} + + assert.Equal(t, expected, resp) +} + +func TestDeleteRecord_error(t *testing.T) { + client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusBadRequest, "dns_rr_delete-error.json") + + ctx := context.Background() + + _, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"}) + require.ErrorAs(t, err, &APIError{}) +} diff --git a/providers/efficientip/internal/fixtures/dns_rr_add.json b/providers/efficientip/internal/fixtures/dns_rr_add.json new file mode 100644 index 0000000..49932c2 --- /dev/null +++ b/providers/efficientip/internal/fixtures/dns_rr_add.json @@ -0,0 +1,5 @@ +[ + { + "ret_oid": "239" + } +] diff --git a/providers/efficientip/internal/fixtures/dns_rr_delete-error.json b/providers/efficientip/internal/fixtures/dns_rr_delete-error.json new file mode 100644 index 0000000..7eb0db6 --- /dev/null +++ b/providers/efficientip/internal/fixtures/dns_rr_delete-error.json @@ -0,0 +1,6 @@ +{ + "errno": "20117", + "errmsg": "This RR does not exist", + "severity": "error", + "category": "dns_rr_delete" +} diff --git a/providers/efficientip/internal/fixtures/dns_rr_delete.json b/providers/efficientip/internal/fixtures/dns_rr_delete.json new file mode 100644 index 0000000..d794fba --- /dev/null +++ b/providers/efficientip/internal/fixtures/dns_rr_delete.json @@ -0,0 +1,5 @@ +[ + { + "ret_oid": "251" + } +] diff --git a/providers/efficientip/internal/fixtures/dns_rr_info.json b/providers/efficientip/internal/fixtures/dns_rr_info.json new file mode 100644 index 0000000..8a2f6d0 --- /dev/null +++ b/providers/efficientip/internal/fixtures/dns_rr_info.json @@ -0,0 +1,64 @@ +[ + { + "errno": "0", + "rr_all_value": "test1", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "test.lego.example.com", + "rr_full_name_utf": "test.lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": "test", + "rr_type": "TXT", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "test1", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "239", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "26", + "rr_value_id": "274", + "rr_type_id": "6", + "rr_glue_id": "21", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + } +] diff --git a/providers/efficientip/internal/fixtures/dns_rr_list.json b/providers/efficientip/internal/fixtures/dns_rr_list.json new file mode 100644 index 0000000..2371eaf --- /dev/null +++ b/providers/efficientip/internal/fixtures/dns_rr_list.json @@ -0,0 +1,436 @@ +[ + { + "errno": "0", + "rr_all_value": "test1", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "test.lego.example.com", + "rr_full_name_utf": "test.lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": "test", + "rr_type": "TXT", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "test1", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "239", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "26", + "rr_value_id": "274", + "rr_type_id": "6", + "rr_glue_id": "21", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "test2", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "test.lego.example.com", + "rr_full_name_utf": "test.lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": "test", + "rr_type": "TXT", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "test2", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "241", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "26", + "rr_value_id": "275", + "rr_type_id": "6", + "rr_glue_id": "21", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "test1", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "lego.example.com", + "rr_full_name_utf": "lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": ".", + "rr_type": "TXT", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "test1", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "245", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "21", + "rr_value_id": "274", + "rr_type_id": "6", + "rr_glue_id": "3", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "test2", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "lego.example.com", + "rr_full_name_utf": "lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": ".", + "rr_type": "TXT", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "test2", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "247", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "21", + "rr_value_id": "275", + "rr_type_id": "6", + "rr_glue_id": "3", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "dns.smart, root@lego.example.com, 2023062719, 1200, 600, 1209600, 3600", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "lego.example.com", + "rr_full_name_utf": "lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": ".", + "rr_type": "SOA", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "dns.smart", + "value2": "root@lego.example.com", + "value3": "2023062719", + "value4": "1200", + "value5": "600", + "value6": "1209600", + "value7": "3600", + "dnszone_id": "9", + "rr_id": "201", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "21", + "rr_value_id": "282", + "rr_type_id": "2", + "rr_glue_id": "3", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "dns.smart", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "lego.example.com", + "rr_full_name_utf": "lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "", + "rr_value_ip4_addr": "", + "rr_glue": ".", + "rr_type": "NS", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "dns.smart", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "200", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "21", + "rr_value_id": "10", + "rr_type_id": "1", + "rr_glue_id": "3", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + }, + { + "errno": "0", + "rr_all_value": "127.0.0.1", + "dnszone_sort_zone": "lego.example.com", + "dnszone_is_rpz": "0", + "dnszone_type": "master", + "rr_full_name": "loopback.lego.example.com", + "rr_full_name_utf": "loopback.lego.example.com", + "rr_name_ip_addr": "", + "rr_name_ip4_addr": "", + "rr_value_ip_addr": "7f000001", + "rr_value_ip4_addr": "7f000001", + "rr_glue": "loopback", + "rr_type": "A", + "ttl": "3600", + "delayed_time": "0", + "rr_class_name": "", + "value1": "127.0.0.1", + "value2": "", + "value3": "", + "value4": "", + "value5": "", + "value6": "", + "value7": "", + "dnszone_id": "9", + "rr_id": "208", + "dns_id": "3", + "dnszone_name_utf": "lego.example.com", + "dnszone_name": "lego.example.com", + "dns_name": "dns.smart", + "dns_type": "vdns", + "dns_cloud": "0", + "vdns_parent_id": "0", + "dnsview_name": "#", + "dnsview_class_name": "", + "dnsview_id": "0", + "dnszone_site_name": "#", + "dnszone_is_reverse": "0", + "dnszone_masters": "", + "vdns_parent_name": "#", + "dnszone_forwarders": "", + "dns_class_name": "", + "dnszone_class_name": "", + "dns_version": "", + "dns_comment": "", + "delayed_create_time": "0", + "delayed_delete_time": "0", + "multistatus": "", + "rr_auth_gsstsig": "0", + "rr_last_update_time": "", + "rr_last_update_days": "", + "rr_name_id": "22", + "rr_value_id": "237", + "rr_type_id": "3", + "rr_glue_id": "17", + "dnsview_class_parameters": "", + "dnsview_class_parameters_properties": "", + "dnsview_class_parameters_inheritance_source": "", + "rr_class_parameters": "", + "rr_class_parameters_properties": "", + "rr_class_parameters_inheritance_source": "" + } +] diff --git a/providers/efficientip/internal/types.go b/providers/efficientip/internal/types.go new file mode 100644 index 0000000..017089a --- /dev/null +++ b/providers/efficientip/internal/types.go @@ -0,0 +1,109 @@ +package internal + +import "fmt" + +type ResourceRecord struct { + ErrorCode string `json:"errno,omitempty"` + + DelayedCreateTime string `json:"delayed_create_time,omitempty"` + DelayedDeleteTime string `json:"delayed_delete_time,omitempty"` + DelayedTime string `json:"delayed_time,omitempty"` + DNSClassName string `json:"dns_class_name,omitempty"` + DNSCloud string `json:"dns_cloud,omitempty"` + DNSComment string `json:"dns_comment,omitempty"` + DNSID string `json:"dns_id,omitempty"` + DNSName string `json:"dns_name,omitempty"` + DNSType string `json:"dns_type,omitempty"` + DNSVersion string `json:"dns_version,omitempty"` + DNSViewClassName string `json:"dnsview_class_name,omitempty"` + DNSViewClassParameters string `json:"dnsview_class_parameters,omitempty"` + DNSViewClassParametersInheritanceSource string `json:"dnsview_class_parameters_inheritance_source,omitempty"` + DNSViewClassParametersProperties string `json:"dnsview_class_parameters_properties,omitempty"` + DNSViewID string `json:"dnsview_id,omitempty"` + DNSViewName string `json:"dnsview_name,omitempty"` + DNSZoneClassName string `json:"dnszone_class_name,omitempty"` + DNSZoneForwarders string `json:"dnszone_forwarders,omitempty"` + DNSZoneID string `json:"dnszone_id,omitempty"` + DNSZoneIsReverse string `json:"dnszone_is_reverse,omitempty"` + DNSZoneIsRpz string `json:"dnszone_is_rpz,omitempty"` + DNSZoneMasters string `json:"dnszone_masters,omitempty"` + DNSZoneName string `json:"dnszone_name,omitempty"` + DNSZoneNameUTF string `json:"dnszone_name_utf,omitempty"` + DNSZoneSiteName string `json:"dnszone_site_name,omitempty"` + DNSZoneSortZone string `json:"dnszone_sort_zone,omitempty"` + DNSZoneType string `json:"dnszone_type,omitempty"` + MultiStatus string `json:"multistatus,omitempty"` + RRAllValue string `json:"rr_all_value,omitempty"` + RRAuthGsstsig string `json:"rr_auth_gsstsig,omitempty"` + RRClassName string `json:"rr_class_name,omitempty"` + RRClassParameters string `json:"rr_class_parameters,omitempty"` + RRClassParametersInheritanceSource string `json:"rr_class_parameters_inheritance_source,omitempty"` + RRClassParametersProperties string `json:"rr_class_parameters_properties,omitempty"` + RRFullName string `json:"rr_full_name,omitempty"` + RRFullNameUTF string `json:"rr_full_name_utf,omitempty"` + RRGlue string `json:"rr_glue,omitempty"` + RRGlueID string `json:"rr_glue_id,omitempty"` + RRID string `json:"rr_id,omitempty"` + RRLastUpdateDays string `json:"rr_last_update_days,omitempty"` + RRLastUpdateTime string `json:"rr_last_update_time,omitempty"` + RRName string `json:"rr_name,omitempty"` + RRNameID string `json:"rr_name_id,omitempty"` + RRNameIP4Addr string `json:"rr_name_ip4_addr,omitempty"` + RRNameIPAddr string `json:"rr_name_ip_addr,omitempty"` + RRType string `json:"rr_type,omitempty"` + RRTypeID string `json:"rr_type_id,omitempty"` + RRValueID string `json:"rr_value_id,omitempty"` + RRValueIP4Addr string `json:"rr_value_ip4_addr,omitempty"` + RRValueIPAddr string `json:"rr_value_ip_addr,omitempty"` + TTL string `json:"ttl,omitempty"` + Value1 string `json:"value1,omitempty"` + Value2 string `json:"value2,omitempty"` + Value3 string `json:"value3,omitempty"` + Value4 string `json:"value4,omitempty"` + Value5 string `json:"value5,omitempty"` + Value6 string `json:"value6,omitempty"` + Value7 string `json:"value7,omitempty"` + VDNSParentID string `json:"vdns_parent_id,omitempty"` + VDNSParentName string `json:"vdns_parent_name,omitempty"` +} + +type DeleteInputParameters struct { + RRID string `url:"rr_id,omitempty"` + DNSName string `url:"dns_name,omitempty"` + DNSViewName string `url:"dnsview_name,omitempty"` + RRName string `url:"rr_name,omitempty"` + RRType string `url:"rr_type,omitempty"` + RRValue1 string `url:"rr_value1,omitempty"` +} + +type BaseOutput struct { + RetOID string `json:"ret_oid,omitempty"` +} + +type APIError struct { + ErrorCode string `json:"errno,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` + Severity string `json:"severity,omitempty"` + Category string `json:"category,omitempty"` + Parameters string `json:"parameters,omitempty"` + ParamFormat string `json:"param_format,omitempty"` + ParamValue string `json:"param_value,omitempty"` +} + +func (a APIError) Error() string { + msg := fmt.Sprintf("%s: %s %s %s", a.Category, a.Severity, a.ErrorCode, a.ErrMsg) + + if a.Parameters != "" { + msg += fmt.Sprintf(" parameters: %s", a.Parameters) + } + + if a.ParamFormat != "" { + msg += fmt.Sprintf(" param_format: %s", a.ParamFormat) + } + + if a.ParamValue != "" { + msg += fmt.Sprintf(" param_value: %s", a.ParamValue) + } + + return msg +} diff --git a/providers/exec/exec.toml b/providers/exec/exec.toml index a274f07..e5868d6 100644 --- a/providers/exec/exec.toml +++ b/providers/exec/exec.toml @@ -69,7 +69,7 @@ EXEC_PATH=./update-dns.sh \ It will then call the program `./update-dns.sh` like this: ```bash -./update-dns.sh "present" "my.example.org." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" +./update-dns.sh "present" "--" "my.example.org." "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" ``` ## Commands @@ -84,29 +84,14 @@ you can use the `--` delimiter to specify the start of positional arguments, and | Mode | Command | |---------|----------------------------------------------------| -| default | `myprogram present -- ` | +| default | `myprogram present ` | | `RAW` | `myprogram present -- ` | ### Cleanup | Mode | Command | |---------|----------------------------------------------------| -| default | `myprogram cleanup -- ` | +| default | `myprogram cleanup ` | | `RAW` | `myprogram cleanup -- ` | -### Timeout - -The command have to display propagation timeout and polling interval into Stdout. - -The values must be formatted as JSON, and times are in seconds. -Example: `{"timeout": 30, "interval": 5}` - -If an error occurs or if the command is not provided: -the default display propagation timeout and polling interval are used. - -| Mode | Command | -|---------|----------------------------------------------------| -| default | `myprogram timeout` | -| `RAW` | `myprogram timeout` | - ''' diff --git a/providers/gandiv5/gandiv5.go b/providers/gandiv5/gandiv5.go index 54ea57a..e3bde99 100644 --- a/providers/gandiv5/gandiv5.go +++ b/providers/gandiv5/gandiv5.go @@ -11,6 +11,7 @@ import ( "time" "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/config/env" "github.com/entrustcorporation/dv/providers/gandiv5/internal" ) @@ -23,7 +24,8 @@ const minTTL = 300 const ( envNamespace = "GANDIV5_" - EnvAPIKey = envNamespace + "API_KEY" + EnvAPIKey = envNamespace + "API_KEY" + EnvPersonalAccessToken = envNamespace + "PERSONAL_ACCESS_TOKEN" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -39,12 +41,13 @@ type inProgressInfo struct { // Config is used to configure the creation of the DNSProvider. type Config struct { - BaseURL string - APIKey string - PropagationTimeout time.Duration - PollingInterval time.Duration - TTL int - HTTPClient *http.Client + BaseURL string + APIKey string // Deprecated use PersonalAccessToken + PersonalAccessToken string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -76,13 +79,10 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Gandi. // Credentials must be passed in the environment variable: GANDIV5_API_KEY. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvAPIKey) - if err != nil { - return nil, fmt.Errorf("gandi: %w", err) - } - + // TODO(ldez): rewrite this when APIKey will be removed. config := NewDefaultConfig() - config.APIKey = values[EnvAPIKey] + config.APIKey = env.GetOrFile(EnvAPIKey) + config.PersonalAccessToken = env.GetOrFile(EnvPersonalAccessToken) return NewDNSProviderConfig(config) } @@ -93,15 +93,19 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("gandiv5: the configuration of the DNS provider is nil") } - if config.APIKey == "" { - return nil, errors.New("gandiv5: no API Key given") + if config.APIKey != "" { + log.Print("gandiv5: API Key is deprecated, use Personal Access Token instead") + } + + if config.APIKey == "" && config.PersonalAccessToken == "" { + return nil, errors.New("gandiv5: credentials information are missing") } if config.TTL < minTTL { return nil, fmt.Errorf("gandiv5: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - client := internal.NewClient(config.APIKey) + client := internal.NewClient(config.APIKey, config.PersonalAccessToken) if config.BaseURL != "" { baseURL, err := url.Parse(config.BaseURL) diff --git a/providers/gandiv5/gandiv5.toml b/providers/gandiv5/gandiv5.toml index 19ed906..4d952b2 100644 --- a/providers/gandiv5/gandiv5.toml +++ b/providers/gandiv5/gandiv5.toml @@ -5,13 +5,14 @@ Code = "gandiv5" Since = "v0.5.0" Example = ''' -GANDIV5_API_KEY=abcdefghijklmnopqrstuvwx \ +GANDIV5_PERSONAL_ACCESS_TOKEN=abcdefghijklmnopqrstuvwx \ lego --email you@example.com --dns gandiv5 --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - GANDIV5_API_KEY = "API key" + GANDIV5_PERSONAL_ACCESS_TOKEN = "Personal Access Token" + GANDIV5_API_KEY = "API key (Deprecated)" [Configuration.Additional] GANDIV5_POLLING_INTERVAL = "Time between DNS propagation check" GANDIV5_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" diff --git a/providers/gandiv5/gandiv5_test.go b/providers/gandiv5/gandiv5_test.go index 36288d7..57fed03 100644 --- a/providers/gandiv5/gandiv5_test.go +++ b/providers/gandiv5/gandiv5_test.go @@ -10,11 +10,10 @@ import ( "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/platform/tester" - "github.com/entrustcorporation/dv/providers/gandiv5/internal" "github.com/stretchr/testify/require" ) -var envTest = tester.NewEnvTest(EnvAPIKey) +var envTest = tester.NewEnvTest(EnvAPIKey, EnvPersonalAccessToken) func TestNewDNSProvider(t *testing.T) { testCases := []struct { @@ -33,7 +32,7 @@ func TestNewDNSProvider(t *testing.T) { envVars: map[string]string{ EnvAPIKey: "", }, - expected: "gandi: some credentials information are missing: GANDIV5_API_KEY", + expected: "gandiv5: credentials information are missing", }, } @@ -70,7 +69,7 @@ func TestNewDNSProviderConfig(t *testing.T) { }, { desc: "missing credentials", - expected: "gandiv5: no API Key given", + expected: "gandiv5: credentials information are missing", }, } @@ -122,8 +121,8 @@ func TestDNSProvider(t *testing.T) { mux.HandleFunc("/domains/example.com/records/_acme-challenge.abc.def/TXT", func(rw http.ResponseWriter, req *http.Request) { log.Infof("request: %s %s", req.Method, req.URL) - if req.Header.Get(internal.APIKeyHeader) == "" { - http.Error(rw, `{"message": "missing API key"}`, http.StatusUnauthorized) + if req.Header.Get("Authorization") != "Bearer 123412341234123412341234" { + http.Error(rw, `{"message": "missing or malformed Authorization"}`, http.StatusUnauthorized) return } @@ -165,7 +164,7 @@ func TestDNSProvider(t *testing.T) { } config := NewDefaultConfig() - config.APIKey = "123412341234123412341234" + config.PersonalAccessToken = "123412341234123412341234" config.BaseURL = server.URL provider, err := NewDNSProviderConfig(config) diff --git a/providers/gandiv5/internal/client.go b/providers/gandiv5/internal/client.go index 96bf9bf..4d7235a 100644 --- a/providers/gandiv5/internal/client.go +++ b/providers/gandiv5/internal/client.go @@ -20,20 +20,25 @@ const defaultBaseURL = "https://dns.api.gandi.net/api/v5" // APIKeyHeader API key header. const APIKeyHeader = "X-Api-Key" +// Related to Personal Access Token. +const authorizationHeader = "Authorization" + // Client the Gandi API v5 client. type Client struct { apiKey string + pat string BaseURL *url.URL HTTPClient *http.Client } // NewClient Creates a new Client. -func NewClient(apiKey string) *Client { +func NewClient(apiKey, pat string) *Client { baseURL, _ := url.Parse(defaultBaseURL) return &Client{ apiKey: apiKey, + pat: pat, BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } @@ -128,6 +133,10 @@ func (c *Client) do(req *http.Request, result any) error { req.Header.Set(APIKeyHeader, c.apiKey) } + if c.pat != "" { + req.Header.Set(authorizationHeader, "Bearer "+c.pat) + } + resp, err := c.HTTPClient.Do(req) if err != nil { return errutils.NewHTTPDoError(req, err) diff --git a/providers/gcore/gcore.go b/providers/gcore/gcore.go index e23b4de..8ad76a7 100644 --- a/providers/gcore/gcore.go +++ b/providers/gcore/gcore.go @@ -57,7 +57,7 @@ type DNSProvider struct { client *internal.Client } -// NewDNSProvider returns an instance of DNSProvider configured for G-Core Labs DNS API. +// NewDNSProvider returns an instance of DNSProvider configured for G-Core DNS API. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(EnvPermanentAPIToken) if err != nil { @@ -70,7 +70,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderConfig return a DNSProvider instance configured for G-Core Labs DNS API. +// NewDNSProviderConfig return a DNSProvider instance configured for G-Core DNS API. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("gcore: the configuration of the DNS provider is nil") @@ -130,8 +130,8 @@ func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { return nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/gcore/gcore.toml b/providers/gcore/gcore.toml index d08caff..121a6d8 100644 --- a/providers/gcore/gcore.toml +++ b/providers/gcore/gcore.toml @@ -1,6 +1,6 @@ -Name = "G-Core Labs" +Name = "G-Core" Description = '''''' -URL = "https://gcorelabs.com/dns/" +URL = "https://gcore.com/dns/" Code = "gcore" Since = "v4.5.0" @@ -11,7 +11,7 @@ lego --email you@example.com --dns gcore --domains my.example.org run [Configuration] [Configuration.Credentials] - GCORE_PERMANENT_API_TOKEN = "Permanent API tokene (https://gcorelabs.com/blog/permanent-api-token-explained/)" + GCORE_PERMANENT_API_TOKEN = "Permanent API token (https://gcore.com/blog/permanent-api-token-explained/)" [Configuration.Additional] GCORE_POLLING_INTERVAL = "Time between DNS propagation check" GCORE_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" @@ -19,4 +19,4 @@ lego --email you@example.com --dns gcore --domains my.example.org run GCORE_HTTP_TIMEOUT = "API request timeout" [Links] - API = "https://dnsapi.gcorelabs.com/docs#tag/zonesV2" + API = "https://api.gcore.com/docs/dns#tag/zones" diff --git a/providers/gcore/internal/client.go b/providers/gcore/internal/client.go index b562dbc..654b61e 100644 --- a/providers/gcore/internal/client.go +++ b/providers/gcore/internal/client.go @@ -14,7 +14,7 @@ import ( "github.com/entrustcorporation/dv/providers/internal/errutils" ) -const defaultBaseURL = "https://api.gcorelabs.com/dns" +const defaultBaseURL = "https://api.gcore.com/dns" const ( authorizationHeader = "Authorization" @@ -43,7 +43,7 @@ func NewClient(token string) *Client { } // GetZone gets zone information. -// https://dnsapi.gcorelabs.com/docs#operation/Zone +// https://api.gcore.com/docs/dns#tag/zones/operation/Zone func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { endpoint := c.baseURL.JoinPath("v2", "zones", name) @@ -57,7 +57,7 @@ func (c *Client) GetZone(ctx context.Context, name string) (Zone, error) { } // GetRRSet gets RRSet item. -// https://dnsapi.gcorelabs.com/docs#operation/RRSet +// https://api.gcore.com/docs/dns#tag/rrsets/operation/RRSet func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) { endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) @@ -71,7 +71,7 @@ func (c *Client) GetRRSet(ctx context.Context, zone, name string) (RRSet, error) } // DeleteRRSet removes RRSet record. -// https://dnsapi.gcorelabs.com/docs#operation/DeleteRRSet +// https://api.gcore.com/docs/dns#tag/rrsets/operation/DeleteRRSet func (c *Client) DeleteRRSet(ctx context.Context, zone, name string) error { endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) @@ -102,14 +102,14 @@ func (c *Client) AddRRSet(ctx context.Context, zone, recordName, value string, t return c.createRRSet(ctx, zone, recordName, record) } -// https://dnsapi.gcorelabs.com/docs#operation/CreateRRSet +// https://api.gcore.com/docs/dns#tag/rrsets/operation/CreateRRSet func (c *Client) createRRSet(ctx context.Context, zone, name string, record RRSet) error { endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) return c.doRequest(ctx, http.MethodPost, endpoint, record, nil) } -// https://dnsapi.gcorelabs.com/docs#operation/UpdateRRSet +// https://api.gcore.com/docs/dns#tag/rrsets/operation/UpdateRRSet func (c *Client) updateRRSet(ctx context.Context, zone, name string, record RRSet) error { endpoint := c.baseURL.JoinPath("v2", "zones", zone, name, txtRecordType) diff --git a/providers/glesys/glesys.go b/providers/glesys/glesys.go index 56432aa..be11fc9 100644 --- a/providers/glesys/glesys.go +++ b/providers/glesys/glesys.go @@ -14,11 +14,7 @@ import ( "github.com/entrustcorporation/dv/providers/glesys/internal" ) -const ( - // defaultBaseURL is the GleSYS API endpoint used by Present and CleanUp. - defaultBaseURL = "https://api.glesys.com/domain" - minTTL = 60 -) +const minTTL = 60 // Environment variables names. const ( diff --git a/providers/godaddy/godaddy.go b/providers/godaddy/godaddy.go index d96dab5..fb6c70a 100644 --- a/providers/godaddy/godaddy.go +++ b/providers/godaddy/godaddy.go @@ -95,8 +95,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, client: client}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/hetzner/hetzner.go b/providers/hetzner/hetzner.go index d6b977e..75b3595 100644 --- a/providers/hetzner/hetzner.go +++ b/providers/hetzner/hetzner.go @@ -91,8 +91,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, client: client}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } diff --git a/providers/hostingde/hostingde.go b/providers/hostingde/hostingde.go index c23d5bc..75bda78 100644 --- a/providers/hostingde/hostingde.go +++ b/providers/hostingde/hostingde.go @@ -11,7 +11,7 @@ import ( "github.com/entrustcorporation/dv/dns01" "github.com/go-acme/lego/v4/platform/config/env" - "github.com/entrustcorporation/dv/providers/hostingde/internal" + "github.com/entrustcorporation/dv/providers/internal/hostingde" ) // Environment variables names. @@ -52,7 +52,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { config *Config - client *internal.Client + client *hostingde.Client recordIDs map[string]string recordIDsMu sync.Mutex @@ -86,7 +86,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{ config: config, - client: internal.NewClient(config.APIKey), + client: hostingde.NewClient(config.APIKey), recordIDs: make(map[string]string), }, nil } @@ -109,8 +109,8 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() // get the ZoneConfig for that domain - zonesFind := internal.ZoneConfigsFindRequest{ - Filter: internal.Filter{Field: "zoneName", Value: zoneName}, + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, Limit: 1, Page: 1, } @@ -122,14 +122,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { zoneConfig.Name = zoneName - rec := []internal.DNSRecord{{ + rec := []hostingde.DNSRecord{{ Type: "TXT", Name: dns01.UnFqdn(info.EffectiveFQDN), Content: info.Value, TTL: d.config.TTL, }} - req := internal.ZoneUpdateRequest{ + req := hostingde.ZoneUpdateRequest{ ZoneConfig: *zoneConfig, RecordsToAdd: rec, } @@ -166,8 +166,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() // get the ZoneConfig for that domain - zonesFind := internal.ZoneConfigsFindRequest{ - Filter: internal.Filter{Field: "zoneName", Value: zoneName}, + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, Limit: 1, Page: 1, } @@ -178,13 +178,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } zoneConfig.Name = zoneName - rec := []internal.DNSRecord{{ + rec := []hostingde.DNSRecord{{ Type: "TXT", Name: dns01.UnFqdn(info.EffectiveFQDN), Content: `"` + info.Value + `"`, }} - req := internal.ZoneUpdateRequest{ + req := hostingde.ZoneUpdateRequest{ ZoneConfig: *zoneConfig, RecordsToDelete: rec, } diff --git a/providers/httpnet/httpnet.go b/providers/httpnet/httpnet.go new file mode 100644 index 0000000..301d047 --- /dev/null +++ b/providers/httpnet/httpnet.go @@ -0,0 +1,223 @@ +// Package httpnet implements a DNS provider for solving the DNS-01 challenge using http.net. +package httpnet + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/internal/hostingde" +) + +// Environment variables names. +const ( + envNamespace = "HTTPNET_" + + EnvAPIKey = envNamespace + "API_KEY" + EnvZoneName = envNamespace + "ZONE_NAME" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + ZoneName string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *hostingde.Client + + recordIDs map[string]string + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for http.net. +// Credentials must be passed in the environment variables: +// HTTPNET_ZONE_NAME and HTTPNET_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("httpnet: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + config.ZoneName = env.GetOrFile(EnvZoneName) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for http.net. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("httpnet: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("httpnet: API key missing") + } + + client := hostingde.NewClient(config.APIKey) + client.BaseURL, _ = url.Parse(hostingde.DefaultHTTPNetBaseURL) + + return &DNSProvider{ + config: config, + client: client, + recordIDs: make(map[string]string), + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } + + zoneConfig.Name = zoneName + + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: info.Value, + TTL: d.config.TTL, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToAdd: rec, + } + + response, err := d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } + + for _, record := range response.Records { + if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) { + d.recordIDsMu.Lock() + d.recordIDs[info.EffectiveFQDN] = record.ID + d.recordIDsMu.Unlock() + } + } + + if d.recordIDs[info.EffectiveFQDN] == "" { + return fmt.Errorf("httpnet: error getting ID of just created record, for domain %s", domain) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + zoneName, err := d.getZoneName(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("httpnet: could not find zone for domain %q: %w", domain, err) + } + + ctx := context.Background() + + // get the ZoneConfig for that domain + zonesFind := hostingde.ZoneConfigsFindRequest{ + Filter: hostingde.Filter{Field: "zoneName", Value: zoneName}, + Limit: 1, + Page: 1, + } + + zoneConfig, err := d.client.GetZone(ctx, zonesFind) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } + zoneConfig.Name = zoneName + + rec := []hostingde.DNSRecord{{ + Type: "TXT", + Name: dns01.UnFqdn(info.EffectiveFQDN), + Content: `"` + info.Value + `"`, + }} + + req := hostingde.ZoneUpdateRequest{ + ZoneConfig: *zoneConfig, + RecordsToDelete: rec, + } + + // Delete record ID from map + d.recordIDsMu.Lock() + delete(d.recordIDs, info.EffectiveFQDN) + d.recordIDsMu.Unlock() + + _, err = d.client.UpdateZone(ctx, req) + if err != nil { + return fmt.Errorf("httpnet: %w", err) + } + return nil +} + +func (d *DNSProvider) getZoneName(fqdn string) (string, error) { + if d.config.ZoneName != "" { + return d.config.ZoneName, nil + } + + zoneName, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return "", fmt.Errorf("could not find zone for FQDN %q: %w", fqdn, err) + } + + if zoneName == "" { + return "", errors.New("empty zone name") + } + + return dns01.UnFqdn(zoneName), nil +} diff --git a/providers/httpnet/httpnet.toml b/providers/httpnet/httpnet.toml new file mode 100644 index 0000000..a465d06 --- /dev/null +++ b/providers/httpnet/httpnet.toml @@ -0,0 +1,25 @@ +Name = "http.net" +Description = '''''' +URL = "https://www.http.net/" +Code = "httpnet" +Since = "v4.15.0" + +Example = ''' +HTTPNET_API_KEY=xxxxxxxx \ +lego --email you@example.com --dns httpnet --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + HTTPNET_API_KEY = "API key" + [Configuration.Additional] + HTTPNET_ZONE_NAME = "Zone name in ACE format" + HTTPNET_POLLING_INTERVAL = "Time between DNS propagation check" + HTTPNET_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + HTTPNET_TTL = "The TTL of the TXT record used for the DNS challenge" + HTTPNET_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://www.http.net/docs/api/#dns" + + diff --git a/providers/httpnet/httpnet_test.go b/providers/httpnet/httpnet_test.go new file mode 100644 index 0000000..a9bc527 --- /dev/null +++ b/providers/httpnet/httpnet_test.go @@ -0,0 +1,139 @@ +package httpnet + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAPIKey, + EnvZoneName). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + EnvZoneName: "example.org", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAPIKey: "", + EnvZoneName: "", + }, + expected: "httpnet: some credentials information are missing: HTTPNET_API_KEY", + }, + { + desc: "missing access key", + envVars: map[string]string{ + EnvAPIKey: "", + EnvZoneName: "456", + }, + expected: "httpnet: some credentials information are missing: HTTPNET_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + zoneName string + expected string + }{ + { + desc: "success", + apiKey: "123", + zoneName: "example.org", + }, + { + desc: "missing credentials", + expected: "httpnet: API key missing", + }, + { + desc: "missing api key", + zoneName: "456", + expected: "httpnet: API key missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.ZoneName = test.zoneName + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.recordIDs) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(2 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/httpreq/httpreq.go b/providers/httpreq/httpreq.go index 9e11d65..98856f8 100644 --- a/providers/httpreq/httpreq.go +++ b/providers/httpreq/httpreq.go @@ -1,4 +1,4 @@ -// Package httpreq implements a DNS provider for solving the DNS-01 challenge through a HTTP server. +// Package httpreq implements a DNS provider for solving the DNS-01 challenge through an HTTP server. package httpreq import ( diff --git a/providers/httpreq/httpreq.toml b/providers/httpreq/httpreq.toml index a3bfd67..cd6c823 100644 --- a/providers/httpreq/httpreq.toml +++ b/providers/httpreq/httpreq.toml @@ -17,7 +17,7 @@ The server must provide: - `POST` `/present` - `POST` `/cleanup` -The URL of the server must be define by `HTTPREQ_ENDPOINT`. +The URL of the server must be defined by `HTTPREQ_ENDPOINT`. ### Mode diff --git a/providers/hurricane/internal/client.go b/providers/hurricane/internal/client.go index 359f285..30ef1c2 100644 --- a/providers/hurricane/internal/client.go +++ b/providers/hurricane/internal/client.go @@ -113,7 +113,7 @@ func evaluateBody(body string, hostname string) error { case codeAbuse: return fmt.Errorf("%s: blocked hostname for abuse: %s", body, hostname) case codeBadAgent: - return fmt.Errorf("%s: user agent not sent or HTTP method not recognized; open an issue on go-acme/lego on Github", body) + return fmt.Errorf("%s: user agent not sent or HTTP method not recognized; open an issue on go-acme/lego on GitHub", body) case codeBadAuth: return fmt.Errorf("%s: wrong authentication token provided for TXT record %s", body, hostname) case codeInterval: diff --git a/providers/ibmcloud/ibmcloud.toml b/providers/ibmcloud/ibmcloud.toml index 1b562ce..2a87c58 100644 --- a/providers/ibmcloud/ibmcloud.toml +++ b/providers/ibmcloud/ibmcloud.toml @@ -12,7 +12,7 @@ lego --email you@example.com --dns ibmcloud --domains my.example.org run [Configuration] [Configuration.Credentials] - SOFTLAYER_USERNAME = "User name (IBM Cloud is _)" + SOFTLAYER_USERNAME = "Username (IBM Cloud is _)" SOFTLAYER_API_KEY = "Classic Infrastructure API key" [Configuration.Additional] SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check" diff --git a/providers/hostingde/internal/client.go b/providers/internal/hostingde/client.go similarity index 90% rename from providers/hostingde/internal/client.go rename to providers/internal/hostingde/client.go index 418c3aa..9c03b22 100644 --- a/providers/hostingde/internal/client.go +++ b/providers/internal/hostingde/client.go @@ -1,4 +1,4 @@ -package internal +package hostingde import ( "bytes" @@ -14,23 +14,26 @@ import ( "github.com/entrustcorporation/dv/providers/internal/errutils" ) -const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" +const ( + DefaultHostingdeBaseURL = "https://secure.hosting.de/api/dns/v1/json" + DefaultHTTPNetBaseURL = "https://partner.http.net/api/dns/v1/json" +) // Client the API client for Hosting.de. type Client struct { apiKey string - baseURL *url.URL + BaseURL *url.URL HTTPClient *http.Client } // NewClient creates new Client. func NewClient(apiKey string) *Client { - baseURL, _ := url.Parse(defaultBaseURL) + baseURL, _ := url.Parse(DefaultHostingdeBaseURL) return &Client{ apiKey: apiKey, - baseURL: baseURL, + BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, } } @@ -71,7 +74,7 @@ func (c Client) GetZone(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneC // ListZoneConfigs lists zone configuration. // https://www.hosting.de/api/?json#list-zoneconfigs func (c Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) (*ZoneResponse, error) { - endpoint := c.baseURL.JoinPath("zoneConfigsFind") + endpoint := c.BaseURL.JoinPath("zoneConfigsFind") req.AuthToken = c.apiKey @@ -96,7 +99,7 @@ func (c Client) ListZoneConfigs(ctx context.Context, req ZoneConfigsFindRequest) // UpdateZone updates a zone. // https://www.hosting.de/api/?json#updating-zones func (c Client) UpdateZone(ctx context.Context, req ZoneUpdateRequest) (*Zone, error) { - endpoint := c.baseURL.JoinPath("zoneUpdate") + endpoint := c.BaseURL.JoinPath("zoneUpdate") req.AuthToken = c.apiKey diff --git a/providers/hostingde/internal/client_test.go b/providers/internal/hostingde/client_test.go similarity index 99% rename from providers/hostingde/internal/client_test.go rename to providers/internal/hostingde/client_test.go index af76d0d..d538c8b 100644 --- a/providers/hostingde/internal/client_test.go +++ b/providers/internal/hostingde/client_test.go @@ -1,4 +1,4 @@ -package internal +package hostingde import ( "bytes" @@ -26,7 +26,7 @@ func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client { client := NewClient("secret") client.HTTPClient = server.Client() - client.baseURL, _ = url.Parse(server.URL) + client.BaseURL, _ = url.Parse(server.URL) mux.HandleFunc(pattern, handler) diff --git a/providers/hostingde/internal/fixtures/zoneConfigsFind.json b/providers/internal/hostingde/fixtures/zoneConfigsFind.json similarity index 100% rename from providers/hostingde/internal/fixtures/zoneConfigsFind.json rename to providers/internal/hostingde/fixtures/zoneConfigsFind.json diff --git a/providers/hostingde/internal/fixtures/zoneConfigsFind_error.json b/providers/internal/hostingde/fixtures/zoneConfigsFind_error.json similarity index 100% rename from providers/hostingde/internal/fixtures/zoneConfigsFind_error.json rename to providers/internal/hostingde/fixtures/zoneConfigsFind_error.json diff --git a/providers/hostingde/internal/fixtures/zoneUpdate.json b/providers/internal/hostingde/fixtures/zoneUpdate.json similarity index 100% rename from providers/hostingde/internal/fixtures/zoneUpdate.json rename to providers/internal/hostingde/fixtures/zoneUpdate.json diff --git a/providers/hostingde/internal/fixtures/zoneUpdate_error.json b/providers/internal/hostingde/fixtures/zoneUpdate_error.json similarity index 100% rename from providers/hostingde/internal/fixtures/zoneUpdate_error.json rename to providers/internal/hostingde/fixtures/zoneUpdate_error.json diff --git a/providers/hostingde/internal/types.go b/providers/internal/hostingde/types.go similarity index 99% rename from providers/hostingde/internal/types.go rename to providers/internal/hostingde/types.go index a706008..4f33471 100644 --- a/providers/hostingde/internal/types.go +++ b/providers/internal/hostingde/types.go @@ -1,4 +1,4 @@ -package internal +package hostingde import "encoding/json" diff --git a/providers/internal/selectel/client.go b/providers/internal/selectel/client.go index f8addd5..8a53091 100644 --- a/providers/internal/selectel/client.go +++ b/providers/internal/selectel/client.go @@ -55,7 +55,7 @@ func (c *Client) GetDomainByName(ctx context.Context, domainName string) (*Domai statusCode, err := c.do(req, domain) if err != nil { if statusCode == http.StatusNotFound && strings.Count(domainName, ".") > 1 { - // Look up for the next sub domain + // Look up for the next subdomain subIndex := strings.Index(domainName, ".") return c.GetDomainByName(ctx, domainName[subIndex+1:]) } diff --git a/providers/inwx/inwx.go b/providers/inwx/inwx.go index f08df71..a4f5d04 100644 --- a/providers/inwx/inwx.go +++ b/providers/inwx/inwx.go @@ -108,7 +108,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { defer func() { errL := d.client.Account.Logout() if errL != nil { - log.Infof("inwx: failed to logout: %v", errL) + log.Infof("inwx: failed to log out: %v", errL) } }() @@ -158,7 +158,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { defer func() { errL := d.client.Account.Logout() if errL != nil { - log.Infof("inwx: failed to logout: %v", errL) + log.Infof("inwx: failed to log out: %v", errL) } }() @@ -199,7 +199,7 @@ func (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error { } if d.config.SharedSecret == "" { - return errors.New("two factor authentication but no shared secret is given") + return errors.New("two-factor authentication but no shared secret is given") } tan, err := totp.GenerateCode(d.config.SharedSecret, time.Now()) diff --git a/providers/ipv64/internal/client.go b/providers/ipv64/internal/client.go new file mode 100644 index 0000000..277c3b8 --- /dev/null +++ b/providers/ipv64/internal/client.go @@ -0,0 +1,153 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" + "golang.org/x/oauth2" +) + +const defaultBaseURL = "https://ipv64.net" + +type Client struct { + baseURL *url.URL + HTTPClient *http.Client +} + +func NewClient(hc *http.Client) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + if hc == nil { + hc = &http.Client{Timeout: 15 * time.Second} + } + + return &Client{ + baseURL: baseURL, + HTTPClient: hc, + } +} + +func (c Client) GetDomains(ctx context.Context) (*Domains, error) { + endpoint := c.baseURL.JoinPath("api") + + query := endpoint.Query() + query.Set("get_domains", "") + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + results := &Domains{} + + err = c.do(req, results) + if err != nil { + return nil, err + } + + return results, nil +} + +func (c Client) AddRecord(ctx context.Context, domain, prefix, recordType, content string) error { + endpoint := c.baseURL.JoinPath("api") + + data := make(url.Values) + data.Set("add_record", domain) + data.Set("praefix", prefix) + data.Set("type", recordType) + data.Set("content", content) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + return c.do(req, nil) +} + +func (c Client) DeleteRecord(ctx context.Context, domain, prefix, recordType, content string) error { + endpoint := c.baseURL.JoinPath("api") + + data := make(url.Values) + data.Set("del_record", domain) + data.Set("praefix", prefix) + data.Set("type", recordType) + data.Set("content", content) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint.String(), strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("unable to create request: %w", err) + } + + return c.do(req, nil) +} + +func (c Client) do(req *http.Request, result any) error { + if req.Method != http.MethodGet { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + if string(raw) == "null" { + return fmt.Errorf("unexpected response: %s", string(raw)) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + errAPI := &APIError{} + err := json.Unmarshal(raw, errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return errAPI +} + +func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client { + if client == nil { + client = &http.Client{Timeout: 15 * time.Second} + } + + client.Transport = &oauth2.Transport{ + Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}), + Base: client.Transport, + } + + return client +} diff --git a/providers/ipv64/internal/client_test.go b/providers/ipv64/internal/client_test.go new file mode 100644 index 0000000..1966f9f --- /dev/null +++ b/providers/ipv64/internal/client_test.go @@ -0,0 +1,149 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testAPIKey = "secret" + +func setupTest(t *testing.T, handler http.HandlerFunc) *Client { + t.Helper() + + server := httptest.NewServer(handler) + + client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey)) + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func testHandler(method, filename string, statusCode int) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed) + return + } + + auth := req.Header.Get("Authorization") + if auth != "Bearer "+testAPIKey { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + file, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = file.Close() }() + + rw.WriteHeader(statusCode) + + _, err = io.Copy(rw, file) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func TestClient_GetDomains(t *testing.T) { + client := setupTest(t, testHandler(http.MethodGet, "get_domains.json", http.StatusOK)) + + domains, err := client.GetDomains(context.Background()) + require.NoError(t, err) + + expected := &Domains{ + APIResponse: APIResponse{ + Status: "200 OK", + Info: "success", + }, + APICall: "get_domains", + Subdomains: map[string]Subdomain{ + "lego.home64.net": { + Updates: 0, + Wildcard: 1, + DomainUpdateHash: "Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v", + Records: []Record{ + { + RecordID: 50665, + Content: "2606:2800:220:1:248:1893:25c8:1946", + TTL: 60, + Type: "AAAA", + Prefix: "", + LastUpdate: "2023-07-19 13:18:59", + RecordKey: "MTA0YzdmMWVjYTFiNDBmZjYwMTU0OGUy", + }, + }, + }, + "lego.ipv64.net": { + Updates: 0, + Wildcard: 1, + DomainUpdateHash: "Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v", + Records: []Record{ + { + RecordID: 50664, + Content: "2606:2800:220:1:248:1893:25c8:1946", + TTL: 60, + Type: "AAAA", + Prefix: "", + LastUpdate: "2023-07-19 13:18:59", + RecordKey: "ZDMxOWUxMjZjOTk5MmQ3N2M3ODc4NjJj", + }, + }, + }, + }, + } + + assert.Equal(t, expected, domains) +} + +func TestClient_GetDomains_error(t *testing.T) { + client := setupTest(t, testHandler(http.MethodGet, "error.json", http.StatusUnauthorized)) + + domains, err := client.GetDomains(context.Background()) + require.Error(t, err) + + require.Nil(t, domains) +} + +func TestClient_AddRecord(t *testing.T) { + client := setupTest(t, testHandler(http.MethodPost, "add_record.json", http.StatusCreated)) + + err := client.AddRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + require.NoError(t, err) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := setupTest(t, testHandler(http.MethodPost, "add_record-error.json", http.StatusBadRequest)) + + err := client.AddRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + require.Error(t, err) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := setupTest(t, testHandler(http.MethodDelete, "del_record.json", http.StatusAccepted)) + + err := client.DeleteRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := setupTest(t, testHandler(http.MethodDelete, "del_record-error.json", http.StatusBadRequest)) + + err := client.DeleteRecord(context.Background(), "lego.ipv64.net", "_acme-challenge", "TXT", "value") + require.Error(t, err) +} diff --git a/providers/ipv64/internal/fixtures/add_record-error.json b/providers/ipv64/internal/fixtures/add_record-error.json new file mode 100644 index 0000000..73f3098 --- /dev/null +++ b/providers/ipv64/internal/fixtures/add_record-error.json @@ -0,0 +1,5 @@ +{ + "info": "error", + "status": "400 Bad Request", + "add_record": "dns record already there" +} diff --git a/providers/ipv64/internal/fixtures/add_record.json b/providers/ipv64/internal/fixtures/add_record.json new file mode 100644 index 0000000..2e5a64e --- /dev/null +++ b/providers/ipv64/internal/fixtures/add_record.json @@ -0,0 +1,5 @@ +{ + "info": "success", + "status": "201 Created", + "add_record": "lego.ipv64.net" +} diff --git a/providers/ipv64/internal/fixtures/del_record-error.json b/providers/ipv64/internal/fixtures/del_record-error.json new file mode 100644 index 0000000..a3d108d --- /dev/null +++ b/providers/ipv64/internal/fixtures/del_record-error.json @@ -0,0 +1,5 @@ +{ + "info": "error", + "status": "403 Forbidden", + "del_record": "del_record" +} diff --git a/providers/ipv64/internal/fixtures/del_record.json b/providers/ipv64/internal/fixtures/del_record.json new file mode 100644 index 0000000..4e27626 --- /dev/null +++ b/providers/ipv64/internal/fixtures/del_record.json @@ -0,0 +1,5 @@ +{ + "info": "success", + "status": "202 Accepted", + "del_record": "del_record" +} diff --git a/providers/ipv64/internal/fixtures/error.json b/providers/ipv64/internal/fixtures/error.json new file mode 100644 index 0000000..ba0fe85 --- /dev/null +++ b/providers/ipv64/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "status": "401 Unauthorized", + "info": "Unauthorized" +} diff --git a/providers/ipv64/internal/fixtures/get_domains.json b/providers/ipv64/internal/fixtures/get_domains.json new file mode 100644 index 0000000..3cb1de8 --- /dev/null +++ b/providers/ipv64/internal/fixtures/get_domains.json @@ -0,0 +1,39 @@ +{ + "subdomains": { + "lego.ipv64.net": { + "updates": 0, + "wildcard": 1, + "domain_update_hash": "Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v", + "records": [ + { + "record_id": 50664, + "content": "2606:2800:220:1:248:1893:25c8:1946", + "ttl": 60, + "type": "AAAA", + "praefix": "", + "last_update": "2023-07-19 13:18:59", + "record_key": "ZDMxOWUxMjZjOTk5MmQ3N2M3ODc4NjJj" + } + ] + }, + "lego.home64.net": { + "updates": 0, + "wildcard": 1, + "domain_update_hash": "Dr4l6jFVgkXITqZPEyMHLNsGAfwoSu9v", + "records": [ + { + "record_id": 50665, + "content": "2606:2800:220:1:248:1893:25c8:1946", + "ttl": 60, + "type": "AAAA", + "praefix": "", + "last_update": "2023-07-19 13:18:59", + "record_key": "MTA0YzdmMWVjYTFiNDBmZjYwMTU0OGUy" + } + ] + } + }, + "info": "success", + "status": "200 OK", + "add_domain": "get_domains" +} diff --git a/providers/ipv64/internal/types.go b/providers/ipv64/internal/types.go new file mode 100644 index 0000000..e9e357e --- /dev/null +++ b/providers/ipv64/internal/types.go @@ -0,0 +1,63 @@ +package internal + +import "fmt" + +type APIResponse struct { + Status string `json:"status"` + Info string `json:"info"` +} + +// error + +type APIError struct { + APIResponse + AddRecordMessage string `json:"add_record"` + DelRecordMessage string `json:"del_record"` + AddDomainMessage string `json:"add_domain"` + DelDomainMessage string `json:"del_domain"` +} + +func (a APIError) Error() string { + msg := a.Info + switch { + case a.AddRecordMessage != "": + msg = a.AddRecordMessage + case a.DelRecordMessage != "": + msg = a.DelRecordMessage + case a.AddDomainMessage != "": + msg = a.AddDomainMessage + case a.DelDomainMessage != "": + msg = a.DelDomainMessage + } + + if msg == "" { + return fmt.Sprintf("%s: %s", a.Status, a.Info) + } + + return fmt.Sprintf("%s (%s): %s", a.Info, a.Status, msg) +} + +// get_domains + +type Domains struct { + APIResponse + APICall string `json:"add_domain"` + Subdomains map[string]Subdomain `json:"subdomains"` +} + +type Subdomain struct { + Updates int `json:"updates"` + Wildcard int `json:"wildcard"` + DomainUpdateHash string `json:"domain_update_hash"` + Records []Record `json:"records"` +} + +type Record struct { + RecordID int `json:"record_id"` + Content string `json:"content"` + TTL int `json:"ttl"` + Type string `json:"type"` + Prefix string `json:"praefix"` + LastUpdate string `json:"last_update"` + RecordKey string `json:"record_key"` +} diff --git a/providers/ipv64/ipv64.go b/providers/ipv64/ipv64.go new file mode 100644 index 0000000..c272095 --- /dev/null +++ b/providers/ipv64/ipv64.go @@ -0,0 +1,143 @@ +// Package ipv64 implements a DNS provider for solving the DNS-01 challenge using IPv64. +// See https://ipv64.net/healthcheck_updater_api for more info on updating TXT records. +package ipv64 + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/ipv64/internal" + "github.com/miekg/dns" +) + +// Environment variables names. +const ( + envNamespace = "IPV64_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" // Deprecated: unused, will be removed in v5. +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + HTTPClient *http.Client + SequenceInterval time.Duration // Deprecated: unused, will be removed in v5. +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a new DNS provider using +// environment variable IPV64_TOKEN for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("ipv64: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for IPv64. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ipv64: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("ipv64: credentials missing") + } + + client := internal.NewClient(internal.OAuthStaticAccessToken(config.HTTPClient, config.APIKey)) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + sub, root, err := splitDomain(dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("ipv64: %w", err) + } + + err = d.client.AddRecord(context.Background(), root, sub, "TXT", info.Value) + if err != nil { + return fmt.Errorf("ipv64: %w", err) + } + + return nil +} + +// CleanUp clears IPv64 TXT record. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + sub, root, err := splitDomain(dns01.UnFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("ipv64: %w", err) + } + + err = d.client.DeleteRecord(context.Background(), root, sub, "TXT", info.Value) + if err != nil { + return fmt.Errorf("ipv64: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func splitDomain(full string) (string, string, error) { + split := dns.Split(full) + if len(split) < 3 { + return "", "", fmt.Errorf("unsupported domain: %s", full) + } + + if len(split) == 3 { + return "", full, nil + } + + domain := full[split[len(split)-3]:] + subDomain := full[:split[len(split)-3]-1] + + return subDomain, domain, nil +} diff --git a/providers/ipv64/ipv64.toml b/providers/ipv64/ipv64.toml new file mode 100644 index 0000000..6bcf841 --- /dev/null +++ b/providers/ipv64/ipv64.toml @@ -0,0 +1,22 @@ +Name = "IPv64" +Description = '''''' +URL = "https://ipv64.net/" +Code = "ipv64" +Since = "v4.13.0" + +Example = ''' +IPV64_API_KEY=xxxxxx \ +lego --email you@example.com --dns ipv64 --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + IPV64_API_KEY = "Account API Key" + [Configuration.Additional] + IPV64_POLLING_INTERVAL = "Time between DNS propagation check" + IPV64_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + IPV64_TTL = "The TTL of the TXT record used for the DNS challenge" + IPV64_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://ipv64.net/dyndns_updater_api" diff --git a/providers/ipv64/ipv64_test.go b/providers/ipv64/ipv64_test.go new file mode 100644 index 0000000..114919e --- /dev/null +++ b/providers/ipv64/ipv64_test.go @@ -0,0 +1,195 @@ +package ipv64 + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func Test_splitDomain(t *testing.T) { + type expected struct { + root string + sub string + requireErr require.ErrorAssertionFunc + } + + testCases := []struct { + desc string + domain string + expected expected + }{ + { + desc: "empty", + domain: "", + expected: expected{ + requireErr: require.Error, + }, + }, + { + desc: "2 levels", + domain: "example.com", + expected: expected{ + requireErr: require.Error, + }, + }, + { + desc: "3 levels", + domain: "_acme-challenge.example.com", + expected: expected{ + root: "_acme-challenge.example.com", + sub: "", + requireErr: require.NoError, + }, + }, + { + desc: "4 levels", + domain: "_acme-challenge.sub.example.com", + expected: expected{ + root: "sub.example.com", + sub: "_acme-challenge", + requireErr: require.NoError, + }, + }, + { + desc: "5 levels", + domain: "_acme-challenge.my.sub.example.com", + expected: expected{ + root: "sub.example.com", + sub: "_acme-challenge.my", + requireErr: require.NoError, + }, + }, + { + desc: "6 levels", + domain: "_acme-challenge.my.sub.sub.example.com", + expected: expected{ + root: "sub.example.com", + sub: "_acme-challenge.my.sub", + requireErr: require.NoError, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + sub, root, err := splitDomain(test.domain) + test.expected.requireErr(t, err) + + assert.Equal(t, test.expected.root, root) + assert.Equal(t, test.expected.sub, sub) + }) + } +} + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing api key", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "ipv64: some credentials information are missing: IPV64_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + expected string + }{ + { + desc: "success", + apiKey: "123", + }, + { + desc: "missing credentials", + expected: "ipv64: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/joker/joker.toml b/providers/joker/joker.toml index 52a17c7..786097a 100644 --- a/providers/joker/joker.toml +++ b/providers/joker/joker.toml @@ -29,7 +29,7 @@ In the SVC mode, username and passsword are not your email and account passwords As per [Joker.com documentation](https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html): -> 1. please login at Joker.com, visit 'My Domains', +> 1. please log in at Joker.com, visit 'My Domains', > find the domain you want to add Let's Encrypt certificate for, and chose "DNS" in the menu > > 2. on the top right, you will find the setting for 'Dynamic DNS'. diff --git a/providers/lightsail/lightsail.go b/providers/lightsail/lightsail.go index 98220fc..aaf26d8 100644 --- a/providers/lightsail/lightsail.go +++ b/providers/lightsail/lightsail.go @@ -2,17 +2,18 @@ package lightsail import ( + "context" "errors" "fmt" "math/rand" "strconv" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/lightsail" + awstypes "github.com/aws/aws-sdk-go-v2/service/lightsail/types" "github.com/entrustcorporation/dv/dns01" "github.com/go-acme/lego/v4/platform/config/env" ) @@ -32,27 +33,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -// customRetryer implements the client.Retryer interface by composing the DefaultRetryer. -// It controls the logic for retrying recoverable request errors (e.g. when rate limits are exceeded). -type customRetryer struct { - client.DefaultRetryer -} - -// RetryRules overwrites the DefaultRetryer's method. -// It uses a basic exponential backoff algorithm that returns an initial -// delay of ~400ms with an upper limit of ~30 seconds which should prevent -// causing a high number of consecutive throttling errors. -// For reference: Route 53 enforces an account-wide(!) 5req/s query limit. -func (c customRetryer) RetryRules(r *request.Request) time.Duration { - retryCount := r.RetryCount - if retryCount > 7 { - retryCount = 7 - } - - delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) - return time.Duration(delay) * time.Millisecond -} - // Config is used to configure the creation of the DNSProvider. type Config struct { DNSZone string @@ -71,7 +51,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - client *lightsail.Lightsail + client *lightsail.Client config *Config } @@ -102,35 +82,55 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("lightsail: the configuration of the DNS provider is nil") } - retryer := customRetryer{} - retryer.NumMaxRetries = maxRetries - - conf := aws.NewConfig().WithRegion(config.Region) - sess, err := session.NewSession(request.WithRetryer(conf, retryer)) + ctx := context.Background() + + cfg, err := awsconfig.LoadDefaultConfig(ctx, + awsconfig.WithRegion(config.Region), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.NewStandard(func(options *retry.StandardOptions) { + options.MaxAttempts = maxRetries + + // It uses a basic exponential backoff algorithm that returns an initial + // delay of ~400ms with an upper limit of ~30 seconds which should prevent + // causing a high number of consecutive throttling errors. + // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. + options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) { + retryCount := attempt + if retryCount > 7 { + retryCount = 7 + } + + delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) + return time.Duration(delay) * time.Millisecond, nil + }) + }) + }), + ) if err != nil { return nil, err } return &DNSProvider{ config: config, - client: lightsail.New(sess), + client: lightsail.NewFromConfig(cfg), }, nil } // Present creates a TXT record using the specified parameters. -func (d *DNSProvider) Present(domain, token, keyAuth string) error { +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + ctx := context.Background() info := dns01.GetChallengeInfo(domain, keyAuth) params := &lightsail.CreateDomainEntryInput{ DomainName: aws.String(d.config.DNSZone), - DomainEntry: &lightsail.DomainEntry{ + DomainEntry: &awstypes.DomainEntry{ Name: aws.String(info.EffectiveFQDN), Target: aws.String(strconv.Quote(info.Value)), Type: aws.String("TXT"), }, } - _, err := d.client.CreateDomainEntry(params) + _, err := d.client.CreateDomainEntry(ctx, params) if err != nil { return fmt.Errorf("lightsail: %w", err) } @@ -139,19 +139,20 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } // CleanUp removes the TXT record matching the specified parameters. -func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { +func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error { + ctx := context.Background() info := dns01.GetChallengeInfo(domain, keyAuth) params := &lightsail.DeleteDomainEntryInput{ DomainName: aws.String(d.config.DNSZone), - DomainEntry: &lightsail.DomainEntry{ + DomainEntry: &awstypes.DomainEntry{ Name: aws.String(info.EffectiveFQDN), Type: aws.String("TXT"), Target: aws.String(strconv.Quote(info.Value)), }, } - _, err := d.client.DeleteDomainEntry(params) + _, err := d.client.DeleteDomainEntry(ctx, params) if err != nil { return fmt.Errorf("lightsail: %w", err) } diff --git a/providers/lightsail/lightsail.toml b/providers/lightsail/lightsail.toml index fab06ef..4ade894 100644 --- a/providers/lightsail/lightsail.toml +++ b/providers/lightsail/lightsail.toml @@ -56,4 +56,4 @@ Alternatively, you can also set the `Resource` to `*` (wildcard), which allow to LIGHTSAIL_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" [Links] - GoClient = "https://github.com/aws/aws-sdk-go/" + GoClient = "https://github.com/aws/aws-sdk-go-v2" diff --git a/providers/lightsail/lightsail_integration_test.go b/providers/lightsail/lightsail_integration_test.go index 4eb7997..20e45ee 100644 --- a/providers/lightsail/lightsail_integration_test.go +++ b/providers/lightsail/lightsail_integration_test.go @@ -1,11 +1,12 @@ package lightsail import ( + "context" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/stretchr/testify/require" ) @@ -24,13 +25,15 @@ func TestLiveTTL(t *testing.T) { err = provider.Present(domain, "foo", "bar") require.NoError(t, err) - // we need a separate Lightsail client here as the one in the DNS provider is - // unexported. + // we need a separate Lightsail client here as the one in the DNS provider is unexported. fqdn := "_acme-challenge." + domain - sess, err := session.NewSession() + + ctx := context.Background() + + cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) - svc := lightsail.New(sess) + svc := lightsail.NewFromConfig(cfg) require.NoError(t, err) defer func() { @@ -44,15 +47,24 @@ func TestLiveTTL(t *testing.T) { DomainName: aws.String(domain), } - resp, err := svc.GetDomain(params) + resp, err := svc.GetDomain(ctx, params) require.NoError(t, err) entries := resp.Domain.DomainEntries for _, entry := range entries { - if aws.StringValue(entry.Type) == "TXT" && aws.StringValue(entry.Name) == fqdn { + if deref(entry.Type) == "TXT" && deref(entry.Name) == fqdn { return } } t.Fatalf("Could not find a TXT record for _acme-challenge.%s", domain) } + +func deref[T string | int | int32 | int64 | bool](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/lightsail/lightsail_test.go b/providers/lightsail/lightsail_test.go index f385745..8ff60c1 100644 --- a/providers/lightsail/lightsail_test.go +++ b/providers/lightsail/lightsail_test.go @@ -1,14 +1,16 @@ package lightsail import ( + "context" "os" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -29,23 +31,26 @@ var envTest = tester.NewEnvTest( WithDomain(EnvDNSZone). WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone) -func makeProvider(serverURL string) (*DNSProvider, error) { - config := &aws.Config{ - Credentials: credentials.NewStaticCredentials("abc", "123", " "), - Endpoint: aws.String(serverURL), - Region: aws.String("mock-region"), - MaxRetries: aws.Int(1), - } +type endpointResolverMock struct { + endpoint string +} - sess, err := session.NewSession(config) - if err != nil { - return nil, err - } +func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{URL: e.endpoint}, nil +} - conf := NewDefaultConfig() +func makeProvider(serverURL string) *DNSProvider { + config := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL}, + RetryMaxAttempts: 1, + } - client := lightsail.New(sess) - return &DNSProvider{client: client, config: conf}, nil + return &DNSProvider{ + client: lightsail.NewFromConfig(config), + config: NewDefaultConfig(), + } } func TestCredentialsFromEnv(t *testing.T) { @@ -56,15 +61,19 @@ func TestCredentialsFromEnv(t *testing.T) { _ = os.Setenv(envAwsSecretAccessKey, "123") _ = os.Setenv(envAwsRegion, "us-east-1") - config := &aws.Config{ - CredentialsChainVerboseErrors: aws.Bool(true), - } - - sess, err := session.NewSession(config) + ctx := context.Background() + cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) - _, err = sess.Config.Credentials.Get() + cs, err := cfg.Credentials.Retrieve(ctx) require.NoError(t, err, "Expected credentials to be set from environment") + + expected := aws.Credentials{ + AccessKeyID: "123", + SecretAccessKey: "123", + Source: "EnvConfigCredentials", + } + assert.Equal(t, expected, cs) } func TestDNSProvider_Present(t *testing.T) { @@ -74,12 +83,11 @@ func TestDNSProvider_Present(t *testing.T) { serverURL := newMockServer(t, mockResponses) - provider, err := makeProvider(serverURL) - require.NoError(t, err) + provider := makeProvider(serverURL) domain := "example.com" keyAuth := "123456d==" - err = provider.Present(domain, "", keyAuth) + err := provider.Present(domain, "", keyAuth) require.NoError(t, err, "Expected Present to return no error") } diff --git a/providers/linode/linode.go b/providers/linode/linode.go index f9fc3dd..97f3b21 100644 --- a/providers/linode/linode.go +++ b/providers/linode/linode.go @@ -104,8 +104,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, client: &client}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (time.Duration, time.Duration) { timeout := d.config.PropagationTimeout if d.config.PropagationTimeout <= 0 { diff --git a/providers/loopia/loopia.go b/providers/loopia/loopia.go index 9ff1b4f..c7698be 100644 --- a/providers/loopia/loopia.go +++ b/providers/loopia/loopia.go @@ -192,7 +192,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { err = d.client.RemoveSubdomain(ctx, authZone, subDomain) if err != nil { - return fmt.Errorf("loopia: failed to remove sub-domain: %w", err) + return fmt.Errorf("loopia: failed to remove subdomain: %w", err) } return nil diff --git a/providers/loopia/loopia_mock_test.go b/providers/loopia/loopia_mock_test.go index f786b3b..869c29e 100644 --- a/providers/loopia/loopia_mock_test.go +++ b/providers/loopia/loopia_mock_test.go @@ -149,10 +149,10 @@ func TestDNSProvider_Cleanup(t *testing.T) { callGetTXTRecords: true, callRemoveSubdomain: true, - expectedError: `loopia: failed to remove sub-domain: unknown error: "UNKNOWN_ERROR"`, + expectedError: `loopia: failed to remove subdomain: unknown error: "UNKNOWN_ERROR"`, }, { - desc: "Dont call removeSubdomain when records", + desc: "Don't call removeSubdomain when records", getTXTRecordsReturn: []internal.RecordObj{{Type: "TXT", Rdata: "LEFTOVER"}}, callAddTXTRecord: true, diff --git a/providers/metaname/metaname.go b/providers/metaname/metaname.go new file mode 100644 index 0000000..e5d5e20 --- /dev/null +++ b/providers/metaname/metaname.go @@ -0,0 +1,159 @@ +// Package metaname implements a DNS provider for solving the DNS-01 challenge using Metaname. +package metaname + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/nzdjb/go-metaname" +) + +// Environment variables names. +const ( + envNamespace = "METANAME_" + + EnvAccountReference = envNamespace + "ACCOUNT_REFERENCE" + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + AccountReference string + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *metaname.MetanameClient + + records map[string]string + recordsMu sync.Mutex +} + +// NewDNSProvider returns a new DNS provider +// using environment variable METANAME_API_KEY for adding and removing the DNS record. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAccountReference, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("metaname: %w", err) + } + + config := NewDefaultConfig() + config.AccountReference = values[EnvAccountReference] + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Metaname. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("metaname: the configuration of the DNS provider is nil") + } + + if config.AccountReference == "" { + return nil, errors.New("metaname: missing account reference") + } + if config.APIKey == "" { + return nil, errors.New("metaname: missing api key") + } + + return &DNSProvider{ + config: config, + client: metaname.NewMetanameClient(config.AccountReference, config.APIKey), + records: make(map[string]string), + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("metaname: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + authZone = dns01.UnFqdn(authZone) + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("metaname: could not extract subDomain: %w", err) + } + + ctx := context.Background() + + r := metaname.ResourceRecord{ + Name: subDomain, + Type: "TXT", + Aux: nil, + Ttl: d.config.TTL, + Data: info.Value, + } + + ref, err := d.client.CreateDnsRecord(ctx, authZone, r) + if err != nil { + return fmt.Errorf("metaname: add record: %w", err) + } + + d.recordsMu.Lock() + d.records[token] = ref + d.recordsMu.Unlock() + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("metaname: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + authZone = dns01.UnFqdn(authZone) + + ctx := context.Background() + + d.recordsMu.Lock() + ref, ok := d.records[token] + d.recordsMu.Unlock() + + if !ok { + return fmt.Errorf("metaname: unknown ref for %s", info.EffectiveFQDN) + } + + err = d.client.DeleteDnsRecord(ctx, authZone, ref) + if err != nil { + return fmt.Errorf("metaname: delete record: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/metaname/metaname.toml b/providers/metaname/metaname.toml new file mode 100644 index 0000000..bacdf9b --- /dev/null +++ b/providers/metaname/metaname.toml @@ -0,0 +1,24 @@ +Name = "Metaname" +Description = '''''' +URL = "https://metaname.net" +Code = "metaname" +Since = "v4.13.0" + +Example = ''' +METANAME_ACCOUNT_REFERENCE=xxxx \ +METANAME_API_KEY=yyyyyyy \ +lego --email you@example.com --dns metaname --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + METANAME_ACCOUNT_REFERENCE = "The four-digit reference of a Metaname account" + METANAME_API_KEY = "API Key" + [Configuration.Additional] + METANAME_POLLING_INTERVAL = "Time between DNS propagation check" + METANAME_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + METANAME_TTL = "The TTL of the TXT record used for the DNS challenge" + +[Links] + API = "https://metaname.net/api/1.1/doc" + GoClient = "https://github.com/nzdjb/go-metaname" diff --git a/providers/metaname/metaname_test.go b/providers/metaname/metaname_test.go new file mode 100644 index 0000000..174af40 --- /dev/null +++ b/providers/metaname/metaname_test.go @@ -0,0 +1,145 @@ +package metaname + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAccountReference, EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAccountReference: "user", + EnvAPIKey: "secret", + }, + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvAccountReference: "", + EnvAPIKey: "secret", + }, + expected: "metaname: some credentials information are missing: METANAME_ACCOUNT_REFERENCE", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvAccountReference: "user", + EnvAPIKey: "", + }, + expected: "metaname: some credentials information are missing: METANAME_API_KEY", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expected: "metaname: some credentials information are missing: METANAME_ACCOUNT_REFERENCE,METANAME_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + accountReference string + apiKey string + expected string + }{ + { + desc: "success", + accountReference: "user", + apiKey: "secret", + }, + { + desc: "missing username", + apiKey: "secret", + expected: "metaname: missing account reference", + }, + { + desc: "missing password", + accountReference: "user", + expected: "metaname: missing api key", + }, + { + desc: "missing all", + expected: "metaname: missing account reference", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + + config.AccountReference = test.accountReference + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + time.Sleep(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/mythicbeasts/internal/client.go b/providers/mythicbeasts/internal/client.go index 48e5efb..01f29ff 100644 --- a/providers/mythicbeasts/internal/client.go +++ b/providers/mythicbeasts/internal/client.go @@ -20,7 +20,7 @@ const ( AuthBaseURL = "https://auth.mythic-beasts.com/login" ) -// Client the EasyDNS API client. +// Client the Mythic Beasts API client. type Client struct { username string password string diff --git a/providers/namecheap/namecheap.go b/providers/namecheap/namecheap.go index fd2f3e9..d494e05 100644 --- a/providers/namecheap/namecheap.go +++ b/providers/namecheap/namecheap.go @@ -18,17 +18,16 @@ import ( ) // Notes about namecheap's tool API: -// 1. Using the API requires registration. Once registered, use your account -// name and API key to access the API. -// 2. There is no API to add or modify a single DNS record. Instead you must -// read the entire list of records, make modifications, and then write the -// entire updated list of records. (Yuck.) -// 3. Namecheap's DNS updates can be slow to propagate. I've seen them take -// as long as an hour. -// 4. Namecheap requires you to whitelist the IP address from which you call -// its APIs. It also requires all API calls to include the whitelisted IP -// address as a form or query string value. This code uses a namecheap -// service to query the client's IP address. +// 1. Using the API requires registration. +// Once registered, use your account name and API key to access the API. +// 2. There is no API to add or modify a single DNS record. +// Instead, you must read the entire list of records, make modifications, +// and then write the entire updated list of records. (Yuck.) +// 3. Namecheap's DNS updates can be slow to propagate. +// I've seen them take as long as an hour. +// 4. Namecheap requires you to whitelist the IP address from which you call its APIs. +// It also requires all API calls to include the whitelisted IP address as a form or query string value. +// This code uses a namecheap service to query the client's IP address. // Environment variables names. const ( diff --git a/providers/nearlyfreespeech/internal/client.go b/providers/nearlyfreespeech/internal/client.go index 0cecb01..8d3d356 100644 --- a/providers/nearlyfreespeech/internal/client.go +++ b/providers/nearlyfreespeech/internal/client.go @@ -28,6 +28,8 @@ type Client struct { login string apiKey string + signer *Signer + baseURL *url.URL HTTPClient *http.Client } @@ -38,6 +40,7 @@ func NewClient(login string, apiKey string) *Client { return &Client{ login: login, apiKey: apiKey, + signer: NewSigner(), baseURL: baseURL, HTTPClient: &http.Client{Timeout: 10 * time.Second}, } @@ -74,7 +77,7 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set(authenticationHeader, c.createSignature(endpoint.Path, payload)) + req.Header.Set(authenticationHeader, c.signer.Sign(endpoint.Path, payload, c.login, c.apiKey)) resp, err := c.HTTPClient.Do(req) if err != nil { @@ -90,33 +93,51 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val return nil } -func (c Client) createSignature(uri string, body string) string { - // This is the only part of this that needs to be serialized. - salt := make([]byte, 16) - for i := 0; i < 16; i++ { - salt[i] = saltBytes[rand.Intn(len(saltBytes))] +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + errAPI := &APIError{} + err := json.Unmarshal(raw, errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) } + return errAPI +} + +type Signer struct { + saltShaker func() []byte + clock func() time.Time +} + +func NewSigner() *Signer { + return &Signer{saltShaker: getRandomSalt, clock: time.Now} +} + +func (c Signer) Sign(uri string, body, login, apiKey string) string { // Header is "login;timestamp;salt;hash". // hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash") // and body-hash is SHA1(body). bodyHash := sha1.Sum([]byte(body)) - timestamp := strconv.FormatInt(time.Now().Unix(), 10) + timestamp := strconv.FormatInt(c.clock().Unix(), 10) - hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", c.login, timestamp, salt, c.apiKey, uri, bodyHash) + // Workaround for https://golang.org/issue/58605 + uri = "/" + strings.TrimLeft(uri, "/") - return fmt.Sprintf("%s;%s;%s;%02x", c.login, timestamp, salt, sha1.Sum([]byte(hashInput))) -} + salt := c.saltShaker() -func parseError(req *http.Request, resp *http.Response) error { - raw, _ := io.ReadAll(resp.Body) + hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", login, timestamp, salt, apiKey, uri, bodyHash) - errAPI := &APIError{} - err := json.Unmarshal(raw, errAPI) - if err != nil { - return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + return fmt.Sprintf("%s;%s;%s;%02x", login, timestamp, salt, sha1.Sum([]byte(hashInput))) +} + +func getRandomSalt() []byte { + // This is the only part of this that needs to be serialized. + salt := make([]byte, 16) + for i := 0; i < 16; i++ { + salt[i] = saltBytes[rand.Intn(len(saltBytes))] } - return errAPI + return salt } diff --git a/providers/nearlyfreespeech/internal/client_test.go b/providers/nearlyfreespeech/internal/client_test.go index 05d7d67..16fa82e 100644 --- a/providers/nearlyfreespeech/internal/client_test.go +++ b/providers/nearlyfreespeech/internal/client_test.go @@ -9,7 +9,9 @@ import ( "net/url" "os" "testing" + "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,6 +26,9 @@ func setupTest(t *testing.T) (*Client, *http.ServeMux) { client.HTTPClient = server.Client() client.baseURL, _ = url.Parse(server.URL) + client.signer.saltShaker = func() []byte { return []byte("0123456789ABCDEF") } + client.signer.clock = func() time.Time { return time.Unix(1692475113, 0) } + return client, mux } @@ -147,3 +152,63 @@ func TestClient_RemoveRecord_error(t *testing.T) { err := client.RemoveRecord(context.Background(), "example.com", record) require.Error(t, err) } + +func TestSigner_Sign(t *testing.T) { + testCases := []struct { + desc string + path string + now int64 + salt string + expected string + }{ + { + desc: "basic", + path: "/path", + now: 1692475113, + salt: "0123456789ABCDEF", + expected: "user;1692475113;0123456789ABCDEF;417a9988c7ad7919b297884dd120b5808d8a1e6f", + }, + { + desc: "another date", + path: "/path", + now: 1692567766, + salt: "0123456789ABCDEF", + expected: "user;1692567766;0123456789ABCDEF;b5c28286fd2e1a45a7c576dc2a6430116f721502", + }, + { + desc: "another salt", + path: "/path", + now: 1692475113, + salt: "FEDCBA9876543210", + expected: "user;1692475113;FEDCBA9876543210;0f766822bda4fdc09829be4e1ea5e27ae3ae334e", + }, + { + desc: "empty path", + path: "", + now: 1692475113, + salt: "0123456789ABCDEF", + expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1", + }, + { + desc: "root path", + path: "/", + now: 1692475113, + salt: "0123456789ABCDEF", + expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + signer := NewSigner() + signer.saltShaker = func() []byte { return []byte(test.salt) } + signer.clock = func() time.Time { return time.Unix(test.now, 0) } + + sign := signer.Sign(test.path, "data", "user", "secret") + + assert.Equal(t, test.expected, sign) + }) + } +} diff --git a/providers/nicmanager/nicmanager.go b/providers/nicmanager/nicmanager.go index 935363c..8d3ee8a 100644 --- a/providers/nicmanager/nicmanager.go +++ b/providers/nicmanager/nicmanager.go @@ -201,5 +201,5 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } } - return fmt.Errorf("nicmanager: no record found to cleanup") + return fmt.Errorf("nicmanager: no record found to clean up") } diff --git a/providers/nicmanager/nicmanager.toml b/providers/nicmanager/nicmanager.toml index 7d902ad..913f685 100644 --- a/providers/nicmanager/nicmanager.toml +++ b/providers/nicmanager/nicmanager.toml @@ -30,7 +30,7 @@ lego --email you@example.com --dns nicmanager --domains my.example.org run Additional = ''' ## Description -You can login using your account name + username or using your email address. +You can log in using your account name + username or using your email address. Optionally if TOTP is configured for your account, set `NICMANAGER_API_OTP`. ''' diff --git a/providers/oraclecloud/oraclecloud.go b/providers/oraclecloud/oraclecloud.go index 347816f..9544c93 100644 --- a/providers/oraclecloud/oraclecloud.go +++ b/providers/oraclecloud/oraclecloud.go @@ -161,7 +161,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } if *domainRecords.OpcTotalItems == 0 { - return errors.New("oraclecloud: no record to CleanUp") + return errors.New("oraclecloud: no record to clean up") } var deleteHash *string @@ -173,7 +173,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } if deleteHash == nil { - return errors.New("oraclecloud: no record to CleanUp") + return errors.New("oraclecloud: no record to clean up") } recordOperation := dns.RecordOperation{ diff --git a/providers/otc/otc.go b/providers/otc/otc.go index a8cd2a1..9f42eea 100644 --- a/providers/otc/otc.go +++ b/providers/otc/otc.go @@ -33,6 +33,7 @@ const ( EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" + EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL" ) // Config is used to configure the creation of the DNSProvider. @@ -44,6 +45,7 @@ type Config struct { Password string PropagationTimeout time.Duration PollingInterval time.Duration + SequenceInterval time.Duration TTL int HTTPClient *http.Client } @@ -55,6 +57,7 @@ func NewDefaultConfig() *Config { PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), + SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), Transport: &http.Transport{ @@ -202,3 +205,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } + +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} diff --git a/providers/otc/otc.toml b/providers/otc/otc.toml index 7f9703b..e3c6015 100644 --- a/providers/otc/otc.toml +++ b/providers/otc/otc.toml @@ -16,6 +16,7 @@ Example = '''''' [Configuration.Additional] OTC_POLLING_INTERVAL = "Time between DNS propagation check" OTC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + OTC_SEQUENCE_INTERVAL = "Time between sequential requests" OTC_TTL = "The TTL of the TXT record used for the DNS challenge" OTC_HTTP_TIMEOUT = "API request timeout" diff --git a/providers/ovh/ovh.toml b/providers/ovh/ovh.toml index ab8ae49..ddd51d2 100644 --- a/providers/ovh/ovh.toml +++ b/providers/ovh/ovh.toml @@ -17,7 +17,7 @@ Additional = ''' Application key and secret can be created by following the [OVH guide](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/). -When requesting the consumer key, the following configuration can be use to define access rights: +When requesting the consumer key, the following configuration can be used to define access rights: ```json { diff --git a/providers/pdns/internal/client.go b/providers/pdns/internal/client.go index f261cc3..fd78b9b 100644 --- a/providers/pdns/internal/client.go +++ b/providers/pdns/internal/client.go @@ -218,7 +218,8 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo req.Header.Set("Accept", "application/json") - if payload != nil { + // PowerDNS doesn't follow HTTP convention about the "Content-Type" header. + if method != http.MethodGet && method != http.MethodDelete { req.Header.Set("Content-Type", "application/json") } diff --git a/providers/pdns/pdns.go b/providers/pdns/pdns.go index de3f2d5..40cd932 100644 --- a/providers/pdns/pdns.go +++ b/providers/pdns/pdns.go @@ -104,8 +104,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, client: client}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } @@ -142,7 +142,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } rec := internal.Record{ - Content: "\"" + info.EffectiveFQDN + "\"", + Content: "\"" + info.Value + "\"", Disabled: false, // pre-v1 API diff --git a/providers/rcodezero/internal/client.go b/providers/rcodezero/internal/client.go new file mode 100644 index 0000000..4893f4d --- /dev/null +++ b/providers/rcodezero/internal/client.go @@ -0,0 +1,114 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" + "github.com/miekg/dns" +) + +const defaultBaseURL = "https://my.rcodezero.at/api" + +const authorizationHeader = "Authorization" + +// Client for the RcodeZero API. +type Client struct { + apiToken string + + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient creates a new Client. +func NewClient(apiToken string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiToken: apiToken, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +func (c *Client) UpdateRecords(ctx context.Context, authZone string, sets []UpdateRRSet) (*APIResponse, error) { + endpoint := c.baseURL.JoinPath("v1", "acme", "zones", strings.TrimSuffix(dns.Fqdn(authZone), "."), "rrsets") + + req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, sets) + if err != nil { + return nil, err + } + + return c.do(req) +} + +func (c *Client) do(req *http.Request) (*APIResponse, error) { + req.Header.Set(authorizationHeader, "Bearer "+c.apiToken) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return nil, parseError(req, resp) + } + + result := &APIResponse{} + raw, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return nil, errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return result, nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + errAPI := &APIResponse{} + err := json.Unmarshal(raw, errAPI) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("[status code: %d] %w", resp.StatusCode, errAPI) +} diff --git a/providers/rcodezero/internal/client_test.go b/providers/rcodezero/internal/client_test.go new file mode 100644 index 0000000..c19e6e5 --- /dev/null +++ b/providers/rcodezero/internal/client_test.go @@ -0,0 +1,96 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + apiToken := req.Header.Get(authorizationHeader) + if apiToken != "Bearer secret" { + http.Error(rw, fmt.Sprintf("invalid credentials: %s", apiToken), http.StatusBadRequest) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func TestClient_UpdateRecords_error(t *testing.T) { + client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusUnprocessableEntity, "error.json") + + rrSet := []UpdateRRSet{{ + Name: "acme.example.org.", + ChangeType: "add", + Type: "TXT", + Records: []Record{{Content: `"my-acme-challenge"`}}, + }} + + resp, err := client.UpdateRecords(context.Background(), "example.org", rrSet) + require.ErrorAs(t, err, new(*APIResponse)) + assert.Nil(t, resp) +} + +func TestClient_UpdateRecords(t *testing.T) { + client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusOK, "rrsets-response.json") + + rrSet := []UpdateRRSet{{ + Name: "acme.example.org.", + ChangeType: "add", + Type: "TXT", + Records: []Record{{Content: `"my-acme-challenge"`}}, + }} + + resp, err := client.UpdateRecords(context.Background(), "example.org", rrSet) + require.NoError(t, err) + + expected := &APIResponse{Status: "ok", Message: "RRsets updated"} + + assert.Equal(t, expected, resp) +} diff --git a/providers/rcodezero/internal/fixtures/error.json b/providers/rcodezero/internal/fixtures/error.json new file mode 100644 index 0000000..6257f75 --- /dev/null +++ b/providers/rcodezero/internal/fixtures/error.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "message": "A human readable error message" +} diff --git a/providers/rcodezero/internal/fixtures/rrsets-response.json b/providers/rcodezero/internal/fixtures/rrsets-response.json new file mode 100644 index 0000000..83bdfa1 --- /dev/null +++ b/providers/rcodezero/internal/fixtures/rrsets-response.json @@ -0,0 +1,4 @@ +{ + "status": "ok", + "message": "RRsets updated" +} diff --git a/providers/rcodezero/internal/types.go b/providers/rcodezero/internal/types.go new file mode 100644 index 0000000..6fdffb8 --- /dev/null +++ b/providers/rcodezero/internal/types.go @@ -0,0 +1,25 @@ +package internal + +import "fmt" + +type UpdateRRSet struct { + Name string `json:"name"` + Type string `json:"type"` + ChangeType string `json:"changetype"` + Records []Record `json:"records"` + TTL int `json:"ttl"` +} + +type Record struct { + Content string `json:"content"` + Disabled bool `json:"disabled"` +} + +type APIResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +func (a APIResponse) Error() string { + return fmt.Sprintf("%s: %s", a.Status, a.Message) +} diff --git a/providers/rcodezero/rcodezero.go b/providers/rcodezero/rcodezero.go new file mode 100644 index 0000000..46b3aae --- /dev/null +++ b/providers/rcodezero/rcodezero.go @@ -0,0 +1,145 @@ +// Package rcodezero implements a DNS provider for solving the DNS-01 challenge using RcodeZero Anycast network. +package rcodezero + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/rcodezero/internal" +) + +// Environment variables names. +const ( + envNamespace = "RCODEZERO_" + + EnvAPIToken = envNamespace + "API_TOKEN" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIToken string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 240*time.Second), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for RcodeZero. +// Credentials must be passed in the environment variable: +// RCODEZERO_API_URL and RCODEZERO_API_TOKEN. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIToken) + if err != nil { + return nil, fmt.Errorf("rcodezero: %w", err) + } + + config := NewDefaultConfig() + config.APIToken = values[EnvAPIToken] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for RcodeZero. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("rcodezero: the configuration of the DNS provider is nil") + } + + if config.APIToken == "" { + return nil, errors.New("rcodezero: API token missing") + } + + client := internal.NewClient(config.APIToken) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{config: config, client: client}, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("rcodezero: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + rrSet := []internal.UpdateRRSet{{ + Name: info.EffectiveFQDN, + ChangeType: "update", + Type: "TXT", + TTL: d.config.TTL, + Records: []internal.Record{{Content: `"` + info.Value + `"`}}, + }} + + _, err = d.client.UpdateRecords(ctx, authZone, rrSet) + if err != nil { + return fmt.Errorf("rcodezero: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + ctx := context.Background() + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("rcodezero: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + rrSet := []internal.UpdateRRSet{{ + Name: info.EffectiveFQDN, + Type: "TXT", + ChangeType: "delete", + }} + + _, err = d.client.UpdateRecords(ctx, authZone, rrSet) + if err != nil { + return fmt.Errorf("rcodezero: %w", err) + } + + return nil +} diff --git a/providers/rcodezero/rcodezero.toml b/providers/rcodezero/rcodezero.toml new file mode 100644 index 0000000..a012736 --- /dev/null +++ b/providers/rcodezero/rcodezero.toml @@ -0,0 +1,33 @@ +Name = "RcodeZero" +Description = '''''' +URL = "https://www.rcodezero.at/" +Code = "rcodezero" +Since = "v4.13" + +Example = ''' +RCODEZERO_API_TOKEN= \ +lego --email you@example.com --dns rcodezero --domains my.example.org run +''' + +Additional = ''' +## Description + +Generate your API Token via https://my.rcodezero.at with the `ACME` permissions. +These are special tokens with limited access for ACME requests only. + +RcodeZero is an Anycast Network so the distribution of the DNS01-Challenge can take up to 2 minutes. + +''' + +[Configuration] + [Configuration.Credentials] + RCODEZERO_API_TOKEN = "API token" + [Configuration.Additional] + RCODEZERO_POLLING_INTERVAL = "Time between DNS propagation check" + RCODEZERO_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + RCODEZERO_TTL = "The TTL of the TXT record used for the DNS challenge" + RCODEZERO_HTTP_TIMEOUT = "API request timeout" + +[Links] + # Note: the API endpoint used inside the client is not documented. + API = "https://my.rcodezero.at/openapi" diff --git a/providers/rcodezero/rcodezero_test.go b/providers/rcodezero/rcodezero_test.go new file mode 100644 index 0000000..1f09460 --- /dev/null +++ b/providers/rcodezero/rcodezero_test.go @@ -0,0 +1,105 @@ +package rcodezero + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest( + EnvAPIToken). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIToken: "123", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAPIToken: "", + }, + expected: "rcodezero: some credentials information are missing: RCODEZERO_API_TOKEN", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiToken string + expected string + }{ + { + desc: "success", + apiToken: "123", + }, + { + desc: "missing credentials", + expected: "rcodezero: API token missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIToken = test.apiToken + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresentAndCleanup(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/rfc2136/rfc2136.go b/providers/rfc2136/rfc2136.go index 656e944..b7615fa 100644 --- a/providers/rfc2136/rfc2136.go +++ b/providers/rfc2136/rfc2136.go @@ -176,7 +176,6 @@ func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // Setup client c := &dns.Client{Timeout: d.config.DNSTimeout} - c.SingleInflight = true // TSIG authentication / msg signing if d.config.TSIGKey != "" && d.config.TSIGSecret != "" { diff --git a/providers/rfc2136/rfc2136_test.go b/providers/rfc2136/rfc2136_test.go index 18c0cf9..44649ae 100644 --- a/providers/rfc2136/rfc2136_test.go +++ b/providers/rfc2136/rfc2136_test.go @@ -85,7 +85,7 @@ func TestServerError(t *testing.T) { err = provider.Present(fakeDomain, "", fakeKeyAuth) require.Error(t, err) if !strings.Contains(err.Error(), "NOTZONE") { - t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string but it did not: %v", err) + t.Errorf("Expected Present() to return an error with the 'NOTZONE' rcode string, but it did not: %v", err) } } diff --git a/providers/route53/route53.go b/providers/route53/route53.go index 75c900f..a4e1262 100644 --- a/providers/route53/route53.go +++ b/providers/route53/route53.go @@ -2,19 +2,21 @@ package route53 import ( + "context" "errors" "fmt" "math/rand" "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/client" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/route53" + awstypes "github.com/aws/aws-sdk-go-v2/service/route53/types" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/entrustcorporation/dv/dns01" "github.com/go-acme/lego/v4/platform/config/env" "github.com/go-acme/lego/v4/platform/wait" @@ -30,6 +32,7 @@ const ( EnvHostedZoneID = envNamespace + "HOSTED_ZONE_ID" EnvMaxRetries = envNamespace + "MAX_RETRIES" EnvAssumeRoleArn = envNamespace + "ASSUME_ROLE_ARN" + EnvExternalID = envNamespace + "EXTERNAL_ID" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -48,12 +51,13 @@ type Config struct { HostedZoneID string MaxRetries int AssumeRoleArn string + ExternalID string TTL int PropagationTimeout time.Duration PollingInterval time.Duration - Client *route53.Route53 + Client *route53.Client } // NewDefaultConfig returns a default configuration for the DNSProvider. @@ -62,6 +66,7 @@ func NewDefaultConfig() *Config { HostedZoneID: env.GetOrFile(EnvHostedZoneID), MaxRetries: env.GetOrDefaultInt(EnvMaxRetries, 5), AssumeRoleArn: env.GetOrDefaultString(EnvAssumeRoleArn, ""), + ExternalID: env.GetOrDefaultString(EnvExternalID, ""), TTL: env.GetOrDefaultInt(EnvTTL, 10), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), @@ -71,31 +76,10 @@ func NewDefaultConfig() *Config { // DNSProvider implements the challenge.Provider interface. type DNSProvider struct { - client *route53.Route53 + client *route53.Client config *Config } -// customRetryer implements the client.Retryer interface by composing the DefaultRetryer. -// It controls the logic for retrying recoverable request errors (e.g. when rate limits are exceeded). -type customRetryer struct { - client.DefaultRetryer -} - -// RetryRules overwrites the DefaultRetryer's method. -// It uses a basic exponential backoff algorithm: -// that returns an initial delay of ~400ms with an upper limit of ~30 seconds, -// which should prevent causing a high number of consecutive throttling errors. -// For reference: Route 53 enforces an account-wide(!) 5req/s query limit. -func (d customRetryer) RetryRules(r *request.Request) time.Duration { - retryCount := r.RetryCount - if retryCount > 7 { - retryCount = 7 - } - - delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) - return time.Duration(delay) * time.Millisecond -} - // NewDNSProvider returns a DNSProvider instance configured for the AWS Route 53 service. // // AWS Credentials are automatically detected in the following locations and prioritized in the following order: @@ -111,7 +95,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(NewDefaultConfig()) } -// NewDNSProviderConfig takes a given config ans returns a custom configured DNSProvider instance. +// NewDNSProviderConfig takes a given config and returns a custom configured DNSProvider instance. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("route53: the configuration of the Route53 DNS provider is nil") @@ -121,13 +105,15 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{client: config.Client, config: config}, nil } - sess, err := createSession(config) + ctx := context.Background() + + cfg, err := createAWSConfig(ctx, config) if err != nil { return nil, err } return &DNSProvider{ - client: route53.New(sess), + client: route53.NewFromConfig(cfg), config: config, }, nil } @@ -139,14 +125,15 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { + ctx := context.Background() info := dns01.GetChallengeInfo(domain, keyAuth) - hostedZoneID, err := d.getHostedZoneID(info.EffectiveFQDN) + hostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("route53: failed to determine hosted zone ID: %w", err) } - records, err := d.getExistingRecordSets(hostedZoneID, info.EffectiveFQDN) + records, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN) if err != nil { return fmt.Errorf("route53: %w", err) } @@ -155,74 +142,93 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { var found bool for _, record := range records { - if aws.StringValue(record.Value) == realValue { + if deref(record.Value) == realValue { found = true } } if !found { - records = append(records, &route53.ResourceRecord{Value: aws.String(realValue)}) + records = append(records, awstypes.ResourceRecord{Value: aws.String(realValue)}) } - recordSet := &route53.ResourceRecordSet{ + recordSet := &awstypes.ResourceRecordSet{ Name: aws.String(info.EffectiveFQDN), - Type: aws.String("TXT"), + Type: "TXT", TTL: aws.Int64(int64(d.config.TTL)), ResourceRecords: records, } - err = d.changeRecord(route53.ChangeActionUpsert, hostedZoneID, recordSet) + err = d.changeRecord(ctx, awstypes.ChangeActionUpsert, hostedZoneID, recordSet) if err != nil { return fmt.Errorf("route53: %w", err) } + return nil } // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() info := dns01.GetChallengeInfo(domain, keyAuth) - hostedZoneID, err := d.getHostedZoneID(info.EffectiveFQDN) + hostedZoneID, err := d.getHostedZoneID(ctx, info.EffectiveFQDN) if err != nil { return fmt.Errorf("failed to determine Route 53 hosted zone ID: %w", err) } - records, err := d.getExistingRecordSets(hostedZoneID, info.EffectiveFQDN) + existingRecords, err := d.getExistingRecordSets(ctx, hostedZoneID, info.EffectiveFQDN) if err != nil { return fmt.Errorf("route53: %w", err) } - if len(records) == 0 { + if len(existingRecords) == 0 { return nil } - recordSet := &route53.ResourceRecordSet{ + var nonLegoRecords []awstypes.ResourceRecord + for _, record := range existingRecords { + if deref(record.Value) != `"`+info.Value+`"` { + nonLegoRecords = append(nonLegoRecords, record) + } + } + + action := awstypes.ChangeActionUpsert + + recordSet := &awstypes.ResourceRecordSet{ Name: aws.String(info.EffectiveFQDN), - Type: aws.String("TXT"), + Type: "TXT", TTL: aws.Int64(int64(d.config.TTL)), - ResourceRecords: records, + ResourceRecords: nonLegoRecords, + } + + // If the records are only records created by lego. + if len(nonLegoRecords) == 0 { + action = awstypes.ChangeActionDelete + + recordSet.ResourceRecords = existingRecords } - err = d.changeRecord(route53.ChangeActionDelete, hostedZoneID, recordSet) + err = d.changeRecord(ctx, action, hostedZoneID, recordSet) if err != nil { return fmt.Errorf("route53: %w", err) } + return nil } -func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route53.ResourceRecordSet) error { +func (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAction, hostedZoneID string, recordSet *awstypes.ResourceRecordSet) error { recordSetInput := &route53.ChangeResourceRecordSetsInput{ HostedZoneId: aws.String(hostedZoneID), - ChangeBatch: &route53.ChangeBatch{ + ChangeBatch: &awstypes.ChangeBatch{ Comment: aws.String("Managed by Lego"), - Changes: []*route53.Change{{ - Action: aws.String(action), + Changes: []awstypes.Change{{ + Action: action, ResourceRecordSet: recordSet, }}, }, } - resp, err := d.client.ChangeResourceRecordSets(recordSetInput) + resp, err := d.client.ChangeResourceRecordSets(ctx, recordSetInput) if err != nil { return fmt.Errorf("failed to change record set: %w", err) } @@ -232,26 +238,26 @@ func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { reqParams := &route53.GetChangeInput{Id: changeID} - resp, err := d.client.GetChange(reqParams) + resp, err := d.client.GetChange(ctx, reqParams) if err != nil { return false, fmt.Errorf("failed to query change status: %w", err) } - if aws.StringValue(resp.ChangeInfo.Status) == route53.ChangeStatusInsync { + if resp.ChangeInfo.Status == awstypes.ChangeStatusInsync { return true, nil } - return false, fmt.Errorf("unable to retrieve change: ID=%s", aws.StringValue(changeID)) + return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID)) }) } -func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route53.ResourceRecord, error) { +func (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, fqdn string) ([]awstypes.ResourceRecord, error) { listInput := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(hostedZoneID), StartRecordName: aws.String(fqdn), - StartRecordType: aws.String("TXT"), + StartRecordType: "TXT", } - recordSetsOutput, err := d.client.ListResourceRecordSets(listInput) + recordSetsOutput, err := d.client.ListResourceRecordSets(ctx, listInput) if err != nil { return nil, err } @@ -260,10 +266,10 @@ func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route return nil, nil } - var records []*route53.ResourceRecord + var records []awstypes.ResourceRecord for _, recordSet := range recordSetsOutput.ResourceRecordSets { - if aws.StringValue(recordSet.Name) == fqdn { + if deref(recordSet.Name) == fqdn { records = append(records, recordSet.ResourceRecords...) } } @@ -271,7 +277,7 @@ func (d *DNSProvider) getExistingRecordSets(hostedZoneID, fqdn string) ([]*route return records, nil } -func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { +func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { if d.config.HostedZoneID != "" { return d.config.HostedZoneID, nil } @@ -285,7 +291,7 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { reqParams := &route53.ListHostedZonesByNameInput{ DNSName: aws.String(dns01.UnFqdn(authZone)), } - resp, err := d.client.ListHostedZonesByName(reqParams) + resp, err := d.client.ListHostedZonesByName(ctx, reqParams) if err != nil { return "", err } @@ -293,8 +299,8 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { var hostedZoneID string for _, hostedZone := range resp.HostedZones { // .Name has a trailing dot - if !aws.BoolValue(hostedZone.Config.PrivateZone) && aws.StringValue(hostedZone.Name) == authZone { - hostedZoneID = aws.StringValue(hostedZone.Id) + if !hostedZone.Config.PrivateZone && deref(hostedZone.Name) == authZone { + hostedZoneID = deref(hostedZone.Id) break } } @@ -308,41 +314,60 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { return hostedZoneID, nil } -func createSession(config *Config) (*session.Session, error) { - if err := createSessionCheckParams(config); err != nil { - return nil, err +func createAWSConfig(ctx context.Context, config *Config) (aws.Config, error) { + if err := createAWSConfigCheckParams(config); err != nil { + return aws.Config{}, err } - retry := customRetryer{} - retry.NumMaxRetries = config.MaxRetries + optFns := []func(options *awsconfig.LoadOptions) error{ + awsconfig.WithRetryer(func() aws.Retryer { + return retry.NewStandard(func(options *retry.StandardOptions) { + options.MaxAttempts = config.MaxRetries + + // It uses a basic exponential backoff algorithm that returns an initial + // delay of ~400ms with an upper limit of ~30 seconds which should prevent + // causing a high number of consecutive throttling errors. + // For reference: Route 53 enforces an account-wide(!) 5req/s query limit. + options.Backoff = retry.BackoffDelayerFunc(func(attempt int, err error) (time.Duration, error) { + retryCount := attempt + if retryCount > 7 { + retryCount = 7 + } + + delay := (1 << uint(retryCount)) * (rand.Intn(50) + 200) + return time.Duration(delay) * time.Millisecond, nil + }) + }) + }), + } - awsConfig := aws.NewConfig() if config.AccessKeyID != "" && config.SecretAccessKey != "" { - awsConfig = awsConfig.WithCredentials(credentials.NewStaticCredentials(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)) + optFns = append(optFns, + awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)), + ) } if config.Region != "" { - awsConfig = awsConfig.WithRegion(config.Region) + optFns = append(optFns, awsconfig.WithRegion(config.Region)) } - sessionCfg := request.WithRetryer(awsConfig, retry) - - sess, err := session.NewSessionWithOptions(session.Options{Config: *sessionCfg}) + cfg, err := awsconfig.LoadDefaultConfig(ctx, optFns...) if err != nil { - return nil, err + return aws.Config{}, err } - if config.AssumeRoleArn == "" { - return sess, nil + if config.AssumeRoleArn != "" { + cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), config.AssumeRoleArn, func(options *stscreds.AssumeRoleOptions) { + if config.ExternalID != "" { + options.ExternalID = &config.ExternalID + } + }) } - return session.NewSession(&aws.Config{ - Region: sess.Config.Region, - Credentials: stscreds.NewCredentials(sess, config.AssumeRoleArn), - }) + return cfg, nil } -func createSessionCheckParams(config *Config) error { +func createAWSConfigCheckParams(config *Config) error { if config == nil { return errors.New("config is nil") } @@ -357,3 +382,12 @@ func createSessionCheckParams(config *Config) error { return nil } + +func deref[T string | int | int32 | int64 | bool](v *T) T { + if v == nil { + var zero T + return zero + } + + return *v +} diff --git a/providers/route53/route53.toml b/providers/route53/route53.toml index 3b5f2a5..d253b54 100644 --- a/providers/route53/route53.toml +++ b/providers/route53/route53.toml @@ -70,7 +70,7 @@ so it is recommended to narrow them down as much as possible if you are using th ### Least privilege policy for production purposes -The following AWS IAM policy document describes least privilege permissions required for lego to complete the DNS challenge. +The following AWS IAM policy document describes the least privilege permissions required for lego to complete the DNS challenge. Write access is limited to a specified hosted zone's DNS TXT records with a key of `_acme-challenge.example.com`. Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with your domain name to use this policy. @@ -129,7 +129,8 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with AWS_HOSTED_ZONE_ID = "Override the hosted zone ID." AWS_PROFILE = "Managed by the AWS client (`AWS_PROFILE_FILE` is not supported)" AWS_SDK_LOAD_CONFIG = "Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported)" - AWS_ASSUME_ROLE_ARN = "Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN` is not supported)" + AWS_ASSUME_ROLE_ARN = "Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported)" + AWS_EXTERNAL_ID = "Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)" [Configuration.Additional] AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file." AWS_MAX_RETRIES = "The number of maximum returns the service will use to make an individual API request" @@ -139,4 +140,4 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with [Links] API = "https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html" - GoClient = "https://github.com/aws/aws-sdk-go/aws" + GoClient = "https://github.com/aws/aws-sdk-go-v2" diff --git a/providers/route53/route53_integration_test.go b/providers/route53/route53_integration_test.go index acc301b..2fbcf52 100644 --- a/providers/route53/route53_integration_test.go +++ b/providers/route53/route53_integration_test.go @@ -1,11 +1,12 @@ package route53 import ( + "context" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/stretchr/testify/require" ) @@ -26,9 +27,13 @@ func TestLiveTTL(t *testing.T) { // we need a separate R53 client here as the one in the DNS provider is unexported. fqdn := "_acme-challenge." + domain + "." - sess, err := session.NewSession() + + ctx := context.Background() + + cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) - svc := route53.New(sess) + + svc := route53.NewFromConfig(cfg) defer func() { errC := provider.CleanUp(domain, "foo", "bar") @@ -37,17 +42,17 @@ func TestLiveTTL(t *testing.T) { } }() - zoneID, err := provider.getHostedZoneID(fqdn) + zoneID, err := provider.getHostedZoneID(context.Background(), fqdn) require.NoError(t, err) params := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zoneID), } - resp, err := svc.ListResourceRecordSets(params) + resp, err := svc.ListResourceRecordSets(ctx, params) require.NoError(t, err) for _, v := range resp.ResourceRecordSets { - if aws.StringValue(v.Name) == fqdn && aws.StringValue(v.Type) == "TXT" && aws.Int64Value(v.TTL) == 10 { + if deref(v.Name) == fqdn && v.Type == "TXT" && deref(v.TTL) == 10 { return } } diff --git a/providers/route53/route53_test.go b/providers/route53/route53_test.go index f1dd1a2..1c8e5f5 100644 --- a/providers/route53/route53_test.go +++ b/providers/route53/route53_test.go @@ -1,14 +1,15 @@ package route53 import ( + "context" "os" "testing" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/route53" + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/go-acme/lego/v4/platform/tester" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,21 +29,26 @@ var envTest = tester.NewEnvTest( WithDomain(envDomain). WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain) +type endpointResolverMock struct { + endpoint string +} + +func (e endpointResolverMock) ResolveEndpoint(_, _ string, _ ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{URL: e.endpoint}, nil +} + func makeTestProvider(t *testing.T, serverURL string) *DNSProvider { t.Helper() - config := &aws.Config{ - Credentials: credentials.NewStaticCredentials("abc", "123", " "), - Endpoint: aws.String(serverURL), - Region: aws.String("mock-region"), - MaxRetries: aws.Int(1), + cfg := aws.Config{ + Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "), + Region: "mock-region", + EndpointResolverWithOptions: endpointResolverMock{endpoint: serverURL}, + RetryMaxAttempts: 1, } - sess, err := session.NewSession(config) - require.NoError(t, err) - return &DNSProvider{ - client: route53.New(sess), + client: route53.NewFromConfig(cfg), config: NewDefaultConfig(), } } @@ -55,22 +61,21 @@ func Test_loadCredentials_FromEnv(t *testing.T) { _ = os.Setenv(EnvSecretAccessKey, "456") _ = os.Setenv(EnvRegion, "us-east-1") - config := &aws.Config{ - CredentialsChainVerboseErrors: aws.Bool(true), - } + ctx := context.Background() - sess, err := session.NewSession(config) + cfg, err := awsconfig.LoadDefaultConfig(ctx) require.NoError(t, err) - value, err := sess.Config.Credentials.Get() + value, err := cfg.Credentials.Retrieve(ctx) require.NoError(t, err, "Expected credentials to be set from environment") - expected := credentials.Value{ + expected := aws.Credentials{ AccessKeyID: "123", SecretAccessKey: "456", SessionToken: "", - ProviderName: "EnvConfigCredentials", + Source: "EnvConfigCredentials", } + assert.Equal(t, expected, value) } @@ -78,13 +83,12 @@ func Test_loadRegion_FromEnv(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() - os.Setenv(EnvRegion, route53.CloudWatchRegionUsEast1) + _ = os.Setenv(EnvRegion, "foo") - sess, err := session.NewSession(aws.NewConfig()) + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) require.NoError(t, err) - region := aws.StringValue(sess.Config.Region) - assert.Equal(t, route53.CloudWatchRegionUsEast1, region, "Region") + assert.Equal(t, "foo", cfg.Region, "Region") } func Test_getHostedZoneID_FromEnv(t *testing.T) { @@ -93,12 +97,12 @@ func Test_getHostedZoneID_FromEnv(t *testing.T) { expectedZoneID := "zoneID" - os.Setenv(EnvHostedZoneID, expectedZoneID) + _ = os.Setenv(EnvHostedZoneID, expectedZoneID) provider, err := NewDNSProvider() require.NoError(t, err) - hostedZoneID, err := provider.getHostedZoneID("whatever") + hostedZoneID, err := provider.getHostedZoneID(context.Background(), "whatever") require.NoError(t, err, "HostedZoneID") assert.Equal(t, expectedZoneID, hostedZoneID) @@ -144,7 +148,7 @@ func TestNewDefaultConfig(t *testing.T) { t.Run(test.desc, func(t *testing.T) { envTest.ClearEnv() for key, value := range test.envVars { - os.Setenv(key, value) + _ = os.Setenv(key, value) } config := NewDefaultConfig() @@ -156,9 +160,9 @@ func TestNewDefaultConfig(t *testing.T) { func TestDNSProvider_Present(t *testing.T) { mockResponses := MockResponseMap{ - "/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse}, - "/2013-04-01/hostedzone/ABCDEFG/rrset/": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, - "/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse}, + "/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse}, + "/2013-04-01/hostedzone/ABCDEFG/rrset": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse}, + "/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse}, "/2013-04-01/hostedzone/ABCDEFG/rrset?name=_acme-challenge.example.com.&type=TXT": { StatusCode: 200, Body: "", @@ -178,12 +182,12 @@ func TestDNSProvider_Present(t *testing.T) { require.NoError(t, err, "Expected Present to return no error") } -func TestCreateSession(t *testing.T) { +func Test_createAWSConfig(t *testing.T) { testCases := []struct { desc string env map[string]string config *Config - wantCreds credentials.Value + wantCreds aws.Credentials wantDefaultChain bool wantRegion string wantErr string @@ -218,11 +222,11 @@ func TestCreateSession(t *testing.T) { AccessKeyID: "one", SecretAccessKey: "two", }, - wantCreds: credentials.Value{ + wantCreds: aws.Credentials{ AccessKeyID: "one", SecretAccessKey: "two", SessionToken: "", - ProviderName: credentials.StaticProviderName, + Source: credentials.StaticCredentialsName, }, }, { @@ -232,11 +236,11 @@ func TestCreateSession(t *testing.T) { SecretAccessKey: "two", SessionToken: "three", }, - wantCreds: credentials.Value{ + wantCreds: aws.Credentials{ AccessKeyID: "one", SecretAccessKey: "two", SessionToken: "three", - ProviderName: credentials.StaticProviderName, + Source: credentials.StaticCredentialsName, }, }, { @@ -268,24 +272,26 @@ func TestCreateSession(t *testing.T) { envTest.Apply(test.env) - sess, err := createSession(test.config) + ctx := context.Background() + + cfg, err := createAWSConfig(ctx, test.config) requireErr(t, err, test.wantErr) if err != nil { return } - gotCreds, err := sess.Config.Credentials.Get() + gotCreds, err := cfg.Credentials.Retrieve(ctx) if test.wantDefaultChain { - assert.NotEqual(t, credentials.StaticProviderName, gotCreds.ProviderName) + assert.NotEqual(t, credentials.StaticCredentialsName, gotCreds.Source) } else { require.NoError(t, err) assert.Equal(t, test.wantCreds, gotCreds) } if test.wantRegion != "" { - assert.Equal(t, test.wantRegion, aws.StringValue(sess.Config.Region)) + assert.Equal(t, test.wantRegion, cfg.Region) } }) } diff --git a/providers/safedns/internal/client.go b/providers/safedns/internal/client.go index 709071c..7d33952 100644 --- a/providers/safedns/internal/client.go +++ b/providers/safedns/internal/client.go @@ -50,7 +50,7 @@ func (c *Client) AddRecord(ctx context.Context, zone string, record Record) (*Ad respData := &AddRecordResponse{} err = c.do(req, respData) if err != nil { - return nil, fmt.Errorf("remove record: %w", err) + return nil, fmt.Errorf("add record: %w", err) } return respData, nil diff --git a/providers/servercow/internal/client.go b/providers/servercow/internal/client.go index 72f4757..14b42c5 100644 --- a/providers/servercow/internal/client.go +++ b/providers/servercow/internal/client.go @@ -149,9 +149,8 @@ func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, paylo req.Header.Set("Accept", "application/json") - if payload != nil { - req.Header.Set("Content-Type", "application/json") - } + // Content-Type should be added even if there is no request body. + req.Header.Set("Content-Type", "application/json") return req, nil } diff --git a/providers/sonic/sonic.toml b/providers/sonic/sonic.toml index 286dee0..c4ba74d 100644 --- a/providers/sonic/sonic.toml +++ b/providers/sonic/sonic.toml @@ -26,7 +26,7 @@ See https://public-api.sonic.net/dyndns/#requesting_an_api_key for additional de This `userid` and `apikey` combo allow modifications to any DNS entries connected to the managed domain (hostname). -Hostname should be the toplevel domain managed e.g `example.com` not `www.example.com`. +Hostname should be the toplevel domain managed e.g. `example.com` not `www.example.com`. ''' [Configuration] diff --git a/providers/transip/fakeclient_test.go b/providers/transip/fakeclient_test.go index 4713274..7696c22 100644 --- a/providers/transip/fakeclient_test.go +++ b/providers/transip/fakeclient_test.go @@ -24,6 +24,18 @@ type fakeClient struct { domainName string } +func (f *fakeClient) PutWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + +func (f *fakeClient) PostWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + +func (f *fakeClient) PatchWithResponse(_ rest.Request) (rest.Response, error) { + panic("not implemented") +} + func (f *fakeClient) Get(request rest.Request, dest interface{}) error { if f.getInfoLatency != 0 { time.Sleep(f.getInfoLatency) diff --git a/providers/transip/transip_test.go b/providers/transip/transip_test.go index 119aa63..a09cafe 100644 --- a/providers/transip/transip_test.go +++ b/providers/transip/transip_test.go @@ -81,7 +81,7 @@ func TestNewDNSProvider(t *testing.T) { } // The error message for a file not existing is different on Windows and Linux. - // Therefore we test if the error type is the same. + // Therefore, we test if the error type is the same. t.Run("could not open private key path", func(t *testing.T) { defer envTest.RestoreEnv() envTest.ClearEnv() @@ -144,7 +144,7 @@ func TestNewDNSProviderConfig(t *testing.T) { } // The error message for a file not existing is different on Windows and Linux. - // Therefore we test if the error type is the same. + // Therefore, we test if the error type is the same. t.Run("could not open private key path", func(t *testing.T) { config := NewDefaultConfig() config.AccountName = "johndoe" diff --git a/providers/vkcloud/vkcloud.toml b/providers/vkcloud/vkcloud.toml index 573daa0..20beeef 100644 --- a/providers/vkcloud/vkcloud.toml +++ b/providers/vkcloud/vkcloud.toml @@ -12,7 +12,7 @@ lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*. ''' Additional = ''' -## Credential inforamtion +## Credential information You can find all required and additional information on ["Project/Keys" page](https://mcs.mail.ru/app/en/project/keys) of your cloud. diff --git a/providers/websupport/websupport.go b/providers/websupport/websupport.go index e7622db..a3a15b0 100644 --- a/providers/websupport/websupport.go +++ b/providers/websupport/websupport.go @@ -133,12 +133,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } - err = internal.ParseError(resp) - if err != nil { - return fmt.Errorf("websupport: %w", err) - } - - return nil + return fmt.Errorf("websupport: %w", internal.ParseError(resp)) } // CleanUp removes the TXT record matching the specified parameters. @@ -172,12 +167,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - err = internal.ParseError(resp) - if err != nil { - return fmt.Errorf("websupport: %w", err) - } - - return nil + return fmt.Errorf("websupport: %w", internal.ParseError(resp)) } // Timeout returns the timeout and interval to use when checking for DNS propagation. diff --git a/providers/wedos/internal/token.go b/providers/wedos/internal/token.go index 7655092..bca3f10 100644 --- a/providers/wedos/internal/token.go +++ b/providers/wedos/internal/token.go @@ -38,7 +38,7 @@ func czechHour() int { func utcToCet(utc time.Time) time.Time { // https://en.wikipedia.org/wiki/Central_European_Time - // As of 2011, all member states of the European Union observe summer time (daylight saving time), + // As of 2011, all member states of the European Union observe Summer Time (daylight saving time), // from the last Sunday in March to the last Sunday in October. // States within the CET area switch to Central European Summer Time (CEST -- UTC+02:00) for the summer.[1] utcMonth := utc.Month() diff --git a/providers/yandex/yandex.go b/providers/yandex/yandex.go index e354b2e..32eaf44 100644 --- a/providers/yandex/yandex.go +++ b/providers/yandex/yandex.go @@ -1,4 +1,4 @@ -// Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex. +// Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex PDD. package yandex import ( diff --git a/providers/yandex360/internal/client.go b/providers/yandex360/internal/client.go new file mode 100644 index 0000000..b131971 --- /dev/null +++ b/providers/yandex360/internal/client.go @@ -0,0 +1,147 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/entrustcorporation/dv/providers/internal/errutils" +) + +const defaultBaseURL = "https://api360.yandex.net/" + +type Client struct { + oauthToken string + orgID int64 + + baseURL *url.URL + HTTPClient *http.Client +} + +func NewClient(oauthToken string, orgID int64) (*Client, error) { + if oauthToken == "" { + return nil, errors.New("OAuth token is required") + } + + if orgID == 0 { + return nil, errors.New("orgID is required") + } + + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + oauthToken: oauthToken, + orgID: orgID, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + }, nil +} + +// AddRecord Adds a DNS record. +// POST https://api30.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns +// https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Create.html +func (c Client) AddRecord(ctx context.Context, domain string, record Record) (*Record, error) { + endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns") + + req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record) + if err != nil { + return nil, err + } + + var newRecord Record + + err = c.do(req, &newRecord) + if err != nil { + return nil, err + } + + return &newRecord, nil +} + +// DeleteRecord Deletes a DNS record. +// DELETE https://api360.yandex.net/directory/v1/org/{orgId}/domains/{domain}/dns/{recordId} +// https://yandex.ru/dev/api360/doc/ref/DomainDNSService/DomainDNSService_Delete.html +func (c Client) DeleteRecord(ctx context.Context, domain string, recordID int64) error { + endpoint := c.baseURL.JoinPath("directory", "v1", "org", strconv.FormatInt(c.orgID, 10), "domains", domain, "dns", strconv.FormatInt(recordID, 10)) + + req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + return c.do(req, nil) +} + +func (c Client) do(req *http.Request, result any) error { + req.Header.Set("Authorization", "OAuth "+c.oauthToken) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode/100 != 2 { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + var apiErr APIError + err := json.Unmarshal(raw, &apiErr) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr) +} diff --git a/providers/yandex360/internal/client_test.go b/providers/yandex360/internal/client_test.go new file mode 100644 index 0000000..d0ddac0 --- /dev/null +++ b/providers/yandex360/internal/client_test.go @@ -0,0 +1,108 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, pattern, method string, status int, filename string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest) + return + } + + open, err := os.Open(filepath.Join("fixtures", filename)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client, err := NewClient("secret", 123456) + require.NoError(t, err) + + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func TestClient_AddRecord(t *testing.T) { + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodPost, http.StatusOK, "add-record.json") + + record := Record{ + Name: "_acme-challenge", + Text: "txtxtxt", + TTL: 60, + Type: "TXT", + } + + newRecord, err := client.AddRecord(context.Background(), "example.com", record) + require.NoError(t, err) + + expected := &Record{ + ID: 789465, + Name: "foo", + Text: "_acme-challenge", + TTL: 60, + Type: "txtxtxt", + } + + assert.Equal(t, expected, newRecord) +} + +func TestClient_AddRecord_error(t *testing.T) { + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodGet, http.StatusUnauthorized, "error.json") + + record := Record{ + Name: "_acme-challenge", + Text: "txtxtxt", + TTL: 60, + Type: "TXT", + } + + newRecord, err := client.AddRecord(context.Background(), "example.com", record) + require.Error(t, err) + + assert.Nil(t, newRecord) +} + +func TestClient_DeleteRecord(t *testing.T) { + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusOK, "delete-record.json") + + err := client.DeleteRecord(context.Background(), "example.com", 789456) + require.NoError(t, err) +} + +func TestClient_DeleteRecord_error(t *testing.T) { + client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusUnauthorized, "error.json") + + err := client.DeleteRecord(context.Background(), "example.com", 789456) + require.Error(t, err) +} diff --git a/providers/yandex360/internal/fixtures/add-record.json b/providers/yandex360/internal/fixtures/add-record.json new file mode 100644 index 0000000..3472d9a --- /dev/null +++ b/providers/yandex360/internal/fixtures/add-record.json @@ -0,0 +1,7 @@ +{ + "recordID": 789465, + "name": "foo", + "text": "_acme-challenge", + "ttl": 60, + "type": "txtxtxt" +} diff --git a/providers/yandex360/internal/fixtures/delete-record.json b/providers/yandex360/internal/fixtures/delete-record.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/providers/yandex360/internal/fixtures/delete-record.json @@ -0,0 +1 @@ +{} diff --git a/providers/yandex360/internal/fixtures/error.json b/providers/yandex360/internal/fixtures/error.json new file mode 100644 index 0000000..6d6801d --- /dev/null +++ b/providers/yandex360/internal/fixtures/error.json @@ -0,0 +1,9 @@ +{ + "code": 123, + "details": [ + { + "@type": "foo" + } + ], + "message": "bar" +} diff --git a/providers/yandex360/internal/types.go b/providers/yandex360/internal/types.go new file mode 100644 index 0000000..5e08e0a --- /dev/null +++ b/providers/yandex360/internal/types.go @@ -0,0 +1,39 @@ +package internal + +import "fmt" + +type Record struct { + ID int64 `json:"recordId,omitempty"` + Address string `json:"address,omitempty"` + Exchange string `json:"exchange,omitempty"` + Flag int64 `json:"flag,omitempty"` + Name string `json:"name,omitempty"` + Port int64 `json:"port,omitempty"` + Preference int64 `json:"preference,omitempty"` + Priority int64 `json:"priority,omitempty"` + Tag string `json:"tag,omitempty"` + Target string `json:"target,omitempty"` + Text string `json:"text,omitempty"` + TTL int `json:"ttl,omitempty"` + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` + Weight int64 `json:"weight,omitempty"` +} + +type APIError struct { + Code int32 `json:"code"` + Details []Detail `json:"details"` + Message string `json:"message"` +} + +func (a APIError) Error() string { + return fmt.Sprintf("%d: %s: %v", a.Code, a.Message, a.Details) +} + +type Detail struct { + Type string `json:"@type"` +} + +func (d Detail) String() string { + return d.Type +} diff --git a/providers/yandex360/yandex360.go b/providers/yandex360/yandex360.go new file mode 100644 index 0000000..4468368 --- /dev/null +++ b/providers/yandex360/yandex360.go @@ -0,0 +1,174 @@ +// Package yandex360 implements a DNS provider for solving the DNS-01 challenge using Yandex 360. +package yandex360 + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/entrustcorporation/dv/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/entrustcorporation/dv/providers/yandex360/internal" +) + +// Environment variables names. +const ( + envNamespace = "YANDEX360_" + + EnvOAuthToken = envNamespace + "OAUTH_TOKEN" + EnvOrgID = envNamespace + "ORG_ID" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + OAuthToken string + OrgID int64 + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, 21600), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + client *internal.Client + config *Config + + recordIDs map[string]int64 + recordIDsMu sync.Mutex +} + +// NewDNSProvider returns a DNSProvider instance configured for Yandex 360. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvOAuthToken, EnvOrgID) + if err != nil { + return nil, fmt.Errorf("yandex360: %w", err) + } + + config := NewDefaultConfig() + config.OAuthToken = values[EnvOAuthToken] + + orgID, err := strconv.ParseInt(values[EnvOrgID], 10, 64) + if err != nil { + return nil, fmt.Errorf("yandex360: %w", err) + } + + config.OrgID = orgID + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Yandex 360. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("yandex360: the configuration of the DNS provider is nil") + } + + client, err := internal.NewClient(config.OAuthToken, config.OrgID) + if err != nil { + return nil, fmt.Errorf("yandex360: %w", err) + } + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + client: client, + config: config, + recordIDs: make(map[string]int64), + }, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("yandex360: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("yandex360: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + record := internal.Record{ + Name: subDomain, + TTL: d.config.TTL, + Text: info.Value, + Type: "TXT", + } + + newRecord, err := d.client.AddRecord(context.Background(), authZone, record) + if err != nil { + return fmt.Errorf("yandex360: add DNS record: %w", err) + } + + d.recordIDsMu.Lock() + d.recordIDs[token] = newRecord.ID + d.recordIDsMu.Unlock() + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(info.EffectiveFQDN)) + if err != nil { + return fmt.Errorf("yandex360: could not find zone for domain %q (%s): %w", domain, info.EffectiveFQDN, err) + } + + authZone = dns01.UnFqdn(authZone) + + d.recordIDsMu.Lock() + recordID, ok := d.recordIDs[token] + d.recordIDsMu.Unlock() + + if !ok { + return fmt.Errorf("yandex360: unknown recordID for %q", info.EffectiveFQDN) + } + + err = d.client.DeleteRecord(context.Background(), authZone, recordID) + if err != nil { + return fmt.Errorf("yandex360: delete DNS record: %w", err) + } + + d.recordIDsMu.Lock() + delete(d.recordIDs, token) + d.recordIDsMu.Unlock() + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/yandex360/yandex360.toml b/providers/yandex360/yandex360.toml new file mode 100644 index 0000000..ad0ce0d --- /dev/null +++ b/providers/yandex360/yandex360.toml @@ -0,0 +1,25 @@ +Name = "Yandex 360" +Description = ''' +''' +URL = "https://360.yandex.ru" +Code = "yandex360" +Since = "v4.14.0" + +Example = ''' +YANDEX360_OAUTH_TOKEN= \ +YANDEX360_ORG_ID= \ +lego --email you@example.com --dns yandex360 --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + YANDEX360_OAUTH_TOKEN = "The OAuth Token" + YANDEX360_ORG_ID = "The organization ID" + [Configuration.Additional] + YANDEX360_POLLING_INTERVAL = "Time between DNS propagation check" + YANDEX360_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + YANDEX360_HTTP_TIMEOUT = "API request timeout" + YANDEX360_TTL = "The TTL of the TXT record used for the DNS challenge" + +[Links] + API = "https://yandex.ru/dev/api360/doc/ref/DomainDNSService.html" diff --git a/providers/yandex360/yandex360_test.go b/providers/yandex360/yandex360_test.go new file mode 100644 index 0000000..545c909 --- /dev/null +++ b/providers/yandex360/yandex360_test.go @@ -0,0 +1,130 @@ +package yandex360 + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvOAuthToken, EnvOrgID).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvOAuthToken: "secret", + EnvOrgID: "123456", + }, + }, + { + desc: "missing org ID", + envVars: map[string]string{ + EnvOAuthToken: "secret", + }, + expected: "yandex360: some credentials information are missing: YANDEX360_ORG_ID", + }, + { + desc: "missing token", + envVars: map[string]string{ + EnvOrgID: "123456", + }, + expected: "yandex360: some credentials information are missing: YANDEX360_OAUTH_TOKEN", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + oauthToken string + orgID int64 + expected string + }{ + { + desc: "success", + oauthToken: "secret", + orgID: 123456, + }, + { + desc: "missing org ID", + oauthToken: "secret", + expected: "yandex360: orgID is required", + }, + { + desc: "missing token", + orgID: 123456, + expected: "yandex360: OAuth token is required", + }, + } + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.OAuthToken = test.oauthToken + config.OrgID = test.orgID + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.client) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/yandexcloud/yandexcloud.toml b/providers/yandexcloud/yandexcloud.toml index 93dbad4..97677b9 100644 --- a/providers/yandexcloud/yandexcloud.toml +++ b/providers/yandexcloud/yandexcloud.toml @@ -37,7 +37,7 @@ cat key.json | base64 [Configuration] [Configuration.Credentials] - YANDEX_CLOUD_IAM_TOKEN = "The base64 encoded json which contains inforamtion about iam token of serivce account with `dns.admin` permissions" + YANDEX_CLOUD_IAM_TOKEN = "The base64 encoded json which contains information about iam token of service account with `dns.admin` permissions" YANDEX_CLOUD_FOLDER_ID = "The string id of folder (aka project) in Yandex Cloud" [Configuration.Additional] YANDEX_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check"