Add DNS provider for Loopia (#1324)

This commit is contained in:
hazzeh 2020-12-26 17:22:01 +01:00 committed by GitHub
parent dfdc625f8f
commit 3efb14404a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 2146 additions and 58 deletions

View file

@ -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/) |
<!-- END DNS PROVIDERS LIST -->

View file

@ -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.`)

View file

@ -0,0 +1,74 @@
---
title: "Loopia"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: loopia
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/loopia/loopia.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Since: v4.2.0
Configuration for [Loopia](https://loopia.com).
<!--more-->
- 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)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/loopia/loopia.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

49
go.sum
View file

@ -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=

View file

@ -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":

View file

@ -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(`<?xml version="1.0"?>`+"\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)
}
}

View file

@ -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, "<?xml version='1.0' encoding='UTF-8'?>")
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>name<name>",
},
{
desc: "faulty string",
xml: "<value><string>foo<string></value>",
},
{
desc: "faulty int",
xml: "<value><int>1<int></value>",
},
}
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
}

View file

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<string>
OK
</string>
</value>
</param>
</params>
</methodResponse>`
const responseAuthError = `<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<string>
AUTH_ERROR
</string>
</value>
</param>
</params>
</methodResponse>`
const responseUnknownError = `<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<string>
UNKNOWN_ERROR
</string>
</value>
</param>
</params>
</methodResponse>`
const responseRPCError = `<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>
faultCode
</name>
<value>
<int>
201
</int>
</value>
</member>
<member>
<name>
faultString
</name>
<value>
<string>
Method signature error: 42
</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>`
const addZoneRecordGoodAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>addZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>type</name>
<value>
<string>TXT</string>
</value>
</member>
<member>
<name>ttl</name>
<value>
<int>123</int>
</value>
</member>
<member>
<name>priority</name>
<value>
<int>0</int>
</value>
</member>
<member>
<name>rdata</name>
<value>
<string>TXTrecord</string>
</value>
</member>
<member>
<name>record_id</name>
<value>
<int>0</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>`
const addZoneRecordBadAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>addZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>badpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>type</name>
<value>
<string>TXT</string>
</value>
</member>
<member>
<name>ttl</name>
<value>
<int>123</int>
</value>
</member>
<member>
<name>priority</name>
<value>
<int>0</int>
</value>
</member>
<member>
<name>rdata</name>
<value>
<string>TXTrecord</string>
</value>
</member>
<member>
<name>record_id</name>
<value>
<int>0</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>`
const addZoneRecordNonValidDomain = `<?xml version="1.0"?>
<methodCall>
<methodName>addZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>badexample.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>type</name>
<value>
<string>TXT</string>
</value>
</member>
<member>
<name>ttl</name>
<value>
<int>123</int>
</value>
</member>
<member>
<name>priority</name>
<value>
<int>0</int>
</value>
</member>
<member>
<name>rdata</name>
<value>
<string>TXTrecord</string>
</value>
</member>
<member>
<name>record_id</name>
<value>
<int>0</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>`
const addZoneRecordEmptyResponse = `<?xml version="1.0"?>
<methodCall>
<methodName>addZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>empty.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>type</name>
<value>
<string>TXT</string>
</value>
</member>
<member>
<name>ttl</name>
<value>
<int>123</int>
</value>
</member>
<member>
<name>priority</name>
<value>
<int>0</int>
</value>
</member>
<member>
<name>rdata</name>
<value>
<string>TXTrecord</string>
</value>
</member>
<member>
<name>record_id</name>
<value>
<int>0</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>`
const getZoneRecords = `<?xml version="1.0"?>
<methodCall>
<methodName>getZoneRecords</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
</params>
</methodCall>`
const getZoneRecordsResponse = `<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>
rdata
</name>
<value>
<string>
LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM
</string>
</value>
</member>
<member>
<name>
record_id
</name>
<value>
<int>
12345678
</int>
</value>
</member>
<member>
<name>
priority
</name>
<value>
<int>
0
</int>
</value>
</member>
<member>
<name>
ttl
</name>
<value>
<int>
300
</int>
</value>
</member>
<member>
<name>
type
</name>
<value>
<string>
TXT
</string>
</value>
</member>
</struct>
</value>
</data>
</array>
</value>
</param>
</params>
</methodResponse>`
const removeRecordGoodAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>removeZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<int>12345678</int>
</value>
</param>
</params>
</methodCall>`
const removeRecordBadAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>removeZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>badpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<int>12345678</int>
</value>
</param>
</params>
</methodCall>`
const removeRecordNonValidDomain = `<?xml version="1.0"?>
<methodCall>
<methodName>removeZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>badexample.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<int>12345678</int>
</value>
</param>
</params>
</methodCall>`
const removeRecordEmptyResponse = `<?xml version="1.0"?>
<methodCall>
<methodName>removeZoneRecord</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>empty.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
<param>
<value>
<int>12345678</int>
</value>
</param>
</params>
</methodCall>`
const removeSubdomainGoodAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>removeSubdomain</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
</params>
</methodCall>`
const removeSubdomainBadAuth = `<?xml version="1.0"?>
<methodCall>
<methodName>removeSubdomain</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>badpassword</string>
</value>
</param>
<param>
<value>
<string>example.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
</params>
</methodCall>`
const removeSubdomainNonValidDomain = `<?xml version="1.0"?>
<methodCall>
<methodName>removeSubdomain</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>badexample.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
</params>
</methodCall>`
const removeSubdomainEmptyResponse = `<?xml version="1.0"?>
<methodCall>
<methodName>removeSubdomain</methodName>
<params>
<param>
<value>
<string>apiuser</string>
</value>
</param>
<param>
<value>
<string>goodpassword</string>
</value>
</param>
<param>
<value>
<string>empty.com</string>
</value>
</param>
<param>
<value>
<string>_acme-challenge</string>
</value>
</param>
</params>
</methodCall>`

View file

@ -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: <name>
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: <value><string>
if err = r.decodeValueString(name, d, start); err != nil {
return err
}
case "int": // An int value of the record object: <value><int>
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
}

View file

@ -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
}

View file

@ -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"

View file

@ -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)
}

View file

@ -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)
}