diff --git a/README.md b/README.md index fca82732..b65e5650 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,14 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | -| [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | -| [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | -| [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | -| [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | -| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) | -| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | -| [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | -| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | -| [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | +| [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | +| [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | +| [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | +| [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | +| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | +| [reg.ru](https://go-acme.github.io/lego/dns/regru/) | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | +| [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | +| [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | +| [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index ae7ceb3d..58cf5f7b 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -60,6 +60,7 @@ func allDNSCodes() string { "lightsail", "linode", "liquidweb", + "loopia", "luadns", "mydnsjp", "mythicbeasts", @@ -1075,6 +1076,27 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/liquidweb`) + case "loopia": + // generated from: providers/dns/loopia/loopia.toml + ew.writeln(`Configuration for Loopia.`) + ew.writeln(`Code: 'loopia'`) + ew.writeln(`Since: 'v4.2.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "LOOPIA_API_PASSWORD": API password`) + ew.writeln(` - "LOOPIA_API_USER": API username`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "LOOPIA_HTTP_TIMEOUT": API request timeout`) + ew.writeln(` - "LOOPIA_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "LOOPIA_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "LOOPIA_TTL": The TTL of the TXT record used for the DNS challenge`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/loopia`) + case "luadns": // generated from: providers/dns/luadns/luadns.toml ew.writeln(`Configuration for LuaDNS.`) diff --git a/docs/content/dns/zz_gen_loopia.md b/docs/content/dns/zz_gen_loopia.md new file mode 100644 index 00000000..f47063dc --- /dev/null +++ b/docs/content/dns/zz_gen_loopia.md @@ -0,0 +1,74 @@ +--- +title: "Loopia" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: loopia +--- + + + + + +Since: v4.2.0 + +Configuration for [Loopia](https://loopia.com). + + + + +- Code: `loopia` + +Here is an example bash command using the Loopia provider: + +```bash +LOOPIA_API_USER=xxxxxxxx \ +LOOPIA_API_PASSWORD=yyyyyyyy \ +lego --email my@email.com --dns loopia --domains my.domain.com run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `LOOPIA_API_PASSWORD` | API password | +| `LOOPIA_API_USER` | API username | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `LOOPIA_HTTP_TIMEOUT` | API request timeout | +| `LOOPIA_POLLING_INTERVAL` | Time between DNS propagation check | +| `LOOPIA_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `LOOPIA_TTL` | The TTL of the TXT record used for the DNS challenge | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + +### API user + +You can [generate a new API user](https://customerzone.loopia.com/api/) from your account page. + +It needs to have the following permissions: + +* addZoneRecord +* getZoneRecords +* removeZoneRecord +* removeSubdomain + + + +## More information + +- [API documentation](https://www.loopia.com/api) + + + + diff --git a/go.sum b/go.sum index 370c4fde..e133af54 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,12 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0 h1:0E3eE8MX426vUOs7aHfI7aN1BrIzzzf4ccKCSfSjGmc= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0 h1:MZQCQQaRwOrAcuKjiHWHrgKykt4fZyuwF2dtiG3fGW8= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= @@ -74,7 +71,6 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= -github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -106,7 +102,6 @@ github.com/exoscale/egoscale v0.23.0 h1:hoUDzrO8yNoobNdnrRvlRFjfg3Ng0vQTrv6bXRJu github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= @@ -139,11 +134,8 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -151,7 +143,6 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= @@ -159,7 +150,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -172,7 +162,6 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d h1:r5dcOhiqucDq0XNmvsN+HMB7+o3TrjZlLdNYGFRtM3o= github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM= github.com/gophercloud/gophercloud v0.7.0 h1:vhmQQEM2SbnGCg2/3EzQnQZ3V7+UCGy9s8exQCprNYg= github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= @@ -200,11 +189,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df h1:MZf03xP9WdakyXhOWuAD5uPK3wHh96wCsqe3hCMKh8E= github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= @@ -249,7 +236,6 @@ github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -319,7 +305,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -327,11 +312,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -348,9 +330,7 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= @@ -365,7 +345,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= @@ -398,7 +377,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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= @@ -411,10 +389,8 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= 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= @@ -426,13 +402,11 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= @@ -441,7 +415,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ 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= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -458,14 +431,11 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -476,7 +446,6 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -486,9 +455,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -525,7 +492,6 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb h1:iKlO7ROJc6SttHKlxzwGytRtBUqX4VARrNTgP2YLX5M= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 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= @@ -534,23 +500,18 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0 h1:VGGbLNyPF7dvYHhcUGYBBGCRDDK0RRJAI6KCvo0CL+E= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0 h1:uMf5uLi4eQMRrMKhCplNik4U4H8Z6C1br3zOtAa/aDE= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0 h1:0q95w+VuFtv4PAx4PZVQdBMmYbaCHbnfKaEiDIcVyag= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -558,20 +519,17 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171 h1:xes2Q2k+d/+YNXVw0FpZkIDJiaux4OVrRKXRAzH6A0U= @@ -579,9 +537,7 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -590,7 +546,6 @@ google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -599,24 +554,20 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy 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.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho= gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ns1/ns1-go.v2 v2.4.2 h1:H6VnvLez0GjxXsXat6MUFmKuiMFuDaMBdGF9qtkmODo= gopkg.in/ns1/ns1-go.v2 v2.4.2/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index d6992785..4469a767 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -51,6 +51,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/lightsail" "github.com/go-acme/lego/v4/providers/dns/linode" "github.com/go-acme/lego/v4/providers/dns/liquidweb" + "github.com/go-acme/lego/v4/providers/dns/loopia" "github.com/go-acme/lego/v4/providers/dns/luadns" "github.com/go-acme/lego/v4/providers/dns/mydnsjp" "github.com/go-acme/lego/v4/providers/dns/mythicbeasts" @@ -182,6 +183,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return liquidweb.NewDNSProvider() case "luadns": return luadns.NewDNSProvider() + case "loopia": + return loopia.NewDNSProvider() case "manual": return dns01.NewDNSProviderManual() case "mydnsjp": diff --git a/providers/dns/loopia/internal/client.go b/providers/dns/loopia/internal/client.go new file mode 100644 index 00000000..3c9a92c0 --- /dev/null +++ b/providers/dns/loopia/internal/client.go @@ -0,0 +1,187 @@ +package internal + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + "time" +) + +// DefaultBaseURL is url to the XML-RPC api. +const DefaultBaseURL = "https://api.loopia.se/RPCSERV" + +// Client the Loopia client. +type Client struct { + APIUser string + APIPassword string + BaseURL string + HTTPClient *http.Client +} + +// NewClient creates a new Loopia Client. +func NewClient(apiUser, apiPassword string) *Client { + return &Client{ + APIUser: apiUser, + APIPassword: apiPassword, + BaseURL: DefaultBaseURL, + HTTPClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// AddTXTRecord adds a TXT record. +func (c *Client) AddTXTRecord(domain string, subdomain string, ttl int, value string) error { + call := &methodCall{ + MethodName: "addZoneRecord", + Params: []param{ + paramString{Value: c.APIUser}, + paramString{Value: c.APIPassword}, + paramString{Value: domain}, + paramString{Value: subdomain}, + paramStruct{ + StructMembers: []structMember{ + structMemberString{Name: "type", Value: "TXT"}, + structMemberInt{Name: "ttl", Value: ttl}, + structMemberInt{Name: "priority", Value: 0}, + structMemberString{Name: "rdata", Value: value}, + structMemberInt{Name: "record_id", Value: 0}, + }, + }, + }, + } + resp := &responseString{} + + err := c.rpcCall(call, resp) + if err != nil { + return err + } + + return checkResponse(resp.Value) +} + +// RemoveTXTRecord removes a TXT record. +func (c *Client) RemoveTXTRecord(domain string, subdomain string, recordID int) error { + call := &methodCall{ + MethodName: "removeZoneRecord", + Params: []param{ + paramString{Value: c.APIUser}, + paramString{Value: c.APIPassword}, + paramString{Value: domain}, + paramString{Value: subdomain}, + paramInt{Value: recordID}, + }, + } + resp := &responseString{} + + err := c.rpcCall(call, resp) + if err != nil { + return err + } + + return checkResponse(resp.Value) +} + +// GetTXTRecords gets TXT records. +func (c *Client) GetTXTRecords(domain string, subdomain string) ([]RecordObj, error) { + call := &methodCall{ + MethodName: "getZoneRecords", + Params: []param{ + paramString{Value: c.APIUser}, + paramString{Value: c.APIPassword}, + paramString{Value: domain}, + paramString{Value: subdomain}, + }, + } + resp := &recordObjectsResponse{} + + err := c.rpcCall(call, resp) + + return resp.Params, err +} + +// RemoveSubdomain remove a sub-domain. +func (c *Client) RemoveSubdomain(domain, subdomain string) error { + call := &methodCall{ + MethodName: "removeSubdomain", + Params: []param{ + paramString{Value: c.APIUser}, + paramString{Value: c.APIPassword}, + paramString{Value: domain}, + paramString{Value: subdomain}, + }, + } + resp := &responseString{} + + err := c.rpcCall(call, resp) + if err != nil { + return err + } + + return checkResponse(resp.Value) +} + +// rpcCall makes an XML-RPC call to Loopia's RPC endpoint +// by marshaling the data given in the call argument to XML and sending that via HTTP Post to Loopia. +// The response is then unmarshalled into the resp argument. +func (c *Client) rpcCall(call *methodCall, resp response) error { + body, err := xml.MarshalIndent(call, "", " ") + if err != nil { + return fmt.Errorf("error during unmarshalling the request body: %w", err) + } + + body = append([]byte(``+"\n"), body...) + + respBody, err := c.httpPost(c.BaseURL, "text/xml", bytes.NewReader(body)) + if err != nil { + return err + } + + err = xml.Unmarshal(respBody, resp) + if err != nil { + return fmt.Errorf("error during unmarshalling the response body: %w", err) + } + + if resp.faultCode() != 0 { + return rpcError{ + faultCode: resp.faultCode(), + faultString: strings.TrimSpace(resp.faultString()), + } + } + + return nil +} + +func (c *Client) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) { + resp, err := c.HTTPClient.Post(url, bodyType, body) + if err != nil { + return nil, fmt.Errorf("HTTP Post Error: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP Post Error: %d", resp.StatusCode) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("HTTP Post Error: %w", err) + } + + return b, nil +} + +func checkResponse(value string) error { + switch v := strings.TrimSpace(value); v { + case "OK": + return nil + case "AUTH_ERROR": + return errors.New("authentication error") + default: + return fmt.Errorf("unknown error: %q", v) + } +} diff --git a/providers/dns/loopia/internal/client_test.go b/providers/dns/loopia/internal/client_test.go new file mode 100644 index 00000000..9eb4b259 --- /dev/null +++ b/providers/dns/loopia/internal/client_test.go @@ -0,0 +1,339 @@ +package internal + +import ( + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_AddZoneRecord(t *testing.T) { + serverResponses := map[string]string{ + addZoneRecordGoodAuth: responseOk, + addZoneRecordBadAuth: responseAuthError, + addZoneRecordNonValidDomain: responseUnknownError, + addZoneRecordEmptyResponse: "", + } + + server := createFakeServer(t, serverResponses) + + testCases := []struct { + desc string + password string + domain string + err string + }{ + { + desc: "auth ok", + password: "goodpassword", + domain: exampleDomain, + }, + { + desc: "auth error", + password: "badpassword", + domain: exampleDomain, + err: "authentication error", + }, + { + desc: "unknown error", + password: "goodpassword", + domain: "badexample.com", + err: `unknown error: "UNKNOWN_ERROR"`, + }, + { + desc: "empty response", + password: "goodpassword", + domain: "empty.com", + err: "error during unmarshalling the response body: EOF", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client := NewClient("apiuser", test.password) + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + err := client.AddTXTRecord(test.domain, exampleSubDomain, 123, "TXTrecord") + if len(test.err) == 0 { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.err) + } + }) + } +} + +func TestClient_RemoveSubdomain(t *testing.T) { + serverResponses := map[string]string{ + removeSubdomainGoodAuth: responseOk, + removeSubdomainBadAuth: responseAuthError, + removeSubdomainNonValidDomain: responseUnknownError, + removeSubdomainEmptyResponse: "", + } + + server := createFakeServer(t, serverResponses) + + testCases := []struct { + desc string + password string + domain string + err string + }{ + { + desc: "auth ok", + password: "goodpassword", + domain: exampleDomain, + }, + { + desc: "auth error", + password: "badpassword", + domain: exampleDomain, + err: "authentication error", + }, + { + desc: "unknown error", + password: "goodpassword", + domain: "badexample.com", + err: `unknown error: "UNKNOWN_ERROR"`, + }, + { + desc: "empty response", + password: "goodpassword", + domain: "empty.com", + err: "error during unmarshalling the response body: EOF", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client := NewClient("apiuser", test.password) + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + err := client.RemoveSubdomain(test.domain, exampleSubDomain) + if len(test.err) == 0 { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.err) + } + }) + } +} + +func TestClient_RemoveZoneRecord(t *testing.T) { + serverResponses := map[string]string{ + removeRecordGoodAuth: responseOk, + removeRecordBadAuth: responseAuthError, + removeRecordNonValidDomain: responseUnknownError, + removeRecordEmptyResponse: "", + } + + server := createFakeServer(t, serverResponses) + + testCases := []struct { + desc string + password string + domain string + err string + }{ + { + desc: "auth ok", + password: "goodpassword", + domain: exampleDomain, + }, + { + desc: "auth error", + password: "badpassword", + domain: exampleDomain, + err: "authentication error", + }, + { + desc: "uknown error", + password: "goodpassword", + domain: "badexample.com", + err: `unknown error: "UNKNOWN_ERROR"`, + }, + { + desc: "empty response", + password: "goodpassword", + domain: "empty.com", + err: "error during unmarshalling the response body: EOF", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + client := NewClient("apiuser", test.password) + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + err := client.RemoveTXTRecord(test.domain, exampleSubDomain, 12345678) + if len(test.err) == 0 { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.err) + } + }) + } +} + +func TestClient_GetZoneRecord(t *testing.T) { + serverResponses := map[string]string{ + getZoneRecords: getZoneRecordsResponse, + } + + server := createFakeServer(t, serverResponses) + + client := NewClient("apiuser", "goodpassword") + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + recordObjs, err := client.GetTXTRecords(exampleDomain, exampleSubDomain) + require.NoError(t, err) + + expected := []RecordObj{ + { + Type: "TXT", + TTL: 300, + Priority: 0, + Rdata: exampleRdata, + RecordID: 12345678, + }, + } + assert.EqualValues(t, expected, recordObjs) +} + +func TestClient_rpcCall_404(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNotFound) + + _, err = fmt.Fprint(w, "") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) + + call := &methodCall{ + MethodName: "dummyMethod", + Params: []param{ + paramString{Value: "test1"}, + }, + } + + client := NewClient("apiuser", "apipassword") + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + err := client.rpcCall(call, &responseString{}) + assert.EqualError(t, err, "HTTP Post Error: 404") +} + +func TestClient_rpcCall_RPCError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = fmt.Fprint(w, responseRPCError) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) + + call := &methodCall{ + MethodName: "getDomains", + Params: []param{ + paramString{Value: "test1"}, + }, + } + + client := NewClient("apiuser", "apipassword") + client.BaseURL = server.URL + "/" + client.HTTPClient = server.Client() + + err := client.rpcCall(call, &responseString{}) + assert.EqualError(t, err, "RPC Error: (201) Method signature error: 42") +} + +func TestUnmarshallFaultyRecordObject(t *testing.T) { + testCases := []struct { + desc string + xml string + }{ + { + desc: "faulty name", + xml: "name", + }, + { + desc: "faulty string", + xml: "foo", + }, + { + desc: "faulty int", + xml: "1", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + resp := &RecordObj{} + + err := xml.Unmarshal([]byte(test.xml), resp) + require.Error(t, err) + }) + } +} + +func createFakeServer(t *testing.T, serverResponses map[string]string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Content-Type") != "text/xml" { + http.Error(w, fmt.Sprintf("invalid content type: %s", r.Header.Get("Content-Type")), http.StatusBadRequest) + return + } + + req, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + resp, ok := serverResponses[string(req)] + if !ok { + http.Error(w, "no response for request", http.StatusBadRequest) + return + } + + _, err = fmt.Fprint(w, resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + + t.Cleanup(server.Close) + + return server +} diff --git a/providers/dns/loopia/internal/mock_test.go b/providers/dns/loopia/internal/mock_test.go new file mode 100644 index 00000000..13b9970b --- /dev/null +++ b/providers/dns/loopia/internal/mock_test.go @@ -0,0 +1,660 @@ +package internal + +const ( + exampleDomain = "example.com" + exampleSubDomain = "_acme-challenge" + exampleRdata = "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" +) + +// Testdata based on real traffic between an xml-rpc client and the api. +const responseOk = ` + + + + + + OK + + + + + ` + +const responseAuthError = ` + + + + + + AUTH_ERROR + + + + + ` + +const responseUnknownError = ` + + + + + + UNKNOWN_ERROR + + + + + ` + +const responseRPCError = ` + + + + + + + faultCode + + + + 201 + + + + + + faultString + + + + Method signature error: 42 + + + + + + +` + +const addZoneRecordGoodAuth = ` + + addZoneRecord + + + + apiuser + + + + + goodpassword + + + + + example.com + + + + + _acme-challenge + + + + + + + type + + TXT + + + + ttl + + 123 + + + + priority + + 0 + + + + rdata + + TXTrecord + + + + record_id + + 0 + + + + + + +` + +const addZoneRecordBadAuth = ` + + addZoneRecord + + + + apiuser + + + + + badpassword + + + + + example.com + + + + + _acme-challenge + + + + + + + type + + TXT + + + + ttl + + 123 + + + + priority + + 0 + + + + rdata + + TXTrecord + + + + record_id + + 0 + + + + + + +` + +const addZoneRecordNonValidDomain = ` + + addZoneRecord + + + + apiuser + + + + + goodpassword + + + + + badexample.com + + + + + _acme-challenge + + + + + + + type + + TXT + + + + ttl + + 123 + + + + priority + + 0 + + + + rdata + + TXTrecord + + + + record_id + + 0 + + + + + + +` + +const addZoneRecordEmptyResponse = ` + + addZoneRecord + + + + apiuser + + + + + goodpassword + + + + + empty.com + + + + + _acme-challenge + + + + + + + type + + TXT + + + + ttl + + 123 + + + + priority + + 0 + + + + rdata + + TXTrecord + + + + record_id + + 0 + + + + + + +` + +const getZoneRecords = ` + + getZoneRecords + + + + apiuser + + + + + goodpassword + + + + + example.com + + + + + _acme-challenge + + + +` + +const getZoneRecordsResponse = ` + + + + + + + + + + + rdata + + + + LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM + + + + + + record_id + + + + 12345678 + + + + + + priority + + + + 0 + + + + + + ttl + + + + 300 + + + + + + type + + + + TXT + + + + + + + + + + +` + +const removeRecordGoodAuth = ` + + removeZoneRecord + + + + apiuser + + + + + goodpassword + + + + + example.com + + + + + _acme-challenge + + + + + 12345678 + + + +` + +const removeRecordBadAuth = ` + + removeZoneRecord + + + + apiuser + + + + + badpassword + + + + + example.com + + + + + _acme-challenge + + + + + 12345678 + + + +` + +const removeRecordNonValidDomain = ` + + removeZoneRecord + + + + apiuser + + + + + goodpassword + + + + + badexample.com + + + + + _acme-challenge + + + + + 12345678 + + + +` + +const removeRecordEmptyResponse = ` + + removeZoneRecord + + + + apiuser + + + + + goodpassword + + + + + empty.com + + + + + _acme-challenge + + + + + 12345678 + + + +` + +const removeSubdomainGoodAuth = ` + + removeSubdomain + + + + apiuser + + + + + goodpassword + + + + + example.com + + + + + _acme-challenge + + + +` + +const removeSubdomainBadAuth = ` + + removeSubdomain + + + + apiuser + + + + + badpassword + + + + + example.com + + + + + _acme-challenge + + + +` + +const removeSubdomainNonValidDomain = ` + + removeSubdomain + + + + apiuser + + + + + goodpassword + + + + + badexample.com + + + + + _acme-challenge + + + +` + +const removeSubdomainEmptyResponse = ` + + removeSubdomain + + + + apiuser + + + + + goodpassword + + + + + empty.com + + + + + _acme-challenge + + + +` diff --git a/providers/dns/loopia/internal/types.go b/providers/dns/loopia/internal/types.go new file mode 100644 index 00000000..9d96da40 --- /dev/null +++ b/providers/dns/loopia/internal/types.go @@ -0,0 +1,173 @@ +package internal + +import ( + "encoding/xml" + "fmt" + "strings" +) + +// types for XML-RPC method calls and parameters + +type param interface { + param() +} + +type paramString struct { + XMLName xml.Name `xml:"param"` + Value string `xml:"value>string"` +} + +func (p paramString) param() {} + +type paramInt struct { + XMLName xml.Name `xml:"param"` + Value int `xml:"value>int"` +} + +func (p paramInt) param() {} + +type paramStruct struct { + XMLName xml.Name `xml:"param"` + StructMembers []structMember `xml:"value>struct>member"` +} + +func (p paramStruct) param() {} + +type structMember interface { + structMember() +} + +type structMemberString struct { + Name string `xml:"name"` + Value string `xml:"value>string"` +} + +func (m structMemberString) structMember() {} + +type structMemberInt struct { + Name string `xml:"name"` + Value int `xml:"value>int"` +} + +func (m structMemberInt) structMember() {} + +type methodCall struct { + XMLName xml.Name `xml:"methodCall"` + MethodName string `xml:"methodName"` + Params []param `xml:"params>param"` +} + +// types for XML-RPC responses + +type response interface { + faultCode() int + faultString() string +} + +type responseString struct { + responseFault + Value string `xml:"params>param>value>string"` +} + +type responseFault struct { + FaultCode int `xml:"fault>value>struct>member>value>int"` + FaultString string `xml:"fault>value>struct>member>value>string"` +} + +func (r responseFault) faultCode() int { return r.FaultCode } +func (r responseFault) faultString() string { return r.FaultString } + +type rpcError struct { + faultCode int + faultString string +} + +func (e rpcError) Error() string { + return fmt.Sprintf("RPC Error: (%d) %s", e.faultCode, e.faultString) +} + +type recordObjectsResponse struct { + responseFault + XMLName xml.Name `xml:"methodResponse"` + Params []RecordObj `xml:"params>param>value>array>data>value>struct"` +} + +type RecordObj struct { + Type string + TTL int + Priority int + Rdata string + RecordID int +} + +func (r *RecordObj) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var name string + for { + t, err := d.Token() + if err != nil { + return err + } + + switch tt := t.(type) { + case xml.StartElement: + switch tt.Name.Local { + case "name": // The name of the record object: + var s string + if err = d.DecodeElement(&s, &start); err != nil { + return err + } + + name = strings.TrimSpace(s) + + case "string": // A string value of the record object: + if err = r.decodeValueString(name, d, start); err != nil { + return err + } + + case "int": // An int value of the record object: + if err = r.decodeValueInt(name, d, start); err != nil { + return err + } + } + case xml.EndElement: + if tt == start.End() { + return nil + } + } + } +} + +func (r *RecordObj) decodeValueString(name string, d *xml.Decoder, start xml.StartElement) error { + var s string + if err := d.DecodeElement(&s, &start); err != nil { + return err + } + + s = strings.TrimSpace(s) + switch name { + case "type": + r.Type = s + case "rdata": + r.Rdata = s + } + + return nil +} + +func (r *RecordObj) decodeValueInt(name string, d *xml.Decoder, start xml.StartElement) error { + var i int + if err := d.DecodeElement(&i, &start); err != nil { + return err + } + + switch name { + case "record_id": + r.RecordID = i + case "ttl": + r.TTL = i + case "priority": + r.Priority = i + } + + return nil +} diff --git a/providers/dns/loopia/loopia.go b/providers/dns/loopia/loopia.go new file mode 100644 index 00000000..639a9872 --- /dev/null +++ b/providers/dns/loopia/loopia.go @@ -0,0 +1,192 @@ +// Package loopia implements a DNS provider for solving the DNS-01 challenge using loopia DNS. +package loopia + +import ( + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/loopia/internal" +) + +const minTTL = 300 + +// Environment variables names. +const ( + envNamespace = "LOOPIA_" + + EnvAPIUser = envNamespace + "API_USER" + EnvAPIPassword = envNamespace + "API_PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +type dnsClient interface { + AddTXTRecord(domain string, subdomain string, ttl int, value string) error + RemoveTXTRecord(domain string, subdomain string, recordID int) error + GetTXTRecords(domain string, subdomain string) ([]internal.RecordObj, error) + RemoveSubdomain(domain, subdomain string) error +} + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + BaseURL string + APIUser string + APIPassword 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{ + BaseURL: internal.DefaultBaseURL, + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 40*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 60*time.Second), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 60*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client dnsClient + + inProgressInfo map[string]int + inProgressMu sync.Mutex + + findZoneByFqdn func(fqdn string) (string, error) +} + +// NewDNSProvider returns a DNSProvider instance configured for Loopia. +// Credentials must be passed in the environment variables: +// LOOPIA_API_USER, LOOPIA_API_PASSWORD. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIUser, EnvAPIPassword) + if err != nil { + return nil, fmt.Errorf("loopia: %w", err) + } + + config := NewDefaultConfig() + config.APIUser = values[EnvAPIUser] + config.APIPassword = values[EnvAPIPassword] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Loopia. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("loopia: the configuration of the DNS provider is nil") + } + + if config.APIUser == "" || config.APIPassword == "" { + return nil, errors.New("loopia: credentials missing") + } + + // Min value for TTL is 300 + if config.TTL < 300 { + config.TTL = 300 + } + + client := internal.NewClient(config.APIUser, config.APIPassword) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + findZoneByFqdn: dns01.FindZoneByFqdn, + inProgressInfo: make(map[string]int), + }, 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 using the specified parameters. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + subdomain, authZone := d.splitDomain(fqdn) + + err := d.client.AddTXTRecord(authZone, subdomain, d.config.TTL, value) + if err != nil { + return fmt.Errorf("loopia: failed to add TXT record: %w", err) + } + + txtRecords, err := d.client.GetTXTRecords(authZone, subdomain) + if err != nil { + return fmt.Errorf("loopia: failed to get TXT records: %w", err) + } + + d.inProgressMu.Lock() + defer d.inProgressMu.Unlock() + + for _, r := range txtRecords { + if r.Rdata == value { + d.inProgressInfo[token] = r.RecordID + return nil + } + } + + return errors.New("loopia: failed to find the stored TXT record") +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + + subdomain, authZone := d.splitDomain(fqdn) + + d.inProgressMu.Lock() + defer d.inProgressMu.Unlock() + + err := d.client.RemoveTXTRecord(authZone, subdomain, d.inProgressInfo[token]) + if err != nil { + return fmt.Errorf("loopia: failed to remove TXT record: %w", err) + } + + records, err := d.client.GetTXTRecords(authZone, subdomain) + if err != nil { + return fmt.Errorf("loopia: failed to get TXT records: %w", err) + } + + if len(records) > 0 { + return nil + } + + err = d.client.RemoveSubdomain(authZone, subdomain) + if err != nil { + return fmt.Errorf("loopia: failed to remove sub-domain: %w", err) + } + + return nil +} + +func (d *DNSProvider) splitDomain(fqdn string) (string, string) { + authZone, _ := d.findZoneByFqdn(fqdn) + authZone = dns01.UnFqdn(authZone) + + subdomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+authZone) + + return subdomain, authZone +} diff --git a/providers/dns/loopia/loopia.toml b/providers/dns/loopia/loopia.toml new file mode 100644 index 00000000..471b815a --- /dev/null +++ b/providers/dns/loopia/loopia.toml @@ -0,0 +1,37 @@ +Name = "Loopia" +Description = '''''' +URL = "https://loopia.com" +Code = "loopia" +Since = "v4.2.0" + +Example = ''' +LOOPIA_API_USER=xxxxxxxx \ +LOOPIA_API_PASSWORD=yyyyyyyy \ +lego --email my@email.com --dns loopia --domains my.domain.com run +''' + +Additional = ''' +### API user + +You can [generate a new API user](https://customerzone.loopia.com/api/) from your account page. + +It needs to have the following permissions: + +* addZoneRecord +* getZoneRecords +* removeZoneRecord +* removeSubdomain +''' + +[Configuration] + [Configuration.Credentials] + LOOPIA_API_USER = "API username" + LOOPIA_API_PASSWORD = "API password" + [Configuration.Additional] + LOOPIA_POLLING_INTERVAL = "Time between DNS propagation check" + LOOPIA_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LOOPIA_TTL = "The TTL of the TXT record used for the DNS challenge" + LOOPIA_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://www.loopia.com/api" diff --git a/providers/dns/loopia/loopia_mock_test.go b/providers/dns/loopia/loopia_mock_test.go new file mode 100644 index 00000000..b8f108fa --- /dev/null +++ b/providers/dns/loopia/loopia_mock_test.go @@ -0,0 +1,236 @@ +package loopia + +import ( + "errors" + "fmt" + "testing" + + "github.com/go-acme/lego/v4/providers/dns/loopia/internal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const ( + exampleDomain = "example.com" + exampleSubDomain = "_acme-challenge" + exampleRdata = "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM" +) + +func TestDNSProvider_Present(t *testing.T) { + mockedFindZoneByFqdn := func(fqdn string) (string, error) { + return exampleDomain + ".", nil + } + + testCases := []struct { + desc string + + getTXTRecordsError error + getTXTRecordsReturn []internal.RecordObj + addTXTRecordError error + callAddTXTRecord bool + callGetTXTRecords bool + + expectedError string + expectedInProgressTokenInfo int + }{ + { + desc: "Present OK", + + getTXTRecordsReturn: []internal.RecordObj{{Type: "TXT", Rdata: exampleRdata, RecordID: 12345678}}, + callAddTXTRecord: true, + callGetTXTRecords: true, + + expectedInProgressTokenInfo: 12345678, + }, + { + desc: "AddTXTRecord fails", + + addTXTRecordError: fmt.Errorf("unknown error: 'ADDTXT'"), + callAddTXTRecord: true, + + expectedError: "loopia: failed to add TXT record: unknown error: 'ADDTXT'", + }, + { + desc: "GetTXTRecords fails", + + getTXTRecordsError: fmt.Errorf("unknown error: 'GETTXT'"), + callAddTXTRecord: true, + callGetTXTRecords: true, + + expectedError: "loopia: failed to get TXT records: unknown error: 'GETTXT'", + }, + { + desc: "Failed to get ID", + + callAddTXTRecord: true, + callGetTXTRecords: true, + + expectedError: "loopia: failed to find the stored TXT record", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIUser = "apiuser" + config.APIPassword = "password" + + client := &mockedClient{} + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + provider.findZoneByFqdn = mockedFindZoneByFqdn + provider.client = client + + if test.callAddTXTRecord { + client.On("AddTXTRecord", exampleDomain, exampleSubDomain, config.TTL, exampleRdata).Return(test.addTXTRecordError) + } + + if test.callGetTXTRecords { + client.On("GetTXTRecords", exampleDomain, exampleSubDomain).Return(test.getTXTRecordsReturn, test.getTXTRecordsError) + } + + err = provider.Present(exampleDomain, "token", "key") + + client.AssertExpectations(t) + + if test.expectedError == "" { + require.NoError(t, err) + assert.Equal(t, test.expectedInProgressTokenInfo, provider.inProgressInfo["token"]) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.expectedError) + } + }) + } +} + +func TestDNSProvider_Cleanup(t *testing.T) { + mockedFindZoneByFqdn := func(fqdn string) (string, error) { + return "example.com.", nil + } + + testCases := []struct { + desc string + + getTXTRecordsError error + getTXTRecordsReturn []internal.RecordObj + removeTXTRecordError error + removeSubdomainError error + callAddTXTRecord bool + callGetTXTRecords bool + callRemoveSubdomain bool + + expectedError string + }{ + { + desc: "Cleanup Ok", + + callAddTXTRecord: true, + callGetTXTRecords: true, + callRemoveSubdomain: true, + }, + { + desc: "removeTXTRecord failed", + + removeTXTRecordError: errors.New("authentication error"), + callAddTXTRecord: true, + + expectedError: "loopia: failed to remove TXT record: authentication error", + }, + { + desc: "removeSubdomain failed", + + removeSubdomainError: errors.New(`unknown error: "UNKNOWN_ERROR"`), + callAddTXTRecord: true, + callGetTXTRecords: true, + callRemoveSubdomain: true, + + expectedError: `loopia: failed to remove sub-domain: unknown error: "UNKNOWN_ERROR"`, + }, + { + desc: "Dont call removeSubdomain when records", + + getTXTRecordsReturn: []internal.RecordObj{{Type: "TXT", Rdata: "LEFTOVER"}}, + callAddTXTRecord: true, + callGetTXTRecords: true, + callRemoveSubdomain: false, + }, + { + desc: "getTXTRecords failed", + + getTXTRecordsError: errors.New(`unknown error: "UNKNOWN_ERROR"`), + callAddTXTRecord: true, + callGetTXTRecords: true, + callRemoveSubdomain: false, + + expectedError: `loopia: failed to get TXT records: unknown error: "UNKNOWN_ERROR"`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIUser = "apiuser" + config.APIPassword = "password" + + client := &mockedClient{} + + provider, err := NewDNSProviderConfig(config) + require.NoError(t, err) + + provider.findZoneByFqdn = mockedFindZoneByFqdn + provider.client = client + provider.inProgressInfo["token"] = 12345678 + + if test.callAddTXTRecord { + client.On("RemoveTXTRecord", "example.com", "_acme-challenge", 12345678).Return(test.removeTXTRecordError) + } + + if test.callGetTXTRecords { + client.On("GetTXTRecords", "example.com", "_acme-challenge").Return(test.getTXTRecordsReturn, test.getTXTRecordsError) + } + + if test.callRemoveSubdomain { + client.On("RemoveSubdomain", "example.com", "_acme-challenge").Return(test.removeSubdomainError) + } + + err = provider.CleanUp("example.com", "token", "key") + + client.AssertExpectations(t) + + if test.expectedError == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.expectedError) + } + }) + } +} + +type mockedClient struct { + mock.Mock +} + +func (c *mockedClient) RemoveTXTRecord(domain string, subdomain string, recordID int) error { + args := c.Called(domain, subdomain, recordID) + return args.Error(0) +} + +func (c *mockedClient) AddTXTRecord(domain string, subdomain string, ttl int, value string) error { + args := c.Called(domain, subdomain, ttl, value) + return args.Error(0) +} + +func (c *mockedClient) GetTXTRecords(domain string, subdomain string) ([]internal.RecordObj, error) { + args := c.Called(domain, subdomain) + return args.Get(0).([]internal.RecordObj), args.Error(1) +} + +func (c *mockedClient) RemoveSubdomain(domain, subdomain string) error { + args := c.Called(domain, subdomain) + return args.Error(0) +} diff --git a/providers/dns/loopia/loopia_test.go b/providers/dns/loopia/loopia_test.go new file mode 100644 index 00000000..9e50e324 --- /dev/null +++ b/providers/dns/loopia/loopia_test.go @@ -0,0 +1,214 @@ +package loopia + +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( + EnvAPIUser, + EnvAPIPassword, + EnvTTL, + EnvPollingInterval, + EnvPropagationTimeout, + EnvHTTPTimeout). + WithDomain(envDomain) + +func TestSplitDomain(t *testing.T) { + provider := &DNSProvider{ + findZoneByFqdn: func(fqdn string) (string, error) { + return "example.com.", nil + }, + } + + testCases := []struct { + desc string + fqdn string + subdomain string + domain string + }{ + { + desc: "single subdomain", + fqdn: "subdomain.example.com", + subdomain: "subdomain", + domain: "example.com", + }, + { + desc: "double subdomain", + fqdn: "sub.domain.example.com", + subdomain: "sub.domain", + domain: "example.com", + }, + { + desc: "asterisk subdomain", + fqdn: "*.example.com", + subdomain: "*", + domain: "example.com", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + subdomain, domain := provider.splitDomain(test.fqdn) + + assert.Equal(t, test.subdomain, subdomain) + assert.Equal(t, test.domain, domain) + }) + } +} + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expectedError string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIUser: "user", + EnvAPIPassword: "secret", + }, + }, + { + desc: "missing API user", + envVars: map[string]string{ + EnvAPIUser: "", + EnvAPIPassword: "secret", + }, + expectedError: "loopia: some credentials information are missing: LOOPIA_API_USER", + }, + { + desc: "missing API password", + envVars: map[string]string{ + EnvAPIUser: "user", + EnvAPIPassword: "", + }, + expectedError: "loopia: some credentials information are missing: LOOPIA_API_PASSWORD", + }, + { + desc: "missing credentials", + envVars: map[string]string{}, + expectedError: "loopia: some credentials information are missing: LOOPIA_API_USER,LOOPIA_API_PASSWORD", + }, + } + + 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 len(test.expectedError) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + } else { + require.Error(t, err) + require.EqualError(t, err, test.expectedError) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + config *Config + expectedTTL int + expectedError string + }{ + { + desc: "success", + config: &Config{ + APIUser: "user", + APIPassword: "secret", + TTL: 3600, + }, + expectedTTL: 3600, + }, + { + desc: "nil config user", + expectedError: "loopia: the configuration of the DNS provider is nil", + }, + { + desc: "empty user", + config: &Config{ + APIUser: "", + APIPassword: "secret", + TTL: 3600, + }, + expectedError: "loopia: credentials missing", + }, + { + desc: "empty password", + config: &Config{ + APIUser: "user", + APIPassword: "", + TTL: 3600, + }, + expectedTTL: 3600, + expectedError: "loopia: credentials missing", + }, + { + desc: "too low TTL", + config: &Config{ + APIUser: "user", + APIPassword: "secret", + TTL: 299, + }, + expectedTTL: 300, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p, err := NewDNSProviderConfig(test.config) + + if len(test.expectedError) == 0 { + require.NoError(t, err) + require.NotNil(t, p) + assert.Equal(t, test.expectedTTL, p.config.TTL) + } else { + require.Error(t, err) + assert.EqualError(t, err, test.expectedError) + } + }) + } +} + +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) +}