From f88c60a8f35040fed614d346a4eecab003c4390c Mon Sep 17 00:00:00 2001 From: Sven Peter Date: Mon, 17 Aug 2020 14:45:44 +0200 Subject: [PATCH] New provider: INWX (#808) * adds initial support for INWX * adds all features to the INWX provider * allows domain for tests in creds.json for INWX * runs go generate to update docs for INWX * fixes formatting with gofmt * changes goinwx to github.com/nrdcg/goinwx v0.8.0 * simplifies inwx sandbox check * changes inwx unknown key error to a warning * adds models.PostProcessRecords for inwx records * replaces strings.TrimRight with [:-1] to remove final dot for inwx * adds a comment about the domain creds.json key for the inwx provider * removes warning for invalid creds.json keys in the inwx provider * adds TOTP calculation support for inwx * adds comments to inwxProvider * improves INWX error messages * adds additional documentation about the TOTP support for INWX * adds inwx documentation * bumps goinwx to 0.8.1 to fix the inwx API --- OWNERS | 1 + README.md | 1 + docs/_includes/matrix.html | 56 +++++ docs/_providers/inwx.md | 99 ++++++++ docs/provider-list.md | 1 + go.mod | 4 +- go.sum | 17 ++ integrationTest/integration_test.go | 8 +- integrationTest/providers.json | 6 + providers/_all/all.go | 1 + providers/capabilities.go | 2 +- providers/inwx/inwxProvider.go | 345 ++++++++++++++++++++++++++++ 12 files changed, 535 insertions(+), 6 deletions(-) create mode 100644 docs/_providers/inwx.md create mode 100644 providers/inwx/inwxProvider.go diff --git a/OWNERS b/OWNERS index aa364ecf8..9bd9cd4af 100644 --- a/OWNERS +++ b/OWNERS @@ -11,6 +11,7 @@ providers/gandi_v5 @TomOnTime # providers/gcloud providers/hexonet @papakai providers/internetbs @pragmaton +providers/inwx @svenpeter42 providers/linode @koesie10 providers/namecheap @captncraig # providers/namedotcom diff --git a/README.md b/README.md index 030a27a9e..0d45ca636 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Currently supported DNS providers: - Google DNS - HEXONET - Internet.bs + - INWX - Linode - NS1 - Name.com diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html index 5bd1e2e1b..214a18d3a 100644 --- a/docs/_includes/matrix.html +++ b/docs/_includes/matrix.html @@ -20,6 +20,7 @@
GCLOUD
HEXONET
INTERNETBS
+
INWX
LINODE
NAMECHEAP
NAMEDOTCOM
@@ -85,6 +86,9 @@ + + + @@ -178,6 +182,9 @@ + + + @@ -241,6 +248,9 @@ + + + @@ -312,6 +322,9 @@ + + + @@ -363,6 +376,9 @@ + + + @@ -420,6 +436,9 @@ + + + @@ -485,6 +504,9 @@ + + + @@ -540,6 +562,9 @@ + + + @@ -595,6 +620,9 @@ + + + @@ -662,6 +690,9 @@ + + + @@ -717,6 +748,9 @@ + + + @@ -768,6 +802,9 @@ + + + @@ -816,6 +853,7 @@ + @@ -840,6 +878,9 @@ + + + @@ -877,6 +918,9 @@ + + + @@ -928,6 +972,9 @@ + + + @@ -1001,6 +1048,9 @@ + + + @@ -1085,6 +1135,9 @@ + + + @@ -1161,6 +1214,9 @@ + + + diff --git a/docs/_providers/inwx.md b/docs/_providers/inwx.md new file mode 100644 index 000000000..f070aa23d --- /dev/null +++ b/docs/_providers/inwx.md @@ -0,0 +1,99 @@ +--- +name: INWX +layout: default +jsId: INWX +--- + +# INWX + +INWX.de is a Berlin-based domain registrar. + +## Configuration +In your `creds.json` file you must provide your INWX +username and password: + +{% highlight json %} +{ + "inwx":{ + "username": "yourUsername", + "password": "yourPassword" + } +} +{% endhighlight %} + +### Two factor authentication + +If two factor authentication has been enabled you will also need to provide a valid TOTP number. +This can also be done +via an environment variable: +{% highlight json %} +{ + "inwx":{ + "username": "yourUsername", + "password": "yourPassword", + "totp": "$INWX_TOTP" + } +} +{% endhighlight %} + +and then you can run + +{% highlight bash %} +$ INWX_TOTP=12345 dnscontrol preview +{% endhighlight %} + +It is also possible to directly provide the shared TOTP secret using the key "totp-key" in `creds.json`. +This secret is only shown once when two factor authentication is enabled and you'll have to make sure to write it down then. + +**Important Notes**: +* Anyone with access to this `creds.json` file will have *full* access to your INWX account and will be able to transfer and/or delete your domains +* Storing the shared secret together with the password weakens two factor authentication because both factors are stored in a single place. + +{% highlight json %} +{ + "inwx":{ + "username": "yourUsername", + "password": "yourPassword", + "totp-key": "yourTOTPSharedSecret" + } +} +{% endhighlight %} + + +### Sandbox +You can optionally also specify sandbox with a value of 1 to +redirect all requests to the sandbox API instead: +{% highlight json %} +{ + "inwx":{ + "username": "yourUsername", + "password": "yourPassword", + "sandbox": "1" + } +} +{% endhighlight %} + +If sandbox is omitted or set to any other value the production +API will be used. + + +## Metadata +This provider does not recognize any special metadata fields unique to +INWX. + +## Usage +Example Javascript for `example.tld` registered with INWX +and delegated to CloudFlare: + +{% highlight js %} +var regInwx = NewRegistrar('inwx', 'INWX') +var dnsCF = NewDnsProvider('cloudflare', 'CLOUDFLAREAPI') + +D("example.tld", regInwx, DnsProvider(dnsCF), + A("test","1.2.3.4") +); + +{%endhighlight%} + + + diff --git a/docs/provider-list.md b/docs/provider-list.md index 00eecdf10..6d70502a7 100644 --- a/docs/provider-list.md +++ b/docs/provider-list.md @@ -80,6 +80,7 @@ Maintainers of contributed providers: * `GANDI_V5` @TomOnTime * `HEXONET` @papakai * `INTERNETBS` @pragmaton +* `INWX` @svenpeter42 * `LINODE` @koesie10 * `NAMECHEAP` @captncraig * `NETCUP` @kordianbruck diff --git a/go.mod b/go.mod index f5f3c4f92..d62d8350d 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,11 @@ require ( github.com/mittwald/go-powerdns v0.4.0 github.com/mjibson/esc v0.2.0 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 + github.com/nrdcg/goinwx v0.8.1 github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d github.com/pkg/errors v0.9.1 + github.com/pquerna/otp v1.2.0 github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff github.com/sergi/go-diff v1.1.0 // indirect @@ -48,7 +50,7 @@ require ( golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20200625001655-4c5254603344 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/tools v0.0.0-20200626032829-bcbc01e07a20 // indirect + golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 // indirect google.golang.org/api v0.28.0 google.golang.org/appengine v1.6.6 // indirect gopkg.in/ini.v1 v1.42.0 // indirect diff --git a/go.sum b/go.sum index 7015b3327..fb01fa798 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/aws/aws-sdk-go v1.32.10/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZve github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/billputer/go-namecheap v0.0.0-20170915210158-0c7adb0710f8 h1:sIv3xbwhhAG94a62Q/rrSBtrWcXiYgldNOeqifyKSgo= github.com/billputer/go-namecheap v0.0.0-20170915210158-0c7adb0710f8/go.mod h1:bqqNsI2akL+lLWyApkYY0cxquWPKwEBU0Wd3chi3TEg= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -92,6 +94,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/exoscale/egoscale v0.23.0 h1:hoUDzrO8yNoobNdnrRvlRFjfg3Ng0vQTrv6bXRJu6z0= github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-acme/lego v2.7.2+incompatible h1:ThhpPBgf6oa9X/vRd0kEmWOsX7+vmYdckmGZSb+FEp0= github.com/go-acme/lego v2.7.2+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= @@ -190,6 +193,8 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8= @@ -208,6 +213,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc= +github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -226,6 +233,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mittwald/go-powerdns v0.4.0 h1:vEl2+4JINusy5NF8weObVRCuvHv8wqNBVMPZSQWq9zo= github.com/mittwald/go-powerdns v0.4.0/go.mod h1:bI/sZBAWyTViDknOTp19VfDxVEnh1U7rWPx2aRKtlzg= @@ -235,6 +244,8 @@ github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAA github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU= +github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 h1:37VE5TYj2m/FLA9SNr4z0+A0JefvTmR60Zwf8XSEV7c= github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= @@ -249,6 +260,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 h1:Wdi9nwnhFNAlseAOekn6B5G/+GMtks9UKbvRU/CMM/o= github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03/go.mod h1:gRAiPF5C5Nd0eyyRdqIu9qTiFSoZzpTq727b5B8fkkU= @@ -295,6 +308,7 @@ github.com/vultr/govultr v0.2.0 h1:CZSNNCk+PHz9hzmfH2PFGkDgc3qNetwZqtcaqL8shlg= github.com/vultr/govultr v0.2.0/go.mod h1:glSLa57Jdj5s860EEc6+DEBbb/t3aUOKnB4gVPmDVlQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= @@ -382,6 +396,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -455,6 +470,8 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200626032829-bcbc01e07a20 h1:q+ysxVHVQNTVHgzwjuk4ApAILRbfOLARfnEaqCIBR6A= golang.org/x/tools v0.0.0-20200626032829-bcbc01e07a20/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 h1:8Kg+JssU1jBZs8GIrL5pl4nVyaqyyhdmHAR4D1zGErg= +golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 3737eae17..a0f76c863 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -623,7 +623,7 @@ func makeTests(t *testing.T) []*TestGroup { ), testgroup("Null MX", - not("AZURE_DNS", "GANDI_V5", "NAMEDOTCOM", "DIGITALOCEAN", "NETCUP", "DNSIMPLE"), // These providers don't support RFC 7505 + not("AZURE_DNS", "GANDI_V5", "INWX", "NAMEDOTCOM", "DIGITALOCEAN", "NETCUP", "DNSIMPLE"), // These providers don't support RFC 7505 tc("Null MX", mx("@", 0, ".")), ), @@ -656,14 +656,14 @@ func makeTests(t *testing.T) []*TestGroup { ), testgroup("ws TXT", - not("CLOUDFLAREAPI", "NAMEDOTCOM"), + not("CLOUDFLAREAPI", "INWX", "NAMEDOTCOM"), // These providers strip whitespace at the end of TXT records. // TODO(tal): Add a check for this in normalize/validate.go tc("Change a TXT with ws at end", txt("foo", "with space at end ")), ), testgroup("empty TXT", - not("CLOUDFLAREAPI", "NETCUP"), + not("CLOUDFLAREAPI", "INWX", "NETCUP"), tc("TXT with empty str", txt("foo1", "")), // https://github.com/StackExchange/dnscontrol/issues/598 // We decided that permitting the TXT target to be an empty @@ -786,7 +786,7 @@ func makeTests(t *testing.T) []*TestGroup { tc("Change Weight", srv("_sip._tcp", 52, 62, 7, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")), tc("Change Port", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, "foo4.com.")), ), - testgroup("SRV w/ null target", requires(providers.CanUseSRV), not("EXOSCALE", "HEXONET", "NAMEDOTCOM"), + testgroup("SRV w/ null target", requires(providers.CanUseSRV), not("EXOSCALE", "HEXONET", "INWX", "NAMEDOTCOM"), tc("Null Target", srv("_sip._tcp", 52, 62, 72, "foo.com."), srv("_sip._tcp", 15, 65, 75, ".")), ), diff --git a/integrationTest/providers.json b/integrationTest/providers.json index c1c94cecf..8c7bc3876 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -123,5 +123,11 @@ "VULTR": { "domain": "$VULTR_DOMAIN", "token": "$VULTR_TOKEN" + }, + "INWX": { + "username": "$INWX_USER", + "password": "$INWX_PASSWORD", + "domain": "$INWX_DOMAIN", + "sandbox": "1", } } diff --git a/providers/_all/all.go b/providers/_all/all.go index 8ee3fbcf8..878fe4ac6 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -17,6 +17,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v3/providers/gcloud" _ "github.com/StackExchange/dnscontrol/v3/providers/hexonet" _ "github.com/StackExchange/dnscontrol/v3/providers/internetbs" + _ "github.com/StackExchange/dnscontrol/v3/providers/inwx" _ "github.com/StackExchange/dnscontrol/v3/providers/linode" _ "github.com/StackExchange/dnscontrol/v3/providers/namecheap" _ "github.com/StackExchange/dnscontrol/v3/providers/namedotcom" diff --git a/providers/capabilities.go b/providers/capabilities.go index d5b0a5c2e..66495385a 100644 --- a/providers/capabilities.go +++ b/providers/capabilities.go @@ -29,7 +29,7 @@ const ( // CanUsePTR indicates the provider can handle PTR records CanUsePTR - // CanUseNAPTR indicates the provider can handle PTR records + // CanUseNAPTR indicates the provider can handle NAPTR records CanUseNAPTR // CanUseSRV indicates the provider can handle SRV records diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go new file mode 100644 index 000000000..7fdfe4813 --- /dev/null +++ b/providers/inwx/inwxProvider.go @@ -0,0 +1,345 @@ +package inwx + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/StackExchange/dnscontrol/v3/models" + "github.com/StackExchange/dnscontrol/v3/pkg/diff" + "github.com/StackExchange/dnscontrol/v3/providers" + + "github.com/nrdcg/goinwx" + "github.com/pquerna/otp/totp" +) + +/* +INWX Registrar and DNS provider + +Info required in `creds.json`: + - username + - password + +Either of the following settings is required when two factor authentication is enabled: + - totp (TOTP code if 2FA is enabled; best specified as an env variable) + - totp-key (shared TOTP secret used to generate a valid TOTP code; not recommended since + this effectively defeats the purpose of two factor authentication by storing + both factors at the same place) + +Additional settings available in `creds.json`: + - sandbox (set to 1 to use the sandbox API from INWX) + +*/ + +// InwxApi is a thin wrapper around goinwx.Client. +type InwxApi struct { + client *goinwx.Client + sandbox bool +} + +// InwxDefaultNs contains the default INWX nameservers. +var InwxDefaultNs = []string{"ns.inwx.de", "ns2.inwx.de", "ns3.inwx.eu", "ns4.inwx.com", "ns5.inwx.net"} + +// InwxSandboxDefaultNs contains the default INWX nameservers in the sandbox / OTE. +var InwxSandboxDefaultNs = []string{"ns.ote.inwx.de", "ns2.ote.inwx.de"} + +// features is used to let dnscontrol know which features are supported by INWX. +var features = providers.DocumentationNotes{ + providers.CanUseAlias: providers.Cannot("INWX does not support the ALIAS or ANAME record type."), + providers.CanUseCAA: providers.Can(), + providers.CanUseDS: providers.Unimplemented("DS records are only supported at the apex and require a different API call that hasn't been implemented yet."), + providers.CanUsePTR: providers.Can("PTR records with empty targets are not supported"), + providers.CanUseNAPTR: providers.Can(), + providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported."), + providers.CanUseSSHFP: providers.Can(), + providers.CanUseTLSA: providers.Can(), + providers.CanUseTXTMulti: providers.Cannot("INWX only supports a single entry for TXT records"), + providers.CanAutoDNSSEC: providers.Unimplemented("Supported by INWX but not implemented yet."), + providers.DocOfficiallySupported: providers.Cannot(), + providers.DocDualHost: providers.Can(), + providers.DocCreateDomains: providers.Unimplemented("Supported by INWX but not implemented yet."), + providers.CanGetZones: providers.Can(), + providers.CanUseAzureAlias: providers.Cannot(), +} + +// init registers the registrar and the domain service provider with dnscontrol. +func init() { + providers.RegisterRegistrarType("INWX", newInwxReg) + providers.RegisterDomainServiceProviderType("INWX", newInwxDsp, features) +} + +// getOTP either returns the TOTPValue or uses TOTPKey and the current time to generate a valid TOTPValue. +func getOTP(TOTPValue string, TOTPKey string) (string, error) { + if TOTPValue != "" { + return TOTPValue, nil + } else if TOTPKey != "" { + tan, err := totp.GenerateCode(TOTPKey, time.Now()) + if err != nil { + return "", fmt.Errorf("INWX: Unable to generate TOTP from totp-key: %v", err) + } + return tan, nil + } else { + return "", fmt.Errorf("INWX: two factor authentication required but no TOTP configured.") + } +} + +// loginHelper tries to login and then unlocks the account using two factor authentication if required. +func (api *InwxApi) loginHelper(TOTPValue string, TOTPKey string) error { + resp, err := api.client.Account.Login() + if err != nil { + return fmt.Errorf("INWX: Unable to login") + } + + switch TFA := resp.TFA; TFA { + case "0": + if TOTPKey != "" || TOTPValue != "" { + fmt.Printf("INWX: Warning: no TOTP requested by INWX but totp/totp-key is present in `creds.json`\n") + } + case "GOOGLE-AUTH": + tan, err := getOTP(TOTPValue, TOTPKey) + if err != nil { + return err + } + + err = api.client.Account.Unlock(tan) + if err != nil { + return fmt.Errorf("INWX: Could not unlock account: %w.", err) + } + default: + return fmt.Errorf("INWX: Unknown two factor authentication mode `%s` has been requested.", resp.TFA) + } + + return nil +} + +// newInwx initializes InwxApi and create a session. +func newInwx(m map[string]string) (*InwxApi, error) { + username, password := m["username"], m["password"] + TOTPValue, TOTPKey := m["totp"], m["totp-key"] + sandbox := m["sandbox"] == "1" + + if username == "" { + return nil, fmt.Errorf("INWX: username must be provided.") + } + if password == "" { + return nil, fmt.Errorf("INWX: password must be provided.") + } + if TOTPValue != "" && TOTPKey != "" { + return nil, fmt.Errorf("INWX: totp and totp-key must not be specified at the same time.") + } + + opts := &goinwx.ClientOptions{Sandbox: sandbox} + client := goinwx.NewClient(username, password, opts) + api := &InwxApi{client: client, sandbox: sandbox} + + err := api.loginHelper(TOTPValue, TOTPKey) + if err != nil { + return nil, err + } + + return api, nil +} + +// newInwxReg is called to initialize the INWX registrar provider. +func newInwxReg(m map[string]string) (providers.Registrar, error) { + return newInwx(m) +} + +// new InwxDsp is called to initialize the INWX domain service provider. +func newInwxDsp(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + return newInwx(m) +} + +// makeNameserverRecordRequest is a helper function used to convert a RecordConfig to an INWX NS Record Request. +func makeNameserverRecordRequest(domain string, rec *models.RecordConfig) *goinwx.NameserverRecordRequest { + content := rec.GetTargetField() + + req := &goinwx.NameserverRecordRequest{ + Domain: domain, + Type: rec.Type, + Content: content, + Name: rec.GetLabel(), + TTL: int(rec.TTL), + } + + switch rType := rec.Type; rType { + /* INWX is a little bit special for CNAME,NS,MX and SRV records: + The API will not accept any target with a final dot but will + instead always add this final dot internally. + Records with empty targets (i.e. records with target ".") + are not allowed. + */ + case "CNAME", "NS": + req.Content = content[:len(content)-1] + case "MX": + req.Priority = int(rec.MxPreference) + req.Content = content[:len(content)-1] + case "SRV": + req.Priority = int(rec.SrvPriority) + req.Content = fmt.Sprintf("%d %d %v", rec.SrvWeight, rec.SrvPort, content[:len(content)-1]) + default: + req.Content = rec.GetTargetCombined() + } + + return req +} + +// createRecord is used by GetDomainCorrections to create a new record. +func (api *InwxApi) createRecord(domain string, rec *models.RecordConfig) error { + req := makeNameserverRecordRequest(domain, rec) + _, err := api.client.Nameservers.CreateRecord(req) + return err +} + +// updateRecord is used by GetDomainCorrections to update an existing record. +func (api *InwxApi) updateRecord(RecordID int, rec *models.RecordConfig) error { + req := makeNameserverRecordRequest("", rec) + err := api.client.Nameservers.UpdateRecord(RecordID, req) + return err +} + +// deleteRecord is used by GetDomainCorrections to delete a record. +func (api *InwxApi) deleteRecord(RecordID int) error { + return api.client.Nameservers.DeleteRecord(RecordID) +} + +// GetDomainCorrections finds the currently existing records and returns the corrections required to update them. +func (api *InwxApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { + dc.Punycode() + + foundRecords, err := api.GetZoneRecords(dc.Name) + if err != nil { + return nil, err + } + + models.PostProcessRecords(foundRecords) + + differ := diff.New(dc) + _, create, del, mod := differ.IncrementalDiff(foundRecords) + corrections := []*models.Correction{} + + for _, d := range create { + des := d.Desired + corrections = append(corrections, &models.Correction{ + Msg: d.String(), + F: func() error { return api.createRecord(dc.Name, des) }, + }) + } + for _, d := range del { + existingID := d.Existing.Original.(goinwx.NameserverRecord).ID + corrections = append(corrections, &models.Correction{ + Msg: d.String(), + F: func() error { return api.deleteRecord(existingID) }, + }) + } + for _, d := range mod { + rec := d.Desired + existingID := d.Existing.Original.(goinwx.NameserverRecord).ID + corrections = append(corrections, &models.Correction{ + Msg: d.String(), + F: func() error { return api.updateRecord(existingID, rec) }, + }) + } + + return corrections, nil +} + +// GetNameservers returns the default nameservers for INWX. +func (api *InwxApi) GetNameservers(domain string) ([]*models.Nameserver, error) { + if api.sandbox { + return models.ToNameservers(InwxSandboxDefaultNs) + } else { + return models.ToNameservers(InwxDefaultNs) + } +} + +// GetZoneRecords receives the current records from Inwx and converts them to models.RecordConfig. +func (api *InwxApi) GetZoneRecords(domain string) (models.Records, error) { + info, err := api.client.Nameservers.Info(&goinwx.NameserverInfoRequest{Domain: domain}) + if err != nil { + return nil, err + } + + var records = []*models.RecordConfig{} + + for _, record := range info.Records { + if record.Type == "SOA" { + continue + } + + /* INWX is a little bit special for CNAME,NS,MX and SRV records: + The API will not accept any target with a final dot but will + instead always add this final dot internally. + Records with empty targets (i.e. records with target ".") + are not allowed. + */ + if record.Type == "CNAME" || record.Type == "MX" || record.Type == "NS" || record.Type == "SRV" { + record.Content = record.Content + "." + } + + rc := &models.RecordConfig{ + TTL: uint32(record.TTL), + Original: record, + } + rc.SetLabelFromFQDN(record.Name, domain) + + switch rType := record.Type; rType { + case "MX": + err = rc.SetTargetMX(uint16(record.Priority), record.Content) + case "SRV": + err = rc.SetTargetSRVPriorityString(uint16(record.Priority), record.Content) + default: + err = rc.PopulateFromString(rType, record.Content, domain) + } + + if err != nil { + panic(fmt.Errorf("INWX: unparsable record received: %w", err)) + } + + records = append(records, rc) + } + + return records, nil +} + +// updateNameservers is used by GetRegistrarCorrections to update the domain's nameservers. +func (api *InwxApi) updateNameservers(ns []string, domain string) func() error { + return func() error { + request := &goinwx.DomainUpdateRequest{ + Domain: domain, + Nameservers: ns, + } + + _, err := api.client.Domains.Update(request) + return err + } +} + +// GetRegistrarCorrections is part of the registrar provider and determines if the nameservers have to be updated. +func (api *InwxApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { + info, err := api.client.Domains.Info(dc.Name, 0) + if err != nil { + return nil, err + } + + sort.Strings(info.Nameservers) + foundNameservers := strings.Join(info.Nameservers, ",") + expected := []string{} + for _, ns := range dc.Nameservers { + expected = append(expected, ns.Name) + } + sort.Strings(expected) + expectedNameservers := strings.Join(expected, ",") + + if foundNameservers != expectedNameservers { + return []*models.Correction{ + { + Msg: fmt.Sprintf("Update nameservers %s -> %s", foundNameservers, expectedNameservers), + F: api.updateNameservers(expected, dc.Name), + }, + }, nil + } + return nil, nil +}