diff --git a/.gitignore b/.gitignore index ee191361..03ba0e69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -lego.exe -lego .lego .gitcookies .idea diff --git a/.golangci.toml b/.golangci.toml index 681218eb..56a54da7 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -1,5 +1,6 @@ [run] - deadline = "2m" + deadline = "5m" + skip-files = [] [linters-settings] @@ -7,13 +8,13 @@ check-shadowing = true [linters-settings.gocyclo] - min-complexity = 16.0 + min-complexity = 12.0 [linters-settings.maligned] suggest-new = true [linters-settings.goconst] - min-len = 2.0 + min-len = 3.0 min-occurrences = 3.0 [linters-settings.misspell] @@ -27,15 +28,27 @@ "gas", "dupl", "prealloc", + "scopelint", ] [issues] + exclude-use-default = false max-per-linter = 0 max-same = 0 exclude = [ - "func (.+)disableAuthz(.) is unused", # acme/client.go#disableAuthz - "type (.+)deactivateAuthMessage(.) is unused", # acme/messages.go#deactivateAuthMessage - "(.)limitReader(.) - (.)numBytes(.) always receives (.)1048576(.)", # acme/crypto.go#limitReader - "cyclomatic complexity (\\d+) of func (.)NewDNSChallengeProviderByName(.) is high", # providers/dns/dns_providers.go#NewDNSChallengeProviderByName - "cyclomatic complexity (\\d+) of func (.)setup(.) is high", # cli_handler.go#setup + "Error return value of (.+) is not checked", + "exported (type|method|function) (.+) should have comment or be unexported", + "possible misuse of unsafe.Pointer", + "cyclomatic complexity (.+) of func `NewDNSChallengeProviderByName` is high (.+)", # providers/dns/dns_providers.go + + "`(tlsFeatureExtensionOID|ocspMustStapleFeature)` is a global variable", # certcrypto/crypto.go + "`(defaultNameservers|recursiveNameservers|dnsTimeout|fqdnToZone|muFqdnToZone)` is a global variable", # challenge/dns01/nameserver.go + "`idPeAcmeIdentifierV1` is a global variable", # challenge/tlsalpn01/tls_alpn_challenge.go + "`Logger` is a global variable", # log/logger.go + "`version` is a global variable", # cli.go + "`load` is a global variable", # e2e/challenges_test.go + "`envTest` is a global variable", # providers/dns/**/*_test.go + "`(tldsMock|testCases)` is a global variable", # providers/dns/namecheap/namecheap_test.go + "`(errorClientErr|errorStorageErr|egTestAccount)` is a global variable", # providers/dns/acmedns/acmedns_test.go + "`memcachedHosts` is a global variable", # providers/http/memcached/memcached_test.go ] diff --git a/.goreleaser.yml b/.goreleaser.yml index 4a68fa94..64290e63 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,6 +2,11 @@ project_name: lego builds: - binary: lego + + main: ./cmd/lego/main.go + ldflags: + - -s -w -X main.version={{.Version}} + goos: - windows - darwin diff --git a/.travis.yml b/.travis.yml index 9cf7851e..e3319b9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,21 @@ language: go go: - - 1.9.x + - 1.10.x - 1.x services: - memcached +addons: + hosts: + # for e2e tests + - acme.wtf + - lego.wtf + - acme.lego.wtf + - légô.wtf + - xn--lg-bja9b.wtf + env: - MEMCACHED_HOSTS=localhost:11211 @@ -17,8 +26,14 @@ before_install: - curl -sI https://github.com/golang/dep/releases/latest | grep -Fi Location | tr -d '\r' | sed "s/tag/download/g" | awk -F " " '{ print $2 "/dep-linux-amd64"}' | wget --output-document=$GOPATH/bin/dep -i - - chmod +x $GOPATH/bin/dep + # Install Pebble + - go get -u github.com/letsencrypt/pebble/... + + # Install challtestsrv + - go get -u github.com/letsencrypt/boulder/test/challtestsrv/... + # Install linters and misspell - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.10.2 + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.12.2 - golangci-lint --version install: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34f79c3f..f5444497 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,7 @@ owners to license your work under the terms of the [MIT License](LICENSE). | Namecheap | `namecheap` | [documentation](https://www.namecheap.com/support/api/methods.aspx) | - | | Name.com | `namedotcom` | [documentation](https://www.name.com/api-docs/DNS) | [Go client](https://github.com/namedotcom/go) | | manual | `manual` | - | - | +| MyDNS.jp | `mydnsjp` | [documentation](https://www.mydns.jp/?MENU=030) | - | | Netcup | `netcup` | [documentation](https://www.netcup-wiki.de/wiki/DNS_API) | - | | NIFCloud | `nifcloud` | [documentation](https://mbaas.nifcloud.com/doc/current/rest/common/format.html) | - | | NS1 | `ns1` | [documentation](https://ns1.com/api) | [Go client](https://github.com/ns1/ns1-go) | @@ -77,8 +78,9 @@ owners to license your work under the terms of the [MIT License](LICENSE). | RFC2136 | `rfc2136` | [documentation](https://tools.ietf.org/html/rfc2136) | - | | Route 53 | `route53` | [documentation](https://docs.aws.amazon.com/Route53/latest/APIReference/API_Operations_Amazon_Route_53.html) | [Go client](https://github.com/aws/aws-sdk-go/aws) | | Sakura Cloud | `sakuracloud` | [documentation](https://developer.sakura.ad.jp/cloud/api/1.1/) | [Go client](https://github.com/sacloud/libsacloud) | -| Selectel | `selectel` | [documentation](https://kb.selectel.com/23136054.html) | - | +| Selectel | `selectel` | [documentation](https://kb.selectel.com/23136054.html) | - | | Stackpath | `stackpath` | [documentation](https://developer.stackpath.com/en/api/dns/#tag/Zone) | - | +| TransIP | `transip` | [documentation](https://api.transip.nl/docs/transip.nl/package-Transip.html) | [Go client](https://github.com/transip/gotransip) | | VegaDNS | `vegadns` | [documentation](https://github.com/shupp/VegaDNS-API) | [Go client](https://github.com/OpenDNS/vegadns2client) | | Vultr | `vultr` | [documentation](https://www.vultr.com/api/#dns) | [Go client](https://github.com/JamesClonk/vultr) | | Vscale | `vscale` | [documentation](https://developers.vscale.io/documentation/api/v1/#api-Domains_Records) | - | \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4bf10eae..cb942944 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,5 @@ RUN make build FROM alpine:3.8 RUN apk update && apk add --no-cache --virtual ca-certificates -COPY --from=builder /go/src/github.com/xenolf/lego/lego /usr/bin/lego +COPY --from=builder /go/src/github.com/xenolf/lego/dist/lego /usr/bin/lego ENTRYPOINT [ "/usr/bin/lego" ] diff --git a/Gopkg.lock b/Gopkg.lock index ad376567..6ecb7f67 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -176,20 +176,20 @@ revision = "5448fe645cb1964ba70ac8f9f2ffe975e61a536c" [[projects]] - branch = "master" - digest = "1:6b873be0e0ec65484ee086d02143f31332e363b968fdc6d6663160fa98fda505" + digest = "1:e856fc44ab196970612bdc8c15e65ccf92ed8d4ccb3a2e65b88dc240a2fe5d0b" name = "github.com/dnsimple/dnsimple-go" packages = ["dnsimple"] pruneopts = "NUT" - revision = "35bcc6b47c20ec9bf3a53adcb7fa9665a75f0e7b" + revision = "f5ead9c20763fd925dea1362f2af5d671ed2a459" + version = "v0.21.0" [[projects]] - digest = "1:e096f1857eedd49e2bd0885d05105d1d4af1bfcf8b1d07fa5710718e6641fd48" + digest = "1:e68d50b8dc605565eb62df1c2b2c67fa729e5b55aa1a6c81456eecbe0326ecdb" name = "github.com/exoscale/egoscale" packages = ["."] pruneopts = "NUT" - revision = "0863d555d5198557e0bf2b61b6c59a873ab0173a" - version = "v0.11.1" + revision = "67368ae928a70cb5cb44ecf6f418ee33a1ade044" + version = "v0.11.6" [[projects]] digest = "1:aa3ed0a71c4e66e4ae6486bf97a3f4cab28edc78df2e50c5ad01dc7d91604b88" @@ -298,12 +298,12 @@ version = "v0.5.1" [[projects]] - digest = "1:24b5f8d41224b90e3f4d22768926ed782a8ca481d945c0e064c8f165bf768280" + digest = "1:6676c63cef61a47c84eae578bcd8fe8352908ccfe3ea663c16797617a29e3c44" name = "github.com/miekg/dns" packages = ["."] pruneopts = "NUT" - revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1" - version = "v1.0.8" + revision = "a220737569d8137d4c610f80bd33f1dc762522e5" + version = "v1.1.0" [[projects]] digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23" @@ -379,7 +379,7 @@ [[projects]] branch = "master" - digest = "1:0f9362b2768972675cf28574249bfb5dd65556aac6ad1c36830b4bc8c2134926" + digest = "1:180e8ec2d3734b269a8a30b51dbca47fede2ce274fa76da2f00e664481cfb39e" name = "github.com/sacloud/libsacloud" packages = [ ".", @@ -388,7 +388,7 @@ "sacloud/ostype", ] pruneopts = "NUT" - revision = "7afff3fbc0a3bdff2e008fe2c429d44d9f66f209" + revision = "108b1efe4b4d106fee6760bdf1847c4f92e1a92e" [[projects]] digest = "1:6bc0652ea6e39e22ccd522458b8bdd8665bf23bdc5a20eec90056e4dc7e273ca" @@ -613,7 +613,7 @@ revision = "028658c6d9be774b6d103a923d8c4b2715135c3f" [[projects]] - digest = "1:3b7124c543146736e07107be13ea6288923c4a743e07c7a31d6b7209a00a9dab" + digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8" name = "gopkg.in/square/go-jose.v2" packages = [ ".", @@ -621,8 +621,8 @@ "json", ] pruneopts = "NUT" - revision = "8254d6c783765f38c8675fae4427a1fe73fbd09d" - version = "v2.1.8" + revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" + version = "v2.1.9" [solve-meta] analyzer-name = "dep" @@ -676,6 +676,7 @@ "github.com/urfave/cli", "golang.org/x/crypto/ocsp", "golang.org/x/net/context", + "golang.org/x/net/idna", "golang.org/x/net/publicsuffix", "golang.org/x/oauth2", "golang.org/x/oauth2/clientcredentials", diff --git a/Gopkg.toml b/Gopkg.toml index 9c365a4c..d94798e9 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -34,7 +34,7 @@ name = "github.com/decker502/dnspod-go" [[constraint]] - branch = "master" + version = "0.21.0" name = "github.com/dnsimple/dnsimple-go" [[constraint]] @@ -92,3 +92,7 @@ [[constraint]] version = "0.11.1" name = "github.com/exoscale/egoscale" + +[[constraint]] + version = "v1.1.0" + name = "github.com/miekg/dns" diff --git a/Makefile b/Makefile index 4a2f1f1c..890ac7ce 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ .PHONY: clean checks test build image dependencies +SRCS = $(shell git ls-files '*.go' | grep -v '^vendor/') + LEGO_IMAGE := xenolf/lego +MAIN_DIRECTORY := ./cmd/lego/ +BIN_OUTPUT := dist/lego TAG_NAME := $(shell git tag -l --contains HEAD) SHA := $(shell git rev-parse HEAD) @@ -13,7 +17,11 @@ clean: build: clean @echo Version: $(VERSION) - go build -v -ldflags '-X "main.version=${VERSION}"' + go build -v -ldflags '-X "main.version=${VERSION}"' -o ${BIN_OUTPUT} ${MAIN_DIRECTORY} + +image: + @echo Version: $(VERSION) + docker build -t $(LEGO_IMAGE) . dependencies: dep ensure -v @@ -21,9 +29,11 @@ dependencies: test: clean go test -v -cover ./... +e2e: clean + LEGO_E2E_TESTS=local go test -count=1 -v ./e2e/... + checks: golangci-lint run -image: - @echo Version: $(VERSION) - docker build -t $(LEGO_IMAGE) . +fmt: + gofmt -s -l -w $(SRCS) diff --git a/README.md b/README.md index 6eb1884e..73538e2e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ yaourt -S lego-git To install from source, just run: ```bash -go get -u github.com/xenolf/lego +go get -u github.com/xenolf/lego/cmd/lego ``` ## Features @@ -71,29 +71,31 @@ COMMANDS: revoke Revoke a certificate renew Renew a certificate dnshelp Shows additional help for the --dns global option + list Display certificates and accounts information. help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --domains value, -d value Add a domain to the process. Can be specified multiple times. - --csr value, -c value Certificate signing request filename, if an external CSR is to be used --server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory") - --email value, -m value Email used for registration and recovery contact. - --filename value Filename of the generated certificate --accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service. + --email value, -m value Email used for registration and recovery contact. + --csr value, -c value Certificate signing request filename, if an external CSR is to be used --eab Use External Account Binding for account registration. Requires --kid and --hmac. --kid value Key identifier from External CA. Used for External Account Binding. --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048") + --filename value Filename of the generated certificate --path value Directory to use for storing the data (default: "./.lego") --exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01", "tls-alpn-01". + --http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) --webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge --memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. --http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port --tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port --dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage. - --http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) - --dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0) + --dns-disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers. --dns-resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined. + --dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. The default is 10 seconds. (default: 0) --pem Generate a .pem file by concatenating the .key and .crt files together. --help, -h show help --version, -v print the version @@ -174,6 +176,104 @@ lego defaults to communicating with the production Let's Encrypt ACME server. If lego --server=https://acme-staging-v02.api.letsencrypt.org/directory … ``` +## ACME Library Usage + +A valid, but bare-bones example use of the acme package: + +```go +package main + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "log" + + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/registration" +) + +// You'll need a user or account type that implements acme.User +type MyUser struct { + Email string + Registration *registration.Resource + key crypto.PrivateKey +} + +func (u *MyUser) GetEmail() string { + return u.Email +} +func (u MyUser) GetRegistration() *registration.Resource { + return u.Registration +} +func (u *MyUser) GetPrivateKey() crypto.PrivateKey { + return u.key +} + +func main() { + + // Create a user. New accounts need an email and private key to start. + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatal(err) + } + + myUser := MyUser{ + Email: "you@yours.com", + key: privateKey, + } + + config := lego.NewConfig(&myUser) + + // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. + config.CADirURL = "http://192.168.99.100:4000/directory" + config.KeyType = certcrypto.RSA2048 + + // A client facilitates communication with the CA server. + client, err := lego.NewClient(config) + if err != nil { + log.Fatal(err) + } + + // We specify an http port of 5002 and an tls port of 5001 on all interfaces + // because we aren't running as root and can't bind a listener to port 80 and 443 + // (used later when we attempt to pass challenges). Keep in mind that you still + // need to proxy challenge traffic to port 5002 and 5001. + if err = client.Challenge.SetHTTP01Address(":5002"); err != nil { + log.Fatal(err) + } + if err = client.Challenge.SetTLSALPN01Address(":5001"); err != nil { + log.Fatal(err) + } + + // New users will need to register + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + log.Fatal(err) + } + myUser.Registration = reg + + request := certificate.ObtainRequest{ + Domains: []string{"mydomain.com"}, + Bundle: true, + } + certificates, err := client.Certificate.Obtain(request) + if err != nil { + log.Fatal(err) + } + + // Each certificate comes back with the cert bytes, the bytes of the client's + // private key, and a certificate URL. SAVE THESE TO DISK. + fmt.Printf("%#v\n", certificates) + + // ... all done. +} +``` + ## DNS Challenge API Details ### AWS Route 53 @@ -183,108 +283,31 @@ Replace `` with the Route 53 zone ID of the dom ```json { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "route53:GetChange", - "route53:ListHostedZonesByName" - ], - "Resource": [ - "*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "route53:ChangeResourceRecordSets" - ], - "Resource": [ - "arn:aws:route53:::hostedzone/" - ] - } - ] + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Action": [ + "route53:GetChange", + "route53:ChangeResourceRecordSets", + "route53:ListResourceRecordSets" + ], + "Resource": [ + "arn:aws:route53:::hostedzone/*", + "arn:aws:route53:::change/*" + ] + }, + { + "Sid": "", + "Effect": "Allow", + "Action": "route53:ListHostedZonesByName", + "Resource": "*" + } + ] } ``` -## ACME Library Usage - -A valid, but bare-bones example use of the acme package: - -```go -// You'll need a user or account type that implements acme.User -type MyUser struct { - Email string - Registration *acme.RegistrationResource - key crypto.PrivateKey -} -func (u MyUser) GetEmail() string { - return u.Email -} -func (u MyUser) GetRegistration() *acme.RegistrationResource { - return u.Registration -} -func (u MyUser) GetPrivateKey() crypto.PrivateKey { - return u.key -} - -// Create a user. New accounts need an email and private key to start. -const rsaKeySize = 2048 -privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySize) -if err != nil { - log.Fatal(err) -} -myUser := MyUser{ - Email: "you@yours.com", - key: privateKey, -} - -// A client facilitates communication with the CA server. This CA URL is -// configured for a local dev instance of Boulder running in Docker in a VM. -client, err := acme.NewClient("http://192.168.99.100:4000/directory", &myUser, acme.RSA2048) -if err != nil { - log.Fatal(err) -} - -// We specify an http port of 5002 and an tls port of 5001 on all interfaces -// because we aren't running as root and can't bind a listener to port 80 and 443 -// (used later when we attempt to pass challenges). Keep in mind that we still -// need to proxy challenge traffic to port 5002 and 5001. -client.SetHTTPAddress(":5002") -client.SetTLSAddress(":5001") - -// New users will need to register -reg, err := client.Register() -if err != nil { - log.Fatal(err) -} -myUser.Registration = reg - -// SAVE THE USER. - -// The client has a URL to the current Let's Encrypt Subscriber -// Agreement. The user will need to agree to it. -err = client.AgreeToTOS() -if err != nil { - log.Fatal(err) -} - -// The acme library takes care of completing the challenges to obtain the certificate(s). -// The domains must resolve to this machine or you have to use the DNS challenge. -bundle := false -certificates, failures := client.ObtainCertificate([]string{"mydomain.com"}, bundle, nil, false) -if len(failures) > 0 { - log.Fatal(failures) -} - -// Each certificate comes back with the cert bytes, the bytes of the client's -// private key, and a certificate URL. SAVE THESE TO DISK. -fmt.Printf("%#v\n", certificates) - -// ... all done. -``` - ## ACME v1 lego introduced support for ACME v2 in [v1.0.0](https://github.com/xenolf/lego/releases/tag/v1.0.0), if you still need to utilize ACME v1, you can do so by using the [v0.5.0](https://github.com/xenolf/lego/releases/tag/v0.5.0) version. diff --git a/account.go b/account.go deleted file mode 100644 index 9d011047..00000000 --- a/account.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "crypto" - "encoding/json" - "io/ioutil" - "os" - "path/filepath" - - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/log" -) - -// Account represents a users local saved credentials -type Account struct { - Email string `json:"email"` - key crypto.PrivateKey - Registration *acme.RegistrationResource `json:"registration"` - - conf *Configuration -} - -// NewAccount creates a new account for an email address -func NewAccount(email string, conf *Configuration) *Account { - accKeysPath := conf.AccountKeysPath(email) - // TODO: move to function in configuration? - accKeyPath := filepath.Join(accKeysPath, email+".key") - if err := checkFolder(accKeysPath); err != nil { - log.Fatalf("Could not check/create directory for account %s: %v", email, err) - } - - var privKey crypto.PrivateKey - if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { - - log.Printf("No key found for account %s. Generating a curve P384 EC key.", email) - privKey, err = generatePrivateKey(accKeyPath) - if err != nil { - log.Fatalf("Could not generate RSA private account key for account %s: %v", email, err) - } - - log.Printf("Saved key to %s", accKeyPath) - } else { - privKey, err = loadPrivateKey(accKeyPath) - if err != nil { - log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) - } - } - - accountFile := filepath.Join(conf.AccountPath(email), "account.json") - if _, err := os.Stat(accountFile); os.IsNotExist(err) { - return &Account{Email: email, key: privKey, conf: conf} - } - - fileBytes, err := ioutil.ReadFile(accountFile) - if err != nil { - log.Fatalf("Could not load file for account %s -> %v", email, err) - } - - var acc Account - err = json.Unmarshal(fileBytes, &acc) - if err != nil { - log.Fatalf("Could not parse file for account %s -> %v", email, err) - } - - acc.key = privKey - acc.conf = conf - - if acc.Registration == nil || acc.Registration.Body.Status == "" { - reg, err := tryRecoverAccount(privKey, conf) - if err != nil { - log.Fatalf("Could not load account for %s. Registration is nil -> %#v", email, err) - } - - acc.Registration = reg - err = acc.Save() - if err != nil { - log.Fatalf("Could not save account for %s. Registration is nil -> %#v", email, err) - } - } - - if acc.conf == nil { - log.Fatalf("Could not load account for %s. Configuration is nil.", email) - } - - return &acc -} - -func tryRecoverAccount(privKey crypto.PrivateKey, conf *Configuration) (*acme.RegistrationResource, error) { - // couldn't load account but got a key. Try to look the account up. - serverURL := conf.context.GlobalString("server") - client, err := acme.NewClient(serverURL, &Account{key: privKey, conf: conf}, acme.RSA2048) - if err != nil { - return nil, err - } - - reg, err := client.ResolveAccountByKey() - if err != nil { - return nil, err - } - return reg, nil -} - -/** Implementation of the acme.User interface **/ - -// GetEmail returns the email address for the account -func (a *Account) GetEmail() string { - return a.Email -} - -// GetPrivateKey returns the private RSA account key. -func (a *Account) GetPrivateKey() crypto.PrivateKey { - return a.key -} - -// GetRegistration returns the server registration -func (a *Account) GetRegistration() *acme.RegistrationResource { - return a.Registration -} - -/** End **/ - -// Save the account to disk -func (a *Account) Save() error { - jsonBytes, err := json.MarshalIndent(a, "", "\t") - if err != nil { - return err - } - - return ioutil.WriteFile( - filepath.Join(a.conf.AccountPath(a.Email), "account.json"), - jsonBytes, - 0600, - ) -} diff --git a/acme/api/account.go b/acme/api/account.go new file mode 100644 index 00000000..489be420 --- /dev/null +++ b/acme/api/account.go @@ -0,0 +1,69 @@ +package api + +import ( + "encoding/base64" + "errors" + "fmt" + + "github.com/xenolf/lego/acme" +) + +type AccountService service + +// New Creates a new account. +func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) { + var account acme.Account + resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account) + location := getLocation(resp) + + if len(location) > 0 { + a.core.jws.SetKid(location) + } + + if err != nil { + return acme.ExtendedAccount{Location: location}, err + } + + return acme.ExtendedAccount{Account: account, Location: location}, nil +} + +// NewEAB Creates a new account with an External Account Binding. +func (a *AccountService) NewEAB(accMsg acme.Account, kid string, hmacEncoded string) (acme.ExtendedAccount, error) { + hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) + if err != nil { + return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %v", err) + } + + eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac) + if err != nil { + return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %v", err) + } + accMsg.ExternalAccountBinding = eabJWS + + return a.New(accMsg) +} + +// Get Retrieves an account. +func (a *AccountService) Get(accountURL string) (acme.Account, error) { + if len(accountURL) == 0 { + return acme.Account{}, errors.New("account[get]: empty URL") + } + + var account acme.Account + _, err := a.core.post(accountURL, acme.Account{}, &account) + if err != nil { + return acme.Account{}, err + } + return account, nil +} + +// Deactivate Deactivates an account. +func (a *AccountService) Deactivate(accountURL string) error { + if len(accountURL) == 0 { + return errors.New("account[deactivate]: empty URL") + } + + req := acme.Account{Status: acme.StatusDeactivated} + _, err := a.core.post(accountURL, req, nil) + return err +} diff --git a/acme/api/api.go b/acme/api/api.go new file mode 100644 index 00000000..e14cd993 --- /dev/null +++ b/acme/api/api.go @@ -0,0 +1,151 @@ +package api + +import ( + "bytes" + "crypto" + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api/internal/nonces" + "github.com/xenolf/lego/acme/api/internal/secure" + "github.com/xenolf/lego/acme/api/internal/sender" + "github.com/xenolf/lego/log" +) + +// Core ACME/LE core API. +type Core struct { + doer *sender.Doer + nonceManager *nonces.Manager + jws *secure.JWS + directory acme.Directory + HTTPClient *http.Client + + common service // Reuse a single struct instead of allocating one for each service on the heap. + Accounts *AccountService + Authorizations *AuthorizationService + Certificates *CertificateService + Challenges *ChallengeService + Orders *OrderService +} + +// New Creates a new Core. +func New(httpClient *http.Client, userAgent string, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) { + doer := sender.NewDoer(httpClient, userAgent) + + dir, err := getDirectory(doer, caDirURL) + if err != nil { + return nil, err + } + + nonceManager := nonces.NewManager(doer, dir.NewNonceURL) + + jws := secure.NewJWS(privateKey, kid, nonceManager) + + c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir} + + c.common.core = c + c.Accounts = (*AccountService)(&c.common) + c.Authorizations = (*AuthorizationService)(&c.common) + c.Certificates = (*CertificateService)(&c.common) + c.Challenges = (*ChallengeService)(&c.common) + c.Orders = (*OrderService)(&c.common) + + return c, nil +} + +// post performs an HTTP POST request and parses the response body as JSON, +// into the provided respBody object. +func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) { + content, err := json.Marshal(reqBody) + if err != nil { + return nil, errors.New("failed to marshal message") + } + + return a.retrievablePost(uri, content, response, 0) +} + +// postAsGet performs an HTTP POST ("POST-as-GET") request. +// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.3 +func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) { + return a.retrievablePost(uri, []byte{}, response, 0) +} + +func (a *Core) retrievablePost(uri string, content []byte, response interface{}, retry int) (*http.Response, error) { + resp, err := a.signedPost(uri, content, response) + if err != nil { + // during tests, 5 retries allow to support ~50% of bad nonce. + if retry >= 5 { + log.Infof("too many retry on a nonce error, retry count: %d", retry) + return resp, err + } + switch err.(type) { + // Retry once if the nonce was invalidated + case *acme.NonceError: + log.Infof("nonce error retry: %s", err) + resp, err = a.retrievablePost(uri, content, response, retry+1) + if err != nil { + return resp, err + } + default: + return resp, err + } + } + + return resp, nil +} + +func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) { + signedContent, err := a.jws.SignContent(uri, content) + if err != nil { + return nil, fmt.Errorf("failed to post JWS message -> failed to sign content -> %v", err) + } + + signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize())) + + resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response) + + // nonceErr is ignored to keep the root error. + nonce, nonceErr := nonces.GetFromResponse(resp) + if nonceErr == nil { + a.nonceManager.Push(nonce) + } + + return resp, err +} + +func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) { + eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac) + if err != nil { + return nil, err + } + + return []byte(eabJWS.FullSerialize()), nil +} + +// GetKeyAuthorization Gets the key authorization +func (a *Core) GetKeyAuthorization(token string) (string, error) { + return a.jws.GetKeyAuthorization(token) +} + +func (a *Core) GetDirectory() acme.Directory { + return a.directory +} + +func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) { + var dir acme.Directory + if _, err := do.Get(caDirURL, &dir); err != nil { + return dir, fmt.Errorf("get directory at '%s': %v", caDirURL, err) + } + + if dir.NewAccountURL == "" { + return dir, errors.New("directory missing new registration URL") + } + if dir.NewOrderURL == "" { + return dir, errors.New("directory missing new order URL") + } + + return dir, nil +} diff --git a/acme/api/authorization.go b/acme/api/authorization.go new file mode 100644 index 00000000..ed4a4867 --- /dev/null +++ b/acme/api/authorization.go @@ -0,0 +1,34 @@ +package api + +import ( + "errors" + + "github.com/xenolf/lego/acme" +) + +type AuthorizationService service + +// Get Gets an authorization. +func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) { + if len(authzURL) == 0 { + return acme.Authorization{}, errors.New("authorization[get]: empty URL") + } + + var authz acme.Authorization + _, err := c.core.postAsGet(authzURL, &authz) + if err != nil { + return acme.Authorization{}, err + } + return authz, nil +} + +// Deactivate Deactivates an authorization. +func (c *AuthorizationService) Deactivate(authzURL string) error { + if len(authzURL) == 0 { + return errors.New("authorization[deactivate]: empty URL") + } + + var disabledAuth acme.Authorization + _, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth) + return err +} diff --git a/acme/api/certificate.go b/acme/api/certificate.go new file mode 100644 index 00000000..db939ae4 --- /dev/null +++ b/acme/api/certificate.go @@ -0,0 +1,99 @@ +package api + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "net/http" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/log" +) + +// maxBodySize is the maximum size of body that we will read. +const maxBodySize = 1024 * 1024 + +type CertificateService service + +// Get Returns the certificate and the issuer certificate. +// 'bundle' is only applied if the issuer is provided by the 'up' link. +func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) { + cert, up, err := c.get(certURL) + if err != nil { + return nil, nil, err + } + + // Get issuerCert from bundled response from Let's Encrypt + // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 + _, issuer := pem.Decode(cert) + if issuer != nil { + return cert, issuer, nil + } + + issuer, err = c.getIssuerFromLink(up) + if err != nil { + // If we fail to acquire the issuer cert, return the issued certificate - do not fail. + log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err) + } else if len(issuer) > 0 { + // If bundle is true, we want to return a certificate bundle. + // To do this, we append the issuer cert to the issued cert. + if bundle { + cert = append(cert, issuer...) + } + } + + return cert, issuer, nil +} + +// Revoke Revokes a certificate. +func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error { + _, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil) + return err +} + +// get Returns the certificate and the "up" link. +func (c *CertificateService) get(certURL string) ([]byte, string, error) { + if len(certURL) == 0 { + return nil, "", errors.New("certificate[get]: empty URL") + } + + resp, err := c.core.postAsGet(certURL, nil) + if err != nil { + return nil, "", err + } + + cert, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) + if err != nil { + return nil, "", err + } + + // The issuer certificate link may be supplied via an "up" link + // in the response headers of a new certificate. + // See https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2 + up := getLink(resp.Header, "up") + + return cert, up, err +} + +// getIssuerFromLink requests the issuer certificate +func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) { + if len(up) == 0 { + return nil, nil + } + + log.Infof("acme: Requesting issuer cert from %s", up) + + cert, _, err := c.get(up) + if err != nil { + return nil, err + } + + _, err = x509.ParseCertificate(cert) + if err != nil { + return nil, err + } + + return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert)), nil +} diff --git a/acme/api/certificate_test.go b/acme/api/certificate_test.go new file mode 100644 index 00000000..2c5642e8 --- /dev/null +++ b/acme/api/certificate_test.go @@ -0,0 +1,129 @@ +package api + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/pem" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/platform/tester" +) + +const certResponseMock = `-----BEGIN CERTIFICATE----- +MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD +Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa +Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag +bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5 +y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy +144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3 +BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE +zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO +BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG +A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD +ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4 +jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9 +IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE +HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd +TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri +OPPkKtAKAbQkKbUIfsHpBZjKZMU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw +NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl +NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT +SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh +0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen +SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx +HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt +D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu +mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm +iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd +QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ +wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv +rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 +7R4IbHGnj0BJA2vMYC4hSw== +-----END CERTIFICATE----- +` + +const issuerMock = `-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw +NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl +NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT +SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh +0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen +SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx +HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt +D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu +mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm +iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd +QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ +wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv +rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 +7R4IbHGnj0BJA2vMYC4hSw== +-----END CERTIFICATE----- +` + +func TestCertificateService_Get_issuerRelUp(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/issuer", func(w http.ResponseWriter, r *http.Request) { + p, _ := pem.Decode([]byte(issuerMock)) + _, err := w.Write(p.Bytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) + require.NoError(t, err) + assert.Equal(t, certResponseMock, string(cert), "Certificate") + assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") +} + +func TestCertificateService_Get_embeddedIssuer(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + cert, issuer, err := core.Certificates.Get(apiURL+"/certificate", true) + require.NoError(t, err) + assert.Equal(t, certResponseMock, string(cert), "Certificate") + assert.Equal(t, issuerMock, string(issuer), "IssuerCertificate") +} diff --git a/acme/api/challenge.go b/acme/api/challenge.go new file mode 100644 index 00000000..afcd0aa2 --- /dev/null +++ b/acme/api/challenge.go @@ -0,0 +1,45 @@ +package api + +import ( + "errors" + + "github.com/xenolf/lego/acme" +) + +type ChallengeService service + +// New Creates a challenge. +func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) { + if len(chlgURL) == 0 { + return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL") + } + + // Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`. + // We use an empty struct instance as the postJSON payload here to achieve this result. + var chlng acme.ExtendedChallenge + resp, err := c.core.post(chlgURL, struct{}{}, &chlng) + if err != nil { + return acme.ExtendedChallenge{}, err + } + + chlng.AuthorizationURL = getLink(resp.Header, "up") + chlng.RetryAfter = getRetryAfter(resp) + return chlng, nil +} + +// Get Gets a challenge. +func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) { + if len(chlgURL) == 0 { + return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL") + } + + var chlng acme.ExtendedChallenge + resp, err := c.core.postAsGet(chlgURL, &chlng) + if err != nil { + return acme.ExtendedChallenge{}, err + } + + chlng.AuthorizationURL = getLink(resp.Header, "up") + chlng.RetryAfter = getRetryAfter(resp) + return chlng, nil +} diff --git a/acme/api/internal/nonces/nonce_manager.go b/acme/api/internal/nonces/nonce_manager.go new file mode 100644 index 00000000..20a53010 --- /dev/null +++ b/acme/api/internal/nonces/nonce_manager.go @@ -0,0 +1,78 @@ +package nonces + +import ( + "errors" + "fmt" + "net/http" + "sync" + + "github.com/xenolf/lego/acme/api/internal/sender" +) + +// Manager Manages nonces. +type Manager struct { + do *sender.Doer + nonceURL string + nonces []string + sync.Mutex +} + +// NewManager Creates a new Manager. +func NewManager(do *sender.Doer, nonceURL string) *Manager { + return &Manager{ + do: do, + nonceURL: nonceURL, + } +} + +// Pop Pops a nonce. +func (n *Manager) Pop() (string, bool) { + n.Lock() + defer n.Unlock() + + if len(n.nonces) == 0 { + return "", false + } + + nonce := n.nonces[len(n.nonces)-1] + n.nonces = n.nonces[:len(n.nonces)-1] + return nonce, true +} + +// Push Pushes a nonce. +func (n *Manager) Push(nonce string) { + n.Lock() + defer n.Unlock() + n.nonces = append(n.nonces, nonce) +} + +// Nonce implement jose.NonceSource +func (n *Manager) Nonce() (string, error) { + if nonce, ok := n.Pop(); ok { + return nonce, nil + } + return n.getNonce() +} + +func (n *Manager) getNonce() (string, error) { + resp, err := n.do.Head(n.nonceURL) + if err != nil { + return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %v", err) + } + + return GetFromResponse(resp) +} + +// GetFromResponse Extracts a nonce from a HTTP response. +func GetFromResponse(resp *http.Response) (string, error) { + if resp == nil { + return "", errors.New("nil response") + } + + nonce := resp.Header.Get("Replay-Nonce") + if nonce == "" { + return "", fmt.Errorf("server did not respond with a proper nonce header") + } + + return nonce, nil +} diff --git a/acme/api/internal/nonces/nonce_manager_test.go b/acme/api/internal/nonces/nonce_manager_test.go new file mode 100644 index 00000000..cc007f73 --- /dev/null +++ b/acme/api/internal/nonces/nonce_manager_test.go @@ -0,0 +1,55 @@ +package nonces + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api/internal/sender" + "github.com/xenolf/lego/platform/tester" +) + +func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(250 * time.Millisecond) + w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") + err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + defer ts.Close() + + doer := sender.NewDoer(http.DefaultClient, "lego-test") + j := NewManager(doer, ts.URL) + ch := make(chan bool) + resultCh := make(chan bool) + go func() { + _, errN := j.Nonce() + if errN != nil { + t.Log(errN) + } + ch <- true + }() + go func() { + _, errN := j.Nonce() + if errN != nil { + t.Log(errN) + } + ch <- true + }() + go func() { + <-ch + <-ch + resultCh <- true + }() + select { + case <-resultCh: + case <-time.After(400 * time.Millisecond): + t.Fatal("JWS is probably holding a lock while making HTTP request") + } +} diff --git a/acme/api/internal/secure/jws.go b/acme/api/internal/secure/jws.go new file mode 100644 index 00000000..7645b6e9 --- /dev/null +++ b/acme/api/internal/secure/jws.go @@ -0,0 +1,134 @@ +package secure + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/base64" + "errors" + "fmt" + + "github.com/xenolf/lego/acme/api/internal/nonces" + "gopkg.in/square/go-jose.v2" +) + +// JWS Represents a JWS. +type JWS struct { + privKey crypto.PrivateKey + kid string // Key identifier + nonces *nonces.Manager +} + +// NewJWS Create a new JWS. +func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS { + return &JWS{ + privKey: privateKey, + nonces: nonceManager, + kid: kid, + } +} + +// SetKid Sets a key identifier. +func (j *JWS) SetKid(kid string) { + j.kid = kid +} + +// SignContent Signs a content with the JWS. +func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) { + var alg jose.SignatureAlgorithm + switch k := j.privKey.(type) { + case *rsa.PrivateKey: + alg = jose.RS256 + case *ecdsa.PrivateKey: + if k.Curve == elliptic.P256() { + alg = jose.ES256 + } else if k.Curve == elliptic.P384() { + alg = jose.ES384 + } + } + + signKey := jose.SigningKey{ + Algorithm: alg, + Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid}, + } + + options := jose.SignerOptions{ + NonceSource: j.nonces, + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "url": url, + }, + } + + if j.kid == "" { + options.EmbedJWK = true + } + + signer, err := jose.NewSigner(signKey, &options) + if err != nil { + return nil, fmt.Errorf("failed to create jose signer -> %v", err) + } + + signed, err := signer.Sign(content) + if err != nil { + return nil, fmt.Errorf("failed to sign content -> %v", err) + } + return signed, nil +} + +// SignEABContent Signs an external account binding content with the JWS. +func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { + jwk := jose.JSONWebKey{Key: j.privKey} + jwkJSON, err := jwk.Public().MarshalJSON() + if err != nil { + return nil, fmt.Errorf("acme: error encoding eab jwk key: %v", err) + } + + signer, err := jose.NewSigner( + jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, + &jose.SignerOptions{ + EmbedJWK: false, + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": kid, + "url": url, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %v", err) + } + + signed, err := signer.Sign(jwkJSON) + if err != nil { + return nil, fmt.Errorf("failed to External Account Binding sign content -> %v", err) + } + + return signed, nil +} + +// GetKeyAuthorization Gets the key authorization for a token. +func (j *JWS) GetKeyAuthorization(token string) (string, error) { + var publicKey crypto.PublicKey + switch k := j.privKey.(type) { + case *ecdsa.PrivateKey: + publicKey = k.Public() + case *rsa.PrivateKey: + publicKey = k.Public() + } + + // Generate the Key Authorization for the challenge + jwk := &jose.JSONWebKey{Key: publicKey} + if jwk == nil { + return "", errors.New("could not generate JWK from key") + } + + thumbBytes, err := jwk.Thumbprint(crypto.SHA256) + if err != nil { + return "", err + } + + // unpad the base64URL + keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) + + return token + "." + keyThumb, nil +} diff --git a/acme/api/internal/secure/jws_test.go b/acme/api/internal/secure/jws_test.go new file mode 100644 index 00000000..48100518 --- /dev/null +++ b/acme/api/internal/secure/jws_test.go @@ -0,0 +1,56 @@ +package secure + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api/internal/nonces" + "github.com/xenolf/lego/acme/api/internal/sender" + "github.com/xenolf/lego/platform/tester" +) + +func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(250 * time.Millisecond) + w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") + err := tester.WriteJSONResponse(w, &acme.Challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) + defer ts.Close() + + doer := sender.NewDoer(http.DefaultClient, "lego-test") + j := nonces.NewManager(doer, ts.URL) + ch := make(chan bool) + resultCh := make(chan bool) + go func() { + _, errN := j.Nonce() + if errN != nil { + t.Log(errN) + } + ch <- true + }() + go func() { + _, errN := j.Nonce() + if errN != nil { + t.Log(errN) + } + ch <- true + }() + go func() { + <-ch + <-ch + resultCh <- true + }() + select { + case <-resultCh: + case <-time.After(400 * time.Millisecond): + t.Fatal("JWS is probably holding a lock while making HTTP request") + } +} diff --git a/acme/api/internal/sender/sender.go b/acme/api/internal/sender/sender.go new file mode 100644 index 00000000..5e74e65f --- /dev/null +++ b/acme/api/internal/sender/sender.go @@ -0,0 +1,146 @@ +package sender + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "runtime" + "strings" + + "github.com/xenolf/lego/acme" +) + +type RequestOption func(*http.Request) error + +func contentType(ct string) RequestOption { + return func(req *http.Request) error { + req.Header.Set("Content-Type", ct) + return nil + } +} + +type Doer struct { + httpClient *http.Client + userAgent string +} + +// NewDoer Creates a new Doer. +func NewDoer(client *http.Client, userAgent string) *Doer { + return &Doer{ + httpClient: client, + userAgent: userAgent, + } +} + +// Get performs a GET request with a proper User-Agent string. +// If "response" is not provided, callers should close resp.Body when done reading from it. +func (d *Doer) Get(url string, response interface{}) (*http.Response, error) { + req, err := d.newRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + return d.do(req, response) +} + +// Head performs a HEAD request with a proper User-Agent string. +// The response body (resp.Body) is already closed when this function returns. +func (d *Doer) Head(url string) (*http.Response, error) { + req, err := d.newRequest(http.MethodHead, url, nil) + if err != nil { + return nil, err + } + + return d.do(req, nil) +} + +// Post performs a POST request with a proper User-Agent string. +// If "response" is not provided, callers should close resp.Body when done reading from it. +func (d *Doer) Post(url string, body io.Reader, bodyType string, response interface{}) (*http.Response, error) { + req, err := d.newRequest(http.MethodPost, url, body, contentType(bodyType)) + if err != nil { + return nil, err + } + + return d.do(req, response) +} + +func (d *Doer) newRequest(method, uri string, body io.Reader, opts ...RequestOption) (*http.Request, error) { + req, err := http.NewRequest(method, uri, body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + req.Header.Set("User-Agent", d.formatUserAgent()) + + for _, opt := range opts { + err = opt(req) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + } + + return req, nil +} + +func (d *Doer) do(req *http.Request, response interface{}) (*http.Response, error) { + resp, err := d.httpClient.Do(req) + if err != nil { + return nil, err + } + + if err = checkError(req, resp); err != nil { + return resp, err + } + + if response != nil { + raw, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp, err + } + + defer resp.Body.Close() + + err = json.Unmarshal(raw, response) + if err != nil { + return resp, fmt.Errorf("failed to unmarshal %q to type %T: %v", raw, response, err) + } + } + + return resp, nil +} + +// formatUserAgent builds and returns the User-Agent string to use in requests. +func (d *Doer) formatUserAgent() string { + ua := fmt.Sprintf("%s %s (%s; %s; %s)", d.userAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) + return strings.TrimSpace(ua) +} + +func checkError(req *http.Request, resp *http.Response) error { + if resp.StatusCode >= http.StatusBadRequest { + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("%d :: %s :: %s :: %v", resp.StatusCode, req.Method, req.URL, err) + } + + var errorDetails *acme.ProblemDetails + err = json.Unmarshal(body, &errorDetails) + if err != nil { + return fmt.Errorf("%d ::%s :: %s :: %v :: %s", resp.StatusCode, req.Method, req.URL, err, string(body)) + } + + errorDetails.Method = req.Method + errorDetails.URL = req.URL.String() + + // Check for errors we handle specifically + if errorDetails.HTTPStatus == http.StatusBadRequest && errorDetails.Type == acme.BadNonceErr { + return &acme.NonceError{ProblemDetails: errorDetails} + } + + return errorDetails + } + return nil +} diff --git a/acme/api/internal/sender/sender_test.go b/acme/api/internal/sender/sender_test.go new file mode 100644 index 00000000..34691479 --- /dev/null +++ b/acme/api/internal/sender/sender_test.go @@ -0,0 +1,68 @@ +package sender + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDo_UserAgentOnAllHTTPMethod(t *testing.T) { + var ua, method string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ua = r.Header.Get("User-Agent") + method = r.Method + })) + defer ts.Close() + + doer := NewDoer(http.DefaultClient, "") + + testCases := []struct { + method string + call func(u string) (*http.Response, error) + }{ + { + method: http.MethodGet, + call: func(u string) (*http.Response, error) { + return doer.Get(u, nil) + }, + }, + { + method: http.MethodHead, + call: doer.Head, + }, + { + method: http.MethodPost, + call: func(u string) (*http.Response, error) { + return doer.Post(u, strings.NewReader("falalalala"), "text/plain", nil) + }, + }, + } + + for _, test := range testCases { + t.Run(test.method, func(t *testing.T) { + + _, err := test.call(ts.URL) + require.NoError(t, err) + + assert.Equal(t, test.method, method) + assert.Contains(t, ua, ourUserAgent, "User-Agent") + }) + } +} + +func TestDo_CustomUserAgent(t *testing.T) { + customUA := "MyApp/1.2.3" + doer := NewDoer(http.DefaultClient, customUA) + + ua := doer.formatUserAgent() + assert.Contains(t, ua, ourUserAgent) + assert.Contains(t, ua, customUA) + if strings.HasSuffix(ua, " ") { + t.Errorf("UA should not have trailing spaces; got '%s'", ua) + } + assert.Len(t, strings.Split(ua, " "), 5) +} diff --git a/acme/api/internal/sender/useragent.go b/acme/api/internal/sender/useragent.go new file mode 100644 index 00000000..d2417b23 --- /dev/null +++ b/acme/api/internal/sender/useragent.go @@ -0,0 +1,11 @@ +package sender + +const ( + // ourUserAgent is the User-Agent of this underlying library package. + ourUserAgent = "xenolf-acme" + + // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. + // values: detach|release + // NOTE: Update this with each tagged release. + ourUserAgentComment = "detach" +) diff --git a/acme/api/order.go b/acme/api/order.go new file mode 100644 index 00000000..40e05a25 --- /dev/null +++ b/acme/api/order.go @@ -0,0 +1,65 @@ +package api + +import ( + "encoding/base64" + "errors" + + "github.com/xenolf/lego/acme" +) + +type OrderService service + +// New Creates a new order. +func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) { + var identifiers []acme.Identifier + for _, domain := range domains { + identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain}) + } + + orderReq := acme.Order{Identifiers: identifiers} + + var order acme.Order + resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order) + if err != nil { + return acme.ExtendedOrder{}, err + } + + return acme.ExtendedOrder{ + Location: resp.Header.Get("Location"), + Order: order, + }, nil +} + +// Get Gets an order. +func (o *OrderService) Get(orderURL string) (acme.Order, error) { + if len(orderURL) == 0 { + return acme.Order{}, errors.New("order[get]: empty URL") + } + + var order acme.Order + _, err := o.core.postAsGet(orderURL, &order) + if err != nil { + return acme.Order{}, err + } + + return order, nil +} + +// UpdateForCSR Updates an order for a CSR. +func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.Order, error) { + csrMsg := acme.CSRMessage{ + Csr: base64.RawURLEncoding.EncodeToString(csr), + } + + var order acme.Order + _, err := o.core.post(orderURL, csrMsg, &order) + if err != nil { + return acme.Order{}, err + } + + if order.Status == acme.StatusInvalid { + return acme.Order{}, order.Error + } + + return order, nil +} diff --git a/acme/api/order_test.go b/acme/api/order_test.go new file mode 100644 index 00000000..7115d31b --- /dev/null +++ b/acme/api/order_test.go @@ -0,0 +1,89 @@ +package api + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/platform/tester" + "gopkg.in/square/go-jose.v2" +) + +func TestOrderService_New(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + // small value keeps test fast + privateKey, errK := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, errK, "Could not generate test key") + + mux.HandleFunc("/newOrder", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + body, err := readSignedBody(r, privateKey) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + order := acme.Order{} + err = json.Unmarshal(body, &order) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = tester.WriteJSONResponse(w, acme.Order{ + Status: acme.StatusValid, + Identifiers: order.Identifiers, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + core, err := New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + order, err := core.Orders.New([]string{"example.com"}) + require.NoError(t, err) + + expected := acme.ExtendedOrder{ + Order: acme.Order{ + Status: "valid", + Identifiers: []acme.Identifier{{Type: "dns", Value: "example.com"}}, + }, + } + assert.Equal(t, expected, order) +} + +func readSignedBody(r *http.Request, privateKey *rsa.PrivateKey) ([]byte, error) { + reqBody, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + jws, err := jose.ParseSigned(string(reqBody)) + if err != nil { + return nil, err + } + + body, err := jws.Verify(&jose.JSONWebKey{ + Key: privateKey.Public(), + Algorithm: "RSA", + }) + if err != nil { + return nil, err + } + + return body, nil +} diff --git a/acme/api/service.go b/acme/api/service.go new file mode 100644 index 00000000..ff043bc7 --- /dev/null +++ b/acme/api/service.go @@ -0,0 +1,45 @@ +package api + +import ( + "net/http" + "regexp" +) + +type service struct { + core *Core +} + +// getLink get a rel into the Link header +func getLink(header http.Header, rel string) string { + var linkExpr = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`) + + for _, link := range header["Link"] { + for _, m := range linkExpr.FindAllStringSubmatch(link, -1) { + if len(m) != 3 { + continue + } + if m[2] == rel { + return m[1] + } + } + } + return "" +} + +// getLocation get the value of the header Location +func getLocation(resp *http.Response) string { + if resp == nil { + return "" + } + + return resp.Header.Get("Location") +} + +// getRetryAfter get the value of the header Retry-After +func getRetryAfter(resp *http.Response) string { + if resp == nil { + return "" + } + + return resp.Header.Get("Retry-After") +} diff --git a/acme/api/service_test.go b/acme/api/service_test.go new file mode 100644 index 00000000..d0095a13 --- /dev/null +++ b/acme/api/service_test.go @@ -0,0 +1,56 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getLink(t *testing.T) { + testCases := []struct { + desc string + header http.Header + relName string + expected string + }{ + { + desc: "success", + header: http.Header{ + "Link": []string{`; rel="next", ; rel="up"`}, + }, + relName: "up", + expected: "https://acme-staging-v02.api.letsencrypt.org/up?query", + }, + { + desc: "success several lines", + header: http.Header{ + "Link": []string{`; rel="next"`, `; rel="up"`}, + }, + relName: "up", + expected: "https://acme-staging-v02.api.letsencrypt.org/up?query", + }, + { + desc: "no link", + header: http.Header{}, + relName: "up", + expected: "", + }, + { + desc: "no header", + relName: "up", + expected: "", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + link := getLink(test.header, test.relName) + + assert.Equal(t, test.expected, link) + }) + } +} diff --git a/acme/challenges.go b/acme/challenges.go deleted file mode 100644 index d10f82b8..00000000 --- a/acme/challenges.go +++ /dev/null @@ -1,17 +0,0 @@ -package acme - -// Challenge is a string that identifies a particular type and version of ACME challenge. -type Challenge string - -const ( - // HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http - // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge - HTTP01 = Challenge("http-01") - - // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns - // Note: DNS01Record returns a DNS record which will fulfill this challenge - DNS01 = Challenge("dns-01") - - // TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01 - TLSALPN01 = Challenge("tls-alpn-01") -) diff --git a/acme/client.go b/acme/client.go deleted file mode 100644 index f9857ebd..00000000 --- a/acme/client.go +++ /dev/null @@ -1,957 +0,0 @@ -// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers. -package acme - -import ( - "crypto" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "net" - "regexp" - "strconv" - "strings" - "time" - - "github.com/xenolf/lego/log" -) - -const ( - // maxBodySize is the maximum size of body that we will read. - maxBodySize = 1024 * 1024 - - // overallRequestLimit is the overall number of request per second limited on the - // “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the - // limitation is 20 requests per second, but using 20 as value doesn't work but 18 do - overallRequestLimit = 18 - - statusValid = "valid" - statusInvalid = "invalid" -) - -// User interface is to be implemented by users of this library. -// It is used by the client type to get user specific information. -type User interface { - GetEmail() string - GetRegistration() *RegistrationResource - GetPrivateKey() crypto.PrivateKey -} - -// Interface for all challenge solvers to implement. -type solver interface { - Solve(challenge challenge, domain string) error -} - -// Interface for challenges like dns, where we can set a record in advance for ALL challenges. -// This saves quite a bit of time vs creating the records and solving them serially. -type preSolver interface { - PreSolve(challenge challenge, domain string) error -} - -// Interface for challenges like dns, where we can solve all the challenges before to delete them. -type cleanup interface { - CleanUp(challenge challenge, domain string) error -} - -type validateFunc func(j *jws, domain, uri string, chlng challenge) error - -// Client is the user-friendy way to ACME -type Client struct { - directory directory - user User - jws *jws - keyType KeyType - solvers map[Challenge]solver -} - -// NewClient creates a new ACME client on behalf of the user. The client will depend on -// the ACME directory located at caDirURL for the rest of its actions. A private -// key of type keyType (see KeyType contants) will be generated when requesting a new -// certificate if one isn't provided. -func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { - privKey := user.GetPrivateKey() - if privKey == nil { - return nil, errors.New("private key was nil") - } - - var dir directory - if _, err := getJSON(caDirURL, &dir); err != nil { - return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) - } - - if dir.NewAccountURL == "" { - return nil, errors.New("directory missing new registration URL") - } - if dir.NewOrderURL == "" { - return nil, errors.New("directory missing new order URL") - } - - jws := &jws{privKey: privKey, getNonceURL: dir.NewNonceURL} - if reg := user.GetRegistration(); reg != nil { - jws.kid = reg.URI - } - - // REVIEW: best possibility? - // Add all available solvers with the right index as per ACME - // spec to this map. Otherwise they won`t be found. - solvers := map[Challenge]solver{ - HTTP01: &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}, - TLSALPN01: &tlsALPNChallenge{jws: jws, validate: validate, provider: &TLSALPNProviderServer{}}, - } - - return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil -} - -// SetChallengeProvider specifies a custom provider p that can solve the given challenge type. -func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error { - switch challenge { - case HTTP01: - c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p} - case DNS01: - c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p} - case TLSALPN01: - c.solvers[challenge] = &tlsALPNChallenge{jws: c.jws, validate: validate, provider: p} - default: - return fmt.Errorf("unknown challenge %v", challenge) - } - return nil -} - -// SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges. -// If this option is not used, the default port 80 and all interfaces will be used. -// To only specify a port and no interface use the ":port" notation. -// -// NOTE: This REPLACES any custom HTTP provider previously set by calling -// c.SetChallengeProvider with the default HTTP challenge provider. -func (c *Client) SetHTTPAddress(iface string) error { - host, port, err := net.SplitHostPort(iface) - if err != nil { - return err - } - - if chlng, ok := c.solvers[HTTP01]; ok { - chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port) - } - - return nil -} - -// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges. -// If this option is not used, the default port 443 and all interfaces will be used. -// To only specify a port and no interface use the ":port" notation. -// -// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling -// c.SetChallengeProvider with the default TLS-ALPN challenge provider. -func (c *Client) SetTLSAddress(iface string) error { - host, port, err := net.SplitHostPort(iface) - if err != nil { - return err - } - - if chlng, ok := c.solvers[TLSALPN01]; ok { - chlng.(*tlsALPNChallenge).provider = NewTLSALPNProviderServer(host, port) - } - return nil -} - -// ExcludeChallenges explicitly removes challenges from the pool for solving. -func (c *Client) ExcludeChallenges(challenges []Challenge) { - // Loop through all challenges and delete the requested one if found. - for _, challenge := range challenges { - delete(c.solvers, challenge) - } -} - -// GetToSURL returns the current ToS URL from the Directory -func (c *Client) GetToSURL() string { - return c.directory.Meta.TermsOfService -} - -// GetExternalAccountRequired returns the External Account Binding requirement of the Directory -func (c *Client) GetExternalAccountRequired() bool { - return c.directory.Meta.ExternalAccountRequired -} - -// Register the current account to the ACME server. -func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { - if c == nil || c.user == nil { - return nil, errors.New("acme: cannot register a nil client or user") - } - log.Infof("acme: Registering account for %s", c.user.GetEmail()) - - accMsg := accountMessage{} - if c.user.GetEmail() != "" { - accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} - } else { - accMsg.Contact = []string{} - } - accMsg.TermsOfServiceAgreed = tosAgreed - - var serverReg accountMessage - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) - if err != nil { - remoteErr, ok := err.(RemoteError) - if ok && remoteErr.StatusCode == 409 { - } else { - return nil, err - } - } - - reg := &RegistrationResource{ - URI: hdr.Get("Location"), - Body: serverReg, - } - c.jws.kid = reg.URI - - return reg, nil -} - -// RegisterWithExternalAccountBinding Register the current account to the ACME server. -func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) { - if c == nil || c.user == nil { - return nil, errors.New("acme: cannot register a nil client or user") - } - log.Infof("acme: Registering account (EAB) for %s", c.user.GetEmail()) - - accMsg := accountMessage{} - if c.user.GetEmail() != "" { - accMsg.Contact = []string{"mailto:" + c.user.GetEmail()} - } else { - accMsg.Contact = []string{} - } - accMsg.TermsOfServiceAgreed = tosAgreed - - hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded) - if err != nil { - return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error()) - } - - eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac) - if err != nil { - return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error()) - } - - eabPayload := eabJWS.FullSerialize() - - accMsg.ExternalAccountBinding = []byte(eabPayload) - - var serverReg accountMessage - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg) - if err != nil { - remoteErr, ok := err.(RemoteError) - if ok && remoteErr.StatusCode == 409 { - } else { - return nil, err - } - } - - reg := &RegistrationResource{ - URI: hdr.Get("Location"), - Body: serverReg, - } - c.jws.kid = reg.URI - - return reg, nil -} - -// ResolveAccountByKey will attempt to look up an account using the given account key -// and return its registration resource. -func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { - log.Infof("acme: Trying to resolve account by key") - - acc := accountMessage{OnlyReturnExisting: true} - hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil) - if err != nil { - return nil, err - } - - accountLink := hdr.Get("Location") - if accountLink == "" { - return nil, errors.New("Server did not return the account link") - } - - var retAccount accountMessage - c.jws.kid = accountLink - _, err = postJSON(c.jws, accountLink, accountMessage{}, &retAccount) - if err != nil { - return nil, err - } - - return &RegistrationResource{URI: accountLink, Body: retAccount}, nil -} - -// DeleteRegistration deletes the client's user registration from the ACME -// server. -func (c *Client) DeleteRegistration() error { - if c == nil || c.user == nil { - return errors.New("acme: cannot unregister a nil client or user") - } - log.Infof("acme: Deleting account for %s", c.user.GetEmail()) - - accMsg := accountMessage{ - Status: "deactivated", - } - - _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, nil) - return err -} - -// QueryRegistration runs a POST request on the client's registration and -// returns the result. -// -// This is similar to the Register function, but acting on an existing -// registration link and resource. -func (c *Client) QueryRegistration() (*RegistrationResource, error) { - if c == nil || c.user == nil { - return nil, errors.New("acme: cannot query the registration of a nil client or user") - } - // Log the URL here instead of the email as the email may not be set - log.Infof("acme: Querying account for %s", c.user.GetRegistration().URI) - - accMsg := accountMessage{} - - var serverReg accountMessage - _, err := postJSON(c.jws, c.user.GetRegistration().URI, accMsg, &serverReg) - if err != nil { - return nil, err - } - - reg := &RegistrationResource{Body: serverReg} - - // Location: header is not returned so this needs to be populated off of - // existing URI - reg.URI = c.user.GetRegistration().URI - - return reg, nil -} - -// ObtainCertificateForCSR tries to obtain a certificate matching the CSR passed into it. -// The domains are inferred from the CommonName and SubjectAltNames, if any. The private key -// for this CSR is not required. -// If bundle is true, the []byte contains both the issuer certificate and -// your issued certificate as a bundle. -// This function will never return a partial certificate. If one domain in the list fails, -// the whole certificate will fail. -func (c *Client) ObtainCertificateForCSR(csr x509.CertificateRequest, bundle bool) (*CertificateResource, error) { - // figure out what domains it concerns - // start with the common name - domains := []string{csr.Subject.CommonName} - - // loop over the SubjectAltName DNS names -DNSNames: - for _, sanName := range csr.DNSNames { - for _, existingName := range domains { - if existingName == sanName { - // duplicate; skip this name - continue DNSNames - } - } - - // name is unique - domains = append(domains, sanName) - } - - if bundle { - log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) - } else { - log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) - } - - order, err := c.createOrderForIdentifiers(domains) - if err != nil { - return nil, err - } - authz, err := c.getAuthzForOrder(order) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - /*for _, auth := range authz { - c.disableAuthz(auth) - }*/ - return nil, err - } - - err = c.solveChallengeForAuthz(authz) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - return nil, err - } - - log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - failures := make(ObtainError) - cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil) - if err != nil { - for _, chln := range authz { - failures[chln.Identifier.Value] = err - } - } - - if cert != nil { - // Add the CSR to the certificate so that it can be used for renewals. - cert.CSR = pemEncode(&csr) - } - - // do not return an empty failures map, because - // it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil -} - -// ObtainCertificate tries to obtain a single certificate using all domains passed into it. -// The first domain in domains is used for the CommonName field of the certificate, all other -// domains are added using the Subject Alternate Names extension. A new private key is generated -// for every invocation of this function. If you do not want that you can supply your own private key -// in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one. -// If bundle is true, the []byte contains both the issuer certificate and -// your issued certificate as a bundle. -// This function will never return a partial certificate. If one domain in the list fails, -// the whole certificate will fail. -func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { - if len(domains) == 0 { - return nil, errors.New("no domains to obtain a certificate for") - } - - if bundle { - log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) - } else { - log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) - } - - order, err := c.createOrderForIdentifiers(domains) - if err != nil { - return nil, err - } - authz, err := c.getAuthzForOrder(order) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - /*for _, auth := range authz { - c.disableAuthz(auth) - }*/ - return nil, err - } - - err = c.solveChallengeForAuthz(authz) - if err != nil { - // If any challenge fails, return. Do not generate partial SAN certificates. - return nil, err - } - - log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) - - failures := make(ObtainError) - cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple) - if err != nil { - for _, auth := range authz { - failures[auth.Identifier.Value] = err - } - } - - // do not return an empty failures map, because - // it would still be a non-nil error value - if len(failures) > 0 { - return cert, failures - } - return cert, nil -} - -// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. -func (c *Client) RevokeCertificate(certificate []byte) error { - certificates, err := parsePEMBundle(certificate) - if err != nil { - return err - } - - x509Cert := certificates[0] - if x509Cert.IsCA { - return fmt.Errorf("Certificate bundle starts with a CA certificate") - } - - encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) - - _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Certificate: encodedCert}, nil) - return err -} - -// RenewCertificate takes a CertificateResource and tries to renew the certificate. -// If the renewal process succeeds, the new certificate will ge returned in a new CertResource. -// Please be aware that this function will return a new certificate in ANY case that is not an error. -// If the server does not provide us with a new cert on a GET request to the CertURL -// this function will start a new-cert flow where a new certificate gets generated. -// If bundle is true, the []byte contains both the issuer certificate and -// your issued certificate as a bundle. -// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. -func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple bool) (*CertificateResource, error) { - // Input certificate is PEM encoded. Decode it here as we may need the decoded - // cert later on in the renewal process. The input may be a bundle or a single certificate. - certificates, err := parsePEMBundle(cert.Certificate) - if err != nil { - return nil, err - } - - x509Cert := certificates[0] - if x509Cert.IsCA { - return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) - } - - // This is just meant to be informal for the user. - timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) - log.Infof("[%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) - - // We always need to request a new certificate to renew. - // Start by checking to see if the certificate was based off a CSR, and - // use that if it's defined. - if len(cert.CSR) > 0 { - csr, errP := pemDecodeTox509CSR(cert.CSR) - if errP != nil { - return nil, errP - } - newCert, failures := c.ObtainCertificateForCSR(*csr, bundle) - return newCert, failures - } - - var privKey crypto.PrivateKey - if cert.PrivateKey != nil { - privKey, err = parsePEMPrivateKey(cert.PrivateKey) - if err != nil { - return nil, err - } - } - - var domains []string - // check for SAN certificate - if len(x509Cert.DNSNames) > 1 { - domains = append(domains, x509Cert.Subject.CommonName) - for _, sanDomain := range x509Cert.DNSNames { - if sanDomain == x509Cert.Subject.CommonName { - continue - } - domains = append(domains, sanDomain) - } - } else { - domains = append(domains, x509Cert.Subject.CommonName) - } - - newCert, err := c.ObtainCertificate(domains, bundle, privKey, mustStaple) - return newCert, err -} - -func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) { - var identifiers []identifier - for _, domain := range domains { - identifiers = append(identifiers, identifier{Type: "dns", Value: domain}) - } - - order := orderMessage{ - Identifiers: identifiers, - } - - var response orderMessage - hdr, err := postJSON(c.jws, c.directory.NewOrderURL, order, &response) - if err != nil { - return orderResource{}, err - } - - orderRes := orderResource{ - URL: hdr.Get("Location"), - Domains: domains, - orderMessage: response, - } - return orderRes, nil -} - -// an authz with the solver we have chosen and the index of the challenge associated with it -type selectedAuthSolver struct { - authz authorization - challengeIndex int - solver solver -} - -// Looks through the challenge combinations to find a solvable match. -// Then solves the challenges in series and returns. -func (c *Client) solveChallengeForAuthz(authorizations []authorization) error { - failures := make(ObtainError) - - authSolvers := []*selectedAuthSolver{} - - // loop through the resources, basically through the domains. First pass just selects a solver for each authz. - for _, authz := range authorizations { - if authz.Status == statusValid { - // Boulder might recycle recent validated authz (see issue #267) - log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value) - continue - } - if i, solvr := c.chooseSolver(authz, authz.Identifier.Value); solvr != nil { - authSolvers = append(authSolvers, &selectedAuthSolver{ - authz: authz, - challengeIndex: i, - solver: solvr, - }) - } else { - failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value) - } - } - - // for all valid presolvers, first submit the challenges so they have max time to propagate - for _, item := range authSolvers { - authz := item.authz - i := item.challengeIndex - if presolver, ok := item.solver.(preSolver); ok { - if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil { - failures[authz.Identifier.Value] = err - } - } - } - - defer func() { - // clean all created TXT records - for _, item := range authSolvers { - if clean, ok := item.solver.(cleanup); ok { - if failures[item.authz.Identifier.Value] != nil { - // already failed in previous loop - continue - } - err := clean.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value) - if err != nil { - log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err) - } - } - } - }() - - // finally solve all challenges for real - for _, item := range authSolvers { - authz := item.authz - i := item.challengeIndex - if failures[authz.Identifier.Value] != nil { - // already failed in previous loop - continue - } - if err := item.solver.Solve(authz.Challenges[i], authz.Identifier.Value); err != nil { - failures[authz.Identifier.Value] = err - } - } - - // be careful not to return an empty failures map, for - // even an empty ObtainError is a non-nil error value - if len(failures) > 0 { - return failures - } - return nil -} - -// Checks all challenges from the server in order and returns the first matching solver. -func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) { - for i, challenge := range auth.Challenges { - if solver, ok := c.solvers[Challenge(challenge.Type)]; ok { - return i, solver - } - log.Infof("[%s] acme: Could not find solver for: %s", domain, challenge.Type) - } - return 0, nil -} - -// Get the challenges needed to proof our identifier to the ACME server. -func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error) { - resc, errc := make(chan authorization), make(chan domainError) - - delay := time.Second / overallRequestLimit - - for _, authzURL := range order.Authorizations { - time.Sleep(delay) - - go func(authzURL string) { - var authz authorization - _, err := postAsGet(c.jws, authzURL, &authz) - if err != nil { - errc <- domainError{Domain: authz.Identifier.Value, Error: err} - return - } - - resc <- authz - }(authzURL) - } - - var responses []authorization - failures := make(ObtainError) - for i := 0; i < len(order.Authorizations); i++ { - select { - case res := <-resc: - responses = append(responses, res) - case err := <-errc: - failures[err.Domain] = err.Error - } - } - - logAuthz(order) - - close(resc) - close(errc) - - // be careful to not return an empty failures map; - // even if empty, they become non-nil error values - if len(failures) > 0 { - return responses, failures - } - return responses, nil -} - -func logAuthz(order orderResource) { - for i, auth := range order.Authorizations { - log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth) - } -} - -// cleanAuthz loops through the passed in slice and disables any auths which are not "valid" -func (c *Client) disableAuthz(authURL string) error { - var disabledAuth authorization - _, err := postJSON(c.jws, authURL, deactivateAuthMessage{Status: "deactivated"}, &disabledAuth) - return err -} - -func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) { - - var err error - if privKey == nil { - privKey, err = generatePrivateKey(c.keyType) - if err != nil { - return nil, err - } - } - - // determine certificate name(s) based on the authorization resources - commonName := order.Domains[0] - - // ACME draft Section 7.4 "Applying for Certificate Issuance" - // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4 - // says: - // Clients SHOULD NOT make any assumptions about the sort order of - // "identifiers" or "authorizations" elements in the returned order - // object. - san := []string{commonName} - for _, auth := range order.Identifiers { - if auth.Value != commonName { - san = append(san, auth.Value) - } - } - - // TODO: should the CSR be customizable? - csr, err := generateCsr(privKey, commonName, san, mustStaple) - if err != nil { - return nil, err - } - - return c.requestCertificateForCsr(order, bundle, csr, pemEncode(privKey)) -} - -func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr []byte, privateKeyPem []byte) (*CertificateResource, error) { - commonName := order.Domains[0] - - csrString := base64.RawURLEncoding.EncodeToString(csr) - var retOrder orderMessage - _, err := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder) - if err != nil { - return nil, err - } - - if retOrder.Status == statusInvalid { - return nil, err - } - - certRes := CertificateResource{ - Domain: commonName, - CertURL: retOrder.Certificate, - PrivateKey: privateKeyPem, - } - - if retOrder.Status == statusValid { - // if the certificate is available right away, short cut! - ok, err := c.checkCertResponse(retOrder, &certRes, bundle) - if err != nil { - return nil, err - } - - if ok { - return &certRes, nil - } - } - - stopTimer := time.NewTimer(30 * time.Second) - defer stopTimer.Stop() - retryTick := time.NewTicker(500 * time.Millisecond) - defer retryTick.Stop() - - for { - select { - case <-stopTimer.C: - return nil, errors.New("certificate polling timed out") - case <-retryTick.C: - _, err := postAsGet(c.jws, order.URL, &retOrder) - if err != nil { - return nil, err - } - - done, err := c.checkCertResponse(retOrder, &certRes, bundle) - if err != nil { - return nil, err - } - if done { - return &certRes, nil - } - } - } -} - -// checkCertResponse checks to see if the certificate is ready and a link is contained in the -// response. if so, loads it into certRes and returns true. If the cert -// is not yet ready, it returns false. The certRes input -// should already have the Domain (common name) field populated. If bundle is -// true, the certificate will be bundled with the issuer's cert. -func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) { - switch order.Status { - case statusValid: - resp, err := postAsGet(c.jws, order.Certificate, nil) - if err != nil { - return false, err - } - - cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return false, err - } - - // The issuer certificate link may be supplied via an "up" link - // in the response headers of a new certificate. See - // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2 - links := parseLinks(resp.Header["Link"]) - if link, ok := links["up"]; ok { - issuerCert, err := c.getIssuerCertificate(link) - - if err != nil { - // If we fail to acquire the issuer cert, return the issued certificate - do not fail. - log.Warnf("[%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err) - } else { - issuerCert = pemEncode(derCertificateBytes(issuerCert)) - - // If bundle is true, we want to return a certificate bundle. - // To do this, we append the issuer cert to the issued cert. - if bundle { - cert = append(cert, issuerCert...) - } - - certRes.IssuerCertificate = issuerCert - } - } else { - // Get issuerCert from bundled response from Let's Encrypt - // See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962 - _, rest := pem.Decode(cert) - if rest != nil { - certRes.IssuerCertificate = rest - } - } - - certRes.Certificate = cert - certRes.CertURL = order.Certificate - certRes.CertStableURL = order.Certificate - log.Infof("[%s] Server responded with a certificate.", certRes.Domain) - return true, nil - - case "processing": - return false, nil - case statusInvalid: - return false, errors.New("order has invalid state: invalid") - default: - return false, nil - } -} - -// getIssuerCertificate requests the issuer certificate -func (c *Client) getIssuerCertificate(url string) ([]byte, error) { - log.Infof("acme: Requesting issuer cert from %s", url) - resp, err := postAsGet(c.jws, url, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return nil, err - } - - _, err = x509.ParseCertificate(issuerBytes) - if err != nil { - return nil, err - } - - return issuerBytes, err -} - -func parseLinks(links []string) map[string]string { - aBrkt := regexp.MustCompile("[<>]") - slver := regexp.MustCompile("(.+) *= *\"(.+)\"") - linkMap := make(map[string]string) - - for _, link := range links { - - link = aBrkt.ReplaceAllString(link, "") - parts := strings.Split(link, ";") - - matches := slver.FindStringSubmatch(parts[1]) - if len(matches) > 0 { - linkMap[matches[2]] = parts[0] - } - } - - return linkMap -} - -// validate makes the ACME server start validating a -// challenge response, only returning once it is done. -func validate(j *jws, domain, uri string, c challenge) error { - var chlng challenge - - // Challenge initiation is done by sending a JWS payload containing the - // trivial JSON object `{}`. We use an empty struct instance as the postJSON - // payload here to achieve this result. - hdr, err := postJSON(j, uri, struct{}{}, &chlng) - if err != nil { - return err - } - - // After the path is sent, the ACME server will access our server. - // Repeatedly check the server for an updated status on our request. - for { - switch chlng.Status { - case statusValid: - log.Infof("[%s] The server validated our request", domain) - return nil - case "pending": - case "processing": - case statusInvalid: - return handleChallengeError(chlng) - default: - return errors.New("the server returned an unexpected state") - } - - ra, err := strconv.Atoi(hdr.Get("Retry-After")) - if err != nil { - // The ACME server MUST return a Retry-After. - // If it doesn't, we'll just poll hard. - ra = 5 - } - - time.Sleep(time.Duration(ra) * time.Second) - - resp, err := postAsGet(j, uri, &chlng) - if err != nil { - return err - } - if resp != nil { - hdr = resp.Header - } - } -} diff --git a/acme/client_test.go b/acme/client_test.go deleted file mode 100644 index bb596447..00000000 --- a/acme/client_test.go +++ /dev/null @@ -1,378 +0,0 @@ -package acme - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/square/go-jose.v2" -) - -func TestNewClient(t *testing.T) { - keyBits := 32 // small value keeps test fast - keyType := RSA2048 - key, err := rsa.GenerateKey(rand.Reader, keyBits) - require.NoError(t, err, "Could not generate test key") - - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{ - NewNonceURL: "http://test", - NewAccountURL: "http://test", - NewOrderURL: "http://test", - RevokeCertURL: "http://test", - KeyChangeURL: "http://test", - }) - - _, err = w.Write(data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - })) - - client, err := NewClient(ts.URL, user, keyType) - require.NoError(t, err, "Could not create client") - - require.NotNil(t, client.jws, "client.jws") - assert.Equal(t, key, client.jws.privKey, "client.jws.privKey") - assert.Equal(t, keyType, client.keyType, "client.keyType") - assert.Len(t, client.solvers, 2, "solvers") -} - -func TestClientOptPort(t *testing.T) { - keyBits := 32 // small value keeps test fast - key, err := rsa.GenerateKey(rand.Reader, keyBits) - require.NoError(t, err, "Could not generate test key") - - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, _ := json.Marshal(directory{ - NewNonceURL: "http://test", - NewAccountURL: "http://test", - NewOrderURL: "http://test", - RevokeCertURL: "http://test", - KeyChangeURL: "http://test", - }) - - _, err = w.Write(data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - })) - - optPort := "1234" - optHost := "" - - client, err := NewClient(ts.URL, user, RSA2048) - require.NoError(t, err, "Could not create client") - - err = client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - require.NoError(t, err) - - require.IsType(t, &httpChallenge{}, client.solvers[HTTP01]) - httpSolver := client.solvers[HTTP01].(*httpChallenge) - - assert.Equal(t, httpSolver.jws, client.jws, "Expected http-01 to have same jws as client") - - httpProviderServer := httpSolver.provider.(*HTTPProviderServer) - assert.Equal(t, optPort, httpProviderServer.port, "port") - assert.Equal(t, optHost, httpProviderServer.iface, "iface") - - // test setting different host - optHost = "127.0.0.1" - err = client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) - require.NoError(t, err) - - assert.Equal(t, optHost, httpSolver.provider.(*HTTPProviderServer).iface, "iface") -} - -func TestNotHoldingLockWhileMakingHTTPRequests(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(250 * time.Millisecond) - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - writeJSONResponse(w, &challenge{Type: "http-01", Status: "Valid", URL: "http://example.com/", Token: "token"}) - })) - defer ts.Close() - - privKey, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err) - - j := &jws{privKey: privKey, getNonceURL: ts.URL} - ch := make(chan bool) - resultCh := make(chan bool) - go func() { - _, errN := j.Nonce() - if errN != nil { - t.Log(errN) - } - ch <- true - }() - go func() { - _, errN := j.Nonce() - if errN != nil { - t.Log(errN) - } - ch <- true - }() - go func() { - <-ch - <-ch - resultCh <- true - }() - select { - case <-resultCh: - case <-time.After(400 * time.Millisecond): - t.Fatal("JWS is probably holding a lock while making HTTP request") - } -} - -func TestValidate(t *testing.T) { - var statuses []string - - privKey, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err) - - // validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body. - // If there is an error doing this, - // or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned. - // We use this to verify challenge POSTs to the ts below do not send a JWS body. - validateNoBody := func(r *http.Request) error { - reqBody, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - jws, err := jose.ParseSigned(string(reqBody)) - if err != nil { - return err - } - - body, err := jws.Verify(&jose.JSONWebKey{ - Key: privKey.Public(), - Algorithm: "RSA", - }) - if err != nil { - return err - } - - if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" { - return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr) - } - return nil - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Minimal stub ACME server for validation. - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - - switch r.Method { - case http.MethodHead: - case http.MethodPost: - if err := validateNoBody(r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - st := statuses[0] - statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) - - case http.MethodGet: - st := statuses[0] - statuses = statuses[1:] - writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"}) - - default: - http.Error(w, r.Method, http.StatusMethodNotAllowed) - } - })) - defer ts.Close() - - j := &jws{privKey: privKey, getNonceURL: ts.URL} - - testCases := []struct { - name string - statuses []string - want string - }{ - { - name: "POST-unexpected", - statuses: []string{"weird"}, - want: "unexpected", - }, - { - name: "POST-valid", - statuses: []string{"valid"}, - }, - { - name: "POST-invalid", - statuses: []string{"invalid"}, - want: "Error", - }, - { - name: "GET-unexpected", - statuses: []string{"pending", "weird"}, - want: "unexpected", - }, - { - name: "GET-valid", - statuses: []string{"pending", "valid"}, - }, - { - name: "GET-invalid", - statuses: []string{"pending", "invalid"}, - want: "Error", - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - statuses = test.statuses - - err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}) - if test.want == "" { - require.NoError(t, err) - } else { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.want) - } - }) - } -} - -func TestGetChallenges(t *testing.T) { - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet, http.MethodHead: - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - writeJSONResponse(w, directory{ - NewNonceURL: ts.URL, - NewAccountURL: ts.URL, - NewOrderURL: ts.URL, - RevokeCertURL: ts.URL, - KeyChangeURL: ts.URL, - }) - case http.MethodPost: - writeJSONResponse(w, orderMessage{}) - } - })) - defer ts.Close() - - keyBits := 512 // small value keeps test fast - keyType := RSA2048 - - key, err := rsa.GenerateKey(rand.Reader, keyBits) - require.NoError(t, err, "Could not generate test key") - - user := mockUser{ - email: "test@test.com", - regres: &RegistrationResource{URI: ts.URL}, - privatekey: key, - } - - client, err := NewClient(ts.URL, user, keyType) - require.NoError(t, err, "Could not create client") - - _, err = client.createOrderForIdentifiers([]string{"example.com"}) - require.NoError(t, err) -} - -func TestResolveAccountByKey(t *testing.T) { - keyBits := 512 - keyType := RSA2048 - - key, err := rsa.GenerateKey(rand.Reader, keyBits) - require.NoError(t, err, "Could not generate test key") - - user := mockUser{ - email: "test@test.com", - regres: new(RegistrationResource), - privatekey: key, - } - - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - case "/directory": - writeJSONResponse(w, directory{ - NewNonceURL: ts.URL + "/nonce", - NewAccountURL: ts.URL + "/account", - NewOrderURL: ts.URL + "/newOrder", - RevokeCertURL: ts.URL + "/revokeCert", - KeyChangeURL: ts.URL + "/keyChange", - }) - case "/nonce": - w.Header().Add("Replay-Nonce", "12345") - w.Header().Add("Retry-After", "0") - case "/account": - w.Header().Set("Location", ts.URL+"/account_recovery") - case "/account_recovery": - writeJSONResponse(w, accountMessage{ - Status: "valid", - }) - } - })) - - client, err := NewClient(ts.URL+"/directory", user, keyType) - require.NoError(t, err, "Could not create client") - - res, err := client.ResolveAccountByKey() - require.NoError(t, err, "Unexpected error resolving account by key") - - assert.Equal(t, "valid", res.Body.Status, "Unexpected account status") -} - -// writeJSONResponse marshals the body as JSON and writes it to the response. -func writeJSONResponse(w http.ResponseWriter, body interface{}) { - bs, err := json.Marshal(body) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write(bs); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -// stubValidate is like validate, except it does nothing. -func stubValidate(_ *jws, _, _ string, _ challenge) error { - return nil -} - -type mockUser struct { - email string - regres *RegistrationResource - privatekey *rsa.PrivateKey -} - -func (u mockUser) GetEmail() string { return u.email } -func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } -func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } diff --git a/acme/commons.go b/acme/commons.go new file mode 100644 index 00000000..c4493696 --- /dev/null +++ b/acme/commons.go @@ -0,0 +1,284 @@ +// Package acme contains all objects related the ACME endpoints. +// https://tools.ietf.org/html/draft-ietf-acme-acme-16 +package acme + +import ( + "encoding/json" + "time" +) + +// Challenge statuses +// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.6 +const ( + StatusPending = "pending" + StatusInvalid = "invalid" + StatusValid = "valid" + StatusProcessing = "processing" + StatusDeactivated = "deactivated" + StatusExpired = "expired" + StatusRevoked = "revoked" +) + +// Directory the ACME directory object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1 +type Directory struct { + NewNonceURL string `json:"newNonce"` + NewAccountURL string `json:"newAccount"` + NewOrderURL string `json:"newOrder"` + NewAuthzURL string `json:"newAuthz"` + RevokeCertURL string `json:"revokeCert"` + KeyChangeURL string `json:"keyChange"` + Meta Meta `json:"meta"` +} + +// Meta the ACME meta object (related to Directory). +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.1 +type Meta struct { + // termsOfService (optional, string): + // A URL identifying the current terms of service. + TermsOfService string `json:"termsOfService"` + + // website (optional, string): + // An HTTP or HTTPS URL locating a website providing more information about the ACME server. + Website string `json:"website"` + + // caaIdentities (optional, array of string): + // The hostnames that the ACME server recognizes as referring to itself + // for the purposes of CAA record validation as defined in [RFC6844]. + // Each string MUST represent the same sequence of ASCII code points + // that the server will expect to see as the "Issuer Domain Name" in a CAA issue or issuewild property tag. + // This allows clients to determine the correct issuer domain name to use when configuring CAA records. + CaaIdentities []string `json:"caaIdentities"` + + // externalAccountRequired (optional, boolean): + // If this field is present and set to "true", + // then the CA requires that all new- account requests include an "externalAccountBinding" field + // associating the new account with an external account. + ExternalAccountRequired bool `json:"externalAccountRequired"` +} + +// ExtendedAccount a extended Account. +type ExtendedAccount struct { + Account + // Contains the value of the response header `Location` + Location string `json:"-"` +} + +// Account the ACME account Object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.2 +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3 +type Account struct { + // status (required, string): + // The status of this account. + // Possible values are: "valid", "deactivated", and "revoked". + // The value "deactivated" should be used to indicate client-initiated deactivation + // whereas "revoked" should be used to indicate server- initiated deactivation. (See Section 7.1.6) + Status string `json:"status,omitempty"` + + // contact (optional, array of string): + // An array of URLs that the server can use to contact the client for issues related to this account. + // For example, the server may wish to notify the client about server-initiated revocation or certificate expiration. + // For information on supported URL schemes, see Section 7.3 + Contact []string `json:"contact,omitempty"` + + // termsOfServiceAgreed (optional, boolean): + // Including this field in a new-account request, + // with a value of true, indicates the client's agreement with the terms of service. + // This field is not updateable by the client. + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + + // orders (required, string): + // A URL from which a list of orders submitted by this account can be fetched via a POST-as-GET request, + // as described in Section 7.1.2.1. + Orders string `json:"orders,omitempty"` + + // onlyReturnExisting (optional, boolean): + // If this field is present with the value "true", + // then the server MUST NOT create a new account if one does not already exist. + // This allows a client to look up an account URL based on an account key (see Section 7.3.1). + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + + // externalAccountBinding (optional, object): + // An optional field for binding the new account with an existing non-ACME account (see Section 7.3.4). + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` +} + +// ExtendedOrder a extended Order. +type ExtendedOrder struct { + Order + // The order URL, contains the value of the response header `Location` + Location string `json:"-"` +} + +// Order the ACME order Object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.3 +type Order struct { + // status (required, string): + // The status of this order. + // Possible values are: "pending", "ready", "processing", "valid", and "invalid". + Status string `json:"status,omitempty"` + + // expires (optional, string): + // The timestamp after which the server will consider this order invalid, + // encoded in the format specified in RFC 3339 [RFC3339]. + // This field is REQUIRED for objects with "pending" or "valid" in the status field. + Expires string `json:"expires,omitempty"` + + // identifiers (required, array of object): + // An array of identifier objects that the order pertains to. + Identifiers []Identifier `json:"identifiers"` + + // notBefore (optional, string): + // The requested value of the notBefore field in the certificate, + // in the date format defined in [RFC3339]. + NotBefore string `json:"notBefore,omitempty"` + + // notAfter (optional, string): + // The requested value of the notAfter field in the certificate, + // in the date format defined in [RFC3339]. + NotAfter string `json:"notAfter,omitempty"` + + // error (optional, object): + // The error that occurred while processing the order, if any. + // This field is structured as a problem document [RFC7807]. + Error *ProblemDetails `json:"error,omitempty"` + + // authorizations (required, array of string): + // For pending orders, + // the authorizations that the client needs to complete before the requested certificate can be issued (see Section 7.5), + // including unexpired authorizations that the client has completed in the past for identifiers specified in the order. + // The authorizations required are dictated by server policy + // and there may not be a 1:1 relationship between the order identifiers and the authorizations required. + // For final orders (in the "valid" or "invalid" state), the authorizations that were completed. + // Each entry is a URL from which an authorization can be fetched with a POST-as-GET request. + Authorizations []string `json:"authorizations,omitempty"` + + // finalize (required, string): + // A URL that a CSR must be POSTed to once all of the order's authorizations are satisfied to finalize the order. + // The result of a successful finalization will be the population of the certificate URL for the order. + Finalize string `json:"finalize,omitempty"` + + // certificate (optional, string): + // A URL for the certificate that has been issued in response to this order + Certificate string `json:"certificate,omitempty"` +} + +// Authorization the ACME authorization object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4 +type Authorization struct { + // status (required, string): + // The status of this authorization. + // Possible values are: "pending", "valid", "invalid", "deactivated", "expired", and "revoked". + Status string `json:"status"` + + // expires (optional, string): + // The timestamp after which the server will consider this authorization invalid, + // encoded in the format specified in RFC 3339 [RFC3339]. + // This field is REQUIRED for objects with "valid" in the "status" field. + Expires time.Time `json:"expires,omitempty"` + + // identifier (required, object): + // The identifier that the account is authorized to represent + Identifier Identifier `json:"identifier,omitempty"` + + // challenges (required, array of objects): + // For pending authorizations, the challenges that the client can fulfill in order to prove possession of the identifier. + // For valid authorizations, the challenge that was validated. + // For invalid authorizations, the challenge that was attempted and failed. + // Each array entry is an object with parameters required to validate the challenge. + // A client should attempt to fulfill one of these challenges, + // and a server should consider any one of the challenges sufficient to make the authorization valid. + Challenges []Challenge `json:"challenges,omitempty"` + + // wildcard (optional, boolean): + // For authorizations created as a result of a newOrder request containing a DNS identifier + // with a value that contained a wildcard prefix this field MUST be present, and true. + Wildcard bool `json:"wildcard,omitempty"` +} + +// ExtendedChallenge a extended Challenge. +type ExtendedChallenge struct { + Challenge + // Contains the value of the response header `Retry-After` + RetryAfter string `json:"-"` + // Contains the value of the response header `Link` rel="up" + AuthorizationURL string `json:"-"` +} + +// Challenge the ACME challenge object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.5 +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8 +type Challenge struct { + // type (required, string): + // The type of challenge encoded in the object. + Type string `json:"type"` + + // url (required, string): + // The URL to which a response can be posted. + URL string `json:"url"` + + // status (required, string): + // The status of this challenge. Possible values are: "pending", "processing", "valid", and "invalid". + Status string `json:"status"` + + // validated (optional, string): + // The time at which the server validated this challenge, + // encoded in the format specified in RFC 3339 [RFC3339]. + // This field is REQUIRED if the "status" field is "valid". + Validated time.Time `json:"validated,omitempty"` + + // error (optional, object): + // Error that occurred while the server was validating the challenge, if any, + // structured as a problem document [RFC7807]. + // Multiple errors can be indicated by using subproblems Section 6.7.1. + // A challenge object with an error MUST have status equal to "invalid". + Error *ProblemDetails `json:"error,omitempty"` + + // token (required, string): + // A random value that uniquely identifies the challenge. + // This value MUST have at least 128 bits of entropy. + // It MUST NOT contain any characters outside the base64url alphabet, + // and MUST NOT include base64 padding characters ("="). + // See [RFC4086] for additional information on randomness requirements. + // https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3 + // https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4 + Token string `json:"token"` + + // https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.1 + KeyAuthorization string `json:"keyAuthorization"` +} + +// Identifier the ACME identifier object. +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-9.7.7 +type Identifier struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// CSRMessage Certificate Signing Request +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.4 +type CSRMessage struct { + // csr (required, string): + // A CSR encoding the parameters for the certificate being requested [RFC2986]. + // The CSR is sent in the base64url-encoded version of the DER format. + // (Note: Because this field uses base64url, and does not include headers, it is different from PEM.). + Csr string `json:"csr"` +} + +// RevokeCertMessage a certificate revocation message +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.6 +// - https://tools.ietf.org/html/rfc5280#section-5.3.1 +type RevokeCertMessage struct { + // certificate (required, string): + // The certificate to be revoked, in the base64url-encoded version of the DER format. + // (Note: Because this field uses base64url, and does not include headers, it is different from PEM.) + Certificate string `json:"certificate"` + + // reason (optional, int): + // One of the revocation reasonCodes defined in Section 5.3.1 of [RFC5280] to be used when generating OCSP responses and CRLs. + // If this field is not set the server SHOULD omit the reasonCode CRL entry extension when generating OCSP responses and CRLs. + // The server MAY disallow a subset of reasonCodes from being used by the user. + // If a request contains a disallowed reasonCode the server MUST reject it with the error type "urn:ietf:params:acme:error:badRevocationReason". + // The problem document detail SHOULD indicate which reasonCodes are allowed. + Reason *uint `json:"reason,omitempty"` +} diff --git a/acme/crypto.go b/acme/crypto.go deleted file mode 100644 index 5ddeeaec..00000000 --- a/acme/crypto.go +++ /dev/null @@ -1,334 +0,0 @@ -package acme - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "io" - "io/ioutil" - "math/big" - "net/http" - "time" - - "golang.org/x/crypto/ocsp" - jose "gopkg.in/square/go-jose.v2" -) - -// KeyType represents the key algo as well as the key size or curve to use. -type KeyType string -type derCertificateBytes []byte - -// Constants for all key types we support. -const ( - EC256 = KeyType("P256") - EC384 = KeyType("P384") - RSA2048 = KeyType("2048") - RSA4096 = KeyType("4096") - RSA8192 = KeyType("8192") -) - -const ( - // OCSPGood means that the certificate is valid. - OCSPGood = ocsp.Good - // OCSPRevoked means that the certificate has been deliberately revoked. - OCSPRevoked = ocsp.Revoked - // OCSPUnknown means that the OCSP responder doesn't know about the certificate. - OCSPUnknown = ocsp.Unknown - // OCSPServerFailed means that the OCSP responder failed to process the request. - OCSPServerFailed = ocsp.ServerFailed -) - -// Constants for OCSP must staple -var ( - tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} - ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} -) - -// GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, -// the parsed response, and an error, if any. The returned []byte can be passed directly -// into the OCSPStaple property of a tls.Certificate. If the bundle only contains the -// issued certificate, this function will try to get the issuer certificate from the -// IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return -// values are nil, the OCSP status may be assumed OCSPUnknown. -func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { - certificates, err := parsePEMBundle(bundle) - if err != nil { - return nil, nil, err - } - - // We expect the certificate slice to be ordered downwards the chain. - // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, - // which should always be the first two certificates. If there's no - // OCSP server listed in the leaf cert, there's nothing to do. And if - // we have only one certificate so far, we need to get the issuer cert. - issuedCert := certificates[0] - if len(issuedCert.OCSPServer) == 0 { - return nil, nil, errors.New("no OCSP server specified in cert") - } - if len(certificates) == 1 { - // TODO: build fallback. If this fails, check the remaining array entries. - if len(issuedCert.IssuingCertificateURL) == 0 { - return nil, nil, errors.New("no issuing certificate URL") - } - - resp, errC := httpGet(issuedCert.IssuingCertificateURL[0]) - if errC != nil { - return nil, nil, errC - } - defer resp.Body.Close() - - issuerBytes, errC := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) - if errC != nil { - return nil, nil, errC - } - - issuerCert, errC := x509.ParseCertificate(issuerBytes) - if errC != nil { - return nil, nil, errC - } - - // Insert it into the slice on position 0 - // We want it ordered right SRV CRT -> CA - certificates = append(certificates, issuerCert) - } - issuerCert := certificates[1] - - // Finally kick off the OCSP request. - ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) - if err != nil { - return nil, nil, err - } - - reader := bytes.NewReader(ocspReq) - req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader) - if err != nil { - return nil, nil, err - } - defer req.Body.Close() - - ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) - if err != nil { - return nil, nil, err - } - - ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) - if err != nil { - return nil, nil, err - } - - return ocspResBytes, ocspRes, nil -} - -func getKeyAuthorization(token string, key interface{}) (string, error) { - var publicKey crypto.PublicKey - switch k := key.(type) { - case *ecdsa.PrivateKey: - publicKey = k.Public() - case *rsa.PrivateKey: - publicKey = k.Public() - } - - // Generate the Key Authorization for the challenge - jwk := &jose.JSONWebKey{Key: publicKey} - if jwk == nil { - return "", errors.New("could not generate JWK from key") - } - thumbBytes, err := jwk.Thumbprint(crypto.SHA256) - if err != nil { - return "", err - } - - // unpad the base64URL - keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes) - - return token + "." + keyThumb, nil -} - -// parsePEMBundle parses a certificate bundle from top to bottom and returns -// a slice of x509 certificates. This function will error if no certificates are found. -func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { - var certificates []*x509.Certificate - var certDERBlock *pem.Block - - for { - certDERBlock, bundle = pem.Decode(bundle) - if certDERBlock == nil { - break - } - - if certDERBlock.Type == "CERTIFICATE" { - cert, err := x509.ParseCertificate(certDERBlock.Bytes) - if err != nil { - return nil, err - } - certificates = append(certificates, cert) - } - } - - if len(certificates) == 0 { - return nil, errors.New("no certificates were found while parsing the bundle") - } - - return certificates, nil -} - -func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { - keyBlock, _ := pem.Decode(key) - - switch keyBlock.Type { - case "RSA PRIVATE KEY": - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) - case "EC PRIVATE KEY": - return x509.ParseECPrivateKey(keyBlock.Bytes) - default: - return nil, errors.New("unknown PEM header value") - } -} - -func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { - - switch keyType { - case EC256: - return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - case EC384: - return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - case RSA2048: - return rsa.GenerateKey(rand.Reader, 2048) - case RSA4096: - return rsa.GenerateKey(rand.Reader, 4096) - case RSA8192: - return rsa.GenerateKey(rand.Reader, 8192) - } - - return nil, fmt.Errorf("invalid KeyType: %s", keyType) -} - -func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { - template := x509.CertificateRequest{ - Subject: pkix.Name{CommonName: domain}, - } - - if len(san) > 0 { - template.DNSNames = san - } - - if mustStaple { - template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ - Id: tlsFeatureExtensionOID, - Value: ocspMustStapleFeature, - }) - } - - return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) -} - -func pemEncode(data interface{}) []byte { - var pemBlock *pem.Block - switch key := data.(type) { - case *ecdsa.PrivateKey: - keyBytes, _ := x509.MarshalECPrivateKey(key) - pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} - case *rsa.PrivateKey: - pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} - case *x509.CertificateRequest: - pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} - case derCertificateBytes: - pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} - } - - return pem.EncodeToMemory(pemBlock) -} - -func pemDecode(data []byte) (*pem.Block, error) { - pemBlock, _ := pem.Decode(data) - if pemBlock == nil { - return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?") - } - - return pemBlock, nil -} - -func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) { - pemBlock, err := pemDecode(pem) - if pemBlock == nil { - return nil, err - } - - if pemBlock.Type != "CERTIFICATE REQUEST" { - return nil, fmt.Errorf("PEM block is not a certificate request") - } - - return x509.ParseCertificateRequest(pemBlock.Bytes) -} - -// GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate. -// The certificate has to be PEM encoded. Any other encodings like DER will fail. -func GetPEMCertExpiration(cert []byte) (time.Time, error) { - pemBlock, err := pemDecode(cert) - if pemBlock == nil { - return time.Time{}, err - } - - return getCertExpiration(pemBlock.Bytes) -} - -// getCertExpiration returns the "NotAfter" date of a DER encoded certificate. -func getCertExpiration(cert []byte) (time.Time, error) { - pCert, err := x509.ParseCertificate(cert) - if err != nil { - return time.Time{}, err - } - - return pCert.NotAfter, nil -} - -func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { - derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil -} - -func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, err - } - - if expiration.IsZero() { - expiration = time.Now().Add(365) - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: "ACME Challenge TEMP", - }, - NotBefore: time.Now(), - NotAfter: expiration, - - KeyUsage: x509.KeyUsageKeyEncipherment, - BasicConstraintsValid: true, - DNSNames: []string{domain}, - ExtraExtensions: extensions, - } - - return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) -} - -func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser { - return http.MaxBytesReader(nil, rd, numBytes) -} diff --git a/acme/crypto_test.go b/acme/crypto_test.go deleted file mode 100644 index c57cdc26..00000000 --- a/acme/crypto_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package acme - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGeneratePrivateKey(t *testing.T) { - key, err := generatePrivateKey(RSA2048) - require.NoError(t, err, "Error generating private key") - - assert.NotNil(t, key) -} - -func TestGenerateCSR(t *testing.T) { - key, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err, "Error generating private key") - - csr, err := generateCsr(key, "fizz.buzz", nil, true) - require.NoError(t, err, "Error generating CSR") - - assert.NotEmpty(t, csr) -} - -func TestPEMEncode(t *testing.T) { - buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") - - reader := MockRandReader{b: buf} - key, err := rsa.GenerateKey(reader, 32) - require.NoError(t, err, "Error generating private key") - - data := pemEncode(key) - require.NotNil(t, data) - assert.Len(t, data, 127) -} - -func TestPEMCertExpiration(t *testing.T) { - privKey, err := generatePrivateKey(RSA2048) - require.NoError(t, err, "Error generating private key") - - expiration := time.Now().Add(365) - expiration = expiration.Round(time.Second) - certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com", nil) - require.NoError(t, err, "Error generating cert") - - buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") - - // Some random string should return an error. - ctime, err := GetPEMCertExpiration(buf.Bytes()) - require.Errorf(t, err, "Expected getCertExpiration to return an error for garbage string but returned %v", ctime) - - // A DER encoded certificate should return an error. - _, err = GetPEMCertExpiration(certBytes) - require.Error(t, err, "Expected getCertExpiration to return an error for DER certificates") - - // A PEM encoded certificate should work ok. - pemCert := pemEncode(derCertificateBytes(certBytes)) - ctime, err = GetPEMCertExpiration(pemCert) - require.NoError(t, err) - - assert.Equal(t, expiration.UTC(), ctime) -} - -type MockRandReader struct { - b *bytes.Buffer -} - -func (r MockRandReader) Read(p []byte) (int, error) { - return r.b.Read(p) -} diff --git a/acme/dns_challenge.go b/acme/dns_challenge.go deleted file mode 100644 index d9c252e7..00000000 --- a/acme/dns_challenge.go +++ /dev/null @@ -1,343 +0,0 @@ -package acme - -import ( - "crypto/sha256" - "encoding/base64" - "errors" - "fmt" - "net" - "strings" - "sync" - "time" - - "github.com/miekg/dns" - "github.com/xenolf/lego/log" -) - -type preCheckDNSFunc func(fqdn, value string) (bool, error) - -var ( - // PreCheckDNS checks DNS propagation before notifying ACME that - // the DNS challenge is ready. - PreCheckDNS preCheckDNSFunc = checkDNSPropagation - fqdnToZone = map[string]string{} - muFqdnToZone sync.Mutex -) - -const defaultResolvConf = "/etc/resolv.conf" - -const ( - // DefaultPropagationTimeout default propagation timeout - DefaultPropagationTimeout = 60 * time.Second - - // DefaultPollingInterval default polling interval - DefaultPollingInterval = 2 * time.Second - - // DefaultTTL default TTL - DefaultTTL = 120 -) - -var defaultNameservers = []string{ - "google-public-dns-a.google.com:53", - "google-public-dns-b.google.com:53", -} - -// RecursiveNameservers are used to pre-check DNS propagation -var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) - -// DNSTimeout is used to override the default DNS timeout of 10 seconds. -var DNSTimeout = 10 * time.Second - -// getNameservers attempts to get systems nameservers before falling back to the defaults -func getNameservers(path string, defaults []string) []string { - config, err := dns.ClientConfigFromFile(path) - if err != nil || len(config.Servers) == 0 { - return defaults - } - - systemNameservers := []string{} - for _, server := range config.Servers { - // ensure all servers have a port number - if _, _, err := net.SplitHostPort(server); err != nil { - systemNameservers = append(systemNameservers, net.JoinHostPort(server, "53")) - } else { - systemNameservers = append(systemNameservers, server) - } - } - return systemNameservers -} - -// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge -func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) { - keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) - // base64URL encoding without padding - value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) - ttl = DefaultTTL - fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) - return -} - -// dnsChallenge implements the dns-01 challenge according to ACME 7.5 -type dnsChallenge struct { - jws *jws - validate validateFunc - provider ChallengeProvider -} - -// PreSolve just submits the txt record to the dns provider. It does not validate record propagation, or -// do anything at all with the acme server. -func (s *dnsChallenge) PreSolve(chlng challenge, domain string) error { - log.Infof("[%s] acme: Preparing to solve DNS-01", domain) - - if s.provider == nil { - return errors.New("no DNS Provider configured") - } - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) - if err != nil { - return err - } - - err = s.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("error presenting token: %s", err) - } - - return nil -} - -func (s *dnsChallenge) Solve(chlng challenge, domain string) error { - log.Infof("[%s] acme: Trying to solve DNS-01", domain) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) - if err != nil { - return err - } - - fqdn, value, _ := DNS01Record(domain, keyAuth) - - log.Infof("[%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers) - - var timeout, interval time.Duration - switch provider := s.provider.(type) { - case ChallengeProviderTimeout: - timeout, interval = provider.Timeout() - default: - timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval - } - - err = WaitFor(timeout, interval, func() (bool, error) { - return PreCheckDNS(fqdn, value) - }) - if err != nil { - return err - } - - return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} - -// CleanUp cleans the challenge -func (s *dnsChallenge) CleanUp(chlng challenge, domain string) error { - keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) - if err != nil { - return err - } - return s.provider.CleanUp(domain, chlng.Token, keyAuth) -} - -// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. -func checkDNSPropagation(fqdn, value string) (bool, error) { - // Initial attempt to resolve at the recursive NS - r, err := dnsQuery(fqdn, dns.TypeTXT, RecursiveNameservers, true) - if err != nil { - return false, err - } - - if r.Rcode == dns.RcodeSuccess { - // If we see a CNAME here then use the alias - for _, rr := range r.Answer { - if cn, ok := rr.(*dns.CNAME); ok { - if cn.Hdr.Name == fqdn { - fqdn = cn.Target - break - } - } - } - } - - authoritativeNss, err := lookupNameservers(fqdn) - if err != nil { - return false, err - } - - return checkAuthoritativeNss(fqdn, value, authoritativeNss) -} - -// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. -func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { - for _, ns := range nameservers { - r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) - if err != nil { - return false, err - } - - if r.Rcode != dns.RcodeSuccess { - return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) - } - - var found bool - for _, rr := range r.Answer { - if txt, ok := rr.(*dns.TXT); ok { - if strings.Join(txt.Txt, "") == value { - found = true - break - } - } - } - - if !found { - return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn) - } - } - - return true, nil -} - -// dnsQuery will query a nameserver, iterating through the supplied servers as it retries -// The nameserver should include a port, to facilitate testing where we talk to a mock dns server. -func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (in *dns.Msg, err error) { - m := new(dns.Msg) - m.SetQuestion(fqdn, rtype) - m.SetEdns0(4096, false) - - if !recursive { - m.RecursionDesired = false - } - - // Will retry the request based on the number of servers (n+1) - for i := 1; i <= len(nameservers)+1; i++ { - ns := nameservers[i%len(nameservers)] - udp := &dns.Client{Net: "udp", Timeout: DNSTimeout} - in, _, err = udp.Exchange(m, ns) - - if err == dns.ErrTruncated { - tcp := &dns.Client{Net: "tcp", Timeout: DNSTimeout} - // If the TCP request succeeds, the err will reset to nil - in, _, err = tcp.Exchange(m, ns) - } - - if err == nil { - break - } - } - return -} - -// lookupNameservers returns the authoritative nameservers for the given fqdn. -func lookupNameservers(fqdn string) ([]string, error) { - var authoritativeNss []string - - zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return nil, fmt.Errorf("could not determine the zone: %v", err) - } - - r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true) - if err != nil { - return nil, err - } - - for _, rr := range r.Answer { - if ns, ok := rr.(*dns.NS); ok { - authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) - } - } - - if len(authoritativeNss) > 0 { - return authoritativeNss, nil - } - return nil, fmt.Errorf("could not determine authoritative nameservers") -} - -// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the -// domain labels until the nameserver returns a SOA record in the answer section. -func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) { - muFqdnToZone.Lock() - defer muFqdnToZone.Unlock() - - // Do we have it cached? - if zone, ok := fqdnToZone[fqdn]; ok { - return zone, nil - } - - labelIndexes := dns.Split(fqdn) - for _, index := range labelIndexes { - domain := fqdn[index:] - - in, err := dnsQuery(domain, dns.TypeSOA, nameservers, true) - if err != nil { - return "", err - } - - // Any response code other than NOERROR and NXDOMAIN is treated as error - if in.Rcode != dns.RcodeNameError && in.Rcode != dns.RcodeSuccess { - return "", fmt.Errorf("unexpected response code '%s' for %s", - dns.RcodeToString[in.Rcode], domain) - } - - // Check if we got a SOA RR in the answer section - if in.Rcode == dns.RcodeSuccess { - - // CNAME records cannot/should not exist at the root of a zone. - // So we skip a domain when a CNAME is found. - if dnsMsgContainsCNAME(in) { - continue - } - - for _, ans := range in.Answer { - if soa, ok := ans.(*dns.SOA); ok { - zone := soa.Hdr.Name - fqdnToZone[fqdn] = zone - return zone, nil - } - } - } - } - - return "", fmt.Errorf("could not find the start of authority") -} - -// dnsMsgContainsCNAME checks for a CNAME answer in msg -func dnsMsgContainsCNAME(msg *dns.Msg) bool { - for _, ans := range msg.Answer { - if _, ok := ans.(*dns.CNAME); ok { - return true - } - } - return false -} - -// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. -func ClearFqdnCache() { - fqdnToZone = map[string]string{} -} - -// ToFqdn converts the name into a fqdn appending a trailing dot. -func ToFqdn(name string) string { - n := len(name) - if n == 0 || name[n-1] == '.' { - return name - } - return name + "." -} - -// UnFqdn converts the fqdn into a name removing the trailing dot. -func UnFqdn(name string) string { - n := len(name) - if n != 0 && name[n-1] == '.' { - return name[:n-1] - } - return name -} diff --git a/acme/dns_challenge_manual.go b/acme/dns_challenge_manual.go deleted file mode 100644 index ca94fcac..00000000 --- a/acme/dns_challenge_manual.go +++ /dev/null @@ -1,55 +0,0 @@ -package acme - -import ( - "bufio" - "fmt" - "os" - - "github.com/xenolf/lego/log" -) - -const ( - dnsTemplate = "%s %d IN TXT \"%s\"" -) - -// DNSProviderManual is an implementation of the ChallengeProvider interface -type DNSProviderManual struct{} - -// NewDNSProviderManual returns a DNSProviderManual instance. -func NewDNSProviderManual() (*DNSProviderManual, error) { - return &DNSProviderManual{}, nil -} - -// Present prints instructions for manually creating the TXT record -func (*DNSProviderManual) Present(domain, token, keyAuth string) error { - fqdn, value, ttl := DNS01Record(domain, keyAuth) - dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value) - - authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return err - } - - log.Infof("acme: Please create the following TXT record in your %s zone:", authZone) - log.Infof("acme: %s", dnsRecord) - log.Infof("acme: Press 'Enter' when you are done") - - reader := bufio.NewReader(os.Stdin) - _, _ = reader.ReadString('\n') - return nil -} - -// CleanUp prints instructions for manually removing the TXT record -func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { - fqdn, _, ttl := DNS01Record(domain, keyAuth) - dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, "...") - - authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers) - if err != nil { - return err - } - - log.Infof("acme: You can now remove this TXT record from your %s zone:", authZone) - log.Infof("acme: %s", dnsRecord) - return nil -} diff --git a/acme/dns_challenge_test.go b/acme/dns_challenge_test.go deleted file mode 100644 index e7e44847..00000000 --- a/acme/dns_challenge_test.go +++ /dev/null @@ -1,321 +0,0 @@ -package acme - -import ( - "bufio" - "crypto/rand" - "crypto/rsa" - "net/http" - "net/http/httptest" - "os" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDNSValidServerResponse(t *testing.T) { - PreCheckDNS = func(fqdn, value string) (bool, error) { - return true, nil - } - - privKey, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err) - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Replay-Nonce", "12345") - - _, err = w.Write([]byte("{\"type\":\"dns01\",\"status\":\"valid\",\"uri\":\"http://some.url\",\"token\":\"http8\"}")) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - })) - - go func() { - time.Sleep(time.Second * 2) - f := bufio.NewWriter(os.Stdout) - defer f.Flush() - _, _ = f.WriteString("\n") - }() - - manualProvider, err := NewDNSProviderManual() - require.NoError(t, err) - - clientChallenge := challenge{Type: "dns01", Status: "pending", URL: ts.URL, Token: "http8"} - - solver := &dnsChallenge{ - jws: &jws{privKey: privKey, getNonceURL: ts.URL}, - validate: validate, - provider: manualProvider, - } - - err = solver.Solve(clientChallenge, "example.com") - require.NoError(t, err) -} - -func TestPreCheckDNS(t *testing.T) { - ok, err := PreCheckDNS("acme-staging.api.letsencrypt.org", "fe01=") - if err != nil || !ok { - t.Errorf("PreCheckDNS failed for acme-staging.api.letsencrypt.org") - } -} - -func TestLookupNameserversOK(t *testing.T) { - testCases := []struct { - fqdn string - nss []string - }{ - { - fqdn: "books.google.com.ng.", - nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - { - fqdn: "www.google.com.", - nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, - }, - { - fqdn: "physics.georgetown.edu.", - nss: []string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.fqdn, func(t *testing.T) { - t.Parallel() - - nss, err := lookupNameservers(test.fqdn) - require.NoError(t, err) - - sort.Strings(nss) - sort.Strings(test.nss) - - assert.EqualValues(t, test.nss, nss) - }) - } -} - -func TestLookupNameserversErr(t *testing.T) { - testCases := []struct { - desc string - fqdn string - error string - }{ - { - desc: "invalid tld", - fqdn: "_null.n0n0.", - error: "could not determine the zone", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := lookupNameservers(test.fqdn) - require.Error(t, err) - assert.Contains(t, err.Error(), test.error) - }) - } -} - -func TestFindZoneByFqdn(t *testing.T) { - testCases := []struct { - desc string - fqdn string - zone string - }{ - { - desc: "domain is a CNAME", - fqdn: "mail.google.com.", - zone: "google.com.", - }, - { - desc: "domain is a non-existent subdomain", - fqdn: "foo.google.com.", - zone: "google.com.", - }, - { - desc: "domain is a eTLD", - fqdn: "example.com.ac.", - zone: "ac.", - }, - { - desc: "domain is a cross-zone CNAME", - fqdn: "cross-zone-example.assets.sh.", - zone: "assets.sh.", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - zone, err := FindZoneByFqdn(test.fqdn, RecursiveNameservers) - require.NoError(t, err) - - assert.Equal(t, test.zone, zone) - }) - } -} - -func TestCheckAuthoritativeNss(t *testing.T) { - testCases := []struct { - desc string - fqdn, value string - ns []string - expected bool - }{ - { - desc: "TXT RR w/ expected value", - fqdn: "8.8.8.8.asn.routeviews.org.", - value: "151698.8.8.024", - ns: []string{"asnums.routeviews.org."}, - expected: true, - }, - { - desc: "No TXT RR", - fqdn: "ns1.google.com.", - ns: []string{"ns2.google.com."}, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns) - assert.Equal(t, test.expected, ok, test.fqdn) - }) - } -} - -func TestCheckAuthoritativeNssErr(t *testing.T) { - testCases := []struct { - desc string - fqdn, value string - ns []string - error string - }{ - { - desc: "TXT RR /w unexpected value", - fqdn: "8.8.8.8.asn.routeviews.org.", - value: "fe01=", - ns: []string{"asnums.routeviews.org."}, - error: "did not return the expected TXT record", - }, - { - desc: "No TXT RR", - fqdn: "ns1.google.com.", - value: "fe01=", - ns: []string{"ns2.google.com."}, - error: "did not return the expected TXT record", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - _, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns) - require.Error(t, err) - assert.Contains(t, err.Error(), test.error) - }) - } -} - -func TestResolveConfServers(t *testing.T) { - var testCases = []struct { - fixture string - expected []string - defaults []string - }{ - { - fixture: "testdata/resolv.conf.1", - defaults: []string{"127.0.0.1:53"}, - expected: []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, - }, - { - fixture: "testdata/resolv.conf.nonexistant", - defaults: []string{"127.0.0.1:53"}, - expected: []string{"127.0.0.1:53"}, - }, - } - - for _, test := range testCases { - t.Run(test.fixture, func(t *testing.T) { - - result := getNameservers(test.fixture, test.defaults) - - sort.Strings(result) - sort.Strings(test.expected) - - assert.Equal(t, test.expected, result) - }) - } -} - -func TestToFqdn(t *testing.T) { - testCases := []struct { - desc string - domain string - expected string - }{ - { - desc: "simple", - domain: "foo.bar.com", - expected: "foo.bar.com.", - }, - { - desc: "already FQDN", - domain: "foo.bar.com.", - expected: "foo.bar.com.", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - fqdn := ToFqdn(test.domain) - assert.Equal(t, test.expected, fqdn) - }) - } -} - -func TestUnFqdn(t *testing.T) { - testCases := []struct { - desc string - fqdn string - expected string - }{ - { - desc: "simple", - fqdn: "foo.bar.com.", - expected: "foo.bar.com", - }, - { - desc: "already domain", - fqdn: "foo.bar.com", - expected: "foo.bar.com", - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - domain := UnFqdn(test.fqdn) - - assert.Equal(t, test.expected, domain) - }) - } -} diff --git a/acme/error.go b/acme/error.go deleted file mode 100644 index 78694deb..00000000 --- a/acme/error.go +++ /dev/null @@ -1,91 +0,0 @@ -package acme - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" -) - -const ( - tosAgreementError = "Terms of service have changed" - invalidNonceError = "urn:ietf:params:acme:error:badNonce" -) - -// RemoteError is the base type for all errors specific to the ACME protocol. -type RemoteError struct { - StatusCode int `json:"status,omitempty"` - Type string `json:"type"` - Detail string `json:"detail"` -} - -func (e RemoteError) Error() string { - return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail) -} - -// TOSError represents the error which is returned if the user needs to -// accept the TOS. -// TODO: include the new TOS url if we can somehow obtain it. -type TOSError struct { - RemoteError -} - -// NonceError represents the error which is returned if the -// nonce sent by the client was not accepted by the server. -type NonceError struct { - RemoteError -} - -type domainError struct { - Domain string - Error error -} - -// ObtainError is returned when there are specific errors available -// per domain. For example in ObtainCertificate -type ObtainError map[string]error - -func (e ObtainError) Error() string { - buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") - for dom, err := range e { - buffer.WriteString(fmt.Sprintf("[%s] %s\n", dom, err)) - } - return buffer.String() -} - -func handleHTTPError(resp *http.Response) error { - var errorDetail RemoteError - - contentType := resp.Header.Get("Content-Type") - if contentType == "application/json" || strings.HasPrefix(contentType, "application/problem+json") { - err := json.NewDecoder(resp.Body).Decode(&errorDetail) - if err != nil { - return err - } - } else { - detailBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize)) - if err != nil { - return err - } - errorDetail.Detail = string(detailBytes) - } - - errorDetail.StatusCode = resp.StatusCode - - // Check for errors we handle specifically - if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError { - return TOSError{errorDetail} - } - - if errorDetail.StatusCode == http.StatusBadRequest && errorDetail.Type == invalidNonceError { - return NonceError{errorDetail} - } - - return errorDetail -} - -func handleChallengeError(chlng challenge) error { - return chlng.Error -} diff --git a/acme/errors.go b/acme/errors.go new file mode 100644 index 00000000..1658fe8d --- /dev/null +++ b/acme/errors.go @@ -0,0 +1,58 @@ +package acme + +import ( + "fmt" +) + +// Errors types +const ( + errNS = "urn:ietf:params:acme:error:" + BadNonceErr = errNS + "badNonce" +) + +// ProblemDetails the problem details object +// - https://tools.ietf.org/html/rfc7807#section-3.1 +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.3.3 +type ProblemDetails struct { + Type string `json:"type,omitempty"` + Detail string `json:"detail,omitempty"` + HTTPStatus int `json:"status,omitempty"` + Instance string `json:"instance,omitempty"` + SubProblems []SubProblem `json:"subproblems,omitempty"` + + // additional values to have a better error message (Not defined by the RFC) + Method string `json:"method,omitempty"` + URL string `json:"url,omitempty"` +} + +// SubProblem a "subproblems" +// - https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.7.1 +type SubProblem struct { + Type string `json:"type,omitempty"` + Detail string `json:"detail,omitempty"` + Identifier Identifier `json:"identifier,omitempty"` +} + +func (p ProblemDetails) Error() string { + msg := fmt.Sprintf("acme: error: %d", p.HTTPStatus) + if len(p.Method) != 0 || len(p.URL) != 0 { + msg += fmt.Sprintf(" :: %s :: %s", p.Method, p.URL) + } + msg += fmt.Sprintf(" :: %s :: %s", p.Type, p.Detail) + + for _, sub := range p.SubProblems { + msg += fmt.Sprintf(", problem: %q :: %s", sub.Type, sub.Detail) + } + + if len(p.Instance) == 0 { + msg += ", url: " + p.Instance + } + + return msg +} + +// NonceError represents the error which is returned +// if the nonce sent by the client was not accepted by the server. +type NonceError struct { + *ProblemDetails +} diff --git a/acme/http.go b/acme/http.go deleted file mode 100644 index 8343b369..00000000 --- a/acme/http.go +++ /dev/null @@ -1,212 +0,0 @@ -package acme - -import ( - "crypto/tls" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - "os" - "runtime" - "strings" - "time" -) - -var ( - // UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. - UserAgent string - - // HTTPClient is an HTTP client with a reasonable timeout value and - // potentially a custom *x509.CertPool based on the caCertificatesEnvVar - // environment variable (see the `initCertPool` function) - HTTPClient = http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 15 * time.Second, - ResponseHeaderTimeout: 15 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: &tls.Config{ - ServerName: os.Getenv(caServerNameEnvVar), - RootCAs: initCertPool(), - }, - }, - } -) - -const ( - // ourUserAgent is the User-Agent of this underlying library package. - // NOTE: Update this with each tagged release. - ourUserAgent = "xenolf-acme/1.2.1" - - // ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package. - // values: detach|release - // NOTE: Update this with each tagged release. - ourUserAgentComment = "detach" - - // caCertificatesEnvVar is the environment variable name that can be used to - // specify the path to PEM encoded CA Certificates that can be used to - // authenticate an ACME server with a HTTPS certificate not issued by a CA in - // the system-wide trusted root list. - caCertificatesEnvVar = "LEGO_CA_CERTIFICATES" - - // caServerNameEnvVar is the environment variable name that can be used to - // specify the CA server name that can be used to - // authenticate an ACME server with a HTTPS certificate not issued by a CA in - // the system-wide trusted root list. - caServerNameEnvVar = "LEGO_CA_SERVER_NAME" -) - -// initCertPool creates a *x509.CertPool populated with the PEM certificates -// found in the filepath specified in the caCertificatesEnvVar OS environment -// variable. If the caCertificatesEnvVar is not set then initCertPool will -// return nil. If there is an error creating a *x509.CertPool from the provided -// caCertificatesEnvVar value then initCertPool will panic. -func initCertPool() *x509.CertPool { - if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" { - customCAs, err := ioutil.ReadFile(customCACertsPath) - if err != nil { - panic(fmt.Sprintf("error reading %s=%q: %v", - caCertificatesEnvVar, customCACertsPath, err)) - } - certPool := x509.NewCertPool() - if ok := certPool.AppendCertsFromPEM(customCAs); !ok { - panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", - caCertificatesEnvVar, customCACertsPath, err)) - } - return certPool - } - return nil -} - -// httpHead performs a HEAD request with a proper User-Agent string. -// The response body (resp.Body) is already closed when this function returns. -func httpHead(url string) (resp *http.Response, err error) { - req, err := http.NewRequest(http.MethodHead, url, nil) - if err != nil { - return nil, fmt.Errorf("failed to head %q: %v", url, err) - } - - req.Header.Set("User-Agent", userAgent()) - - resp, err = HTTPClient.Do(req) - if err != nil { - return resp, fmt.Errorf("failed to do head %q: %v", url, err) - } - resp.Body.Close() - return resp, err -} - -// httpPost performs a POST request with a proper User-Agent string. -// Callers should close resp.Body when done reading from it. -func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { - req, err := http.NewRequest(http.MethodPost, url, body) - if err != nil { - return nil, fmt.Errorf("failed to post %q: %v", url, err) - } - req.Header.Set("Content-Type", bodyType) - req.Header.Set("User-Agent", userAgent()) - - return HTTPClient.Do(req) -} - -// httpGet performs a GET request with a proper User-Agent string. -// Callers should close resp.Body when done reading from it. -func httpGet(url string) (resp *http.Response, err error) { - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("failed to get %q: %v", url, err) - } - req.Header.Set("User-Agent", userAgent()) - - return HTTPClient.Do(req) -} - -// getJSON performs an HTTP GET request and parses the response body -// as JSON, into the provided respBody object. -func getJSON(uri string, respBody interface{}) (http.Header, error) { - resp, err := httpGet(uri) - if err != nil { - return nil, fmt.Errorf("failed to get json %q: %v", uri, err) - } - defer resp.Body.Close() - - if resp.StatusCode >= http.StatusBadRequest { - return resp.Header, handleHTTPError(resp) - } - - return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) -} - -// postJSON performs an HTTP POST request and parses the response body -// as JSON, into the provided respBody object. -func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { - jsonBytes, err := json.Marshal(reqBody) - if err != nil { - return nil, errors.New("failed to marshal network message") - } - - resp, err := post(j, uri, jsonBytes, respBody) - if resp == nil { - return nil, err - } - - defer resp.Body.Close() - - return resp.Header, err -} - -func postAsGet(j *jws, uri string, respBody interface{}) (*http.Response, error) { - return post(j, uri, []byte{}, respBody) -} - -func post(j *jws, uri string, reqBody []byte, respBody interface{}) (*http.Response, error) { - resp, err := j.post(uri, reqBody) - if err != nil { - return nil, fmt.Errorf("failed to post JWS message. -> %v", err) - } - - if resp.StatusCode >= http.StatusBadRequest { - err = handleHTTPError(resp) - switch err.(type) { - case NonceError: - // Retry once if the nonce was invalidated - - retryResp, errP := j.post(uri, reqBody) - if errP != nil { - return nil, fmt.Errorf("failed to post JWS message. -> %v", errP) - } - - if retryResp.StatusCode >= http.StatusBadRequest { - return retryResp, handleHTTPError(retryResp) - } - - if respBody == nil { - return retryResp, nil - } - - return retryResp, json.NewDecoder(retryResp.Body).Decode(respBody) - default: - return resp, err - } - } - - if respBody == nil { - return resp, nil - } - - return resp, json.NewDecoder(resp.Body).Decode(respBody) -} - -// userAgent builds and returns the User-Agent string to use in requests. -func userAgent() string { - ua := fmt.Sprintf("%s %s (%s; %s; %s)", UserAgent, ourUserAgent, ourUserAgentComment, runtime.GOOS, runtime.GOARCH) - return strings.TrimSpace(ua) -} diff --git a/acme/http_challenge.go b/acme/http_challenge.go deleted file mode 100644 index 77a8edd4..00000000 --- a/acme/http_challenge.go +++ /dev/null @@ -1,42 +0,0 @@ -package acme - -import ( - "fmt" - - "github.com/xenolf/lego/log" -) - -type httpChallenge struct { - jws *jws - validate validateFunc - provider ChallengeProvider -} - -// HTTP01ChallengePath returns the URL path for the `http-01` challenge -func HTTP01ChallengePath(token string) string { - return "/.well-known/acme-challenge/" + token -} - -func (s *httpChallenge) Solve(chlng challenge, domain string) error { - - log.Infof("[%s] acme: Trying to solve HTTP-01", domain) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) - if err != nil { - return err - } - - err = s.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("[%s] error presenting token: %v", domain, err) - } - defer func() { - err := s.provider.CleanUp(domain, chlng.Token, keyAuth) - if err != nil { - log.Warnf("[%s] error cleaning up: %v", domain, err) - } - }() - - return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} diff --git a/acme/http_challenge_test.go b/acme/http_challenge_test.go deleted file mode 100644 index 17c4a4e7..00000000 --- a/acme/http_challenge_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package acme - -import ( - "crypto/rand" - "crypto/rsa" - "io/ioutil" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestHTTPChallenge(t *testing.T) { - mockValidate := func(_ *jws, _, _ string, chlng challenge) error { - uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token - resp, err := httpGet(uri) - if err != nil { - return err - } - defer resp.Body.Close() - - if want := "text/plain"; resp.Header.Get("Content-Type") != want { - t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - bodyStr := string(body) - - if bodyStr != chlng.KeyAuthorization { - t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) - } - - return nil - } - - privKey, err := rsa.GenerateKey(rand.Reader, 512) - require.NoError(t, err, "Could not generate test key") - - solver := &httpChallenge{ - jws: &jws{privKey: privKey}, - validate: mockValidate, - provider: &HTTPProviderServer{port: "23457"}, - } - - clientChallenge := challenge{Type: string(HTTP01), Token: "http1"} - - err = solver.Solve(clientChallenge, "localhost:23457") - require.NoError(t, err) -} - -func TestHTTPChallengeInvalidPort(t *testing.T) { - privKey, err := rsa.GenerateKey(rand.Reader, 128) - require.NoError(t, err, "Could not generate test key") - - solver := &httpChallenge{ - jws: &jws{privKey: privKey}, - validate: stubValidate, - provider: &HTTPProviderServer{port: "123456"}, - } - - clientChallenge := challenge{Type: string(HTTP01), Token: "http2"} - - err = solver.Solve(clientChallenge, "localhost:123456") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid port") - assert.Contains(t, err.Error(), "123456") -} diff --git a/acme/http_test.go b/acme/http_test.go deleted file mode 100644 index db1ca265..00000000 --- a/acme/http_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package acme - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestHTTPUserAgent(t *testing.T) { - var ua, method string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ua = r.Header.Get("User-Agent") - method = r.Method - })) - defer ts.Close() - - testCases := []struct { - method string - call func(u string) (resp *http.Response, err error) - }{ - { - method: http.MethodGet, - call: httpGet, - }, - { - method: http.MethodHead, - call: httpHead, - }, - { - method: http.MethodPost, - call: func(u string) (resp *http.Response, err error) { - return httpPost(u, "text/plain", strings.NewReader("falalalala")) - }, - }, - } - - for _, test := range testCases { - t.Run(test.method, func(t *testing.T) { - - _, err := test.call(ts.URL) - require.NoError(t, err) - - assert.Equal(t, test.method, method) - assert.Contains(t, ua, ourUserAgent, "User-Agent") - }) - } -} - -func TestUserAgent(t *testing.T) { - ua := userAgent() - - assert.Contains(t, ua, ourUserAgent) - if strings.HasSuffix(ua, " ") { - t.Errorf("UA should not have trailing spaces; got '%s'", ua) - } - - // customize the UA by appending a value - UserAgent = "MyApp/1.2.3" - ua = userAgent() - - assert.Contains(t, ua, ourUserAgent) - assert.Contains(t, ua, UserAgent) -} - -// TestInitCertPool tests the http.go initCertPool function for customizing the -// HTTP Client *x509.CertPool with an environment variable. -func TestInitCertPool(t *testing.T) { - // writeTemp creates a temp file with the given contents & prefix and returns - // the file path. If an error occurs, t.Fatalf is called to end the test run. - writeTemp := func(t *testing.T, contents, prefix string) string { - t.Helper() - tmpFile, err := ioutil.TempFile("", prefix) - if err != nil { - t.Fatalf("Unable to create tempfile: %v", err) - } - err = ioutil.WriteFile(tmpFile.Name(), []byte(contents), 0700) - if err != nil { - t.Fatalf("Unable to write tempfile contents: %v", err) - } - return tmpFile.Name() - } - - invalidFileContents := "not a certificate" - invalidFile := writeTemp(t, invalidFileContents, "invalid.pem") - - // validFileContents is lifted from Pebble[0]. Generate your own CA cert with - // MiniCA[1]. - // [0]: https://github.com/letsencrypt/pebble/blob/de6fa233ea1f283eeb9751d42c8e1ae72718c44e/test/certs/pebble.minica.pem - // [1]: https://github.com/jsha/minica - validFileContents := ` ------BEGIN CERTIFICATE----- -MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE -AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx -MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ -alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn -Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu -9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 -toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 -Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB -AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB -BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v -d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF -WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll -xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix -Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 -2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF -p9BI7gVKtWSZYegicA== ------END CERTIFICATE----- - ` - validFile := writeTemp(t, validFileContents, "valid.pem") - - testCases := []struct { - Name string - EnvVar string - ExpectPanic bool - ExpectNil bool - }{ - // Setting the env var to a file that doesn't exist should panic - { - Name: "Env var with missing file", - EnvVar: "not.a.real.file.pem", - ExpectPanic: true, - }, - // Setting the env var to a file that contains invalid content should panic - { - Name: "Env var with invalid content", - EnvVar: invalidFile, - ExpectPanic: true, - }, - // Setting the env var to the empty string should not panic and should - // return nil - { - Name: "No env var", - EnvVar: "", - ExpectPanic: false, - ExpectNil: true, - }, - // Setting the env var to a file that contains valid content should not - // panic and should not return nil - { - Name: "Env var with valid content", - EnvVar: validFile, - ExpectPanic: false, - ExpectNil: false, - }, - } - - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - os.Setenv(caCertificatesEnvVar, test.EnvVar) - defer os.Setenv(caCertificatesEnvVar, "") - - defer func() { - r := recover() - - if test.ExpectPanic { - assert.NotNil(t, r, "expected initCertPool() to panic") - } else { - assert.Nil(t, r, "expected initCertPool() to not panic") - } - }() - - result := initCertPool() - - if test.ExpectNil { - assert.Nil(t, result) - } else { - assert.NotNil(t, result) - } - }) - } -} diff --git a/acme/jws.go b/acme/jws.go deleted file mode 100644 index bea76210..00000000 --- a/acme/jws.go +++ /dev/null @@ -1,167 +0,0 @@ -package acme - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rsa" - "fmt" - "net/http" - "sync" - - "gopkg.in/square/go-jose.v2" -) - -type jws struct { - getNonceURL string - privKey crypto.PrivateKey - kid string - nonces nonceManager -} - -// Posts a JWS signed message to the specified URL. -// It does NOT close the response body, so the caller must -// do that if no error was returned. -func (j *jws) post(url string, content []byte) (*http.Response, error) { - signedContent, err := j.signContent(url, content) - if err != nil { - return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) - } - - data := bytes.NewBuffer([]byte(signedContent.FullSerialize())) - resp, err := httpPost(url, "application/jose+json", data) - if err != nil { - return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error()) - } - - nonce, nonceErr := getNonceFromResponse(resp) - if nonceErr == nil { - j.nonces.Push(nonce) - } - - return resp, nil -} - -func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) { - - var alg jose.SignatureAlgorithm - switch k := j.privKey.(type) { - case *rsa.PrivateKey: - alg = jose.RS256 - case *ecdsa.PrivateKey: - if k.Curve == elliptic.P256() { - alg = jose.ES256 - } else if k.Curve == elliptic.P384() { - alg = jose.ES384 - } - } - - jsonKey := jose.JSONWebKey{ - Key: j.privKey, - KeyID: j.kid, - } - - signKey := jose.SigningKey{ - Algorithm: alg, - Key: jsonKey, - } - options := jose.SignerOptions{ - NonceSource: j, - ExtraHeaders: make(map[jose.HeaderKey]interface{}), - } - options.ExtraHeaders["url"] = url - if j.kid == "" { - options.EmbedJWK = true - } - - signer, err := jose.NewSigner(signKey, &options) - if err != nil { - return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error()) - } - - signed, err := signer.Sign(content) - if err != nil { - return nil, fmt.Errorf("failed to sign content -> %s", err.Error()) - } - return signed, nil -} - -func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { - jwk := jose.JSONWebKey{Key: j.privKey} - jwkJSON, err := jwk.Public().MarshalJSON() - if err != nil { - return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error()) - } - - signer, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, - &jose.SignerOptions{ - EmbedJWK: false, - ExtraHeaders: map[jose.HeaderKey]interface{}{ - "kid": kid, - "url": url, - }, - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error()) - } - - signed, err := signer.Sign(jwkJSON) - if err != nil { - return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error()) - } - - return signed, nil -} - -func (j *jws) Nonce() (string, error) { - if nonce, ok := j.nonces.Pop(); ok { - return nonce, nil - } - - return getNonce(j.getNonceURL) -} - -type nonceManager struct { - nonces []string - sync.Mutex -} - -func (n *nonceManager) Pop() (string, bool) { - n.Lock() - defer n.Unlock() - - if len(n.nonces) == 0 { - return "", false - } - - nonce := n.nonces[len(n.nonces)-1] - n.nonces = n.nonces[:len(n.nonces)-1] - return nonce, true -} - -func (n *nonceManager) Push(nonce string) { - n.Lock() - defer n.Unlock() - n.nonces = append(n.nonces, nonce) -} - -func getNonce(url string) (string, error) { - resp, err := httpHead(url) - if err != nil { - return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error()) - } - - return getNonceFromResponse(resp) -} - -func getNonceFromResponse(resp *http.Response) (string, error) { - nonce := resp.Header.Get("Replay-Nonce") - if nonce == "" { - return "", fmt.Errorf("server did not respond with a proper nonce header") - } - - return nonce, nil -} diff --git a/acme/messages.go b/acme/messages.go deleted file mode 100644 index 6946cc15..00000000 --- a/acme/messages.go +++ /dev/null @@ -1,103 +0,0 @@ -package acme - -import ( - "encoding/json" - "time" -) - -// RegistrationResource represents all important informations about a registration -// of which the client needs to keep track itself. -type RegistrationResource struct { - Body accountMessage `json:"body,omitempty"` - URI string `json:"uri,omitempty"` -} - -type directory struct { - NewNonceURL string `json:"newNonce"` - NewAccountURL string `json:"newAccount"` - NewOrderURL string `json:"newOrder"` - RevokeCertURL string `json:"revokeCert"` - KeyChangeURL string `json:"keyChange"` - Meta struct { - TermsOfService string `json:"termsOfService"` - Website string `json:"website"` - CaaIdentities []string `json:"caaIdentities"` - ExternalAccountRequired bool `json:"externalAccountRequired"` - } `json:"meta"` -} - -type accountMessage struct { - Status string `json:"status,omitempty"` - Contact []string `json:"contact,omitempty"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` - Orders string `json:"orders,omitempty"` - OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` - ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` -} - -type orderResource struct { - URL string `json:"url,omitempty"` - Domains []string `json:"domains,omitempty"` - orderMessage `json:"body,omitempty"` -} - -type orderMessage struct { - Status string `json:"status,omitempty"` - Expires string `json:"expires,omitempty"` - Identifiers []identifier `json:"identifiers"` - NotBefore string `json:"notBefore,omitempty"` - NotAfter string `json:"notAfter,omitempty"` - Authorizations []string `json:"authorizations,omitempty"` - Finalize string `json:"finalize,omitempty"` - Certificate string `json:"certificate,omitempty"` -} - -type authorization struct { - Status string `json:"status"` - Expires time.Time `json:"expires"` - Identifier identifier `json:"identifier"` - Challenges []challenge `json:"challenges"` -} - -type identifier struct { - Type string `json:"type"` - Value string `json:"value"` -} - -type challenge struct { - URL string `json:"url"` - Type string `json:"type"` - Status string `json:"status"` - Token string `json:"token"` - Validated time.Time `json:"validated"` - KeyAuthorization string `json:"keyAuthorization"` - Error RemoteError `json:"error"` -} - -type csrMessage struct { - Csr string `json:"csr"` -} - -type revokeCertMessage struct { - Certificate string `json:"certificate"` -} - -type deactivateAuthMessage struct { - Status string `jsom:"status"` -} - -// CertificateResource represents a CA issued certificate. -// PrivateKey, Certificate and IssuerCertificate are all -// already PEM encoded and can be directly written to disk. -// Certificate may be a certificate bundle, depending on the -// options supplied to create it. -type CertificateResource struct { - Domain string `json:"domain"` - CertURL string `json:"certUrl"` - CertStableURL string `json:"certStableUrl"` - AccountRef string `json:"accountRef,omitempty"` - PrivateKey []byte `json:"-"` - Certificate []byte `json:"-"` - IssuerCertificate []byte `json:"-"` - CSR []byte `json:"-"` -} diff --git a/acme/tls_alpn_challenge.go b/acme/tls_alpn_challenge.go deleted file mode 100644 index cc70c350..00000000 --- a/acme/tls_alpn_challenge.go +++ /dev/null @@ -1,104 +0,0 @@ -package acme - -import ( - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "crypto/x509/pkix" - "encoding/asn1" - "fmt" - - "github.com/xenolf/lego/log" -) - -// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. -// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1 -var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} - -type tlsALPNChallenge struct { - jws *jws - validate validateFunc - provider ChallengeProvider -} - -// Solve manages the provider to validate and solve the challenge. -func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error { - log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", domain) - - // Generate the Key Authorization for the challenge - keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) - if err != nil { - return err - } - - err = t.provider.Present(domain, chlng.Token, keyAuth) - if err != nil { - return fmt.Errorf("[%s] error presenting token: %v", domain, err) - } - defer func() { - err := t.provider.CleanUp(domain, chlng.Token, keyAuth) - if err != nil { - log.Warnf("[%s] error cleaning up: %v", domain, err) - } - }() - - return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) -} - -// TLSALPNChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension -// and domain name for the `tls-alpn-01` challenge. -func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { - // Compute the SHA-256 digest of the key authorization. - zBytes := sha256.Sum256([]byte(keyAuth)) - - value, err := asn1.Marshal(zBytes[:sha256.Size]) - if err != nil { - return nil, nil, err - } - - // Add the keyAuth digest as the acmeValidation-v1 extension - // (marked as critical such that it won't be used by non-ACME software). - // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3 - extensions := []pkix.Extension{ - { - Id: idPeAcmeIdentifierV1, - Critical: true, - Value: value, - }, - } - - // Generate a new RSA key for the certificates. - tempPrivKey, err := generatePrivateKey(RSA2048) - if err != nil { - return nil, nil, err - } - - rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) - - // Generate the PEM certificate using the provided private key, domain, and extra extensions. - tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions) - if err != nil { - return nil, nil, err - } - - // Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair. - rsaPrivPEM := pemEncode(rsaPrivKey) - - return tempCertPEM, rsaPrivPEM, nil -} - -// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1 extension -// and domain name for the `tls-alpn-01` challenge. -func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) { - tempCertPEM, rsaPrivPEM, err := TLSALPNChallengeBlocks(domain, keyAuth) - if err != nil { - return nil, err - } - - certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) - if err != nil { - return nil, err - } - - return &certificate, nil -} diff --git a/certcrypto/crypto.go b/certcrypto/crypto.go new file mode 100644 index 00000000..bb99d7d2 --- /dev/null +++ b/certcrypto/crypto.go @@ -0,0 +1,252 @@ +package certcrypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "errors" + "fmt" + "math/big" + "time" + + "golang.org/x/crypto/ocsp" +) + +// Constants for all key types we support. +const ( + EC256 = KeyType("P256") + EC384 = KeyType("P384") + RSA2048 = KeyType("2048") + RSA4096 = KeyType("4096") + RSA8192 = KeyType("8192") +) + +const ( + // OCSPGood means that the certificate is valid. + OCSPGood = ocsp.Good + // OCSPRevoked means that the certificate has been deliberately revoked. + OCSPRevoked = ocsp.Revoked + // OCSPUnknown means that the OCSP responder doesn't know about the certificate. + OCSPUnknown = ocsp.Unknown + // OCSPServerFailed means that the OCSP responder failed to process the request. + OCSPServerFailed = ocsp.ServerFailed +) + +// Constants for OCSP must staple +var ( + tlsFeatureExtensionOID = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24} + ocspMustStapleFeature = []byte{0x30, 0x03, 0x02, 0x01, 0x05} +) + +// KeyType represents the key algo as well as the key size or curve to use. +type KeyType string + +type DERCertificateBytes []byte + +// ParsePEMBundle parses a certificate bundle from top to bottom and returns +// a slice of x509 certificates. This function will error if no certificates are found. +func ParsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + var certDERBlock *pem.Block + + for { + certDERBlock, bundle = pem.Decode(bundle) + if certDERBlock == nil { + break + } + + if certDERBlock.Type == "CERTIFICATE" { + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + certificates = append(certificates, cert) + } + } + + if len(certificates) == 0 { + return nil, errors.New("no certificates were found while parsing the bundle") + } + + return certificates, nil +} + +func ParsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { + keyBlock, _ := pem.Decode(key) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + default: + return nil, errors.New("unknown PEM header value") + } +} + +func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { + switch keyType { + case EC256: + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case EC384: + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case RSA2048: + return rsa.GenerateKey(rand.Reader, 2048) + case RSA4096: + return rsa.GenerateKey(rand.Reader, 4096) + case RSA8192: + return rsa.GenerateKey(rand.Reader, 8192) + } + + return nil, fmt.Errorf("invalid KeyType: %s", keyType) +} + +func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { + template := x509.CertificateRequest{ + Subject: pkix.Name{CommonName: domain}, + DNSNames: san, + } + + if mustStaple { + template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{ + Id: tlsFeatureExtensionOID, + Value: ocspMustStapleFeature, + }) + } + + return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) +} + +func PEMEncode(data interface{}) []byte { + var pemBlock *pem.Block + switch key := data.(type) { + case *ecdsa.PrivateKey: + keyBytes, _ := x509.MarshalECPrivateKey(key) + pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} + case *rsa.PrivateKey: + pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} + case *x509.CertificateRequest: + pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw} + case DERCertificateBytes: + pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(DERCertificateBytes))} + } + + return pem.EncodeToMemory(pemBlock) +} + +func pemDecode(data []byte) (*pem.Block, error) { + pemBlock, _ := pem.Decode(data) + if pemBlock == nil { + return nil, fmt.Errorf("PEM decode did not yield a valid block. Is the certificate in the right format?") + } + + return pemBlock, nil +} + +func PemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) { + pemBlock, err := pemDecode(pem) + if pemBlock == nil { + return nil, err + } + + if pemBlock.Type != "CERTIFICATE REQUEST" { + return nil, fmt.Errorf("PEM block is not a certificate request") + } + + return x509.ParseCertificateRequest(pemBlock.Bytes) +} + +// ParsePEMCertificate returns Certificate from a PEM encoded certificate. +// The certificate has to be PEM encoded. Any other encodings like DER will fail. +func ParsePEMCertificate(cert []byte) (*x509.Certificate, error) { + pemBlock, err := pemDecode(cert) + if pemBlock == nil { + return nil, err + } + + // from a DER encoded certificate + return x509.ParseCertificate(pemBlock.Bytes) +} + +func ExtractDomains(cert *x509.Certificate) []string { + domains := []string{cert.Subject.CommonName} + + // Check for SAN certificate + for _, sanDomain := range cert.DNSNames { + if sanDomain == cert.Subject.CommonName { + continue + } + domains = append(domains, sanDomain) + } + + return domains +} + +func ExtractDomainsCSR(csr *x509.CertificateRequest) []string { + domains := []string{csr.Subject.CommonName} + + // loop over the SubjectAltName DNS names + for _, sanName := range csr.DNSNames { + if containsSAN(domains, sanName) { + // Duplicate; skip this name + continue + } + + // Name is unique + domains = append(domains, sanName) + } + + return domains +} + +func containsSAN(domains []string, sanName string) bool { + for _, existingName := range domains { + if existingName == sanName { + return true + } + } + return false +} + +func GeneratePemCert(privateKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) { + derBytes, err := generateDerCert(privateKey, time.Time{}, domain, extensions) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil +} + +func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + if expiration.IsZero() { + expiration = time.Now().Add(365) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "ACME Challenge TEMP", + }, + NotBefore: time.Now(), + NotAfter: expiration, + + KeyUsage: x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + DNSNames: []string{domain}, + ExtraExtensions: extensions, + } + + return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) +} diff --git a/certcrypto/crypto_test.go b/certcrypto/crypto_test.go new file mode 100644 index 00000000..27e3b412 --- /dev/null +++ b/certcrypto/crypto_test.go @@ -0,0 +1,149 @@ +package certcrypto + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGeneratePrivateKey(t *testing.T) { + key, err := GeneratePrivateKey(RSA2048) + require.NoError(t, err, "Error generating private key") + + assert.NotNil(t, key) +} + +func TestGenerateCSR(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err, "Error generating private key") + + type expected struct { + len int + error bool + } + + testCases := []struct { + desc string + privateKey crypto.PrivateKey + domain string + san []string + mustStaple bool + expected expected + }{ + { + desc: "without SAN", + privateKey: privateKey, + domain: "lego.acme", + mustStaple: true, + expected: expected{len: 245}, + }, + { + desc: "without SAN", + privateKey: privateKey, + domain: "lego.acme", + san: []string{}, + mustStaple: true, + expected: expected{len: 245}, + }, + { + desc: "with SAN", + privateKey: privateKey, + domain: "lego.acme", + san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, + mustStaple: true, + expected: expected{len: 296}, + }, + { + desc: "no domain", + privateKey: privateKey, + domain: "", + mustStaple: true, + expected: expected{len: 225}, + }, + { + desc: "no domain with SAN", + privateKey: privateKey, + domain: "", + san: []string{"a.lego.acme", "b.lego.acme", "c.lego.acme"}, + mustStaple: true, + expected: expected{len: 276}, + }, + { + desc: "private key nil", + privateKey: nil, + domain: "fizz.buzz", + mustStaple: true, + expected: expected{error: true}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + csr, err := GenerateCSR(test.privateKey, test.domain, test.san, test.mustStaple) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err, "Error generating CSR") + + assert.NotEmpty(t, csr) + assert.Len(t, csr, test.expected.len) + } + }) + } +} + +func TestPEMEncode(t *testing.T) { + buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") + + reader := MockRandReader{b: buf} + key, err := rsa.GenerateKey(reader, 32) + require.NoError(t, err, "Error generating private key") + + data := PEMEncode(key) + require.NotNil(t, data) + assert.Len(t, data, 127) +} + +func TestParsePEMCertificate(t *testing.T) { + privateKey, err := GeneratePrivateKey(RSA2048) + require.NoError(t, err, "Error generating private key") + + expiration := time.Now().Add(365).Round(time.Second) + certBytes, err := generateDerCert(privateKey.(*rsa.PrivateKey), expiration, "test.com", nil) + require.NoError(t, err, "Error generating cert") + + buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") + + // Some random string should return an error. + cert, err := ParsePEMCertificate(buf.Bytes()) + require.Errorf(t, err, "returned %v", cert) + + // A DER encoded certificate should return an error. + _, err = ParsePEMCertificate(certBytes) + require.Error(t, err, "Expected to return an error for DER certificates") + + // A PEM encoded certificate should work ok. + pemCert := PEMEncode(DERCertificateBytes(certBytes)) + cert, err = ParsePEMCertificate(pemCert) + require.NoError(t, err) + + assert.Equal(t, expiration.UTC(), cert.NotAfter) +} + +type MockRandReader struct { + b *bytes.Buffer +} + +func (r MockRandReader) Read(p []byte) (int, error) { + return r.b.Read(p) +} diff --git a/certificate/authorization.go b/certificate/authorization.go new file mode 100644 index 00000000..c35de109 --- /dev/null +++ b/certificate/authorization.go @@ -0,0 +1,69 @@ +package certificate + +import ( + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/log" +) + +const ( + // overallRequestLimit is the overall number of request per second + // limited on the "new-reg", "new-authz" and "new-cert" endpoints. + // From the documentation the limitation is 20 requests per second, + // but using 20 as value doesn't work but 18 do + overallRequestLimit = 18 +) + +func (c *Certifier) getAuthorizations(order acme.ExtendedOrder) ([]acme.Authorization, error) { + resc, errc := make(chan acme.Authorization), make(chan domainError) + + delay := time.Second / overallRequestLimit + + for _, authzURL := range order.Authorizations { + time.Sleep(delay) + + go func(authzURL string) { + authz, err := c.core.Authorizations.Get(authzURL) + if err != nil { + errc <- domainError{Domain: authz.Identifier.Value, Error: err} + return + } + + resc <- authz + }(authzURL) + } + + var responses []acme.Authorization + failures := make(obtainError) + for i := 0; i < len(order.Authorizations); i++ { + select { + case res := <-resc: + responses = append(responses, res) + case err := <-errc: + failures[err.Domain] = err.Error + } + } + + for i, auth := range order.Authorizations { + log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth) + } + + close(resc) + close(errc) + + // be careful to not return an empty failures map; + // even if empty, they become non-nil error values + if len(failures) > 0 { + return responses, failures + } + return responses, nil +} + +func (c *Certifier) deactivateAuthorizations(order acme.ExtendedOrder) { + for _, auth := range order.Authorizations { + if err := c.core.Authorizations.Deactivate(auth); err != nil { + log.Infof("Unable to deactivated authorizations: %s", auth) + } + } +} diff --git a/certificate/certificates.go b/certificate/certificates.go new file mode 100644 index 00000000..e9c04197 --- /dev/null +++ b/certificate/certificates.go @@ -0,0 +1,493 @@ +package certificate + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/log" + "golang.org/x/crypto/ocsp" + "golang.org/x/net/idna" +) + +// maxBodySize is the maximum size of body that we will read. +const maxBodySize = 1024 * 1024 + +// Resource represents a CA issued certificate. +// PrivateKey, Certificate and IssuerCertificate are all +// already PEM encoded and can be directly written to disk. +// Certificate may be a certificate bundle, +// depending on the options supplied to create it. +type Resource struct { + Domain string `json:"domain"` + CertURL string `json:"certUrl"` + CertStableURL string `json:"certStableUrl"` + PrivateKey []byte `json:"-"` + Certificate []byte `json:"-"` + IssuerCertificate []byte `json:"-"` + CSR []byte `json:"-"` +} + +// ObtainRequest The request to obtain certificate. +// +// The first domain in domains is used for the CommonName field of the certificate, +// all other domains are added using the Subject Alternate Names extension. +// +// A new private key is generated for every invocation of the function Obtain. +// If you do not want that you can supply your own private key in the privateKey parameter. +// If this parameter is non-nil it will be used instead of generating a new one. +// +// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. +type ObtainRequest struct { + Domains []string + Bundle bool + PrivateKey crypto.PrivateKey + MustStaple bool +} + +type resolver interface { + Solve(authorizations []acme.Authorization) error +} + +type Certifier struct { + core *api.Core + keyType certcrypto.KeyType + resolver resolver +} + +func NewCertifier(core *api.Core, keyType certcrypto.KeyType, resolver resolver) *Certifier { + return &Certifier{ + core: core, + keyType: keyType, + resolver: resolver, + } +} + +// Obtain tries to obtain a single certificate using all domains passed into it. +// +// This function will never return a partial certificate. +// If one domain in the list fails, the whole certificate will fail. +func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) { + if len(request.Domains) == 0 { + return nil, errors.New("no domains to obtain a certificate for") + } + + domains := sanitizeDomain(request.Domains) + + if request.Bundle { + log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) + } else { + log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) + } + + order, err := c.core.Orders.New(domains) + if err != nil { + return nil, err + } + + authz, err := c.getAuthorizations(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + c.deactivateAuthorizations(order) + return nil, err + } + + err = c.resolver.Solve(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err + } + + log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + failures := make(obtainError) + cert, err := c.getForOrder(domains, order, request.Bundle, request.PrivateKey, request.MustStaple) + if err != nil { + for _, auth := range authz { + failures[challenge.GetTargetedDomain(auth)] = err + } + } + + // Do not return an empty failures map, because + // it would still be a non-nil error value + if len(failures) > 0 { + return cert, failures + } + return cert, nil +} + +// ObtainForCSR tries to obtain a certificate matching the CSR passed into it. +// +// The domains are inferred from the CommonName and SubjectAltNames, if any. +// The private key for this CSR is not required. +// +// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. +// +// This function will never return a partial certificate. +// If one domain in the list fails, the whole certificate will fail. +func (c *Certifier) ObtainForCSR(csr x509.CertificateRequest, bundle bool) (*Resource, error) { + // figure out what domains it concerns + // start with the common name + domains := certcrypto.ExtractDomainsCSR(&csr) + + if bundle { + log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", ")) + } else { + log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", ")) + } + + order, err := c.core.Orders.New(domains) + if err != nil { + return nil, err + } + + authz, err := c.getAuthorizations(order) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + c.deactivateAuthorizations(order) + return nil, err + } + + err = c.resolver.Solve(authz) + if err != nil { + // If any challenge fails, return. Do not generate partial SAN certificates. + return nil, err + } + + log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) + + failures := make(obtainError) + cert, err := c.getForCSR(domains, order, bundle, csr.Raw, nil) + if err != nil { + for _, auth := range authz { + failures[challenge.GetTargetedDomain(auth)] = err + } + } + + if cert != nil { + // Add the CSR to the certificate so that it can be used for renewals. + cert.CSR = certcrypto.PEMEncode(&csr) + } + + // Do not return an empty failures map, + // because it would still be a non-nil error value + if len(failures) > 0 { + return cert, failures + } + return cert, nil +} + +func (c *Certifier) getForOrder(domains []string, order acme.ExtendedOrder, bundle bool, privateKey crypto.PrivateKey, mustStaple bool) (*Resource, error) { + if privateKey == nil { + var err error + privateKey, err = certcrypto.GeneratePrivateKey(c.keyType) + if err != nil { + return nil, err + } + } + + // Determine certificate name(s) based on the authorization resources + commonName := domains[0] + + // ACME draft Section 7.4 "Applying for Certificate Issuance" + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4 + // says: + // Clients SHOULD NOT make any assumptions about the sort order of + // "identifiers" or "authorizations" elements in the returned order + // object. + san := []string{commonName} + for _, auth := range order.Identifiers { + if auth.Value != commonName { + san = append(san, auth.Value) + } + } + + // TODO: should the CSR be customizable? + csr, err := certcrypto.GenerateCSR(privateKey, commonName, san, mustStaple) + if err != nil { + return nil, err + } + + return c.getForCSR(domains, order, bundle, csr, certcrypto.PEMEncode(privateKey)) +} + +func (c *Certifier) getForCSR(domains []string, order acme.ExtendedOrder, bundle bool, csr []byte, privateKeyPem []byte) (*Resource, error) { + respOrder, err := c.core.Orders.UpdateForCSR(order.Finalize, csr) + if err != nil { + return nil, err + } + + commonName := domains[0] + certRes := &Resource{ + Domain: commonName, + CertURL: respOrder.Certificate, + PrivateKey: privateKeyPem, + } + + if respOrder.Status == acme.StatusValid { + // if the certificate is available right away, short cut! + ok, err := c.checkResponse(respOrder, certRes, bundle) + if err != nil { + return nil, err + } + + if ok { + return certRes, nil + } + } + + return c.waitForCertificate(certRes, order.Location, bundle) +} + +func (c *Certifier) waitForCertificate(certRes *Resource, orderURL string, bundle bool) (*Resource, error) { + stopTimer := time.NewTimer(30 * time.Second) + defer stopTimer.Stop() + retryTick := time.NewTicker(500 * time.Millisecond) + defer retryTick.Stop() + + for { + select { + case <-stopTimer.C: + return nil, errors.New("certificate polling timed out") + case <-retryTick.C: + order, err := c.core.Orders.Get(orderURL) + if err != nil { + return nil, err + } + + done, err := c.checkResponse(order, certRes, bundle) + if err != nil { + return nil, err + } + if done { + return certRes, nil + } + } + } +} + +// checkResponse checks to see if the certificate is ready and a link is contained in the response. +// +// If so, loads it into certRes and returns true. +// If the cert is not yet ready, it returns false. +// +// The certRes input should already have the Domain (common name) field populated. +// +// If bundle is true, the certificate will be bundled with the issuer's cert. +func (c *Certifier) checkResponse(order acme.Order, certRes *Resource, bundle bool) (bool, error) { + valid, err := checkOrderStatus(order) + if err != nil || !valid { + return valid, err + } + + cert, issuer, err := c.core.Certificates.Get(order.Certificate, bundle) + if err != nil { + return false, err + } + + log.Infof("[%s] Server responded with a certificate.", certRes.Domain) + + certRes.IssuerCertificate = issuer + certRes.Certificate = cert + certRes.CertURL = order.Certificate + certRes.CertStableURL = order.Certificate + + return true, nil +} + +// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA. +func (c *Certifier) Revoke(cert []byte) error { + certificates, err := certcrypto.ParsePEMBundle(cert) + if err != nil { + return err + } + + x509Cert := certificates[0] + if x509Cert.IsCA { + return fmt.Errorf("certificate bundle starts with a CA certificate") + } + + revokeMsg := acme.RevokeCertMessage{ + Certificate: base64.RawURLEncoding.EncodeToString(x509Cert.Raw), + } + + return c.core.Certificates.Revoke(revokeMsg) +} + +// Renew takes a Resource and tries to renew the certificate. +// +// If the renewal process succeeds, the new certificate will ge returned in a new CertResource. +// Please be aware that this function will return a new certificate in ANY case that is not an error. +// If the server does not provide us with a new cert on a GET request to the CertURL +// this function will start a new-cert flow where a new certificate gets generated. +// +// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle. +// +// For private key reuse the PrivateKey property of the passed in Resource should be non-nil. +func (c *Certifier) Renew(certRes Resource, bundle, mustStaple bool) (*Resource, error) { + // Input certificate is PEM encoded. + // Decode it here as we may need the decoded cert later on in the renewal process. + // The input may be a bundle or a single certificate. + certificates, err := certcrypto.ParsePEMBundle(certRes.Certificate) + if err != nil { + return nil, err + } + + x509Cert := certificates[0] + if x509Cert.IsCA { + return nil, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", certRes.Domain) + } + + // This is just meant to be informal for the user. + timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) + log.Infof("[%s] acme: Trying renewal with %d hours remaining", certRes.Domain, int(timeLeft.Hours())) + + // We always need to request a new certificate to renew. + // Start by checking to see if the certificate was based off a CSR, + // and use that if it's defined. + if len(certRes.CSR) > 0 { + csr, errP := certcrypto.PemDecodeTox509CSR(certRes.CSR) + if errP != nil { + return nil, errP + } + + return c.ObtainForCSR(*csr, bundle) + } + + var privateKey crypto.PrivateKey + if certRes.PrivateKey != nil { + privateKey, err = certcrypto.ParsePEMPrivateKey(certRes.PrivateKey) + if err != nil { + return nil, err + } + } + + query := ObtainRequest{ + Domains: certcrypto.ExtractDomains(x509Cert), + Bundle: bundle, + PrivateKey: privateKey, + MustStaple: mustStaple, + } + return c.Obtain(query) +} + +// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response, +// the parsed response, and an error, if any. +// +// The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate. +// If the bundle only contains the issued certificate, +// this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate. +// +// If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown. +func (c *Certifier) GetOCSP(bundle []byte) ([]byte, *ocsp.Response, error) { + certificates, err := certcrypto.ParsePEMBundle(bundle) + if err != nil { + return nil, nil, err + } + + // We expect the certificate slice to be ordered downwards the chain. + // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, + // which should always be the first two certificates. + // If there's no OCSP server listed in the leaf cert, there's nothing to do. + // And if we have only one certificate so far, we need to get the issuer cert. + + issuedCert := certificates[0] + + if len(issuedCert.OCSPServer) == 0 { + return nil, nil, errors.New("no OCSP server specified in cert") + } + + if len(certificates) == 1 { + // TODO: build fallback. If this fails, check the remaining array entries. + if len(issuedCert.IssuingCertificateURL) == 0 { + return nil, nil, errors.New("no issuing certificate URL") + } + + resp, errC := c.core.HTTPClient.Get(issuedCert.IssuingCertificateURL[0]) + if errC != nil { + return nil, nil, errC + } + defer resp.Body.Close() + + issuerBytes, errC := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) + if errC != nil { + return nil, nil, errC + } + + issuerCert, errC := x509.ParseCertificate(issuerBytes) + if errC != nil { + return nil, nil, errC + } + + // Insert it into the slice on position 0 + // We want it ordered right SRV CRT -> CA + certificates = append(certificates, issuerCert) + } + + issuerCert := certificates[1] + + // Finally kick off the OCSP request. + ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) + if err != nil { + return nil, nil, err + } + + resp, err := c.core.HTTPClient.Post(issuedCert.OCSPServer[0], "application/ocsp-request", bytes.NewReader(ocspReq)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + ocspResBytes, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize)) + if err != nil { + return nil, nil, err + } + + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) + if err != nil { + return nil, nil, err + } + + return ocspResBytes, ocspRes, nil +} + +func checkOrderStatus(order acme.Order) (bool, error) { + switch order.Status { + case acme.StatusValid: + return true, nil + case acme.StatusInvalid: + return false, order.Error + default: + return false, nil + } +} + +// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-7.1.4 +// The domain name MUST be encoded +// in the form in which it would appear in a certificate. That is, it +// MUST be encoded according to the rules in Section 7 of [RFC5280]. +// +// https://tools.ietf.org/html/rfc5280#section-7 +func sanitizeDomain(domains []string) []string { + var sanitizedDomains []string + for _, domain := range domains { + sanitizedDomain, err := idna.ToASCII(domain) + if err != nil { + log.Infof("skip domain %q: unable to sanitize (punnycode): %v", domain, err) + } else { + sanitizedDomains = append(sanitizedDomains, sanitizedDomain) + } + } + return sanitizedDomains +} diff --git a/certificate/certificates_test.go b/certificate/certificates_test.go new file mode 100644 index 00000000..0c01a02b --- /dev/null +++ b/certificate/certificates_test.go @@ -0,0 +1,211 @@ +package certificate + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/pem" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/platform/tester" +) + +const certResponseMock = `-----BEGIN CERTIFICATE----- +MIIDEDCCAfigAwIBAgIHPhckqW5fPDANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQD +Ex1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDM5NWU2MTAeFw0xODExMDcxNzQ2NTZa +Fw0yMzExMDcxNzQ2NTZaMBMxETAPBgNVBAMTCGFjbWUud3RmMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwtLNKvZXD20XPUQCWYSK9rUSKxD9Eb0c9fag +bxOxOkLRTgL8LH6yln+bxc3MrHDou4PpDUdeo2CyOQu3CKsTS5mrH3NXYHu0H7p5 +y3riOJTHnfkGKLT9LciGz7GkXd62nvNP57bOf5Sk4P2M+Qbxd0hPTSfu52740LSy +144cnxe2P1aDYehrEp6nYCESuyD/CtUHTo0qwJmzIy163Sp3rSs15BuCPyhySnE3 +BJ8Ggv+qC6D5I1932DfSqyQJ79iq/HRm0Fn84am3KwvRlUfWxabmsUGARXoqCgnE +zcbJVOZKewv0zlQJpfac+b+Imj6Lvt1TGjIz2mVyefYgLx8gwwIDAQABo1QwUjAO +BgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwG +A1UdEwEB/wQCMAAwEwYDVR0RBAwwCoIIYWNtZS53dGYwDQYJKoZIhvcNAQELBQAD +ggEBABB/0iYhmfPSQot5RaeeovQnsqYjI5ryQK2cwzW6qcTJfv8N6+p6XkqF1+W4 +jXZjrQP8MvgO9KNWlvx12vhINE6wubk88L+2piAi5uS2QejmZbXpyYB9s+oPqlk9 +IDvfdlVYOqvYAhSx7ggGi+j73mjZVtjAavP6dKuu475ZCeq+NIC15RpbbikWKtYE +HBJ7BW8XQKx67iHGx8ygHTDLbREL80Bck3oUm7wIYGMoNijD6RBl25p4gYl9dzOd +TqGl5hW/1P5hMbgEzHbr4O3BfWqU2g7tV36TASy3jbC3ONFRNNYrpEZ1AL3+cUri +OPPkKtAKAbQkKbUIfsHpBZjKZMU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw +NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl +NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT +SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh +0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen +SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx +HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt +D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu +mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm +iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd +QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ +wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv +rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 +7R4IbHGnj0BJA2vMYC4hSw== +-----END CERTIFICATE----- +` + +const issuerMock = `-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIIOV5hkYJx0JwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVUGViYmxlIFJvb3QgQ0EgNTBmZmJkMB4XDTE4MTEwNzE3NDY0N1oXDTQ4MTEw +NzE3NDY0N1owKDEmMCQGA1UEAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAzOTVl +NjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCacwXN4LmyRTgYS8TT +SZYgz758npHiPTBDKgeN5WVmkkwW0TuN4W2zXhEmcM82uxOEjWS2drvK0+iJKneh +0fQR8ZF35dIYFe8WXTg3kEmqcizSgh4LxlOntsXvatfX/6GU/ADo3xAFoBKCijen +SRBIY65yq5m00cWx3RMIcQq1B0X8nJS0O1P7MYE/Vvidz5St/36RXVu1oWLeS5Fx +HAezW0lqxEUzvC+uLTFWC6f/CilzmI7SsPAkZBk7dO5Qs0d7m/zWF588vlGS+0pt +D1on+lU85Ma2zuAd0qmB6LY66N8pEKKtMk93wF/o4Z5i58ahbwNvTKAzz4JSRWSu +mB9LAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +upU0DjzvIvoCOYKbq1RRN7rPdYad39mfjxgkeV0iOF5JoIdO6y1C7XAm9lT69Wjm +iUPvnCTMDYft40N2SvmXuuMaPOm4zjNwn4K33jw5XBnpwxC7By/Y0oV+Sl10fBsd +QqXC6H7LcSGkv+4eJbgY33P4uH5ZAy+2TkHUuZDkpufkAshzBust7nDAjfv3AIuQ +wlPoyZfI11eqyiOqRzOq+B5dIBr1JzKnEzSL6n0JLNQiPO7iN03rud/wYD3gbmcv +rzFL1KZfz+HZdnFwFW2T2gVW8L3ii1l9AJDuKzlvjUH3p6bgihVq02sjT8mx+GM2 +7R4IbHGnj0BJA2vMYC4hSw== +-----END CERTIFICATE----- +` + +func Test_checkResponse(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + + order := acme.Order{ + Status: acme.StatusValid, + Certificate: apiURL + "/certificate", + } + certRes := &Resource{} + bundle := false + + valid, err := certifier.checkResponse(order, certRes, bundle) + require.NoError(t, err) + assert.True(t, valid) + assert.NotNil(t, certRes) + assert.Equal(t, "", certRes.Domain) + assert.Contains(t, certRes.CertStableURL, "/certificate") + assert.Contains(t, certRes.CertURL, "/certificate") + assert.Nil(t, certRes.CSR) + assert.Nil(t, certRes.PrivateKey) + assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") + assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") +} + +func Test_checkResponse_issuerRelUp(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Link", "<"+apiURL+`/issuer>; rel="up"`) + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/issuer", func(w http.ResponseWriter, r *http.Request) { + p, _ := pem.Decode([]byte(issuerMock)) + _, err := w.Write(p.Bytes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + + order := acme.Order{ + Status: acme.StatusValid, + Certificate: apiURL + "/certificate", + } + certRes := &Resource{} + bundle := false + + valid, err := certifier.checkResponse(order, certRes, bundle) + require.NoError(t, err) + assert.True(t, valid) + assert.NotNil(t, certRes) + assert.Equal(t, "", certRes.Domain) + assert.Contains(t, certRes.CertStableURL, "/certificate") + assert.Contains(t, certRes.CertURL, "/certificate") + assert.Nil(t, certRes.CSR) + assert.Nil(t, certRes.PrivateKey) + assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") + assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") +} + +func Test_checkResponse_embeddedIssuer(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/certificate", func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(certResponseMock)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + certifier := NewCertifier(core, certcrypto.RSA2048, &resolverMock{}) + + order := acme.Order{ + Status: acme.StatusValid, + Certificate: apiURL + "/certificate", + } + certRes := &Resource{} + bundle := false + + valid, err := certifier.checkResponse(order, certRes, bundle) + require.NoError(t, err) + assert.True(t, valid) + assert.NotNil(t, certRes) + assert.Equal(t, "", certRes.Domain) + assert.Contains(t, certRes.CertStableURL, "/certificate") + assert.Contains(t, certRes.CertURL, "/certificate") + assert.Nil(t, certRes.CSR) + assert.Nil(t, certRes.PrivateKey) + assert.Equal(t, certResponseMock, string(certRes.Certificate), "Certificate") + assert.Equal(t, issuerMock, string(certRes.IssuerCertificate), "IssuerCertificate") +} + +type resolverMock struct { + error error +} + +func (r *resolverMock) Solve(authorizations []acme.Authorization) error { + return r.error +} diff --git a/certificate/errors.go b/certificate/errors.go new file mode 100644 index 00000000..0fec7c16 --- /dev/null +++ b/certificate/errors.go @@ -0,0 +1,30 @@ +package certificate + +import ( + "bytes" + "fmt" + "sort" +) + +// obtainError is returned when there are specific errors available per domain. +type obtainError map[string]error + +func (e obtainError) Error() string { + buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") + + var domains []string + for domain := range e { + domains = append(domains, domain) + } + sort.Strings(domains) + + for _, domain := range domains { + buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain])) + } + return buffer.String() +} + +type domainError struct { + Domain string + Error error +} diff --git a/challenge/challenges.go b/challenge/challenges.go new file mode 100644 index 00000000..e8c862ea --- /dev/null +++ b/challenge/challenges.go @@ -0,0 +1,44 @@ +package challenge + +import ( + "fmt" + + "github.com/xenolf/lego/acme" +) + +// Type is a string that identifies a particular challenge type and version of ACME challenge. +type Type string + +const ( + // HTTP01 is the "http-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.3 + // Note: ChallengePath returns the URL path to fulfill this challenge + HTTP01 = Type("http-01") + + // DNS01 is the "dns-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-8.4 + // Note: GetRecord returns a DNS record which will fulfill this challenge + DNS01 = Type("dns-01") + + // TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05 + TLSALPN01 = Type("tls-alpn-01") +) + +func (t Type) String() string { + return string(t) +} + +func FindChallenge(chlgType Type, authz acme.Authorization) (acme.Challenge, error) { + for _, chlg := range authz.Challenges { + if chlg.Type == string(chlgType) { + return chlg, nil + } + } + + return acme.Challenge{}, fmt.Errorf("[%s] acme: unable to find challenge %s", GetTargetedDomain(authz), chlgType) +} + +func GetTargetedDomain(authz acme.Authorization) string { + if authz.Wildcard { + return "*." + authz.Identifier.Value + } + return authz.Identifier.Value +} diff --git a/challenge/dns01/dns_challenge.go b/challenge/dns01/dns_challenge.go new file mode 100644 index 00000000..1dc457af --- /dev/null +++ b/challenge/dns01/dns_challenge.go @@ -0,0 +1,174 @@ +package dns01 + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/platform/wait" +) + +const ( + // DefaultPropagationTimeout default propagation timeout + DefaultPropagationTimeout = 60 * time.Second + + // DefaultPollingInterval default polling interval + DefaultPollingInterval = 2 * time.Second + + // DefaultTTL default TTL + DefaultTTL = 120 +) + +type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error + +type ChallengeOption func(*Challenge) error + +// CondOption Conditional challenge option. +func CondOption(condition bool, opt ChallengeOption) ChallengeOption { + if !condition { + // NoOp options + return func(*Challenge) error { + return nil + } + } + return opt +} + +// Challenge implements the dns-01 challenge +type Challenge struct { + core *api.Core + validate ValidateFunc + provider challenge.Provider + preCheck preCheck + dnsTimeout time.Duration +} + +func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge { + chlg := &Challenge{ + core: core, + validate: validate, + provider: provider, + preCheck: newPreCheck(), + dnsTimeout: 10 * time.Second, + } + + for _, opt := range opts { + err := opt(chlg) + if err != nil { + log.Infof("challenge option error: %v", err) + } + } + + return chlg +} + +// PreSolve just submits the txt record to the dns provider. +// It does not validate record propagation, or do anything at all with the acme server. +func (c *Challenge) PreSolve(authz acme.Authorization) error { + domain := challenge.GetTargetedDomain(authz) + log.Infof("[%s] acme: Preparing to solve DNS-01", domain) + + chlng, err := challenge.FindChallenge(challenge.DNS01, authz) + if err != nil { + return err + } + + if c.provider == nil { + return fmt.Errorf("[%s] acme: no DNS Provider configured", domain) + } + + // Generate the Key Authorization for the challenge + keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) + if err != nil { + return err + } + + err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("[%s] acme: error presenting token: %s", domain, err) + } + + return nil +} + +func (c *Challenge) Solve(authz acme.Authorization) error { + domain := challenge.GetTargetedDomain(authz) + log.Infof("[%s] acme: Trying to solve DNS-01", domain) + + chlng, err := challenge.FindChallenge(challenge.DNS01, authz) + if err != nil { + return err + } + + // Generate the Key Authorization for the challenge + keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) + if err != nil { + return err + } + + fqdn, value := GetRecord(authz.Identifier.Value, keyAuth) + + var timeout, interval time.Duration + switch provider := c.provider.(type) { + case challenge.ProviderTimeout: + timeout, interval = provider.Timeout() + default: + timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval + } + + log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers) + + err = wait.For(timeout, interval, func() (bool, error) { + stop, errP := c.preCheck.call(fqdn, value) + if !stop || errP != nil { + log.Infof("[%s] acme: Waiting for DNS record propagation.", domain) + } + return stop, errP + }) + if err != nil { + return err + } + + chlng.KeyAuthorization = keyAuth + return c.validate(c.core, authz.Identifier.Value, chlng) +} + +// CleanUp cleans the challenge. +func (c *Challenge) CleanUp(authz acme.Authorization) error { + chlng, err := challenge.FindChallenge(challenge.DNS01, authz) + if err != nil { + return err + } + + keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) + if err != nil { + return err + } + + return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) +} + +func (c *Challenge) Sequential() (bool, time.Duration) { + if p, ok := c.provider.(sequential); ok { + return ok, p.Sequential() + } + return false, 0 +} + +type sequential interface { + Sequential() time.Duration +} + +// GetRecord returns a DNS record which will fulfill the `dns-01` challenge +func GetRecord(domain, keyAuth string) (fqdn string, value string) { + keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) + // base64URL encoding without padding + value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) + fqdn = fmt.Sprintf("_acme-challenge.%s.", domain) + return +} diff --git a/challenge/dns01/dns_challenge_manual.go b/challenge/dns01/dns_challenge_manual.go new file mode 100644 index 00000000..490108dd --- /dev/null +++ b/challenge/dns01/dns_challenge_manual.go @@ -0,0 +1,52 @@ +package dns01 + +import ( + "bufio" + "fmt" + "os" +) + +const ( + dnsTemplate = `%s %d IN TXT "%s"` +) + +// DNSProviderManual is an implementation of the ChallengeProvider interface +type DNSProviderManual struct{} + +// NewDNSProviderManual returns a DNSProviderManual instance. +func NewDNSProviderManual() (*DNSProviderManual, error) { + return &DNSProviderManual{}, nil +} + +// Present prints instructions for manually creating the TXT record +func (*DNSProviderManual) Present(domain, token, keyAuth string) error { + fqdn, value := GetRecord(domain, keyAuth) + + authZone, err := FindZoneByFqdn(fqdn) + if err != nil { + return err + } + + fmt.Printf("lego: Please create the following TXT record in your %s zone:\n", authZone) + fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, value) + fmt.Printf("lego: Press 'Enter' when you are done\n") + + _, err = bufio.NewReader(os.Stdin).ReadBytes('\n') + + return err +} + +// CleanUp prints instructions for manually removing the TXT record +func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := GetRecord(domain, keyAuth) + + authZone, err := FindZoneByFqdn(fqdn) + if err != nil { + return err + } + + fmt.Printf("lego: You can now remove this TXT record from your %s zone:\n", authZone) + fmt.Printf(dnsTemplate+"\n", fqdn, DefaultTTL, "...") + + return nil +} diff --git a/challenge/dns01/dns_challenge_manual_test.go b/challenge/dns01/dns_challenge_manual_test.go new file mode 100644 index 00000000..8802c35a --- /dev/null +++ b/challenge/dns01/dns_challenge_manual_test.go @@ -0,0 +1,61 @@ +package dns01 + +import ( + "io" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDNSProviderManual(t *testing.T) { + backupStdin := os.Stdin + defer func() { os.Stdin = backupStdin }() + + testCases := []struct { + desc string + input string + expectError bool + }{ + { + desc: "Press enter", + input: "ok\n", + }, + { + desc: "Missing enter", + input: "ok", + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + file, err := ioutil.TempFile("", "lego_test") + assert.NoError(t, err) + defer func() { _ = os.Remove(file.Name()) }() + + _, err = io.WriteString(file, test.input) + assert.NoError(t, err) + + _, err = file.Seek(0, io.SeekStart) + assert.NoError(t, err) + + os.Stdin = file + + manualProvider, err := NewDNSProviderManual() + require.NoError(t, err) + + err = manualProvider.Present("example.com", "", "") + if test.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + + err = manualProvider.CleanUp("example.com", "", "") + require.NoError(t, err) + } + }) + } +} diff --git a/challenge/dns01/dns_challenge_test.go b/challenge/dns01/dns_challenge_test.go new file mode 100644 index 00000000..93bec0ce --- /dev/null +++ b/challenge/dns01/dns_challenge_test.go @@ -0,0 +1,285 @@ +package dns01 + +import ( + "crypto/rand" + "crypto/rsa" + "errors" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/platform/tester" +) + +type providerMock struct { + present, cleanUp error +} + +func (p *providerMock) Present(domain, token, keyAuth string) error { return p.present } +func (p *providerMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp } + +type providerTimeoutMock struct { + present, cleanUp error + timeout, interval time.Duration +} + +func (p *providerTimeoutMock) Present(domain, token, keyAuth string) error { return p.present } +func (p *providerTimeoutMock) CleanUp(domain, token, keyAuth string) error { return p.cleanUp } +func (p *providerTimeoutMock) Timeout() (time.Duration, time.Duration) { return p.timeout, p.interval } + +func TestChallenge_PreSolve(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err) + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + testCases := []struct { + desc string + validate ValidateFunc + preCheck PreCheckFunc + provider challenge.Provider + expectError bool + }{ + { + desc: "success", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{}, + }, + { + desc: "validate fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: nil, + cleanUp: nil, + }, + }, + { + desc: "preCheck fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return false, errors.New("OOPS") }, + provider: &providerTimeoutMock{ + timeout: 2 * time.Second, + interval: 500 * time.Millisecond, + }, + }, + { + desc: "present fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: errors.New("OOPS"), + }, + expectError: true, + }, + { + desc: "cleanUp fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + cleanUp: errors.New("OOPS"), + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + + chlg := NewChallenge(core, test.validate, test.provider, AddPreCheck(test.preCheck)) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "example.com", + }, + Challenges: []acme.Challenge{ + {Type: challenge.DNS01.String()}, + }, + } + + err = chlg.PreSolve(authz) + if test.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestChallenge_Solve(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err) + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + testCases := []struct { + desc string + validate ValidateFunc + preCheck PreCheckFunc + provider challenge.Provider + expectError bool + }{ + { + desc: "success", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{}, + }, + { + desc: "validate fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: nil, + cleanUp: nil, + }, + expectError: true, + }, + { + desc: "preCheck fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return false, errors.New("OOPS") }, + provider: &providerTimeoutMock{ + timeout: 2 * time.Second, + interval: 500 * time.Millisecond, + }, + expectError: true, + }, + { + desc: "present fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: errors.New("OOPS"), + }, + }, + { + desc: "cleanUp fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + cleanUp: errors.New("OOPS"), + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + + chlg := NewChallenge(core, test.validate, test.provider, AddPreCheck(test.preCheck)) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "example.com", + }, + Challenges: []acme.Challenge{ + {Type: challenge.DNS01.String()}, + }, + } + + err = chlg.Solve(authz) + if test.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestChallenge_CleanUp(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err) + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + testCases := []struct { + desc string + validate ValidateFunc + preCheck PreCheckFunc + provider challenge.Provider + expectError bool + }{ + { + desc: "success", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{}, + }, + { + desc: "validate fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return errors.New("OOPS") }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: nil, + cleanUp: nil, + }, + }, + { + desc: "preCheck fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return false, errors.New("OOPS") }, + provider: &providerTimeoutMock{ + timeout: 2 * time.Second, + interval: 500 * time.Millisecond, + }, + }, + { + desc: "present fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + present: errors.New("OOPS"), + }, + }, + { + desc: "cleanUp fail", + validate: func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + preCheck: func(_, _ string) (bool, error) { return true, nil }, + provider: &providerMock{ + cleanUp: errors.New("OOPS"), + }, + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + + chlg := NewChallenge(core, test.validate, test.provider, AddPreCheck(test.preCheck)) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "example.com", + }, + Challenges: []acme.Challenge{ + {Type: challenge.DNS01.String()}, + }, + } + + err = chlg.CleanUp(authz) + if test.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/acme/testdata/resolv.conf.1 b/challenge/dns01/fixtures/resolv.conf.1 similarity index 100% rename from acme/testdata/resolv.conf.1 rename to challenge/dns01/fixtures/resolv.conf.1 diff --git a/challenge/dns01/fqdn.go b/challenge/dns01/fqdn.go new file mode 100644 index 00000000..c238c8cf --- /dev/null +++ b/challenge/dns01/fqdn.go @@ -0,0 +1,19 @@ +package dns01 + +// ToFqdn converts the name into a fqdn appending a trailing dot. +func ToFqdn(name string) string { + n := len(name) + if n == 0 || name[n-1] == '.' { + return name + } + return name + "." +} + +// UnFqdn converts the fqdn into a name removing the trailing dot. +func UnFqdn(name string) string { + n := len(name) + if n != 0 && name[n-1] == '.' { + return name[:n-1] + } + return name +} diff --git a/challenge/dns01/fqdn_test.go b/challenge/dns01/fqdn_test.go new file mode 100644 index 00000000..ab7be2a4 --- /dev/null +++ b/challenge/dns01/fqdn_test.go @@ -0,0 +1,66 @@ +package dns01 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToFqdn(t *testing.T) { + testCases := []struct { + desc string + domain string + expected string + }{ + { + desc: "simple", + domain: "foo.bar.com", + expected: "foo.bar.com.", + }, + { + desc: "already FQDN", + domain: "foo.bar.com.", + expected: "foo.bar.com.", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + fqdn := ToFqdn(test.domain) + assert.Equal(t, test.expected, fqdn) + }) + } +} + +func TestUnFqdn(t *testing.T) { + testCases := []struct { + desc string + fqdn string + expected string + }{ + { + desc: "simple", + fqdn: "foo.bar.com.", + expected: "foo.bar.com", + }, + { + desc: "already domain", + fqdn: "foo.bar.com", + expected: "foo.bar.com", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + domain := UnFqdn(test.fqdn) + + assert.Equal(t, test.expected, domain) + }) + } +} diff --git a/challenge/dns01/nameserver.go b/challenge/dns01/nameserver.go new file mode 100644 index 00000000..03f1a8d1 --- /dev/null +++ b/challenge/dns01/nameserver.go @@ -0,0 +1,232 @@ +package dns01 + +import ( + "fmt" + "net" + "strings" + "sync" + "time" + + "github.com/miekg/dns" +) + +const defaultResolvConf = "/etc/resolv.conf" + +// dnsTimeout is used to override the default DNS timeout of 10 seconds. +var dnsTimeout = 10 * time.Second + +var ( + fqdnToZone = map[string]string{} + muFqdnToZone sync.Mutex +) + +var defaultNameservers = []string{ + "google-public-dns-a.google.com:53", + "google-public-dns-b.google.com:53", +} + +// recursiveNameservers are used to pre-check DNS propagation +var recursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers) + +// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing. +func ClearFqdnCache() { + muFqdnToZone.Lock() + fqdnToZone = map[string]string{} + muFqdnToZone.Unlock() +} + +func AddDNSTimeout(timeout time.Duration) ChallengeOption { + return func(_ *Challenge) error { + dnsTimeout = timeout + return nil + } +} + +func AddRecursiveNameservers(nameservers []string) ChallengeOption { + return func(_ *Challenge) error { + recursiveNameservers = ParseNameservers(nameservers) + return nil + } +} + +// getNameservers attempts to get systems nameservers before falling back to the defaults +func getNameservers(path string, defaults []string) []string { + config, err := dns.ClientConfigFromFile(path) + if err != nil || len(config.Servers) == 0 { + return defaults + } + + return ParseNameservers(config.Servers) +} + +func ParseNameservers(servers []string) []string { + var resolvers []string + for _, resolver := range servers { + // ensure all servers have a port number + if _, _, err := net.SplitHostPort(resolver); err != nil { + resolvers = append(resolvers, net.JoinHostPort(resolver, "53")) + } else { + resolvers = append(resolvers, resolver) + } + } + return resolvers +} + +// lookupNameservers returns the authoritative nameservers for the given fqdn. +func lookupNameservers(fqdn string) ([]string, error) { + var authoritativeNss []string + + zone, err := FindZoneByFqdn(fqdn) + if err != nil { + return nil, fmt.Errorf("could not determine the zone: %v", err) + } + + r, err := dnsQuery(zone, dns.TypeNS, recursiveNameservers, true) + if err != nil { + return nil, err + } + + for _, rr := range r.Answer { + if ns, ok := rr.(*dns.NS); ok { + authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) + } + } + + if len(authoritativeNss) > 0 { + return authoritativeNss, nil + } + return nil, fmt.Errorf("could not determine authoritative nameservers") +} + +// FindZoneByFqdn determines the zone apex for the given fqdn +// by recursing up the domain labels until the nameserver returns a SOA record in the answer section. +func FindZoneByFqdn(fqdn string) (string, error) { + return FindZoneByFqdnCustom(fqdn, recursiveNameservers) +} + +// FindZoneByFqdnCustom determines the zone apex for the given fqdn +// by recursing up the domain labels until the nameserver returns a SOA record in the answer section. +func FindZoneByFqdnCustom(fqdn string, nameservers []string) (string, error) { + muFqdnToZone.Lock() + defer muFqdnToZone.Unlock() + + // Do we have it cached? + if zone, ok := fqdnToZone[fqdn]; ok { + return zone, nil + } + + var err error + var in *dns.Msg + + labelIndexes := dns.Split(fqdn) + for _, index := range labelIndexes { + domain := fqdn[index:] + + in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) + if err != nil { + continue + } + + if in == nil { + continue + } + + switch in.Rcode { + case dns.RcodeSuccess: + // Check if we got a SOA RR in the answer section + + if len(in.Answer) == 0 { + continue + } + + // CNAME records cannot/should not exist at the root of a zone. + // So we skip a domain when a CNAME is found. + if dnsMsgContainsCNAME(in) { + continue + } + + for _, ans := range in.Answer { + if soa, ok := ans.(*dns.SOA); ok { + zone := soa.Hdr.Name + fqdnToZone[fqdn] = zone + return zone, nil + } + } + case dns.RcodeNameError: + // NXDOMAIN + default: + // Any response code other than NOERROR and NXDOMAIN is treated as error + return "", fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain) + } + } + + return "", fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err)) +} + +// dnsMsgContainsCNAME checks for a CNAME answer in msg +func dnsMsgContainsCNAME(msg *dns.Msg) bool { + for _, ans := range msg.Answer { + if _, ok := ans.(*dns.CNAME); ok { + return true + } + } + return false +} + +func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) { + m := createDNSMsg(fqdn, rtype, recursive) + + var in *dns.Msg + var err error + + for _, ns := range nameservers { + in, err = sendDNSQuery(m, ns) + if err == nil && len(in.Answer) > 0 { + break + } + } + return in, err +} + +func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { + m := new(dns.Msg) + m.SetQuestion(fqdn, rtype) + m.SetEdns0(4096, false) + + if !recursive { + m.RecursionDesired = false + } + + return m +} + +func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { + udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} + in, _, err := udp.Exchange(m, ns) + + if in != nil && in.Truncated { + tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} + // If the TCP request succeeds, the err will reset to nil + in, _, err = tcp.Exchange(m, ns) + } + + return in, err +} + +func formatDNSError(msg *dns.Msg, err error) string { + var parts []string + + if msg != nil { + parts = append(parts, dns.RcodeToString[msg.Rcode]) + } + + if err != nil { + parts = append(parts, fmt.Sprintf("%v", err)) + } + + if len(parts) > 0 { + return ": " + strings.Join(parts, " ") + } + + return "" +} diff --git a/challenge/dns01/nameserver_test.go b/challenge/dns01/nameserver_test.go new file mode 100644 index 00000000..37b73c78 --- /dev/null +++ b/challenge/dns01/nameserver_test.go @@ -0,0 +1,177 @@ +package dns01 + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLookupNameserversOK(t *testing.T) { + testCases := []struct { + fqdn string + nss []string + }{ + { + fqdn: "books.google.com.ng.", + nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + { + fqdn: "www.google.com.", + nss: []string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."}, + }, + { + fqdn: "physics.georgetown.edu.", + nss: []string{"ns4.georgetown.edu.", "ns5.georgetown.edu.", "ns6.georgetown.edu."}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.fqdn, func(t *testing.T) { + t.Parallel() + + nss, err := lookupNameservers(test.fqdn) + require.NoError(t, err) + + sort.Strings(nss) + sort.Strings(test.nss) + + assert.EqualValues(t, test.nss, nss) + }) + } +} + +func TestLookupNameserversErr(t *testing.T) { + testCases := []struct { + desc string + fqdn string + error string + }{ + { + desc: "invalid tld", + fqdn: "_null.n0n0.", + error: "could not determine the zone", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + _, err := lookupNameservers(test.fqdn) + require.Error(t, err) + assert.Contains(t, err.Error(), test.error) + }) + } +} + +func TestFindZoneByFqdnCustom(t *testing.T) { + testCases := []struct { + desc string + fqdn string + zone string + nameservers []string + expectedError string + }{ + { + desc: "domain is a CNAME", + fqdn: "mail.google.com.", + zone: "google.com.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a non-existent subdomain", + fqdn: "foo.google.com.", + zone: "google.com.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a eTLD", + fqdn: "example.com.ac.", + zone: "ac.", + nameservers: recursiveNameservers, + }, + { + desc: "domain is a cross-zone CNAME", + fqdn: "cross-zone-example.assets.sh.", + zone: "assets.sh.", + nameservers: recursiveNameservers, + }, + { + desc: "NXDOMAIN", + fqdn: "test.loho.jkl.", + zone: "loho.jkl.", + nameservers: []string{"1.1.1.1:53"}, + expectedError: "could not find the start of authority for test.loho.jkl.: NXDOMAIN", + }, + { + desc: "several non existent nameservers", + fqdn: "mail.google.com.", + zone: "google.com.", + nameservers: []string{":7053", ":8053", "1.1.1.1:53"}, + }, + { + desc: "only non existent nameservers", + fqdn: "mail.google.com.", + zone: "google.com.", + nameservers: []string{":7053", ":8053", ":9053"}, + expectedError: "could not find the start of authority for mail.google.com.: read udp", + }, + { + desc: "no nameservers", + fqdn: "test.ldez.com.", + zone: "ldez.com.", + nameservers: []string{}, + expectedError: "could not find the start of authority for test.ldez.com.", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + ClearFqdnCache() + + zone, err := FindZoneByFqdnCustom(test.fqdn, test.nameservers) + if test.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, test.zone, zone) + } + }) + } +} + +func TestResolveConfServers(t *testing.T) { + var testCases = []struct { + fixture string + expected []string + defaults []string + }{ + { + fixture: "fixtures/resolv.conf.1", + defaults: []string{"127.0.0.1:53"}, + expected: []string{"10.200.3.249:53", "10.200.3.250:5353", "[2001:4860:4860::8844]:53", "[10.0.0.1]:5353"}, + }, + { + fixture: "fixtures/resolv.conf.nonexistant", + defaults: []string{"127.0.0.1:53"}, + expected: []string{"127.0.0.1:53"}, + }, + } + + for _, test := range testCases { + t.Run(test.fixture, func(t *testing.T) { + + result := getNameservers(test.fixture, test.defaults) + + sort.Strings(result) + sort.Strings(test.expected) + + assert.Equal(t, test.expected, result) + }) + } +} diff --git a/challenge/dns01/precheck.go b/challenge/dns01/precheck.go new file mode 100644 index 00000000..2639dfea --- /dev/null +++ b/challenge/dns01/precheck.go @@ -0,0 +1,110 @@ +package dns01 + +import ( + "fmt" + "net" + "strings" + + "github.com/miekg/dns" +) + +// PreCheckFunc checks DNS propagation before notifying ACME that the DNS challenge is ready. +type PreCheckFunc func(fqdn, value string) (bool, error) + +func AddPreCheck(preCheck PreCheckFunc) ChallengeOption { + // Prevent race condition + check := preCheck + return func(chlg *Challenge) error { + chlg.preCheck.checkFunc = check + return nil + } +} + +func DisableCompletePropagationRequirement() ChallengeOption { + return func(chlg *Challenge) error { + chlg.preCheck.requireCompletePropagation = false + return nil + } +} + +type preCheck struct { + // checks DNS propagation before notifying ACME that the DNS challenge is ready. + checkFunc PreCheckFunc + // require the TXT record to be propagated to all authoritative name servers + requireCompletePropagation bool +} + +func newPreCheck() preCheck { + return preCheck{ + requireCompletePropagation: true, + } +} + +func (p preCheck) call(fqdn, value string) (bool, error) { + if p.checkFunc == nil { + return p.checkDNSPropagation(fqdn, value) + } + return p.checkFunc(fqdn, value) +} + +// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. +func (p preCheck) checkDNSPropagation(fqdn, value string) (bool, error) { + // Initial attempt to resolve at the recursive NS + r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameservers, true) + if err != nil { + return false, err + } + + if !p.requireCompletePropagation { + return true, nil + } + + if r.Rcode == dns.RcodeSuccess { + // If we see a CNAME here then use the alias + for _, rr := range r.Answer { + if cn, ok := rr.(*dns.CNAME); ok { + if cn.Hdr.Name == fqdn { + fqdn = cn.Target + break + } + } + } + } + + authoritativeNss, err := lookupNameservers(fqdn) + if err != nil { + return false, err + } + + return checkAuthoritativeNss(fqdn, value, authoritativeNss) +} + +// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. +func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) { + for _, ns := range nameservers { + r, err := dnsQuery(fqdn, dns.TypeTXT, []string{net.JoinHostPort(ns, "53")}, false) + if err != nil { + return false, err + } + + if r.Rcode != dns.RcodeSuccess { + return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) + } + + var found bool + for _, rr := range r.Answer { + if txt, ok := rr.(*dns.TXT); ok { + if strings.Join(txt.Txt, "") == value { + found = true + break + } + } + } + + if !found { + return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn) + } + } + + return true, nil +} diff --git a/challenge/dns01/precheck_test.go b/challenge/dns01/precheck_test.go new file mode 100644 index 00000000..05e20497 --- /dev/null +++ b/challenge/dns01/precheck_test.go @@ -0,0 +1,117 @@ +package dns01 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckDNSPropagation(t *testing.T) { + testCases := []struct { + desc string + fqdn string + value string + expectError bool + }{ + { + desc: "success", + fqdn: "postman-echo.com.", + value: "postman-domain-verification=c85de626cb79d941310696e06558e2e790223802f3697dfbdcaf65510152d52c", + }, + { + desc: "no TXT record", + fqdn: "acme-staging.api.letsencrypt.org.", + value: "fe01=", + expectError: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + ClearFqdnCache() + + check := newPreCheck() + + ok, err := check.checkDNSPropagation(test.fqdn, test.value) + if test.expectError { + assert.Errorf(t, err, "PreCheckDNS must failed for %s", test.fqdn) + assert.False(t, ok, "PreCheckDNS must failed for %s", test.fqdn) + } else { + assert.NoErrorf(t, err, "PreCheckDNS failed for %s", test.fqdn) + assert.True(t, ok, "PreCheckDNS failed for %s", test.fqdn) + } + }) + } +} + +func TestCheckAuthoritativeNss(t *testing.T) { + testCases := []struct { + desc string + fqdn, value string + ns []string + expected bool + }{ + { + desc: "TXT RR w/ expected value", + fqdn: "8.8.8.8.asn.routeviews.org.", + value: "151698.8.8.024", + ns: []string{"asnums.routeviews.org."}, + expected: true, + }, + { + desc: "No TXT RR", + fqdn: "ns1.google.com.", + ns: []string{"ns2.google.com."}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + ClearFqdnCache() + + ok, _ := checkAuthoritativeNss(test.fqdn, test.value, test.ns) + assert.Equal(t, test.expected, ok, test.fqdn) + }) + } +} + +func TestCheckAuthoritativeNssErr(t *testing.T) { + testCases := []struct { + desc string + fqdn, value string + ns []string + error string + }{ + { + desc: "TXT RR /w unexpected value", + fqdn: "8.8.8.8.asn.routeviews.org.", + value: "fe01=", + ns: []string{"asnums.routeviews.org."}, + error: "did not return the expected TXT record", + }, + { + desc: "No TXT RR", + fqdn: "ns1.google.com.", + value: "fe01=", + ns: []string{"ns2.google.com."}, + error: "did not return the expected TXT record", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + ClearFqdnCache() + + _, err := checkAuthoritativeNss(test.fqdn, test.value, test.ns) + require.Error(t, err) + assert.Contains(t, err.Error(), test.error) + }) + } +} diff --git a/challenge/http01/http_challenge.go b/challenge/http01/http_challenge.go new file mode 100644 index 00000000..4176ecae --- /dev/null +++ b/challenge/http01/http_challenge.go @@ -0,0 +1,65 @@ +package http01 + +import ( + "fmt" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/log" +) + +type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error + +// ChallengePath returns the URL path for the `http-01` challenge +func ChallengePath(token string) string { + return "/.well-known/acme-challenge/" + token +} + +type Challenge struct { + core *api.Core + validate ValidateFunc + provider challenge.Provider +} + +func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { + return &Challenge{ + core: core, + validate: validate, + provider: provider, + } +} + +func (c *Challenge) SetProvider(provider challenge.Provider) { + c.provider = provider +} + +func (c *Challenge) Solve(authz acme.Authorization) error { + domain := challenge.GetTargetedDomain(authz) + log.Infof("[%s] acme: Trying to solve HTTP-01", domain) + + chlng, err := challenge.FindChallenge(challenge.HTTP01, authz) + if err != nil { + return err + } + + // Generate the Key Authorization for the challenge + keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) + if err != nil { + return err + } + + err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("[%s] acme: error presenting token: %v", domain, err) + } + defer func() { + err := c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) + if err != nil { + log.Warnf("[%s] acme: error cleaning up: %v", domain, err) + } + }() + + chlng.KeyAuthorization = keyAuth + return c.validate(c.core, authz.Identifier.Value, chlng) +} diff --git a/acme/http_challenge_server.go b/challenge/http01/http_challenge_server.go similarity index 61% rename from acme/http_challenge_server.go rename to challenge/http01/http_challenge_server.go index 9c595d7f..aa463bb4 100644 --- a/acme/http_challenge_server.go +++ b/challenge/http01/http_challenge_server.go @@ -1,4 +1,4 @@ -package acme +package http01 import ( "fmt" @@ -9,31 +9,31 @@ import ( "github.com/xenolf/lego/log" ) -// HTTPProviderServer implements ChallengeProvider for `http-01` challenge -// It may be instantiated without using the NewHTTPProviderServer function if +// ProviderServer implements ChallengeProvider for `http-01` challenge +// It may be instantiated without using the NewProviderServer function if // you want only to use the default values. -type HTTPProviderServer struct { +type ProviderServer struct { iface string port string done chan bool listener net.Listener } -// NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port. +// NewProviderServer creates a new ProviderServer on the selected interface and port. // Setting iface and / or port to an empty string will make the server fall back to // the "any" interface and port 80 respectively. -func NewHTTPProviderServer(iface, port string) *HTTPProviderServer { - return &HTTPProviderServer{iface: iface, port: port} +func NewProviderServer(iface, port string) *ProviderServer { + return &ProviderServer{iface: iface, port: port} } -// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests. -func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error { +// Present starts a web server and makes the token available at `ChallengePath(token)` for web requests. +func (s *ProviderServer) Present(domain, token, keyAuth string) error { if s.port == "" { s.port = "80" } var err error - s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port)) + s.listener, err = net.Listen("tcp", s.GetAddress()) if err != nil { return fmt.Errorf("could not start HTTP server for challenge -> %v", err) } @@ -43,8 +43,12 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error { return nil } -// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)` -func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error { +func (s *ProviderServer) GetAddress() string { + return net.JoinHostPort(s.iface, s.port) +} + +// CleanUp closes the HTTP server and removes the token from `ChallengePath(token)` +func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } @@ -53,8 +57,8 @@ func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error { return nil } -func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { - path := HTTP01ChallengePath(token) +func (s *ProviderServer) serve(domain, token, keyAuth string) { + path := ChallengePath(token) // The handler validates the HOST header and request type. // For validation it then writes the token the server returned with the challenge @@ -80,12 +84,12 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { httpServer := &http.Server{Handler: mux} - // Once httpServer is shut down we don't want any lingering - // connections, so disable KeepAlives. + // Once httpServer is shut down + // we don't want any lingering connections, so disable KeepAlives. httpServer.SetKeepAlivesEnabled(false) err := httpServer.Serve(s.listener) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Println(err) } s.done <- true diff --git a/challenge/http01/http_challenge_test.go b/challenge/http01/http_challenge_test.go new file mode 100644 index 00000000..a250369e --- /dev/null +++ b/challenge/http01/http_challenge_test.go @@ -0,0 +1,98 @@ +package http01 + +import ( + "crypto/rand" + "crypto/rsa" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/platform/tester" +) + +func TestChallenge(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + providerServer := &ProviderServer{port: "23457"} + + validate := func(_ *api.Core, _ string, chlng acme.Challenge) error { + uri := "http://localhost" + providerServer.GetAddress() + ChallengePath(chlng.Token) + + resp, err := http.DefaultClient.Get(uri) + if err != nil { + return err + } + defer resp.Body.Close() + + if want := "text/plain"; resp.Header.Get("Content-Type") != want { + t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + bodyStr := string(body) + + if bodyStr != chlng.KeyAuthorization { + t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) + } + + return nil + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + solver := NewChallenge(core, validate, providerServer) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "localhost:23457", + }, + Challenges: []acme.Challenge{ + {Type: challenge.HTTP01.String(), Token: "http1"}, + }, + } + + err = solver.Solve(authz) + require.NoError(t, err) +} + +func TestChallengeInvalidPort(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + privateKey, err := rsa.GenerateKey(rand.Reader, 128) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + validate := func(_ *api.Core, _ string, _ acme.Challenge) error { return nil } + + solver := NewChallenge(core, validate, &ProviderServer{port: "123456"}) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "localhost:123456", + }, + Challenges: []acme.Challenge{ + {Type: challenge.HTTP01.String(), Token: "http2"}, + }, + } + + err = solver.Solve(authz) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid port") + assert.Contains(t, err.Error(), "123456") +} diff --git a/acme/provider.go b/challenge/provider.go similarity index 58% rename from acme/provider.go rename to challenge/provider.go index d177ff07..d7cc213f 100644 --- a/acme/provider.go +++ b/challenge/provider.go @@ -1,28 +1,28 @@ -package acme +package challenge import "time" -// ChallengeProvider enables implementing a custom challenge +// Provider enables implementing a custom challenge // provider. Present presents the solution to a challenge available to // be solved. CleanUp will be called by the challenge if Present ends // in a non-error state. -type ChallengeProvider interface { +type Provider interface { Present(domain, token, keyAuth string) error CleanUp(domain, token, keyAuth string) error } -// ChallengeProviderTimeout allows for implementing a -// ChallengeProvider where an unusually long timeout is required when +// ProviderTimeout allows for implementing a +// Provider where an unusually long timeout is required when // waiting for an ACME challenge to be satisfied, such as when -// checking for DNS record progagation. If an implementor of a -// ChallengeProvider provides a Timeout method, then the return values +// checking for DNS record propagation. If an implementor of a +// Provider provides a Timeout method, then the return values // of the Timeout method will be used when appropriate by the acme // package. The interval value is the time between checks. // // The default values used for timeout and interval are 60 seconds and // 2 seconds respectively. These are used when no Timeout method is -// defined for the ChallengeProvider. -type ChallengeProviderTimeout interface { - ChallengeProvider +// defined for the Provider. +type ProviderTimeout interface { + Provider Timeout() (timeout, interval time.Duration) } diff --git a/challenge/resolver/errors.go b/challenge/resolver/errors.go new file mode 100644 index 00000000..9d609143 --- /dev/null +++ b/challenge/resolver/errors.go @@ -0,0 +1,25 @@ +package resolver + +import ( + "bytes" + "fmt" + "sort" +) + +// obtainError is returned when there are specific errors available per domain. +type obtainError map[string]error + +func (e obtainError) Error() string { + buffer := bytes.NewBufferString("acme: Error -> One or more domains had a problem:\n") + + var domains []string + for domain := range e { + domains = append(domains, domain) + } + sort.Strings(domains) + + for _, domain := range domains { + buffer.WriteString(fmt.Sprintf("[%s] %s\n", domain, e[domain])) + } + return buffer.String() +} diff --git a/challenge/resolver/prober.go b/challenge/resolver/prober.go new file mode 100644 index 00000000..1f7aa7e4 --- /dev/null +++ b/challenge/resolver/prober.go @@ -0,0 +1,172 @@ +package resolver + +import ( + "fmt" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/log" +) + +// Interface for all challenge solvers to implement. +type solver interface { + Solve(authorization acme.Authorization) error +} + +// Interface for challenges like dns, where we can set a record in advance for ALL challenges. +// This saves quite a bit of time vs creating the records and solving them serially. +type preSolver interface { + PreSolve(authorization acme.Authorization) error +} + +// Interface for challenges like dns, where we can solve all the challenges before to delete them. +type cleanup interface { + CleanUp(authorization acme.Authorization) error +} + +type sequential interface { + Sequential() (bool, time.Duration) +} + +// an authz with the solver we have chosen and the index of the challenge associated with it +type selectedAuthSolver struct { + authz acme.Authorization + solver solver +} + +type Prober struct { + solverManager *SolverManager +} + +func NewProber(solverManager *SolverManager) *Prober { + return &Prober{ + solverManager: solverManager, + } +} + +// Solve Looks through the challenge combinations to find a solvable match. +// Then solves the challenges in series and returns. +func (p *Prober) Solve(authorizations []acme.Authorization) error { + failures := make(obtainError) + + var authSolvers []*selectedAuthSolver + var authSolversSequential []*selectedAuthSolver + + // Loop through the resources, basically through the domains. + // First pass just selects a solver for each authz. + for _, authz := range authorizations { + domain := challenge.GetTargetedDomain(authz) + if authz.Status == acme.StatusValid { + // Boulder might recycle recent validated authz (see issue #267) + log.Infof("[%s] acme: authorization already valid; skipping challenge", domain) + continue + } + + if solvr := p.solverManager.chooseSolver(authz); solvr != nil { + authSolver := &selectedAuthSolver{authz: authz, solver: solvr} + + switch s := solvr.(type) { + case sequential: + if ok, _ := s.Sequential(); ok { + authSolversSequential = append(authSolversSequential, authSolver) + } else { + authSolvers = append(authSolvers, authSolver) + } + default: + authSolvers = append(authSolvers, authSolver) + } + } else { + failures[domain] = fmt.Errorf("[%s] acme: could not determine solvers", domain) + } + } + + parallelSolve(authSolvers, failures) + + sequentialSolve(authSolversSequential, failures) + + // Be careful not to return an empty failures map, + // for even an empty obtainError is a non-nil error value + if len(failures) > 0 { + return failures + } + return nil +} + +func sequentialSolve(authSolvers []*selectedAuthSolver, failures obtainError) { + for i, authSolver := range authSolvers { + // Submit the challenge + domain := challenge.GetTargetedDomain(authSolver.authz) + + if solvr, ok := authSolver.solver.(preSolver); ok { + err := solvr.PreSolve(authSolver.authz) + if err != nil { + failures[domain] = err + cleanUp(authSolver.solver, authSolver.authz) + continue + } + } + + // Solve challenge + err := authSolver.solver.Solve(authSolver.authz) + if err != nil { + failures[authSolver.authz.Identifier.Value] = err + cleanUp(authSolver.solver, authSolver.authz) + continue + } + + // Clean challenge + cleanUp(authSolver.solver, authSolver.authz) + + if len(authSolvers)-1 > i { + solvr := authSolver.solver.(sequential) + _, interval := solvr.Sequential() + log.Infof("sequence: wait for %s", interval) + time.Sleep(interval) + } + } +} + +func parallelSolve(authSolvers []*selectedAuthSolver, failures obtainError) { + // For all valid preSolvers, first submit the challenges so they have max time to propagate + for _, authSolver := range authSolvers { + authz := authSolver.authz + if solvr, ok := authSolver.solver.(preSolver); ok { + err := solvr.PreSolve(authz) + if err != nil { + failures[challenge.GetTargetedDomain(authz)] = err + } + } + } + + defer func() { + // Clean all created TXT records + for _, authSolver := range authSolvers { + cleanUp(authSolver.solver, authSolver.authz) + } + }() + + // Finally solve all challenges for real + for _, authSolver := range authSolvers { + authz := authSolver.authz + if failures[authz.Identifier.Value] != nil { + // already failed in previous loop + continue + } + + err := authSolver.solver.Solve(authz) + if err != nil { + failures[authz.Identifier.Value] = err + } + } +} + +func cleanUp(solvr solver, authz acme.Authorization) { + if solvr, ok := solvr.(cleanup); ok { + domain := challenge.GetTargetedDomain(authz) + err := solvr.CleanUp(authz) + if err != nil { + log.Warnf("[%s] acme: error cleaning up: %v ", domain, err) + } + } +} diff --git a/challenge/resolver/prober_mock_test.go b/challenge/resolver/prober_mock_test.go new file mode 100644 index 00000000..a4820377 --- /dev/null +++ b/challenge/resolver/prober_mock_test.go @@ -0,0 +1,42 @@ +package resolver + +import ( + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge" +) + +type preSolverMock struct { + preSolve map[string]error + solve map[string]error + cleanUp map[string]error +} + +func (s *preSolverMock) PreSolve(authorization acme.Authorization) error { + return s.preSolve[authorization.Identifier.Value] +} +func (s *preSolverMock) Solve(authorization acme.Authorization) error { + return s.solve[authorization.Identifier.Value] +} +func (s *preSolverMock) CleanUp(authorization acme.Authorization) error { + return s.cleanUp[authorization.Identifier.Value] +} + +func createStubAuthorizationHTTP01(domain, status string) acme.Authorization { + return acme.Authorization{ + Status: status, + Expires: time.Now(), + Identifier: acme.Identifier{ + Type: challenge.HTTP01.String(), + Value: domain, + }, + Challenges: []acme.Challenge{ + { + Type: challenge.HTTP01.String(), + Validated: time.Now(), + Error: nil, + }, + }, + } +} diff --git a/challenge/resolver/prober_test.go b/challenge/resolver/prober_test.go new file mode 100644 index 00000000..c2a8ac20 --- /dev/null +++ b/challenge/resolver/prober_test.go @@ -0,0 +1,118 @@ +package resolver + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge" +) + +func TestProber_Solve(t *testing.T) { + testCases := []struct { + desc string + solvers map[challenge.Type]solver + authz []acme.Authorization + expectedError string + }{ + { + desc: "success", + solvers: map[challenge.Type]solver{ + challenge.HTTP01: &preSolverMock{ + preSolve: map[string]error{}, + solve: map[string]error{}, + cleanUp: map[string]error{}, + }, + }, + authz: []acme.Authorization{ + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), + }, + }, + { + desc: "already valid", + solvers: map[challenge.Type]solver{ + challenge.HTTP01: &preSolverMock{ + preSolve: map[string]error{}, + solve: map[string]error{}, + cleanUp: map[string]error{}, + }, + }, + authz: []acme.Authorization{ + createStubAuthorizationHTTP01("acme.wtf", acme.StatusValid), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusValid), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusValid), + }, + }, + { + desc: "when preSolve fail, auth is flagged as error and skipped", + solvers: map[challenge.Type]solver{ + challenge.HTTP01: &preSolverMock{ + preSolve: map[string]error{ + "acme.wtf": errors.New("preSolve error acme.wtf"), + }, + solve: map[string]error{ + "acme.wtf": errors.New("solve error acme.wtf"), + }, + cleanUp: map[string]error{ + "acme.wtf": errors.New("clean error acme.wtf"), + }, + }, + }, + authz: []acme.Authorization{ + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), + }, + expectedError: `acme: Error -> One or more domains had a problem: +[acme.wtf] preSolve error acme.wtf +`, + }, + { + desc: "errors at different stages", + solvers: map[challenge.Type]solver{ + challenge.HTTP01: &preSolverMock{ + preSolve: map[string]error{ + "acme.wtf": errors.New("preSolve error acme.wtf"), + }, + solve: map[string]error{ + "acme.wtf": errors.New("solve error acme.wtf"), + "lego.wtf": errors.New("solve error lego.wtf"), + }, + cleanUp: map[string]error{ + "mydomain.wtf": errors.New("clean error mydomain.wtf"), + }, + }, + }, + authz: []acme.Authorization{ + createStubAuthorizationHTTP01("acme.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("lego.wtf", acme.StatusProcessing), + createStubAuthorizationHTTP01("mydomain.wtf", acme.StatusProcessing), + }, + expectedError: `acme: Error -> One or more domains had a problem: +[acme.wtf] preSolve error acme.wtf +[lego.wtf] solve error lego.wtf +`, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + prober := &Prober{ + solverManager: &SolverManager{solvers: test.solvers}, + } + + err := prober.Solve(test.authz) + if test.expectedError != "" { + require.EqualError(t, err, test.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/challenge/resolver/solver_manager.go b/challenge/resolver/solver_manager.go new file mode 100644 index 00000000..c6feea82 --- /dev/null +++ b/challenge/resolver/solver_manager.go @@ -0,0 +1,201 @@ +package resolver + +import ( + "errors" + "fmt" + "net" + "sort" + "strconv" + "time" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/challenge/http01" + "github.com/xenolf/lego/challenge/tlsalpn01" + "github.com/xenolf/lego/log" +) + +type byType []acme.Challenge + +func (a byType) Len() int { return len(a) } +func (a byType) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byType) Less(i, j int) bool { return a[i].Type < a[j].Type } + +type SolverManager struct { + core *api.Core + solvers map[challenge.Type]solver +} + +func NewSolversManager(core *api.Core) *SolverManager { + solvers := map[challenge.Type]solver{ + challenge.HTTP01: http01.NewChallenge(core, validate, &http01.ProviderServer{}), + challenge.TLSALPN01: tlsalpn01.NewChallenge(core, validate, &tlsalpn01.ProviderServer{}), + } + + return &SolverManager{ + solvers: solvers, + core: core, + } +} + +// SetHTTP01Address specifies a custom interface:port to be used for HTTP based challenges. +// If this option is not used, the default port 80 and all interfaces will be used. +// To only specify a port and no interface use the ":port" notation. +// +// NOTE: This REPLACES any custom HTTP provider previously set by calling +// c.SetProvider with the default HTTP challenge provider. +func (c *SolverManager) SetHTTP01Address(iface string) error { + host, port, err := net.SplitHostPort(iface) + if err != nil { + return err + } + + if chlng, ok := c.solvers[challenge.HTTP01]; ok { + chlng.(*http01.Challenge).SetProvider(http01.NewProviderServer(host, port)) + } + + return nil +} + +// SetTLSALPN01Address specifies a custom interface:port to be used for TLS based challenges. +// If this option is not used, the default port 443 and all interfaces will be used. +// To only specify a port and no interface use the ":port" notation. +// +// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling +// c.SetProvider with the default TLS-ALPN challenge provider. +func (c *SolverManager) SetTLSALPN01Address(iface string) error { + host, port, err := net.SplitHostPort(iface) + if err != nil { + return err + } + + if chlng, ok := c.solvers[challenge.TLSALPN01]; ok { + chlng.(*tlsalpn01.Challenge).SetProvider(tlsalpn01.NewProviderServer(host, port)) + } + + return nil +} + +// SetHTTP01Provider specifies a custom provider p that can solve the given HTTP-01 challenge. +func (c *SolverManager) SetHTTP01Provider(p challenge.Provider) error { + c.solvers[challenge.HTTP01] = http01.NewChallenge(c.core, validate, p) + return nil +} + +// SetTLSALPN01Provider specifies a custom provider p that can solve the given TLS-ALPN-01 challenge. +func (c *SolverManager) SetTLSALPN01Provider(p challenge.Provider) error { + c.solvers[challenge.TLSALPN01] = tlsalpn01.NewChallenge(c.core, validate, p) + return nil +} + +// SetDNS01Provider specifies a custom provider p that can solve the given DNS-01 challenge. +func (c *SolverManager) SetDNS01Provider(p challenge.Provider, opts ...dns01.ChallengeOption) error { + c.solvers[challenge.DNS01] = dns01.NewChallenge(c.core, validate, p, opts...) + return nil +} + +// Exclude explicitly removes challenges from the pool for solving. +func (c *SolverManager) Exclude(challenges []challenge.Type) { + // Loop through all challenges and delete the requested one if found. + for _, chlg := range challenges { + delete(c.solvers, chlg) + } +} + +// Checks all challenges from the server in order and returns the first matching solver. +func (c *SolverManager) chooseSolver(authz acme.Authorization) solver { + // Allow to have a deterministic challenge order + sort.Sort(sort.Reverse(byType(authz.Challenges))) + + domain := challenge.GetTargetedDomain(authz) + for _, chlg := range authz.Challenges { + if solvr, ok := c.solvers[challenge.Type(chlg.Type)]; ok { + log.Infof("[%s] acme: use %s solver", domain, chlg.Type) + return solvr + } + log.Infof("[%s] acme: Could not find solver for: %s", domain, chlg.Type) + } + + return nil +} + +func validate(core *api.Core, domain string, chlg acme.Challenge) error { + chlng, err := core.Challenges.New(chlg.URL) + if err != nil { + return fmt.Errorf("failed to initiate challenge: %v", err) + } + + valid, err := checkChallengeStatus(chlng) + if err != nil { + return err + } + + if valid { + log.Infof("[%s] The server validated our request", domain) + return nil + } + + // After the path is sent, the ACME server will access our server. + // Repeatedly check the server for an updated status on our request. + for { + authz, err := core.Authorizations.Get(chlng.AuthorizationURL) + if err != nil { + return err + } + + valid, err := checkAuthorizationStatus(authz) + if err != nil { + return err + } + + if valid { + log.Infof("[%s] The server validated our request", domain) + return nil + } + + ra, err := strconv.Atoi(chlng.RetryAfter) + if err != nil { + // The ACME server MUST return a Retry-After. + // If it doesn't, we'll just poll hard. + // Boulder does not implement the ability to retry challenges or the Retry-After header. + // https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md#section-82 + ra = 5 + } + time.Sleep(time.Duration(ra) * time.Second) + } +} + +func checkChallengeStatus(chlng acme.ExtendedChallenge) (bool, error) { + switch chlng.Status { + case acme.StatusValid: + return true, nil + case acme.StatusPending, acme.StatusProcessing: + return false, nil + case acme.StatusInvalid: + return false, chlng.Error + default: + return false, errors.New("the server returned an unexpected state") + } +} + +func checkAuthorizationStatus(authz acme.Authorization) (bool, error) { + switch authz.Status { + case acme.StatusValid: + return true, nil + case acme.StatusPending, acme.StatusProcessing: + return false, nil + case acme.StatusDeactivated, acme.StatusExpired, acme.StatusRevoked: + return false, fmt.Errorf("the authorization state %s", authz.Status) + case acme.StatusInvalid: + for _, chlg := range authz.Challenges { + if chlg.Status == acme.StatusInvalid && chlg.Error != nil { + return false, chlg.Error + } + } + return false, fmt.Errorf("the authorization state %s", authz.Status) + default: + return false, errors.New("the server returned an unexpected state") + } +} diff --git a/challenge/resolver/solver_manager_test.go b/challenge/resolver/solver_manager_test.go new file mode 100644 index 00000000..ae22b373 --- /dev/null +++ b/challenge/resolver/solver_manager_test.go @@ -0,0 +1,203 @@ +package resolver + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "io/ioutil" + "net" + "net/http" + "reflect" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/http01" + "github.com/xenolf/lego/platform/tester" + "gopkg.in/square/go-jose.v2" +) + +func TestSolverManager_SetHTTP01Address(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + keyBits := 32 // small value keeps test fast + key, err := rsa.GenerateKey(rand.Reader, keyBits) + require.NoError(t, err, "Could not generate test key") + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + solversManager := NewSolversManager(core) + + optPort := "1234" + optHost := "" + + err = solversManager.SetHTTP01Address(net.JoinHostPort(optHost, optPort)) + require.NoError(t, err) + + require.IsType(t, &http01.Challenge{}, solversManager.solvers[challenge.HTTP01]) + httpSolver := solversManager.solvers[challenge.HTTP01].(*http01.Challenge) + + httpProviderServer := (*http01.ProviderServer)(unsafe.Pointer(reflect.ValueOf(httpSolver).Elem().FieldByName("provider").InterfaceData()[1])) + assert.Equal(t, net.JoinHostPort(optHost, optPort), httpProviderServer.GetAddress()) + + // test setting different host + optHost = "127.0.0.1" + err = solversManager.SetHTTP01Address(net.JoinHostPort(optHost, optPort)) + require.NoError(t, err) + + httpProviderServer = (*http01.ProviderServer)(unsafe.Pointer(reflect.ValueOf(httpSolver).Elem().FieldByName("provider").InterfaceData()[1])) + assert.Equal(t, net.JoinHostPort(optHost, optPort), httpProviderServer.GetAddress()) +} + +func TestValidate(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + var statuses []string + + privateKey, _ := rsa.GenerateKey(rand.Reader, 512) + + mux.HandleFunc("/chlg", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + if err := validateNoBody(privateKey, r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Link", "<"+apiURL+`/my-authz>; rel="up"`) + + st := statuses[0] + statuses = statuses[1:] + + chlg := &acme.Challenge{Type: "http-01", Status: st, URL: "http://example.com/", Token: "token"} + if st == acme.StatusInvalid { + chlg.Error = &acme.ProblemDetails{} + } + + err := tester.WriteJSONResponse(w, chlg) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/my-authz", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + st := statuses[0] + statuses = statuses[1:] + + authorization := acme.Authorization{ + Status: st, + Challenges: []acme.Challenge{}, + } + + if st == acme.StatusInvalid { + chlg := acme.Challenge{ + Status: acme.StatusInvalid, + Error: &acme.ProblemDetails{}, + } + authorization.Challenges = append(authorization.Challenges, chlg) + } + + err := tester.WriteJSONResponse(w, authorization) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + testCases := []struct { + name string + statuses []string + want string + }{ + { + name: "POST-unexpected", + statuses: []string{"weird"}, + want: "unexpected", + }, + { + name: "POST-valid", + statuses: []string{acme.StatusValid}, + }, + { + name: "POST-invalid", + statuses: []string{acme.StatusInvalid}, + want: "error", + }, + { + name: "POST-pending-unexpected", + statuses: []string{acme.StatusPending, "weird"}, + want: "unexpected", + }, + { + name: "POST-pending-valid", + statuses: []string{acme.StatusPending, acme.StatusValid}, + }, + { + name: "POST-pending-invalid", + statuses: []string{acme.StatusPending, acme.StatusInvalid}, + want: "error", + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + statuses = test.statuses + + err := validate(core, "example.com", acme.Challenge{Type: "http-01", Token: "token", URL: apiURL + "/chlg"}) + if test.want == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), test.want) + } + }) + } +} + +// validateNoBody reads the http.Request POST body, parses the JWS and validates it to read the body. +// If there is an error doing this, +// or if the JWS body is not the empty JSON payload "{}" or a POST-as-GET payload "" an error is returned. +// We use this to verify challenge POSTs to the ts below do not send a JWS body. +func validateNoBody(privateKey *rsa.PrivateKey, r *http.Request) error { + reqBody, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + jws, err := jose.ParseSigned(string(reqBody)) + if err != nil { + return err + } + + body, err := jws.Verify(&jose.JSONWebKey{ + Key: privateKey.Public(), + Algorithm: "RSA", + }) + if err != nil { + return err + } + + if bodyStr := string(body); bodyStr != "{}" && bodyStr != "" { + return fmt.Errorf(`expected JWS POST body "{}" or "", got %q`, bodyStr) + } + return nil +} diff --git a/challenge/tlsalpn01/tls_alpn_challenge.go b/challenge/tlsalpn01/tls_alpn_challenge.go new file mode 100644 index 00000000..fa03ffdb --- /dev/null +++ b/challenge/tlsalpn01/tls_alpn_challenge.go @@ -0,0 +1,129 @@ +package tlsalpn01 + +import ( + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/log" +) + +// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension. +// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-5.1 +var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} + +type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error + +type Challenge struct { + core *api.Core + validate ValidateFunc + provider challenge.Provider +} + +func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { + return &Challenge{ + core: core, + validate: validate, + provider: provider, + } +} + +func (c *Challenge) SetProvider(provider challenge.Provider) { + c.provider = provider +} + +// Solve manages the provider to validate and solve the challenge. +func (c *Challenge) Solve(authz acme.Authorization) error { + domain := authz.Identifier.Value + log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", challenge.GetTargetedDomain(authz)) + + chlng, err := challenge.FindChallenge(challenge.TLSALPN01, authz) + if err != nil { + return err + } + + // Generate the Key Authorization for the challenge + keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) + if err != nil { + return err + } + + err = c.provider.Present(domain, chlng.Token, keyAuth) + if err != nil { + return fmt.Errorf("[%s] acme: error presenting token: %v", challenge.GetTargetedDomain(authz), err) + } + defer func() { + err := c.provider.CleanUp(domain, chlng.Token, keyAuth) + if err != nil { + log.Warnf("[%s] acme: error cleaning up: %v", challenge.GetTargetedDomain(authz), err) + } + }() + + chlng.KeyAuthorization = keyAuth + return c.validate(c.core, domain, chlng) +} + +// ChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension +// and domain name for the `tls-alpn-01` challenge. +func ChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) { + // Compute the SHA-256 digest of the key authorization. + zBytes := sha256.Sum256([]byte(keyAuth)) + + value, err := asn1.Marshal(zBytes[:sha256.Size]) + if err != nil { + return nil, nil, err + } + + // Add the keyAuth digest as the acmeValidation-v1 extension + // (marked as critical such that it won't be used by non-ACME software). + // Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-05#section-3 + extensions := []pkix.Extension{ + { + Id: idPeAcmeIdentifierV1, + Critical: true, + Value: value, + }, + } + + // Generate a new RSA key for the certificates. + tempPrivateKey, err := certcrypto.GeneratePrivateKey(certcrypto.RSA2048) + if err != nil { + return nil, nil, err + } + + rsaPrivateKey := tempPrivateKey.(*rsa.PrivateKey) + + // Generate the PEM certificate using the provided private key, domain, and extra extensions. + tempCertPEM, err := certcrypto.GeneratePemCert(rsaPrivateKey, domain, extensions) + if err != nil { + return nil, nil, err + } + + // Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair. + rsaPrivatePEM := certcrypto.PEMEncode(rsaPrivateKey) + + return tempCertPEM, rsaPrivatePEM, nil +} + +// ChallengeCert returns a certificate with the acmeValidation-v1 extension +// and domain name for the `tls-alpn-01` challenge. +func ChallengeCert(domain, keyAuth string) (*tls.Certificate, error) { + tempCertPEM, rsaPrivatePEM, err := ChallengeBlocks(domain, keyAuth) + if err != nil { + return nil, err + } + + cert, err := tls.X509KeyPair(tempCertPEM, rsaPrivatePEM) + if err != nil { + return nil, err + } + + return &cert, nil +} diff --git a/acme/tls_alpn_challenge_server.go b/challenge/tlsalpn01/tls_alpn_challenge_server.go similarity index 54% rename from acme/tls_alpn_challenge_server.go rename to challenge/tlsalpn01/tls_alpn_challenge_server.go index ee06a16d..1f7480c2 100644 --- a/acme/tls_alpn_challenge_server.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_server.go @@ -1,49 +1,54 @@ -package acme +package tlsalpn01 import ( "crypto/tls" "fmt" - "log" "net" "net/http" + "strings" + + "github.com/xenolf/lego/log" ) const ( // ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol. ACMETLS1Protocol = "acme-tls/1" - // defaultTLSPort is the port that the TLSALPNProviderServer will default to + // defaultTLSPort is the port that the ProviderServer will default to // when no other port is provided. defaultTLSPort = "443" ) -// TLSALPNProviderServer implements ChallengeProvider for `TLS-ALPN-01` -// challenge. It may be instantiated without using the NewTLSALPNProviderServer +// ProviderServer implements ChallengeProvider for `TLS-ALPN-01` challenge. +// It may be instantiated without using the NewProviderServer // if you want only to use the default values. -type TLSALPNProviderServer struct { +type ProviderServer struct { iface string port string listener net.Listener } -// NewTLSALPNProviderServer creates a new TLSALPNProviderServer on the selected -// interface and port. Setting iface and / or port to an empty string will make -// the server fall back to the "any" interface and port 443 respectively. -func NewTLSALPNProviderServer(iface, port string) *TLSALPNProviderServer { - return &TLSALPNProviderServer{iface: iface, port: port} +// NewProviderServer creates a new ProviderServer on the selected interface and port. +// Setting iface and / or port to an empty string will make the server fall back to +// the "any" interface and port 443 respectively. +func NewProviderServer(iface, port string) *ProviderServer { + return &ProviderServer{iface: iface, port: port} +} + +func (s *ProviderServer) GetAddress() string { + return net.JoinHostPort(s.iface, s.port) } // Present generates a certificate with a SHA-256 digest of the keyAuth provided -// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN -// spec. -func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error { - if t.port == "" { +// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN spec. +func (s *ProviderServer) Present(domain, token, keyAuth string) error { + if s.port == "" { // Fallback to port 443 if the port was not provided. - t.port = defaultTLSPort + s.port = defaultTLSPort } // Generate the challenge certificate using the provided keyAuth and domain. - cert, err := TLSALPNChallengeCert(domain, keyAuth) + cert, err := ChallengeCert(domain, keyAuth) if err != nil { return err } @@ -59,15 +64,15 @@ func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error { tlsConf.NextProtos = []string{ACMETLS1Protocol} // Create the listener with the created tls.Config. - t.listener, err = tls.Listen("tcp", net.JoinHostPort(t.iface, t.port), tlsConf) + s.listener, err = tls.Listen("tcp", s.GetAddress(), tlsConf) if err != nil { return fmt.Errorf("could not start HTTPS server for challenge -> %v", err) } // Shut the server down when we're finished. go func() { - err := http.Serve(t.listener, nil) - if err != nil { + err := http.Serve(s.listener, nil) + if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Println(err) } }() @@ -76,13 +81,13 @@ func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error { } // CleanUp closes the HTTPS server. -func (t *TLSALPNProviderServer) CleanUp(domain, token, keyAuth string) error { - if t.listener == nil { +func (s *ProviderServer) CleanUp(domain, token, keyAuth string) error { + if s.listener == nil { return nil } // Server was created, close it. - if err := t.listener.Close(); err != nil && err != http.ErrServerClosed { + if err := s.listener.Close(); err != nil && err != http.ErrServerClosed { return err } diff --git a/acme/tls_alpn_challenge_test.go b/challenge/tlsalpn01/tls_alpn_challenge_test.go similarity index 57% rename from acme/tls_alpn_challenge_test.go rename to challenge/tlsalpn01/tls_alpn_challenge_test.go index b8426593..a3e4f14a 100644 --- a/acme/tls_alpn_challenge_test.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_test.go @@ -1,4 +1,4 @@ -package acme +package tlsalpn01 import ( "crypto/rand" @@ -7,16 +7,24 @@ import ( "crypto/subtle" "crypto/tls" "encoding/asn1" + "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/platform/tester" ) -func TestTLSALPNChallenge(t *testing.T) { +func TestChallenge(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + domain := "localhost:23457" - mockValidate := func(_ *jws, _, _ string, chlng challenge) error { + mockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error { conn, err := tls.Dial("tcp", domain, &tls.Config{ InsecureSkipVerify: true, }) @@ -48,41 +56,64 @@ func TestTLSALPNChallenge(t *testing.T) { value, err := asn1.Marshal(zBytes[:sha256.Size]) require.NoError(t, err, "Expected marshaling of the keyAuth to return no error") - if subtle.ConstantTimeCompare(value[:], ext.Value) != 1 { + if subtle.ConstantTimeCompare(value, ext.Value) != 1 { t.Errorf("Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth, %v, but was %v", zBytes[:], ext.Value) } return nil } - privKey, err := rsa.GenerateKey(rand.Reader, 512) + privateKey, err := rsa.GenerateKey(rand.Reader, 512) require.NoError(t, err, "Could not generate test key") - solver := &tlsALPNChallenge{ - jws: &jws{privKey: privKey}, - validate: mockValidate, - provider: &TLSALPNProviderServer{port: "23457"}, + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + solver := NewChallenge( + core, + mockValidate, + &ProviderServer{port: "23457"}, + ) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: domain, + }, + Challenges: []acme.Challenge{ + {Type: challenge.TLSALPN01.String(), Token: "tlsalpn1"}, + }, } - clientChallenge := challenge{Type: string(TLSALPN01), Token: "tlsalpn1"} - - err = solver.Solve(clientChallenge, domain) + err = solver.Solve(authz) require.NoError(t, err) } -func TestTLSALPNChallengeInvalidPort(t *testing.T) { - privKey, err := rsa.GenerateKey(rand.Reader, 128) +func TestChallengeInvalidPort(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + privateKey, err := rsa.GenerateKey(rand.Reader, 128) require.NoError(t, err, "Could not generate test key") - solver := &tlsALPNChallenge{ - jws: &jws{privKey: privKey}, - validate: stubValidate, - provider: &TLSALPNProviderServer{port: "123456"}, + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey) + require.NoError(t, err) + + solver := NewChallenge( + core, + func(_ *api.Core, _ string, _ acme.Challenge) error { return nil }, + &ProviderServer{port: "123456"}, + ) + + authz := acme.Authorization{ + Identifier: acme.Identifier{ + Value: "localhost:123456", + }, + Challenges: []acme.Challenge{ + {Type: challenge.TLSALPN01.String(), Token: "tlsalpn1"}, + }, } - clientChallenge := challenge{Type: string(TLSALPN01), Token: "tlsalpn1"} - - err = solver.Solve(clientChallenge, "localhost:123456") + err = solver.Solve(authz) require.Error(t, err) assert.Contains(t, err.Error(), "invalid port") assert.Contains(t, err.Error(), "123456") diff --git a/cli_handlers.go b/cli_handlers.go deleted file mode 100644 index 768c62e7..00000000 --- a/cli_handlers.go +++ /dev/null @@ -1,470 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "io/ioutil" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/urfave/cli" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/log" - "github.com/xenolf/lego/providers/dns" - "github.com/xenolf/lego/providers/http/memcached" - "github.com/xenolf/lego/providers/http/webroot" -) - -func checkFolder(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - return os.MkdirAll(path, 0700) - } - return nil -} - -func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { - if c.GlobalIsSet("http-timeout") { - acme.HTTPClient = http.Client{Timeout: time.Duration(c.GlobalInt("http-timeout")) * time.Second} - } - - if c.GlobalIsSet("dns-timeout") { - acme.DNSTimeout = time.Duration(c.GlobalInt("dns-timeout")) * time.Second - } - - if len(c.GlobalStringSlice("dns-resolvers")) > 0 { - var resolvers []string - for _, resolver := range c.GlobalStringSlice("dns-resolvers") { - if !strings.Contains(resolver, ":") { - resolver += ":53" - } - resolvers = append(resolvers, resolver) - } - acme.RecursiveNameservers = resolvers - } - - err := checkFolder(c.GlobalString("path")) - if err != nil { - log.Fatalf("Could not check/create path: %v", err) - } - - conf := NewConfiguration(c) - if len(c.GlobalString("email")) == 0 { - log.Fatal("You have to pass an account (email address) to the program using --email or -m") - } - - // TODO: move to account struct? Currently MUST pass email. - acc := NewAccount(c.GlobalString("email"), conf) - - keyType, err := conf.KeyType() - if err != nil { - log.Fatal(err) - } - - acme.UserAgent = fmt.Sprintf("lego-cli/%s", c.App.Version) - - client, err := acme.NewClient(c.GlobalString("server"), acc, keyType) - if err != nil { - log.Fatalf("Could not create client: %v", err) - } - - if len(c.GlobalStringSlice("exclude")) > 0 { - client.ExcludeChallenges(conf.ExcludedSolvers()) - } - - if c.GlobalIsSet("webroot") { - provider, errO := webroot.NewHTTPProvider(c.GlobalString("webroot")) - if errO != nil { - log.Fatal(errO) - } - - errO = client.SetChallengeProvider(acme.HTTP01, provider) - if errO != nil { - log.Fatal(errO) - } - - // --webroot=foo indicates that the user specifically want to do a HTTP challenge - // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSALPN01}) - } - - if c.GlobalIsSet("memcached-host") { - provider, errO := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host")) - if errO != nil { - log.Fatal(errO) - } - - errO = client.SetChallengeProvider(acme.HTTP01, provider) - if errO != nil { - log.Fatal(errO) - } - - // --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge - // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSALPN01}) - } - - if c.GlobalIsSet("http") { - if !strings.Contains(c.GlobalString("http"), ":") { - log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") - } - - err = client.SetHTTPAddress(c.GlobalString("http")) - if err != nil { - log.Fatal(err) - } - } - - if c.GlobalIsSet("tls") { - if !strings.Contains(c.GlobalString("tls"), ":") { - log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") - } - - err = client.SetTLSAddress(c.GlobalString("tls")) - if err != nil { - log.Fatal(err) - } - } - - if c.GlobalIsSet("dns") { - provider, errO := dns.NewDNSChallengeProviderByName(c.GlobalString("dns")) - if errO != nil { - log.Fatal(errO) - } - - errO = client.SetChallengeProvider(acme.DNS01, provider) - if errO != nil { - log.Fatal(errO) - } - - // --dns=foo indicates that the user specifically want to do a DNS challenge - // infer that the user also wants to exclude all other challenges - client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSALPN01}) - } - - if client.GetExternalAccountRequired() && !c.GlobalIsSet("eab") { - log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.") - } - - return conf, acc, client -} - -func saveCertRes(certRes *acme.CertificateResource, conf *Configuration) { - var domainName string - - // Check filename cli parameter - if conf.context.GlobalString("filename") == "" { - // Make sure no funny chars are in the cert names (like wildcards ;)) - domainName = strings.Replace(certRes.Domain, "*", "_", -1) - } else { - domainName = conf.context.GlobalString("filename") - } - - // We store the certificate, private key and metadata in different files - // as web servers would not be able to work with a combined file. - certOut := filepath.Join(conf.CertPath(), domainName+".crt") - privOut := filepath.Join(conf.CertPath(), domainName+".key") - pemOut := filepath.Join(conf.CertPath(), domainName+".pem") - metaOut := filepath.Join(conf.CertPath(), domainName+".json") - issuerOut := filepath.Join(conf.CertPath(), domainName+".issuer.crt") - - err := checkFolder(filepath.Dir(certOut)) - if err != nil { - log.Fatalf("Could not check/create path: %v", err) - } - - err = ioutil.WriteFile(certOut, certRes.Certificate, 0600) - if err != nil { - log.Fatalf("Unable to save Certificate for domain %s\n\t%v", certRes.Domain, err) - } - - if certRes.IssuerCertificate != nil { - err = ioutil.WriteFile(issuerOut, certRes.IssuerCertificate, 0600) - if err != nil { - log.Fatalf("Unable to save IssuerCertificate for domain %s\n\t%v", certRes.Domain, err) - } - } - - if certRes.PrivateKey != nil { - // if we were given a CSR, we don't know the private key - err = ioutil.WriteFile(privOut, certRes.PrivateKey, 0600) - if err != nil { - log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", certRes.Domain, err) - } - - if conf.context.GlobalBool("pem") { - err = ioutil.WriteFile(pemOut, bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil), 0600) - if err != nil { - log.Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", certRes.Domain, err) - } - } - - } else if conf.context.GlobalBool("pem") { - // we don't have the private key; can't write the .pem file - log.Fatalf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", certRes.Domain, err) - } - - jsonBytes, err := json.MarshalIndent(certRes, "", "\t") - if err != nil { - log.Fatalf("Unable to marshal CertResource for domain %s\n\t%v", certRes.Domain, err) - } - - err = ioutil.WriteFile(metaOut, jsonBytes, 0600) - if err != nil { - log.Fatalf("Unable to save CertResource for domain %s\n\t%v", certRes.Domain, err) - } -} - -func handleTOS(c *cli.Context, client *acme.Client) bool { - // Check for a global accept override - if c.GlobalBool("accept-tos") { - return true - } - - reader := bufio.NewReader(os.Stdin) - log.Printf("Please review the TOS at %s", client.GetToSURL()) - - for { - log.Println("Do you accept the TOS? Y/n") - text, err := reader.ReadString('\n') - if err != nil { - log.Fatalf("Could not read from console: %v", err) - } - - text = strings.Trim(text, "\r\n") - - if text == "n" { - log.Fatal("You did not accept the TOS. Unable to proceed.") - } - - if text == "Y" || text == "y" || text == "" { - return true - } - - log.Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.") - } -} - -func readCSRFile(filename string) (*x509.CertificateRequest, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - raw := bytes - - // see if we can find a PEM-encoded CSR - var p *pem.Block - rest := bytes - for { - // decode a PEM block - p, rest = pem.Decode(rest) - - // did we fail? - if p == nil { - break - } - - // did we get a CSR? - if p.Type == "CERTIFICATE REQUEST" { - raw = p.Bytes - } - } - - // no PEM-encoded CSR - // assume we were given a DER-encoded ASN.1 CSR - // (if this assumption is wrong, parsing these bytes will fail) - return x509.ParseCertificateRequest(raw) -} - -func run(c *cli.Context) error { - var err error - - conf, acc, client := setup(c) - if acc.Registration == nil { - accepted := handleTOS(c, client) - if !accepted { - log.Fatal("You did not accept the TOS. Unable to proceed.") - } - - var reg *acme.RegistrationResource - - if c.GlobalBool("eab") { - kid := c.GlobalString("kid") - hmacEncoded := c.GlobalString("hmac") - - if kid == "" || hmacEncoded == "" { - log.Fatalf("Requires arguments --kid and --hmac.") - } - - reg, err = client.RegisterWithExternalAccountBinding( - accepted, - kid, - hmacEncoded, - ) - } else { - reg, err = client.Register(accepted) - } - - if err != nil { - log.Fatalf("Could not complete registration\n\t%v", err) - } - - acc.Registration = reg - err = acc.Save() - if err != nil { - log.Fatal(err) - } - - log.Print("!!!! HEADS UP !!!!") - log.Printf(` - Your account credentials have been saved in your Let's Encrypt - configuration directory at "%s". - You should make a secure backup of this folder now. This - configuration directory will also contain certificates and - private keys obtained from Let's Encrypt so making regular - backups of this folder is ideal.`, conf.AccountPath(c.GlobalString("email"))) - - } - - // we require either domains or csr, but not both - hasDomains := len(c.GlobalStringSlice("domains")) > 0 - hasCsr := len(c.GlobalString("csr")) > 0 - if hasDomains && hasCsr { - log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") - } - if !hasDomains && !hasCsr { - log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") - } - - var cert *acme.CertificateResource - - if hasDomains { - // obtain a certificate, generating a new private key - cert, err = client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil, c.Bool("must-staple")) - } else { - // read the CSR - var csr *x509.CertificateRequest - csr, err = readCSRFile(c.GlobalString("csr")) - if err == nil { - // obtain a certificate for this CSR - cert, err = client.ObtainCertificateForCSR(*csr, !c.Bool("no-bundle")) - } - } - - if err != nil { - // Make sure to return a non-zero exit code if ObtainSANCertificate - // returned at least one error. Due to us not returning partial - // certificate we can just exit here instead of at the end. - log.Fatalf("Could not obtain certificates\n\t%v", err) - } - - if err = checkFolder(conf.CertPath()); err != nil { - log.Fatalf("Could not check/create path: %v", err) - } - - saveCertRes(cert, conf) - - return nil -} - -func revoke(c *cli.Context) error { - conf, acc, client := setup(c) - if acc.Registration == nil { - log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) - } - - if err := checkFolder(conf.CertPath()); err != nil { - log.Fatalf("Could not check/create path: %v", err) - } - - for _, domain := range c.GlobalStringSlice("domains") { - log.Printf("Trying to revoke certificate for domain %s", domain) - - certPath := filepath.Join(conf.CertPath(), domain+".crt") - certBytes, err := ioutil.ReadFile(certPath) - if err != nil { - log.Println(err) - } - - err = client.RevokeCertificate(certBytes) - if err != nil { - log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) - } else { - log.Println("Certificate was revoked.") - } - } - - return nil -} - -func renew(c *cli.Context) error { - conf, acc, client := setup(c) - if acc.Registration == nil { - log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) - } - - if len(c.GlobalStringSlice("domains")) <= 0 { - log.Fatal("Please specify at least one domain.") - } - - domain := c.GlobalStringSlice("domains")[0] - domain = strings.Replace(domain, "*", "_", -1) - - // load the cert resource from files. - // We store the certificate, private key and metadata in different files - // as web servers would not be able to work with a combined file. - certPath := filepath.Join(conf.CertPath(), domain+".crt") - privPath := filepath.Join(conf.CertPath(), domain+".key") - metaPath := filepath.Join(conf.CertPath(), domain+".json") - - certBytes, err := ioutil.ReadFile(certPath) - if err != nil { - log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) - } - - if c.IsSet("days") { - expTime, errE := acme.GetPEMCertExpiration(certBytes) - if errE != nil { - log.Printf("Could not get Certification expiration for domain %s", domain) - } - - if int(time.Until(expTime).Hours()/24.0) > c.Int("days") { - return nil - } - } - - metaBytes, err := ioutil.ReadFile(metaPath) - if err != nil { - log.Fatalf("Error while loading the meta data for domain %s\n\t%v", domain, err) - } - - var certRes acme.CertificateResource - if err = json.Unmarshal(metaBytes, &certRes); err != nil { - log.Fatalf("Error while marshaling the meta data for domain %s\n\t%v", domain, err) - } - - if c.Bool("reuse-key") { - keyBytes, errR := ioutil.ReadFile(privPath) - if errR != nil { - log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, errR) - } - certRes.PrivateKey = keyBytes - } - - certRes.Certificate = certBytes - - newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"), c.Bool("must-staple")) - if err != nil { - log.Fatal(err) - } - - saveCertRes(newCert, conf) - - return nil -} diff --git a/cmd/account.go b/cmd/account.go new file mode 100644 index 00000000..5cc43f52 --- /dev/null +++ b/cmd/account.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "crypto" + + "github.com/xenolf/lego/registration" +) + +// Account represents a users local saved credentials +type Account struct { + Email string `json:"email"` + Registration *registration.Resource `json:"registration"` + key crypto.PrivateKey +} + +/** Implementation of the registration.User interface **/ + +// GetEmail returns the email address for the account +func (a *Account) GetEmail() string { + return a.Email +} + +// GetPrivateKey returns the private RSA account key. +func (a *Account) GetPrivateKey() crypto.PrivateKey { + return a.key +} + +// GetRegistration returns the server registration +func (a *Account) GetRegistration() *registration.Resource { + return a.Registration +} + +/** End **/ diff --git a/cmd/accounts_storage.go b/cmd/accounts_storage.go new file mode 100644 index 00000000..4c1f18ec --- /dev/null +++ b/cmd/accounts_storage.go @@ -0,0 +1,251 @@ +package cmd + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/urfave/cli" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/registration" +) + +const ( + baseAccountsRootFolderName = "accounts" + baseKeysFolderName = "keys" + accountFileName = "account.json" +) + +// AccountsStorage A storage for account data. +// +// rootPath: +// +// ./.lego/accounts/ +// │ └── root accounts directory +// └── "path" option +// +// rootUserPath: +// +// ./.lego/accounts/localhost_14000/hubert@hubert.com/ +// │ │ │ └── userID ("email" option) +// │ │ └── CA server ("server" option) +// │ └── root accounts directory +// └── "path" option +// +// keysPath: +// +// ./.lego/accounts/localhost_14000/hubert@hubert.com/keys/ +// │ │ │ │ └── root keys directory +// │ │ │ └── userID ("email" option) +// │ │ └── CA server ("server" option) +// │ └── root accounts directory +// └── "path" option +// +// accountFilePath: +// +// ./.lego/accounts/localhost_14000/hubert@hubert.com/account.json +// │ │ │ │ └── account file +// │ │ │ └── userID ("email" option) +// │ │ └── CA server ("server" option) +// │ └── root accounts directory +// └── "path" option +// +type AccountsStorage struct { + userID string + rootPath string + rootUserPath string + keysPath string + accountFilePath string + ctx *cli.Context +} + +// NewAccountsStorage Creates a new AccountsStorage. +func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { + // TODO: move to account struct? Currently MUST pass email. + email := getEmail(ctx) + + serverURL, err := url.Parse(ctx.GlobalString("server")) + if err != nil { + log.Fatal(err) + } + + rootPath := filepath.Join(ctx.GlobalString("path"), baseAccountsRootFolderName) + serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) + accountsPath := filepath.Join(rootPath, serverPath) + rootUserPath := filepath.Join(accountsPath, email) + + return &AccountsStorage{ + userID: email, + rootPath: rootPath, + rootUserPath: rootUserPath, + keysPath: filepath.Join(rootUserPath, baseKeysFolderName), + accountFilePath: filepath.Join(rootUserPath, accountFileName), + ctx: ctx, + } +} + +func (s *AccountsStorage) ExistsAccountFilePath() bool { + accountFile := filepath.Join(s.rootUserPath, accountFileName) + if _, err := os.Stat(accountFile); os.IsNotExist(err) { + return false + } else if err != nil { + log.Fatal(err) + } + return true +} + +func (s *AccountsStorage) GetRootPath() string { + return s.rootPath +} + +func (s *AccountsStorage) GetRootUserPath() string { + return s.rootUserPath +} + +func (s *AccountsStorage) GetUserID() string { + return s.userID +} + +func (s *AccountsStorage) Save(account *Account) error { + jsonBytes, err := json.MarshalIndent(account, "", "\t") + if err != nil { + return err + } + + return ioutil.WriteFile(s.accountFilePath, jsonBytes, filePerm) +} + +func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { + fileBytes, err := ioutil.ReadFile(s.accountFilePath) + if err != nil { + log.Fatalf("Could not load file for account %s -> %v", s.userID, err) + } + + var account Account + err = json.Unmarshal(fileBytes, &account) + if err != nil { + log.Fatalf("Could not parse file for account %s -> %v", s.userID, err) + } + + account.key = privateKey + + if account.Registration == nil || account.Registration.Body.Status == "" { + reg, err := tryRecoverRegistration(s.ctx, privateKey) + if err != nil { + log.Fatalf("Could not load account for %s. Registration is nil -> %#v", s.userID, err) + } + + account.Registration = reg + err = s.Save(&account) + if err != nil { + log.Fatalf("Could not save account for %s. Registration is nil -> %#v", s.userID, err) + } + } + + return &account +} + +func (s *AccountsStorage) GetPrivateKey() crypto.PrivateKey { + accKeyPath := filepath.Join(s.keysPath, s.userID+".key") + + if _, err := os.Stat(accKeyPath); os.IsNotExist(err) { + log.Printf("No key found for account %s. Generating a curve P384 EC key.", s.userID) + s.createKeysFolder() + + privateKey, err := generatePrivateKey(accKeyPath) + if err != nil { + log.Fatalf("Could not generate RSA private account key for account %s: %v", s.userID, err) + } + + log.Printf("Saved key to %s", accKeyPath) + return privateKey + } + + privateKey, err := loadPrivateKey(accKeyPath) + if err != nil { + log.Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err) + } + + return privateKey +} + +func (s *AccountsStorage) createKeysFolder() { + if err := createNonExistingFolder(s.keysPath); err != nil { + log.Fatalf("Could not check/create directory for account %s: %v", s.userID, err) + } +} + +func generatePrivateKey(file string) (crypto.PrivateKey, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, err + } + + keyBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + + pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} + + certOut, err := os.Create(file) + if err != nil { + return nil, err + } + defer certOut.Close() + + err = pem.Encode(certOut, &pemKey) + if err != nil { + return nil, err + } + + return privateKey, nil +} + +func loadPrivateKey(file string) (crypto.PrivateKey, error) { + keyBytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + keyBlock, _ := pem.Decode(keyBytes) + + switch keyBlock.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(keyBlock.Bytes) + } + + return nil, errors.New("unknown private key type") +} + +func tryRecoverRegistration(ctx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) { + // couldn't load account but got a key. Try to look the account up. + config := lego.NewConfig(&Account{key: privateKey}) + config.CADirURL = ctx.GlobalString("server") + config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version) + + client, err := lego.NewClient(config) + if err != nil { + return nil, err + } + + reg, err := client.Registration.ResolveAccountByKey() + if err != nil { + return nil, err + } + return reg, nil +} diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go new file mode 100644 index 00000000..533794b0 --- /dev/null +++ b/cmd/certs_storage.go @@ -0,0 +1,204 @@ +package cmd + +import ( + "bytes" + "crypto/x509" + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/urfave/cli" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/log" + "golang.org/x/net/idna" +) + +const ( + baseCertificatesFolderName = "certificates" + baseArchivesFolderName = "archives" +) + +// CertificatesStorage a certificates storage. +// +// rootPath: +// +// ./.lego/certificates/ +// │ └── root certificates directory +// └── "path" option +// +// archivePath: +// +// ./.lego/archives/ +// │ └── archived certificates directory +// └── "path" option +// +type CertificatesStorage struct { + rootPath string + archivePath string + pem bool + filename string // Deprecated +} + +// NewCertificatesStorage create a new certificates storage. +func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { + return &CertificatesStorage{ + rootPath: filepath.Join(ctx.GlobalString("path"), baseCertificatesFolderName), + archivePath: filepath.Join(ctx.GlobalString("path"), baseArchivesFolderName), + pem: ctx.GlobalBool("pem"), + filename: ctx.GlobalString("filename"), + } +} + +func (s *CertificatesStorage) CreateRootFolder() { + err := createNonExistingFolder(s.rootPath) + if err != nil { + log.Fatalf("Could not check/create path: %v", err) + } +} + +func (s *CertificatesStorage) CreateArchiveFolder() { + err := createNonExistingFolder(s.archivePath) + if err != nil { + log.Fatalf("Could not check/create path: %v", err) + } +} + +func (s *CertificatesStorage) GetRootPath() string { + return s.rootPath +} + +func (s *CertificatesStorage) SaveResource(certRes *certificate.Resource) { + domain := certRes.Domain + + // We store the certificate, private key and metadata in different files + // as web servers would not be able to work with a combined file. + err := s.WriteFile(domain, ".crt", certRes.Certificate) + if err != nil { + log.Fatalf("Unable to save Certificate for domain %s\n\t%v", domain, err) + } + + if certRes.IssuerCertificate != nil { + err = s.WriteFile(domain, ".issuer.crt", certRes.IssuerCertificate) + if err != nil { + log.Fatalf("Unable to save IssuerCertificate for domain %s\n\t%v", domain, err) + } + } + + if certRes.PrivateKey != nil { + // if we were given a CSR, we don't know the private key + err = s.WriteFile(domain, ".key", certRes.PrivateKey) + if err != nil { + log.Fatalf("Unable to save PrivateKey for domain %s\n\t%v", domain, err) + } + + if s.pem { + err = s.WriteFile(domain, ".pem", bytes.Join([][]byte{certRes.Certificate, certRes.PrivateKey}, nil)) + if err != nil { + log.Fatalf("Unable to save Certificate and PrivateKey in .pem for domain %s\n\t%v", domain, err) + } + } + } else if s.pem { + // we don't have the private key; can't write the .pem file + log.Fatalf("Unable to save pem without private key for domain %s\n\t%v; are you using a CSR?", domain, err) + } + + jsonBytes, err := json.MarshalIndent(certRes, "", "\t") + if err != nil { + log.Fatalf("Unable to marshal CertResource for domain %s\n\t%v", domain, err) + } + + err = s.WriteFile(domain, ".json", jsonBytes) + if err != nil { + log.Fatalf("Unable to save CertResource for domain %s\n\t%v", domain, err) + } +} + +func (s *CertificatesStorage) ReadResource(domain string) certificate.Resource { + raw, err := s.ReadFile(domain, ".json") + if err != nil { + log.Fatalf("Error while loading the meta data for domain %s\n\t%v", domain, err) + } + + var resource certificate.Resource + if err = json.Unmarshal(raw, &resource); err != nil { + log.Fatalf("Error while marshaling the meta data for domain %s\n\t%v", domain, err) + } + + return resource +} + +func (s *CertificatesStorage) ExistsFile(domain, extension string) bool { + filename := sanitizedDomain(domain) + extension + filePath := filepath.Join(s.rootPath, filename) + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return false + } else if err != nil { + log.Fatal(err) + } + return true +} + +func (s *CertificatesStorage) ReadFile(domain, extension string) ([]byte, error) { + filename := sanitizedDomain(domain) + extension + filePath := filepath.Join(s.rootPath, filename) + + return ioutil.ReadFile(filePath) +} + +func (s *CertificatesStorage) ReadCertificate(domain, extension string) ([]*x509.Certificate, error) { + content, err := s.ReadFile(domain, extension) + if err != nil { + return nil, err + } + + // The input may be a bundle or a single certificate. + return certcrypto.ParsePEMBundle(content) +} + +func (s *CertificatesStorage) WriteFile(domain, extension string, data []byte) error { + var baseFileName string + if s.filename != "" { + baseFileName = s.filename + } else { + baseFileName = sanitizedDomain(domain) + } + + filePath := filepath.Join(s.rootPath, baseFileName+extension) + + return ioutil.WriteFile(filePath, data, filePerm) +} + +func (s *CertificatesStorage) MoveToArchive(domain string) error { + matches, err := filepath.Glob(filepath.Join(s.rootPath, sanitizedDomain(domain)+".*")) + if err != nil { + return err + } + + for _, oldFile := range matches { + date := strconv.FormatInt(time.Now().Unix(), 10) + filename := date + "." + filepath.Base(oldFile) + newFile := filepath.Join(s.archivePath, filename) + + err = os.Rename(oldFile, newFile) + if err != nil { + return err + } + } + + return nil +} + +// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)) +func sanitizedDomain(domain string) string { + safe, err := idna.ToASCII(strings.Replace(domain, "*", "_", -1)) + if err != nil { + log.Fatal(err) + } + return safe +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 00000000..2c63a5af --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,14 @@ +package cmd + +import "github.com/urfave/cli" + +// CreateCommands Creates all CLI commands +func CreateCommands() []cli.Command { + return []cli.Command{ + createRun(), + createRevoke(), + createRenew(), + createDNSHelp(), + createList(), + } +} diff --git a/cmd/cmd_before.go b/cmd/cmd_before.go new file mode 100644 index 00000000..87e0d2b0 --- /dev/null +++ b/cmd/cmd_before.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/urfave/cli" + "github.com/xenolf/lego/log" +) + +func Before(ctx *cli.Context) error { + if len(ctx.GlobalString("path")) == 0 { + log.Fatal("Could not determine current working directory. Please pass --path.") + } + + err := createNonExistingFolder(ctx.GlobalString("path")) + if err != nil { + log.Fatalf("Could not check/create path: %v", err) + } + + if len(ctx.GlobalString("server")) == 0 { + log.Fatal("Could not determine current working server. Please pass --server.") + } + + return nil +} diff --git a/cli.go b/cmd/cmd_dnshelp.go similarity index 61% rename from cli.go rename to cmd/cmd_dnshelp.go index 319d9a7e..027d7ade 100644 --- a/cli.go +++ b/cmd/cmd_dnshelp.go @@ -1,189 +1,18 @@ -// Let's Encrypt client to go! -// CLI application for generating Let's Encrypt certificates using the ACME package. -package main +package cmd import ( "fmt" "os" - "path/filepath" "text/tabwriter" "github.com/urfave/cli" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/log" ) -var ( - version = "dev" -) - -func main() { - app := cli.NewApp() - app.Name = "lego" - app.Usage = "Let's Encrypt client written in Go" - - app.Version = version - - acme.UserAgent = "lego/" + app.Version - - defaultPath := "" - cwd, err := os.Getwd() - if err == nil { - defaultPath = filepath.Join(cwd, ".lego") - } - - app.Before = func(c *cli.Context) error { - if c.GlobalString("path") == "" { - log.Fatal("Could not determine current working directory. Please pass --path.") - } - return nil - } - - app.Commands = []cli.Command{ - { - Name: "run", - Usage: "Register an account, then create and install a certificate", - Action: run, - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "no-bundle", - Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", - }, - cli.BoolFlag{ - Name: "must-staple", - Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", - }, - }, - }, - { - Name: "revoke", - Usage: "Revoke a certificate", - Action: revoke, - }, - { - Name: "renew", - Usage: "Renew a certificate", - Action: renew, - Flags: []cli.Flag{ - cli.IntFlag{ - Name: "days", - Value: 0, - Usage: "The number of days left on a certificate to renew it.", - }, - cli.BoolFlag{ - Name: "reuse-key", - Usage: "Used to indicate you want to reuse your current private key for the new certificate.", - }, - cli.BoolFlag{ - Name: "no-bundle", - Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", - }, - cli.BoolFlag{ - Name: "must-staple", - Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", - }, - }, - }, - { - Name: "dnshelp", - Usage: "Shows additional help for the --dns global option", - Action: dnsHelp, - }, - } - - app.Flags = []cli.Flag{ - cli.StringSliceFlag{ - Name: "domains, d", - Usage: "Add a domain to the process. Can be specified multiple times.", - }, - cli.StringFlag{ - Name: "csr, c", - Usage: "Certificate signing request filename, if an external CSR is to be used", - }, - cli.StringFlag{ - Name: "server, s", - Value: "https://acme-v02.api.letsencrypt.org/directory", - Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", - }, - cli.StringFlag{ - Name: "email, m", - Usage: "Email used for registration and recovery contact.", - }, - cli.StringFlag{ - Name: "filename", - Usage: "Filename of the generated certificate", - }, - cli.BoolFlag{ - Name: "accept-tos, a", - Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", - }, - cli.BoolFlag{ - Name: "eab", - Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", - }, - cli.StringFlag{ - Name: "kid", - Usage: "Key identifier from External CA. Used for External Account Binding.", - }, - cli.StringFlag{ - Name: "hmac", - Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", - }, - cli.StringFlag{ - Name: "key-type, k", - Value: "rsa2048", - Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384", - }, - cli.StringFlag{ - Name: "path", - Usage: "Directory to use for storing the data", - Value: defaultPath, - }, - cli.StringSliceFlag{ - Name: "exclude, x", - Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"dns-01\", \"tls-alpn-01\".", - }, - cli.StringFlag{ - Name: "webroot", - Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge", - }, - cli.StringSliceFlag{ - Name: "memcached-host", - Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.", - }, - cli.StringFlag{ - Name: "http", - Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", - }, - cli.StringFlag{ - Name: "tls", - Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port", - }, - cli.StringFlag{ - Name: "dns", - Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.", - }, - cli.IntFlag{ - Name: "http-timeout", - Usage: "Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds.", - }, - cli.IntFlag{ - Name: "dns-timeout", - Usage: "Set the DNS timeout value to a specific value in seconds. The default is 10 seconds.", - }, - cli.StringSliceFlag{ - Name: "dns-resolvers", - Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.", - }, - cli.BoolFlag{ - Name: "pem", - Usage: "Generate a .pem file by concatenating the .key and .crt files together.", - }, - } - - err = app.Run(os.Args) - if err != nil { - log.Fatal(err) +func createDNSHelp() cli.Command { + return cli.Command{ + Name: "dnshelp", + Usage: "Shows additional help for the --dns global option", + Action: dnsHelp, } } @@ -209,7 +38,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tbluecat:\tBLUECAT_SERVER_URL, BLUECAT_USER_NAME, BLUECAT_PASSWORD, BLUECAT_CONFIG_NAME, BLUECAT_DNS_VIEW") fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY") fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_API_KEY, CLOUDXNS_SECRET_KEY") - fmt.Fprintln(w, "\tconoha:\tCONOHA_REGION, CONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD") + fmt.Fprintln(w, "\tconoha:\tCONOHA_TENANT_ID, CONOHA_API_USERNAME, CONOHA_API_PASSWORD") fmt.Fprintln(w, "\tdigitalocean:\tDO_AUTH_TOKEN") fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_EMAIL, DNSIMPLE_OAUTH_TOKEN") fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET") @@ -246,11 +75,12 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\trfc2136:\tRFC2136_TSIG_KEY, RFC2136_TSIG_SECRET,\n\t\tRFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER") fmt.Fprintln(w, "\troute53:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID") fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_ACCESS_TOKEN, SAKURACLOUD_ACCESS_TOKEN_SECRET") - fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, STACKPATH_STACK_ID") fmt.Fprintln(w, "\tselectel:\tSELECTEL_API_TOKEN") + fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, STACKPATH_STACK_ID") + fmt.Fprintln(w, "\ttransip:\tTRANSIP_ACCOUNT_NAME, TRANSIP_PRIVATE_KEY_PATH") fmt.Fprintln(w, "\tvegadns:\tSECRET_VEGADNS_KEY, SECRET_VEGADNS_SECRET, VEGADNS_URL") - fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY") fmt.Fprintln(w, "\tvscale:\tVSCALE_API_TOKEN") + fmt.Fprintln(w, "\tvultr:\tVULTR_API_KEY") fmt.Fprintln(w) fmt.Fprintln(w, "Additional configuration environment variables:") fmt.Fprintln(w) @@ -260,21 +90,22 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tbluecat:\tBLUECAT_POLLING_INTERVAL, BLUECAT_PROPAGATION_TIMEOUT, BLUECAT_TTL, BLUECAT_HTTP_TIMEOUT") fmt.Fprintln(w, "\tcloudflare:\tCLOUDFLARE_POLLING_INTERVAL, CLOUDFLARE_PROPAGATION_TIMEOUT, CLOUDFLARE_TTL, CLOUDFLARE_HTTP_TIMEOUT") fmt.Fprintln(w, "\tcloudxns:\tCLOUDXNS_POLLING_INTERVAL, CLOUDXNS_PROPAGATION_TIMEOUT, CLOUDXNS_TTL, CLOUDXNS_HTTP_TIMEOUT") - fmt.Fprintln(w, "\tconoha:\tCONOHA_POLLING_INTERVAL, CONOHA_PROPAGATION_TIMEOUT, CONOHA_TTL, CONOHA_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tconoha:\tCONOHA_POLLING_INTERVAL, CONOHA_PROPAGATION_TIMEOUT, CONOHA_TTL, CONOHA_HTTP_TIMEOUT, CONOHA_REGION") fmt.Fprintln(w, "\tdigitalocean:\tDO_POLLING_INTERVAL, DO_PROPAGATION_TIMEOUT, DO_TTL, DO_HTTP_TIMEOUT") fmt.Fprintln(w, "\tdnsimple:\tDNSIMPLE_TTL, DNSIMPLE_POLLING_INTERVAL, DNSIMPLE_PROPAGATION_TIMEOUT") fmt.Fprintln(w, "\tdnsmadeeasy:\tDNSMADEEASY_POLLING_INTERVAL, DNSMADEEASY_PROPAGATION_TIMEOUT, DNSMADEEASY_TTL, DNSMADEEASY_HTTP_TIMEOUT") fmt.Fprintln(w, "\tdnspod:\tDNSPOD_POLLING_INTERVAL, DNSPOD_PROPAGATION_TIMEOUT, DNSPOD_TTL, DNSPOD_HTTP_TIMEOUT") fmt.Fprintln(w, "\tdreamhost:\tDREAMHOST_POLLING_INTERVAL, DREAMHOST_PROPAGATION_TIMEOUT, DREAMHOST_HTTP_TIMEOUT") - fmt.Fprintln(w, "\tduckdns:\tDUCKDNS_POLLING_INTERVAL, DUCKDNS_PROPAGATION_TIMEOUT, DUCKDNS_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tduckdns:\tDUCKDNS_POLLING_INTERVAL, DUCKDNS_PROPAGATION_TIMEOUT, DUCKDNS_HTTP_TIMEOUT, DUCKDNS_SEQUENCE_INTERVAL") fmt.Fprintln(w, "\tdyn:\tDYN_POLLING_INTERVAL, DYN_PROPAGATION_TIMEOUT, DYN_TTL, DYN_HTTP_TIMEOUT") + fmt.Fprintln(w, "\texec:\tEXEC_POLLING_INTERVAL, EXEC_PROPAGATION_TIMEOUT") fmt.Fprintln(w, "\texoscale:\tEXOSCALE_POLLING_INTERVAL, EXOSCALE_PROPAGATION_TIMEOUT, EXOSCALE_TTL, EXOSCALE_HTTP_TIMEOUT") fmt.Fprintln(w, "\tfastdns:\tAKAMAI_POLLING_INTERVAL, AKAMAI_PROPAGATION_TIMEOUT, AKAMAI_TTL") fmt.Fprintln(w, "\tgandi:\tGANDI_POLLING_INTERVAL, GANDI_PROPAGATION_TIMEOUT, GANDI_TTL, GANDI_HTTP_TIMEOUT") fmt.Fprintln(w, "\tgandiv5:\tGANDIV5_POLLING_INTERVAL, GANDIV5_PROPAGATION_TIMEOUT, GANDIV5_TTL, GANDIV5_HTTP_TIMEOUT") fmt.Fprintln(w, "\tgcloud:\tGCE_POLLING_INTERVAL, GCE_PROPAGATION_TIMEOUT, GCE_TTL") fmt.Fprintln(w, "\tglesys:\tGLESYS_POLLING_INTERVAL, GLESYS_PROPAGATION_TIMEOUT, GLESYS_TTL, GLESYS_HTTP_TIMEOUT") - fmt.Fprintln(w, "\tgodaddy:\tGODADDY_POLLING_INTERVAL, GODADDY_PROPAGATION_TIMEOUT, GODADDY_TTL, GODADDY_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tgodaddy:\tGODADDY_POLLING_INTERVAL, GODADDY_PROPAGATION_TIMEOUT, GODADDY_TTL, GODADDY_HTTP_TIMEOUT, GODADDY_SEQUENCE_INTERVAL") fmt.Fprintln(w, "\thostingde:\tHOSTINGDE_POLLING_INTERVAL, HOSTINGDE_PROPAGATION_TIMEOUT, HOSTINGDE_TTL, HOSTINGDE_HTTP_TIMEOUT") fmt.Fprintln(w, "\thttpreq:\t,HTTPREQ_POLLING_INTERVAL, HTTPREQ_PROPAGATION_TIMEOUT, HTTPREQ_HTTP_TIMEOUT") fmt.Fprintln(w, "\tiij:\tIIJ_POLLING_INTERVAL, IIJ_PROPAGATION_TIMEOUT, IIJ_TTL") @@ -282,6 +113,7 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\tlightsail:\tLIGHTSAIL_POLLING_INTERVAL, LIGHTSAIL_PROPAGATION_TIMEOUT") fmt.Fprintln(w, "\tlinode:\tLINODE_POLLING_INTERVAL, LINODE_TTL, LINODE_HTTP_TIMEOUT") fmt.Fprintln(w, "\tlinodev4:\tLINODE_POLLING_INTERVAL, LINODE_TTL, LINODE_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tmydnsjp:\tMYDNSJP_PROPAGATION_TIMEOUT, MYDNSJP_POLLING_INTERVAL, MYDNSJP_HTTP_TIMEOUT") fmt.Fprintln(w, "\tnamecheap:\tNAMECHEAP_POLLING_INTERVAL, NAMECHEAP_PROPAGATION_TIMEOUT, NAMECHEAP_TTL, NAMECHEAP_HTTP_TIMEOUT") fmt.Fprintln(w, "\tnamedotcom:\tNAMECOM_POLLING_INTERVAL, NAMECOM_PROPAGATION_TIMEOUT, NAMECOM_TTL, NAMECOM_HTTP_TIMEOUT") fmt.Fprintln(w, "\tnetcup:\tNETCUP_POLLING_INTERVAL, NETCUP_PROPAGATION_TIMEOUT, NETCUP_TTL, NETCUP_HTTP_TIMEOUT") @@ -294,11 +126,12 @@ Here is an example bash command using the CloudFlare DNS provider: fmt.Fprintln(w, "\trfc2136:\tRFC2136_POLLING_INTERVAL, RFC2136_PROPAGATION_TIMEOUT, RFC2136_TTL") fmt.Fprintln(w, "\troute53:\tAWS_POLLING_INTERVAL, AWS_PROPAGATION_TIMEOUT, AWS_TTL") fmt.Fprintln(w, "\tsakuracloud:\tSAKURACLOUD_POLLING_INTERVAL, SAKURACLOUD_PROPAGATION_TIMEOUT, SAKURACLOUD_TTL") - fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_POLLING_INTERVAL, STACKPATH_PROPAGATION_TIMEOUT, STACKPATH_TTL") fmt.Fprintln(w, "\tselectel:\tSELECTEL_BASE_URL, SELECTEL_TTL, SELECTEL_PROPAGATION_TIMEOUT, SELECTEL_POLLING_INTERVAL, SELECTEL_HTTP_TIMEOUT") + fmt.Fprintln(w, "\ttransip:\tTRANSIP_POLLING_INTERVAL, TRANSIP_PROPAGATION_TIMEOUT, TRANSIP_TTL") + fmt.Fprintln(w, "\tstackpath:\tSTACKPATH_POLLING_INTERVAL, STACKPATH_PROPAGATION_TIMEOUT, STACKPATH_TTL") fmt.Fprintln(w, "\tvegadns:\tVEGADNS_POLLING_INTERVAL, VEGADNS_PROPAGATION_TIMEOUT, VEGADNS_TTL") - fmt.Fprintln(w, "\tvultr:\tVULTR_POLLING_INTERVAL, VULTR_PROPAGATION_TIMEOUT, VULTR_TTL, VULTR_HTTP_TIMEOUT") fmt.Fprintln(w, "\tvscale:\tVSCALE_BASE_URL, VSCALE_TTL, VSCALE_PROPAGATION_TIMEOUT, VSCALE_POLLING_INTERVAL, VSCALE_HTTP_TIMEOUT") + fmt.Fprintln(w, "\tvultr:\tVULTR_POLLING_INTERVAL, VULTR_PROPAGATION_TIMEOUT, VULTR_TTL, VULTR_HTTP_TIMEOUT") w.Flush() diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go new file mode 100644 index 00000000..69228ee4 --- /dev/null +++ b/cmd/cmd_list.go @@ -0,0 +1,121 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "path/filepath" + "strings" + + "github.com/urfave/cli" + "github.com/xenolf/lego/certcrypto" +) + +func createList() cli.Command { + return cli.Command{ + Name: "list", + Usage: "Display certificates and accounts information.", + Action: list, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "accounts, a", + Usage: "Display accounts.", + }, + }, + } +} + +func list(ctx *cli.Context) error { + if ctx.Bool("accounts") { + if err := listAccount(ctx); err != nil { + return err + } + } + + return listCertificates(ctx) +} + +func listCertificates(ctx *cli.Context) error { + certsStorage := NewCertificatesStorage(ctx) + + matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt")) + if err != nil { + return err + } + + if len(matches) == 0 { + fmt.Println("No certificates found.") + return nil + } + + fmt.Println("Found the following certs:") + for _, filename := range matches { + if strings.HasSuffix(filename, ".issuer.crt") { + continue + } + + data, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + pCert, err := certcrypto.ParsePEMCertificate(data) + if err != nil { + return err + } + + fmt.Println(" Certificate Name:", pCert.Subject.CommonName) + fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", ")) + fmt.Println(" Expiry Date:", pCert.NotAfter) + fmt.Println(" Certificate Path:", filename) + fmt.Println() + } + + return nil +} + +func listAccount(ctx *cli.Context) error { + // fake email, needed by NewAccountsStorage + if err := ctx.GlobalSet("email", "unknown"); err != nil { + return err + } + + accountsStorage := NewAccountsStorage(ctx) + + matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json")) + if err != nil { + return err + } + + if len(matches) == 0 { + fmt.Println("No accounts found.") + return nil + } + + fmt.Println("Found the following accounts:") + for _, filename := range matches { + data, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + var account Account + err = json.Unmarshal(data, &account) + if err != nil { + return err + } + + uri, err := url.Parse(account.Registration.URI) + if err != nil { + return err + } + + fmt.Println(" Email:", account.Email) + fmt.Println(" Server:", uri.Host) + fmt.Println(" Path:", filepath.Dir(filename)) + fmt.Println() + } + + return nil +} diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go new file mode 100644 index 00000000..7d48e320 --- /dev/null +++ b/cmd/cmd_renew.go @@ -0,0 +1,192 @@ +package cmd + +import ( + "crypto" + "crypto/x509" + "time" + + "github.com/urfave/cli" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/log" +) + +func createRenew() cli.Command { + return cli.Command{ + Name: "renew", + Usage: "Renew a certificate", + Action: renew, + Before: func(ctx *cli.Context) error { + // we require either domains or csr, but not both + hasDomains := len(ctx.GlobalStringSlice("domains")) > 0 + hasCsr := len(ctx.GlobalString("csr")) > 0 + if hasDomains && hasCsr { + log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") + } + if !hasDomains && !hasCsr { + log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") + } + return nil + }, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "days", + Value: 15, + Usage: "The number of days left on a certificate to renew it.", + }, + cli.BoolFlag{ + Name: "reuse-key", + Usage: "Used to indicate you want to reuse your current private key for the new certificate.", + }, + cli.BoolFlag{ + Name: "no-bundle", + Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", + }, + cli.BoolFlag{ + Name: "must-staple", + Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", + }, + }, + } +} + +func renew(ctx *cli.Context) error { + account, client := setup(ctx, NewAccountsStorage(ctx)) + + if account.Registration == nil { + log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", account.Email) + } + + certsStorage := NewCertificatesStorage(ctx) + + bundle := !ctx.Bool("no-bundle") + + // CSR + if ctx.GlobalIsSet("csr") { + return renewForCSR(ctx, client, certsStorage, bundle) + } + + // Domains + return renewForDomains(ctx, client, certsStorage, bundle) +} + +func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool) error { + domains := ctx.GlobalStringSlice("domains") + domain := domains[0] + + // load the cert resource from files. + // We store the certificate, private key and metadata in different files + // as web servers would not be able to work with a combined file. + certificates, err := certsStorage.ReadCertificate(domain, ".crt") + if err != nil { + log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) + } + + cert := certificates[0] + + if !needRenewal(cert, domain, ctx.Int("days")) { + return nil + } + + // This is just meant to be informal for the user. + timeLeft := cert.NotAfter.Sub(time.Now().UTC()) + log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) + + certDomains := certcrypto.ExtractDomains(cert) + + var privateKey crypto.PrivateKey + if ctx.Bool("reuse-key") { + keyBytes, errR := certsStorage.ReadFile(domain, ".key") + if errR != nil { + log.Fatalf("Error while loading the private key for domain %s\n\t%v", domain, errR) + } + + privateKey, errR = certcrypto.ParsePEMPrivateKey(keyBytes) + if errR != nil { + return errR + } + } + + request := certificate.ObtainRequest{ + Domains: merge(certDomains, domains), + Bundle: bundle, + PrivateKey: privateKey, + MustStaple: ctx.Bool("must-staple"), + } + certRes, err := client.Certificate.Obtain(request) + if err != nil { + log.Fatal(err) + } + + certsStorage.SaveResource(certRes) + + return nil +} + +func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool) error { + csr, err := readCSRFile(ctx.GlobalString("csr")) + if err != nil { + log.Fatal(err) + } + + domain := csr.Subject.CommonName + + // load the cert resource from files. + // We store the certificate, private key and metadata in different files + // as web servers would not be able to work with a combined file. + certificates, err := certsStorage.ReadCertificate(domain, ".crt") + if err != nil { + log.Fatalf("Error while loading the certificate for domain %s\n\t%v", domain, err) + } + + cert := certificates[0] + + if !needRenewal(cert, domain, ctx.Int("days")) { + return nil + } + + // This is just meant to be informal for the user. + timeLeft := cert.NotAfter.Sub(time.Now().UTC()) + log.Infof("[%s] acme: Trying renewal with %d hours remaining", domain, int(timeLeft.Hours())) + + certRes, err := client.Certificate.ObtainForCSR(*csr, bundle) + if err != nil { + log.Fatal(err) + } + + certsStorage.SaveResource(certRes) + + return nil + +} + +func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool { + if x509Cert.IsCA { + log.Fatalf("[%s] Certificate bundle starts with a CA certificate", domain) + } + + if days >= 0 { + if int(time.Until(x509Cert.NotAfter).Hours()/24.0) > days { + return false + } + } + + return true +} + +func merge(prevDomains []string, nextDomains []string) []string { + for _, next := range nextDomains { + var found bool + for _, prev := range prevDomains { + if prev == next { + found = true + break + } + } + if !found { + prevDomains = append(prevDomains, next) + } + } + return prevDomains +} diff --git a/cmd/cmd_renew_test.go b/cmd/cmd_renew_test.go new file mode 100644 index 00000000..69ddcc92 --- /dev/null +++ b/cmd/cmd_renew_test.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_merge(t *testing.T) { + testCases := []struct { + desc string + prevDomains []string + nextDomains []string + expected []string + }{ + { + desc: "all empty", + prevDomains: []string{}, + nextDomains: []string{}, + expected: []string{}, + }, + { + desc: "next empty", + prevDomains: []string{"a", "b", "c"}, + nextDomains: []string{}, + expected: []string{"a", "b", "c"}, + }, + { + desc: "prev empty", + prevDomains: []string{}, + nextDomains: []string{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + desc: "merge append", + prevDomains: []string{"a", "b", "c"}, + nextDomains: []string{"a", "c", "d"}, + expected: []string{"a", "b", "c", "d"}, + }, + { + desc: "merge same", + prevDomains: []string{"a", "b", "c"}, + nextDomains: []string{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := merge(test.prevDomains, test.nextDomains) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/cmd/cmd_revoke.go b/cmd/cmd_revoke.go new file mode 100644 index 00000000..da446c04 --- /dev/null +++ b/cmd/cmd_revoke.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "github.com/urfave/cli" + "github.com/xenolf/lego/log" +) + +func createRevoke() cli.Command { + return cli.Command{ + Name: "revoke", + Usage: "Revoke a certificate", + Action: revoke, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "keep, k", + Usage: "Keep the certificates after the revocation instead of archiving them.", + }, + }, + } +} + +func revoke(ctx *cli.Context) error { + acc, client := setup(ctx, NewAccountsStorage(ctx)) + + if acc.Registration == nil { + log.Fatalf("Account %s is not registered. Use 'run' to register a new account.\n", acc.Email) + } + + certsStorage := NewCertificatesStorage(ctx) + certsStorage.CreateRootFolder() + + for _, domain := range ctx.GlobalStringSlice("domains") { + log.Printf("Trying to revoke certificate for domain %s", domain) + + certBytes, err := certsStorage.ReadFile(domain, ".crt") + if err != nil { + log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) + } + + err = client.Certificate.Revoke(certBytes) + if err != nil { + log.Fatalf("Error while revoking the certificate for domain %s\n\t%v", domain, err) + } + + log.Println("Certificate was revoked.") + + if ctx.Bool("keep") { + return nil + } + + certsStorage.CreateArchiveFolder() + + err = certsStorage.MoveToArchive(domain) + if err != nil { + return err + } + + log.Println("Certificate was archived for domain:", domain) + } + + return nil +} diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go new file mode 100644 index 00000000..82c5cced --- /dev/null +++ b/cmd/cmd_run.go @@ -0,0 +1,162 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/urfave/cli" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/registration" +) + +func createRun() cli.Command { + return cli.Command{ + Name: "run", + Usage: "Register an account, then create and install a certificate", + Before: func(ctx *cli.Context) error { + // we require either domains or csr, but not both + hasDomains := len(ctx.GlobalStringSlice("domains")) > 0 + hasCsr := len(ctx.GlobalString("csr")) > 0 + if hasDomains && hasCsr { + log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") + } + if !hasDomains && !hasCsr { + log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") + } + return nil + }, + Action: run, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "no-bundle", + Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.", + }, + cli.BoolFlag{ + Name: "must-staple", + Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", + }, + }, + } +} + +func run(ctx *cli.Context) error { + accountsStorage := NewAccountsStorage(ctx) + + account, client := setup(ctx, accountsStorage) + + if account.Registration == nil { + reg, err := register(ctx, client) + if err != nil { + log.Fatalf("Could not complete registration\n\t%v", err) + } + + account.Registration = reg + + if err = accountsStorage.Save(account); err != nil { + log.Fatal(err) + } + + fmt.Println("!!!! HEADS UP !!!!") + fmt.Printf(` + Your account credentials have been saved in your Let's Encrypt + configuration directory at "%s". + You should make a secure backup of this folder now. This + configuration directory will also contain certificates and + private keys obtained from Let's Encrypt so making regular + backups of this folder is ideal.`, accountsStorage.GetRootPath()) + } + + certsStorage := NewCertificatesStorage(ctx) + certsStorage.CreateRootFolder() + + cert, err := obtainCertificate(ctx, client) + if err != nil { + // Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error. + // Due to us not returning partial certificate we can just exit here instead of at the end. + log.Fatalf("Could not obtain certificates:\n\t%v", err) + } + + certsStorage.SaveResource(cert) + + return nil +} + +func handleTOS(ctx *cli.Context, client *lego.Client) bool { + // Check for a global accept override + if ctx.GlobalBool("accept-tos") { + return true + } + + reader := bufio.NewReader(os.Stdin) + log.Printf("Please review the TOS at %s", client.GetToSURL()) + + for { + fmt.Println("Do you accept the TOS? Y/n") + text, err := reader.ReadString('\n') + if err != nil { + log.Fatalf("Could not read from console: %v", err) + } + + text = strings.Trim(text, "\r\n") + switch text { + case "", "y", "Y": + return true + case "n", "N": + log.Fatal("You did not accept the TOS. Unable to proceed.") + default: + fmt.Println("Your input was invalid. Please answer with one of Y/y, n/N or by pressing enter.") + } + } +} + +func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, error) { + accepted := handleTOS(ctx, client) + if !accepted { + log.Fatal("You did not accept the TOS. Unable to proceed.") + } + + if ctx.GlobalBool("eab") { + kid := ctx.GlobalString("kid") + hmacEncoded := ctx.GlobalString("hmac") + + if kid == "" || hmacEncoded == "" { + log.Fatalf("Requires arguments --kid and --hmac.") + } + + return client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: accepted, + Kid: kid, + HmacEncoded: hmacEncoded, + }) + } + + return client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) +} + +func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Resource, error) { + bundle := !ctx.Bool("no-bundle") + + domains := ctx.GlobalStringSlice("domains") + if len(domains) > 0 { + // obtain a certificate, generating a new private key + request := certificate.ObtainRequest{ + Domains: domains, + Bundle: bundle, + MustStaple: ctx.Bool("must-staple"), + } + return client.Certificate.Obtain(request) + } + + // read the CSR + csr, err := readCSRFile(ctx.GlobalString("csr")) + if err != nil { + return nil, err + } + + // obtain a certificate for this CSR + return client.Certificate.ObtainForCSR(*csr, bundle) +} diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 00000000..f5783dba --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "github.com/urfave/cli" + "github.com/xenolf/lego/lego" +) + +func CreateFlags(defaultPath string) []cli.Flag { + return []cli.Flag{ + cli.StringSliceFlag{ + Name: "domains, d", + Usage: "Add a domain to the process. Can be specified multiple times.", + }, + cli.StringFlag{ + Name: "server, s", + Value: lego.LEDirectoryProduction, + Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", + }, + cli.BoolFlag{ + Name: "accept-tos, a", + Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", + }, + cli.StringFlag{ + Name: "email, m", + Usage: "Email used for registration and recovery contact.", + }, + cli.StringFlag{ + Name: "csr, c", + Usage: "Certificate signing request filename, if an external CSR is to be used", + }, + cli.BoolFlag{ + Name: "eab", + Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", + }, + cli.StringFlag{ + Name: "kid", + Usage: "Key identifier from External CA. Used for External Account Binding.", + }, + cli.StringFlag{ + Name: "hmac", + Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", + }, + cli.StringFlag{ + Name: "key-type, k", + Value: "rsa2048", + Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384", + }, + cli.StringFlag{ + Name: "filename", + Usage: "Filename of the generated certificate", + }, + cli.StringFlag{ + Name: "path", + Usage: "Directory to use for storing the data", + Value: defaultPath, + }, + cli.StringSliceFlag{ + Name: "exclude, x", + Usage: "Explicitly disallow solvers by name from being used. Solvers: \"http-01\", \"dns-01\", \"tls-alpn-01\".", + }, + cli.IntFlag{ + Name: "http-timeout", + Usage: "Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds.", + }, + cli.StringFlag{ + Name: "webroot", + Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge", + }, + cli.StringSliceFlag{ + Name: "memcached-host", + Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.", + }, + cli.StringFlag{ + Name: "http", + Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", + }, + cli.StringFlag{ + Name: "tls", + Usage: "Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port", + }, + cli.StringFlag{ + Name: "dns", + Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.", + }, + cli.BoolFlag{ + Name: "dns-disable-cp", + Usage: "By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.", + }, + cli.StringSliceFlag{ + Name: "dns-resolvers", + Usage: "Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.", + }, + cli.IntFlag{ + Name: "dns-timeout", + Usage: "Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. The default is 10 seconds.", + }, + cli.BoolFlag{ + Name: "pem", + Usage: "Generate a .pem file by concatenating the .key and .crt files together.", + }, + } +} diff --git a/cmd/lego/main.go b/cmd/lego/main.go new file mode 100644 index 00000000..9d0d0af8 --- /dev/null +++ b/cmd/lego/main.go @@ -0,0 +1,40 @@ +// Let's Encrypt client to go! +// CLI application for generating Let's Encrypt certificates using the ACME package. +package main + +import ( + "os" + "path/filepath" + + "github.com/urfave/cli" + "github.com/xenolf/lego/cmd" + "github.com/xenolf/lego/log" +) + +var ( + version = "dev" +) + +func main() { + app := cli.NewApp() + app.Name = "lego" + app.HelpName = "lego" + app.Usage = "Let's Encrypt client written in Go" + app.Version = version + + defaultPath := "" + cwd, err := os.Getwd() + if err == nil { + defaultPath = filepath.Join(cwd, ".lego") + } + app.Flags = cmd.CreateFlags(defaultPath) + + app.Before = cmd.Before + + app.Commands = cmd.CreateCommands() + + err = app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/setup.go b/cmd/setup.go new file mode 100644 index 00000000..84e76a82 --- /dev/null +++ b/cmd/setup.go @@ -0,0 +1,128 @@ +package cmd + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/urfave/cli" + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/registration" +) + +const filePerm os.FileMode = 0600 + +func setup(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, *lego.Client) { + privateKey := accountsStorage.GetPrivateKey() + + var account *Account + if accountsStorage.ExistsAccountFilePath() { + account = accountsStorage.LoadAccount(privateKey) + } else { + account = &Account{Email: accountsStorage.GetUserID(), key: privateKey} + } + + client := newClient(ctx, account) + + return account, client +} + +func newClient(ctx *cli.Context, acc registration.User) *lego.Client { + keyType := getKeyType(ctx) + + config := lego.NewConfig(acc) + config.CADirURL = ctx.GlobalString("server") + config.KeyType = keyType + config.UserAgent = fmt.Sprintf("lego-cli/%s", ctx.App.Version) + + if ctx.GlobalIsSet("http-timeout") { + config.HTTPClient.Timeout = time.Duration(ctx.GlobalInt("http-timeout")) * time.Second + } + + client, err := lego.NewClient(config) + if err != nil { + log.Fatalf("Could not create client: %v", err) + } + + setupChallenges(ctx, client) + + if client.GetExternalAccountRequired() && !ctx.GlobalIsSet("eab") { + log.Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.") + } + + return client +} + +// getKeyType the type from which private keys should be generated +func getKeyType(ctx *cli.Context) certcrypto.KeyType { + keyType := ctx.GlobalString("key-type") + switch strings.ToUpper(keyType) { + case "RSA2048": + return certcrypto.RSA2048 + case "RSA4096": + return certcrypto.RSA4096 + case "RSA8192": + return certcrypto.RSA8192 + case "EC256": + return certcrypto.EC256 + case "EC384": + return certcrypto.EC384 + } + + log.Fatalf("Unsupported KeyType: %s", keyType) + return "" +} + +func getEmail(ctx *cli.Context) string { + email := ctx.GlobalString("email") + if len(email) == 0 { + log.Fatal("You have to pass an account (email address) to the program using --email or -m") + } + return email +} + +func createNonExistingFolder(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return os.MkdirAll(path, 0700) + } else if err != nil { + return err + } + return nil +} + +func readCSRFile(filename string) (*x509.CertificateRequest, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + raw := bytes + + // see if we can find a PEM-encoded CSR + var p *pem.Block + rest := bytes + for { + // decode a PEM block + p, rest = pem.Decode(rest) + + // did we fail? + if p == nil { + break + } + + // did we get a CSR? + if p.Type == "CERTIFICATE REQUEST" { + raw = p.Bytes + } + } + + // no PEM-encoded CSR + // assume we were given a DER-encoded ASN.1 CSR + // (if this assumption is wrong, parsing these bytes will fail) + return x509.ParseCertificateRequest(raw) +} diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go new file mode 100644 index 00000000..f1b59084 --- /dev/null +++ b/cmd/setup_challenges.go @@ -0,0 +1,123 @@ +package cmd + +import ( + "strings" + "time" + + "github.com/urfave/cli" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/log" + "github.com/xenolf/lego/providers/dns" + "github.com/xenolf/lego/providers/http/memcached" + "github.com/xenolf/lego/providers/http/webroot" +) + +func setupChallenges(ctx *cli.Context, client *lego.Client) { + if len(ctx.GlobalStringSlice("exclude")) > 0 { + excludedSolvers(ctx, client) + } + + if ctx.GlobalIsSet("webroot") { + setupWebroot(client, ctx.GlobalString("webroot")) + } + + if ctx.GlobalIsSet("memcached-host") { + setupMemcached(client, ctx.GlobalStringSlice("memcached-host")) + } + + if ctx.GlobalIsSet("http") { + setupHTTP(client, ctx.GlobalString("http")) + } + + if ctx.GlobalIsSet("tls") { + setupTLS(client, ctx.GlobalString("tls")) + } + + if ctx.GlobalIsSet("dns") { + setupDNS(ctx, client) + } +} + +func excludedSolvers(ctx *cli.Context, client *lego.Client) { + var cc []challenge.Type + for _, s := range ctx.GlobalStringSlice("exclude") { + cc = append(cc, challenge.Type(s)) + } + client.Challenge.Exclude(cc) +} + +func setupWebroot(client *lego.Client, path string) { + provider, err := webroot.NewHTTPProvider(path) + if err != nil { + log.Fatal(err) + } + + err = client.Challenge.SetHTTP01Provider(provider) + if err != nil { + log.Fatal(err) + } + + // --webroot=foo indicates that the user specifically want to do a HTTP challenge + // infer that the user also wants to exclude all other challenges + client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) +} + +func setupMemcached(client *lego.Client, hosts []string) { + provider, err := memcached.NewMemcachedProvider(hosts) + if err != nil { + log.Fatal(err) + } + + err = client.Challenge.SetHTTP01Provider(provider) + if err != nil { + log.Fatal(err) + } + + // --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge + // infer that the user also wants to exclude all other challenges + client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) +} + +func setupHTTP(client *lego.Client, iface string) { + if !strings.Contains(iface, ":") { + log.Fatalf("The --http switch only accepts interface:port or :port for its argument.") + } + + err := client.Challenge.SetHTTP01Address(iface) + if err != nil { + log.Fatal(err) + } +} + +func setupTLS(client *lego.Client, iface string) { + if !strings.Contains(iface, ":") { + log.Fatalf("The --tls switch only accepts interface:port or :port for its argument.") + } + + err := client.Challenge.SetTLSALPN01Address(iface) + if err != nil { + log.Fatal(err) + } +} + +func setupDNS(ctx *cli.Context, client *lego.Client) { + provider, err := dns.NewDNSChallengeProviderByName(ctx.GlobalString("dns")) + if err != nil { + log.Fatal(err) + } + + servers := ctx.GlobalStringSlice("dns-resolvers") + err = client.Challenge.SetDNS01Provider(provider, + dns01.CondOption(len(servers) > 0, + dns01.AddRecursiveNameservers(dns01.ParseNameservers(ctx.GlobalStringSlice("dns-resolvers")))), + dns01.CondOption(ctx.GlobalIsSet("dns-disable-cp"), + dns01.DisableCompletePropagationRequirement()), + dns01.CondOption(ctx.GlobalIsSet("dns-timeout"), + dns01.AddDNSTimeout(time.Duration(ctx.GlobalInt("dns-timeout"))*time.Second)), + ) + if err != nil { + log.Fatal(err) + } +} diff --git a/configuration.go b/configuration.go deleted file mode 100644 index 4a8eac83..00000000 --- a/configuration.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "fmt" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/urfave/cli" - "github.com/xenolf/lego/acme" -) - -// Configuration type from CLI and config files. -type Configuration struct { - context *cli.Context -} - -// NewConfiguration creates a new configuration from CLI data. -func NewConfiguration(c *cli.Context) *Configuration { - return &Configuration{context: c} -} - -// KeyType the type from which private keys should be generated -func (c *Configuration) KeyType() (acme.KeyType, error) { - switch strings.ToUpper(c.context.GlobalString("key-type")) { - case "RSA2048": - return acme.RSA2048, nil - case "RSA4096": - return acme.RSA4096, nil - case "RSA8192": - return acme.RSA8192, nil - case "EC256": - return acme.EC256, nil - case "EC384": - return acme.EC384, nil - } - - return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type")) -} - -// ExcludedSolvers is a list of solvers that are to be excluded. -func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) { - for _, s := range c.context.GlobalStringSlice("exclude") { - cc = append(cc, acme.Challenge(s)) - } - return -} - -// ServerPath returns the OS dependent path to the data for a specific CA -func (c *Configuration) ServerPath() string { - srv, _ := url.Parse(c.context.GlobalString("server")) - return strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(srv.Host) -} - -// CertPath gets the path for certificates. -func (c *Configuration) CertPath() string { - return filepath.Join(c.context.GlobalString("path"), "certificates") -} - -// AccountsPath returns the OS dependent path to the -// local accounts for a specific CA -func (c *Configuration) AccountsPath() string { - return filepath.Join(c.context.GlobalString("path"), "accounts", c.ServerPath()) -} - -// AccountPath returns the OS dependent path to a particular account -func (c *Configuration) AccountPath(acc string) string { - return filepath.Join(c.AccountsPath(), acc) -} - -// AccountKeysPath returns the OS dependent path to the keys of a particular account -func (c *Configuration) AccountKeysPath(acc string) string { - return filepath.Join(c.AccountPath(acc), "keys") -} diff --git a/crypto.go b/crypto.go deleted file mode 100644 index 27cbf494..00000000 --- a/crypto.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "errors" - "io/ioutil" - "os" -) - -func generatePrivateKey(file string) (crypto.PrivateKey, error) { - privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return nil, err - } - - keyBytes, err := x509.MarshalECPrivateKey(privateKey) - if err != nil { - return nil, err - } - - pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} - - certOut, err := os.Create(file) - if err != nil { - return nil, err - } - defer certOut.Close() - - err = pem.Encode(certOut, &pemKey) - if err != nil { - return nil, err - } - - return privateKey, nil -} - -func loadPrivateKey(file string) (crypto.PrivateKey, error) { - keyBytes, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - - keyBlock, _ := pem.Decode(keyBytes) - - switch keyBlock.Type { - case "RSA PRIVATE KEY": - return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) - case "EC PRIVATE KEY": - return x509.ParseECPrivateKey(keyBlock.Bytes) - } - - return nil, errors.New("unknown private key type") -} diff --git a/e2e/challenges_test.go b/e2e/challenges_test.go new file mode 100644 index 00000000..af038abb --- /dev/null +++ b/e2e/challenges_test.go @@ -0,0 +1,344 @@ +package e2e + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/e2e/loader" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/registration" +) + +var load = loader.EnvLoader{ + PebbleOptions: &loader.CmdOption{ + HealthCheckURL: "https://localhost:14000/dir", + Args: []string{"-strict", "-config", "fixtures/pebble-config.json"}, + Env: []string{"PEBBLE_VA_NOSLEEP=1", "PEBBLE_WFE_NONCEREJECT=20"}, + }, + LegoOptions: []string{ + "LEGO_CA_CERTIFICATES=./fixtures/certs/pebble.minica.pem", + }, +} + +func TestMain(m *testing.M) { + os.Exit(load.MainTest(m)) +} + +func TestHelp(t *testing.T) { + output, err := load.RunLego("-h") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", output) + t.Fatal(err) + } + + fmt.Fprintf(os.Stdout, "%s\n", output) +} + +func TestChallengeHTTP_Run(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "tls-alpn-01", + "-s", "https://localhost:14000/dir", + "-d", "acme.wtf", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeTLS_Run_Domains(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-d", "acme.wtf", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeTLS_Run_CSR(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-csr", "./fixtures/csr.raw", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeTLS_Run_CSR_PEM(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-csr", "./fixtures/csr.cert", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeTLS_Run_Revoke(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-d", "lego.wtf", + "-d", "acme.lego.wtf", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } + + output, err = load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-d", "lego.wtf", + "--http", ":5002", + "--tls", ":5001", + "revoke") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeTLS_Run_Revoke_Non_ASCII(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-d", "légô.wtf", + "--http", ":5002", + "--tls", ":5001", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } + + output, err = load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "dns-01", + "-x", "http-01", + "-s", "https://localhost:14000/dir", + "-d", "légô.wtf", + "--http", ":5002", + "--tls", ":5001", + "revoke") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeHTTP_Client_Obtain(t *testing.T) { + err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") + require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + user := &fakeUser{privateKey: privateKey} + config := lego.NewConfig(user) + config.CADirURL = load.PebbleOptions.HealthCheckURL + + client, err := lego.NewClient(config) + require.NoError(t, err) + + client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.TLSALPN01}) + err = client.Challenge.SetHTTP01Address(":5002") + require.NoError(t, err) + + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + require.NoError(t, err) + user.registration = reg + + request := certificate.ObtainRequest{ + Domains: []string{"acme.wtf"}, + Bundle: true, + } + resource, err := client.Certificate.Obtain(request) + require.NoError(t, err) + + require.NotNil(t, resource) + assert.Equal(t, "acme.wtf", resource.Domain) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) + assert.NotEmpty(t, resource.Certificate) + assert.NotEmpty(t, resource.IssuerCertificate) + assert.Empty(t, resource.CSR) +} + +func TestChallengeTLS_Client_Obtain(t *testing.T) { + err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") + require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + user := &fakeUser{privateKey: privateKey} + config := lego.NewConfig(user) + config.CADirURL = load.PebbleOptions.HealthCheckURL + + client, err := lego.NewClient(config) + require.NoError(t, err) + + client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.HTTP01}) + err = client.Challenge.SetTLSALPN01Address(":5001") + require.NoError(t, err) + + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + require.NoError(t, err) + user.registration = reg + + request := certificate.ObtainRequest{ + Domains: []string{"acme.wtf"}, + Bundle: true, + PrivateKey: privateKey, + } + resource, err := client.Certificate.Obtain(request) + require.NoError(t, err) + + require.NotNil(t, resource) + assert.Equal(t, "acme.wtf", resource.Domain) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) + assert.NotEmpty(t, resource.Certificate) + assert.NotEmpty(t, resource.IssuerCertificate) + assert.Empty(t, resource.CSR) +} + +func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) { + err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem") + require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + user := &fakeUser{privateKey: privateKey} + config := lego.NewConfig(user) + config.CADirURL = load.PebbleOptions.HealthCheckURL + + client, err := lego.NewClient(config) + require.NoError(t, err) + + client.Challenge.Exclude([]challenge.Type{challenge.DNS01, challenge.HTTP01}) + err = client.Challenge.SetTLSALPN01Address(":5001") + require.NoError(t, err) + + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + require.NoError(t, err) + user.registration = reg + + csrRaw, err := ioutil.ReadFile("./fixtures/csr.raw") + require.NoError(t, err) + + csr, err := x509.ParseCertificateRequest(csrRaw) + require.NoError(t, err) + + resource, err := client.Certificate.ObtainForCSR(*csr, true) + require.NoError(t, err) + + require.NotNil(t, resource) + assert.Equal(t, "acme.wtf", resource.Domain) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL) + assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL) + assert.NotEmpty(t, resource.Certificate) + assert.NotEmpty(t, resource.IssuerCertificate) + assert.NotEmpty(t, resource.CSR) +} + +type fakeUser struct { + email string + privateKey crypto.PrivateKey + registration *registration.Resource +} + +func (f *fakeUser) GetEmail() string { return f.email } +func (f *fakeUser) GetRegistration() *registration.Resource { return f.registration } +func (f *fakeUser) GetPrivateKey() crypto.PrivateKey { return f.privateKey } diff --git a/e2e/dnschallenge/dns_challenges_test.go b/e2e/dnschallenge/dns_challenges_test.go new file mode 100644 index 00000000..f067f0e3 --- /dev/null +++ b/e2e/dnschallenge/dns_challenges_test.go @@ -0,0 +1,137 @@ +package dnschallenge + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/e2e/loader" + "github.com/xenolf/lego/lego" + "github.com/xenolf/lego/providers/dns" + "github.com/xenolf/lego/registration" +) + +var load = loader.EnvLoader{ + PebbleOptions: &loader.CmdOption{ + HealthCheckURL: "https://localhost:15000/dir", + Args: []string{"-strict", "-config", "fixtures/pebble-config-dns.json", "-dnsserver", "localhost:8053"}, + Env: []string{"PEBBLE_VA_NOSLEEP=1", "PEBBLE_WFE_NONCEREJECT=20"}, + Dir: "../", + }, + LegoOptions: []string{ + "LEGO_CA_CERTIFICATES=../fixtures/certs/pebble.minica.pem", + "EXEC_PATH=../fixtures/update-dns.sh", + }, + ChallSrv: &loader.CmdOption{ + Args: []string{"-http01", ":5012", "-tlsalpn01", ":5011"}, + }, +} + +func TestMain(m *testing.M) { + os.Exit(load.MainTest(m)) +} + +func TestDNSHelp(t *testing.T) { + output, err := load.RunLego("dnshelp") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", output) + t.Fatal(err) + } + + fmt.Fprintf(os.Stdout, "%s\n", output) +} + +func TestChallengeDNS_Run(t *testing.T) { + loader.CleanLegoFiles() + + output, err := load.RunLego( + "-m", "hubert@hubert.com", + "--accept-tos", + "-x", "http-01", + "-x", "tls-alpn-01", + "--dns-disable-cp", + "--dns-resolvers", ":8053", + "--dns", "exec", + "-s", "https://localhost:15000/dir", + "-d", "*.légo.acme", + "-d", "légo.acme", + "--http", ":5004", + "--tls", ":5003", + "run") + + if len(output) > 0 { + fmt.Fprintf(os.Stdout, "%s\n", output) + } + if err != nil { + t.Fatal(err) + } +} + +func TestChallengeDNS_Client_Obtain(t *testing.T) { + err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem") + require.NoError(t, err) + defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }() + + err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh") + require.NoError(t, err) + defer func() { _ = os.Unsetenv("EXEC_PATH") }() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "Could not generate test key") + + user := &fakeUser{privateKey: privateKey} + config := lego.NewConfig(user) + config.CADirURL = "https://localhost:15000/dir" + + client, err := lego.NewClient(config) + require.NoError(t, err) + + provider, err := dns.NewDNSChallengeProviderByName("exec") + require.NoError(t, err) + + err = client.Challenge.SetDNS01Provider(provider, + dns01.AddRecursiveNameservers([]string{":8053"}), + dns01.DisableCompletePropagationRequirement()) + client.Challenge.Exclude([]challenge.Type{challenge.HTTP01, challenge.TLSALPN01}) + require.NoError(t, err) + + reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + require.NoError(t, err) + user.registration = reg + + domains := []string{"*.légo.acme", "légo.acme"} + + request := certificate.ObtainRequest{ + Domains: domains, + Bundle: true, + PrivateKey: privateKey, + } + resource, err := client.Certificate.Obtain(request) + require.NoError(t, err) + + require.NotNil(t, resource) + assert.Equal(t, "*.xn--lgo-bma.acme", resource.Domain) + assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL) + assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL) + assert.NotEmpty(t, resource.Certificate) + assert.NotEmpty(t, resource.IssuerCertificate) + assert.Empty(t, resource.CSR) +} + +type fakeUser struct { + email string + privateKey crypto.PrivateKey + registration *registration.Resource +} + +func (f *fakeUser) GetEmail() string { return f.email } +func (f *fakeUser) GetRegistration() *registration.Resource { return f.registration } +func (f *fakeUser) GetPrivateKey() crypto.PrivateKey { return f.privateKey } diff --git a/e2e/fixtures/certs/README.md b/e2e/fixtures/certs/README.md new file mode 100644 index 00000000..7cde76f5 --- /dev/null +++ b/e2e/fixtures/certs/README.md @@ -0,0 +1,25 @@ +# certs/ + +This directory contains a CA certificate (`pebble.minica.pem`) and a private key +(`pebble.minica.key.pem`) that are used to issue a end-entity certificate (See +`certs/localhost`) for the Pebble HTTPS server. + +To get your **testing code** to use Pebble without HTTPS errors you should +configure your ACME client to trust the `pebble.minica.pem` CA certificate. Your +ACME client should offer a runtime option to specify a list of root CAs that you +can configure to include the `pebble.minica.pem` file. + +**Do not** add this CA certificate to the system trust store or in production +code!!! The CA's private key is **public** and anyone can use it to issue +certificates that will be trusted by a system with the Pebble CA in the trust +store. + +To re-create all of the Pebble certificates run: + + minica -ca-cert pebble.minica.pem \ + -ca-key pebble.minica.key.pem \ + -domains localhost,pebble \ + -ip-addresses 127.0.0.1 + +From the `test/certs/` directory after [installing +MiniCA](https://github.com/jsha/minica#installation) diff --git a/e2e/fixtures/certs/localhost/README.md b/e2e/fixtures/certs/localhost/README.md new file mode 100644 index 00000000..efa49ae2 --- /dev/null +++ b/e2e/fixtures/certs/localhost/README.md @@ -0,0 +1,5 @@ +# certs/localhost + +This directory contains an end-entity (leaf) certificate (`cert.pem`) and +a private key (`key.pem`) for the Pebble HTTPS server. It includes `127.0.0.1` +as an IP address SAN, and `[localhost, pebble]` as DNS SANs. diff --git a/e2e/fixtures/certs/localhost/cert.pem b/e2e/fixtures/certs/localhost/cert.pem new file mode 100644 index 00000000..2866a2b4 --- /dev/null +++ b/e2e/fixtures/certs/localhost/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIIbEfayDFsBtwwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMDcx +MjA2MTk0MjEwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCbFMW3DXXdErvQf2lCZ0qz0DGEWadDoF0O2neM5mVa +VQ7QGW0xc5Qwvn3Tl62C0JtwLpF0pG2BICIN+DHdVaIUwkf77iBS2doH1I3waE1I +8GkV9JrYmFY+j0dA1SwBmqUZNXhLNwZGq1a91nFSI59DZNy/JciqxoPX2K++ojU2 +FPpuXe2t51NmXMsszpa+TDqF/IeskA9A/ws6UIh4Mzhghx7oay2/qqj2IIPjAmJj +i73kdUvtEry3wmlkBvtVH50+FscS9WmPC5h3lDTk5nbzSAXKuFusotuqy3XTgY5B +PiRAwkZbEY43JNfqenQPHo7mNTt29i+NVVrBsnAa5ovrAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0T +AQH/BAIwADAiBgNVHREEGzAZgglsb2NhbGhvc3SCBnBlYmJsZYcEfwAAATANBgkq +hkiG9w0BAQsFAAOCAQEAYIkXff8H28KS0KyLHtbbSOGU4sujHHVwiVXSATACsNAE +D0Qa8hdtTQ6AUqA6/n8/u1tk0O4rPE/cTpsM3IJFX9S3rZMRsguBP7BSr1Lq/XAB +7JP/CNHt+Z9aKCKcg11wIX9/B9F7pyKM3TdKgOpqXGV6TMuLjg5PlYWI/07lVGFW +/mSJDRs8bSCFmbRtEqc4lpwlrpz+kTTnX6G7JDLfLWYw/xXVqwFfdengcDTHCc8K +wtgGq/Gu6vcoBxIO3jaca+OIkMfxxXmGrcNdseuUCa3RMZ8Qy03DqGu6Y6XQyK4B +W8zIG6H9SVKkAznM2yfYhW8v2ktcaZ95/OBHY97ZIw== +-----END CERTIFICATE----- diff --git a/e2e/fixtures/certs/localhost/key.pem b/e2e/fixtures/certs/localhost/key.pem new file mode 100644 index 00000000..66be6daa --- /dev/null +++ b/e2e/fixtures/certs/localhost/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAmxTFtw113RK70H9pQmdKs9AxhFmnQ6BdDtp3jOZlWlUO0Blt +MXOUML5905etgtCbcC6RdKRtgSAiDfgx3VWiFMJH++4gUtnaB9SN8GhNSPBpFfSa +2JhWPo9HQNUsAZqlGTV4SzcGRqtWvdZxUiOfQ2TcvyXIqsaD19ivvqI1NhT6bl3t +redTZlzLLM6Wvkw6hfyHrJAPQP8LOlCIeDM4YIce6Gstv6qo9iCD4wJiY4u95HVL +7RK8t8JpZAb7VR+dPhbHEvVpjwuYd5Q05OZ280gFyrhbrKLbqst104GOQT4kQMJG +WxGONyTX6np0Dx6O5jU7dvYvjVVawbJwGuaL6wIDAQABAoIBAGW9W/S6lO+DIcoo +PHL+9sg+tq2gb5ZzN3nOI45BfI6lrMEjXTqLG9ZasovFP2TJ3J/dPTnrwZdr8Et/ +357YViwORVFnKLeSCnMGpFPq6YEHj7mCrq+YSURjlRhYgbVPsi52oMOfhrOIJrEG +ZXPAwPRi0Ftqu1omQEqz8qA7JHOkjB2p0i2Xc/uOSJccCmUDMlksRYz8zFe8wHuD +XvUL2k23n2pBZ6wiez6Xjr0wUQ4ESI02x7PmYgA3aqF2Q6ECDwHhjVeQmAuypMF6 +IaTjIJkWdZCW96pPaK1t+5nTNZ+Mg7tpJ/PRE4BkJvqcfHEOOl6wAE8gSk5uVApY +ZRKGmGkCgYEAzF9iRXYo7A/UphL11bR0gqxB6qnQl54iLhqS/E6CVNcmwJ2d9pF8 +5HTfSo1/lOXT3hGV8gizN2S5RmWBrc9HBZ+dNrVo7FYeeBiHu+opbX1X/C1HC0m1 +wJNsyoXeqD1OFc1WbDpHz5iv4IOXzYdOdKiYEcTv5JkqE7jomqBLQk8CgYEAwkG/ +rnwr4ThUo/DG5oH+l0LVnHkrJY+BUSI33g3eQ3eM0MSbfJXGT7snh5puJW0oXP7Z +Gw88nK3Vnz2nTPesiwtO2OkUVgrIgWryIvKHaqrYnapZHuM+io30jbZOVaVTMR9c +X/7/d5/evwXuP7p2DIdZKQKKFgROm1XnhNqVgaUCgYBD/ogHbCR5RVsOVciMbRlG +UGEt3YmUp/vfMuAsKUKbT2mJM+dWHVlb+LZBa4pC06QFgfxNJi/aAhzSGvtmBEww +xsXbaceauZwxgJfIIUPfNZCMSdQVIVTi2Smcx6UofBz6i/Jw14MEwlvhamaa7qVf +kqflYYwelga1wRNCPopLaQKBgQCWsZqZKQqBNMm0Q9yIhN+TR+2d7QFjqeePoRPl +1qxNejhq25ojE607vNv1ff9kWUGuoqSZMUC76r6FQba/JoNbefI4otd7x/GzM9uS +8MHMJazU4okwROkHYwgLxxkNp6rZuJJYheB4VDTfyyH/ng5lubmY7rdgTQcNyZ5I +majRYQKBgAMKJ3RlII0qvAfNFZr4Y2bNIq+60Z+Qu2W5xokIHCFNly3W1XDDKGFe +CCPHSvQljinke3P9gPt2HVdXxcnku9VkTti+JygxuLkVg7E0/SWwrWfGsaMJs+84 +fK+mTZay2d3v24r9WKEKwLykngYPyZw5+BdWU0E+xx5lGUd3U4gG +-----END RSA PRIVATE KEY----- diff --git a/e2e/fixtures/certs/pebble.minica.key.pem b/e2e/fixtures/certs/pebble.minica.key.pem new file mode 100644 index 00000000..6a7fcd9d --- /dev/null +++ b/e2e/fixtures/certs/pebble.minica.key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuVoGTaFSWp3Y+N5JC8lOdL8wmWpaM73UaNzhYiqA7ZqijzVk +TTtoQvQFDcUwyXKOdWHONrv1ld3z224Us504jjlbZwI5uoquCOZ2WJbRhmXrRgzk +Fq+/MtoFmPkhtO/DLjjtocgyIirVXN8Yl2APvB5brvRfCm6kktYeecsWfW/O3ikf +gdM7tmocwQiBypiloHOjdd5e2g8cWNw+rqvILSUVNLaLpsi23cxnLqVb424wz9dZ +5dO0REg1gSxtf4N5LSb6iGuAVoFNhzIeKzQ+svDg9x8tx/DGOghJS/jDgmxSY1qo +bTsXhcmWVfat5GJ5PQgLkCSjBBrjeBlOrc4VtQIDAQABAoIBAQCAoRoou6C0ZEDU +DScyN8TrvlcS0LzClaWYFFmRT5/jxOG1cr8l3elwNXpgYQ2Hb6mvim2ajHxVQg/e +oxlYwO4jvWhSJzg63c0DPjS5LAlCNO6+0Wlk2RheSPGDhLlAoPeZ10YKdS1dis5B +Qk4Fl1O0IHlOBCcEzV4GzPOfYDI+X6/f4xY7qz1s+CgoIxjIeiG+1/WpZQpYhobY +7CfSDdYDKtksXi7iQkc5earUAHBqZ1gQTq6e5LVm9AjRzENhMctFgcPs5zOjp2ak +PluixrA8LTAfu9wQzvxDkPl0UarZVxCerw6nlAziILpQ+U6PtoPZj49VpntTc+cq +1qjzkbhBAoGBANElJmFWY2X6LgBpszeqt0ZOSbkFg2bC0wHCJrMlRzUMEn83w9e8 +Z2Fqml9eCC5qxJcyxWDVQeoAX6090m0qgP8xNmGdafcVic2cUlrqtkqhhst2OHCO +MCQEB7cdsjiidNNrOgLbQ3i1bYID8BVLf/TDhEbRgvTewDaz6XPdoSIRAoGBAOLg +RuOec5gn50SrVycx8BLFO8AXjXojpZb1Xg26V5miz1IavSfDcgae/699ppSz+UWi +jGMFr/PokY2JxDVs3PyQLu7ahMzyFHr16Agvp5g5kq056XV+uI/HhqLHOWSQ09DS +1Vrj7FOYpKRzge3/AC7ty9Vr35uMiebpm4/CLFVlAoGALnsIJZfSbWaFdLgJCXUa +WDir77/G7T6dMIXanfPJ+IMfVUCqeLa5bxAHEOzP+qjl2giBjzy18nB00warTnGk +y5I/WMBoPW5++sAkGWqSatGtKGi0sGcZUdfHcy3ZXvbT6eyprtrWCuyfUsbXQ5RM +8rPFIQwNA6jBpSak2ohF+FECgYEAn+6IKncNd6pRfnfmdSvf1+uPxkcUJZCxb2xC +xByjGhvKWE+fHkPJwt8c0SIbZuJEC5Gds0RUF/XPfV4roZm/Yo9ldl02lp7kTxXA +XtzxIP8c5d5YM8qD4l8+Csu0Kq9pkeC+JFddxkRpc8A1TIehInPhZ+6mb6mvoMb3 +MW0pAX0CgYATT74RYuIYWZvx0TK4ZXIKTw2i6HObLF63Y6UwyPXXdEVie/ToYRNH +JIxE1weVpHvnHZvVD6D3yGk39ZsCIt31VvKpatWXlWBm875MbBc6kuIGsYT+mSSj +y9TXaE89E5zfL27nZe15QLJ+Xw8Io6PMLZ/jtC5TYoEixSZ9J8v6HA== +-----END RSA PRIVATE KEY----- diff --git a/e2e/fixtures/certs/pebble.minica.pem b/e2e/fixtures/certs/pebble.minica.pem new file mode 100644 index 00000000..a69a4c41 --- /dev/null +++ b/e2e/fixtures/certs/pebble.minica.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIIJOLbes8sTr4wDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE +AxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMCAXDTE3MTIwNjE5NDIxMFoYDzIxMTcx +MjA2MTk0MjEwWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ +alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn +Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu +9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0 +toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3 +Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB +AAGjRTBDMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAF85v +d40HK1ouDAtWeO1PbnWfGEmC5Xa478s9ddOd9Clvp2McYzNlAFfM7kdcj6xeiNhF +WPIfaGAi/QdURSL/6C1KsVDqlFBlTs9zYfh2g0UXGvJtj1maeih7zxFLvet+fqll +xseM4P9EVJaQxwuK/F78YBt0tCNfivC6JNZMgxKF59h0FBpH70ytUSHXdz7FKwix +Mfn3qEb9BXSk0Q3prNV5sOV3vgjEtB4THfDxSz9z3+DepVnW3vbbqwEbkXdk3j82 +2muVldgOUgTwK8eT+XdofVdntzU/kzygSAtAQwLJfn51fS1GvEcYGBc1bDryIqmF +p9BI7gVKtWSZYegicA== +-----END CERTIFICATE----- diff --git a/e2e/fixtures/csr.cert b/e2e/fixtures/csr.cert new file mode 100644 index 00000000..cece7dde --- /dev/null +++ b/e2e/fixtures/csr.cert @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICfjCCAWYCAQAwEzERMA8GA1UEAxMIYWNtZS53dGYwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDAhXnho1w9OPHWs4YSMahYbG4Ui1K6hsHytBZfhsz0 +09igSWzHMEFZYHZJVuSr60enuJSZRhgwDjfhQWSUgHgKItLPnlNVYM6RhVaW0WfT +w6CpmE2AuH3WuQbrR2he1Nt0xfUJla+VWOFZuW7GhgBiV5iWBvdLv6Ztgh8eATjo +2vG2R+KuSUzrm6h+sb3nUR28OYunZ3vESjNwnL3/D/1th2rFpe3EA3em1HArJdXN +F4eclciun5Js17AS9tdoHEEZMMBWyViiuz3CQlh+YD2qAvqaubanWNa+r+iijMvd +4HlDHC99LTk6TJoSKoL+E/OGKmntLqmBJ1UrCFgvnw3DAgMBAAGgJjAkBgkqhkiG +9w0BCQ4xFzAVMBMGA1UdEQQMMAqCCGFjbWUud3RmMA0GCSqGSIb3DQEBCwUAA4IB +AQAfBLR8njftxf15V49szNsgNaG7Y5UQFwgl8pyiIaanGvX1DE0BtU1RB/w7itzX +wW5W/wjielEbs1XkI2uz3hkebvHVA1QpA7bbrX01WonS18xCkiRDj8ZqFEG4vEGa +HswzGUfq2v0gCOIPpVGE+8Q2Y7In5zwEfev+5DkHox4/vgwMhyPMI+y7jKtdG/dV +U58SFnt/F1raoSmR6vfDcAFXm/L8LXEkxqqefFbhiRHRqQar1Wr15BH//swmNzEW +5SVCCHcyIqreSua8uPjBcJ8aYVLniX6DMRyYv4ij/PSvSQy9xJDewLqR235WfTd/ +tk4hhJaqizKDpsvB+UFod5o5 +-----END CERTIFICATE REQUEST----- diff --git a/e2e/fixtures/csr.raw b/e2e/fixtures/csr.raw new file mode 100644 index 00000000..f4bb701c Binary files /dev/null and b/e2e/fixtures/csr.raw differ diff --git a/e2e/fixtures/pebble-config-dns.json b/e2e/fixtures/pebble-config-dns.json new file mode 100644 index 00000000..4834825a --- /dev/null +++ b/e2e/fixtures/pebble-config-dns.json @@ -0,0 +1,9 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:15000", + "certificate": "fixtures/certs/localhost/cert.pem", + "privateKey": "fixtures/certs/localhost/key.pem", + "httpPort": 5004, + "tlsPort": 5003 + } +} diff --git a/e2e/fixtures/pebble-config.json b/e2e/fixtures/pebble-config.json new file mode 100644 index 00000000..f2abe6ab --- /dev/null +++ b/e2e/fixtures/pebble-config.json @@ -0,0 +1,9 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:14000", + "certificate": "fixtures/certs/localhost/cert.pem", + "privateKey": "fixtures/certs/localhost/key.pem", + "httpPort": 5002, + "tlsPort": 5001 + } +} diff --git a/e2e/fixtures/update-dns.sh b/e2e/fixtures/update-dns.sh new file mode 100755 index 00000000..44343c6a --- /dev/null +++ b/e2e/fixtures/update-dns.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Simple DNS challenge exec solver. +# Use challtestsrv https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv + +set -e + +case "$1" in + "present") + echo "Present" + payload="{\"host\":\"$2\", \"value\":\"$3\"}" + echo "payload=${payload}" + curl -s -X POST -d "${payload}" localhost:8055/set-txt + ;; + "cleanup") + echo "cleanup" + payload="{\"host\":\"$2\"}" + echo "payload=${payload}" + curl -s -X POST -d "${payload}" localhost:8055/clear-txt + ;; + *) + echo "OOPS" + ;; +esac diff --git a/e2e/loader/loader.go b/e2e/loader/loader.go new file mode 100644 index 00000000..bad90a0f --- /dev/null +++ b/e2e/loader/loader.go @@ -0,0 +1,304 @@ +package loader + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/xenolf/lego/platform/wait" +) + +const ( + cmdNamePebble = "pebble" + cmdNameChallSrv = "challtestsrv" +) + +type CmdOption struct { + HealthCheckURL string + Args []string + Env []string + Dir string +} + +type EnvLoader struct { + PebbleOptions *CmdOption + LegoOptions []string + ChallSrv *CmdOption + lego string +} + +func (l *EnvLoader) MainTest(m *testing.M) int { + _, force := os.LookupEnv("LEGO_E2E_TESTS") + if _, ci := os.LookupEnv("CI"); !ci && !force { + fmt.Fprintln(os.Stderr, "skipping test: e2e tests are disabled. (no 'CI' or 'LEGO_E2E_TESTS' env var)") + fmt.Println("PASS") + return 0 + } + + if _, err := exec.LookPath("git"); err != nil { + fmt.Fprintln(os.Stderr, "skipping because git command not found") + fmt.Println("PASS") + return 0 + } + + if l.PebbleOptions != nil { + if _, err := exec.LookPath(cmdNamePebble); err != nil { + fmt.Fprintln(os.Stderr, "skipping because pebble binary not found") + fmt.Println("PASS") + return 0 + } + } + + if l.ChallSrv != nil { + if _, err := exec.LookPath(cmdNameChallSrv); err != nil { + fmt.Fprintln(os.Stderr, "skipping because challtestsrv binary not found") + fmt.Println("PASS") + return 0 + } + } + + pebbleTearDown := l.launchPebble() + defer pebbleTearDown() + + challSrvTearDown := l.launchChallSrv() + defer challSrvTearDown() + + legoBinary, tearDown, err := buildLego() + defer tearDown() + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + + l.lego = legoBinary + + if l.PebbleOptions != nil && l.PebbleOptions.HealthCheckURL != "" { + pebbleHealthCheck(l.PebbleOptions) + } + + return m.Run() +} + +func (l *EnvLoader) RunLego(arg ...string) ([]byte, error) { + cmd := exec.Command(l.lego, arg...) + cmd.Env = l.LegoOptions + + fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) + + return cmd.CombinedOutput() +} + +func (l *EnvLoader) launchPebble() func() { + if l.PebbleOptions == nil { + return func() {} + } + + pebble, outPebble := l.cmdPebble() + go func() { + err := pebble.Run() + if err != nil { + fmt.Println(err) + } + }() + + return func() { + err := pebble.Process.Kill() + if err != nil { + fmt.Println(err) + } + fmt.Println(outPebble.String()) + } +} + +func (l *EnvLoader) cmdPebble() (*exec.Cmd, *bytes.Buffer) { + cmd := exec.Command(cmdNamePebble, l.PebbleOptions.Args...) + cmd.Env = l.PebbleOptions.Env + + dir, err := filepath.Abs(l.PebbleOptions.Dir) + if err != nil { + panic(err) + } + cmd.Dir = dir + + fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) + + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + return cmd, &b +} + +func pebbleHealthCheck(options *CmdOption) { + client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} + err := wait.For(10*time.Second, 500*time.Millisecond, func() (bool, error) { + resp, err := client.Get(options.HealthCheckURL) + if err != nil { + return false, err + } + + if resp.StatusCode != http.StatusOK { + return false, nil + } + + return true, nil + }) + if err != nil { + panic(err) + } +} + +func (l *EnvLoader) launchChallSrv() func() { + if l.ChallSrv == nil { + return func() {} + } + + challtestsrv, outChalSrv := l.cmdChallSrv() + go func() { + err := challtestsrv.Run() + if err != nil { + fmt.Println(err) + } + }() + + return func() { + err := challtestsrv.Process.Kill() + if err != nil { + fmt.Println(err) + } + fmt.Println(outChalSrv.String()) + } +} + +func (l *EnvLoader) cmdChallSrv() (*exec.Cmd, *bytes.Buffer) { + cmd := exec.Command(cmdNameChallSrv, l.ChallSrv.Args...) + + fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) + + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + return cmd, &b +} + +func buildLego() (string, func(), error) { + here, err := os.Getwd() + if err != nil { + return "", func() {}, err + } + defer func() { _ = os.Chdir(here) }() + + buildPath, err := ioutil.TempDir("", "lego_test") + if err != nil { + return "", func() {}, err + } + + projectRoot, err := getProjectRoot() + if err != nil { + return "", func() {}, err + } + + mainFolder := filepath.Join(projectRoot, "cmd", "lego") + + err = os.Chdir(mainFolder) + if err != nil { + return "", func() {}, err + } + + binary := filepath.Join(buildPath, "lego") + + err = build(binary) + if err != nil { + return "", func() {}, err + } + + err = os.Chdir(here) + if err != nil { + return "", func() {}, err + } + + return binary, func() { + _ = os.RemoveAll(buildPath) + CleanLegoFiles() + }, nil +} + +func getProjectRoot() (string, error) { + git := exec.Command("git", "rev-parse", "--show-toplevel") + + output, err := git.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", output) + return "", err + } + + return strings.TrimSpace(string(output)), nil +} + +func build(binary string) error { + toolPath, err := goToolPath() + if err != nil { + return err + } + cmd := exec.Command(toolPath, "build", "-o", binary) + + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", output) + return err + } + + return nil +} + +func goToolPath() (string, error) { + // inspired by go1.11.1/src/internal/testenv/testenv.go + if os.Getenv("GO_GCFLAGS") != "" { + return "", errors.New("'go build' not compatible with setting $GO_GCFLAGS") + } + + if runtime.GOOS == "darwin" && strings.HasPrefix(runtime.GOARCH, "arm") { + return "", fmt.Errorf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH) + } + + return goTool() +} + +func goTool() (string, error) { + var exeSuffix string + if runtime.GOOS == "windows" { + exeSuffix = ".exe" + } + + path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) + if _, err := os.Stat(path); err == nil { + return path, nil + } + + goBin, err := exec.LookPath("go" + exeSuffix) + if err != nil { + return "", fmt.Errorf("cannot find go tool: %v", err) + } + + return goBin, nil +} + +func CleanLegoFiles() { + cmd := exec.Command("rm", "-rf", ".lego") + fmt.Printf("$ %s\n", strings.Join(cmd.Args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(output)) + } +} diff --git a/e2e/readme.md b/e2e/readme.md new file mode 100644 index 00000000..20e8bb85 --- /dev/null +++ b/e2e/readme.md @@ -0,0 +1,27 @@ +# E2E tests + +How to run: + +- Add the following entries to your `/etc/hosts`: +``` +127.0.0.1 acme.wtf +127.0.0.1 lego.wtf +127.0.0.1 acme.lego.wtf +127.0.0.1 légô.wtf +127.0.0.1 xn--lg-bja9b.wtf +``` + +- Install [Pebble](https://github.com/letsencrypt/pebble): +```bash +go get -u github.com/letsencrypt/pebble/... +``` + +- Install [challtestsrv](https://github.com/letsencrypt/boulder/tree/master/test/challtestsrv): +```bash +go get -u github.com/letsencrypt/boulder/test/challtestsrv/... +``` + +- Launch tests: +```bash +make e2e +``` diff --git a/lego/client.go b/lego/client.go new file mode 100644 index 00000000..5235b1de --- /dev/null +++ b/lego/client.go @@ -0,0 +1,73 @@ +package lego + +import ( + "errors" + "net/url" + + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/certificate" + "github.com/xenolf/lego/challenge/resolver" + "github.com/xenolf/lego/registration" +) + +// Client is the user-friendly way to ACME +type Client struct { + Certificate *certificate.Certifier + Challenge *resolver.SolverManager + Registration *registration.Registrar + core *api.Core +} + +// NewClient creates a new ACME client on behalf of the user. +// The client will depend on the ACME directory located at CADirURL for the rest of its actions. +// A private key of type keyType (see KeyType constants) will be generated when requesting a new certificate if one isn't provided. +func NewClient(config *Config) (*Client, error) { + if config == nil { + return nil, errors.New("a configuration must be provided") + } + + _, err := url.Parse(config.CADirURL) + if err != nil { + return nil, err + } + + if config.HTTPClient == nil { + return nil, errors.New("the HTTP client cannot be nil") + } + + privateKey := config.User.GetPrivateKey() + if privateKey == nil { + return nil, errors.New("private key was nil") + } + + var kid string + if reg := config.User.GetRegistration(); reg != nil { + kid = reg.URI + } + + core, err := api.New(config.HTTPClient, config.UserAgent, config.CADirURL, kid, privateKey) + if err != nil { + return nil, err + } + + solversManager := resolver.NewSolversManager(core) + + prober := resolver.NewProber(solversManager) + + return &Client{ + Certificate: certificate.NewCertifier(core, config.KeyType, prober), + Challenge: solversManager, + Registration: registration.NewRegistrar(core, config.User), + core: core, + }, nil +} + +// GetToSURL returns the current ToS URL from the Directory +func (c *Client) GetToSURL() string { + return c.core.GetDirectory().Meta.TermsOfService +} + +// GetExternalAccountRequired returns the External Account Binding requirement of the Directory +func (c *Client) GetExternalAccountRequired() bool { + return c.core.GetDirectory().Meta.ExternalAccountRequired +} diff --git a/lego/client_config.go b/lego/client_config.go new file mode 100644 index 00000000..738be86a --- /dev/null +++ b/lego/client_config.go @@ -0,0 +1,96 @@ +package lego + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "time" + + "github.com/xenolf/lego/certcrypto" + "github.com/xenolf/lego/registration" +) + +const ( + // caCertificatesEnvVar is the environment variable name that can be used to + // specify the path to PEM encoded CA Certificates that can be used to + // authenticate an ACME server with a HTTPS certificate not issued by a CA in + // the system-wide trusted root list. + caCertificatesEnvVar = "LEGO_CA_CERTIFICATES" + + // caServerNameEnvVar is the environment variable name that can be used to + // specify the CA server name that can be used to + // authenticate an ACME server with a HTTPS certificate not issued by a CA in + // the system-wide trusted root list. + caServerNameEnvVar = "LEGO_CA_SERVER_NAME" + + // LEDirectoryProduction URL to the Let's Encrypt production + LEDirectoryProduction = "https://acme-v02.api.letsencrypt.org/directory" + + // LEDirectoryStaging URL to the Let's Encrypt staging + LEDirectoryStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" +) + +type Config struct { + CADirURL string + User registration.User + KeyType certcrypto.KeyType + UserAgent string + HTTPClient *http.Client +} + +func NewConfig(user registration.User) *Config { + return &Config{ + CADirURL: LEDirectoryProduction, + User: user, + KeyType: certcrypto.RSA2048, + HTTPClient: createDefaultHTTPClient(), + } +} + +// createDefaultHTTPClient Creates an HTTP client with a reasonable timeout value +// and potentially a custom *x509.CertPool +// based on the caCertificatesEnvVar environment variable (see the `initCertPool` function) +func createDefaultHTTPClient() *http.Client { + return &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 15 * time.Second, + ResponseHeaderTimeout: 15 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{ + ServerName: os.Getenv(caServerNameEnvVar), + RootCAs: initCertPool(), + }, + }, + } +} + +// initCertPool creates a *x509.CertPool populated with the PEM certificates +// found in the filepath specified in the caCertificatesEnvVar OS environment +// variable. If the caCertificatesEnvVar is not set then initCertPool will +// return nil. If there is an error creating a *x509.CertPool from the provided +// caCertificatesEnvVar value then initCertPool will panic. +func initCertPool() *x509.CertPool { + if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" { + customCAs, err := ioutil.ReadFile(customCACertsPath) + if err != nil { + panic(fmt.Sprintf("error reading %s=%q: %v", + caCertificatesEnvVar, customCACertsPath, err)) + } + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(customCAs); !ok { + panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v", + caCertificatesEnvVar, customCACertsPath, err)) + } + return certPool + } + return nil +} diff --git a/lego/client_test.go b/lego/client_test.go new file mode 100644 index 00000000..f9be9cff --- /dev/null +++ b/lego/client_test.go @@ -0,0 +1,46 @@ +package lego + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/platform/tester" + "github.com/xenolf/lego/registration" +) + +func TestNewClient(t *testing.T) { + _, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + keyBits := 32 // small value keeps test fast + key, err := rsa.GenerateKey(rand.Reader, keyBits) + require.NoError(t, err, "Could not generate test key") + + user := mockUser{ + email: "test@test.com", + regres: new(registration.Resource), + privatekey: key, + } + + config := NewConfig(user) + config.CADirURL = apiURL + "/dir" + + client, err := NewClient(config) + require.NoError(t, err, "Could not create client") + + assert.NotNil(t, client) +} + +type mockUser struct { + email string + regres *registration.Resource + privatekey *rsa.PrivateKey +} + +func (u mockUser) GetEmail() string { return u.email } +func (u mockUser) GetRegistration() *registration.Resource { return u.regres } +func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } diff --git a/platform/tester/api.go b/platform/tester/api.go new file mode 100644 index 00000000..b5cbf122 --- /dev/null +++ b/platform/tester/api.go @@ -0,0 +1,62 @@ +package tester + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + + "github.com/xenolf/lego/acme" +) + +// SetupFakeAPI Minimal stub ACME server for validation. +func SetupFakeAPI() (*http.ServeMux, string, func()) { + mux := http.NewServeMux() + ts := httptest.NewServer(mux) + + mux.HandleFunc("/dir", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + err := WriteJSONResponse(w, acme.Directory{ + NewNonceURL: ts.URL + "/nonce", + NewAccountURL: ts.URL + "/account", + NewOrderURL: ts.URL + "/newOrder", + RevokeCertURL: ts.URL + "/revokeCert", + KeyChangeURL: ts.URL + "/keyChange", + }) + + mux.HandleFunc("/nonce", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + w.Header().Add("Replay-Nonce", "12345") + w.Header().Add("Retry-After", "0") + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + return mux, ts.URL, ts.Close +} + +// WriteJSONResponse marshals the body as JSON and writes it to the response. +func WriteJSONResponse(w http.ResponseWriter, body interface{}) error { + bs, err := json.Marshal(body) + if err != nil { + return err + } + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(bs); err != nil { + return err + } + + return nil +} diff --git a/platform/tester/env_test.go b/platform/tester/env_test.go index a17f0ab1..3e6495e8 100644 --- a/platform/tester/env_test.go +++ b/platform/tester/env_test.go @@ -9,7 +9,7 @@ import ( "github.com/xenolf/lego/platform/tester" ) -var ( +const ( envNamespace = "LEGO_TEST_" envVar01 = envNamespace + "01" envVar02 = envNamespace + "02" diff --git a/acme/utils.go b/platform/wait/wait.go similarity index 71% rename from acme/utils.go rename to platform/wait/wait.go index f3160806..65090bf8 100644 --- a/acme/utils.go +++ b/platform/wait/wait.go @@ -1,4 +1,4 @@ -package acme +package wait import ( "fmt" @@ -7,8 +7,8 @@ import ( "github.com/xenolf/lego/log" ) -// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. -func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { +// For polls the given function 'f', once every 'interval', up to 'timeout'. +func For(timeout, interval time.Duration, f func() (bool, error)) error { log.Infof("Wait [timeout: %s, interval: %s]", timeout, interval) var lastErr string diff --git a/acme/utils_test.go b/platform/wait/wait_test.go similarity index 71% rename from acme/utils_test.go rename to platform/wait/wait_test.go index 158af411..42f00894 100644 --- a/acme/utils_test.go +++ b/platform/wait/wait_test.go @@ -1,14 +1,14 @@ -package acme +package wait import ( "testing" "time" ) -func TestWaitForTimeout(t *testing.T) { +func TestForTimeout(t *testing.T) { c := make(chan error) go func() { - err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) { + err := For(3*time.Second, 1*time.Second, func() (bool, error) { return false, nil }) c <- err diff --git a/providers/dns/acmedns/acmedns.go b/providers/dns/acmedns/acmedns.go index 9ad0ef36..e642420a 100644 --- a/providers/dns/acmedns/acmedns.go +++ b/providers/dns/acmedns/acmedns.go @@ -7,7 +7,7 @@ import ( "fmt" "github.com/cpu/goacmedns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -17,21 +17,19 @@ const ( // apiBaseEnvVar is the environment variable name for the ACME-DNS API address // (e.g. https://acmedns.your-domain.com). apiBaseEnvVar = envNamespace + "API_BASE" - // storagePathEnvVar is the environment variable name for the ACME-DNS JSON - // account data file. A per-domain account will be registered/persisted to - // this file and used for TXT updates. + // storagePathEnvVar is the environment variable name for the ACME-DNS JSON account data file. + // A per-domain account will be registered/persisted to this file and used for TXT updates. storagePathEnvVar = envNamespace + "STORAGE_PATH" ) -// acmeDNSClient is an interface describing the goacmedns.Client functions -// the DNSProvider uses. It makes it easier for tests to shim a mock Client into -// the DNSProvider. +// acmeDNSClient is an interface describing the goacmedns.Client functions the DNSProvider uses. +// It makes it easier for tests to shim a mock Client into the DNSProvider. type acmeDNSClient interface { - // UpdateTXTRecord updates the provided account's TXT record to the given - // value or returns an error. + // UpdateTXTRecord updates the provided account's TXT record + // to the given value or returns an error. UpdateTXTRecord(goacmedns.Account, string) error - // RegisterAccount registers and returns a new account with the given - // allowFrom restriction or returns an error. + // RegisterAccount registers and returns a new account + // with the given allowFrom restriction or returns an error. RegisterAccount([]string) (goacmedns.Account, error) } @@ -43,8 +41,7 @@ type DNSProvider struct { } // NewDNSProvider creates an ACME-DNS provider using file based account storage. -// Its configuration is loaded from the environment by reading apiBaseEnvVar and -// storagePathEnvVar. +// Its configuration is loaded from the environment by reading apiBaseEnvVar and storagePathEnvVar. func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get(apiBaseEnvVar, storagePathEnvVar) if err != nil { @@ -56,8 +53,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderClient(client, storage) } -// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given -// acmeDNSClient and goacmedns.Storage. +// NewDNSProviderClient creates an ACME-DNS DNSProvider with the given acmeDNSClient and goacmedns.Storage. func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNSProvider, error) { if client == nil { return nil, errors.New("ACME-DNS Client must be not nil") @@ -76,8 +72,7 @@ func NewDNSProviderClient(client acmeDNSClient, storage goacmedns.Storage) (*DNS // ErrCNAMERequired is returned by Present when the Domain indicated had no // existing ACME-DNS account in the Storage and additional setup is required. // The user must create a CNAME in the DNS zone for Domain that aliases FQDN -// to Target in order to complete setup for the ACME-DNS account that was -// created. +// to Target in order to complete setup for the ACME-DNS account that was created. type ErrCNAMERequired struct { // The Domain that is being issued for. Domain string @@ -100,18 +95,16 @@ func (e ErrCNAMERequired) Error() string { e.Domain, e.Domain, e.FQDN, e.Target) } -// Present creates a TXT record to fulfill the DNS-01 challenge. If there is an -// existing account for the domain in the provider's storage then it will be -// used to set the challenge response TXT record with the ACME-DNS server and -// issuance will continue. If there is not an account for the given domain -// present in the DNSProvider storage one will be created and registered with -// the ACME DNS server and an ErrCNAMERequired error is returned. This will halt -// issuance and indicate to the user that a one-time manual setup is required -// for the domain. +// Present creates a TXT record to fulfill the DNS-01 challenge. +// If there is an existing account for the domain in the provider's storage +// then it will be used to set the challenge response TXT record with the ACME-DNS server and issuance will continue. +// If there is not an account for the given domain present in the DNSProvider storage +// one will be created and registered with the ACME DNS server and an ErrCNAMERequired error is returned. +// This will halt issuance and indicate to the user that a one-time manual setup is required for the domain. func (d *DNSProvider) Present(domain, _, keyAuth string) error { // Compute the challenge response FQDN and TXT value for the domain based // on the keyAuth. - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) // Check if credentials were previously saved for this domain. account, err := d.storage.Fetch(domain) @@ -132,15 +125,15 @@ func (d *DNSProvider) Present(domain, _, keyAuth string) error { // CleanUp removes the record matching the specified parameters. It is not // implemented for the ACME-DNS provider. func (d *DNSProvider) CleanUp(_, _, _ string) error { - // ACME-DNS doesn't support the notion of removing a record. For users of - // ACME-DNS it is expected the stale records remain in-place. + // ACME-DNS doesn't support the notion of removing a record. + // For users of ACME-DNS it is expected the stale records remain in-place. return nil } -// register creates a new ACME-DNS account for the given domain. If account -// creation works as expected a ErrCNAMERequired error is returned describing -// the one-time manual CNAME setup required to complete setup of the ACME-DNS -// hook for the domain. If any other error occurs it is returned as-is. +// register creates a new ACME-DNS account for the given domain. +// If account creation works as expected a ErrCNAMERequired error is returned describing +// the one-time manual CNAME setup required to complete setup of the ACME-DNS hook for the domain. +// If any other error occurs it is returned as-is. func (d *DNSProvider) register(domain, fqdn string) error { // TODO(@cpu): Read CIDR whitelists from the environment newAcct, err := d.client.RegisterAccount(nil) @@ -158,9 +151,9 @@ func (d *DNSProvider) register(domain, fqdn string) error { return err } - // Stop issuance by returning an error. The user needs to perform a manual - // one-time CNAME setup in their DNS zone to complete the setup of the new - // account we created. + // Stop issuance by returning an error. + // The user needs to perform a manual one-time CNAME setup in their DNS zone + // to complete the setup of the new account we created. return ErrCNAMERequired{ Domain: domain, FQDN: fqdn, diff --git a/providers/dns/acmedns/acmedns_test.go b/providers/dns/acmedns/acmedns_test.go index 398dea95..07b554b0 100644 --- a/providers/dns/acmedns/acmedns_test.go +++ b/providers/dns/acmedns/acmedns_test.go @@ -14,19 +14,22 @@ var ( errorClientErr = errors.New("errorClient always errors") // errorStorageErr is used by the Storage mocks that return an error. errorStorageErr = errors.New("errorStorage always errors") +) +const ( // Fixed test data for unit tests. egDomain = "threeletter.agency" egFQDN = "_acme-challenge." + egDomain + "." egKeyAuth = "⚷" - egAccount = goacmedns.Account{ - FullDomain: "acme-dns." + egDomain, - SubDomain: "random-looking-junk." + egDomain, - Username: "spooky.mulder", - Password: "trustno1", - } ) +var egTestAccount = goacmedns.Account{ + FullDomain: "acme-dns." + egDomain, + SubDomain: "random-looking-junk." + egDomain, + Username: "spooky.mulder", + Password: "trustno1", +} + // mockClient is a mock implementing the acmeDNSClient interface that always // returns a fixed goacmedns.Account from calls to Register. type mockClient struct { @@ -115,7 +118,7 @@ func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error { return errorStorageErr } -// errorSaveStoragr is a mock implementing the goacmedns.Storage interface that +// errorSaveStorage is a mock implementing the goacmedns.Storage interface that // always returns errors from Save. type errorSaveStorage struct { mockStorage @@ -140,16 +143,16 @@ func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) { // TestPresent tests that the ACME-DNS Present function for updating a DNS-01 // challenge response TXT record works as expected. func TestPresent(t *testing.T) { - // validAccountStorage is a mockStorage configured to return the egAccount. + // validAccountStorage is a mockStorage configured to return the egTestAccount. validAccountStorage := mockStorage{ map[string]goacmedns.Account{ - egDomain: egAccount, + egDomain: egTestAccount, }, } - // validUpdateClient is a mockClient configured with the egAccount that will + // validUpdateClient is a mockClient configured with the egTestAccount that will // track TXT updates in a map. validUpdateClient := mockUpdateClient{ - mockClient{egAccount}, + mockClient{egTestAccount}, make(map[goacmedns.Account]string), } @@ -161,17 +164,17 @@ func TestPresent(t *testing.T) { }{ { Name: "present when client storage returns unexpected error", - Client: mockClient{egAccount}, + Client: mockClient{egTestAccount}, Storage: errorFetchStorage{}, ExpectedError: errorStorageErr, }, { Name: "present when client storage returns ErrDomainNotFound", - Client: mockClient{egAccount}, + Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, - Target: egAccount.FullDomain, + Target: egTestAccount.FullDomain, }, }, { @@ -187,32 +190,32 @@ func TestPresent(t *testing.T) { }, } - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - dp, err := NewDNSProviderClient(tc.Client, mockStorage{make(map[string]goacmedns.Account)}) + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) require.NoError(t, err) - // override the storage mock if required by the testcase. - if tc.Storage != nil { - dp.storage = tc.Storage + // override the storage mock if required by the test case. + if test.Storage != nil { + dp.storage = test.Storage } // call Present. The token argument can be garbage because the ACME-DNS // provider does not use it. err = dp.Present(egDomain, "foo", egKeyAuth) - if tc.ExpectedError != nil { - assert.Equal(t, tc.ExpectedError, err) + if test.ExpectedError != nil { + assert.Equal(t, test.ExpectedError, err) } else { require.NoError(t, err) } }) } - // Check that the success testcase set a record. + // Check that the success test case set a record. assert.Len(t, validUpdateClient.records, 1) - // Check that the success testcase set the right record for the right account. - assert.Len(t, validUpdateClient.records[egAccount], 43) + // Check that the success test case set the right record for the right account. + assert.Len(t, validUpdateClient.records[egTestAccount], 43) } // TestRegister tests that the ACME-DNS register function works correctly. @@ -232,41 +235,41 @@ func TestRegister(t *testing.T) { }, { Name: "register when acme-dns storage put returns an error", - Client: mockClient{egAccount}, + Client: mockClient{egTestAccount}, Storage: errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when acme-dns storage save returns an error", - Client: mockClient{egAccount}, + Client: mockClient{egTestAccount}, Storage: errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}}, ExpectedError: errorStorageErr, }, { Name: "register when everything works", - Client: mockClient{egAccount}, + Client: mockClient{egTestAccount}, ExpectedError: ErrCNAMERequired{ Domain: egDomain, FQDN: egFQDN, - Target: egAccount.FullDomain, + Target: egTestAccount.FullDomain, }, }, } - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - dp, err := NewDNSProviderClient(tc.Client, mockStorage{make(map[string]goacmedns.Account)}) + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)}) require.NoError(t, err) // override the storage mock if required by the testcase. - if tc.Storage != nil { - dp.storage = tc.Storage + if test.Storage != nil { + dp.storage = test.Storage } // Call register for the example domain/fqdn. err = dp.register(egDomain, egFQDN) - if tc.ExpectedError != nil { - assert.Equal(t, tc.ExpectedError, err) + if test.ExpectedError != nil { + assert.Equal(t, test.ExpectedError, err) } else { require.NoError(t, err) } diff --git a/providers/dns/alidns/alidns.go b/providers/dns/alidns/alidns.go index 41aeb905..3a8d76e6 100644 --- a/providers/dns/alidns/alidns.go +++ b/providers/dns/alidns/alidns.go @@ -1,5 +1,4 @@ -// Package alidns implements a DNS provider for solving the DNS-01 challenge -// using Alibaba Cloud DNS. +// Package alidns implements a DNS provider for solving the DNS-01 challenge using Alibaba Cloud DNS. package alidns import ( @@ -12,7 +11,7 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -33,8 +32,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("ALICLOUD_TTL", 600), - PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("ALICLOUD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("ALICLOUD_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPTimeout: env.GetOrDefaultSecond("ALICLOUD_HTTP_TIMEOUT", 10*time.Second), } } @@ -61,18 +60,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for alidns. -// Deprecated -func NewDNSProviderCredentials(apiKey, secretKey, regionID string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - config.SecretKey = secretKey - config.RegionID = regionID - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for alidns. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -106,7 +93,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) _, zoneName, err := d.getHostedZone(domain) if err != nil { @@ -124,7 +111,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.findTxtRecords(domain, fqdn) if err != nil { @@ -154,14 +141,14 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { return "", "", fmt.Errorf("API call failed: %v", err) } - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", "", err } var hostedZone alidns.Domain for _, zone := range zones.Domains.Domain { - if zone.DomainName == acme.UnFqdn(authZone) { + if zone.DomainName == dns01.UnFqdn(authZone) { hostedZone = zone } } @@ -209,7 +196,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]alidns.Record, erro } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/auroradns/auroradns.go b/providers/dns/auroradns/auroradns.go index 21e455c9..56dff6d0 100644 --- a/providers/dns/auroradns/auroradns.go +++ b/providers/dns/auroradns/auroradns.go @@ -8,7 +8,7 @@ import ( "time" "github.com/ldez/go-auroradns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -28,8 +28,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("AURORA_TTL", 300), - PropagationTimeout: env.GetOrDefaultSecond("AURORA_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("AURORA_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("AURORA_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("AURORA_POLLING_INTERVAL", dns01.DefaultPollingInterval), } } @@ -58,18 +58,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for AuroraDNS. -// Deprecated -func NewDNSProviderCredentials(baseURL string, userID string, key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = baseURL - config.UserID = userID - config.Key = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for AuroraDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -103,9 +91,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a record with a secret func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return fmt.Errorf("aurora: could not determine zone for domain: '%s'. %s", domain, err) } @@ -119,7 +107,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { subdomain := fqdn[0 : len(fqdn)-len(authZone)-1] - authZone = acme.UnFqdn(authZone) + authZone = dns01.UnFqdn(authZone) zone, err := d.getZoneInformationByName(authZone) if err != nil { @@ -147,7 +135,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes a given record that was generated by Present func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) d.recordIDsMu.Lock() recordID, ok := d.recordIDs[fqdn] @@ -157,12 +145,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("unknown recordID for %q", fqdn) } - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return fmt.Errorf("could not determine zone for domain: %q. %v", domain, err) } - authZone = acme.UnFqdn(authZone) + authZone = dns01.UnFqdn(authZone) zone, err := d.getZoneInformationByName(authZone) if err != nil { diff --git a/providers/dns/azure/azure.go b/providers/dns/azure/azure.go index 48ff4e38..5d95b7cb 100644 --- a/providers/dns/azure/azure.go +++ b/providers/dns/azure/azure.go @@ -1,5 +1,4 @@ -// Package azure implements a DNS provider for solving the DNS-01 -// challenge using azure DNS. +// Package azure implements a DNS provider for solving the DNS-01 challenge using azure DNS. // Azure doesn't like trailing dots on domain names, most of the acme code does. package azure @@ -18,7 +17,7 @@ import ( "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/Azure/go-autorest/autorest/to" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -72,20 +71,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for azure. -// Deprecated -func NewDNSProviderCredentials(clientID, clientSecret, subscriptionID, tenantID, resourceGroup string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.ClientID = clientID - config.ClientSecret = clientSecret - config.TenantID = tenantID - config.SubscriptionID = subscriptionID - config.ResourceGroup = resourceGroup - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Azure. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -128,8 +113,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config, authorizer: authorizer}, nil } -// Timeout returns the timeout and interval to use when checking for DNS -// propagation. Adjusting here to cope with spikes in propagation times. +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } @@ -137,7 +122,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { ctx := context.Background() - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { @@ -147,7 +132,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { rsc := dns.NewRecordSetsClient(d.config.SubscriptionID) rsc.Authorizer = d.authorizer - relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) + relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) // Get existing record set rset, err := rsc.Get(ctx, d.config.ResourceGroup, zone, relative, dns.TXT) @@ -192,14 +177,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { ctx := context.Background() - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneID(ctx, fqdn) if err != nil { return fmt.Errorf("azure: %v", err) } - relative := toRelativeRecord(fqdn, acme.ToFqdn(zone)) + relative := toRelativeRecord(fqdn, dns01.ToFqdn(zone)) rsc := dns.NewRecordSetsClient(d.config.SubscriptionID) rsc.Authorizer = d.authorizer @@ -212,7 +197,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { // Checks that azure has a zone for this domain name. func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, error) { - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } @@ -220,7 +205,7 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, dc := dns.NewZonesClient(d.config.SubscriptionID) dc.Authorizer = d.authorizer - zone, err := dc.Get(ctx, d.config.ResourceGroup, acme.UnFqdn(authZone)) + zone, err := dc.Get(ctx, d.config.ResourceGroup, dns01.UnFqdn(authZone)) if err != nil { return "", err } @@ -231,7 +216,7 @@ func (d *DNSProvider) getHostedZoneID(ctx context.Context, fqdn string) (string, // Returns the relative record to the domain func toRelativeRecord(domain, zone string) string { - return acme.UnFqdn(strings.TrimSuffix(domain, zone)) + return dns01.UnFqdn(strings.TrimSuffix(domain, zone)) } func getAuthorizer(config *Config) (autorest.Authorizer, error) { @@ -285,5 +270,5 @@ func getMetadata(config *Config, field string) (string, error) { return "", err } - return string(respBody[:]), nil + return string(respBody), nil } diff --git a/providers/dns/bluecat/bluecat.go b/providers/dns/bluecat/bluecat.go index fabfdae3..3cee02fb 100644 --- a/providers/dns/bluecat/bluecat.go +++ b/providers/dns/bluecat/bluecat.go @@ -1,27 +1,25 @@ -// Package bluecat implements a DNS provider for solving the DNS-01 challenge -// using a self-hosted Bluecat Address Manager. +// Package bluecat implements a DNS provider for solving the DNS-01 challenge using a self-hosted Bluecat Address Manager. package bluecat import ( - "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" - "regexp" "strconv" - "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) -const configType = "Configuration" -const viewType = "View" -const txtType = "TXTRecord" -const zoneType = "Zone" +const ( + configType = "Configuration" + viewType = "View" + zoneType = "Zone" + txtType = "TXTRecord" +) // Config is used to configure the creation of the DNSProvider type Config struct { @@ -39,9 +37,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("BLUECAT_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("BLUECAT_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("BLUECAT_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("BLUECAT_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("BLUECAT_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("BLUECAT_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("BLUECAT_HTTP_TIMEOUT", 30*time.Second), }, @@ -58,8 +56,8 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS. // Credentials must be passed in the environment variables: BLUECAT_SERVER_URL, BLUECAT_USER_NAME and BLUECAT_PASSWORD. // BLUECAT_SERVER_URL should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server. -// The REST endpoint will be appended. In addition, the Configuration name -// and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and BLUECAT_DNS_VIEW +// The REST endpoint will be appended. +// In addition, the Configuration name and external DNS View Name must be passed in BLUECAT_CONFIG_NAME and BLUECAT_DNS_VIEW func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get("BLUECAT_SERVER_URL", "BLUECAT_USER_NAME", "BLUECAT_PASSWORD", "BLUECAT_CONFIG_NAME", "BLUECAT_DNS_VIEW") if err != nil { @@ -76,24 +74,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Bluecat DNS. -// Deprecated -func NewDNSProviderCredentials(baseURL, userName, password, configName, dnsView string, httpClient *http.Client) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = baseURL - config.UserName = userName - config.Password = password - config.ConfigName = configName - config.DNSView = dnsView - - if httpClient != nil { - config.HTTPClient = httpClient - } - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -111,7 +91,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // This will *not* create a subzone to contain the TXT record, // so make sure the FQDN specified is within an extant zone. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.login() if err != nil { @@ -162,7 +142,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) err := d.login() if err != nil { @@ -219,223 +199,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// Send a REST request, using query parameters specified. The Authorization -// header will be set if we have an active auth token -func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) { - url := fmt.Sprintf("%s/Services/REST/v1/%s", d.config.BaseURL, resource) - - body, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("bluecat: %v", err) - } - - req, err := http.NewRequest(method, url, bytes.NewReader(body)) - if err != nil { - return nil, fmt.Errorf("bluecat: %v", err) - } - req.Header.Set("Content-Type", "application/json") - if len(d.token) > 0 { - req.Header.Set("Authorization", d.token) - } - - // Add all query parameters - q := req.URL.Query() - for argName, argVal := range queryArgs { - q.Add(argName, argVal) - } - req.URL.RawQuery = q.Encode() - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("bluecat: %v", err) - } - - if resp.StatusCode >= 400 { - errBytes, _ := ioutil.ReadAll(resp.Body) - errResp := string(errBytes) - return nil, fmt.Errorf("bluecat: request failed with HTTP status code %d\n Full message: %s", - resp.StatusCode, errResp) - } - - return resp, nil -} - -// Starts a new Bluecat API Session. Authenticates using customerName, userName, -// password and receives a token to be used in for subsequent requests. -func (d *DNSProvider) login() error { - queryArgs := map[string]string{ - "username": d.config.UserName, - "password": d.config.Password, - } - - resp, err := d.sendRequest(http.MethodGet, "login", nil, queryArgs) - if err != nil { - return err - } - defer resp.Body.Close() - - authBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("bluecat: %v", err) - } - authResp := string(authBytes) - - if strings.Contains(authResp, "Authentication Error") { - msg := strings.Trim(authResp, "\"") - return fmt.Errorf("bluecat: request failed: %s", msg) - } - // Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username" - d.token = regexp.MustCompile("BAMAuthToken: [^ ]+").FindString(authResp) - return nil -} - -// Destroys Bluecat Session -func (d *DNSProvider) logout() error { - if len(d.token) == 0 { - // nothing to do - return nil - } - - resp, err := d.sendRequest(http.MethodGet, "logout", nil, nil) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return fmt.Errorf("bluecat: request failed to delete session with HTTP status code %d", resp.StatusCode) - } - - authBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - authResp := string(authBytes) - - if !strings.Contains(authResp, "successfully") { - msg := strings.Trim(authResp, "\"") - return fmt.Errorf("bluecat: request failed to delete session: %s", msg) - } - - d.token = "" - - return nil -} - -// Lookup the entity ID of the configuration named in our properties -func (d *DNSProvider) lookupConfID() (uint, error) { - queryArgs := map[string]string{ - "parentId": strconv.Itoa(0), - "name": d.config.ConfigName, - "type": configType, - } - - resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - var conf entityResponse - err = json.NewDecoder(resp.Body).Decode(&conf) - if err != nil { - return 0, fmt.Errorf("bluecat: %v", err) - } - return conf.ID, nil -} - -// Find the DNS view with the given name within -func (d *DNSProvider) lookupViewID(viewName string) (uint, error) { - confID, err := d.lookupConfID() - if err != nil { - return 0, err - } - - queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(confID), 10), - "name": viewName, - "type": viewType, - } - - resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - var view entityResponse - err = json.NewDecoder(resp.Body).Decode(&view) - if err != nil { - return 0, fmt.Errorf("bluecat: %v", err) - } - - return view.ID, nil -} - -// Return the entityId of the parent zone by recursing from the root view -// Also return the simple name of the host -func (d *DNSProvider) lookupParentZoneID(viewID uint, fqdn string) (uint, string, error) { - parentViewID := viewID - name := "" - - if fqdn != "" { - zones := strings.Split(strings.Trim(fqdn, "."), ".") - last := len(zones) - 1 - name = zones[0] - - for i := last; i > -1; i-- { - zoneID, err := d.getZone(parentViewID, zones[i]) - if err != nil || zoneID == 0 { - return parentViewID, name, err - } - if i > 0 { - name = strings.Join(zones[0:i], ".") - } - parentViewID = zoneID - } - } - - return parentViewID, name, nil -} - -// Get the DNS zone with the specified name under the parentId -func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) { - queryArgs := map[string]string{ - "parentId": strconv.FormatUint(uint64(parentID), 10), - "name": name, - "type": zoneType, - } - - resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) - // Return an empty zone if the named zone doesn't exist - if resp != nil && resp.StatusCode == 404 { - return 0, fmt.Errorf("bluecat: could not find zone named %s", name) - } - if err != nil { - return 0, err - } - defer resp.Body.Close() - - var zone entityResponse - err = json.NewDecoder(resp.Body).Decode(&zone) - if err != nil { - return 0, fmt.Errorf("bluecat: %v", err) - } - - return zone.ID, nil -} - -// Deploy the DNS config for the specified entity to the authoritative servers -func (d *DNSProvider) deploy(entityID uint) error { - queryArgs := map[string]string{ - "entityId": strconv.FormatUint(uint64(entityID), 10), - } - - resp, err := d.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs) - if err != nil { - return err - } - defer resp.Body.Close() - - return nil -} diff --git a/providers/dns/bluecat/client.go b/providers/dns/bluecat/client.go index 55deeed4..d910594c 100644 --- a/providers/dns/bluecat/client.go +++ b/providers/dns/bluecat/client.go @@ -1,5 +1,16 @@ package bluecat +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strconv" + "strings" +) + // JSON body for Bluecat entity requests and responses type bluecatEntity struct { ID string `json:"id,omitempty"` @@ -14,3 +25,225 @@ type entityResponse struct { Type string `json:"type"` Properties string `json:"properties"` } + +// Starts a new Bluecat API Session. Authenticates using customerName, userName, +// password and receives a token to be used in for subsequent requests. +func (d *DNSProvider) login() error { + queryArgs := map[string]string{ + "username": d.config.UserName, + "password": d.config.Password, + } + + resp, err := d.sendRequest(http.MethodGet, "login", nil, queryArgs) + if err != nil { + return err + } + defer resp.Body.Close() + + authBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("bluecat: %v", err) + } + authResp := string(authBytes) + + if strings.Contains(authResp, "Authentication Error") { + msg := strings.Trim(authResp, "\"") + return fmt.Errorf("bluecat: request failed: %s", msg) + } + + // Upon success, API responds with "Session Token-> BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM= <- for User : username" + d.token = regexp.MustCompile("BAMAuthToken: [^ ]+").FindString(authResp) + return nil +} + +// Destroys Bluecat Session +func (d *DNSProvider) logout() error { + if len(d.token) == 0 { + // nothing to do + return nil + } + + resp, err := d.sendRequest(http.MethodGet, "logout", nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("bluecat: request failed to delete session with HTTP status code %d", resp.StatusCode) + } + + authBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + authResp := string(authBytes) + + if !strings.Contains(authResp, "successfully") { + msg := strings.Trim(authResp, "\"") + return fmt.Errorf("bluecat: request failed to delete session: %s", msg) + } + + d.token = "" + + return nil +} + +// Lookup the entity ID of the configuration named in our properties +func (d *DNSProvider) lookupConfID() (uint, error) { + queryArgs := map[string]string{ + "parentId": strconv.Itoa(0), + "name": d.config.ConfigName, + "type": configType, + } + + resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var conf entityResponse + err = json.NewDecoder(resp.Body).Decode(&conf) + if err != nil { + return 0, fmt.Errorf("bluecat: %v", err) + } + return conf.ID, nil +} + +// Find the DNS view with the given name within +func (d *DNSProvider) lookupViewID(viewName string) (uint, error) { + confID, err := d.lookupConfID() + if err != nil { + return 0, err + } + + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(confID), 10), + "name": viewName, + "type": viewType, + } + + resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var view entityResponse + err = json.NewDecoder(resp.Body).Decode(&view) + if err != nil { + return 0, fmt.Errorf("bluecat: %v", err) + } + + return view.ID, nil +} + +// Return the entityId of the parent zone by recursing from the root view +// Also return the simple name of the host +func (d *DNSProvider) lookupParentZoneID(viewID uint, fqdn string) (uint, string, error) { + parentViewID := viewID + name := "" + + if fqdn != "" { + zones := strings.Split(strings.Trim(fqdn, "."), ".") + last := len(zones) - 1 + name = zones[0] + + for i := last; i > -1; i-- { + zoneID, err := d.getZone(parentViewID, zones[i]) + if err != nil || zoneID == 0 { + return parentViewID, name, err + } + if i > 0 { + name = strings.Join(zones[0:i], ".") + } + parentViewID = zoneID + } + } + + return parentViewID, name, nil +} + +// Get the DNS zone with the specified name under the parentId +func (d *DNSProvider) getZone(parentID uint, name string) (uint, error) { + queryArgs := map[string]string{ + "parentId": strconv.FormatUint(uint64(parentID), 10), + "name": name, + "type": zoneType, + } + + resp, err := d.sendRequest(http.MethodGet, "getEntityByName", nil, queryArgs) + + // Return an empty zone if the named zone doesn't exist + if resp != nil && resp.StatusCode == http.StatusNotFound { + return 0, fmt.Errorf("bluecat: could not find zone named %s", name) + } + if err != nil { + return 0, err + } + defer resp.Body.Close() + + var zone entityResponse + err = json.NewDecoder(resp.Body).Decode(&zone) + if err != nil { + return 0, fmt.Errorf("bluecat: %v", err) + } + + return zone.ID, nil +} + +// Deploy the DNS config for the specified entity to the authoritative servers +func (d *DNSProvider) deploy(entityID uint) error { + queryArgs := map[string]string{ + "entityId": strconv.FormatUint(uint64(entityID), 10), + } + + resp, err := d.sendRequest(http.MethodPost, "quickDeploy", nil, queryArgs) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// Send a REST request, using query parameters specified. The Authorization +// header will be set if we have an active auth token +func (d *DNSProvider) sendRequest(method, resource string, payload interface{}, queryArgs map[string]string) (*http.Response, error) { + url := fmt.Sprintf("%s/Services/REST/v1/%s", d.config.BaseURL, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("bluecat: %v", err) + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("bluecat: %v", err) + } + req.Header.Set("Content-Type", "application/json") + if len(d.token) > 0 { + req.Header.Set("Authorization", d.token) + } + + // Add all query parameters + q := req.URL.Query() + for argName, argVal := range queryArgs { + q.Add(argName, argVal) + } + req.URL.RawQuery = q.Encode() + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("bluecat: %v", err) + } + + if resp.StatusCode >= 400 { + errBytes, _ := ioutil.ReadAll(resp.Body) + errResp := string(errBytes) + return nil, fmt.Errorf("bluecat: request failed with HTTP status code %d\n Full message: %s", + resp.StatusCode, errResp) + } + + return resp, nil +} diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go index 92c4b69f..c530326a 100644 --- a/providers/dns/cloudflare/cloudflare.go +++ b/providers/dns/cloudflare/cloudflare.go @@ -1,5 +1,4 @@ -// Package cloudflare implements a DNS provider for solving the DNS-01 -// challenge using cloudflare DNS. +// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS. package cloudflare import ( @@ -9,14 +8,11 @@ import ( "time" "github.com/cloudflare/cloudflare-go" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) -// CloudFlareAPIURL represents the API endpoint to call. -const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4" // Deprecated - const ( minTTL = 120 ) @@ -67,17 +63,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Cloudflare. -// Deprecated -func NewDNSProviderCredentials(email, key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AuthEmail = email - config.AuthKey = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -93,9 +78,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, err } - // TODO: must be remove. keep only for compatibility reason. - client.BaseURL = CloudFlareAPIURL - return &DNSProvider{client: client, config: config}, nil } @@ -107,21 +89,21 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("cloudflare: %v", err) } - zoneID, err := d.client.ZoneIDByName(acme.UnFqdn(authZone)) + zoneID, err := d.client.ZoneIDByName(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err) } dnsRecord := cloudflare.DNSRecord{ Type: "TXT", - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), Content: value, TTL: d.config.TTL, } @@ -142,21 +124,21 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("cloudflare: %v", err) } - zoneID, err := d.client.ZoneIDByName(acme.UnFqdn(authZone)) + zoneID, err := d.client.ZoneIDByName(dns01.UnFqdn(authZone)) if err != nil { return fmt.Errorf("cloudflare: failed to find zone %s: %v", authZone, err) } dnsRecord := cloudflare.DNSRecord{ Type: "TXT", - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), } records, err := d.client.DNSRecords(zoneID, dnsRecord) diff --git a/providers/dns/cloudxns/cloudxns.go b/providers/dns/cloudxns/cloudxns.go index 6f4bb6bd..0a3a7d89 100644 --- a/providers/dns/cloudxns/cloudxns.go +++ b/providers/dns/cloudxns/cloudxns.go @@ -1,5 +1,4 @@ -// Package cloudxns implements a DNS provider for solving the DNS-01 challenge -// using CloudXNS DNS. +// Package cloudxns implements a DNS provider for solving the DNS-01 challenge using CloudXNS DNS. package cloudxns import ( @@ -8,8 +7,9 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/providers/dns/cloudxns/internal" ) // Config is used to configure the creation of the DNSProvider @@ -24,21 +24,20 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { - client := acme.HTTPClient - client.Timeout = time.Second * time.Duration(env.GetOrDefaultInt("CLOUDXNS_HTTP_TIMEOUT", 30)) - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond("CLOUDXNS_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("CLOUDXNS_POLLING_INTERVAL", acme.DefaultPollingInterval), - TTL: env.GetOrDefaultInt("CLOUDXNS_TTL", 120), - HTTPClient: &client, + PropagationTimeout: env.GetOrDefaultSecond("CLOUDXNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("CLOUDXNS_POLLING_INTERVAL", dns01.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("CLOUDXNS_TTL", dns01.DefaultTTL), + HTTPClient: &http.Client{ + Timeout: time.Second * time.Duration(env.GetOrDefaultInt("CLOUDXNS_HTTP_TIMEOUT", 30)), + }, } } // DNSProvider is an implementation of the acme.ChallengeProvider interface type DNSProvider struct { config *Config - client *Client + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for CloudXNS. @@ -50,15 +49,9 @@ func NewDNSProvider() (*DNSProvider, error) { return nil, fmt.Errorf("CloudXNS: %v", err) } - return NewDNSProviderCredentials(values["CLOUDXNS_API_KEY"], values["CLOUDXNS_SECRET_KEY"]) -} - -// NewDNSProviderCredentials uses the supplied credentials to return a -// DNSProvider instance configured for CloudXNS. -func NewDNSProviderCredentials(apiKey, secretKey string) (*DNSProvider, error) { config := NewDefaultConfig() - config.APIKey = apiKey - config.SecretKey = secretKey + config.APIKey = values["CLOUDXNS_API_KEY"] + config.SecretKey = values["CLOUDXNS_SECRET_KEY"] return NewDNSProviderConfig(config) } @@ -69,7 +62,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("CloudXNS: the configuration of the DNS provider is nil") } - client, err := NewClient(config.APIKey, config.SecretKey) + client, err := internal.NewClient(config.APIKey, config.SecretKey) if err != nil { return nil, err } @@ -81,7 +74,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) info, err := d.client.GetDomainInformation(fqdn) if err != nil { @@ -93,7 +86,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) info, err := d.client.GetDomainInformation(fqdn) if err != nil { diff --git a/providers/dns/cloudxns/client.go b/providers/dns/cloudxns/internal/client.go similarity index 95% rename from providers/dns/cloudxns/client.go rename to providers/dns/cloudxns/internal/client.go index 4cf1dd20..fd3319a0 100644 --- a/providers/dns/cloudxns/client.go +++ b/providers/dns/cloudxns/internal/client.go @@ -1,4 +1,4 @@ -package cloudxns +package internal import ( "bytes" @@ -12,7 +12,7 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" ) const defaultBaseURL = "https://www.cloudxns.net/api2/" @@ -70,7 +70,7 @@ type Client struct { // GetDomainInformation Get domain name information for a FQDN func (c *Client) GetDomainInformation(fqdn string) (*Data, error) { - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func (c *Client) FindTxtRecord(zoneID, fqdn string) (*TXTRecord, error) { } for _, record := range records { - if record.Host == acme.UnFqdn(fqdn) && record.Type == "TXT" { + if record.Host == dns01.UnFqdn(fqdn) && record.Type == "TXT" { return &record, nil } } @@ -128,7 +128,7 @@ func (c *Client) AddTxtRecord(info *Data, fqdn, value string, ttl int) error { payload := TXTRecord{ ID: id, - Host: acme.UnFqdn(strings.TrimSuffix(fqdn, info.Domain)), + Host: dns01.UnFqdn(strings.TrimSuffix(fqdn, info.Domain)), Value: value, Type: "TXT", LineID: 1, diff --git a/providers/dns/cloudxns/client_test.go b/providers/dns/cloudxns/internal/client_test.go similarity index 99% rename from providers/dns/cloudxns/client_test.go rename to providers/dns/cloudxns/internal/client_test.go index da533fa4..c9d62096 100644 --- a/providers/dns/cloudxns/client_test.go +++ b/providers/dns/cloudxns/internal/client_test.go @@ -1,4 +1,4 @@ -package cloudxns +package internal import ( "encoding/json" diff --git a/providers/dns/conoha/conoha.go b/providers/dns/conoha/conoha.go index a872789f..d2f93798 100644 --- a/providers/dns/conoha/conoha.go +++ b/providers/dns/conoha/conoha.go @@ -1,5 +1,4 @@ -// Package conoha implements a DNS provider for solving the DNS-01 challenge -// using ConoHa DNS. +// Package conoha implements a DNS provider for solving the DNS-01 challenge using ConoHa DNS. package conoha import ( @@ -8,8 +7,9 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/providers/dns/conoha/internal" ) // Config is used to configure the creation of the DNSProvider @@ -29,8 +29,8 @@ func NewDefaultConfig() *Config { return &Config{ Region: env.GetOrDefaultString("CONOHA_REGION", "tyo1"), TTL: env.GetOrDefaultInt("CONOHA_TTL", 60), - PropagationTimeout: env.GetOrDefaultSecond("CONOHA_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("CONOHA_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("CONOHA_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("CONOHA_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("CONOHA_HTTP_TIMEOUT", 30*time.Second), }, @@ -40,7 +40,7 @@ func NewDefaultConfig() *Config { // DNSProvider is an implementation of the acme.ChallengeProvider interface type DNSProvider struct { config *Config - client *Client + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for ConoHa DNS. @@ -69,15 +69,15 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, errors.New("conoha: some credentials information are missing") } - auth := Auth{ + auth := internal.Auth{ TenantID: config.TenantID, - PasswordCredentials: PasswordCredentials{ + PasswordCredentials: internal.PasswordCredentials{ Username: config.Username, Password: config.Password, }, } - client, err := NewClient(config.Region, auth, config.HTTPClient) + client, err := internal.NewClient(config.Region, auth, config.HTTPClient) if err != nil { return nil, fmt.Errorf("conoha: failed to create client: %v", err) } @@ -87,9 +87,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } @@ -99,7 +99,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("conoha: failed to get domain ID: %v", err) } - record := Record{ + record := internal.Record{ Name: fqdn, Type: "TXT", Data: value, @@ -116,9 +116,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp clears ConoHa DNS TXT record func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } diff --git a/providers/dns/conoha/client.go b/providers/dns/conoha/internal/client.go similarity index 99% rename from providers/dns/conoha/client.go rename to providers/dns/conoha/internal/client.go index 3fa8b5bb..3136a24d 100644 --- a/providers/dns/conoha/client.go +++ b/providers/dns/conoha/internal/client.go @@ -1,4 +1,4 @@ -package conoha +package internal import ( "bytes" diff --git a/providers/dns/conoha/client_test.go b/providers/dns/conoha/internal/client_test.go similarity index 99% rename from providers/dns/conoha/client_test.go rename to providers/dns/conoha/internal/client_test.go index 2db899ae..75f2bdb4 100644 --- a/providers/dns/conoha/client_test.go +++ b/providers/dns/conoha/internal/client_test.go @@ -1,4 +1,4 @@ -package conoha +package internal import ( "fmt" diff --git a/providers/dns/digitalocean/client.go b/providers/dns/digitalocean/client.go index d2f6c95f..ce0be050 100644 --- a/providers/dns/digitalocean/client.go +++ b/providers/dns/digitalocean/client.go @@ -1,26 +1,132 @@ package digitalocean +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/xenolf/lego/challenge/dns01" +) + const defaultBaseURL = "https://api.digitalocean.com" -// txtRecordRequest represents the request body to DO's API to make a TXT record -type txtRecordRequest struct { - RecordType string `json:"type"` - Name string `json:"name"` - Data string `json:"data"` - TTL int `json:"ttl"` -} - // txtRecordResponse represents a response from DO's API after making a TXT record type txtRecordResponse struct { - DomainRecord struct { - ID int `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Data string `json:"data"` - } `json:"domain_record"` + DomainRecord record `json:"domain_record"` } -type digitalOceanAPIError struct { +type record struct { + ID int `json:"id,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Data string `json:"data,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +type apiError struct { ID string `json:"id"` Message string `json:"message"` } + +func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error { + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + if err != nil { + return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) + } + + reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", d.config.BaseURL, dns01.UnFqdn(authZone), recordID) + req, err := d.newRequest(http.MethodDelete, reqURL, nil) + if err != nil { + return err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return readError(req, resp) + } + + return nil +} + +func (d *DNSProvider) addTxtRecord(domain, fqdn, value string) (*txtRecordResponse, error) { + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) + if err != nil { + return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) + } + + reqData := record{Type: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL} + body, err := json.Marshal(reqData) + if err != nil { + return nil, err + } + + reqURL := fmt.Sprintf("%s/v2/domains/%s/records", d.config.BaseURL, dns01.UnFqdn(authZone)) + req, err := d.newRequest(http.MethodPost, reqURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, readError(req, resp) + } + + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.New(toUnreadableBodyMessage(req, content)) + } + + // Everything looks good; but we'll need the ID later to delete the record + respData := &txtRecordResponse{} + err = json.Unmarshal(content, respData) + if err != nil { + return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content)) + } + + return respData, nil +} + +func (d *DNSProvider) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequest(method, reqURL, body) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.AuthToken)) + + return req, nil +} + +func readError(req *http.Request, resp *http.Response) error { + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.New(toUnreadableBodyMessage(req, content)) + } + + var errInfo apiError + err = json.Unmarshal(content, &errInfo) + if err != nil { + return fmt.Errorf("apiError unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content)) + } + + return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message) +} + +func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { + return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) +} diff --git a/providers/dns/digitalocean/digitalocean.go b/providers/dns/digitalocean/digitalocean.go index 97620dbf..0f9f3e77 100644 --- a/providers/dns/digitalocean/digitalocean.go +++ b/providers/dns/digitalocean/digitalocean.go @@ -1,19 +1,14 @@ -// Package digitalocean implements a DNS provider for solving the DNS-01 -// challenge using digitalocean DNS. +// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS. package digitalocean import ( - "bytes" - "encoding/json" "errors" "fmt" - "io" - "io/ioutil" "net/http" "sync" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -63,16 +58,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Digital Ocean. -// Deprecated -func NewDNSProviderCredentials(apiAuthToken string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AuthToken = apiAuthToken - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Digital Ocean. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -101,7 +86,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) respData, err := d.addTxtRecord(domain, fqdn, value) if err != nil { @@ -117,7 +102,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() @@ -139,102 +124,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - -func (d *DNSProvider) removeTxtRecord(domain string, recordID int) error { - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) - if err != nil { - return fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) - } - - reqURL := fmt.Sprintf("%s/v2/domains/%s/records/%d", d.config.BaseURL, acme.UnFqdn(authZone), recordID) - req, err := d.newRequest(http.MethodDelete, reqURL, nil) - if err != nil { - return err - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return readError(req, resp) - } - - return nil -} - -func (d *DNSProvider) addTxtRecord(domain, fqdn, value string) (*txtRecordResponse, error) { - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) - if err != nil { - return nil, fmt.Errorf("could not determine zone for domain: '%s'. %s", domain, err) - } - - reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value, TTL: d.config.TTL} - body, err := json.Marshal(reqData) - if err != nil { - return nil, err - } - - reqURL := fmt.Sprintf("%s/v2/domains/%s/records", d.config.BaseURL, acme.UnFqdn(authZone)) - req, err := d.newRequest(http.MethodPost, reqURL, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return nil, readError(req, resp) - } - - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.New(toUnreadableBodyMessage(req, content)) - } - - // Everything looks good; but we'll need the ID later to delete the record - respData := &txtRecordResponse{} - err = json.Unmarshal(content, respData) - if err != nil { - return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content)) - } - - return respData, nil -} - -func (d *DNSProvider) newRequest(method, reqURL string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, reqURL, body) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.config.AuthToken)) - - return req, nil -} - -func readError(req *http.Request, resp *http.Response) error { - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return errors.New(toUnreadableBodyMessage(req, content)) - } - - var errInfo digitalOceanAPIError - err = json.Unmarshal(content, &errInfo) - if err != nil { - return fmt.Errorf("digitalOceanAPIError unmarshaling error: %v: %s", err, toUnreadableBodyMessage(req, content)) - } - - return fmt.Errorf("HTTP %d: %s: %s", resp.StatusCode, errInfo.ID, errInfo.Message) -} - -func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { - return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) -} diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 604137d5..3b661f65 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -3,7 +3,8 @@ package dns import ( "fmt" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/providers/dns/acmedns" "github.com/xenolf/lego/providers/dns/alidns" "github.com/xenolf/lego/providers/dns/auroradns" @@ -56,7 +57,7 @@ import ( ) // NewDNSChallengeProviderByName Factory for DNS providers -func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) { +func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { switch name { case "acme-dns": return acmedns.NewDNSProvider() @@ -119,7 +120,7 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error) case "linodev4": return linodev4.NewDNSProvider() case "manual": - return acme.NewDNSProviderManual() + return dns01.NewDNSProviderManual() case "mydnsjp": return mydnsjp.NewDNSProvider() case "namecheap": diff --git a/providers/dns/dnsimple/dnsimple.go b/providers/dns/dnsimple/dnsimple.go index 8f52cac6..3709b84f 100644 --- a/providers/dns/dnsimple/dnsimple.go +++ b/providers/dns/dnsimple/dnsimple.go @@ -1,8 +1,8 @@ -// Package dnsimple implements a DNS provider for solving the DNS-01 challenge -// using dnsimple DNS. +// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS. package dnsimple import ( + "context" "errors" "fmt" "strconv" @@ -10,8 +10,9 @@ import ( "time" "github.com/dnsimple/dnsimple-go/dnsimple" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "golang.org/x/oauth2" ) // Config is used to configure the creation of the DNSProvider @@ -26,9 +27,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("DNSIMPLE_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("DNSIMPLE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("DNSIMPLE_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("DNSIMPLE_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("DNSIMPLE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DNSIMPLE_POLLING_INTERVAL", dns01.DefaultPollingInterval), } } @@ -50,17 +51,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for DNSimple. -// Deprecated -func NewDNSProviderCredentials(accessToken, baseURL string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.AccessToken = accessToken - config.BaseURL = baseURL - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for DNSimple. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -71,8 +61,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("dnsimple: OAuth token is missing") } - client := dnsimple.NewClient(dnsimple.NewOauthTokenCredentials(config.AccessToken)) - client.UserAgent = acme.UserAgent + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: config.AccessToken}) + client := dnsimple.NewClient(oauth2.NewClient(context.Background(), ts)) if config.BaseURL != "" { client.BaseURL = config.BaseURL @@ -83,7 +73,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, err := d.getHostedZone(domain) if err != nil { @@ -106,7 +96,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.findTxtRecords(domain, fqdn) if err != nil { @@ -136,7 +126,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func (d *DNSProvider) getHostedZone(domain string) (string, error) { - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", err } @@ -146,7 +136,7 @@ func (d *DNSProvider) getHostedZone(domain string) (string, error) { return "", err } - zoneName := acme.UnFqdn(authZone) + zoneName := dns01.UnFqdn(authZone) zones, err := d.client.Zones.ListZones(accountID, &dnsimple.ZoneListOptions{NameLike: zoneName}) if err != nil { @@ -200,7 +190,7 @@ func newTxtRecord(zoneName, fqdn, value string, ttl int) dnsimple.ZoneRecord { } func extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/dnsimple/dnsimple_test.go b/providers/dns/dnsimple/dnsimple_test.go index 08d502f5..d21a8dff 100644 --- a/providers/dns/dnsimple/dnsimple_test.go +++ b/providers/dns/dnsimple/dnsimple_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xenolf/lego/acme" "github.com/xenolf/lego/platform/tester" ) @@ -21,14 +20,12 @@ var envTest = tester.NewEnvTest( func TestNewDNSProvider(t *testing.T) { testCases := []struct { - desc string - userAgent string - envVars map[string]string - expected string + desc string + envVars map[string]string + expected string }{ { - desc: "success", - userAgent: "lego", + desc: "success", envVars: map[string]string{ "DNSIMPLE_OAUTH_TOKEN": "my_token", }, @@ -56,10 +53,6 @@ func TestNewDNSProvider(t *testing.T) { envTest.Apply(test.envVars) - if test.userAgent != "" { - acme.UserAgent = test.userAgent - } - p, err := NewDNSProvider() if len(test.expected) == 0 { @@ -72,11 +65,6 @@ func TestNewDNSProvider(t *testing.T) { if baseURL != "" { assert.Equal(t, baseURL, p.client.BaseURL) } - - if test.userAgent != "" { - assert.Equal(t, "lego", p.client.UserAgent) - } - } else { require.EqualError(t, err, test.expected) } diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy.go b/providers/dns/dnsmadeeasy/dnsmadeeasy.go index a55ea397..712c3e75 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy.go @@ -9,8 +9,9 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/providers/dns/dnsmadeeasy/internal" ) // Config is used to configure the creation of the DNSProvider @@ -28,9 +29,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("DNSMADEEASY_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("DNSMADEEASY_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("DNSMADEEASY_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("DNSMADEEASY_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("DNSMADEEASY_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DNSMADEEASY_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("DNSMADEEASY_HTTP_TIMEOUT", 10*time.Second), Transport: &http.Transport{ @@ -44,7 +45,7 @@ func NewDefaultConfig() *Config { // DNSMadeEasy's DNS API to manage TXT records for a domain. type DNSProvider struct { config *Config - client *Client + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for DNSMadeEasy DNS. @@ -64,18 +65,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for DNS Made Easy. -// Deprecated -func NewDNSProviderCredentials(baseURL, apiKey, apiSecret string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = baseURL - config.APIKey = apiKey - config.APISecret = apiSecret - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for DNS Made Easy. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -93,7 +82,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } } - client, err := NewClient(config.APIKey, config.APISecret) + client, err := internal.NewClient(config.APIKey, config.APISecret) if err != nil { return nil, fmt.Errorf("dnsmadeeasy: %v", err) } @@ -109,9 +98,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domainName, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domainName, keyAuth) + fqdn, value := dns01.GetRecord(domainName, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err) } @@ -124,7 +113,7 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { // create the TXT record name := strings.Replace(fqdn, "."+authZone, "", 1) - record := &Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL} + record := &internal.Record{Type: "TXT", Name: name, Value: value, TTL: d.config.TTL} err = d.client.CreateRecord(domain, record) if err != nil { @@ -135,9 +124,9 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { // CleanUp removes the TXT records matching the specified parameters func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domainName, keyAuth) + fqdn, _ := dns01.GetRecord(domainName, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dnsmadeeasy: unable to find zone for %s: %v", fqdn, err) } diff --git a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go index 0bc0dbe3..51d11ce7 100644 --- a/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go +++ b/providers/dns/dnsmadeeasy/dnsmadeeasy_test.go @@ -13,11 +13,9 @@ var envTest = tester.NewEnvTest( "DNSMADEEASY_API_SECRET"). WithDomain("DNSMADEEASY_DOMAIN") -func init() { - os.Setenv("DNSMADEEASY_SANDBOX", "true") -} - func TestNewDNSProvider(t *testing.T) { + os.Setenv("DNSMADEEASY_SANDBOX", "true") + testCases := []struct { desc string envVars map[string]string @@ -78,6 +76,8 @@ func TestNewDNSProvider(t *testing.T) { } func TestNewDNSProviderConfig(t *testing.T) { + os.Setenv("DNSMADEEASY_SANDBOX", "true") + testCases := []struct { desc string apiKey string @@ -130,6 +130,8 @@ func TestLivePresentAndCleanup(t *testing.T) { t.Skip("skipping live test") } + os.Setenv("DNSMADEEASY_SANDBOX", "true") + envTest.RestoreEnv() provider, err := NewDNSProvider() require.NoError(t, err) diff --git a/providers/dns/dnsmadeeasy/client.go b/providers/dns/dnsmadeeasy/internal/client.go similarity index 99% rename from providers/dns/dnsmadeeasy/client.go rename to providers/dns/dnsmadeeasy/internal/client.go index 95f2dda0..748d385d 100644 --- a/providers/dns/dnsmadeeasy/client.go +++ b/providers/dns/dnsmadeeasy/internal/client.go @@ -1,4 +1,4 @@ -package dnsmadeeasy +package internal import ( "bytes" diff --git a/providers/dns/dnspod/dnspod.go b/providers/dns/dnspod/dnspod.go index 1aa8d9e9..fb8afcbd 100644 --- a/providers/dns/dnspod/dnspod.go +++ b/providers/dns/dnspod/dnspod.go @@ -1,5 +1,4 @@ -// Package dnspod implements a DNS provider for solving the DNS-01 challenge -// using dnspod DNS. +// Package dnspod implements a DNS provider for solving the DNS-01 challenge using dnspod DNS. package dnspod import ( @@ -11,7 +10,7 @@ import ( "time" "github.com/decker502/dnspod-go" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -28,8 +27,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("DNSPOD_TTL", 600), - PropagationTimeout: env.GetOrDefaultSecond("DNSPOD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("DNSPOD_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("DNSPOD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DNSPOD_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("DNSPOD_HTTP_TIMEOUT", 0), }, @@ -56,16 +55,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for dnspod. -// Deprecated -func NewDNSProviderCredentials(key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.LoginToken = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for dnspod. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -86,7 +75,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, zoneName, err := d.getHostedZone(domain) if err != nil { return err @@ -103,7 +92,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.findTxtRecords(domain, fqdn) if err != nil { @@ -136,14 +125,14 @@ func (d *DNSProvider) getHostedZone(domain string) (string, string, error) { return "", "", fmt.Errorf("API call failed: %v", err) } - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", "", err } var hostedZone dnspod.Domain for _, zone := range zones { - if zone.Name == acme.UnFqdn(authZone) { + if zone.Name == dns01.UnFqdn(authZone) { hostedZone = zone } } @@ -192,7 +181,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnspod.Record, erro } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/dreamhost/dreamhost.go b/providers/dns/dreamhost/dreamhost.go index 9edd27b7..6b4dd2f6 100644 --- a/providers/dns/dreamhost/dreamhost.go +++ b/providers/dns/dreamhost/dreamhost.go @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -72,8 +72,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) - record := acme.UnFqdn(fqdn) + fqdn, value := dns01.GetRecord(domain, keyAuth) + record := dns01.UnFqdn(fqdn) u, err := d.buildQuery(cmdAddRecord, record, value) if err != nil { @@ -89,8 +89,8 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp clears DreamHost TXT record func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) - record := acme.UnFqdn(fqdn) + fqdn, value := dns01.GetRecord(domain, keyAuth) + record := dns01.UnFqdn(fqdn) u, err := d.buildQuery(cmdRemoveRecord, record, value) if err != nil { diff --git a/providers/dns/dreamhost/dreamhost_test.go b/providers/dns/dreamhost/dreamhost_test.go index fc22fe3d..8823fbb9 100644 --- a/providers/dns/dreamhost/dreamhost_test.go +++ b/providers/dns/dreamhost/dreamhost_test.go @@ -15,7 +15,7 @@ import ( var envTest = tester.NewEnvTest("DREAMHOST_API_KEY"). WithDomain("DREAMHOST_TEST_DOMAIN") -var ( +const ( fakeAPIKey = "asdf1234" fakeChallengeToken = "foobar" fakeKeyAuth = "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI" diff --git a/providers/dns/duckdns/client.go b/providers/dns/duckdns/client.go new file mode 100644 index 00000000..b7e6cae4 --- /dev/null +++ b/providers/dns/duckdns/client.go @@ -0,0 +1,68 @@ +package duckdns + +import ( + "fmt" + "io/ioutil" + "net/url" + "strconv" + "strings" + + "github.com/miekg/dns" + "github.com/xenolf/lego/challenge/dns01" +) + +// updateTxtRecord Update the domains TXT record +// To update the TXT record we just need to make one simple get request. +// In DuckDNS you only have one TXT record shared with the domain and all sub domains. +func (d *DNSProvider) updateTxtRecord(domain, token, txt string, clear bool) error { + u, _ := url.Parse("https://www.duckdns.org/update") + + mainDomain := getMainDomain(domain) + if len(mainDomain) == 0 { + return fmt.Errorf("unable to find the main domain for: %s", domain) + } + + query := u.Query() + query.Set("domains", mainDomain) + query.Set("token", token) + query.Set("clear", strconv.FormatBool(clear)) + query.Set("txt", txt) + u.RawQuery = query.Encode() + + response, err := d.config.HTTPClient.Get(u.String()) + if err != nil { + return err + } + defer response.Body.Close() + + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + body := string(bodyBytes) + if body != "OK" { + return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, u) + } + return nil +} + +// DuckDNS only lets you write to your subdomain +// so it must be in format subdomain.duckdns.org +// not in format subsubdomain.subdomain.duckdns.org +// so strip off everything that is not top 3 levels +func getMainDomain(domain string) string { + domain = dns01.UnFqdn(domain) + + split := dns.Split(domain) + if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") { + if len(split) < 3 { + return "" + } + + firstSubDomainIndex := split[len(split)-3] + return domain[firstSubDomainIndex:] + } + + return domain[split[len(split)-1]:] +} diff --git a/providers/dns/duckdns/duckdns.go b/providers/dns/duckdns/duckdns.go index 7581af17..bd13be19 100644 --- a/providers/dns/duckdns/duckdns.go +++ b/providers/dns/duckdns/duckdns.go @@ -5,15 +5,10 @@ package duckdns import ( "errors" "fmt" - "io/ioutil" "net/http" - "net/url" - "strconv" - "strings" "time" - "github.com/miekg/dns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -22,18 +17,19 @@ type Config struct { Token string PropagationTimeout time.Duration PollingInterval time.Duration + SequenceInterval time.Duration HTTPClient *http.Client } // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { - client := acme.HTTPClient - client.Timeout = env.GetOrDefaultSecond("DUCKDNS_HTTP_TIMEOUT", 30*time.Second) - return &Config{ - PropagationTimeout: env.GetOrDefaultSecond("DUCKDNS_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("DUCKDNS_POLLING_INTERVAL", acme.DefaultPollingInterval), - HTTPClient: &client, + PropagationTimeout: env.GetOrDefaultSecond("DUCKDNS_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DUCKDNS_POLLING_INTERVAL", dns01.DefaultPollingInterval), + SequenceInterval: env.GetOrDefaultSecond("DUCKDNS_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond("DUCKDNS_HTTP_TIMEOUT", 30*time.Second), + }, } } @@ -56,16 +52,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for http://duckdns.org -// Deprecated -func NewDNSProviderCredentials(token string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Token = token - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for DuckDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -81,13 +67,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - _, txtRecord, _ := acme.DNS01Record(domain, keyAuth) - return updateTxtRecord(domain, d.config.Token, txtRecord, false) + _, txtRecord := dns01.GetRecord(domain, keyAuth) + return d.updateTxtRecord(domain, d.config.Token, txtRecord, false) } // CleanUp clears DuckDNS TXT record func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - return updateTxtRecord(domain, d.config.Token, "", true) + return d.updateTxtRecord(domain, d.config.Token, "", true) } // Timeout returns the timeout and interval to use when checking for DNS propagation. @@ -96,53 +82,8 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -// updateTxtRecord Update the domains TXT record -// To update the TXT record we just need to make one simple get request. -// In DuckDNS you only have one TXT record shared with the domain and all sub domains. -func updateTxtRecord(domain, token, txt string, clear bool) error { - u, _ := url.Parse("https://www.duckdns.org/update") - - query := u.Query() - query.Set("domains", getMainDomain(domain)) - query.Set("token", token) - query.Set("clear", strconv.FormatBool(clear)) - query.Set("txt", txt) - u.RawQuery = query.Encode() - - response, err := acme.HTTPClient.Get(u.String()) - if err != nil { - return err - } - defer response.Body.Close() - - bodyBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - return err - } - - body := string(bodyBytes) - if body != "OK" { - return fmt.Errorf("request to change TXT record for DuckDNS returned the following result (%s) this does not match expectation (OK) used url [%s]", body, u) - } - return nil -} - -// DuckDNS only lets you write to your subdomain -// so it must be in format subdomain.duckdns.org -// not in format subsubdomain.subdomain.duckdns.org -// so strip off everything that is not top 3 levels -func getMainDomain(domain string) string { - domain = acme.UnFqdn(domain) - - split := dns.Split(domain) - if strings.HasSuffix(strings.ToLower(domain), "duckdns.org") { - if len(split) < 3 { - return "" - } - - firstSubDomainIndex := split[len(split)-3] - return domain[firstSubDomainIndex:] - } - - return domain[split[len(split)-1]:] +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval } diff --git a/providers/dns/dyn/client.go b/providers/dns/dyn/client.go index 1ca7b8e5..f7e6cee8 100644 --- a/providers/dns/dyn/client.go +++ b/providers/dns/dyn/client.go @@ -1,6 +1,11 @@ package dyn -import "encoding/json" +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) const defaultBaseURL = "https://api.dynect.net/REST" @@ -18,7 +23,7 @@ type dynResponse struct { Messages json.RawMessage `json:"msgs"` } -type creds struct { +type credentials struct { Customer string `json:"customer_name"` User string `json:"user_name"` Pass string `json:"password"` @@ -33,3 +38,109 @@ type publish struct { Publish bool `json:"publish"` Notes string `json:"notes"` } + +// Starts a new Dyn API Session. Authenticates using customerName, userName, +// password and receives a token to be used in for subsequent requests. +func (d *DNSProvider) login() error { + payload := &credentials{Customer: d.config.CustomerName, User: d.config.UserName, Pass: d.config.Password} + dynRes, err := d.sendRequest(http.MethodPost, "Session", payload) + if err != nil { + return err + } + + var s session + err = json.Unmarshal(dynRes.Data, &s) + if err != nil { + return err + } + + d.token = s.Token + + return nil +} + +// Destroys Dyn Session +func (d *DNSProvider) logout() error { + if len(d.token) == 0 { + // nothing to do + return nil + } + + url := fmt.Sprintf("%s/Session", defaultBaseURL) + req, err := http.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Auth-Token", d.token) + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return err + } + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API request failed to delete session with HTTP status code %d", resp.StatusCode) + } + + d.token = "" + + return nil +} + +func (d *DNSProvider) publish(zone, notes string) error { + pub := &publish{Publish: true, Notes: notes} + resource := fmt.Sprintf("Zone/%s/", zone) + + _, err := d.sendRequest(http.MethodPut, resource, pub) + return err +} + +func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) { + url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if len(d.token) > 0 { + req.Header.Set("Auth-Token", d.token) + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 500 { + return nil, fmt.Errorf("API request failed with HTTP status code %d", resp.StatusCode) + } + + var dynRes dynResponse + err = json.NewDecoder(resp.Body).Decode(&dynRes) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages) + } else if resp.StatusCode == 307 { + // TODO add support for HTTP 307 response and long running jobs + return nil, fmt.Errorf("API request returned HTTP 307. This is currently unsupported") + } + + if dynRes.Status == "failure" { + // TODO add better error handling + return nil, fmt.Errorf("API request failed: %s", dynRes.Messages) + } + + return &dynRes, nil +} diff --git a/providers/dns/dyn/dyn.go b/providers/dns/dyn/dyn.go index 8ec7753f..40fdc19f 100644 --- a/providers/dns/dyn/dyn.go +++ b/providers/dns/dyn/dyn.go @@ -1,17 +1,14 @@ -// Package dyn implements a DNS provider for solving the DNS-01 challenge -// using Dyn Managed DNS. +// Package dyn implements a DNS provider for solving the DNS-01 challenge using Dyn Managed DNS. package dyn import ( - "bytes" - "encoding/json" "errors" "fmt" "net/http" "strconv" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -29,9 +26,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("DYN_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("DYN_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("DYN_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("DYN_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("DYN_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("DYN_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("DYN_HTTP_TIMEOUT", 10*time.Second), }, @@ -62,18 +59,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Dyn DNS. -// Deprecated -func NewDNSProviderCredentials(customerName, userName, password string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.CustomerName = customerName - config.UserName = userName - config.Password = password - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Dyn DNS func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -89,9 +74,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dyn: %v", err) } @@ -124,9 +109,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("dyn: %v", err) } @@ -170,109 +155,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// Starts a new Dyn API Session. Authenticates using customerName, userName, -// password and receives a token to be used in for subsequent requests. -func (d *DNSProvider) login() error { - payload := &creds{Customer: d.config.CustomerName, User: d.config.UserName, Pass: d.config.Password} - dynRes, err := d.sendRequest(http.MethodPost, "Session", payload) - if err != nil { - return err - } - - var s session - err = json.Unmarshal(dynRes.Data, &s) - if err != nil { - return err - } - - d.token = s.Token - - return nil -} - -// Destroys Dyn Session -func (d *DNSProvider) logout() error { - if len(d.token) == 0 { - // nothing to do - return nil - } - - url := fmt.Sprintf("%s/Session", defaultBaseURL) - req, err := http.NewRequest(http.MethodDelete, url, nil) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Auth-Token", d.token) - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API request failed to delete session with HTTP status code %d", resp.StatusCode) - } - - d.token = "" - - return nil -} - -func (d *DNSProvider) publish(zone, notes string) error { - pub := &publish{Publish: true, Notes: notes} - resource := fmt.Sprintf("Zone/%s/", zone) - - _, err := d.sendRequest(http.MethodPut, resource, pub) - return err -} - -func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (*dynResponse, error) { - url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) - - body, err := json.Marshal(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(method, url, bytes.NewReader(body)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - if len(d.token) > 0 { - req.Header.Set("Auth-Token", d.token) - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode >= 500 { - return nil, fmt.Errorf("API request failed with HTTP status code %d", resp.StatusCode) - } - - var dynRes dynResponse - err = json.NewDecoder(resp.Body).Decode(&dynRes) - if err != nil { - return nil, err - } - - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("API request failed with HTTP status code %d: %s", resp.StatusCode, dynRes.Messages) - } else if resp.StatusCode == 307 { - // TODO add support for HTTP 307 response and long running jobs - return nil, fmt.Errorf("API request returned HTTP 307. This is currently unsupported") - } - - if dynRes.Status == "failure" { - // TODO add better error handling - return nil, fmt.Errorf("API request failed: %s", dynRes.Messages) - } - - return &dynRes, nil -} diff --git a/providers/dns/exec/doc.go b/providers/dns/exec/doc.go deleted file mode 100644 index 9aa53fcc..00000000 --- a/providers/dns/exec/doc.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Package exec implements a manual DNS provider which runs a program for adding/removing the DNS record. - -The file name of the external program is specified in the environment variable `EXEC_PATH`. -When it is run by lego, three command-line parameters are passed to it: -The action ("present" or "cleanup"), the fully-qualified domain name, the value for the record and the TTL. - -For example, requesting a certificate for the domain 'foo.example.com' can be achieved by calling lego as follows: - - EXEC_PATH=./update-dns.sh \ - lego --dns exec \ - --domains foo.example.com \ - --email invalid@example.com run - -It will then call the program './update-dns.sh' with like this: - - ./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" "120" - -The program then needs to make sure the record is inserted. -When it returns an error via a non-zero exit code, lego aborts. - -When the record is to be removed again, -the program is called with the first command-line parameter set to "cleanup" instead of "present". - -If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`: - - EXEC_MODE=RAW \ - EXEC_PATH=./update-dns.sh \ - lego --dns exec \ - --domains foo.example.com \ - --email invalid@example.com run - -It will then call the program './update-dns.sh' like this: - - ./update-dns.sh "present" "foo.example.com." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" - -NOTE: -The `--` is because the token MAY start with a `-`, and the called program may try and interpret a - as indicating a flag. -In the case of urfave, which is commonly used, -you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely. -*/ -package exec diff --git a/providers/dns/exec/exec.go b/providers/dns/exec/exec.go index b8ff0c7d..dc0467e1 100644 --- a/providers/dns/exec/exec.go +++ b/providers/dns/exec/exec.go @@ -1,3 +1,4 @@ +// Package exec implements a DNS provider which runs a program for adding/removing the DNS record. package exec import ( @@ -5,17 +6,27 @@ import ( "fmt" "os" "os/exec" - "strconv" + "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) // Config Provider configuration. type Config struct { - Program string - Mode string + Program string + Mode string + PropagationTimeout time.Duration + PollingInterval time.Duration +} + +// NewDefaultConfig returns a default configuration for the DNSProvider +func NewDefaultConfig() *Config { + return &Config{ + PropagationTimeout: env.GetOrDefaultSecond("EXEC_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("EXEC_POLLING_INTERVAL", dns01.DefaultPollingInterval), + } } // DNSProvider adds and removes the record for the DNS challenge by calling a @@ -32,10 +43,11 @@ func NewDNSProvider() (*DNSProvider, error) { return nil, fmt.Errorf("exec: %v", err) } - return NewDNSProviderConfig(&Config{ - Program: values["EXEC_PATH"], - Mode: os.Getenv("EXEC_MODE"), - }) + config := NewDefaultConfig() + config.Program = values["EXEC_PATH"] + config.Mode = os.Getenv("EXEC_MODE") + + return NewDNSProviderConfig(config) } // NewDNSProviderConfig returns a new DNS provider which runs the given configuration @@ -48,25 +60,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{config: config}, nil } -// NewDNSProviderProgram returns a new DNS provider which runs the given program -// for adding and removing the DNS record. -// Deprecated: use NewDNSProviderConfig instead -func NewDNSProviderProgram(program string) (*DNSProvider, error) { - if len(program) == 0 { - return nil, errors.New("the program is undefined") - } - - return NewDNSProviderConfig(&Config{Program: program}) -} - // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { var args []string if d.config.Mode == "RAW" { args = []string{"present", "--", domain, token, keyAuth} } else { - fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) - args = []string{"present", fqdn, value, strconv.Itoa(ttl)} + fqdn, value := dns01.GetRecord(domain, keyAuth) + args = []string{"present", fqdn, value} } cmd := exec.Command(d.config.Program, args...) @@ -85,8 +86,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { if d.config.Mode == "RAW" { args = []string{"cleanup", "--", domain, token, keyAuth} } else { - fqdn, value, ttl := acme.DNS01Record(domain, keyAuth) - args = []string{"cleanup", fqdn, value, strconv.Itoa(ttl)} + fqdn, value := dns01.GetRecord(domain, keyAuth) + args = []string{"cleanup", fqdn, value} } cmd := exec.Command(d.config.Program, args...) @@ -98,3 +99,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return err } + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} diff --git a/providers/dns/exec/exec_test.go b/providers/dns/exec/exec_test.go index c7fe4c02..56e4a818 100644 --- a/providers/dns/exec/exec_test.go +++ b/providers/dns/exec/exec_test.go @@ -37,7 +37,7 @@ func TestDNSProvider_Present(t *testing.T) { Mode: "", }, expected: expected{ - args: "present _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM 120\n", + args: "present _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\n", }, }, { @@ -110,7 +110,7 @@ func TestDNSProvider_CleanUp(t *testing.T) { Mode: "", }, expected: expected{ - args: "cleanup _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM 120\n", + args: "cleanup _acme-challenge.domain. pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\n", }, }, { diff --git a/providers/dns/exec/readme.md b/providers/dns/exec/readme.md new file mode 100644 index 00000000..3fa2f7cf --- /dev/null +++ b/providers/dns/exec/readme.md @@ -0,0 +1,86 @@ +# Execute an external program + +Solving the DNS-01 challenge using an external program. + +## Description + +The file name of the external program is specified in the environment variable `EXEC_PATH`. + +When it is run by lego, three command-line parameters are passed to it: +The action ("present" or "cleanup"), the fully-qualified domain name and the value for the record. + +For example, requesting a certificate for the domain 'foo.example.com' can be achieved by calling lego as follows: + +```bash +EXEC_PATH=./update-dns.sh \ + lego --dns exec \ + --domains foo.example.com \ + --email invalid@example.com run +``` + +It will then call the program './update-dns.sh' with like this: + +```bash +./update-dns.sh "present" "_acme-challenge.foo.example.com." "MsijOYZxqyjGnFGwhjrhfg-Xgbl5r68WPda0J9EgqqI" +``` + +The program then needs to make sure the record is inserted. +When it returns an error via a non-zero exit code, lego aborts. + +When the record is to be removed again, +the program is called with the first command-line parameter set to `cleanup` instead of `present`. + +If you want to use the raw domain, token, and keyAuth values with your program, you can set `EXEC_MODE=RAW`: + +```bash +EXEC_MODE=RAW \ +EXEC_PATH=./update-dns.sh \ + lego --dns exec \ + --domains foo.example.com \ + --email invalid@example.com run +``` + +It will then call the program `./update-dns.sh` like this: + +```bash +./update-dns.sh "present" "foo.example.com." "--" "some-token" "KxAy-J3NwUmg9ZQuM-gP_Mq1nStaYSaP9tYQs5_-YsE.ksT-qywTd8058G-SHHWA3RAN72Pr0yWtPYmmY5UBpQ8" +``` + +## Commands + +### Present + +| Mode | Command | +|---------|----------------------------------------------------| +| default | `myprogram present -- ` | +| `RAW` | `myprogram present -- ` | + +### Cleanup + +| Mode | Command | +|---------|----------------------------------------------------| +| default | `myprogram cleanup -- ` | +| `RAW` | `myprogram cleanup -- ` | + +### Timeout + +The command have to display propagation timeout and polling interval into Stdout. + +The values must be formatted as JSON, and times are in seconds. +Example: `{"timeout": 30, "interval": 5}` + +If an error occurs or if the command is not provided: +the default display propagation timeout and polling interval are used. + +| Mode | Command | +|---------|----------------------------------------------------| +| default | `myprogram timeout` | +| `RAW` | `myprogram timeout` | + + +## NOTE + +The `--` is because the token MAY start with a `-`, and the called program may try and interpret a - as indicating a flag. + +In the case of urfave, which is commonly used, +you can use the `--` delimiter to specify the start of positional arguments, and handle such a string safely. diff --git a/providers/dns/exoscale/exoscale.go b/providers/dns/exoscale/exoscale.go index 4ffcc6e7..10a7a592 100644 --- a/providers/dns/exoscale/exoscale.go +++ b/providers/dns/exoscale/exoscale.go @@ -1,5 +1,4 @@ -// Package exoscale implements a DNS provider for solving the DNS-01 challenge -// using exoscale DNS. +// Package exoscale implements a DNS provider for solving the DNS-01 challenge using exoscale DNS. package exoscale import ( @@ -9,7 +8,7 @@ import ( "time" "github.com/exoscale/egoscale" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -29,9 +28,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("EXOSCALE_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("EXOSCALE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("EXOSCALE_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("EXOSCALE_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("EXOSCALE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("EXOSCALE_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("EXOSCALE_HTTP_TIMEOUT", 0), }, @@ -60,18 +59,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderClient Uses the supplied parameters -// to return a DNSProvider instance configured for Exoscale. -// Deprecated -func NewDNSProviderClient(key, secret, endpoint string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = key - config.APISecret = secret - config.Endpoint = endpoint - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Exoscale. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -94,7 +81,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain) if err != nil { return err @@ -137,7 +124,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, recordName, err := d.FindZoneAndRecordName(fqdn, domain) if err != nil { return err @@ -181,12 +168,12 @@ func (d *DNSProvider) FindExistingRecordID(zone, recordName string) (int64, erro // FindZoneAndRecordName Extract DNS zone and DNS entry name func (d *DNSProvider) FindZoneAndRecordName(fqdn, domain string) (string, string, error) { - zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", "", err } - zone = acme.UnFqdn(zone) - name := acme.UnFqdn(fqdn) + zone = dns01.UnFqdn(zone) + name := dns01.UnFqdn(fqdn) name = name[:len(name)-len("."+zone)] return zone, name, nil diff --git a/providers/dns/fastdns/fastdns.go b/providers/dns/fastdns/fastdns.go index 9ef6cb0e..a8d94f5d 100644 --- a/providers/dns/fastdns/fastdns.go +++ b/providers/dns/fastdns/fastdns.go @@ -1,3 +1,4 @@ +// Package fastdns implements a DNS provider for solving the DNS-01 challenge using FastDNS. package fastdns import ( @@ -8,7 +9,7 @@ import ( configdns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v1" "github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -23,9 +24,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - PropagationTimeout: env.GetOrDefaultSecond("AKAMAI_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("AKAMAI_POLLING_INTERVAL", acme.DefaultPollingInterval), - TTL: env.GetOrDefaultInt("AKAMAI_TTL", 120), + PropagationTimeout: env.GetOrDefaultSecond("AKAMAI_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("AKAMAI_POLLING_INTERVAL", dns01.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("AKAMAI_TTL", dns01.DefaultTTL), } } @@ -54,22 +55,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderClient uses the supplied parameters -// to return a DNSProvider instance configured for FastDNS. -// Deprecated -func NewDNSProviderClient(host, clientToken, clientSecret, accessToken string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Config = edgegrid.Config{ - Host: host, - ClientToken: clientToken, - ClientSecret: clientSecret, - AccessToken: accessToken, - MaxBody: 131072, - } - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for FastDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -85,7 +70,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fullfil the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain) if err != nil { return fmt.Errorf("fastdns: %v", err) @@ -121,7 +106,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneName, recordName, err := d.findZoneAndRecordName(fqdn, domain) if err != nil { return fmt.Errorf("fastdns: %v", err) @@ -154,12 +139,12 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func (d *DNSProvider) findZoneAndRecordName(fqdn, domain string) (string, string, error) { - zone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", "", err } - zone = acme.UnFqdn(zone) - name := acme.UnFqdn(fqdn) + zone = dns01.UnFqdn(zone) + name := dns01.UnFqdn(fqdn) name = name[:len(name)-len("."+zone)] return zone, name, nil diff --git a/providers/dns/gandi/client.go b/providers/dns/gandi/client.go index 0ed0bee4..901a7894 100644 --- a/providers/dns/gandi/client.go +++ b/providers/dns/gandi/client.go @@ -1,8 +1,11 @@ package gandi import ( + "bytes" "encoding/xml" "fmt" + "io" + "io/ioutil" ) // types for XML-RPC method calls and parameters @@ -90,3 +93,224 @@ type rpcError struct { func (e rpcError) Error() string { return fmt.Sprintf("Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString) } + +// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by +// marshaling the data given in the call argument to XML and sending +// that via HTTP Post to Gandi. +// The response is then unmarshalled into the resp argument. +func (d *DNSProvider) rpcCall(call *methodCall, resp response) error { + // marshal + b, err := xml.MarshalIndent(call, "", " ") + if err != nil { + return fmt.Errorf("marshal error: %v", err) + } + + // post + b = append([]byte(``+"\n"), b...) + respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b)) + if err != nil { + return err + } + + // unmarshal + err = xml.Unmarshal(respBody, resp) + if err != nil { + return fmt.Errorf("unmarshal error: %v", err) + } + if resp.faultCode() != 0 { + return rpcError{ + faultCode: resp.faultCode(), faultString: resp.faultString()} + } + return nil +} + +// functions to perform API actions + +func (d *DNSProvider) getZoneID(domain string) (int, error) { + resp := &responseStruct{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.info", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramString{Value: domain}, + }, + }, resp) + if err != nil { + return 0, err + } + + var zoneID int + for _, member := range resp.StructMembers { + if member.Name == "zone_id" { + zoneID = member.ValueInt + } + } + + if zoneID == 0 { + return 0, fmt.Errorf("could not determine zone_id for %s", domain) + } + return zoneID, nil +} + +func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) { + resp := &responseStruct{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.clone", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramInt{Value: zoneID}, + paramInt{Value: 0}, + paramStruct{ + StructMembers: []structMember{ + structMemberString{ + Name: "name", + Value: name, + }}, + }, + }, + }, resp) + if err != nil { + return 0, err + } + + var newZoneID int + for _, member := range resp.StructMembers { + if member.Name == "id" { + newZoneID = member.ValueInt + } + } + + if newZoneID == 0 { + return 0, fmt.Errorf("could not determine cloned zone_id") + } + return newZoneID, nil +} + +func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) { + resp := &responseInt{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.version.new", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramInt{Value: zoneID}, + }, + }, resp) + if err != nil { + return 0, err + } + + if resp.Value == 0 { + return 0, fmt.Errorf("could not create new zone version") + } + return resp.Value, nil +} + +func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error { + resp := &responseStruct{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.record.add", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramInt{Value: zoneID}, + paramInt{Value: version}, + paramStruct{ + StructMembers: []structMember{ + structMemberString{ + Name: "type", + Value: "TXT", + }, structMemberString{ + Name: "name", + Value: name, + }, structMemberString{ + Name: "value", + Value: value, + }, structMemberInt{ + Name: "ttl", + Value: ttl, + }}, + }, + }, + }, resp) + return err +} + +func (d *DNSProvider) setZoneVersion(zoneID int, version int) error { + resp := &responseBool{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.version.set", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramInt{Value: zoneID}, + paramInt{Value: version}, + }, + }, resp) + if err != nil { + return err + } + + if !resp.Value { + return fmt.Errorf("could not set zone version") + } + return nil +} + +func (d *DNSProvider) setZone(domain string, zoneID int) error { + resp := &responseStruct{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.set", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramString{Value: domain}, + paramInt{Value: zoneID}, + }, + }, resp) + if err != nil { + return err + } + + var respZoneID int + for _, member := range resp.StructMembers { + if member.Name == "zone_id" { + respZoneID = member.ValueInt + } + } + + if respZoneID != zoneID { + return fmt.Errorf("could not set new zone_id for %s", domain) + } + return nil +} + +func (d *DNSProvider) deleteZone(zoneID int) error { + resp := &responseBool{} + err := d.rpcCall(&methodCall{ + MethodName: "domain.zone.delete", + Params: []param{ + paramString{Value: d.config.APIKey}, + paramInt{Value: zoneID}, + }, + }, resp) + if err != nil { + return err + } + + if !resp.Value { + return fmt.Errorf("could not delete zone_id") + } + return nil +} + +func (d *DNSProvider) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) { + resp, err := d.config.HTTPClient.Post(url, bodyType, body) + if err != nil { + return nil, fmt.Errorf("HTTP Post Error: %v", err) + } + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("HTTP Post Error: %v", err) + } + + return b, nil +} diff --git a/providers/dns/gandi/gandi.go b/providers/dns/gandi/gandi.go index f282d28b..1a486ecf 100644 --- a/providers/dns/gandi/gandi.go +++ b/providers/dns/gandi/gandi.go @@ -1,20 +1,15 @@ -// Package gandi implements a DNS provider for solving the DNS-01 -// challenge using Gandi DNS. +// Package gandi implements a DNS provider for solving the DNS-01 challenge using Gandi DNS. package gandi import ( - "bytes" - "encoding/xml" "errors" "fmt" - "io" - "io/ioutil" "net/http" "strings" "sync" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -27,10 +22,6 @@ const ( minTTL = 300 ) -// findZoneByFqdn determines the DNS zone of an fqdn. -// It is overridden during tests. -var findZoneByFqdn = acme.FindZoneByFqdn - // Config is used to configure the creation of the DNSProvider type Config struct { BaseURL string @@ -68,6 +59,8 @@ type DNSProvider struct { inProgressAuthZones map[string]struct{} inProgressMu sync.Mutex config *Config + // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests. + findZoneByFqdn func(fqdn string) (string, error) } // NewDNSProvider returns a DNSProvider instance configured for Gandi. @@ -84,16 +77,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Gandi. -// Deprecated -func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Gandi. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -112,6 +95,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { config: config, inProgressFQDNs: make(map[string]inProgressInfo), inProgressAuthZones: make(map[string]struct{}), + findZoneByFqdn: dns01.FindZoneByFqdn, }, nil } @@ -119,14 +103,14 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // does this by creating and activating a new temporary Gandi DNS // zone. This new zone contains the TXT record. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) if d.config.TTL < minTTL { d.config.TTL = minTTL // 300 is gandi minimum value for ttl } // find authZone and Gandi zone_id for fqdn - authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := d.findZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("gandi: findZoneByFqdn failure: %v", err) } @@ -154,7 +138,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // perform API actions to create and activate new gandi zone // containing the required TXT record - newZoneName := fmt.Sprintf("%s [ACME Challenge %s]", acme.UnFqdn(authZone), time.Now().Format(time.RFC822Z)) + newZoneName := fmt.Sprintf("%s [ACME Challenge %s]", dns01.UnFqdn(authZone), time.Now().Format(time.RFC822Z)) newZoneID, err := d.cloneZone(zoneID, newZoneName) if err != nil { @@ -196,7 +180,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // parameters. It does this by restoring the old Gandi DNS zone and // removing the temporary one created by Present. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve zoneID, newZoneID and authZone d.inProgressMu.Lock() @@ -228,224 +212,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by -// marshaling the data given in the call argument to XML and sending -// that via HTTP Post to Gandi. -// The response is then unmarshalled into the resp argument. -func (d *DNSProvider) rpcCall(call *methodCall, resp response) error { - // marshal - b, err := xml.MarshalIndent(call, "", " ") - if err != nil { - return fmt.Errorf("marshal error: %v", err) - } - - // post - b = append([]byte(``+"\n"), b...) - respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b)) - if err != nil { - return err - } - - // unmarshal - err = xml.Unmarshal(respBody, resp) - if err != nil { - return fmt.Errorf("unmarshal error: %v", err) - } - if resp.faultCode() != 0 { - return rpcError{ - faultCode: resp.faultCode(), faultString: resp.faultString()} - } - return nil -} - -// functions to perform API actions - -func (d *DNSProvider) getZoneID(domain string) (int, error) { - resp := &responseStruct{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.info", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramString{Value: domain}, - }, - }, resp) - if err != nil { - return 0, err - } - - var zoneID int - for _, member := range resp.StructMembers { - if member.Name == "zone_id" { - zoneID = member.ValueInt - } - } - - if zoneID == 0 { - return 0, fmt.Errorf("could not determine zone_id for %s", domain) - } - return zoneID, nil -} - -func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) { - resp := &responseStruct{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.clone", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramInt{Value: zoneID}, - paramInt{Value: 0}, - paramStruct{ - StructMembers: []structMember{ - structMemberString{ - Name: "name", - Value: name, - }}, - }, - }, - }, resp) - if err != nil { - return 0, err - } - - var newZoneID int - for _, member := range resp.StructMembers { - if member.Name == "id" { - newZoneID = member.ValueInt - } - } - - if newZoneID == 0 { - return 0, fmt.Errorf("could not determine cloned zone_id") - } - return newZoneID, nil -} - -func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) { - resp := &responseInt{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.version.new", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramInt{Value: zoneID}, - }, - }, resp) - if err != nil { - return 0, err - } - - if resp.Value == 0 { - return 0, fmt.Errorf("could not create new zone version") - } - return resp.Value, nil -} - -func (d *DNSProvider) addTXTRecord(zoneID int, version int, name string, value string, ttl int) error { - resp := &responseStruct{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.record.add", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramInt{Value: zoneID}, - paramInt{Value: version}, - paramStruct{ - StructMembers: []structMember{ - structMemberString{ - Name: "type", - Value: "TXT", - }, structMemberString{ - Name: "name", - Value: name, - }, structMemberString{ - Name: "value", - Value: value, - }, structMemberInt{ - Name: "ttl", - Value: ttl, - }}, - }, - }, - }, resp) - return err -} - -func (d *DNSProvider) setZoneVersion(zoneID int, version int) error { - resp := &responseBool{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.version.set", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramInt{Value: zoneID}, - paramInt{Value: version}, - }, - }, resp) - if err != nil { - return err - } - - if !resp.Value { - return fmt.Errorf("could not set zone version") - } - return nil -} - -func (d *DNSProvider) setZone(domain string, zoneID int) error { - resp := &responseStruct{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.set", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramString{Value: domain}, - paramInt{Value: zoneID}, - }, - }, resp) - if err != nil { - return err - } - - var respZoneID int - for _, member := range resp.StructMembers { - if member.Name == "zone_id" { - respZoneID = member.ValueInt - } - } - - if respZoneID != zoneID { - return fmt.Errorf("could not set new zone_id for %s", domain) - } - return nil -} - -func (d *DNSProvider) deleteZone(zoneID int) error { - resp := &responseBool{} - err := d.rpcCall(&methodCall{ - MethodName: "domain.zone.delete", - Params: []param{ - paramString{Value: d.config.APIKey}, - paramInt{Value: zoneID}, - }, - }, resp) - if err != nil { - return err - } - - if !resp.Value { - return fmt.Errorf("could not delete zone_id") - } - return nil -} - -func (d *DNSProvider) httpPost(url string, bodyType string, body io.Reader) ([]byte, error) { - resp, err := d.config.HTTPClient.Post(url, bodyType, body) - if err != nil { - return nil, fmt.Errorf("HTTP Post Error: %v", err) - } - defer resp.Body.Close() - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("HTTP Post Error: %v", err) - } - - return b, nil -} diff --git a/providers/dns/gandi/gandi_mock_test.go b/providers/dns/gandi/gandi_mock_test.go new file mode 100644 index 00000000..66c63fa3 --- /dev/null +++ b/providers/dns/gandi/gandi_mock_test.go @@ -0,0 +1,817 @@ +package gandi + +// CleanUp Request->Response 1 (setZone) +const cleanup1RequestMock = ` + + domain.zone.set + + + 123412341234123412341234 + + + + + example.com. + + + + + 1234567 + + +` + +// CleanUp Request->Response 1 (setZone) +const cleanup1ResponseMock = ` + + + + + +date_updated +20160216T16:24:38 + + +date_delete +20170331T16:04:06 + + +is_premium +0 + + +date_hold_begin +20170215T02:04:06 + + +date_registry_end +20170215T02:04:06 + + +authinfo_expiration_date +20161211T21:31:20 + + +contacts + + +owner + + +handle +LEGO-GANDI + + +id +111111 + + + + +admin + + +handle +LEGO-GANDI + + +id +111111 + + + + +bill + + +handle +LEGO-GANDI + + +id +111111 + + + + +tech + + +handle +LEGO-GANDI + + +id +111111 + + + + +reseller + + + + +nameservers + +a.dns.gandi.net +b.dns.gandi.net +c.dns.gandi.net + + + +date_restore_end +20170501T02:04:06 + + +id +2222222 + + +authinfo +ABCDABCDAB + + +status + +clientTransferProhibited +serverTransferProhibited + + + +tags + + + + +date_hold_end +20170401T02:04:06 + + +services + +gandidns +gandimail + + + +date_pending_delete_end +20170506T02:04:06 + + +zone_id +1234567 + + +date_renew_begin +20120101T00:00:00 + + +fqdn +example.com + + +autorenew + + +date_registry_creation +20150215T02:04:06 + + +tld +org + + +date_created +20150215T03:04:06 + + + + + +` + +// CleanUp Request->Response 2 (deleteZone) +const cleanup2RequestMock = ` + + domain.zone.delete + + + 123412341234123412341234 + + + + + 7654321 + + +` + +// CleanUp Request->Response 2 (deleteZone) +const cleanup2ResponseMock = ` + + + +1 + + + +` + +// Present Request->Response 1 (getZoneID) +const present1RequestMock = ` + + domain.info + + + 123412341234123412341234 + + + + + example.com. + + +` + +// Present Request->Response 1 (getZoneID) +const present1ResponseMock = ` + + + + + +date_updated +20160216T16:14:23 + + +date_delete +20170331T16:04:06 + + +is_premium +0 + + +date_hold_begin +20170215T02:04:06 + + +date_registry_end +20170215T02:04:06 + + +authinfo_expiration_date +20161211T21:31:20 + + +contacts + + +owner + + +handle +LEGO-GANDI + + +id +111111 + + + + +admin + + +handle +LEGO-GANDI + + +id +111111 + + + + +bill + + +handle +LEGO-GANDI + + +id +111111 + + + + +tech + + +handle +LEGO-GANDI + + +id +111111 + + + + +reseller + + + + +nameservers + +a.dns.gandi.net +b.dns.gandi.net +c.dns.gandi.net + + + +date_restore_end +20170501T02:04:06 + + +id +2222222 + + +authinfo +ABCDABCDAB + + +status + +clientTransferProhibited +serverTransferProhibited + + + +tags + + + + +date_hold_end +20170401T02:04:06 + + +services + +gandidns +gandimail + + + +date_pending_delete_end +20170506T02:04:06 + + +zone_id +1234567 + + +date_renew_begin +20120101T00:00:00 + + +fqdn +example.com + + +autorenew + + +date_registry_creation +20150215T02:04:06 + + +tld +org + + +date_created +20150215T03:04:06 + + + + + +` + +// Present Request->Response 2 (cloneZone) +const present2RequestMock = ` + + domain.zone.clone + + + 123412341234123412341234 + + + + + 1234567 + + + + + 0 + + + + + + + name + + example.com [ACME Challenge 01 Jan 16 00:00 +0000] + + + + + +` + +// Present Request->Response 2 (cloneZone) +const present2ResponseMock = ` + + + + + +name +example.com [ACME Challenge 01 Jan 16 00:00 +0000] + + +versions + +1 + + + +date_updated +20160216T16:24:29 + + +id +7654321 + + +owner +LEGO-GANDI + + +version +1 + + +domains +0 + + +public +0 + + + + + +` + +// Present Request->Response 3 (newZoneVersion) +const present3RequestMock = ` + + domain.zone.version.new + + + 123412341234123412341234 + + + + + 7654321 + + +` + +// Present Request->Response 3 (newZoneVersion) +const present3ResponseMock = ` + + + +2 + + + +` + +// Present Request->Response 4 (addTXTRecord) +const present4RequestMock = ` + + domain.zone.record.add + + + 123412341234123412341234 + + + + + 7654321 + + + + + 2 + + + + + + + type + + TXT + + + + name + + _acme-challenge.abc.def + + + + value + + ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ + + + + ttl + + 300 + + + + + +` + +// Present Request->Response 4 (addTXTRecord) +const present4ResponseMock = ` + + + + + +name +_acme-challenge.abc.def + + +type +TXT + + +id +333333333 + + +value +"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ" + + +ttl +300 + + + + + +` + +// Present Request->Response 5 (setZoneVersion) +const present5RequestMock = ` + + domain.zone.version.set + + + 123412341234123412341234 + + + + + 7654321 + + + + + 2 + + +` + +// Present Request->Response 5 (setZoneVersion) +const present5ResponseMock = ` + + + +1 + + + +` + +// Present Request->Response 6 (setZone) +const present6RequestMock = ` + + domain.zone.set + + + 123412341234123412341234 + + + + + example.com. + + + + + 7654321 + + +` + +// Present Request->Response 6 (setZone) +const present6ResponseMock = ` + + + + + +date_updated +20160216T16:14:23 + + +date_delete +20170331T16:04:06 + + +is_premium +0 + + +date_hold_begin +20170215T02:04:06 + + +date_registry_end +20170215T02:04:06 + + +authinfo_expiration_date +20161211T21:31:20 + + +contacts + + +owner + + +handle +LEGO-GANDI + + +id +111111 + + + + +admin + + +handle +LEGO-GANDI + + +id +111111 + + + + +bill + + +handle +LEGO-GANDI + + +id +111111 + + + + +tech + + +handle +LEGO-GANDI + + +id +111111 + + + + +reseller + + + + +nameservers + +a.dns.gandi.net +b.dns.gandi.net +c.dns.gandi.net + + + +date_restore_end +20170501T02:04:06 + + +id +2222222 + + +authinfo +ABCDABCDAB + + +status + +clientTransferProhibited +serverTransferProhibited + + + +tags + + + + +date_hold_end +20170401T02:04:06 + + +services + +gandidns +gandimail + + + +date_pending_delete_end +20170506T02:04:06 + + +zone_id +7654321 + + +date_renew_begin +20120101T00:00:00 + + +fqdn +example.com + + +autorenew + + +date_registry_creation +20150215T02:04:06 + + +tld +org + + +date_created +20150215T03:04:06 + + + + + +` diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go index 69ca8350..4e76c4f9 100644 --- a/providers/dns/gandi/gandi_test.go +++ b/providers/dns/gandi/gandi_test.go @@ -97,10 +97,32 @@ func TestNewDNSProviderConfig(t *testing.T) { // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { + // serverResponses is the XML-RPC Request->Response map used by the + // fake RPC server. It was generated by recording a real RPC session + // which resulted in the successful issue of a cert, and then + // anonymizing the RPC data. + var serverResponses = map[string]string{ + // Present Request->Response 1 (getZoneID) + present1RequestMock: present1ResponseMock, + // Present Request->Response 2 (cloneZone) + present2RequestMock: present2ResponseMock, + // Present Request->Response 3 (newZoneVersion) + present3RequestMock: present3ResponseMock, + // Present Request->Response 4 (addTXTRecord) + present4RequestMock: present4ResponseMock, + // Present Request->Response 5 (setZoneVersion) + present5RequestMock: present5ResponseMock, + // Present Request->Response 6 (setZone) + present6RequestMock: present6ResponseMock, + // CleanUp Request->Response 1 (setZone) + cleanup1RequestMock: cleanup1ResponseMock, + // CleanUp Request->Response 2 (deleteZone) + cleanup2RequestMock: cleanup2ResponseMock, + } + fakeKeyAuth := "XXXX" - regexpDate, err := regexp.Compile(`\[ACME Challenge [^\]:]*:[^\]]*\]`) - require.NoError(t, err) + regexpDate := regexp.MustCompile(`\[ACME Challenge [^\]:]*:[^\]]*\]`) // start fake RPC server fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -119,7 +141,7 @@ func TestDNSProvider(t *testing.T) { defer fakeServer.Close() // define function to override findZoneByFqdn with - fakeFindZoneByFqdn := func(fqdn string, nameserver []string) (string, error) { + fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } @@ -131,11 +153,11 @@ func TestDNSProvider(t *testing.T) { require.NoError(t, err) // override findZoneByFqdn function - savedFindZoneByFqdn := findZoneByFqdn + savedFindZoneByFqdn := provider.findZoneByFqdn defer func() { - findZoneByFqdn = savedFindZoneByFqdn + provider.findZoneByFqdn = savedFindZoneByFqdn }() - findZoneByFqdn = fakeFindZoneByFqdn + provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present err = provider.Present("abc.def.example.com", "", fakeKeyAuth) @@ -145,794 +167,3 @@ func TestDNSProvider(t *testing.T) { err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) } - -// serverResponses is the XML-RPC Request->Response map used by the -// fake RPC server. It was generated by recording a real RPC session -// which resulted in the successful issue of a cert, and then -// anonymizing the RPC data. -var serverResponses = map[string]string{ - // Present Request->Response 1 (getZoneID) - ` - - domain.info - - - 123412341234123412341234 - - - - - example.com. - - -`: ` - - - - - -date_updated -20160216T16:14:23 - - -date_delete -20170331T16:04:06 - - -is_premium -0 - - -date_hold_begin -20170215T02:04:06 - - -date_registry_end -20170215T02:04:06 - - -authinfo_expiration_date -20161211T21:31:20 - - -contacts - - -owner - - -handle -LEGO-GANDI - - -id -111111 - - - - -admin - - -handle -LEGO-GANDI - - -id -111111 - - - - -bill - - -handle -LEGO-GANDI - - -id -111111 - - - - -tech - - -handle -LEGO-GANDI - - -id -111111 - - - - -reseller - - - - -nameservers - -a.dns.gandi.net -b.dns.gandi.net -c.dns.gandi.net - - - -date_restore_end -20170501T02:04:06 - - -id -2222222 - - -authinfo -ABCDABCDAB - - -status - -clientTransferProhibited -serverTransferProhibited - - - -tags - - - - -date_hold_end -20170401T02:04:06 - - -services - -gandidns -gandimail - - - -date_pending_delete_end -20170506T02:04:06 - - -zone_id -1234567 - - -date_renew_begin -20120101T00:00:00 - - -fqdn -example.com - - -autorenew - - -date_registry_creation -20150215T02:04:06 - - -tld -org - - -date_created -20150215T03:04:06 - - - - - -`, - // Present Request->Response 2 (cloneZone) - ` - - domain.zone.clone - - - 123412341234123412341234 - - - - - 1234567 - - - - - 0 - - - - - - - name - - example.com [ACME Challenge 01 Jan 16 00:00 +0000] - - - - - -`: ` - - - - - -name -example.com [ACME Challenge 01 Jan 16 00:00 +0000] - - -versions - -1 - - - -date_updated -20160216T16:24:29 - - -id -7654321 - - -owner -LEGO-GANDI - - -version -1 - - -domains -0 - - -public -0 - - - - - -`, - // Present Request->Response 3 (newZoneVersion) - ` - - domain.zone.version.new - - - 123412341234123412341234 - - - - - 7654321 - - -`: ` - - - -2 - - - -`, - // Present Request->Response 4 (addTXTRecord) - ` - - domain.zone.record.add - - - 123412341234123412341234 - - - - - 7654321 - - - - - 2 - - - - - - - type - - TXT - - - - name - - _acme-challenge.abc.def - - - - value - - ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ - - - - ttl - - 300 - - - - - -`: ` - - - - - -name -_acme-challenge.abc.def - - -type -TXT - - -id -333333333 - - -value -"ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ" - - -ttl -300 - - - - - -`, - // Present Request->Response 5 (setZoneVersion) - ` - - domain.zone.version.set - - - 123412341234123412341234 - - - - - 7654321 - - - - - 2 - - -`: ` - - - -1 - - - -`, - // Present Request->Response 6 (setZone) - ` - - domain.zone.set - - - 123412341234123412341234 - - - - - example.com. - - - - - 7654321 - - -`: ` - - - - - -date_updated -20160216T16:14:23 - - -date_delete -20170331T16:04:06 - - -is_premium -0 - - -date_hold_begin -20170215T02:04:06 - - -date_registry_end -20170215T02:04:06 - - -authinfo_expiration_date -20161211T21:31:20 - - -contacts - - -owner - - -handle -LEGO-GANDI - - -id -111111 - - - - -admin - - -handle -LEGO-GANDI - - -id -111111 - - - - -bill - - -handle -LEGO-GANDI - - -id -111111 - - - - -tech - - -handle -LEGO-GANDI - - -id -111111 - - - - -reseller - - - - -nameservers - -a.dns.gandi.net -b.dns.gandi.net -c.dns.gandi.net - - - -date_restore_end -20170501T02:04:06 - - -id -2222222 - - -authinfo -ABCDABCDAB - - -status - -clientTransferProhibited -serverTransferProhibited - - - -tags - - - - -date_hold_end -20170401T02:04:06 - - -services - -gandidns -gandimail - - - -date_pending_delete_end -20170506T02:04:06 - - -zone_id -7654321 - - -date_renew_begin -20120101T00:00:00 - - -fqdn -example.com - - -autorenew - - -date_registry_creation -20150215T02:04:06 - - -tld -org - - -date_created -20150215T03:04:06 - - - - - -`, - // CleanUp Request->Response 1 (setZone) - ` - - domain.zone.set - - - 123412341234123412341234 - - - - - example.com. - - - - - 1234567 - - -`: ` - - - - - -date_updated -20160216T16:24:38 - - -date_delete -20170331T16:04:06 - - -is_premium -0 - - -date_hold_begin -20170215T02:04:06 - - -date_registry_end -20170215T02:04:06 - - -authinfo_expiration_date -20161211T21:31:20 - - -contacts - - -owner - - -handle -LEGO-GANDI - - -id -111111 - - - - -admin - - -handle -LEGO-GANDI - - -id -111111 - - - - -bill - - -handle -LEGO-GANDI - - -id -111111 - - - - -tech - - -handle -LEGO-GANDI - - -id -111111 - - - - -reseller - - - - -nameservers - -a.dns.gandi.net -b.dns.gandi.net -c.dns.gandi.net - - - -date_restore_end -20170501T02:04:06 - - -id -2222222 - - -authinfo -ABCDABCDAB - - -status - -clientTransferProhibited -serverTransferProhibited - - - -tags - - - - -date_hold_end -20170401T02:04:06 - - -services - -gandidns -gandimail - - - -date_pending_delete_end -20170506T02:04:06 - - -zone_id -1234567 - - -date_renew_begin -20120101T00:00:00 - - -fqdn -example.com - - -autorenew - - -date_registry_creation -20150215T02:04:06 - - -tld -org - - -date_created -20150215T03:04:06 - - - - - -`, - // CleanUp Request->Response 2 (deleteZone) - ` - - domain.zone.delete - - - 123412341234123412341234 - - - - - 7654321 - - -`: ` - - - -1 - - - -`, -} diff --git a/providers/dns/gandiv5/client.go b/providers/dns/gandiv5/client.go index e342007f..d1701096 100644 --- a/providers/dns/gandiv5/client.go +++ b/providers/dns/gandiv5/client.go @@ -6,6 +6,8 @@ import ( "fmt" "io/ioutil" "net/http" + + "github.com/xenolf/lego/log" ) const apiKeyHeader = "X-Api-Key" @@ -24,6 +26,80 @@ type Record struct { RRSetType string `json:"rrset_type,omitempty"` } +func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error { + // Get exiting values for the TXT records + // Needed to create challenges for both wildcard and base name domains + txtRecord, err := d.getTXTRecord(domain, name) + if err != nil { + return err + } + + values := []string{value} + if len(txtRecord.RRSetValues) > 0 { + values = append(values, txtRecord.RRSetValues...) + } + + target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) + + newRecord := &Record{RRSetTTL: ttl, RRSetValues: values} + req, err := d.newRequest(http.MethodPut, target, newRecord) + if err != nil { + return err + } + + message := &apiResponse{} + err = d.do(req, message) + if err != nil { + return fmt.Errorf("unable to create TXT record for domain %s and name %s: %v", domain, name, err) + } + + if message != nil && len(message.Message) > 0 { + log.Infof("API response: %s", message.Message) + } + + return nil +} + +func (d *DNSProvider) getTXTRecord(domain, name string) (*Record, error) { + target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) + + // Get exiting values for the TXT records + // Needed to create challenges for both wildcard and base name domains + req, err := d.newRequest(http.MethodGet, target, nil) + if err != nil { + return nil, err + } + + txtRecord := &Record{} + err = d.do(req, txtRecord) + if err != nil { + return nil, fmt.Errorf("unable to get TXT records for domain %s and name %s: %v", domain, name, err) + } + + return txtRecord, nil +} + +func (d *DNSProvider) deleteTXTRecord(domain string, name string) error { + target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) + + req, err := d.newRequest(http.MethodDelete, target, nil) + if err != nil { + return err + } + + message := &apiResponse{} + err = d.do(req, message) + if err != nil { + return fmt.Errorf("unable to delete TXT record for domain %s and name %s: %v", domain, name, err) + } + + if message != nil && len(message.Message) > 0 { + log.Infof("API response: %s", message.Message) + } + + return nil +} + func (d *DNSProvider) newRequest(method, resource string, body interface{}) (*http.Request, error) { u := fmt.Sprintf("%s/%s", d.config.BaseURL, resource) diff --git a/providers/dns/gandiv5/gandiv5.go b/providers/dns/gandiv5/gandiv5.go index 0962d159..2cf94ba7 100644 --- a/providers/dns/gandiv5/gandiv5.go +++ b/providers/dns/gandiv5/gandiv5.go @@ -1,5 +1,4 @@ -// Package gandiv5 implements a DNS provider for solving the DNS-01 -// challenge using Gandi LiveDNS api. +// Package gandiv5 implements a DNS provider for solving the DNS-01 challenge using Gandi LiveDNS api. package gandiv5 import ( @@ -10,8 +9,7 @@ import ( "sync" "time" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/log" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -23,10 +21,6 @@ const ( minTTL = 300 ) -// findZoneByFqdn determines the DNS zone of an fqdn. -// It is overridden during tests. -var findZoneByFqdn = acme.FindZoneByFqdn - // inProgressInfo contains information about an in-progress challenge type inProgressInfo struct { fieldName string @@ -62,6 +56,8 @@ type DNSProvider struct { config *Config inProgressFQDNs map[string]inProgressInfo inProgressMu sync.Mutex + // findZoneByFqdn determines the DNS zone of an fqdn. It is overridden during tests. + findZoneByFqdn func(fqdn string) (string, error) } // NewDNSProvider returns a DNSProvider instance configured for Gandi. @@ -78,16 +74,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Gandi. -// Deprecated -func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Gandi. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -109,15 +95,16 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{ config: config, inProgressFQDNs: make(map[string]inProgressInfo), + findZoneByFqdn: dns01.FindZoneByFqdn, }, nil } // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone - authZone, err := findZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := d.findZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("gandiv5: findZoneByFqdn failure: %v", err) } @@ -135,7 +122,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { defer d.inProgressMu.Unlock() // add TXT record into authZone - err = d.addTXTRecord(acme.UnFqdn(authZone), name, value, d.config.TTL) + err = d.addTXTRecord(dns01.UnFqdn(authZone), name, value, d.config.TTL) if err != nil { return err } @@ -150,7 +137,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve authZone d.inProgressMu.Lock() @@ -165,7 +152,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { delete(d.inProgressFQDNs, fqdn) // delete TXT record from authZone - err := d.deleteTXTRecord(acme.UnFqdn(authZone), fieldName) + err := d.deleteTXTRecord(dns01.UnFqdn(authZone), fieldName) if err != nil { return fmt.Errorf("gandiv5: %v", err) } @@ -178,79 +165,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// functions to perform API actions - -func (d *DNSProvider) addTXTRecord(domain string, name string, value string, ttl int) error { - // Get exiting values for the TXT records - // Needed to create challenges for both wildcard and base name domains - txtRecord, err := d.getTXTRecord(domain, name) - if err != nil { - return err - } - - values := []string{value} - if len(txtRecord.RRSetValues) > 0 { - values = append(values, txtRecord.RRSetValues...) - } - - target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) - - newRecord := &Record{RRSetTTL: ttl, RRSetValues: values} - req, err := d.newRequest(http.MethodPut, target, newRecord) - if err != nil { - return err - } - - message := &apiResponse{} - err = d.do(req, message) - if err != nil { - return fmt.Errorf("unable to create TXT record for domain %s and name %s: %v", domain, name, err) - } - - if message != nil && len(message.Message) > 0 { - log.Infof("API response: %s", message.Message) - } - - return nil -} - -func (d *DNSProvider) getTXTRecord(domain, name string) (*Record, error) { - target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) - - // Get exiting values for the TXT records - // Needed to create challenges for both wildcard and base name domains - req, err := d.newRequest(http.MethodGet, target, nil) - if err != nil { - return nil, err - } - - txtRecord := &Record{} - err = d.do(req, txtRecord) - if err != nil { - return nil, fmt.Errorf("unable to get TXT records for domain %s and name %s: %v", domain, name, err) - } - - return txtRecord, nil -} - -func (d *DNSProvider) deleteTXTRecord(domain string, name string) error { - target := fmt.Sprintf("domains/%s/records/%s/TXT", domain, name) - - req, err := d.newRequest(http.MethodDelete, target, nil) - if err != nil { - return err - } - - message := &apiResponse{} - err = d.do(req, message) - if err != nil { - return fmt.Errorf("unable to delete TXT record for domain %s and name %s: %v", domain, name, err) - } - - if message != nil && len(message.Message) > 0 { - log.Infof("API response: %s", message.Message) - } - - return nil -} diff --git a/providers/dns/gandiv5/gandiv5_test.go b/providers/dns/gandiv5/gandiv5_test.go index cf0172aa..2126d31f 100644 --- a/providers/dns/gandiv5/gandiv5_test.go +++ b/providers/dns/gandiv5/gandiv5_test.go @@ -95,10 +95,23 @@ func TestNewDNSProviderConfig(t *testing.T) { // TestDNSProvider runs Present and CleanUp against a fake Gandi RPC // Server, whose responses are predetermined for particular requests. func TestDNSProvider(t *testing.T) { + // serverResponses is the JSON Request->Response map used by the + // fake JSON server. + var serverResponses = map[string]map[string]string{ + http.MethodGet: { + ``: `{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`, + }, + http.MethodPut: { + `{"rrset_ttl":300,"rrset_values":["TOKEN"]}`: `{"message": "Zone Record Created"}`, + }, + http.MethodDelete: { + ``: ``, + }, + } + fakeKeyAuth := "XXXX" - regexpToken, err := regexp.Compile(`"rrset_values":\[".+"\]`) - require.NoError(t, err) + regexpToken := regexp.MustCompile(`"rrset_values":\[".+"\]`) // start fake RPC server handler := http.NewServeMux() @@ -146,7 +159,7 @@ func TestDNSProvider(t *testing.T) { defer server.Close() // define function to override findZoneByFqdn with - fakeFindZoneByFqdn := func(fqdn string, nameserver []string) (string, error) { + fakeFindZoneByFqdn := func(fqdn string) (string, error) { return "example.com.", nil } @@ -158,11 +171,11 @@ func TestDNSProvider(t *testing.T) { require.NoError(t, err) // override findZoneByFqdn function - savedFindZoneByFqdn := findZoneByFqdn + savedFindZoneByFqdn := provider.findZoneByFqdn defer func() { - findZoneByFqdn = savedFindZoneByFqdn + provider.findZoneByFqdn = savedFindZoneByFqdn }() - findZoneByFqdn = fakeFindZoneByFqdn + provider.findZoneByFqdn = fakeFindZoneByFqdn // run Present err = provider.Present("abc.def.example.com", "", fakeKeyAuth) @@ -172,17 +185,3 @@ func TestDNSProvider(t *testing.T) { err = provider.CleanUp("abc.def.example.com", "", fakeKeyAuth) require.NoError(t, err) } - -// serverResponses is the JSON Request->Response map used by the -// fake JSON server. -var serverResponses = map[string]map[string]string{ - http.MethodGet: { - ``: `{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`, - }, - http.MethodPut: { - `{"rrset_ttl":300,"rrset_values":["TOKEN"]}`: `{"message": "Zone Record Created"}`, - }, - http.MethodDelete: { - ``: ``, - }, -} diff --git a/providers/dns/gcloud/googlecloud.go b/providers/dns/gcloud/googlecloud.go index f799461b..13c7a3aa 100644 --- a/providers/dns/gcloud/googlecloud.go +++ b/providers/dns/gcloud/googlecloud.go @@ -1,5 +1,4 @@ -// Package gcloud implements a DNS provider for solving the DNS-01 -// challenge using Google Cloud DNS. +// Package gcloud implements a DNS provider for solving the DNS-01 challenge using Google Cloud DNS. package gcloud import ( @@ -11,7 +10,7 @@ import ( "os" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" "golang.org/x/net/context" "golang.org/x/oauth2/google" @@ -30,7 +29,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("GCE_TTL", 120), + TTL: env.GetOrDefaultInt("GCE_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("GCE_PROPAGATION_TIMEOUT", 180*time.Second), PollingInterval: env.GetOrDefaultSecond("GCE_POLLING_INTERVAL", 5*time.Second), } @@ -124,7 +123,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(domain) if err != nil { @@ -178,7 +177,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(domain) if err != nil { @@ -209,7 +208,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // getHostedZone returns the managed-zone func (d *DNSProvider) getHostedZone(domain string) (string, error) { - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return "", err } diff --git a/providers/dns/glesys/client.go b/providers/dns/glesys/client.go index a7e8cf8e..1ab552cd 100644 --- a/providers/dns/glesys/client.go +++ b/providers/dns/glesys/client.go @@ -1,5 +1,14 @@ package glesys +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/xenolf/lego/log" +) + // types for JSON method calls, parameters, and responses type addRecordRequest struct { @@ -22,3 +31,61 @@ type responseStruct struct { Record deleteRecordRequest `json:"record"` } `json:"response"` } + +func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) { + response, err := d.sendRequest(http.MethodPost, "addrecord", addRecordRequest{ + DomainName: domain, + Host: name, + Type: "TXT", + Data: value, + TTL: ttl, + }) + + if response != nil && response.Response.Status.Code == http.StatusOK { + log.Infof("[%s]: Successfully created record id %d", fqdn, response.Response.Record.RecordID) + return response.Response.Record.RecordID, nil + } + return 0, err +} + +func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error { + response, err := d.sendRequest(http.MethodPost, "deleterecord", deleteRecordRequest{ + RecordID: recordid, + }) + if response != nil && response.Response.Status.Code == 200 { + log.Infof("[%s]: Successfully deleted record id %d", fqdn, recordid) + } + return err +} + +func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) { + url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(d.config.APIUser, d.config.APIKey) + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode) + } + + var response responseStruct + err = json.NewDecoder(resp.Body).Decode(&response) + + return &response, err +} diff --git a/providers/dns/glesys/glesys.go b/providers/dns/glesys/glesys.go index 4aab8744..98667d8d 100644 --- a/providers/dns/glesys/glesys.go +++ b/providers/dns/glesys/glesys.go @@ -1,10 +1,7 @@ -// Package glesys implements a DNS provider for solving the DNS-01 -// challenge using GleSYS api. +// Package glesys implements a DNS provider for solving the DNS-01 challenge using GleSYS api. package glesys import ( - "bytes" - "encoding/json" "errors" "fmt" "net/http" @@ -12,8 +9,7 @@ import ( "sync" "time" - "github.com/xenolf/lego/acme" - "github.com/xenolf/lego/log" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -72,17 +68,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for GleSYS. -// Deprecated -func NewDNSProviderCredentials(apiUser string, apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIUser = apiUser - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for GleSYS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -105,10 +90,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) // find authZone - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("glesys: findZoneByFqdn failure: %v", err) } @@ -126,7 +111,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { defer d.inProgressMu.Unlock() // add TXT record into authZone - recordID, err := d.addTXTRecord(domain, acme.UnFqdn(authZone), name, value, d.config.TTL) + recordID, err := d.addTXTRecord(domain, dns01.UnFqdn(authZone), name, value, d.config.TTL) if err != nil { return err } @@ -138,7 +123,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) // acquire lock and retrieve authZone d.inProgressMu.Lock() @@ -161,63 +146,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -func (d *DNSProvider) sendRequest(method string, resource string, payload interface{}) (*responseStruct, error) { - url := fmt.Sprintf("%s/%s", defaultBaseURL, resource) - - body, err := json.Marshal(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(method, url, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(d.config.APIUser, d.config.APIKey) - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("request failed with HTTP status code %d", resp.StatusCode) - } - - var response responseStruct - err = json.NewDecoder(resp.Body).Decode(&response) - - return &response, err -} - -// functions to perform API actions - -func (d *DNSProvider) addTXTRecord(fqdn string, domain string, name string, value string, ttl int) (int, error) { - response, err := d.sendRequest(http.MethodPost, "addrecord", addRecordRequest{ - DomainName: domain, - Host: name, - Type: "TXT", - Data: value, - TTL: ttl, - }) - - if response != nil && response.Response.Status.Code == http.StatusOK { - log.Infof("[%s]: Successfully created record id %d", fqdn, response.Response.Record.RecordID) - return response.Response.Record.RecordID, nil - } - return 0, err -} - -func (d *DNSProvider) deleteTXTRecord(fqdn string, recordid int) error { - response, err := d.sendRequest(http.MethodPost, "deleterecord", deleteRecordRequest{ - RecordID: recordid, - }) - if response != nil && response.Response.Status.Code == 200 { - log.Infof("[%s]: Successfully deleted record id %d", fqdn, recordid) - } - return err -} diff --git a/providers/dns/godaddy/client.go b/providers/dns/godaddy/client.go new file mode 100644 index 00000000..212b8e69 --- /dev/null +++ b/providers/dns/godaddy/client.go @@ -0,0 +1,53 @@ +package godaddy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// DNSRecord a DNS record +type DNSRecord struct { + Type string `json:"type"` + Name string `json:"name"` + Data string `json:"data"` + Priority int `json:"priority,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +func (d *DNSProvider) updateRecords(records []DNSRecord, domainZone string, recordName string) error { + body, err := json.Marshal(records) + if err != nil { + return err + } + + var resp *http.Response + resp, err = d.makeRequest(http.MethodPut, fmt.Sprintf("/v1/domains/%s/records/TXT/%s", domainZone, recordName), bytes.NewReader(body)) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes)) + } + return nil +} + +func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret)) + + return d.config.HTTPClient.Do(req) +} diff --git a/providers/dns/godaddy/godaddy.go b/providers/dns/godaddy/godaddy.go index 8aebde65..dd054ab5 100644 --- a/providers/dns/godaddy/godaddy.go +++ b/providers/dns/godaddy/godaddy.go @@ -2,17 +2,13 @@ package godaddy import ( - "bytes" - "encoding/json" "errors" "fmt" - "io" - "io/ioutil" "net/http" "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -28,6 +24,7 @@ type Config struct { APISecret string PropagationTimeout time.Duration PollingInterval time.Duration + SequenceInterval time.Duration TTL int HTTPClient *http.Client } @@ -38,6 +35,7 @@ func NewDefaultConfig() *Config { TTL: env.GetOrDefaultInt("GODADDY_TTL", minTTL), PropagationTimeout: env.GetOrDefaultSecond("GODADDY_PROPAGATION_TIMEOUT", 120*time.Second), PollingInterval: env.GetOrDefaultSecond("GODADDY_POLLING_INTERVAL", 2*time.Second), + SequenceInterval: env.GetOrDefaultSecond("GODADDY_SEQUENCE_INTERVAL", dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("GODADDY_HTTP_TIMEOUT", 30*time.Second), }, @@ -65,17 +63,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for godaddy. -// Deprecated -func NewDNSProviderCredentials(apiKey, apiSecret string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - config.APISecret = apiSecret - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for godaddy. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -99,17 +86,9 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } -func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) - if idx := strings.Index(name, "."+domain); idx != -1 { - return name[:idx] - } - return name -} - // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) domainZone, err := d.getZone(fqdn) if err != nil { return err @@ -128,30 +107,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return d.updateRecords(rec, domainZone, recordName) } -func (d *DNSProvider) updateRecords(records []DNSRecord, domainZone string, recordName string) error { - body, err := json.Marshal(records) - if err != nil { - return err - } - - var resp *http.Response - resp, err = d.makeRequest(http.MethodPut, fmt.Sprintf("/v1/domains/%s/records/TXT/%s", domainZone, recordName), bytes.NewReader(body)) - if err != nil { - return err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf("could not create record %v; Status: %v; Body: %s", string(body), resp.StatusCode, string(bodyBytes)) - } - return nil -} - // CleanUp sets null value in the TXT DNS record as GoDaddy has no proper DELETE record method func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) domainZone, err := d.getZone(fqdn) if err != nil { return err @@ -169,33 +127,25 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return d.updateRecords(rec, domainZone, recordName) } +// Sequential All DNS challenges for this provider will be resolved sequentially. +// Returns the interval between each iteration. +func (d *DNSProvider) Sequential() time.Duration { + return d.config.SequenceInterval +} + +func (d *DNSProvider) extractRecordName(fqdn, domain string) string { + name := dns01.UnFqdn(fqdn) + if idx := strings.Index(name, "."+domain); idx != -1 { + return name[:idx] + } + return name +} + func (d *DNSProvider) getZone(fqdn string) (string, error) { - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } - return acme.UnFqdn(authZone), nil -} - -func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body) - if err != nil { - return nil, err - } - - req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret)) - - return d.config.HTTPClient.Do(req) -} - -// DNSRecord a DNS record -type DNSRecord struct { - Type string `json:"type"` - Name string `json:"name"` - Data string `json:"data"` - Priority int `json:"priority,omitempty"` - TTL int `json:"ttl,omitempty"` + return dns01.UnFqdn(authZone), nil } diff --git a/providers/dns/hostingde/client.go b/providers/dns/hostingde/client.go index 0bee9ccc..6f2c9635 100644 --- a/providers/dns/hostingde/client.go +++ b/providers/dns/hostingde/client.go @@ -1,5 +1,16 @@ package hostingde +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" + // RecordsAddRequest represents a DNS record to add type RecordsAddRequest struct { Name string `json:"name"` @@ -89,3 +100,44 @@ type ZoneUpdateRequest struct { RecordsToAdd []RecordsAddRequest `json:"recordsToAdd"` RecordsToDelete []RecordsDeleteRequest `json:"recordsToDelete"` } + +func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) { + body, err := json.Marshal(updateRequest) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, defaultBaseURL+"/zoneUpdate", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error querying API: %v", err) + } + + defer resp.Body.Close() + + content, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.New(toUnreadableBodyMessage(req, content)) + } + + // Everything looks good; but we'll need the ID later to delete the record + updateResponse := &ZoneUpdateResponse{} + err = json.Unmarshal(content, updateResponse) + if err != nil { + return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content)) + } + + if updateResponse.Status != "success" && updateResponse.Status != "pending" { + return updateResponse, errors.New(toUnreadableBodyMessage(req, content)) + } + + return updateResponse, nil +} + +func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { + return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) +} diff --git a/providers/dns/hostingde/hostingde.go b/providers/dns/hostingde/hostingde.go index 7a64b1b8..738771bf 100644 --- a/providers/dns/hostingde/hostingde.go +++ b/providers/dns/hostingde/hostingde.go @@ -1,23 +1,17 @@ -// Package hostingde implements a DNS provider for solving the DNS-01 -// challenge using hosting.de. +// Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de. package hostingde import ( - "bytes" - "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" "sync" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) -const defaultBaseURL = "https://secure.hosting.de/api/dns/v1/json" - // Config is used to configure the creation of the DNSProvider type Config struct { APIKey string @@ -31,7 +25,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("HOSTINGDE_TTL", 120), + TTL: env.GetOrDefaultInt("HOSTINGDE_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("HOSTINGDE_PROPAGATION_TIMEOUT", 2*time.Minute), PollingInterval: env.GetOrDefaultSecond("HOSTINGDE_POLLING_INTERVAL", 2*time.Second), HTTPClient: &http.Client{ @@ -91,11 +85,11 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) rec := []RecordsAddRequest{{ Type: "TXT", - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), Content: value, TTL: d.config.TTL, }} @@ -114,7 +108,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { } for _, record := range resp.Response.Records { - if record.Name == acme.UnFqdn(fqdn) && record.Content == fmt.Sprintf(`"%s"`, value) { + if record.Name == dns01.UnFqdn(fqdn) && record.Content == fmt.Sprintf(`"%s"`, value) { d.recordIDsMu.Lock() d.recordIDs[fqdn] = record.ID d.recordIDsMu.Unlock() @@ -130,7 +124,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() @@ -142,7 +136,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { rec := []RecordsDeleteRequest{{ Type: "TXT", - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), Content: value, ID: recordID, }} @@ -166,44 +160,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { } return nil } - -func (d *DNSProvider) updateZone(updateRequest ZoneUpdateRequest) (*ZoneUpdateResponse, error) { - body, err := json.Marshal(updateRequest) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, defaultBaseURL+"/zoneUpdate", bytes.NewReader(body)) - if err != nil { - return nil, err - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error querying API: %v", err) - } - - defer resp.Body.Close() - - content, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.New(toUnreadableBodyMessage(req, content)) - } - - // Everything looks good; but we'll need the ID later to delete the record - updateResponse := &ZoneUpdateResponse{} - err = json.Unmarshal(content, updateResponse) - if err != nil { - return nil, fmt.Errorf("%v: %s", err, toUnreadableBodyMessage(req, content)) - } - - if updateResponse.Status != "success" && updateResponse.Status != "pending" { - return updateResponse, errors.New(toUnreadableBodyMessage(req, content)) - } - - return updateResponse, nil -} - -func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string { - return fmt.Sprintf("the request %s sent a response with a body which is an invalid format: %q", req.URL, string(rawBody)) -} diff --git a/providers/dns/httpreq/httpreq.go b/providers/dns/httpreq/httpreq.go index cf957d41..f6a1fe0c 100644 --- a/providers/dns/httpreq/httpreq.go +++ b/providers/dns/httpreq/httpreq.go @@ -12,7 +12,7 @@ import ( "os" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -41,8 +41,8 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - PropagationTimeout: env.GetOrDefaultSecond("HTTPREQ_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("HTTPREQ_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("HTTPREQ_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("HTTPREQ_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("HTTPREQ_HTTP_TIMEOUT", 30*time.Second), }, @@ -109,7 +109,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return nil } - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) msg := &message{ FQDN: fqdn, Value: value, @@ -138,7 +138,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return nil } - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) msg := &message{ FQDN: fqdn, Value: value, diff --git a/providers/dns/iij/iij.go b/providers/dns/iij/iij.go index fc09f863..9372a9c7 100644 --- a/providers/dns/iij/iij.go +++ b/providers/dns/iij/iij.go @@ -9,7 +9,7 @@ import ( "github.com/iij/doapi" "github.com/iij/doapi/protocol" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -73,7 +73,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - _, value, _ := acme.DNS01Record(domain, keyAuth) + _, value := dns01.GetRecord(domain, keyAuth) err := d.addTxtRecord(domain, value) if err != nil { @@ -84,7 +84,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - _, value, _ := acme.DNS01Record(domain, keyAuth) + _, value := dns01.GetRecord(domain, keyAuth) err := d.deleteTxtRecord(domain, value) if err != nil { diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go index ba6c8cbe..dd99a5b1 100644 --- a/providers/dns/inwx/inwx.go +++ b/providers/dns/inwx/inwx.go @@ -7,7 +7,7 @@ import ( "time" "github.com/smueller18/goinwx" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) @@ -25,8 +25,8 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - PropagationTimeout: env.GetOrDefaultSecond("INWX_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("INWX_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("INWX_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("INWX_POLLING_INTERVAL", dns01.DefaultPollingInterval), TTL: env.GetOrDefaultInt("INWX_TTL", 300), Sandbox: env.GetOrDefaultBool("INWX_SANDBOX", false), } @@ -75,9 +75,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("inwx: %v", err) } @@ -95,8 +95,8 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { }() var request = &goinwx.NameserverRecordRequest{ - Domain: acme.UnFqdn(authZone), - Name: acme.UnFqdn(fqdn), + Domain: dns01.UnFqdn(authZone), + Name: dns01.UnFqdn(fqdn), Type: "TXT", Content: value, Ttl: d.config.TTL, @@ -104,9 +104,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { _, err = d.client.Nameservers.CreateRecord(request) if err != nil { - switch err.(type) { + switch er := err.(type) { case *goinwx.ErrorResponse: - if err.(*goinwx.ErrorResponse).Message == "Object exists" { + if er.Message == "Object exists" { return nil } return fmt.Errorf("inwx: %v", err) @@ -120,9 +120,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("inwx: %v", err) } @@ -140,8 +140,8 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { }() response, err := d.client.Nameservers.Info(&goinwx.NameserverInfoRequest{ - Domain: acme.UnFqdn(authZone), - Name: acme.UnFqdn(fqdn), + Domain: dns01.UnFqdn(authZone), + Name: dns01.UnFqdn(fqdn), Type: "TXT", }) if err != nil { diff --git a/providers/dns/lightsail/lightsail.go b/providers/dns/lightsail/lightsail.go index 3b3fe678..44a70bd1 100644 --- a/providers/dns/lightsail/lightsail.go +++ b/providers/dns/lightsail/lightsail.go @@ -1,5 +1,4 @@ -// Package lightsail implements a DNS provider for solving the DNS-01 challenge -// using AWS Lightsail DNS. +// Package lightsail implements a DNS provider for solving the DNS-01 challenge using AWS Lightsail DNS. package lightsail import ( @@ -13,7 +12,7 @@ import ( "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lightsail" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -54,8 +53,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ DNSZone: env.GetOrFile("DNS_ZONE"), - PropagationTimeout: env.GetOrDefaultSecond("LIGHTSAIL_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("LIGHTSAIL_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("LIGHTSAIL_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("LIGHTSAIL_POLLING_INTERVAL", dns01.DefaultPollingInterval), Region: env.GetOrDefaultString("LIGHTSAIL_REGION", "us-east-1"), } } @@ -105,7 +104,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.newTxtRecord(fqdn, `"`+value+`"`) if err != nil { @@ -116,7 +115,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) params := &lightsail.DeleteDomainEntryInput{ DomainName: aws.String(d.config.DNSZone), diff --git a/providers/dns/linode/linode.go b/providers/dns/linode/linode.go index 09b92e99..3a129e8c 100644 --- a/providers/dns/linode/linode.go +++ b/providers/dns/linode/linode.go @@ -1,5 +1,4 @@ -// Package linode implements a DNS provider for solving the DNS-01 challenge -// using Linode DNS. +// Package linode implements a DNS provider for solving the DNS-01 challenge using Linode DNS. package linode import ( @@ -9,7 +8,7 @@ import ( "time" "github.com/timewasted/linode/dns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -59,16 +58,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Linode. -// Deprecated -func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Linode. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -108,13 +97,13 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { return err } - if _, err = d.client.CreateDomainResourceTXT(zone.domainID, acme.UnFqdn(fqdn), value, d.config.TTL); err != nil { + if _, err = d.client.CreateDomainResourceTXT(zone.domainID, dns01.UnFqdn(fqdn), value, d.config.TTL); err != nil { return err } @@ -123,7 +112,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { return err @@ -155,7 +144,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { // Lookup the zone that handles the specified FQDN. - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } @@ -163,7 +152,7 @@ func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { resourceName := strings.TrimSuffix(fqdn, "."+authZone) // Query the authority zone. - domain, err := d.client.GetDomain(acme.UnFqdn(authZone)) + domain, err := d.client.GetDomain(dns01.UnFqdn(authZone)) if err != nil { return nil, err } diff --git a/providers/dns/linodev4/linodev4.go b/providers/dns/linodev4/linodev4.go index 97903e9c..eee0ddc9 100644 --- a/providers/dns/linodev4/linodev4.go +++ b/providers/dns/linodev4/linodev4.go @@ -1,5 +1,4 @@ -// Package linodev4 implements a DNS provider for solving the DNS-01 challenge -// using Linode DNS and Linode's APIv4 +// Package linodev4 implements a DNS provider for solving the DNS-01 challenge using Linode DNS and Linode's APIv4 package linodev4 import ( @@ -12,7 +11,7 @@ import ( "time" "github.com/linode/linodego" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" "golang.org/x/oauth2" ) @@ -115,14 +114,14 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { return err } createOpts := linodego.DomainRecordCreateOptions{ - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), Target: value, TTLSec: d.config.TTL, Type: linodego.RecordTypeTXT, @@ -134,7 +133,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZoneInfo(fqdn) if err != nil { @@ -163,13 +162,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) getHostedZoneInfo(fqdn string) (*hostedZoneInfo, error) { // Lookup the zone that handles the specified FQDN. - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return nil, err } // Query the authority zone. - data, err := json.Marshal(map[string]string{"domain": acme.UnFqdn(authZone)}) + data, err := json.Marshal(map[string]string{"domain": dns01.UnFqdn(authZone)}) if err != nil { return nil, err } diff --git a/providers/dns/mydnsjp/client.go b/providers/dns/mydnsjp/client.go new file mode 100644 index 00000000..d8fc5841 --- /dev/null +++ b/providers/dns/mydnsjp/client.go @@ -0,0 +1,52 @@ +package mydnsjp + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +func (d *DNSProvider) doRequest(domain, value string, cmd string) error { + req, err := d.buildRequest(domain, value, cmd) + if err != nil { + return err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return fmt.Errorf("error querying API: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + var content []byte + content, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + return fmt.Errorf("request %s failed [status code %d]: %s", req.URL, resp.StatusCode, string(content)) + } + + return nil +} + +func (d *DNSProvider) buildRequest(domain, value string, cmd string) (*http.Request, error) { + params := url.Values{} + params.Set("CERTBOT_DOMAIN", domain) + params.Set("CERTBOT_VALIDATION", value) + params.Set("EDIT_CMD", cmd) + + req, err := http.NewRequest(http.MethodPost, defaultBaseURL, strings.NewReader(params.Encode())) + if err != nil { + return nil, fmt.Errorf("invalid request: %v", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(d.config.MasterID, d.config.Password) + + return req, nil +} diff --git a/providers/dns/mydnsjp/mydnsjp.go b/providers/dns/mydnsjp/mydnsjp.go index 05cad568..fd5d7814 100644 --- a/providers/dns/mydnsjp/mydnsjp.go +++ b/providers/dns/mydnsjp/mydnsjp.go @@ -1,17 +1,13 @@ -// Package mydnsjp implements a DNS provider for solving the DNS-01 -// challenge using MyDNS.jp. +// Package mydnsjp implements a DNS provider for solving the DNS-01 challenge using MyDNS.jp. package mydnsjp import ( "errors" "fmt" - "io/ioutil" "net/http" - "net/url" - "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -78,7 +74,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - _, value, _ := acme.DNS01Record(domain, keyAuth) + _, value := dns01.GetRecord(domain, keyAuth) err := d.doRequest(domain, value, "REGIST") if err != nil { return fmt.Errorf("mydnsjp: %v", err) @@ -88,53 +84,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - _, value, _ := acme.DNS01Record(domain, keyAuth) + _, value := dns01.GetRecord(domain, keyAuth) err := d.doRequest(domain, value, "DELETE") if err != nil { return fmt.Errorf("mydnsjp: %v", err) } return nil } - -func (d *DNSProvider) doRequest(domain, value string, cmd string) error { - req, err := d.buildRequest(domain, value, cmd) - if err != nil { - return err - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return fmt.Errorf("error querying API: %v", err) - } - - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - var content []byte - content, err = ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - - return fmt.Errorf("request %s failed [status code %d]: %s", req.URL, resp.StatusCode, string(content)) - } - - return nil -} - -func (d *DNSProvider) buildRequest(domain, value string, cmd string) (*http.Request, error) { - params := url.Values{} - params.Set("CERTBOT_DOMAIN", domain) - params.Set("CERTBOT_VALIDATION", value) - params.Set("EDIT_CMD", cmd) - - req, err := http.NewRequest(http.MethodPost, defaultBaseURL, strings.NewReader(params.Encode())) - if err != nil { - return nil, fmt.Errorf("invalid request: %v", err) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(d.config.MasterID, d.config.Password) - - return req, nil -} diff --git a/providers/dns/mydnsjp/mydnsjp_test.go b/providers/dns/mydnsjp/mydnsjp_test.go index 23a71e33..ff511411 100644 --- a/providers/dns/mydnsjp/mydnsjp_test.go +++ b/providers/dns/mydnsjp/mydnsjp_test.go @@ -4,10 +4,9 @@ import ( "testing" "time" - "github.com/xenolf/lego/platform/tester" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/xenolf/lego/platform/tester" ) var envTest = tester.NewEnvTest("MYDNSJP_MASTER_ID", "MYDNSJP_PASSWORD"). diff --git a/providers/dns/namecheap/client.go b/providers/dns/namecheap/client.go index d52f6550..85bec85f 100644 --- a/providers/dns/namecheap/client.go +++ b/providers/dns/namecheap/client.go @@ -20,8 +20,8 @@ type Record struct { TTL string `xml:",attr"` } -// apierror describes an error record in a namecheap API response. -type apierror struct { +// apiError describes an error record in a namecheap API response. +type apiError struct { Number int `xml:",attr"` Description string `xml:",innerxml"` } @@ -29,7 +29,7 @@ type apierror struct { type setHostsResponse struct { XMLName xml.Name `xml:"ApiResponse"` Status string `xml:"Status,attr"` - Errors []apierror `xml:"Errors>Error"` + Errors []apiError `xml:"Errors>Error"` Result struct { IsSuccess string `xml:",attr"` } `xml:"CommandResponse>DomainDNSSetHostsResult"` @@ -38,13 +38,13 @@ type setHostsResponse struct { type getHostsResponse struct { XMLName xml.Name `xml:"ApiResponse"` Status string `xml:"Status,attr"` - Errors []apierror `xml:"Errors>Error"` + Errors []apiError `xml:"Errors>Error"` Hosts []Record `xml:"CommandResponse>DomainDNSGetHostsResult>host"` } type getTldsResponse struct { XMLName xml.Name `xml:"ApiResponse"` - Errors []apierror `xml:"Errors>Error"` + Errors []apiError `xml:"Errors>Error"` Result []struct { Name string `xml:",attr"` } `xml:"CommandResponse>Tlds>Tld"` diff --git a/providers/dns/namecheap/namecheap.go b/providers/dns/namecheap/namecheap.go index 731699ac..2bcf9198 100644 --- a/providers/dns/namecheap/namecheap.go +++ b/providers/dns/namecheap/namecheap.go @@ -1,5 +1,4 @@ -// Package namecheap implements a DNS provider for solving the DNS-01 -// challenge using namecheap DNS. +// Package namecheap implements a DNS provider for solving the DNS-01 challenge using namecheap DNS. package namecheap import ( @@ -11,7 +10,7 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) @@ -64,7 +63,7 @@ func NewDefaultConfig() *Config { return &Config{ BaseURL: defaultBaseURL, Debug: env.GetOrDefaultBool("NAMECHEAP_DEBUG", false), - TTL: env.GetOrDefaultInt("NAMECHEAP_TTL", 120), + TTL: env.GetOrDefaultInt("NAMECHEAP_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("NAMECHEAP_PROPAGATION_TIMEOUT", 60*time.Minute), PollingInterval: env.GetOrDefaultSecond("NAMECHEAP_POLLING_INTERVAL", 15*time.Second), HTTPClient: &http.Client{ @@ -95,17 +94,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for namecheap. -// Deprecated -func NewDNSProviderCredentials(apiUser, apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIUser = apiUser - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for namecheap. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -233,7 +221,7 @@ func getClientIP(client *http.Client, debug bool) (addr string, err error) { // newChallenge builds a challenge record from a domain name, a challenge // authentication key, and a map of available TLDs. func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, error) { - domain = acme.UnFqdn(domain) + domain = dns01.UnFqdn(domain) parts := strings.Split(domain, ".") // Find the longest matching TLD. @@ -256,7 +244,7 @@ func newChallenge(domain, keyAuth string, tlds map[string]string) (*challenge, e host = strings.Join(parts[:longest-1], ".") } - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) return &challenge{ domain: domain, diff --git a/providers/dns/namecheap/namecheap_test.go b/providers/dns/namecheap/namecheap_test.go index f3fd0705..ec084831 100644 --- a/providers/dns/namecheap/namecheap_test.go +++ b/providers/dns/namecheap/namecheap_test.go @@ -12,22 +12,22 @@ import ( "github.com/stretchr/testify/require" ) -var ( +const ( envTestUser = "foo" envTestKey = "bar" envTestClientIP = "10.0.0.1" - - tlds = map[string]string{ - "com.au": "com.au", - "com": "com", - "co.uk": "co.uk", - "uk": "uk", - "edu": "edu", - "co.com": "co.com", - "za.com": "za.com", - } ) +var tldsMock = map[string]string{ + "com.au": "com.au", + "com": "com", + "co.uk": "co.uk", + "uk": "uk", + "edu": "edu", + "co.com": "co.com", + "za.com": "za.com", +} + func TestDNSProvider_getHosts(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { @@ -36,7 +36,7 @@ func TestDNSProvider_getHosts(t *testing.T) { provider := mockDNSProvider(mock.URL) - ch, err := newChallenge(test.domain, "", tlds) + ch, err := newChallenge(test.domain, "", tldsMock) require.NoError(t, err) hosts, err := provider.getHosts(ch.sld, ch.tld) @@ -77,7 +77,7 @@ func TestDNSProvider_setHosts(t *testing.T) { prov := mockDNSProvider(mock.URL) - ch, err := newChallenge(test.domain, "", tlds) + ch, err := newChallenge(test.domain, "", tldsMock) require.NoError(t, err) hosts, err := prov.getHosts(ch.sld, ch.tld) @@ -160,7 +160,7 @@ func TestDomainSplit(t *testing.T) { test := test t.Run(test.domain, func(t *testing.T) { valid := true - ch, err := newChallenge(test.domain, "", tlds) + ch, err := newChallenge(test.domain, "", tldsMock) if err != nil { valid = false } @@ -188,7 +188,7 @@ func assertEq(t *testing.T, variable, got, want string) { } func assertHdr(tc *testCase, t *testing.T, values *url.Values) { - ch, _ := newChallenge(tc.domain, "", tlds) + ch, _ := newChallenge(tc.domain, "", tldsMock) assertEq(t, "ApiUser", values.Get("ApiUser"), envTestUser) assertEq(t, "ApiKey", values.Get("ApiKey"), envTestKey) @@ -296,7 +296,7 @@ var testCases = []testCase{ }, } -var responseGetHostsSuccess1 = ` +const responseGetHostsSuccess1 = ` @@ -316,7 +316,7 @@ var responseGetHostsSuccess1 = ` 3.338 ` -var responseSetHostsSuccess1 = ` +const responseSetHostsSuccess1 = ` @@ -331,7 +331,7 @@ var responseSetHostsSuccess1 = ` 2.347 ` -var responseGetHostsSuccess2 = ` +const responseGetHostsSuccess2 = ` @@ -347,7 +347,7 @@ var responseGetHostsSuccess2 = ` 3.338 ` -var responseSetHostsSuccess2 = ` +const responseSetHostsSuccess2 = ` @@ -362,7 +362,7 @@ var responseSetHostsSuccess2 = ` 2.347 ` -var responseGetHostsErrorBadAPIKey1 = ` +const responseGetHostsErrorBadAPIKey1 = ` API Key is invalid or API access has not been enabled @@ -374,7 +374,7 @@ var responseGetHostsErrorBadAPIKey1 = ` 0 ` -var responseGetTlds = ` +const responseGetTlds = ` diff --git a/providers/dns/namedotcom/namedotcom.go b/providers/dns/namedotcom/namedotcom.go index 2cd2b26d..4f6137b4 100644 --- a/providers/dns/namedotcom/namedotcom.go +++ b/providers/dns/namedotcom/namedotcom.go @@ -1,5 +1,4 @@ -// Package namedotcom implements a DNS provider for solving the DNS-01 challenge -// using Name.com's DNS service. +// Package namedotcom implements a DNS provider for solving the DNS-01 challenge using Name.com's DNS service. package namedotcom import ( @@ -10,7 +9,7 @@ import ( "time" "github.com/namedotcom/go/namecom" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -32,8 +31,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("NAMECOM_TTL", minTTL), - PropagationTimeout: env.GetOrDefaultSecond("NAMECOM_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("NAMECOM_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("NAMECOM_PROPAGATION_TIMEOUT", 15*time.Minute), + PollingInterval: env.GetOrDefaultSecond("NAMECOM_POLLING_INTERVAL", 20*time.Second), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("NAMECOM_HTTP_TIMEOUT", 10*time.Second), }, @@ -63,18 +62,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for namedotcom. -// Deprecated -func NewDNSProviderCredentials(username, apiToken, server string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Username = username - config.APIToken = apiToken - config.Server = server - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for namedotcom. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -105,7 +92,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) request := &namecom.Record{ DomainName: domain, @@ -125,7 +112,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) records, err := d.getRecords(domain) if err != nil { @@ -175,7 +162,7 @@ func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) { } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/netcup/client.go b/providers/dns/netcup/internal/client.go similarity index 94% rename from providers/dns/netcup/client.go rename to providers/dns/netcup/internal/client.go index 17a2ae96..733bfd0b 100644 --- a/providers/dns/netcup/client.go +++ b/providers/dns/netcup/internal/client.go @@ -1,4 +1,4 @@ -package netcup +package internal import ( "bytes" @@ -7,8 +7,6 @@ import ( "io/ioutil" "net/http" "time" - - "github.com/xenolf/lego/acme" ) // defaultBaseURL for reaching the jSON-based API-Endpoint of netcup @@ -246,7 +244,6 @@ func (c *Client) doRequest(payload interface{}, responseData interface{}) error req.Close = true req.Header.Set("content-type", "application/json") - req.Header.Set("User-Agent", acme.UserAgent) resp, err := c.HTTPClient.Do(req) if err != nil { @@ -316,3 +313,15 @@ func decodeResponseMsg(resp *http.Response) (*ResponseMsg, error) { return &respMsg, nil } + +// GetDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord +// equivalence is determined by Destination and RecortType attributes +// returns index of given DNSRecord in given array of DNSRecords +func GetDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) { + for index, element := range records { + if record.Destination == element.Destination && record.RecordType == element.RecordType { + return index, nil + } + } + return -1, fmt.Errorf("no DNS Record found") +} diff --git a/providers/dns/netcup/client_test.go b/providers/dns/netcup/internal/client_test.go similarity index 81% rename from providers/dns/netcup/client_test.go rename to providers/dns/netcup/internal/client_test.go index dd40cccd..db41e565 100644 --- a/providers/dns/netcup/client_test.go +++ b/providers/dns/netcup/internal/client_test.go @@ -1,4 +1,4 @@ -package netcup +package internal import ( "fmt" @@ -9,11 +9,19 @@ import ( "strings" "testing" + "github.com/xenolf/lego/platform/tester" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" ) +var envTest = tester.NewEnvTest( + "NETCUP_CUSTOMER_NUMBER", + "NETCUP_API_KEY", + "NETCUP_API_PASSWORD"). + WithDomain("NETCUP_DOMAIN") + func setupClientTest() (*Client, *http.ServeMux, func()) { handler := http.NewServeMux() server := httptest.NewServer(handler) @@ -27,6 +35,108 @@ func setupClientTest() (*Client, *http.ServeMux, func()) { return client, handler, server.Close } +func TestGetDNSRecordIdx(t *testing.T) { + records := []DNSRecord{ + { + ID: 12345, + Hostname: "asdf", + RecordType: "TXT", + Priority: "0", + Destination: "randomtext", + DeleteRecord: false, + State: "yes", + }, + { + ID: 23456, + Hostname: "@", + RecordType: "A", + Priority: "0", + Destination: "127.0.0.1", + DeleteRecord: false, + State: "yes", + }, + { + ID: 34567, + Hostname: "dfgh", + RecordType: "CNAME", + Priority: "0", + Destination: "example.com", + DeleteRecord: false, + State: "yes", + }, + { + ID: 45678, + Hostname: "fghj", + RecordType: "MX", + Priority: "10", + Destination: "mail.example.com", + DeleteRecord: false, + State: "yes", + }, + } + + testCases := []struct { + desc string + record DNSRecord + expectError bool + }{ + { + desc: "simple", + record: DNSRecord{ + ID: 12345, + Hostname: "asdf", + RecordType: "TXT", + Priority: "0", + Destination: "randomtext", + DeleteRecord: false, + State: "yes", + }, + }, + { + desc: "wrong Destination", + record: DNSRecord{ + ID: 12345, + Hostname: "asdf", + RecordType: "TXT", + Priority: "0", + Destination: "wrong", + DeleteRecord: false, + State: "yes", + }, + expectError: true, + }, + { + desc: "record type CNAME", + record: DNSRecord{ + ID: 12345, + Hostname: "asdf", + RecordType: "CNAME", + Priority: "0", + Destination: "randomtext", + DeleteRecord: false, + State: "yes", + }, + expectError: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + idx, err := GetDNSRecordIdx(records, test.record) + if test.expectError { + assert.Error(t, err) + assert.Equal(t, -1, idx) + } else { + assert.NoError(t, err) + assert.Equal(t, records[idx], test.record) + } + }) + } +} + func TestClient_Login(t *testing.T) { client, mux, tearDown := setupClientTest() defer tearDown() @@ -425,12 +535,12 @@ func TestLiveClientGetDnsRecords(t *testing.T) { sessionID, err := client.Login() require.NoError(t, err) - fqdn, _, _ := acme.DNS01Record(envTest.GetDomain(), "123d==") + fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") - zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, "error finding DNSZone") - zone = acme.UnFqdn(zone) + zone = dns01.UnFqdn(zone) // TestMethod _, err = client.GetDNSRecords(zone, sessionID) @@ -458,17 +568,23 @@ func TestLiveClientUpdateDnsRecord(t *testing.T) { sessionID, err := client.Login() require.NoError(t, err) - fqdn, _, _ := acme.DNS01Record(envTest.GetDomain(), "123d==") + fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") - zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, fmt.Errorf("error finding DNSZone, %v", err)) hostname := strings.Replace(fqdn, "."+zone, "", 1) - record := createTxtRecord(hostname, "asdf5678", 120) + record := DNSRecord{ + Hostname: hostname, + RecordType: "TXT", + Destination: "asdf5678", + DeleteRecord: false, + TTL: 120, + } // test - zone = acme.UnFqdn(zone) + zone = dns01.UnFqdn(zone) err = client.UpdateDNSRecord(sessionID, zone, []DNSRecord{record}) require.NoError(t, err) @@ -476,7 +592,7 @@ func TestLiveClientUpdateDnsRecord(t *testing.T) { records, err := client.GetDNSRecords(zone, sessionID) require.NoError(t, err) - recordIdx, err := getDNSRecordIdx(records, record) + recordIdx, err := GetDNSRecordIdx(records, record) require.NoError(t, err) assert.Equal(t, record.Hostname, records[recordIdx].Hostname) diff --git a/providers/dns/netcup/netcup.go b/providers/dns/netcup/netcup.go index 0dc59f96..8e8e1b5b 100644 --- a/providers/dns/netcup/netcup.go +++ b/providers/dns/netcup/netcup.go @@ -8,7 +8,9 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns/netcup/internal" + + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) @@ -27,7 +29,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("NETCUP_TTL", 120), + TTL: env.GetOrDefaultInt("NETCUP_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("NETCUP_PROPAGATION_TIMEOUT", 120*time.Second), PollingInterval: env.GetOrDefaultSecond("NETCUP_POLLING_INTERVAL", 5*time.Second), HTTPClient: &http.Client{ @@ -38,7 +40,7 @@ func NewDefaultConfig() *Config { // DNSProvider is an implementation of the acme.ChallengeProvider interface type DNSProvider struct { - client *Client + client *internal.Client config *Config } @@ -59,25 +61,13 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for netcup. -// Deprecated -func NewDNSProviderCredentials(customer, key, password string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Customer = customer - config.Key = key - config.Password = password - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for netcup. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("netcup: the configuration of the DNS provider is nil") } - client, err := NewClient(config.Customer, config.Key, config.Password) + client, err := internal.NewClient(config.Customer, config.Key, config.Password) if err != nil { return nil, fmt.Errorf("netcup: %v", err) } @@ -89,9 +79,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domainName, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domainName, keyAuth) + fqdn, value := dns01.GetRecord(domainName, keyAuth) - zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netcup: failed to find DNSZone, %v", err) } @@ -109,9 +99,14 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { }() hostname := strings.Replace(fqdn, "."+zone, "", 1) - record := createTxtRecord(hostname, value, d.config.TTL) + record := internal.DNSRecord{ + Hostname: hostname, + RecordType: "TXT", + Destination: value, + TTL: d.config.TTL, + } - zone = acme.UnFqdn(zone) + zone = dns01.UnFqdn(zone) records, err := d.client.GetDNSRecords(zone, sessionID) if err != nil { @@ -131,9 +126,9 @@ func (d *DNSProvider) Present(domainName, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domainName, keyAuth) + fqdn, value := dns01.GetRecord(domainName, keyAuth) - zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("netcup: failed to find DNSZone, %v", err) } @@ -152,23 +147,27 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { hostname := strings.Replace(fqdn, "."+zone, "", 1) - zone = acme.UnFqdn(zone) + zone = dns01.UnFqdn(zone) records, err := d.client.GetDNSRecords(zone, sessionID) if err != nil { return fmt.Errorf("netcup: %v", err) } - record := createTxtRecord(hostname, value, 0) + record := internal.DNSRecord{ + Hostname: hostname, + RecordType: "TXT", + Destination: value, + } - idx, err := getDNSRecordIdx(records, record) + idx, err := internal.GetDNSRecordIdx(records, record) if err != nil { return fmt.Errorf("netcup: %v", err) } records[idx].DeleteRecord = true - err = d.client.UpdateDNSRecord(sessionID, zone, []DNSRecord{records[idx]}) + err = d.client.UpdateDNSRecord(sessionID, zone, []internal.DNSRecord{records[idx]}) if err != nil { return fmt.Errorf("netcup: %v", err) } @@ -181,29 +180,3 @@ func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// getDNSRecordIdx searches a given array of DNSRecords for a given DNSRecord -// equivalence is determined by Destination and RecortType attributes -// returns index of given DNSRecord in given array of DNSRecords -func getDNSRecordIdx(records []DNSRecord, record DNSRecord) (int, error) { - for index, element := range records { - if record.Destination == element.Destination && record.RecordType == element.RecordType { - return index, nil - } - } - return -1, fmt.Errorf("no DNS Record found") -} - -// createTxtRecord uses the supplied values to return a DNSRecord of type TXT for the dns-01 challenge -func createTxtRecord(hostname, value string, ttl int) DNSRecord { - return DNSRecord{ - ID: 0, - Hostname: hostname, - RecordType: "TXT", - Priority: "", - Destination: value, - DeleteRecord: false, - State: "", - TTL: ttl, - } -} diff --git a/providers/dns/netcup/netcup_test.go b/providers/dns/netcup/netcup_test.go index bb62bfa6..d095704f 100644 --- a/providers/dns/netcup/netcup_test.go +++ b/providers/dns/netcup/netcup_test.go @@ -4,9 +4,8 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/tester" ) @@ -151,108 +150,6 @@ func TestNewDNSProviderConfig(t *testing.T) { } } -func TestGetDNSRecordIdx(t *testing.T) { - records := []DNSRecord{ - { - ID: 12345, - Hostname: "asdf", - RecordType: "TXT", - Priority: "0", - Destination: "randomtext", - DeleteRecord: false, - State: "yes", - }, - { - ID: 23456, - Hostname: "@", - RecordType: "A", - Priority: "0", - Destination: "127.0.0.1", - DeleteRecord: false, - State: "yes", - }, - { - ID: 34567, - Hostname: "dfgh", - RecordType: "CNAME", - Priority: "0", - Destination: "example.com", - DeleteRecord: false, - State: "yes", - }, - { - ID: 45678, - Hostname: "fghj", - RecordType: "MX", - Priority: "10", - Destination: "mail.example.com", - DeleteRecord: false, - State: "yes", - }, - } - - testCases := []struct { - desc string - record DNSRecord - expectError bool - }{ - { - desc: "simple", - record: DNSRecord{ - ID: 12345, - Hostname: "asdf", - RecordType: "TXT", - Priority: "0", - Destination: "randomtext", - DeleteRecord: false, - State: "yes", - }, - }, - { - desc: "wrong Destination", - record: DNSRecord{ - ID: 12345, - Hostname: "asdf", - RecordType: "TXT", - Priority: "0", - Destination: "wrong", - DeleteRecord: false, - State: "yes", - }, - expectError: true, - }, - { - desc: "record type CNAME", - record: DNSRecord{ - ID: 12345, - Hostname: "asdf", - RecordType: "CNAME", - Priority: "0", - Destination: "randomtext", - DeleteRecord: false, - State: "yes", - }, - expectError: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - idx, err := getDNSRecordIdx(records, test.record) - if test.expectError { - assert.Error(t, err) - assert.Equal(t, -1, idx) - } else { - assert.NoError(t, err) - assert.Equal(t, records[idx], test.record) - } - }) - } -} - func TestLivePresentAndCleanup(t *testing.T) { if !envTest.IsLiveTest() { t.Skip("skipping live test") @@ -262,12 +159,12 @@ func TestLivePresentAndCleanup(t *testing.T) { p, err := NewDNSProvider() require.NoError(t, err) - fqdn, _, _ := acme.DNS01Record(envTest.GetDomain(), "123d==") + fqdn, _ := dns01.GetRecord(envTest.GetDomain(), "123d==") - zone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + zone, err := dns01.FindZoneByFqdn(fqdn) require.NoError(t, err, "error finding DNSZone") - zone = acme.UnFqdn(zone) + zone = dns01.UnFqdn(zone) testCases := []string{ zone, @@ -276,12 +173,12 @@ func TestLivePresentAndCleanup(t *testing.T) { "*.sub." + zone, } - for _, tc := range testCases { - t.Run(fmt.Sprintf("domain(%s)", tc), func(t *testing.T) { - err = p.Present(tc, "987d", "123d==") + for _, test := range testCases { + t.Run(fmt.Sprintf("domain(%s)", test), func(t *testing.T) { + err = p.Present(test, "987d", "123d==") require.NoError(t, err) - err = p.CleanUp(tc, "987d", "123d==") + err = p.CleanUp(test, "987d", "123d==") require.NoError(t, err, "Did not clean up! Please remove record yourself.") }) } diff --git a/providers/dns/nifcloud/client.go b/providers/dns/nifcloud/internal/client.go similarity index 97% rename from providers/dns/nifcloud/client.go rename to providers/dns/nifcloud/internal/client.go index 24a8f52b..4d229f3b 100644 --- a/providers/dns/nifcloud/client.go +++ b/providers/dns/nifcloud/internal/client.go @@ -1,6 +1,4 @@ -// Package nifcloud implements a DNS provider for solving the DNS-01 challenge -// using NIFCLOUD DNS. -package nifcloud +package internal import ( "bytes" @@ -17,7 +15,8 @@ import ( const ( defaultBaseURL = "https://dns.api.cloud.nifty.com" apiVersion = "2012-12-12N2013-12-16" - xmlNs = "https://route53.amazonaws.com/doc/2012-12-12/" + // XMLNs XML NS of Route53 + XMLNs = "https://route53.amazonaws.com/doc/2012-12-12/" ) // ChangeResourceRecordSetsRequest is a complex type that contains change information for the resource record set. diff --git a/providers/dns/nifcloud/client_test.go b/providers/dns/nifcloud/internal/client_test.go similarity index 99% rename from providers/dns/nifcloud/client_test.go rename to providers/dns/nifcloud/internal/client_test.go index f940c4ee..2845591e 100644 --- a/providers/dns/nifcloud/client_test.go +++ b/providers/dns/nifcloud/internal/client_test.go @@ -1,4 +1,4 @@ -package nifcloud +package internal import ( "fmt" diff --git a/providers/dns/nifcloud/nifcloud.go b/providers/dns/nifcloud/nifcloud.go index c8cf0484..022e34b1 100644 --- a/providers/dns/nifcloud/nifcloud.go +++ b/providers/dns/nifcloud/nifcloud.go @@ -1,5 +1,4 @@ -// Package nifcloud implements a DNS provider for solving the DNS-01 challenge -// using NIFCLOUD DNS. +// Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS. package nifcloud import ( @@ -8,8 +7,11 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/providers/dns/nifcloud/internal" + + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/platform/wait" ) // Config is used to configure the creation of the DNSProvider @@ -26,9 +28,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("NIFCLOUD_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("NIFCLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("NIFCLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("NIFCLOUD_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("NIFCLOUD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("NIFCLOUD_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("NIFCLOUD_HTTP_TIMEOUT", 30*time.Second), }, @@ -37,7 +39,7 @@ func NewDefaultConfig() *Config { // DNSProvider implements the acme.ChallengeProvider interface type DNSProvider struct { - client *Client + client *internal.Client config *Config } @@ -58,26 +60,13 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for NIFCLOUD. -// Deprecated -func NewDNSProviderCredentials(httpClient *http.Client, endpoint, accessKey, secretKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.HTTPClient = httpClient - config.BaseURL = endpoint - config.AccessKey = accessKey - config.SecretKey = secretKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("nifcloud: the configuration of the DNS provider is nil") } - client, err := NewClient(config.AccessKey, config.SecretKey) + client, err := internal.NewClient(config.AccessKey, config.SecretKey) if err != nil { return nil, fmt.Errorf("nifcloud: %v", err) } @@ -95,7 +84,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("CREATE", fqdn, value, domain, d.config.TTL) if err != nil { @@ -106,7 +95,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("DELETE", fqdn, value, domain, d.config.TTL) if err != nil { @@ -122,22 +111,22 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func (d *DNSProvider) changeRecord(action, fqdn, value, domain string, ttl int) error { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) - reqParams := ChangeResourceRecordSetsRequest{ - XMLNs: xmlNs, - ChangeBatch: ChangeBatch{ + reqParams := internal.ChangeResourceRecordSetsRequest{ + XMLNs: internal.XMLNs, + ChangeBatch: internal.ChangeBatch{ Comment: "Managed by Lego", - Changes: Changes{ - Change: []Change{ + Changes: internal.Changes{ + Change: []internal.Change{ { Action: action, - ResourceRecordSet: ResourceRecordSet{ + ResourceRecordSet: internal.ResourceRecordSet{ Name: name, Type: "TXT", TTL: ttl, - ResourceRecords: ResourceRecords{ - ResourceRecord: []ResourceRecord{ + ResourceRecords: internal.ResourceRecords{ + ResourceRecord: []internal.ResourceRecord{ { Value: value, }, @@ -157,7 +146,7 @@ func (d *DNSProvider) changeRecord(action, fqdn, value, domain string, ttl int) statusID := resp.ChangeInfo.ID - return acme.WaitFor(120*time.Second, 4*time.Second, func() (bool, error) { + return wait.For(120*time.Second, 4*time.Second, func() (bool, error) { resp, err := d.client.GetChange(statusID) if err != nil { return false, fmt.Errorf("failed to query NIFCLOUD DNS change status: %v", err) diff --git a/providers/dns/ns1/ns1.go b/providers/dns/ns1/ns1.go index 16be1c90..63691248 100644 --- a/providers/dns/ns1/ns1.go +++ b/providers/dns/ns1/ns1.go @@ -1,5 +1,4 @@ -// Package ns1 implements a DNS provider for solving the DNS-01 challenge -// using NS1 DNS. +// Package ns1 implements a DNS provider for solving the DNS-01 challenge using NS1 DNS. package ns1 import ( @@ -9,7 +8,7 @@ import ( "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" "gopkg.in/ns1/ns1-go.v2/rest" @@ -28,9 +27,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("NS1_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("NS1_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("NS1_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("NS1_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("NS1_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("NS1_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("NS1_HTTP_TIMEOUT", 10*time.Second), }, @@ -57,16 +56,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for NS1. -// Deprecated -func NewDNSProviderCredentials(key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for NS1. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -84,20 +73,20 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("ns1: %v", err) } - record, _, err := d.client.Records.Get(zone.Zone, acme.UnFqdn(fqdn), "TXT") + record, _, err := d.client.Records.Get(zone.Zone, dns01.UnFqdn(fqdn), "TXT") // Create a new record if err == rest.ErrRecordMissing || record == nil { log.Infof("Create a new record for [zone: %s, fqdn: %s, domain: %s]", zone.Zone, fqdn) - record = dns.NewRecord(zone.Zone, acme.UnFqdn(fqdn), "TXT") + record = dns.NewRecord(zone.Zone, dns01.UnFqdn(fqdn), "TXT") record.TTL = d.config.TTL record.Answers = []*dns.Answer{{Rdata: []string{value}}} @@ -128,14 +117,14 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("ns1: %v", err) } - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) _, err = d.client.Records.Delete(zone.Zone, name, "TXT") if err != nil { return fmt.Errorf("ns1: failed to delete record [zone: %q, domain: %q]: %v", zone.Zone, name, err) @@ -164,7 +153,7 @@ func (d *DNSProvider) getHostedZone(fqdn string) (*dns.Zone, error) { } func getAuthZone(fqdn string) (string, error) { - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } diff --git a/providers/dns/ns1/ns1_test.go b/providers/dns/ns1/ns1_test.go index 16e3548b..44050c29 100644 --- a/providers/dns/ns1/ns1_test.go +++ b/providers/dns/ns1/ns1_test.go @@ -111,14 +111,14 @@ func Test_getAuthZone(t *testing.T) { desc: "invalid fqdn", fqdn: "_acme-challenge.myhost.sub.example.com", expected: expected{ - Error: "dns: domain must be fully qualified", + Error: "could not find the start of authority for _acme-challenge.myhost.sub.example.com: dns: domain must be fully qualified", }, }, { desc: "invalid authority", fqdn: "_acme-challenge.myhost.sub.domain.tld.", expected: expected{ - Error: "could not find the start of authority", + Error: "could not find the start of authority for _acme-challenge.myhost.sub.domain.tld.: NXDOMAIN", }, }, } diff --git a/providers/dns/otc/client.go b/providers/dns/otc/client.go index 7f4fd34b..1cd71f55 100644 --- a/providers/dns/otc/client.go +++ b/providers/dns/otc/client.go @@ -1,5 +1,14 @@ package otc +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" +) + type recordset struct { Name string `json:"name"` Description string `json:"description"` @@ -41,14 +50,20 @@ type loginResponse struct { } type endpointResponse struct { - Token struct { - Catalog []struct { - Type string `json:"type"` - Endpoints []struct { - URL string `json:"url"` - } `json:"endpoints"` - } `json:"catalog"` - } `json:"token"` + Token token `json:"token"` +} + +type token struct { + Catalog []catalog `json:"catalog"` +} + +type catalog struct { + Type string `json:"type"` + Endpoints []endpoint `json:"endpoints"` +} + +type endpoint struct { + URL string `json:"url"` } type zoneItem struct { @@ -66,3 +81,183 @@ type recordSet struct { type recordSetsResponse struct { RecordSets []recordSet `json:"recordsets"` } + +// Starts a new OTC API Session. Authenticates using userName, password +// and receives a token to be used in for subsequent requests. +func (d *DNSProvider) login() error { + return d.loginRequest() +} + +func (d *DNSProvider) loginRequest() error { + userResp := userResponse{ + Name: d.config.UserName, + Password: d.config.Password, + Domain: nameResponse{ + Name: d.config.DomainName, + }, + } + + loginResp := loginResponse{ + Auth: authResponse{ + Identity: identityResponse{ + Methods: []string{"password"}, + Password: passwordResponse{ + User: userResp, + }, + }, + Scope: scopeResponse{ + Project: nameResponse{ + Name: d.config.ProjectName, + }, + }, + }, + } + + body, err := json.Marshal(loginResp) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, d.config.IdentityEndpoint, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: d.config.HTTPClient.Timeout} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return fmt.Errorf("OTC API request failed with HTTP status code %d", resp.StatusCode) + } + + d.token = resp.Header.Get("X-Subject-Token") + + if d.token == "" { + return fmt.Errorf("unable to get auth token") + } + + var endpointResp endpointResponse + + err = json.NewDecoder(resp.Body).Decode(&endpointResp) + if err != nil { + return err + } + + var endpoints []endpoint + for _, v := range endpointResp.Token.Catalog { + if v.Type == "dns" { + endpoints = append(endpoints, v.Endpoints...) + } + } + + if len(endpoints) > 0 { + d.baseURL = fmt.Sprintf("%s/v2", endpoints[0].URL) + } else { + return fmt.Errorf("unable to get dns endpoint") + } + + return nil +} + +func (d *DNSProvider) getZoneID(zone string) (string, error) { + resource := fmt.Sprintf("zones?name=%s", zone) + resp, err := d.sendRequest(http.MethodGet, resource, nil) + if err != nil { + return "", err + } + + var zonesRes zonesResponse + err = json.NewDecoder(resp).Decode(&zonesRes) + if err != nil { + return "", err + } + + if len(zonesRes.Zones) < 1 { + return "", fmt.Errorf("zone %s not found", zone) + } + + if len(zonesRes.Zones) > 1 { + return "", fmt.Errorf("to many zones found") + } + + if zonesRes.Zones[0].ID == "" { + return "", fmt.Errorf("id not found") + } + + return zonesRes.Zones[0].ID, nil +} + +func (d *DNSProvider) getRecordSetID(zoneID string, fqdn string) (string, error) { + resource := fmt.Sprintf("zones/%s/recordsets?type=TXT&name=%s", zoneID, fqdn) + resp, err := d.sendRequest(http.MethodGet, resource, nil) + if err != nil { + return "", err + } + + var recordSetsRes recordSetsResponse + err = json.NewDecoder(resp).Decode(&recordSetsRes) + if err != nil { + return "", err + } + + if len(recordSetsRes.RecordSets) < 1 { + return "", fmt.Errorf("record not found") + } + + if len(recordSetsRes.RecordSets) > 1 { + return "", fmt.Errorf("to many records found") + } + + if recordSetsRes.RecordSets[0].ID == "" { + return "", fmt.Errorf("id not found") + } + + return recordSetsRes.RecordSets[0].ID, nil +} + +func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error { + resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID) + + _, err := d.sendRequest(http.MethodDelete, resource, nil) + return err +} + +func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (io.Reader, error) { + url := fmt.Sprintf("%s/%s", d.baseURL, resource) + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if len(d.token) > 0 { + req.Header.Set("X-Auth-Token", d.token) + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("OTC API request %s failed with HTTP status code %d", url, resp.StatusCode) + } + + body1, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return bytes.NewReader(body1), nil +} diff --git a/providers/dns/otc/mock_test.go b/providers/dns/otc/mock_test.go index 8eab3563..5a96c466 100644 --- a/providers/dns/otc/mock_test.go +++ b/providers/dns/otc/mock_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -var fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f" +const fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f" // DNSServerMock mock type DNSServerMock struct { diff --git a/providers/dns/otc/otc.go b/providers/dns/otc/otc.go index fd0c40df..4eeca22f 100644 --- a/providers/dns/otc/otc.go +++ b/providers/dns/otc/otc.go @@ -1,19 +1,14 @@ -// Package otc implements a DNS provider for solving the DNS-01 challenge -// using Open Telekom Cloud Managed DNS. +// Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS. package otc import ( - "bytes" - "encoding/json" "errors" "fmt" - "io" - "io/ioutil" "net" "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -39,8 +34,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ IdentityEndpoint: env.GetOrDefaultString("OTC_IDENTITY_ENDPOINT", defaultIdentityEndpoint), - PropagationTimeout: env.GetOrDefaultSecond("OTC_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("OTC_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("OTC_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("OTC_POLLING_INTERVAL", dns01.DefaultPollingInterval), TTL: env.GetOrDefaultInt("OTC_TTL", minTTL), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("OTC_HTTP_TIMEOUT", 10*time.Second), @@ -89,20 +84,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for OTC DNS. -// Deprecated -func NewDNSProviderCredentials(domainName, userName, password, projectName, identityEndpoint string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.IdentityEndpoint = identityEndpoint - config.DomainName = domainName - config.UserName = userName - config.Password = password - config.ProjectName = projectName - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -126,9 +107,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("otc: %v", err) } @@ -162,9 +143,9 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return fmt.Errorf("otc: %v", err) } @@ -196,184 +177,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// sendRequest send request -func (d *DNSProvider) sendRequest(method, resource string, payload interface{}) (io.Reader, error) { - url := fmt.Sprintf("%s/%s", d.baseURL, resource) - - body, err := json.Marshal(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(method, url, bytes.NewReader(body)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - if len(d.token) > 0 { - req.Header.Set("X-Auth-Token", d.token) - } - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("OTC API request %s failed with HTTP status code %d", url, resp.StatusCode) - } - - body1, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return bytes.NewReader(body1), nil -} - -func (d *DNSProvider) loginRequest() error { - userResp := userResponse{ - Name: d.config.UserName, - Password: d.config.Password, - Domain: nameResponse{ - Name: d.config.DomainName, - }, - } - - loginResp := loginResponse{ - Auth: authResponse{ - Identity: identityResponse{ - Methods: []string{"password"}, - Password: passwordResponse{ - User: userResp, - }, - }, - Scope: scopeResponse{ - Project: nameResponse{ - Name: d.config.ProjectName, - }, - }, - }, - } - - body, err := json.Marshal(loginResp) - if err != nil { - return err - } - - req, err := http.NewRequest(http.MethodPost, d.config.IdentityEndpoint, bytes.NewReader(body)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{Timeout: d.config.HTTPClient.Timeout} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode >= 400 { - return fmt.Errorf("OTC API request failed with HTTP status code %d", resp.StatusCode) - } - - d.token = resp.Header.Get("X-Subject-Token") - - if d.token == "" { - return fmt.Errorf("unable to get auth token") - } - - var endpointResp endpointResponse - - err = json.NewDecoder(resp.Body).Decode(&endpointResp) - if err != nil { - return err - } - - for _, v := range endpointResp.Token.Catalog { - if v.Type == "dns" { - for _, endpoint := range v.Endpoints { - d.baseURL = fmt.Sprintf("%s/v2", endpoint.URL) - continue - } - } - } - - if d.baseURL == "" { - return fmt.Errorf("unable to get dns endpoint") - } - - return nil -} - -// Starts a new OTC API Session. Authenticates using userName, password -// and receives a token to be used in for subsequent requests. -func (d *DNSProvider) login() error { - return d.loginRequest() -} - -func (d *DNSProvider) getZoneID(zone string) (string, error) { - resource := fmt.Sprintf("zones?name=%s", zone) - resp, err := d.sendRequest(http.MethodGet, resource, nil) - if err != nil { - return "", err - } - - var zonesRes zonesResponse - err = json.NewDecoder(resp).Decode(&zonesRes) - if err != nil { - return "", err - } - - if len(zonesRes.Zones) < 1 { - return "", fmt.Errorf("zone %s not found", zone) - } - - if len(zonesRes.Zones) > 1 { - return "", fmt.Errorf("to many zones found") - } - - if zonesRes.Zones[0].ID == "" { - return "", fmt.Errorf("id not found") - } - - return zonesRes.Zones[0].ID, nil -} - -func (d *DNSProvider) getRecordSetID(zoneID string, fqdn string) (string, error) { - resource := fmt.Sprintf("zones/%s/recordsets?type=TXT&name=%s", zoneID, fqdn) - resp, err := d.sendRequest(http.MethodGet, resource, nil) - if err != nil { - return "", err - } - - var recordSetsRes recordSetsResponse - err = json.NewDecoder(resp).Decode(&recordSetsRes) - if err != nil { - return "", err - } - - if len(recordSetsRes.RecordSets) < 1 { - return "", fmt.Errorf("record not found") - } - - if len(recordSetsRes.RecordSets) > 1 { - return "", fmt.Errorf("to many records found") - } - - if recordSetsRes.RecordSets[0].ID == "" { - return "", fmt.Errorf("id not found") - } - - return recordSetsRes.RecordSets[0].ID, nil -} - -func (d *DNSProvider) deleteRecordSet(zoneID, recordID string) error { - resource := fmt.Sprintf("zones/%s/recordsets/%s", zoneID, recordID) - - _, err := d.sendRequest(http.MethodDelete, resource, nil) - return err -} diff --git a/providers/dns/ovh/ovh.go b/providers/dns/ovh/ovh.go index e5e09244..ee01c009 100644 --- a/providers/dns/ovh/ovh.go +++ b/providers/dns/ovh/ovh.go @@ -1,5 +1,4 @@ -// Package ovh implements a DNS provider for solving the DNS-01 -// challenge using OVH DNS. +// Package ovh implements a DNS provider for solving the DNS-01 challenge using OVH DNS. package ovh import ( @@ -11,13 +10,23 @@ import ( "time" "github.com/ovh/go-ovh/ovh" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) // OVH API reference: https://eu.api.ovh.com/ // Create a Token: https://eu.api.ovh.com/createToken/ +// Record a DNS record +type Record struct { + ID int `json:"id,omitempty"` + FieldType string `json:"fieldType,omitempty"` + SubDomain string `json:"subDomain,omitempty"` + Target string `json:"target,omitempty"` + TTL int `json:"ttl,omitempty"` + Zone string `json:"zone,omitempty"` +} + // Config is used to configure the creation of the DNSProvider type Config struct { APIEndpoint string @@ -33,9 +42,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("OVH_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("OVH_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("OVH_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("OVH_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("OVH_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("OVH_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("OVH_HTTP_TIMEOUT", ovh.DefaultTimeout), }, @@ -72,19 +81,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for OVH. -// Deprecated -func NewDNSProviderCredentials(apiEndpoint, applicationKey, applicationSecret, consumerKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIEndpoint = apiEndpoint - config.ApplicationKey = applicationKey - config.ApplicationSecret = applicationSecret - config.ConsumerKey = consumerKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for OVH. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -116,32 +112,32 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) // Parse domain name - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return fmt.Errorf("ovh: could not determine zone for domain: '%s'. %s", domain, err) } - authZone = acme.UnFqdn(authZone) + authZone = dns01.UnFqdn(authZone) subDomain := d.extractRecordName(fqdn, authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone) - reqData := txtRecordRequest{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: d.config.TTL} - var respData txtRecordResponse + reqData := Record{FieldType: "TXT", SubDomain: subDomain, Target: value, TTL: d.config.TTL} // Create TXT record + var respData Record err = d.client.Post(reqURL, reqData, &respData) if err != nil { - return fmt.Errorf("ovh: error when call api to add record: %v", err) + return fmt.Errorf("ovh: error when call api to add record (%s): %v", reqURL, err) } // Apply the change reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone) err = d.client.Post(reqURL, nil, nil) if err != nil { - return fmt.Errorf("ovh: error when call api to refresh zone: %v", err) + return fmt.Errorf("ovh: error when call api to refresh zone (%s): %v", reqURL, err) } d.recordIDsMu.Lock() @@ -153,7 +149,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) // get the record's unique ID from when we created it d.recordIDsMu.Lock() @@ -163,18 +159,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("ovh: unknown record ID for '%s'", fqdn) } - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return fmt.Errorf("ovh: could not determine zone for domain: '%s'. %s", domain, err) } - authZone = acme.UnFqdn(authZone) + authZone = dns01.UnFqdn(authZone) reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID) err = d.client.Delete(reqURL, nil) if err != nil { - return fmt.Errorf("ovh: error when call OVH api to delete challenge record: %v", err) + return fmt.Errorf("ovh: error when call OVH api to delete challenge record (%s): %v", reqURL, err) } // Delete record ID from map @@ -192,27 +188,9 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } return name } - -// txtRecordRequest represents the request body to DO's API to make a TXT record -type txtRecordRequest struct { - FieldType string `json:"fieldType"` - SubDomain string `json:"subDomain"` - Target string `json:"target"` - TTL int `json:"ttl"` -} - -// txtRecordResponse represents a response from DO's API after making a TXT record -type txtRecordResponse struct { - ID int `json:"id"` - FieldType string `json:"fieldType"` - SubDomain string `json:"subDomain"` - Target string `json:"target"` - TTL int `json:"ttl"` - Zone string `json:"zone"` -} diff --git a/providers/dns/pdns/README.md b/providers/dns/pdns/README.md index a59b9ad8..c13e7f9d 100644 --- a/providers/dns/pdns/README.md +++ b/providers/dns/pdns/README.md @@ -1,6 +1,6 @@ ## PowerDNS provider -Tested and confirmed to work with PowerDNS authoratative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface. +Tested and confirmed to work with PowerDNS authoritative server 3.4.8 and 4.0.1. Refer to [PowerDNS documentation](https://doc.powerdns.com/md/httpapi/README/) instructions on how to enable the built-in API interface. PowerDNS Notes: - PowerDNS API does not currently support SSL, therefore you should take care to ensure that traffic between lego and the PowerDNS API is over a trusted network, VPN etc. diff --git a/providers/dns/pdns/client.go b/providers/dns/pdns/client.go new file mode 100644 index 00000000..88f892c3 --- /dev/null +++ b/providers/dns/pdns/client.go @@ -0,0 +1,220 @@ +package pdns + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/xenolf/lego/challenge/dns01" +) + +type Record struct { + Content string `json:"content"` + Disabled bool `json:"disabled"` + + // pre-v1 API + Name string `json:"name"` + Type string `json:"type"` + TTL int `json:"ttl,omitempty"` +} + +type hostedZone struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + RRSets []rrSet `json:"rrsets"` + + // pre-v1 API + Records []Record `json:"records"` +} + +type rrSet struct { + Name string `json:"name"` + Type string `json:"type"` + Kind string `json:"kind"` + ChangeType string `json:"changetype"` + Records []Record `json:"records"` + TTL int `json:"ttl,omitempty"` +} + +type rrSets struct { + RRSets []rrSet `json:"rrsets"` +} + +type apiError struct { + ShortMsg string `json:"error"` +} + +func (a apiError) Error() string { + return a.ShortMsg +} + +type apiVersion struct { + URL string `json:"url"` + Version int `json:"version"` +} + +func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) { + var zone hostedZone + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return nil, err + } + + u := "/servers/localhost/zones" + result, err := d.sendRequest(http.MethodGet, u, nil) + if err != nil { + return nil, err + } + + var zones []hostedZone + err = json.Unmarshal(result, &zones) + if err != nil { + return nil, err + } + + u = "" + for _, zone := range zones { + if dns01.UnFqdn(zone.Name) == dns01.UnFqdn(authZone) { + u = zone.URL + break + } + } + + result, err = d.sendRequest(http.MethodGet, u, nil) + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &zone) + if err != nil { + return nil, err + } + + // convert pre-v1 API result + if len(zone.Records) > 0 { + zone.RRSets = []rrSet{} + for _, record := range zone.Records { + set := rrSet{ + Name: record.Name, + Type: record.Type, + Records: []Record{record}, + } + zone.RRSets = append(zone.RRSets, set) + } + } + + return &zone, nil +} + +func (d *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) { + zone, err := d.getHostedZone(fqdn) + if err != nil { + return nil, err + } + + _, err = d.sendRequest(http.MethodGet, zone.URL, nil) + if err != nil { + return nil, err + } + + for _, set := range zone.RRSets { + if (set.Name == dns01.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" { + return &set, nil + } + } + + return nil, fmt.Errorf("no existing record found for %s", fqdn) +} + +func (d *DNSProvider) getAPIVersion() (int, error) { + result, err := d.sendRequest(http.MethodGet, "/api", nil) + if err != nil { + return 0, err + } + + var versions []apiVersion + err = json.Unmarshal(result, &versions) + if err != nil { + return 0, err + } + + latestVersion := 0 + for _, v := range versions { + if v.Version > latestVersion { + latestVersion = v.Version + } + } + + return latestVersion, err +} + +func (d *DNSProvider) sendRequest(method, uri string, body io.Reader) (json.RawMessage, error) { + req, err := d.makeRequest(method, uri, body) + if err != nil { + return nil, err + } + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error talking to PDNS API -> %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) { + return nil, fmt.Errorf("unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, req.URL) + } + + var msg json.RawMessage + err = json.NewDecoder(resp.Body).Decode(&msg) + if err != nil { + if err == io.EOF { + // empty body + return nil, nil + } + // other error + return nil, err + } + + // check for PowerDNS error message + if len(msg) > 0 && msg[0] == '{' { + var errInfo apiError + err = json.Unmarshal(msg, &errInfo) + if err != nil { + return nil, err + } + if errInfo.ShortMsg != "" { + return nil, fmt.Errorf("error talking to PDNS API -> %v", errInfo) + } + } + return msg, nil +} + +func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Request, error) { + var path = "" + if d.config.Host.Path != "/" { + path = d.config.Host.Path + } + + if !strings.HasPrefix(uri, "/") { + uri = "/" + uri + } + + if d.apiVersion > 0 && !strings.HasPrefix(uri, "/api/v") { + uri = "/api/v" + strconv.Itoa(d.apiVersion) + uri + } + + u := d.config.Host.Scheme + "://" + d.config.Host.Host + path + uri + req, err := http.NewRequest(method, u, body) + if err != nil { + return nil, err + } + + req.Header.Set("X-API-Key", d.config.APIKey) + + return req, nil +} diff --git a/providers/dns/pdns/pdns.go b/providers/dns/pdns/pdns.go index 8dc9c175..19510289 100644 --- a/providers/dns/pdns/pdns.go +++ b/providers/dns/pdns/pdns.go @@ -1,5 +1,4 @@ -// Package pdns implements a DNS provider for solving the DNS-01 -// challenge using PowerDNS nameserver. +// Package pdns implements a DNS provider for solving the DNS-01 challenge using PowerDNS nameserver. package pdns import ( @@ -7,14 +6,11 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/http" "net/url" - "strconv" - "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" ) @@ -32,7 +28,7 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("PDNS_TTL", 120), + TTL: env.GetOrDefaultInt("PDNS_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("PDNS_PROPAGATION_TIMEOUT", 120*time.Second), PollingInterval: env.GetOrDefaultSecond("PDNS_POLLING_INTERVAL", 2*time.Second), HTTPClient: &http.Client{ @@ -68,17 +64,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for pdns. -// Deprecated -func NewDNSProviderCredentials(host *url.URL, key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Host = host - config.APIKey = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for pdns. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -112,7 +97,8 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) + zone, err := d.getHostedZone(fqdn) if err != nil { return fmt.Errorf("pdns: %v", err) @@ -122,10 +108,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // pre-v1 API wants non-fqdn if d.apiVersion == 0 { - name = acme.UnFqdn(fqdn) + name = dns01.UnFqdn(fqdn) } - rec := pdnsRecord{ + rec := Record{ Content: "\"" + value + "\"", Disabled: false, @@ -143,7 +129,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { Type: "TXT", Kind: "Master", TTL: d.config.TTL, - Records: []pdnsRecord{rec}, + Records: []Record{rec}, }, }, } @@ -153,7 +139,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("pdns: %v", err) } - _, err = d.makeRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) + _, err = d.sendRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("pdns: %v", err) } @@ -162,7 +148,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(fqdn) if err != nil { @@ -188,203 +174,9 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("pdns: %v", err) } - _, err = d.makeRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) + _, err = d.sendRequest(http.MethodPatch, zone.URL, bytes.NewReader(body)) if err != nil { return fmt.Errorf("pdns: %v", err) } return nil } - -func (d *DNSProvider) getHostedZone(fqdn string) (*hostedZone, error) { - var zone hostedZone - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) - if err != nil { - return nil, err - } - - u := "/servers/localhost/zones" - result, err := d.makeRequest(http.MethodGet, u, nil) - if err != nil { - return nil, err - } - - var zones []hostedZone - err = json.Unmarshal(result, &zones) - if err != nil { - return nil, err - } - - u = "" - for _, zone := range zones { - if acme.UnFqdn(zone.Name) == acme.UnFqdn(authZone) { - u = zone.URL - } - } - - result, err = d.makeRequest(http.MethodGet, u, nil) - if err != nil { - return nil, err - } - - err = json.Unmarshal(result, &zone) - if err != nil { - return nil, err - } - - // convert pre-v1 API result - if len(zone.Records) > 0 { - zone.RRSets = []rrSet{} - for _, record := range zone.Records { - set := rrSet{ - Name: record.Name, - Type: record.Type, - Records: []pdnsRecord{record}, - } - zone.RRSets = append(zone.RRSets, set) - } - } - - return &zone, nil -} - -func (d *DNSProvider) findTxtRecord(fqdn string) (*rrSet, error) { - zone, err := d.getHostedZone(fqdn) - if err != nil { - return nil, err - } - - _, err = d.makeRequest(http.MethodGet, zone.URL, nil) - if err != nil { - return nil, err - } - - for _, set := range zone.RRSets { - if (set.Name == acme.UnFqdn(fqdn) || set.Name == fqdn) && set.Type == "TXT" { - return &set, nil - } - } - - return nil, fmt.Errorf("no existing record found for %s", fqdn) -} - -func (d *DNSProvider) getAPIVersion() (int, error) { - type APIVersion struct { - URL string `json:"url"` - Version int `json:"version"` - } - - result, err := d.makeRequest(http.MethodGet, "/api", nil) - if err != nil { - return 0, err - } - - var versions []APIVersion - err = json.Unmarshal(result, &versions) - if err != nil { - return 0, err - } - - latestVersion := 0 - for _, v := range versions { - if v.Version > latestVersion { - latestVersion = v.Version - } - } - - return latestVersion, err -} - -func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { - type APIError struct { - Error string `json:"error"` - } - - var path = "" - if d.config.Host.Path != "/" { - path = d.config.Host.Path - } - - if !strings.HasPrefix(uri, "/") { - uri = "/" + uri - } - - if d.apiVersion > 0 && !strings.HasPrefix(uri, "/api/v") { - uri = "/api/v" + strconv.Itoa(d.apiVersion) + uri - } - - u := d.config.Host.Scheme + "://" + d.config.Host.Host + path + uri - req, err := http.NewRequest(method, u, body) - if err != nil { - return nil, err - } - - req.Header.Set("X-API-Key", d.config.APIKey) - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error talking to PDNS API -> %v", err) - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusUnprocessableEntity && (resp.StatusCode < 200 || resp.StatusCode >= 300) { - return nil, fmt.Errorf("unexpected HTTP status code %d when fetching '%s'", resp.StatusCode, u) - } - - var msg json.RawMessage - err = json.NewDecoder(resp.Body).Decode(&msg) - switch { - case err == io.EOF: - // empty body - return nil, nil - case err != nil: - // other error - return nil, err - } - - // check for PowerDNS error message - if len(msg) > 0 && msg[0] == '{' { - var apiError APIError - err = json.Unmarshal(msg, &apiError) - if err != nil { - return nil, err - } - if apiError.Error != "" { - return nil, fmt.Errorf("error talking to PDNS API -> %v", apiError.Error) - } - } - return msg, nil -} - -type pdnsRecord struct { - Content string `json:"content"` - Disabled bool `json:"disabled"` - - // pre-v1 API - Name string `json:"name"` - Type string `json:"type"` - TTL int `json:"ttl,omitempty"` -} - -type hostedZone struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - RRSets []rrSet `json:"rrsets"` - - // pre-v1 API - Records []pdnsRecord `json:"records"` -} - -type rrSet struct { - Name string `json:"name"` - Type string `json:"type"` - Kind string `json:"kind"` - ChangeType string `json:"changetype"` - Records []pdnsRecord `json:"records"` - TTL int `json:"ttl,omitempty"` -} - -type rrSets struct { - RRSets []rrSet `json:"rrsets"` -} diff --git a/providers/dns/rackspace/client.go b/providers/dns/rackspace/client.go index 54b05c9d..e1085626 100644 --- a/providers/dns/rackspace/client.go +++ b/providers/dns/rackspace/client.go @@ -1,5 +1,15 @@ package rackspace +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/xenolf/lego/challenge/dns01" +) + // APIKeyCredentials API credential type APIKeyCredentials struct { Username string `json:"username"` @@ -69,3 +79,127 @@ type Record struct { TTL int `json:"ttl,omitempty"` ID string `json:"id,omitempty"` } + +// getHostedZoneID performs a lookup to get the DNS zone which needs +// modifying for a given FQDN +func (d *DNSProvider) getHostedZoneID(fqdn string) (int, error) { + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return 0, err + } + + result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains?name=%s", dns01.UnFqdn(authZone)), nil) + if err != nil { + return 0, err + } + + var zoneSearchResponse ZoneSearchResponse + err = json.Unmarshal(result, &zoneSearchResponse) + if err != nil { + return 0, err + } + + // If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur) + if zoneSearchResponse.TotalEntries != 1 { + return 0, fmt.Errorf("found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) + } + + return zoneSearchResponse.HostedZones[0].ID, nil +} + +// findTxtRecord searches a DNS zone for a TXT record with a specific name +func (d *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*Record, error) { + result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, dns01.UnFqdn(fqdn)), nil) + if err != nil { + return nil, err + } + + var records Records + err = json.Unmarshal(result, &records) + if err != nil { + return nil, err + } + + switch len(records.Record) { + case 1: + case 0: + return nil, fmt.Errorf("no TXT record found for %s", fqdn) + default: + return nil, fmt.Errorf("more than 1 TXT record found for %s", fqdn) + } + + return &records.Record[0], nil +} + +// makeRequest is a wrapper function used for making DNS API requests +func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { + url := d.cloudDNSEndpoint + uri + + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + req.Header.Set("X-Auth-Token", d.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := d.config.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error querying DNS API: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return nil, fmt.Errorf("request failed for %s %s. Response code: %d", method, url, resp.StatusCode) + } + + var r json.RawMessage + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, fmt.Errorf("JSON decode failed for %s %s. Response code: %d", method, url, resp.StatusCode) + } + + return r, nil +} + +func login(config *Config) (*Identity, error) { + authData := AuthData{ + Auth: Auth{ + APIKeyCredentials: APIKeyCredentials{ + Username: config.APIUser, + APIKey: config.APIKey, + }, + }, + } + + body, err := json.Marshal(authData) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, config.BaseURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := config.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("error querying Identity API: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("authentication failed: response code: %d", resp.StatusCode) + } + + var identity Identity + err = json.NewDecoder(resp.Body).Decode(&identity) + if err != nil { + return nil, err + } + + return &identity, nil +} diff --git a/providers/dns/rackspace/rackspace.go b/providers/dns/rackspace/rackspace.go index 7b5e73d4..a0ddb4aa 100644 --- a/providers/dns/rackspace/rackspace.go +++ b/providers/dns/rackspace/rackspace.go @@ -1,5 +1,4 @@ -// Package rackspace implements a DNS provider for solving the DNS-01 -// challenge using rackspace DNS. +// Package rackspace implements a DNS provider for solving the DNS-01 challenge using rackspace DNS. package rackspace import ( @@ -7,11 +6,10 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -34,8 +32,8 @@ func NewDefaultConfig() *Config { return &Config{ BaseURL: defaultBaseURL, TTL: env.GetOrDefaultInt("RACKSPACE_TTL", 300), - PropagationTimeout: env.GetOrDefaultSecond("RACKSPACE_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("RACKSPACE_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("RACKSPACE_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("RACKSPACE_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("RACKSPACE_HTTP_TIMEOUT", 30*time.Second), }, @@ -66,18 +64,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Rackspace. -// It authenticates against the API, also grabbing the DNS Endpoint. -// Deprecated -func NewDNSProviderCredentials(user, key string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIUser = user - config.APIKey = key - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Rackspace. // It authenticates against the API, also grabbing the DNS Endpoint. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { @@ -117,7 +103,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zoneID, err := d.getHostedZoneID(fqdn) if err != nil { @@ -126,7 +112,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { rec := Records{ Record: []Record{{ - Name: acme.UnFqdn(fqdn), + Name: dns01.UnFqdn(fqdn), Type: "TXT", Data: value, TTL: d.config.TTL, @@ -147,7 +133,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneID, err := d.getHostedZoneID(fqdn) if err != nil { @@ -171,127 +157,3 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { return d.config.PropagationTimeout, d.config.PollingInterval } - -// getHostedZoneID performs a lookup to get the DNS zone which needs -// modifying for a given FQDN -func (d *DNSProvider) getHostedZoneID(fqdn string) (int, error) { - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) - if err != nil { - return 0, err - } - - result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains?name=%s", acme.UnFqdn(authZone)), nil) - if err != nil { - return 0, err - } - - var zoneSearchResponse ZoneSearchResponse - err = json.Unmarshal(result, &zoneSearchResponse) - if err != nil { - return 0, err - } - - // If nothing was returned, or for whatever reason more than 1 was returned (the search uses exact match, so should not occur) - if zoneSearchResponse.TotalEntries != 1 { - return 0, fmt.Errorf("found %d zones for %s in Rackspace for domain %s", zoneSearchResponse.TotalEntries, authZone, fqdn) - } - - return zoneSearchResponse.HostedZones[0].ID, nil -} - -// findTxtRecord searches a DNS zone for a TXT record with a specific name -func (d *DNSProvider) findTxtRecord(fqdn string, zoneID int) (*Record, error) { - result, err := d.makeRequest(http.MethodGet, fmt.Sprintf("/domains/%d/records?type=TXT&name=%s", zoneID, acme.UnFqdn(fqdn)), nil) - if err != nil { - return nil, err - } - - var records Records - err = json.Unmarshal(result, &records) - if err != nil { - return nil, err - } - - switch len(records.Record) { - case 1: - case 0: - return nil, fmt.Errorf("no TXT record found for %s", fqdn) - default: - return nil, fmt.Errorf("more than 1 TXT record found for %s", fqdn) - } - - return &records.Record[0], nil -} - -// makeRequest is a wrapper function used for making DNS API requests -func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) { - url := d.cloudDNSEndpoint + uri - - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - - req.Header.Set("X-Auth-Token", d.token) - req.Header.Set("Content-Type", "application/json") - - resp, err := d.config.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error querying DNS API: %v", err) - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { - return nil, fmt.Errorf("request failed for %s %s. Response code: %d", method, url, resp.StatusCode) - } - - var r json.RawMessage - err = json.NewDecoder(resp.Body).Decode(&r) - if err != nil { - return nil, fmt.Errorf("JSON decode failed for %s %s. Response code: %d", method, url, resp.StatusCode) - } - - return r, nil -} - -func login(config *Config) (*Identity, error) { - authData := AuthData{ - Auth: Auth{ - APIKeyCredentials: APIKeyCredentials{ - Username: config.APIUser, - APIKey: config.APIKey, - }, - }, - } - - body, err := json.Marshal(authData) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, config.BaseURL, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := config.HTTPClient.Do(req) - if err != nil { - return nil, fmt.Errorf("error querying Identity API: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("authentication failed: response code: %d", resp.StatusCode) - } - - var identity Identity - err = json.NewDecoder(resp.Body).Decode(&identity) - if err != nil { - return nil, err - } - - return &identity, nil -} diff --git a/providers/dns/rackspace/rackspace_mock_test.go b/providers/dns/rackspace/rackspace_mock_test.go new file mode 100644 index 00000000..0874543d --- /dev/null +++ b/providers/dns/rackspace/rackspace_mock_test.go @@ -0,0 +1,87 @@ +package rackspace + +const recordDeleteMock = ` +{ + "status": "RUNNING", + "verb": "DELETE", + "jobId": "00000000-0000-0000-0000-0000000000", + "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", + "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321" +} +` + +const recordDetailsMock = ` +{ + "records": [ + { + "name": "_acme-challenge.example.com", + "id": "TXT-654321", + "type": "TXT", + "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM", + "ttl": 300, + "updated": "1970-01-01T00:00:00.000+0000", + "created": "1970-01-01T00:00:00.000+0000" + } + ] +} +` + +const zoneDetailsMock = ` +{ + "domains": [ + { + "name": "example.com", + "id": 112233, + "emailAddress": "hostmaster@example.com", + "updated": "1970-01-01T00:00:00.000+0000", + "created": "1970-01-01T00:00:00.000+0000" + } + ], + "totalEntries": 1 +} +` + +const identityResponseMock = ` +{ + "access": { + "token": { + "id": "testToken", + "expires": "1970-01-01T00:00:00.000Z", + "tenant": { + "id": "123456", + "name": "123456" + }, + "RAX-AUTH:authenticatedBy": [ + "APIKEY" + ] + }, + "serviceCatalog": [ + { + "type": "rax:dns", + "endpoints": [ + { + "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456", + "tenantId": "123456" + } + ], + "name": "cloudDNS" + } + ], + "user": { + "id": "fakeUseID", + "name": "testUser" + } + } +} +` + +const recordResponseMock = ` +{ + "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}", + "status": "RUNNING", + "verb": "POST", + "jobId": "00000000-0000-0000-0000-0000000000", + "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000", + "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records" +} +` diff --git a/providers/dns/rackspace/rackspace_test.go b/providers/dns/rackspace/rackspace_test.go index 874e6721..fcaef794 100644 --- a/providers/dns/rackspace/rackspace_test.go +++ b/providers/dns/rackspace/rackspace_test.go @@ -128,13 +128,12 @@ func identityHandler(dnsEndpoint string) http.Handler { return } - resp, found := jsonMap[string(reqBody)] - if !found { + if string(reqBody) != `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}` { w.WriteHeader(http.StatusBadRequest) return } - resp = strings.Replace(resp, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1) + resp := strings.Replace(identityResponseMock, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, resp) }) @@ -147,7 +146,7 @@ func dnsHandler() *http.ServeMux { mux.HandleFunc("/123456/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("name") == "example.com" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["zoneDetails"]) + fmt.Fprintf(w, zoneDetailsMock) return } w.WriteHeader(http.StatusBadRequest) @@ -162,18 +161,19 @@ func dnsHandler() *http.ServeMux { http.Error(w, err.Error(), http.StatusInternalServerError) return } - resp, found := jsonMap[string(reqBody)] - if !found { + + if string(reqBody) != `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}` { w.WriteHeader(http.StatusBadRequest) return } + w.WriteHeader(http.StatusAccepted) - fmt.Fprintf(w, resp) + fmt.Fprintf(w, recordResponseMock) // Used by `findTxtRecord()` finding `record.ID` "?type=TXT&name=_acme-challenge.example.com" case http.MethodGet: if r.URL.Query().Get("type") == "TXT" && r.URL.Query().Get("name") == "_acme-challenge.example.com" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["recordDetails"]) + fmt.Fprintf(w, recordDetailsMock) return } w.WriteHeader(http.StatusBadRequest) @@ -182,7 +182,7 @@ func dnsHandler() *http.ServeMux { case http.MethodDelete: if r.URL.Query().Get("id") == "TXT-654321" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["recordDelete"]) + fmt.Fprintf(w, recordDeleteMock) return } w.WriteHeader(http.StatusBadRequest) @@ -196,11 +196,3 @@ func dnsHandler() *http.ServeMux { return mux } - -var jsonMap = map[string]string{ - `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}`: `{"access":{"token":{"id":"testToken","expires":"1970-01-01T00:00:00.000Z","tenant":{"id":"123456","name":"123456"},"RAX-AUTH:authenticatedBy":["APIKEY"]},"serviceCatalog":[{"type":"rax:dns","endpoints":[{"publicURL":"https://dns.api.rackspacecloud.com/v1.0/123456","tenantId":"123456"}],"name":"cloudDNS"}],"user":{"id":"fakeUseID","name":"testUser"}}}`, - "zoneDetails": `{"domains":[{"name":"example.com","id":112233,"emailAddress":"hostmaster@example.com","updated":"1970-01-01T00:00:00.000+0000","created":"1970-01-01T00:00:00.000+0000"}],"totalEntries":1}`, - `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}`: `{"request":"{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}","status":"RUNNING","verb":"POST","jobId":"00000000-0000-0000-0000-0000000000","callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000","requestUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records"}`, - "recordDetails": `{"records":[{"name":"_acme-challenge.example.com","id":"TXT-654321","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300,"updated":"1970-01-01T00:00:00.000+0000","created":"1970-01-01T00:00:00.000+0000"}]}`, - "recordDelete": `{"status":"RUNNING","verb":"DELETE","jobId":"00000000-0000-0000-0000-0000000000","callbackUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000","requestUrl":"https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321"}`, -} diff --git a/providers/dns/rfc2136/rfc2136.go b/providers/dns/rfc2136/rfc2136.go index 797b90a8..dcc80197 100644 --- a/providers/dns/rfc2136/rfc2136.go +++ b/providers/dns/rfc2136/rfc2136.go @@ -1,5 +1,4 @@ -// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge -// using the rfc2136 dynamic update. +// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update. package rfc2136 import ( @@ -10,12 +9,10 @@ import ( "time" "github.com/miekg/dns" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) -const defaultTimeout = 60 * time.Second - // Config is used to configure the creation of the DNSProvider type Config struct { Nameserver string @@ -31,7 +28,7 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TSIGAlgorithm: env.GetOrDefaultString("RFC2136_TSIG_ALGORITHM", dns.HmacMD5), - TTL: env.GetOrDefaultInt("RFC2136_TTL", 120), + TTL: env.GetOrDefaultInt("RFC2136_TTL", dns01.DefaultTTL), PropagationTimeout: env.GetOrDefaultSecond("RFC2136_PROPAGATION_TIMEOUT", env.GetOrDefaultSecond("RFC2136_TIMEOUT", 60*time.Second)), PollingInterval: env.GetOrDefaultSecond("RFC2136_POLLING_INTERVAL", 2*time.Second), @@ -67,34 +64,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for rfc2136 dynamic update. -// To disable TSIG authentication, leave the TSIG parameters as empty strings. -// nameserver must be a network address in the form "host" or "host:port". -// Deprecated -func NewDNSProviderCredentials(nameserver, tsigAlgorithm, tsigKey, tsigSecret, rawTimeout string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Nameserver = nameserver - config.TSIGAlgorithm = tsigAlgorithm - config.TSIGKey = tsigKey - config.TSIGSecret = tsigSecret - - timeout := defaultTimeout - if rawTimeout != "" { - t, err := time.ParseDuration(rawTimeout) - if err != nil { - return nil, err - } else if t < 0 { - return nil, fmt.Errorf("rfc2136: invalid/negative RFC2136_TIMEOUT: %v", rawTimeout) - } else { - timeout = t - } - } - config.PropagationTimeout = timeout - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for rfc2136. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -135,7 +104,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("INSERT", fqdn, value, d.config.TTL) if err != nil { @@ -146,7 +115,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) err := d.changeRecord("REMOVE", fqdn, value, d.config.TTL) if err != nil { @@ -157,7 +126,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { func (d *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error { // Find the zone for the given fqdn - zone, err := acme.FindZoneByFqdn(fqdn, []string{d.config.Nameserver}) + zone, err := dns01.FindZoneByFqdnCustom(fqdn, []string{d.config.Nameserver}) if err != nil { return err } diff --git a/providers/dns/rfc2136/rfc2136_test.go b/providers/dns/rfc2136/rfc2136_test.go index a3625fe1..152ff2e7 100644 --- a/providers/dns/rfc2136/rfc2136_test.go +++ b/providers/dns/rfc2136/rfc2136_test.go @@ -12,10 +12,10 @@ import ( "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" ) -var ( +const ( envTestDomain = "123456789.www.example.com" envTestKeyAuth = "123d==" envTestValue = "Now36o-3BmlB623-0c1qCIUmgWVVmDJb88KGl24pqpo" @@ -26,10 +26,8 @@ var ( envTestTsigSecret = "IwBTJx9wrDp4Y1RyC3H0gA==" ) -var reqChan = make(chan *dns.Msg, 10) - func TestCanaryLocalTestServer(t *testing.T) { - acme.ClearFqdnCache() + dns01.ClearFqdnCache() dns.HandleFunc("example.com.", serverHandlerHello) defer dns.HandleRemove("example.com.") @@ -51,7 +49,7 @@ func TestCanaryLocalTestServer(t *testing.T) { } func TestServerSuccess(t *testing.T) { - acme.ClearFqdnCache() + dns01.ClearFqdnCache() dns.HandleFunc(envTestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(envTestZone) @@ -70,7 +68,7 @@ func TestServerSuccess(t *testing.T) { } func TestServerError(t *testing.T) { - acme.ClearFqdnCache() + dns01.ClearFqdnCache() dns.HandleFunc(envTestZone, serverHandlerReturnErr) defer dns.HandleRemove(envTestZone) @@ -92,7 +90,7 @@ func TestServerError(t *testing.T) { } func TestTsigClient(t *testing.T) { - acme.ClearFqdnCache() + dns01.ClearFqdnCache() dns.HandleFunc(envTestZone, serverHandlerReturnSuccess) defer dns.HandleRemove(envTestZone) @@ -113,8 +111,10 @@ func TestTsigClient(t *testing.T) { } func TestValidUpdatePacket(t *testing.T) { - acme.ClearFqdnCache() - dns.HandleFunc(envTestZone, serverHandlerPassBackRequest) + var reqChan = make(chan *dns.Msg, 10) + + dns01.ClearFqdnCache() + dns.HandleFunc(envTestZone, serverHandlerPassBackRequest(reqChan)) defer dns.HandleRemove(envTestZone) server, addr, err := runLocalDNSTestServer(false) @@ -163,7 +163,15 @@ func runLocalDNSTestServer(tsig bool) (*dns.Server, string, error) { return nil, "", err } - server := &dns.Server{PacketConn: pc, ReadTimeout: time.Hour, WriteTimeout: time.Hour} + server := &dns.Server{ + PacketConn: pc, + ReadTimeout: time.Hour, + WriteTimeout: time.Hour, + MsgAcceptFunc: func(dh dns.Header) dns.MsgAcceptAction { + // bypass defaultMsgAcceptFunc to allow dynamic update (https://github.com/miekg/dns/pull/830) + return dns.MsgAccept + }} + if tsig { server.TsigSecret = map[string]string{envTestTsigKey: envTestTsigSecret} } @@ -217,25 +225,27 @@ func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) { _ = w.WriteMsg(m) } -func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) { - m := new(dns.Msg) - m.SetReply(req) - if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { - // Return SOA to appease findZoneByFqdn() - soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", envTestZone, envTestTTL, envTestZone, envTestZone)) - m.Answer = []dns.RR{soaRR} - } +func serverHandlerPassBackRequest(reqChan chan *dns.Msg) func(w dns.ResponseWriter, req *dns.Msg) { + return func(w dns.ResponseWriter, req *dns.Msg) { + m := new(dns.Msg) + m.SetReply(req) + if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET { + // Return SOA to appease findZoneByFqdn() + soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", envTestZone, envTestTTL, envTestZone, envTestZone)) + m.Answer = []dns.RR{soaRR} + } - if t := req.IsTsig(); t != nil { - if w.TsigStatus() == nil { - // Validated - m.SetTsig(envTestZone, dns.HmacMD5, 300, time.Now().Unix()) + if t := req.IsTsig(); t != nil { + if w.TsigStatus() == nil { + // Validated + m.SetTsig(envTestZone, dns.HmacMD5, 300, time.Now().Unix()) + } + } + + _ = w.WriteMsg(m) + if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { + // Only talk back when it is not the SOA RR. + reqChan <- req } } - - _ = w.WriteMsg(m) - if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET { - // Only talk back when it is not the SOA RR. - reqChan <- req - } } diff --git a/providers/dns/route53/fixtures_test.go b/providers/dns/route53/fixtures_test.go index a5cc9c87..444a8800 100644 --- a/providers/dns/route53/fixtures_test.go +++ b/providers/dns/route53/fixtures_test.go @@ -1,6 +1,6 @@ package route53 -var ChangeResourceRecordSetsResponse = ` +const ChangeResourceRecordSetsResponse = ` /change/123456 @@ -9,7 +9,7 @@ var ChangeResourceRecordSetsResponse = ` ` -var ListHostedZonesByNameResponse = ` +const ListHostedZonesByNameResponse = ` @@ -29,7 +29,7 @@ var ListHostedZonesByNameResponse = ` 1 ` -var GetChangeResponse = ` +const GetChangeResponse = ` 123456 diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go index 1cace78e..6a70ae09 100644 --- a/providers/dns/route53/route53.go +++ b/providers/dns/route53/route53.go @@ -1,5 +1,4 @@ -// Package route53 implements a DNS provider for solving the DNS-01 challenge -// using AWS Route 53 DNS. +// Package route53 implements a DNS provider for solving the DNS-01 challenge using AWS Route 53 DNS. package route53 import ( @@ -14,8 +13,9 @@ import ( "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/platform/wait" ) // Config is used to configure the creation of the DNSProvider @@ -107,7 +107,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record using the specified parameters func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) hostedZoneID, err := d.getHostedZoneID(fqdn) if err != nil { @@ -148,7 +148,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) hostedZoneID, err := d.getHostedZoneID(fqdn) if err != nil { @@ -197,7 +197,7 @@ func (d *DNSProvider) changeRecord(action, hostedZoneID string, recordSet *route changeID := resp.ChangeInfo.Id - return acme.WaitFor(d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { + return wait.For(d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) { reqParams := &route53.GetChangeInput{Id: changeID} resp, err := d.client.GetChange(reqParams) @@ -244,14 +244,14 @@ func (d *DNSProvider) getHostedZoneID(fqdn string) (string, error) { return d.config.HostedZoneID, nil } - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return "", err } // .DNSName should not have a trailing dot reqParams := &route53.ListHostedZonesByNameInput{ - DNSName: aws.String(acme.UnFqdn(authZone)), + DNSName: aws.String(dns01.UnFqdn(authZone)), } resp, err := d.client.ListHostedZonesByName(reqParams) if err != nil { diff --git a/providers/dns/sakuracloud/sakuracloud.go b/providers/dns/sakuracloud/sakuracloud.go index b0227f6c..54c92a9f 100644 --- a/providers/dns/sakuracloud/sakuracloud.go +++ b/providers/dns/sakuracloud/sakuracloud.go @@ -1,5 +1,4 @@ -// Package sakuracloud implements a DNS provider for solving the DNS-01 challenge -// using sakuracloud DNS. +// Package sakuracloud implements a DNS provider for solving the DNS-01 challenge using SakuraCloud DNS. package sakuracloud import ( @@ -11,7 +10,7 @@ import ( "github.com/sacloud/libsacloud/api" "github.com/sacloud/libsacloud/sacloud" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -27,9 +26,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("SAKURACLOUD_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("SAKURACLOUD_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("SAKURACLOUD_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("SAKURACLOUD_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("SAKURACLOUD_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("SAKURACLOUD_POLLING_INTERVAL", dns01.DefaultPollingInterval), } } @@ -39,7 +38,7 @@ type DNSProvider struct { client *api.Client } -// NewDNSProvider returns a DNSProvider instance configured for sakuracloud. +// NewDNSProvider returns a DNSProvider instance configured for SakuraCloud. // Credentials must be passed in the environment variables: SAKURACLOUD_ACCESS_TOKEN & SAKURACLOUD_ACCESS_TOKEN_SECRET func NewDNSProvider() (*DNSProvider, error) { values, err := env.Get("SAKURACLOUD_ACCESS_TOKEN", "SAKURACLOUD_ACCESS_TOKEN_SECRET") @@ -54,18 +53,7 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for sakuracloud. -// Deprecated -func NewDNSProviderCredentials(token, secret string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.Token = token - config.Secret = secret - - return NewDNSProviderConfig(config) -} - -// NewDNSProviderConfig return a DNSProvider instance configured for GleSYS. +// NewDNSProviderConfig return a DNSProvider instance configured for SakuraCloud. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { return nil, errors.New("sakuracloud: the configuration of the DNS provider is nil") @@ -80,14 +68,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { } client := api.NewClient(config.Token, config.Secret, "tk1a") - client.UserAgent = acme.UserAgent return &DNSProvider{client: client, config: config}, nil } // Present creates a TXT record to fulfill the dns-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(domain) if err != nil { @@ -107,7 +94,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zone, err := d.getHostedZone(domain) if err != nil { @@ -144,12 +131,12 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { } func (d *DNSProvider) getHostedZone(domain string) (*sacloud.DNS, error) { - authZone, err := acme.FindZoneByFqdn(acme.ToFqdn(domain), acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(dns01.ToFqdn(domain)) if err != nil { return nil, err } - zoneName := acme.UnFqdn(authZone) + zoneName := dns01.UnFqdn(authZone) res, err := d.client.GetDNSAPI().WithNameLike(zoneName).Find() if err != nil { @@ -181,7 +168,7 @@ func (d *DNSProvider) findTxtRecords(fqdn string, zone *sacloud.DNS) ([]sacloud. } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/dns/selectel/client.go b/providers/dns/selectel/internal/client.go similarity index 99% rename from providers/dns/selectel/client.go rename to providers/dns/selectel/internal/client.go index cb02230a..129d2b95 100644 --- a/providers/dns/selectel/client.go +++ b/providers/dns/selectel/internal/client.go @@ -1,4 +1,4 @@ -package selectel +package internal import ( "bytes" diff --git a/providers/dns/selectel/selectel.go b/providers/dns/selectel/selectel.go index 9e48b85d..f1e23965 100644 --- a/providers/dns/selectel/selectel.go +++ b/providers/dns/selectel/selectel.go @@ -9,8 +9,9 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" + "github.com/xenolf/lego/providers/dns/selectel/internal" ) const ( @@ -54,7 +55,7 @@ func NewDefaultConfig() *Config { // DNSProvider is an implementation of the acme.ChallengeProvider interface. type DNSProvider struct { config *Config - client *Client + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Selectel Domains API. @@ -85,10 +86,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("selectel: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - client := NewClient(ClientOpts{ + client := internal.NewClient(internal.ClientOpts{ BaseURL: config.BaseURL, Token: config.Token, - UserAgent: acme.UserAgent, HTTPClient: config.HTTPClient, }) @@ -103,14 +103,14 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("selectel: %v", err) } - txtRecord := Record{ + txtRecord := internal.Record{ Type: "TXT", TTL: d.config.TTL, Name: fqdn, @@ -126,7 +126,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes a TXT record used for DNS-01 challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) domainObj, err := d.client.GetDomainByName(domain) if err != nil { diff --git a/providers/dns/stackpath/client.go b/providers/dns/stackpath/client.go index 495d8c55..4665ca56 100644 --- a/providers/dns/stackpath/client.go +++ b/providers/dns/stackpath/client.go @@ -8,7 +8,7 @@ import ( "net/http" "path" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "golang.org/x/net/publicsuffix" ) @@ -49,7 +49,7 @@ func (e *ErrorResponse) Error() string { // https://developer.stackpath.com/en/api/dns/#operation/GetZones func (d *DNSProvider) getZones(domain string) (*Zone, error) { - domain = acme.UnFqdn(domain) + domain = dns01.UnFqdn(domain) tld, err := publicsuffix.EffectiveTLDPlusOne(domain) if err != nil { return nil, err diff --git a/providers/dns/stackpath/stackpath.go b/providers/dns/stackpath/stackpath.go index 4c247def..df05aee7 100644 --- a/providers/dns/stackpath/stackpath.go +++ b/providers/dns/stackpath/stackpath.go @@ -6,13 +6,13 @@ import ( "context" "errors" "fmt" - "log" "net/http" "net/url" "strings" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/log" "github.com/xenolf/lego/platform/config/env" "golang.org/x/oauth2/clientcredentials" ) @@ -36,8 +36,8 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ TTL: env.GetOrDefaultInt("STACKPATH_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("STACKPATH_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("STACKPATH_POLLING_INTERVAL", acme.DefaultPollingInterval), + PropagationTimeout: env.GetOrDefaultSecond("STACKPATH_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("STACKPATH_POLLING_INTERVAL", dns01.DefaultPollingInterval), } } @@ -105,7 +105,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("stackpath: %v", err) } - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) parts := strings.Split(fqdn, ".") record := Record{ @@ -125,7 +125,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("stackpath: %v", err) } - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) parts := strings.Split(fqdn, ".") records, err := d.getZoneRecords(parts[0], zone) diff --git a/providers/dns/transip/transip.go b/providers/dns/transip/transip.go index aeb155f8..062fc91a 100644 --- a/providers/dns/transip/transip.go +++ b/providers/dns/transip/transip.go @@ -9,7 +9,7 @@ import ( "github.com/transip/gotransip" transipdomain "github.com/transip/gotransip/domain" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -78,17 +78,17 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } - domainName := acme.UnFqdn(authZone) + domainName := dns01.UnFqdn(authZone) // get the subDomain - subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + subDomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+domainName) // get all DNS entries info, err := transipdomain.GetInfo(d.client, domainName) @@ -114,17 +114,17 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) - authZone, err := acme.FindZoneByFqdn(fqdn, acme.RecursiveNameservers) + authZone, err := dns01.FindZoneByFqdn(fqdn) if err != nil { return err } - domainName := acme.UnFqdn(authZone) + domainName := dns01.UnFqdn(authZone) // get the subDomain - subDomain := strings.TrimSuffix(acme.UnFqdn(fqdn), "."+domainName) + subDomain := strings.TrimSuffix(dns01.UnFqdn(fqdn), "."+domainName) // get all DNS entries info, err := transipdomain.GetInfo(d.client, domainName) diff --git a/providers/dns/vegadns/vegadns.go b/providers/dns/vegadns/vegadns.go index 6b4cf7d1..c1e218da 100644 --- a/providers/dns/vegadns/vegadns.go +++ b/providers/dns/vegadns/vegadns.go @@ -1,5 +1,4 @@ -// Package vegadns implements a DNS provider for solving the DNS-01 -// challenge using VegaDNS. +// Package vegadns implements a DNS provider for solving the DNS-01 challenge using VegaDNS. package vegadns import ( @@ -9,7 +8,7 @@ import ( "time" vegaClient "github.com/OpenDNS/vegadns2client" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -55,18 +54,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for VegaDNS. -// Deprecated -func NewDNSProviderCredentials(vegaDNSURL string, key string, secret string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.BaseURL = vegaDNSURL - config.APIKey = key - config.APISecret = secret - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for VegaDNS. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -88,7 +75,7 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill the dns-01 challenge func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) _, domainID, err := d.client.GetAuthZone(fqdn) if err != nil { @@ -104,7 +91,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) _, domainID, err := d.client.GetAuthZone(fqdn) if err != nil { diff --git a/providers/dns/vegadns/vegadns_mock_test.go b/providers/dns/vegadns/vegadns_mock_test.go new file mode 100644 index 00000000..5a705e09 --- /dev/null +++ b/providers/dns/vegadns/vegadns_mock_test.go @@ -0,0 +1,85 @@ +package vegadns + +const tokenResponseMock = ` +{ + "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", + "token_type":"bearer", + "expires_in":3600 +} +` + +const domainsResponseMock = ` +{ + "domains":[ + { + "domain_id":1, + "domain":"example.com", + "status":"active", + "owner_id":0 + } + ] +} +` + +const recordsResponseMock = ` +{ + "status":"ok", + "total_records":2, + "domain":{ + "status":"active", + "domain":"example.com", + "owner_id":0, + "domain_id":1 + }, + "records":[ + { + "retry":"2048", + "minimum":"2560", + "refresh":"16384", + "email":"hostmaster.example.com", + "record_type":"SOA", + "expire":"1048576", + "ttl":86400, + "record_id":1, + "nameserver":"ns1.example.com", + "domain_id":1, + "serial":"" + }, + { + "name":"example.com", + "value":"ns1.example.com", + "record_type":"NS", + "ttl":3600, + "record_id":2, + "location_id":null, + "domain_id":1 + }, + { + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } + ] +} +` + +const recordCreatedResponseMock = ` +{ + "status":"ok", + "record":{ + "name":"_acme-challenge.example.com", + "value":"my_challenge", + "record_type":"TXT", + "ttl":3600, + "record_id":3, + "location_id":null, + "domain_id":1 + } +} +` + +const recordDeletedResponseMock = `{"status": "ok"}` diff --git a/providers/dns/vegadns/vegadns_test.go b/providers/dns/vegadns/vegadns_test.go index 69888d7e..97414f33 100644 --- a/providers/dns/vegadns/vegadns_test.go +++ b/providers/dns/vegadns/vegadns_test.go @@ -18,89 +18,7 @@ import ( const testDomain = "example.com" -var ipPort = "127.0.0.1:2112" - -var jsonMap = map[string]string{ - "token": ` -{ - "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f", - "token_type":"bearer", - "expires_in":3600 -} -`, - "domains": ` -{ - "domains":[ - { - "domain_id":1, - "domain":"example.com", - "status":"active", - "owner_id":0 - } - ] -} -`, - "records": ` -{ - "status":"ok", - "total_records":2, - "domain":{ - "status":"active", - "domain":"example.com", - "owner_id":0, - "domain_id":1 - }, - "records":[ - { - "retry":"2048", - "minimum":"2560", - "refresh":"16384", - "email":"hostmaster.example.com", - "record_type":"SOA", - "expire":"1048576", - "ttl":86400, - "record_id":1, - "nameserver":"ns1.example.com", - "domain_id":1, - "serial":"" - }, - { - "name":"example.com", - "value":"ns1.example.com", - "record_type":"NS", - "ttl":3600, - "record_id":2, - "location_id":null, - "domain_id":1 - }, - { - "name":"_acme-challenge.example.com", - "value":"my_challenge", - "record_type":"TXT", - "ttl":3600, - "record_id":3, - "location_id":null, - "domain_id":1 - } - ] -} -`, - "recordCreated": ` -{ - "status":"ok", - "record":{ - "name":"_acme-challenge.example.com", - "value":"my_challenge", - "record_type":"TXT", - "ttl":3600, - "record_id":3, - "location_id":null, - "domain_id":1 - } -} -`, - "recordDeleted": `{"status": "ok"}`, -} +const ipPort = "127.0.0.1:2112" var envTest = tester.NewEnvTest("SECRET_VEGADNS_KEY", "SECRET_VEGADNS_SECRET", "VEGADNS_URL") @@ -225,10 +143,9 @@ func muxSuccess() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodPost: + if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["token"]) + fmt.Fprintf(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) @@ -237,7 +154,7 @@ func muxSuccess() *http.ServeMux { mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == "example.com" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["domains"]) + fmt.Fprintf(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) @@ -248,24 +165,23 @@ func muxSuccess() *http.ServeMux { case http.MethodGet: if r.URL.Query().Get("domain_id") == "1" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["records"]) + fmt.Fprintf(w, recordsResponseMock) return } w.WriteHeader(http.StatusNotFound) return case http.MethodPost: w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, jsonMap["recordCreated"]) + fmt.Fprintf(w, recordCreatedResponseMock) return } w.WriteHeader(http.StatusBadRequest) }) mux.HandleFunc("/1.0/records/3", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodDelete: + if r.Method == http.MethodDelete { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["recordDeleted"]) + fmt.Fprintf(w, recordDeletedResponseMock) return } w.WriteHeader(http.StatusBadRequest) @@ -283,10 +199,9 @@ func muxFailToFindZone() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodPost: + if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["token"]) + fmt.Fprintf(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) @@ -303,10 +218,9 @@ func muxFailToCreateTXT() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodPost: + if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["token"]) + fmt.Fprintf(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) @@ -315,7 +229,7 @@ func muxFailToCreateTXT() *http.ServeMux { mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == testDomain { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["domains"]) + fmt.Fprintf(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) @@ -326,7 +240,7 @@ func muxFailToCreateTXT() *http.ServeMux { case http.MethodGet: if r.URL.Query().Get("domain_id") == "1" { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["records"]) + fmt.Fprintf(w, recordsResponseMock) return } w.WriteHeader(http.StatusNotFound) @@ -345,10 +259,9 @@ func muxFailToGetRecordID() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodPost: + if r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["token"]) + fmt.Fprintf(w, tokenResponseMock) return } w.WriteHeader(http.StatusBadRequest) @@ -357,15 +270,14 @@ func muxFailToGetRecordID() *http.ServeMux { mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("search") == testDomain { w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, jsonMap["domains"]) + fmt.Fprintf(w, domainsResponseMock) return } w.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: + if r.Method == http.MethodGet { w.WriteHeader(http.StatusNotFound) return } diff --git a/providers/dns/vscale/client.go b/providers/dns/vscale/internal/client.go similarity index 99% rename from providers/dns/vscale/client.go rename to providers/dns/vscale/internal/client.go index 1f4826f2..129d2b95 100644 --- a/providers/dns/vscale/client.go +++ b/providers/dns/vscale/internal/client.go @@ -1,4 +1,4 @@ -package vscale +package internal import ( "bytes" diff --git a/providers/dns/vscale/vscale.go b/providers/dns/vscale/vscale.go index b44b159d..69060be3 100644 --- a/providers/dns/vscale/vscale.go +++ b/providers/dns/vscale/vscale.go @@ -1,4 +1,4 @@ -// Package selectel implements a DNS provider for solving the DNS-01 challenge using Vscale Domains API. +// Package vscale implements a DNS provider for solving the DNS-01 challenge using Vscale Domains API. // Vscale Domain API reference: https://developers.vscale.io/documentation/api/v1/#api-Domains // Token: https://vscale.io/panel/settings/tokens/ package vscale @@ -9,7 +9,9 @@ import ( "net/http" "time" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" + "github.com/xenolf/lego/providers/dns/vscale/internal" + "github.com/xenolf/lego/platform/config/env" ) @@ -54,7 +56,7 @@ func NewDefaultConfig() *Config { // DNSProvider is an implementation of the acme.ChallengeProvider interface. type DNSProvider struct { config *Config - client *Client + client *internal.Client } // NewDNSProvider returns a DNSProvider instance configured for Vscale Domains API. @@ -85,10 +87,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("vscale: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL) } - client := NewClient(ClientOpts{ + client := internal.NewClient(internal.ClientOpts{ BaseURL: config.BaseURL, Token: config.Token, - UserAgent: acme.UserAgent, HTTPClient: config.HTTPClient, }) @@ -103,14 +104,14 @@ func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { // Present creates a TXT record to fulfill DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) domainObj, err := d.client.GetDomainByName(domain) if err != nil { return fmt.Errorf("vscale: %v", err) } - txtRecord := Record{ + txtRecord := internal.Record{ Type: "TXT", TTL: d.config.TTL, Name: fqdn, @@ -126,7 +127,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes a TXT record used for DNS-01 challenge. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) domainObj, err := d.client.GetDomainByName(domain) if err != nil { diff --git a/providers/dns/vultr/vultr.go b/providers/dns/vultr/vultr.go index 80feb9f6..9faa3d60 100644 --- a/providers/dns/vultr/vultr.go +++ b/providers/dns/vultr/vultr.go @@ -1,5 +1,4 @@ -// Package vultr implements a DNS provider for solving the DNS-01 challenge using -// the vultr DNS. +// Package vultr implements a DNS provider for solving the DNS-01 challenge using the vultr DNS. // See https://www.vultr.com/api/#dns package vultr @@ -12,7 +11,7 @@ import ( "time" vultr "github.com/JamesClonk/vultr/lib" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/dns01" "github.com/xenolf/lego/platform/config/env" ) @@ -28,9 +27,9 @@ type Config struct { // NewDefaultConfig returns a default configuration for the DNSProvider func NewDefaultConfig() *Config { return &Config{ - TTL: env.GetOrDefaultInt("VULTR_TTL", 120), - PropagationTimeout: env.GetOrDefaultSecond("VULTR_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout), - PollingInterval: env.GetOrDefaultSecond("VULTR_POLLING_INTERVAL", acme.DefaultPollingInterval), + TTL: env.GetOrDefaultInt("VULTR_TTL", dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond("VULTR_PROPAGATION_TIMEOUT", dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond("VULTR_POLLING_INTERVAL", dns01.DefaultPollingInterval), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond("VULTR_HTTP_TIMEOUT", 0), // from Vultr Client @@ -61,16 +60,6 @@ func NewDNSProvider() (*DNSProvider, error) { return NewDNSProviderConfig(config) } -// NewDNSProviderCredentials uses the supplied credentials -// to return a DNSProvider instance configured for Vultr. -// Deprecated -func NewDNSProviderCredentials(apiKey string) (*DNSProvider, error) { - config := NewDefaultConfig() - config.APIKey = apiKey - - return NewDNSProviderConfig(config) -} - // NewDNSProviderConfig return a DNSProvider instance configured for Vultr. func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { if config == nil { @@ -83,7 +72,6 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { options := &vultr.Options{ HTTPClient: config.HTTPClient, - UserAgent: acme.UserAgent, } client := vultr.NewClient(config.APIKey, options) @@ -92,7 +80,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { // Present creates a TXT record to fulfill the DNS-01 challenge. func (d *DNSProvider) Present(domain, token, keyAuth string) error { - fqdn, value, _ := acme.DNS01Record(domain, keyAuth) + fqdn, value := dns01.GetRecord(domain, keyAuth) zoneDomain, err := d.getHostedZone(domain) if err != nil { @@ -111,7 +99,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the TXT record matching the specified parameters. func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { - fqdn, _, _ := acme.DNS01Record(domain, keyAuth) + fqdn, _ := dns01.GetRecord(domain, keyAuth) zoneDomain, records, err := d.findTxtRecords(domain, fqdn) if err != nil { @@ -183,7 +171,7 @@ func (d *DNSProvider) findTxtRecords(domain, fqdn string) (string, []vultr.DNSRe } func (d *DNSProvider) extractRecordName(fqdn, domain string) string { - name := acme.UnFqdn(fqdn) + name := dns01.UnFqdn(fqdn) if idx := strings.Index(name, "."+domain); idx != -1 { return name[:idx] } diff --git a/providers/http/memcached/memcached.go b/providers/http/memcached/memcached.go index fc625bc5..d64d7511 100644 --- a/providers/http/memcached/memcached.go +++ b/providers/http/memcached/memcached.go @@ -7,7 +7,7 @@ import ( "path" "github.com/rainycape/memcache" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/http01" ) // HTTPProvider implements HTTPProvider for `http-01` challenge @@ -32,7 +32,7 @@ func NewMemcachedProvider(hosts []string) (*HTTPProvider, error) { func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var errs []error - challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + challengePath := path.Join("/", http01.ChallengePath(token)) for _, host := range w.hosts { mc, err := memcache.New(host) if err != nil { diff --git a/providers/http/memcached/memcached_test.go b/providers/http/memcached/memcached_test.go index 7c61007b..bc994924 100644 --- a/providers/http/memcached/memcached_test.go +++ b/providers/http/memcached/memcached_test.go @@ -9,11 +9,7 @@ import ( "github.com/rainycape/memcache" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xenolf/lego/acme" -) - -var ( - memcachedHosts []string + "github.com/xenolf/lego/challenge/http01" ) const ( @@ -22,11 +18,14 @@ const ( keyAuth = "bar" ) -func init() { +var memcachedHosts = loadMemcachedHosts() + +func loadMemcachedHosts() []string { memcachedHostsStr := os.Getenv("MEMCACHED_HOSTS") if len(memcachedHostsStr) > 0 { - memcachedHosts = strings.Split(memcachedHostsStr, ",") + return strings.Split(memcachedHostsStr, ",") } + return nil } func TestNewMemcachedProviderEmpty(t *testing.T) { @@ -50,7 +49,7 @@ func TestMemcachedPresentSingleHost(t *testing.T) { p, err := NewMemcachedProvider(memcachedHosts[0:1]) require.NoError(t, err) - challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) @@ -68,7 +67,7 @@ func TestMemcachedPresentMultiHost(t *testing.T) { p, err := NewMemcachedProvider(memcachedHosts) require.NoError(t, err) - challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) @@ -89,7 +88,7 @@ func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) { p, err := NewMemcachedProvider(hosts) require.NoError(t, err) - challengePath := path.Join("/", acme.HTTP01ChallengePath(token)) + challengePath := path.Join("/", http01.ChallengePath(token)) err = p.Present(domain, token, keyAuth) require.NoError(t, err) diff --git a/providers/http/webroot/webroot.go b/providers/http/webroot/webroot.go index 680aa8ad..371db0f4 100644 --- a/providers/http/webroot/webroot.go +++ b/providers/http/webroot/webroot.go @@ -7,7 +7,7 @@ import ( "os" "path/filepath" - "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/challenge/http01" ) // HTTPProvider implements ChallengeProvider for `http-01` challenge @@ -28,7 +28,7 @@ func NewHTTPProvider(path string) (*HTTPProvider, error) { func (w *HTTPProvider) Present(domain, token, keyAuth string) error { var err error - challengeFilePath := filepath.Join(w.path, acme.HTTP01ChallengePath(token)) + challengeFilePath := filepath.Join(w.path, http01.ChallengePath(token)) err = os.MkdirAll(filepath.Dir(challengeFilePath), 0755) if err != nil { return fmt.Errorf("could not create required directories in webroot for HTTP challenge -> %v", err) @@ -44,7 +44,7 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { // CleanUp removes the file created for the challenge func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { - err := os.Remove(filepath.Join(w.path, acme.HTTP01ChallengePath(token))) + err := os.Remove(filepath.Join(w.path, http01.ChallengePath(token))) if err != nil { return fmt.Errorf("could not remove file in webroot after HTTP challenge -> %v", err) } diff --git a/registration/registar.go b/registration/registar.go new file mode 100644 index 00000000..a1b850ec --- /dev/null +++ b/registration/registar.go @@ -0,0 +1,146 @@ +package registration + +import ( + "errors" + "net/http" + + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/log" +) + +// Resource represents all important information about a registration +// of which the client needs to keep track itself. +// Deprecated: will be remove in the future (acme.ExtendedAccount). +type Resource struct { + Body acme.Account `json:"body,omitempty"` + URI string `json:"uri,omitempty"` +} + +type RegisterOptions struct { + TermsOfServiceAgreed bool +} + +type RegisterEABOptions struct { + TermsOfServiceAgreed bool + Kid string + HmacEncoded string +} + +type Registrar struct { + core *api.Core + user User +} + +func NewRegistrar(core *api.Core, user User) *Registrar { + return &Registrar{ + core: core, + user: user, + } +} + +// Register the current account to the ACME server. +func (r *Registrar) Register(options RegisterOptions) (*Resource, error) { + if r == nil || r.user == nil { + return nil, errors.New("acme: cannot register a nil client or user") + } + + accMsg := acme.Account{ + TermsOfServiceAgreed: options.TermsOfServiceAgreed, + Contact: []string{}, + } + + if r.user.GetEmail() != "" { + log.Infof("acme: Registering account for %s", r.user.GetEmail()) + accMsg.Contact = []string{"mailto:" + r.user.GetEmail()} + } + + account, err := r.core.Accounts.New(accMsg) + if err != nil { + // FIXME seems impossible + errorDetails, ok := err.(acme.ProblemDetails) + if !ok || errorDetails.HTTPStatus != http.StatusConflict { + return nil, err + } + } + + return &Resource{URI: account.Location, Body: account.Account}, nil +} + +// RegisterWithExternalAccountBinding Register the current account to the ACME server. +func (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOptions) (*Resource, error) { + accMsg := acme.Account{ + TermsOfServiceAgreed: options.TermsOfServiceAgreed, + Contact: []string{}, + } + + if r.user.GetEmail() != "" { + log.Infof("acme: Registering account for %s", r.user.GetEmail()) + accMsg.Contact = []string{"mailto:" + r.user.GetEmail()} + } + + account, err := r.core.Accounts.NewEAB(accMsg, options.Kid, options.HmacEncoded) + if err != nil { + errorDetails, ok := err.(acme.ProblemDetails) + // FIXME seems impossible + if !ok || errorDetails.HTTPStatus != http.StatusConflict { + return nil, err + } + } + + return &Resource{URI: account.Location, Body: account.Account}, nil +} + +// QueryRegistration runs a POST request on the client's registration and returns the result. +// +// This is similar to the Register function, +// but acting on an existing registration link and resource. +func (r *Registrar) QueryRegistration() (*Resource, error) { + if r == nil || r.user == nil { + return nil, errors.New("acme: cannot query the registration of a nil client or user") + } + + // Log the URL here instead of the email as the email may not be set + log.Infof("acme: Querying account for %s", r.user.GetRegistration().URI) + + account, err := r.core.Accounts.Get(r.user.GetRegistration().URI) + if err != nil { + return nil, err + } + + return &Resource{ + Body: account, + // Location: header is not returned so this needs to be populated off of existing URI + URI: r.user.GetRegistration().URI, + }, nil +} + +// DeleteRegistration deletes the client's user registration from the ACME server. +func (r *Registrar) DeleteRegistration() error { + if r == nil || r.user == nil { + return errors.New("acme: cannot unregister a nil client or user") + } + + log.Infof("acme: Deleting account for %s", r.user.GetEmail()) + + return r.core.Accounts.Deactivate(r.user.GetRegistration().URI) +} + +// ResolveAccountByKey will attempt to look up an account using the given account key +// and return its registration resource. +func (r *Registrar) ResolveAccountByKey() (*Resource, error) { + log.Infof("acme: Trying to resolve account by key") + + accMsg := acme.Account{OnlyReturnExisting: true} + accountTransit, err := r.core.Accounts.New(accMsg) + if err != nil { + return nil, err + } + + account, err := r.core.Accounts.Get(accountTransit.Location) + if err != nil { + return nil, err + } + + return &Resource{URI: accountTransit.Location, Body: account}, nil +} diff --git a/registration/registar_test.go b/registration/registar_test.go new file mode 100644 index 00000000..221d8c25 --- /dev/null +++ b/registration/registar_test.go @@ -0,0 +1,56 @@ +package registration + +import ( + "crypto/rand" + "crypto/rsa" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xenolf/lego/acme" + "github.com/xenolf/lego/acme/api" + "github.com/xenolf/lego/platform/tester" +) + +func TestRegistrar_ResolveAccountByKey(t *testing.T) { + mux, apiURL, tearDown := tester.SetupFakeAPI() + defer tearDown() + + mux.HandleFunc("/account", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", apiURL+"/account_recovery") + _, err := w.Write([]byte("{}")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/account_recovery", func(w http.ResponseWriter, r *http.Request) { + err := tester.WriteJSONResponse(w, acme.Account{ + Status: "valid", + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + key, err := rsa.GenerateKey(rand.Reader, 512) + require.NoError(t, err, "Could not generate test key") + + user := mockUser{ + email: "test@test.com", + regres: &Resource{}, + privatekey: key, + } + + core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", key) + require.NoError(t, err) + + registrar := NewRegistrar(core, user) + + res, err := registrar.ResolveAccountByKey() + require.NoError(t, err, "Unexpected error resolving account by key") + + assert.Equal(t, "valid", res.Body.Status, "Unexpected account status") +} diff --git a/registration/user.go b/registration/user.go new file mode 100644 index 00000000..1e29300e --- /dev/null +++ b/registration/user.go @@ -0,0 +1,13 @@ +package registration + +import ( + "crypto" +) + +// User interface is to be implemented by users of this library. +// It is used by the client type to get user specific information. +type User interface { + GetEmail() string + GetRegistration() *Resource + GetPrivateKey() crypto.PrivateKey +} diff --git a/registration/user_test.go b/registration/user_test.go new file mode 100644 index 00000000..b9c5de33 --- /dev/null +++ b/registration/user_test.go @@ -0,0 +1,16 @@ +package registration + +import ( + "crypto" + "crypto/rsa" +) + +type mockUser struct { + email string + regres *Resource + privatekey *rsa.PrivateKey +} + +func (u mockUser) GetEmail() string { return u.email } +func (u mockUser) GetRegistration() *Resource { return u.regres } +func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } diff --git a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/authentication.go b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/authentication.go index 48e375a7..dac6f51b 100644 --- a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/authentication.go +++ b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/authentication.go @@ -1,68 +1,52 @@ package dnsimple import ( - "encoding/base64" + "net/http" ) -const ( - httpHeaderDomainToken = "X-DNSimple-Domain-Token" - httpHeaderApiToken = "X-DNSimple-Token" - httpHeaderAuthorization = "Authorization" -) +// BasicAuthTransport is an http.RoundTripper that authenticates all requests +// using HTTP Basic Authentication with the provided username and password. +type BasicAuthTransport struct { + Username string + Password string -// Provides credentials that can be used for authenticating with DNSimple. -// -// See https://developer.dnsimple.com/v2/#authentication -type Credentials interface { - // Returns the HTTP headers that should be set - // to authenticate the HTTP Request. - Headers() map[string]string + // Transport is the transport RoundTripper used to make HTTP requests. + // If nil, http.DefaultTransport is used. + Transport http.RoundTripper } -// Domain token authentication -type domainTokenCredentials struct { - domainToken string +// RoundTrip implements the RoundTripper interface. We just add the +// basic auth and return the RoundTripper for this transport type. +func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := cloneRequest(req) // per RoundTripper contract + + req2.SetBasicAuth(t.Username, t.Password) + return t.transport().RoundTrip(req2) } -// NewDomainTokenCredentials construct Credentials using the DNSimple Domain Token method. -func NewDomainTokenCredentials(domainToken string) Credentials { - return &domainTokenCredentials{domainToken: domainToken} +// Client returns an *http.Client that uses the BasicAuthTransport transport +// to authenticate the request via HTTP Basic Auth. +func (t *BasicAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} } -func (c *domainTokenCredentials) Headers() map[string]string { - return map[string]string{httpHeaderDomainToken: c.domainToken} +func (t *BasicAuthTransport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport } -// HTTP basic authentication -type httpBasicCredentials struct { - email string - password string -} - -// NewHTTPBasicCredentials construct Credentials using HTTP Basic Auth. -func NewHTTPBasicCredentials(email, password string) Credentials { - return &httpBasicCredentials{email, password} -} - -func (c *httpBasicCredentials) Headers() map[string]string { - return map[string]string{httpHeaderAuthorization: "Basic " + c.basicAuth(c.email, c.password)} -} - -func (c *httpBasicCredentials) basicAuth(username, password string) string { - auth := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(auth)) -} - -// OAuth token authentication -type oauthTokenCredentials struct { - oauthToken string -} - -// NewOauthTokenCredentials construct Credentials using the OAuth access token. -func NewOauthTokenCredentials(oauthToken string) Credentials { - return &oauthTokenCredentials{oauthToken: oauthToken} -} - -func (c *oauthTokenCredentials) Headers() map[string]string { - return map[string]string{httpHeaderAuthorization: "Bearer " + c.oauthToken} +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header, len(r.Header)) + for k, s := range r.Header { + r2.Header[k] = append([]string(nil), s...) + } + return r2 } diff --git a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/dnsimple.go b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/dnsimple.go index 609c429d..6c32e163 100644 --- a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/dnsimple.go +++ b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/dnsimple.go @@ -23,7 +23,7 @@ const ( // This is a pro-forma convention given that Go dependencies // tends to be fetched directly from the repo. // It is also used in the user-agent identify the client. - Version = "0.16.0" + Version = "0.21.0" // defaultBaseURL to the DNSimple production API. defaultBaseURL = "https://api.dnsimple.com" @@ -37,12 +37,9 @@ const ( // Client represents a client to the DNSimple API. type Client struct { - // HttpClient is the underlying HTTP client + // httpClient is the underlying HTTP client // used to communicate with the API. - HttpClient *http.Client - - // Credentials used for accessing the DNSimple API - Credentials Credentials + httpClient *http.Client // BaseURL for API requests. // Defaults to the public DNSimple API, but can be set to a different endpoint (e.g. the sandbox). @@ -85,9 +82,12 @@ type ListOptions struct { Sort string `url:"sort,omitempty"` } -// NewClient returns a new DNSimple API client using the given credentials. -func NewClient(credentials Credentials) *Client { - c := &Client{Credentials: credentials, HttpClient: &http.Client{}, BaseURL: defaultBaseURL} +// NewClient returns a new DNSimple API client. +// +// To authenticate you must provide an http.Client that will perform authentication +// for you with one of the currently supported mechanisms: OAuth or HTTP Basic. +func NewClient(httpClient *http.Client) *Client { + c := &Client{httpClient: httpClient, BaseURL: defaultBaseURL} c.Identity = &IdentityService{client: c} c.Accounts = &AccountsService{client: c} c.Certificates = &CertificatesService{client: c} @@ -126,9 +126,6 @@ func (c *Client) NewRequest(method, path string, payload interface{}) (*http.Req req.Header.Set("Content-Type", "application/json") req.Header.Add("Accept", "application/json") req.Header.Add("User-Agent", formatUserAgent(c.UserAgent)) - for key, value := range c.Credentials.Headers() { - req.Header.Add(key, value) - } return req, nil } @@ -212,7 +209,7 @@ func (c *Client) Do(req *http.Request, obj interface{}) (*http.Response, error) log.Printf("Executing request (%v): %#v", req.URL, req) } - resp, err := c.HttpClient.Do(req) + resp, err := c.httpClient.Do(req) if err != nil { return nil, err } diff --git a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/oauth.go b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/oauth.go index fc209891..74a2b34a 100644 --- a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/oauth.go +++ b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/oauth.go @@ -72,7 +72,7 @@ func (s *OauthService) ExchangeAuthorizationForToken(authorization *ExchangeAuth return nil, err } - resp, err := s.client.HttpClient.Do(req) + resp, err := s.client.httpClient.Do(req) if err != nil { return nil, err } diff --git a/vendor/github.com/dnsimple/dnsimple-go/dnsimple/zone_distributions.go b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/zone_distributions.go new file mode 100644 index 00000000..f92d7579 --- /dev/null +++ b/vendor/github.com/dnsimple/dnsimple-go/dnsimple/zone_distributions.go @@ -0,0 +1,46 @@ +package dnsimple + +import "fmt" + +// ZoneDistribution is the result of the zone distribution check. +type ZoneDistribution struct { + Distributed bool `json:"distributed"` +} + +// zoneDistributionResponse represents a response from an API method that returns a ZoneDistribution struct. +type zoneDistributionResponse struct { + Response + Data *ZoneDistribution `json:"data"` +} + +// CheckZoneDistribution checks if a zone is fully distributed across DNSimple nodes. +// +// See https://developer.dnsimple.com/v2/zones/#checkZoneDistribution +func (s *ZonesService) CheckZoneDistribution(accountID string, zoneName string) (*zoneDistributionResponse, error) { + path := versioned(fmt.Sprintf("/%v/zones/%v/distribution", accountID, zoneName)) + zoneDistributionResponse := &zoneDistributionResponse{} + + resp, err := s.client.get(path, zoneDistributionResponse) + if err != nil { + return nil, err + } + + zoneDistributionResponse.HttpResponse = resp + return zoneDistributionResponse, nil +} + +// CheckZoneRecordDistribution checks if a zone is fully distributed across DNSimple nodes. +// +// See https://developer.dnsimple.com/v2/zones/#checkZoneRecordDistribution +func (s *ZonesService) CheckZoneRecordDistribution(accountID string, zoneName string, recordID int64) (*zoneDistributionResponse, error) { + path := versioned(fmt.Sprintf("/%v/zones/%v/records/%v/distribution", accountID, zoneName, recordID)) + zoneDistributionResponse := &zoneDistributionResponse{} + + resp, err := s.client.get(path, zoneDistributionResponse) + if err != nil { + return nil, err + } + + zoneDistributionResponse.HttpResponse = resp + return zoneDistributionResponse, nil +} diff --git a/vendor/github.com/exoscale/egoscale/apis.go b/vendor/github.com/exoscale/egoscale/apis.go index 2f426c0a..a5213c9f 100644 --- a/vendor/github.com/exoscale/egoscale/apis.go +++ b/vendor/github.com/exoscale/egoscale/apis.go @@ -33,7 +33,7 @@ type APIField struct { // ListAPIs represents a query to list the api type ListAPIs struct { Name string `json:"name,omitempty" doc:"API name"` - _ bool `name:"listApis" description:"lists all available apis on the server, provided by the Api Discovery plugin"` + _ bool `name:"listApis" description:"lists all available apis on the server"` } // ListAPIsResponse represents a list of API diff --git a/vendor/github.com/exoscale/egoscale/async_jobs.go b/vendor/github.com/exoscale/egoscale/async_jobs.go index a3e0b188..d6cdf78d 100644 --- a/vendor/github.com/exoscale/egoscale/async_jobs.go +++ b/vendor/github.com/exoscale/egoscale/async_jobs.go @@ -78,10 +78,6 @@ func (a AsyncJobResult) Result(i interface{}) error { return json.Unmarshal(*(a.JobResult), i) } - // more than one keys are list...response - if len(m) > 1 { - return json.Unmarshal(*(a.JobResult), i) - } // otherwise, pick the first key for k := range m { return json.Unmarshal(m[k], i) diff --git a/vendor/github.com/exoscale/egoscale/client.go b/vendor/github.com/exoscale/egoscale/client.go index fdf46cad..eaf102e5 100644 --- a/vendor/github.com/exoscale/egoscale/client.go +++ b/vendor/github.com/exoscale/egoscale/client.go @@ -308,7 +308,7 @@ func (client *Client) APIName(command Command) string { func (client *Client) APIDescription(command Command) string { info, err := info(command) if err != nil { - panic(err) + return "*missing description*" } return info.Description } diff --git a/vendor/github.com/exoscale/egoscale/networks.go b/vendor/github.com/exoscale/egoscale/networks.go index 89c8d0a1..1d52df40 100644 --- a/vendor/github.com/exoscale/egoscale/networks.go +++ b/vendor/github.com/exoscale/egoscale/networks.go @@ -22,6 +22,7 @@ type Network struct { DNS2 net.IP `json:"dns2,omitempty" doc:"the second DNS for the network"` Domain string `json:"domain,omitempty" doc:"the domain name of the network owner"` DomainID *UUID `json:"domainid,omitempty" doc:"the domain id of the network owner"` + EndIP net.IP `json:"endip,omitempty" doc:"the ending IP address in the network IP range. Required for managed networks."` Gateway net.IP `json:"gateway,omitempty" doc:"the network's gateway"` ID *UUID `json:"id,omitempty" doc:"the id of the network"` IP6CIDR *CIDR `json:"ip6cidr,omitempty" doc:"the cidr of IPv6 network"` @@ -44,6 +45,7 @@ type Network struct { RestartRequired bool `json:"restartrequired,omitempty" doc:"true network requires restart"` Service []Service `json:"service,omitempty" doc:"the list of services"` SpecifyIPRanges bool `json:"specifyipranges,omitempty" doc:"true if network supports specifying ip ranges, false otherwise"` + StartIP net.IP `json:"startip,omitempty" doc:"the beginning IP address in the network IP range. Required for managed networks."` State string `json:"state,omitempty" doc:"state of the network"` StrechedL2Subnet bool `json:"strechedl2subnet,omitempty" doc:"true if network can span multiple zones"` SubdomainAccess bool `json:"subdomainaccess,omitempty" doc:"true if users from subdomains can access the domain level network"` @@ -63,6 +65,7 @@ func (network Network) ListRequest() (ListCommand, error) { Account: network.Account, DomainID: network.DomainID, ID: network.ID, + Keyword: network.Name, // this is a hack as listNetworks doesn't support to search by name. PhysicalNetworkID: network.PhysicalNetworkID, TrafficType: network.TrafficType, Type: network.Type, @@ -114,18 +117,18 @@ type CreateNetwork struct { DisplayNetwork *bool `json:"displaynetwork,omitempty" doc:"an optional field, whether to the display the network to the end user or not."` DisplayText string `json:"displaytext,omitempty" doc:"the display text of the network"` // This field is required but might be empty DomainID *UUID `json:"domainid,omitempty" doc:"domain ID of the account owning a network"` - EndIP net.IP `json:"endip,omitempty" doc:"the ending IP address in the network IP range. If not specified, will be defaulted to startIP"` + EndIP net.IP `json:"endip,omitempty" doc:"the ending IP address in the network IP range. Required for managed networks."` EndIpv6 net.IP `json:"endipv6,omitempty" doc:"the ending IPv6 address in the IPv6 network range"` Gateway net.IP `json:"gateway,omitempty" doc:"the gateway of the network. Required for Shared networks and Isolated networks when it belongs to VPC"` IP6CIDR *CIDR `json:"ip6cidr,omitempty" doc:"the CIDR of IPv6 network, must be at least /64"` IP6Gateway net.IP `json:"ip6gateway,omitempty" doc:"the gateway of the IPv6 network. Required for Shared networks and Isolated networks when it belongs to VPC"` IsolatedPVlan string `json:"isolatedpvlan,omitempty" doc:"the isolated private vlan for this network"` Name string `json:"name,omitempty" doc:"the name of the network"` // This field is required but might be empty - Netmask net.IP `json:"netmask,omitempty" doc:"the netmask of the network. Required for Shared networks and Isolated networks when it belongs to VPC"` + Netmask net.IP `json:"netmask,omitempty" doc:"the netmask of the network. Required for managed networks."` NetworkDomain string `json:"networkdomain,omitempty" doc:"network domain"` NetworkOfferingID *UUID `json:"networkofferingid" doc:"the network offering id"` PhysicalNetworkID *UUID `json:"physicalnetworkid,omitempty" doc:"the Physical Network ID the network belongs to"` - StartIP net.IP `json:"startip,omitempty" doc:"the beginning IP address in the network IP range"` + StartIP net.IP `json:"startip,omitempty" doc:"the beginning IP address in the network IP range. Required for managed networks."` StartIpv6 net.IP `json:"startipv6,omitempty" doc:"the beginning IPv6 address in the IPv6 network range"` SubdomainAccess *bool `json:"subdomainaccess,omitempty" doc:"Defines whether to allow subdomains to use networks dedicated to their parent domain(s). Should be used with aclType=Domain, defaulted to allow.subdomain.network.access global config if not specified"` Vlan string `json:"vlan,omitempty" doc:"the ID or VID of the network"` diff --git a/vendor/github.com/exoscale/egoscale/request.go b/vendor/github.com/exoscale/egoscale/request.go index 976f2c82..59e1d302 100644 --- a/vendor/github.com/exoscale/egoscale/request.go +++ b/vendor/github.com/exoscale/egoscale/request.go @@ -37,6 +37,7 @@ var responseKeys = map[string]string{ "addiptonicresponse": "addiptovmnicresponse", "activateip6response": "activateip6nicresponse", "restorevirtualmachineresponse": "restorevmresponse", + "updatevmaffinitygroupresponse": "updatevirtualmachineresponse", } func (client *Client) parseResponse(resp *http.Response, apiName string) (json.RawMessage, error) { @@ -87,7 +88,8 @@ func (client *Client) parseResponse(resp *http.Response, apiName string) (json.R return nil, err } - if len(n) > 1 { + // list response may contain only one key + if len(n) > 1 || strings.HasPrefix(key, "list") { return response, nil } @@ -118,7 +120,7 @@ func (client *Client) asyncRequest(ctx context.Context, asyncCommand AsyncComman err = e return false } - if j.JobStatus == Success { + if j.JobStatus != Pending { if r := j.Result(resp); r != nil { err = r } @@ -268,14 +270,8 @@ func (client *Client) AsyncRequestWithContext(ctx context.Context, asyncCommand } } - if result.JobStatus == Failure { - if !callback(nil, result.Error()) { - return - } - } else { - if !callback(result, nil) { - return - } + if !callback(result, nil) { + return } } } diff --git a/vendor/github.com/exoscale/egoscale/security_groups.go b/vendor/github.com/exoscale/egoscale/security_groups.go index 78b06c43..db0cd5c2 100644 --- a/vendor/github.com/exoscale/egoscale/security_groups.go +++ b/vendor/github.com/exoscale/egoscale/security_groups.go @@ -18,12 +18,6 @@ type SecurityGroup struct { ID *UUID `json:"id,omitempty" doc:"the ID of the security group"` IngressRule []IngressRule `json:"ingressrule,omitempty" doc:"the list of ingress rules associated with the security group"` Name string `json:"name,omitempty" doc:"the name of the security group"` - Tags []ResourceTag `json:"tags,omitempty" doc:"the list of resource tags associated with the rule"` -} - -// ResourceType returns the type of the resource -func (SecurityGroup) ResourceType() string { - return "SecurityGroup" } // UserSecurityGroup converts a SecurityGroup to a UserSecurityGroup @@ -36,7 +30,6 @@ func (sg SecurityGroup) UserSecurityGroup() UserSecurityGroup { // ListRequest builds the ListSecurityGroups request func (sg SecurityGroup) ListRequest() (ListCommand, error) { - //TODO add tags req := &ListSecurityGroups{ Account: sg.Account, DomainID: sg.DomainID, @@ -97,7 +90,6 @@ type IngressRule struct { SecurityGroupID *UUID `json:"securitygroupid,omitempty"` SecurityGroupName string `json:"securitygroupname,omitempty" doc:"security group name"` StartPort uint16 `json:"startport,omitempty" doc:"the starting port of the security group rule"` - Tags []ResourceTag `json:"tags,omitempty" doc:"the list of resource tags associated with the rule"` UserSecurityGroupList []UserSecurityGroup `json:"usersecuritygrouplist,omitempty"` } @@ -223,18 +215,17 @@ func (RevokeSecurityGroupEgress) asyncResponse() interface{} { // ListSecurityGroups represents a search for security groups type ListSecurityGroups struct { - Account string `json:"account,omitempty" doc:"list resources by account. Must be used with the domainId parameter."` - DomainID *UUID `json:"domainid,omitempty" doc:"list only resources belonging to the domain specified"` - ID *UUID `json:"id,omitempty" doc:"list the security group by the id provided"` - IsRecursive *bool `json:"isrecursive,omitempty" doc:"defaults to false, but if true, lists all resources from the parent specified by the domainId till leaves."` - Keyword string `json:"keyword,omitempty" doc:"List by keyword"` - ListAll *bool `json:"listall,omitempty" doc:"If set to false, list only resources belonging to the command's caller; if set to true - list resources that the caller is authorized to see. Default value is false"` - Page int `json:"page,omitempty"` - PageSize int `json:"pagesize,omitempty"` - SecurityGroupName string `json:"securitygroupname,omitempty" doc:"lists security groups by name"` - Tags []ResourceTag `json:"tags,omitempty" doc:"List resources by tags (key/value pairs)"` - VirtualMachineID *UUID `json:"virtualmachineid,omitempty" doc:"lists security groups by virtual machine id"` - _ bool `name:"listSecurityGroups" description:"Lists security groups"` + Account string `json:"account,omitempty" doc:"list resources by account. Must be used with the domainId parameter."` + DomainID *UUID `json:"domainid,omitempty" doc:"list only resources belonging to the domain specified"` + ID *UUID `json:"id,omitempty" doc:"list the security group by the id provided"` + IsRecursive *bool `json:"isrecursive,omitempty" doc:"defaults to false, but if true, lists all resources from the parent specified by the domainId till leaves."` + Keyword string `json:"keyword,omitempty" doc:"List by keyword"` + ListAll *bool `json:"listall,omitempty" doc:"If set to false, list only resources belonging to the command's caller; if set to true - list resources that the caller is authorized to see. Default value is false"` + Page int `json:"page,omitempty"` + PageSize int `json:"pagesize,omitempty"` + SecurityGroupName string `json:"securitygroupname,omitempty" doc:"lists security groups by name"` + VirtualMachineID *UUID `json:"virtualmachineid,omitempty" doc:"lists security groups by virtual machine id"` + _ bool `name:"listSecurityGroups" description:"Lists security groups"` } // ListSecurityGroupsResponse represents a list of security groups diff --git a/vendor/github.com/exoscale/egoscale/version.go b/vendor/github.com/exoscale/egoscale/version.go index c42b1d30..c012ffe3 100644 --- a/vendor/github.com/exoscale/egoscale/version.go +++ b/vendor/github.com/exoscale/egoscale/version.go @@ -1,4 +1,4 @@ package egoscale // Version of the library -const Version = "0.11.1" +const Version = "0.11.6" diff --git a/vendor/github.com/exoscale/egoscale/virtual_machines.go b/vendor/github.com/exoscale/egoscale/virtual_machines.go index 418b12ad..5408831b 100644 --- a/vendor/github.com/exoscale/egoscale/virtual_machines.go +++ b/vendor/github.com/exoscale/egoscale/virtual_machines.go @@ -626,3 +626,14 @@ func (MigrateVirtualMachine) response() interface{} { func (MigrateVirtualMachine) asyncResponse() interface{} { return new(VirtualMachine) } + +// UpdateVMNicIP updates the default IP address of a VM Nic +type UpdateVMNicIP struct { + _ bool `name:"updateVmNicIp" description:"Update the default Ip of a VM Nic"` + IPAddress net.IP `json:"ipaddress" doc:"Static IP address lease for the corresponding NIC and network which should be in the range defined in the network. Also, the last IP of the network is reserved by the DHCP server."` + NicID *UUID `json:"nicid" doc:"the ID of the nic."` +} + +func (UpdateVMNicIP) response() interface{} { + return new(VirtualMachine) +} diff --git a/vendor/github.com/miekg/dns/acceptfunc.go b/vendor/github.com/miekg/dns/acceptfunc.go new file mode 100644 index 00000000..fcc6104f --- /dev/null +++ b/vendor/github.com/miekg/dns/acceptfunc.go @@ -0,0 +1,54 @@ +package dns + +// MsgAcceptFunc is used early in the server code to accept or reject a message with RcodeFormatError. +// It returns a MsgAcceptAction to indicate what should happen with the message. +type MsgAcceptFunc func(dh Header) MsgAcceptAction + +// DefaultMsgAcceptFunc checks the request and will reject if: +// +// * isn't a request (don't respond in that case). +// * opcode isn't OpcodeQuery or OpcodeNotify +// * Zero bit isn't zero +// * has more than 1 question in the question section +// * has more than 0 RRs in the Answer section +// * has more than 0 RRs in the Authority section +// * has more than 2 RRs in the Additional section +var DefaultMsgAcceptFunc MsgAcceptFunc = defaultMsgAcceptFunc + +// MsgAcceptAction represents the action to be taken. +type MsgAcceptAction int + +const ( + MsgAccept MsgAcceptAction = iota // Accept the message + MsgReject // Reject the message with a RcodeFormatError + MsgIgnore // Ignore the error and send nothing back. +) + +var defaultMsgAcceptFunc = func(dh Header) MsgAcceptAction { + if isResponse := dh.Bits&_QR != 0; isResponse { + return MsgIgnore + } + + // Don't allow dynamic updates, because then the sections can contain a whole bunch of RRs. + opcode := int(dh.Bits>>11) & 0xF + if opcode != OpcodeQuery && opcode != OpcodeNotify { + return MsgReject + } + + if isZero := dh.Bits&_Z != 0; isZero { + return MsgReject + } + if dh.Qdcount != 1 { + return MsgReject + } + if dh.Ancount != 0 { + return MsgReject + } + if dh.Nscount != 0 { + return MsgReject + } + if dh.Arcount > 2 { + return MsgReject + } + return MsgAccept +} diff --git a/vendor/github.com/miekg/dns/client.go b/vendor/github.com/miekg/dns/client.go index dd6b512a..770a946c 100644 --- a/vendor/github.com/miekg/dns/client.go +++ b/vendor/github.com/miekg/dns/client.go @@ -7,11 +7,8 @@ import ( "context" "crypto/tls" "encoding/binary" - "fmt" "io" - "io/ioutil" "net" - "net/http" "strings" "time" ) @@ -19,8 +16,6 @@ import ( const ( dnsTimeout time.Duration = 2 * time.Second tcpIdleTimeout time.Duration = 8 * time.Second - - dohMimeType = "application/dns-message" ) // A Conn represents a connection to a DNS server. @@ -44,7 +39,6 @@ type Client struct { DialTimeout time.Duration // net.DialTimeout, defaults to 2 seconds, or net.Dialer.Timeout if expiring earlier - overridden by Timeout when that value is non-zero ReadTimeout time.Duration // net.Conn.SetReadTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero WriteTimeout time.Duration // net.Conn.SetWriteTimeout value for connections, defaults to 2 seconds - overridden by Timeout when that value is non-zero - HTTPClient *http.Client // The http.Client to use for DNS-over-HTTPS TsigSecret map[string]string // secret(s) for Tsig map[], zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2) SingleInflight bool // if true suppress multiple outstanding queries for the same Qname, Qtype and Qclass group singleflight @@ -89,32 +83,22 @@ func (c *Client) Dial(address string) (conn *Conn, err error) { // create a new dialer with the appropriate timeout var d net.Dialer if c.Dialer == nil { - d = net.Dialer{Timeout:c.getTimeoutForRequest(c.dialTimeout())} + d = net.Dialer{Timeout: c.getTimeoutForRequest(c.dialTimeout())} } else { - d = net.Dialer(*c.Dialer) + d = *c.Dialer } - network := "udp" - useTLS := false - - switch c.Net { - case "tcp-tls": - network = "tcp" - useTLS = true - case "tcp4-tls": - network = "tcp4" - useTLS = true - case "tcp6-tls": - network = "tcp6" - useTLS = true - default: - if c.Net != "" { - network = c.Net - } + network := c.Net + if network == "" { + network = "udp" } + useTLS := strings.HasPrefix(network, "tcp") && strings.HasSuffix(network, "-tls") + conn = new(Conn) if useTLS { + network = strings.TrimSuffix(network, "-tls") + conn.Conn, err = tls.DialWithDialer(&d, network, address, c.TLSConfig) } else { conn.Conn, err = d.Dial(network, address) @@ -122,6 +106,7 @@ func (c *Client) Dial(address string) (conn *Conn, err error) { if err != nil { return nil, err } + return conn, nil } @@ -141,11 +126,6 @@ func (c *Client) Dial(address string) (conn *Conn, err error) { // attribute appropriately func (c *Client) Exchange(m *Msg, address string) (r *Msg, rtt time.Duration, err error) { if !c.SingleInflight { - if c.Net == "https" { - // TODO(tmthrgd): pipe timeouts into exchangeDOH - return c.exchangeDOH(context.TODO(), m, address) - } - return c.exchange(m, address) } @@ -158,11 +138,6 @@ func (c *Client) Exchange(m *Msg, address string) (r *Msg, rtt time.Duration, er cl = cl1 } r, rtt, err, shared := c.group.Do(m.Question[0].Name+t+cl, func() (*Msg, time.Duration, error) { - if c.Net == "https" { - // TODO(tmthrgd): pipe timeouts into exchangeDOH - return c.exchangeDOH(context.TODO(), m, address) - } - return c.exchange(m, address) }) if r != nil && shared { @@ -208,67 +183,6 @@ func (c *Client) exchange(m *Msg, a string) (r *Msg, rtt time.Duration, err erro return r, rtt, err } -func (c *Client) exchangeDOH(ctx context.Context, m *Msg, a string) (r *Msg, rtt time.Duration, err error) { - p, err := m.Pack() - if err != nil { - return nil, 0, err - } - - req, err := http.NewRequest(http.MethodPost, a, bytes.NewReader(p)) - if err != nil { - return nil, 0, err - } - - req.Header.Set("Content-Type", dohMimeType) - req.Header.Set("Accept", dohMimeType) - - hc := http.DefaultClient - if c.HTTPClient != nil { - hc = c.HTTPClient - } - - if ctx != context.Background() && ctx != context.TODO() { - req = req.WithContext(ctx) - } - - t := time.Now() - - resp, err := hc.Do(req) - if err != nil { - return nil, 0, err - } - defer closeHTTPBody(resp.Body) - - if resp.StatusCode != http.StatusOK { - return nil, 0, fmt.Errorf("dns: server returned HTTP %d error: %q", resp.StatusCode, resp.Status) - } - - if ct := resp.Header.Get("Content-Type"); ct != dohMimeType { - return nil, 0, fmt.Errorf("dns: unexpected Content-Type %q; expected %q", ct, dohMimeType) - } - - p, err = ioutil.ReadAll(resp.Body) - if err != nil { - return nil, 0, err - } - - rtt = time.Since(t) - - r = new(Msg) - if err := r.Unpack(p); err != nil { - return r, 0, err - } - - // TODO: TSIG? Is it even supported over DoH? - - return r, rtt, nil -} - -func closeHTTPBody(r io.ReadCloser) error { - io.Copy(ioutil.Discard, io.LimitReader(r, 8<<20)) - return r.Close() -} - // ReadMsg reads a message from the connection co. // If the received message contains a TSIG record the transaction signature // is verified. This method always tries to return the message, however if an @@ -568,19 +482,15 @@ func DialTimeoutWithTLS(network, address string, tlsConfig *tls.Config, timeout // context, if present. If there is both a context deadline and a configured // timeout on the client, the earliest of the two takes effect. func (c *Client) ExchangeContext(ctx context.Context, m *Msg, a string) (r *Msg, rtt time.Duration, err error) { - if !c.SingleInflight && c.Net == "https" { - return c.exchangeDOH(ctx, m, a) - } - var timeout time.Duration if deadline, ok := ctx.Deadline(); !ok { timeout = 0 } else { - timeout = deadline.Sub(time.Now()) + timeout = time.Until(deadline) } // not passing the context to the underlying calls, as the API does not support // context. For timeouts you should set up Client.Dialer and call Client.Exchange. - // TODO(tmthrgd): this is a race condition + // TODO(tmthrgd,miekg): this is a race condition. c.Dialer = &net.Dialer{Timeout: timeout} return c.Exchange(m, a) } diff --git a/vendor/github.com/miekg/dns/compress_generate.go b/vendor/github.com/miekg/dns/compress_generate.go index 9a136c41..4bcefcb8 100644 --- a/vendor/github.com/miekg/dns/compress_generate.go +++ b/vendor/github.com/miekg/dns/compress_generate.go @@ -101,7 +101,7 @@ Names: // compressionLenHelperType - all types that have domain-name/cdomain-name can be used for compressing names - fmt.Fprint(b, "func compressionLenHelperType(c map[string]int, r RR, initLen int) int {\n") + fmt.Fprint(b, "func compressionLenHelperType(c map[string]struct{}, r RR, initLen int) int {\n") fmt.Fprint(b, "currentLen := initLen\n") fmt.Fprint(b, "switch x := r.(type) {\n") for _, name := range domainTypes { @@ -145,7 +145,7 @@ Names: // compressionLenSearchType - search cdomain-tags types for compressible names. - fmt.Fprint(b, "func compressionLenSearchType(c map[string]int, r RR) (int, bool, int) {\n") + fmt.Fprint(b, "func compressionLenSearchType(c map[string]struct{}, r RR) (int, bool, int) {\n") fmt.Fprint(b, "switch x := r.(type) {\n") for _, name := range cdomainTypes { o := scope.Lookup(name) diff --git a/vendor/github.com/miekg/dns/dnssec.go b/vendor/github.com/miekg/dns/dnssec.go index 7e6bac42..c3491a18 100644 --- a/vendor/github.com/miekg/dns/dnssec.go +++ b/vendor/github.com/miekg/dns/dnssec.go @@ -173,7 +173,7 @@ func (k *DNSKEY) KeyTag() uint16 { keytag += int(v) << 8 } } - keytag += (keytag >> 16) & 0xFFFF + keytag += keytag >> 16 & 0xFFFF keytag &= 0xFFFF } return uint16(keytag) @@ -401,7 +401,7 @@ func (rr *RRSIG) Verify(k *DNSKEY, rrset []RR) error { if rr.Algorithm != k.Algorithm { return ErrKey } - if strings.ToLower(rr.SignerName) != strings.ToLower(k.Hdr.Name) { + if !strings.EqualFold(rr.SignerName, k.Hdr.Name) { return ErrKey } if k.Protocol != 3 { @@ -512,8 +512,8 @@ func (rr *RRSIG) ValidityPeriod(t time.Time) bool { } modi := (int64(rr.Inception) - utc) / year68 mode := (int64(rr.Expiration) - utc) / year68 - ti := int64(rr.Inception) + (modi * year68) - te := int64(rr.Expiration) + (mode * year68) + ti := int64(rr.Inception) + modi*year68 + te := int64(rr.Expiration) + mode*year68 return ti <= utc && utc <= te } @@ -533,6 +533,11 @@ func (k *DNSKEY) publicKeyRSA() *rsa.PublicKey { return nil } + if len(keybuf) < 1+1+64 { + // Exponent must be at least 1 byte and modulus at least 64 + return nil + } + // RFC 2537/3110, section 2. RSA Public KEY Resource Records // Length is in the 0th byte, unless its zero, then it // it in bytes 1 and 2 and its a 16 bit number @@ -542,13 +547,22 @@ func (k *DNSKEY) publicKeyRSA() *rsa.PublicKey { explen = uint16(keybuf[1])<<8 | uint16(keybuf[2]) keyoff = 3 } - if explen > 4 { - // Larger exponent than supported by the crypto package. + + if explen > 4 || explen == 0 || keybuf[keyoff] == 0 { + // Exponent larger than supported by the crypto package, + // empty, or contains prohibited leading zero. return nil } + + modoff := keyoff + int(explen) + modlen := len(keybuf) - modoff + if modlen < 64 || modlen > 512 || keybuf[modoff] == 0 { + // Modulus is too small, large, or contains prohibited leading zero. + return nil + } + pubkey := new(rsa.PublicKey) - pubkey.N = big.NewInt(0) expo := uint64(0) for i := 0; i < int(explen); i++ { expo <<= 8 @@ -560,7 +574,9 @@ func (k *DNSKEY) publicKeyRSA() *rsa.PublicKey { } pubkey.E = int(expo) - pubkey.N.SetBytes(keybuf[keyoff+int(explen):]) + pubkey.N = big.NewInt(0) + pubkey.N.SetBytes(keybuf[modoff:]) + return pubkey } diff --git a/vendor/github.com/miekg/dns/dnssec_keyscan.go b/vendor/github.com/miekg/dns/dnssec_keyscan.go index e2d9d8f9..5e654223 100644 --- a/vendor/github.com/miekg/dns/dnssec_keyscan.go +++ b/vendor/github.com/miekg/dns/dnssec_keyscan.go @@ -1,7 +1,7 @@ package dns import ( - "bytes" + "bufio" "crypto" "crypto/dsa" "crypto/ecdsa" @@ -181,22 +181,10 @@ func readPrivateKeyED25519(m map[string]string) (ed25519.PrivateKey, error) { if err != nil { return nil, err } - if len(p1) != 32 { + if len(p1) != ed25519.SeedSize { return nil, ErrPrivKey } - // RFC 8080 and Golang's x/crypto/ed25519 differ as to how the - // private keys are represented. RFC 8080 specifies that private - // keys be stored solely as the seed value (p1 above) while the - // ed25519 package represents them as the seed value concatenated - // to the public key, which is derived from the seed value. - // - // ed25519.GenerateKey reads exactly 32 bytes from the passed in - // io.Reader and uses them as the seed. It also derives the - // public key and produces a compatible private key. - _, p, err = ed25519.GenerateKey(bytes.NewReader(p1)) - if err != nil { - return nil, err - } + p = ed25519.NewKeyFromSeed(p1) case "created", "publish", "activate": /* not used in Go (yet) */ } @@ -207,23 +195,12 @@ func readPrivateKeyED25519(m map[string]string) (ed25519.PrivateKey, error) { // parseKey reads a private key from r. It returns a map[string]string, // with the key-value pairs, or an error when the file is not correct. func parseKey(r io.Reader, file string) (map[string]string, error) { - s, cancel := scanInit(r) m := make(map[string]string) - c := make(chan lex) - k := "" - defer func() { - cancel() - // zlexer can send up to two tokens, the next one and possibly 1 remainders. - // Do a non-blocking read. - _, ok := <-c - _, ok = <-c - if !ok { - // too bad - } - }() - // Start the lexer - go klexer(s, c) - for l := range c { + var k string + + c := newKLexer(r) + + for l, ok := c.Next(); ok; l, ok = c.Next() { // It should alternate switch l.value { case zKey: @@ -232,41 +209,111 @@ func parseKey(r io.Reader, file string) (map[string]string, error) { if k == "" { return nil, &ParseError{file, "no private key seen", l} } - //println("Setting", strings.ToLower(k), "to", l.token, "b") + m[strings.ToLower(k)] = l.token k = "" } } + + // Surface any read errors from r. + if err := c.Err(); err != nil { + return nil, &ParseError{file: file, err: err.Error()} + } + return m, nil } -// klexer scans the sourcefile and returns tokens on the channel c. -func klexer(s *scan, c chan lex) { - var l lex - str := "" // Hold the current read text - commt := false - key := true - x, err := s.tokenText() - defer close(c) - for err == nil { - l.column = s.position.Column - l.line = s.position.Line +type klexer struct { + br io.ByteReader + + readErr error + + line int + column int + + key bool + + eol bool // end-of-line +} + +func newKLexer(r io.Reader) *klexer { + br, ok := r.(io.ByteReader) + if !ok { + br = bufio.NewReaderSize(r, 1024) + } + + return &klexer{ + br: br, + + line: 1, + + key: true, + } +} + +func (kl *klexer) Err() error { + if kl.readErr == io.EOF { + return nil + } + + return kl.readErr +} + +// readByte returns the next byte from the input +func (kl *klexer) readByte() (byte, bool) { + if kl.readErr != nil { + return 0, false + } + + c, err := kl.br.ReadByte() + if err != nil { + kl.readErr = err + return 0, false + } + + // delay the newline handling until the next token is delivered, + // fixes off-by-one errors when reporting a parse error. + if kl.eol { + kl.line++ + kl.column = 0 + kl.eol = false + } + + if c == '\n' { + kl.eol = true + } else { + kl.column++ + } + + return c, true +} + +func (kl *klexer) Next() (lex, bool) { + var ( + l lex + + str strings.Builder + + commt bool + ) + + for x, ok := kl.readByte(); ok; x, ok = kl.readByte() { + l.line, l.column = kl.line, kl.column + switch x { case ':': - if commt { + if commt || !kl.key { break } - l.token = str - if key { - l.value = zKey - c <- l - // Next token is a space, eat it - s.tokenText() - key = false - str = "" - } else { - l.value = zValue - } + + kl.key = false + + // Next token is a space, eat it + kl.readByte() + + l.value = zKey + l.token = str.String() + return l, true case ';': commt = true case '\n': @@ -274,24 +321,32 @@ func klexer(s *scan, c chan lex) { // Reset a comment commt = false } + + kl.key = true + l.value = zValue - l.token = str - c <- l - str = "" - commt = false - key = true + l.token = str.String() + return l, true default: if commt { break } - str += string(x) + + str.WriteByte(x) } - x, err = s.tokenText() } - if len(str) > 0 { + + if kl.readErr != nil && kl.readErr != io.EOF { + // Don't return any tokens after a read error occurs. + return lex{value: zEOF}, false + } + + if str.Len() > 0 { // Send remainder - l.token = str l.value = zValue - c <- l + l.token = str.String() + return l, true } + + return lex{value: zEOF}, false } diff --git a/vendor/github.com/miekg/dns/dnssec_privkey.go b/vendor/github.com/miekg/dns/dnssec_privkey.go index 46f3215c..0c65be17 100644 --- a/vendor/github.com/miekg/dns/dnssec_privkey.go +++ b/vendor/github.com/miekg/dns/dnssec_privkey.go @@ -82,7 +82,7 @@ func (r *DNSKEY) PrivateKeyString(p crypto.PrivateKey) string { "Public_value(y): " + pub + "\n" case ed25519.PrivateKey: - private := toBase64(p[:32]) + private := toBase64(p.Seed()) return format + "Algorithm: " + algorithm + "\n" + "PrivateKey: " + private + "\n" diff --git a/vendor/github.com/miekg/dns/doc.go b/vendor/github.com/miekg/dns/doc.go index 0389d724..d3d7cec9 100644 --- a/vendor/github.com/miekg/dns/doc.go +++ b/vendor/github.com/miekg/dns/doc.go @@ -1,20 +1,20 @@ /* Package dns implements a full featured interface to the Domain Name System. -Server- and client-side programming is supported. -The package allows complete control over what is sent out to the DNS. The package -API follows the less-is-more principle, by presenting a small, clean interface. +Both server- and client-side programming is supported. The package allows +complete control over what is sent out to the DNS. The API follows the +less-is-more principle, by presenting a small, clean interface. -The package dns supports (asynchronous) querying/replying, incoming/outgoing zone transfers, +It supports (asynchronous) querying/replying, incoming/outgoing zone transfers, TSIG, EDNS0, dynamic updates, notifies and DNSSEC validation/signing. -Note that domain names MUST be fully qualified, before sending them, unqualified + +Note that domain names MUST be fully qualified before sending them, unqualified names in a message will result in a packing failure. -Resource records are native types. They are not stored in wire format. -Basic usage pattern for creating a new resource record: +Resource records are native types. They are not stored in wire format. Basic +usage pattern for creating a new resource record: r := new(dns.MX) - r.Hdr = dns.RR_Header{Name: "miek.nl.", Rrtype: dns.TypeMX, - Class: dns.ClassINET, Ttl: 3600} + r.Hdr = dns.RR_Header{Name: "miek.nl.", Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 3600} r.Preference = 10 r.Mx = "mx.miek.nl." @@ -30,8 +30,8 @@ Or even: mx, err := dns.NewRR("$ORIGIN nl.\nmiek 1H IN MX 10 mx.miek") -In the DNS messages are exchanged, these messages contain resource -records (sets). Use pattern for creating a message: +In the DNS messages are exchanged, these messages contain resource records +(sets). Use pattern for creating a message: m := new(dns.Msg) m.SetQuestion("miek.nl.", dns.TypeMX) @@ -40,8 +40,8 @@ Or when not certain if the domain name is fully qualified: m.SetQuestion(dns.Fqdn("miek.nl"), dns.TypeMX) -The message m is now a message with the question section set to ask -the MX records for the miek.nl. zone. +The message m is now a message with the question section set to ask the MX +records for the miek.nl. zone. The following is slightly more verbose, but more flexible: @@ -51,9 +51,8 @@ The following is slightly more verbose, but more flexible: m1.Question = make([]dns.Question, 1) m1.Question[0] = dns.Question{"miek.nl.", dns.TypeMX, dns.ClassINET} -After creating a message it can be sent. -Basic use pattern for synchronous querying the DNS at a -server configured on 127.0.0.1 and port 53: +After creating a message it can be sent. Basic use pattern for synchronous +querying the DNS at a server configured on 127.0.0.1 and port 53: c := new(dns.Client) in, rtt, err := c.Exchange(m1, "127.0.0.1:53") @@ -99,25 +98,24 @@ the Answer section: Domain Name and TXT Character String Representations -Both domain names and TXT character strings are converted to presentation -form both when unpacked and when converted to strings. +Both domain names and TXT character strings are converted to presentation form +both when unpacked and when converted to strings. For TXT character strings, tabs, carriage returns and line feeds will be -converted to \t, \r and \n respectively. Back slashes and quotations marks -will be escaped. Bytes below 32 and above 127 will be converted to \DDD -form. +converted to \t, \r and \n respectively. Back slashes and quotations marks will +be escaped. Bytes below 32 and above 127 will be converted to \DDD form. -For domain names, in addition to the above rules brackets, periods, -spaces, semicolons and the at symbol are escaped. +For domain names, in addition to the above rules brackets, periods, spaces, +semicolons and the at symbol are escaped. DNSSEC -DNSSEC (DNS Security Extension) adds a layer of security to the DNS. It -uses public key cryptography to sign resource records. The -public keys are stored in DNSKEY records and the signatures in RRSIG records. +DNSSEC (DNS Security Extension) adds a layer of security to the DNS. It uses +public key cryptography to sign resource records. The public keys are stored in +DNSKEY records and the signatures in RRSIG records. -Requesting DNSSEC information for a zone is done by adding the DO (DNSSEC OK) bit -to a request. +Requesting DNSSEC information for a zone is done by adding the DO (DNSSEC OK) +bit to a request. m := new(dns.Msg) m.SetEdns0(4096, true) @@ -126,9 +124,9 @@ Signature generation, signature verification and key generation are all supporte DYNAMIC UPDATES -Dynamic updates reuses the DNS message format, but renames three of -the sections. Question is Zone, Answer is Prerequisite, Authority is -Update, only the Additional is not renamed. See RFC 2136 for the gory details. +Dynamic updates reuses the DNS message format, but renames three of the +sections. Question is Zone, Answer is Prerequisite, Authority is Update, only +the Additional is not renamed. See RFC 2136 for the gory details. You can set a rather complex set of rules for the existence of absence of certain resource records or names in a zone to specify if resource records @@ -145,10 +143,9 @@ DNS function shows which functions exist to specify the prerequisites. NONE rrset empty RRset does not exist dns.RRsetNotUsed zone rrset rr RRset exists (value dep) dns.Used -The prerequisite section can also be left empty. -If you have decided on the prerequisites you can tell what RRs should -be added or deleted. The next table shows the options you have and -what functions to call. +The prerequisite section can also be left empty. If you have decided on the +prerequisites you can tell what RRs should be added or deleted. The next table +shows the options you have and what functions to call. 3.4.2.6 - Table Of Metavalues Used In Update Section @@ -181,10 +178,10 @@ changes to the RRset after calling SetTsig() the signature will be incorrect. ... // When sending the TSIG RR is calculated and filled in before sending -When requesting an zone transfer (almost all TSIG usage is when requesting zone transfers), with -TSIG, this is the basic use pattern. In this example we request an AXFR for -miek.nl. with TSIG key named "axfr." and secret "so6ZGir4GPAqINNh9U5c3A==" -and using the server 176.58.119.54: +When requesting an zone transfer (almost all TSIG usage is when requesting zone +transfers), with TSIG, this is the basic use pattern. In this example we +request an AXFR for miek.nl. with TSIG key named "axfr." and secret +"so6ZGir4GPAqINNh9U5c3A==" and using the server 176.58.119.54: t := new(dns.Transfer) m := new(dns.Msg) @@ -194,8 +191,8 @@ and using the server 176.58.119.54: c, err := t.In(m, "176.58.119.54:53") for r := range c { ... } -You can now read the records from the transfer as they come in. Each envelope is checked with TSIG. -If something is not correct an error is returned. +You can now read the records from the transfer as they come in. Each envelope +is checked with TSIG. If something is not correct an error is returned. Basic use pattern validating and replying to a message that has TSIG set. @@ -220,29 +217,30 @@ Basic use pattern validating and replying to a message that has TSIG set. PRIVATE RRS -RFC 6895 sets aside a range of type codes for private use. This range -is 65,280 - 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these +RFC 6895 sets aside a range of type codes for private use. This range is 65,280 +- 65,534 (0xFF00 - 0xFFFE). When experimenting with new Resource Records these can be used, before requesting an official type code from IANA. -see http://miek.nl/2014/September/21/idn-and-private-rr-in-go-dns/ for more +See https://miek.nl/2014/September/21/idn-and-private-rr-in-go-dns/ for more information. EDNS0 -EDNS0 is an extension mechanism for the DNS defined in RFC 2671 and updated -by RFC 6891. It defines an new RR type, the OPT RR, which is then completely +EDNS0 is an extension mechanism for the DNS defined in RFC 2671 and updated by +RFC 6891. It defines an new RR type, the OPT RR, which is then completely abused. + Basic use pattern for creating an (empty) OPT RR: o := new(dns.OPT) o.Hdr.Name = "." // MUST be the root zone, per definition. o.Hdr.Rrtype = dns.TypeOPT -The rdata of an OPT RR consists out of a slice of EDNS0 (RFC 6891) -interfaces. Currently only a few have been standardized: EDNS0_NSID -(RFC 5001) and EDNS0_SUBNET (draft-vandergaast-edns-client-subnet-02). Note -that these options may be combined in an OPT RR. -Basic use pattern for a server to check if (and which) options are set: +The rdata of an OPT RR consists out of a slice of EDNS0 (RFC 6891) interfaces. +Currently only a few have been standardized: EDNS0_NSID (RFC 5001) and +EDNS0_SUBNET (draft-vandergaast-edns-client-subnet-02). Note that these options +may be combined in an OPT RR. Basic use pattern for a server to check if (and +which) options are set: // o is a dns.OPT for _, s := range o.Option { @@ -262,10 +260,9 @@ From RFC 2931: ... protection for glue records, DNS requests, protection for message headers on requests and responses, and protection of the overall integrity of a response. -It works like TSIG, except that SIG(0) uses public key cryptography, instead of the shared -secret approach in TSIG. -Supported algorithms: DSA, ECDSAP256SHA256, ECDSAP384SHA384, RSASHA1, RSASHA256 and -RSASHA512. +It works like TSIG, except that SIG(0) uses public key cryptography, instead of +the shared secret approach in TSIG. Supported algorithms: DSA, ECDSAP256SHA256, +ECDSAP384SHA384, RSASHA1, RSASHA256 and RSASHA512. Signing subsequent messages in multi-message sessions is not implemented. */ diff --git a/vendor/github.com/miekg/dns/duplicate.go b/vendor/github.com/miekg/dns/duplicate.go new file mode 100644 index 00000000..6372e8a1 --- /dev/null +++ b/vendor/github.com/miekg/dns/duplicate.go @@ -0,0 +1,25 @@ +package dns + +//go:generate go run duplicate_generate.go + +// IsDuplicate checks of r1 and r2 are duplicates of each other, excluding the TTL. +// So this means the header data is equal *and* the RDATA is the same. Return true +// is so, otherwise false. +// It's is a protocol violation to have identical RRs in a message. +func IsDuplicate(r1, r2 RR) bool { + if r1.Header().Class != r2.Header().Class { + return false + } + if r1.Header().Rrtype != r2.Header().Rrtype { + return false + } + if !isDulicateName(r1.Header().Name, r2.Header().Name) { + return false + } + // ignore TTL + + return isDuplicateRdata(r1, r2) +} + +// isDulicateName checks if the domain names s1 and s2 are equal. +func isDulicateName(s1, s2 string) bool { return equal(s1, s2) } diff --git a/vendor/github.com/miekg/dns/duplicate_generate.go b/vendor/github.com/miekg/dns/duplicate_generate.go new file mode 100644 index 00000000..83ac1cf7 --- /dev/null +++ b/vendor/github.com/miekg/dns/duplicate_generate.go @@ -0,0 +1,158 @@ +//+build ignore + +// types_generate.go is meant to run with go generate. It will use +// go/{importer,types} to track down all the RR struct types. Then for each type +// it will generate conversion tables (TypeToRR and TypeToString) and banal +// methods (len, Header, copy) based on the struct tags. The generated source is +// written to ztypes.go, and is meant to be checked into git. +package main + +import ( + "bytes" + "fmt" + "go/format" + "go/importer" + "go/types" + "log" + "os" +) + +var packageHdr = ` +// Code generated by "go run duplicate_generate.go"; DO NOT EDIT. + +package dns + +` + +func getTypeStruct(t types.Type, scope *types.Scope) (*types.Struct, bool) { + st, ok := t.Underlying().(*types.Struct) + if !ok { + return nil, false + } + if st.Field(0).Type() == scope.Lookup("RR_Header").Type() { + return st, false + } + if st.Field(0).Anonymous() { + st, _ := getTypeStruct(st.Field(0).Type(), scope) + return st, true + } + return nil, false +} + +func main() { + // Import and type-check the package + pkg, err := importer.Default().Import("github.com/miekg/dns") + fatalIfErr(err) + scope := pkg.Scope() + + // Collect actual types (*X) + var namedTypes []string + for _, name := range scope.Names() { + o := scope.Lookup(name) + if o == nil || !o.Exported() { + continue + } + + if st, _ := getTypeStruct(o.Type(), scope); st == nil { + continue + } + + if name == "PrivateRR" || name == "RFC3597" { + continue + } + if name == "OPT" || name == "ANY" || name == "IXFR" || name == "AXFR" { + continue + } + + namedTypes = append(namedTypes, o.Name()) + } + + b := &bytes.Buffer{} + b.WriteString(packageHdr) + + // Generate the giant switch that calls the correct function for each type. + fmt.Fprint(b, "// isDuplicateRdata calls the rdata specific functions\n") + fmt.Fprint(b, "func isDuplicateRdata(r1, r2 RR) bool {\n") + fmt.Fprint(b, "switch r1.Header().Rrtype {\n") + + for _, name := range namedTypes { + + o := scope.Lookup(name) + _, isEmbedded := getTypeStruct(o.Type(), scope) + if isEmbedded { + continue + } + fmt.Fprintf(b, "case Type%s:\nreturn isDuplicate%s(r1.(*%s), r2.(*%s))\n", name, name, name, name) + } + fmt.Fprintf(b, "}\nreturn false\n}\n") + + // Generate the duplicate check for each type. + fmt.Fprint(b, "// isDuplicate() functions\n\n") + for _, name := range namedTypes { + + o := scope.Lookup(name) + st, isEmbedded := getTypeStruct(o.Type(), scope) + if isEmbedded { + continue + } + fmt.Fprintf(b, "func isDuplicate%s(r1, r2 *%s) bool {\n", name, name) + for i := 1; i < st.NumFields(); i++ { + field := st.Field(i).Name() + o2 := func(s string) { fmt.Fprintf(b, s+"\n", field, field) } + o3 := func(s string) { fmt.Fprintf(b, s+"\n", field, field, field) } + + // For some reason, a and aaaa don't pop up as *types.Slice here (mostly like because the are + // *indirectly* defined as a slice in the net package). + if _, ok := st.Field(i).Type().(*types.Slice); ok || st.Tag(i) == `dns:"a"` || st.Tag(i) == `dns:"aaaa"` { + o2("if len(r1.%s) != len(r2.%s) {\nreturn false\n}") + + if st.Tag(i) == `dns:"cdomain-name"` || st.Tag(i) == `dns:"domain-name"` { + o3(`for i := 0; i < len(r1.%s); i++ { + if !isDulicateName(r1.%s[i], r2.%s[i]) { + return false + } + }`) + + continue + } + + o3(`for i := 0; i < len(r1.%s); i++ { + if r1.%s[i] != r2.%s[i] { + return false + } + }`) + + continue + } + + switch st.Tag(i) { + case `dns:"-"`: + // ignored + case `dns:"cdomain-name"`, `dns:"domain-name"`: + o2("if !isDulicateName(r1.%s, r2.%s) {\nreturn false\n}") + default: + o2("if r1.%s != r2.%s {\nreturn false\n}") + } + } + fmt.Fprintf(b, "return true\n}\n\n") + } + + // gofmt + res, err := format.Source(b.Bytes()) + if err != nil { + b.WriteTo(os.Stderr) + log.Fatal(err) + } + + // write result + f, err := os.Create("zduplicate.go") + fatalIfErr(err) + defer f.Close() + f.Write(res) +} + +func fatalIfErr(err error) { + if err != nil { + log.Fatal(err) + } +} diff --git a/vendor/github.com/miekg/dns/edns.go b/vendor/github.com/miekg/dns/edns.go index 55059eb1..32047151 100644 --- a/vendor/github.com/miekg/dns/edns.go +++ b/vendor/github.com/miekg/dns/edns.go @@ -92,25 +92,24 @@ func (rr *OPT) len() int { // Version returns the EDNS version used. Only zero is defined. func (rr *OPT) Version() uint8 { - return uint8((rr.Hdr.Ttl & 0x00FF0000) >> 16) + return uint8(rr.Hdr.Ttl & 0x00FF0000 >> 16) } // SetVersion sets the version of EDNS. This is usually zero. func (rr *OPT) SetVersion(v uint8) { - rr.Hdr.Ttl = rr.Hdr.Ttl&0xFF00FFFF | (uint32(v) << 16) + rr.Hdr.Ttl = rr.Hdr.Ttl&0xFF00FFFF | uint32(v)<<16 } // ExtendedRcode returns the EDNS extended RCODE field (the upper 8 bits of the TTL). func (rr *OPT) ExtendedRcode() int { - return int((rr.Hdr.Ttl&0xFF000000)>>24) + 15 + return int(rr.Hdr.Ttl&0xFF000000>>24) << 4 } // SetExtendedRcode sets the EDNS extended RCODE field. -func (rr *OPT) SetExtendedRcode(v uint8) { - if v < RcodeBadVers { // Smaller than 16.. Use the 4 bits you have! - return - } - rr.Hdr.Ttl = rr.Hdr.Ttl&0x00FFFFFF | (uint32(v-15) << 24) +// +// If the RCODE is not an extended RCODE, will reset the extended RCODE field to 0. +func (rr *OPT) SetExtendedRcode(v uint16) { + rr.Hdr.Ttl = rr.Hdr.Ttl&0x00FFFFFF | uint32(v>>4)<<24 } // UDPSize returns the UDP buffer size. @@ -274,22 +273,16 @@ func (e *EDNS0_SUBNET) unpack(b []byte) error { if e.SourceNetmask > net.IPv4len*8 || e.SourceScope > net.IPv4len*8 { return errors.New("dns: bad netmask") } - addr := make([]byte, net.IPv4len) - for i := 0; i < net.IPv4len && 4+i < len(b); i++ { - addr[i] = b[4+i] - } - e.Address = net.IPv4(addr[0], addr[1], addr[2], addr[3]) + addr := make(net.IP, net.IPv4len) + copy(addr, b[4:]) + e.Address = addr.To16() case 2: if e.SourceNetmask > net.IPv6len*8 || e.SourceScope > net.IPv6len*8 { return errors.New("dns: bad netmask") } - addr := make([]byte, net.IPv6len) - for i := 0; i < net.IPv6len && 4+i < len(b); i++ { - addr[i] = b[4+i] - } - e.Address = net.IP{addr[0], addr[1], addr[2], addr[3], addr[4], - addr[5], addr[6], addr[7], addr[8], addr[9], addr[10], - addr[11], addr[12], addr[13], addr[14], addr[15]} + addr := make(net.IP, net.IPv6len) + copy(addr, b[4:]) + e.Address = addr default: return errors.New("dns: bad address family") } diff --git a/vendor/github.com/miekg/dns/generate.go b/vendor/github.com/miekg/dns/generate.go index e4481a4b..97bc39f5 100644 --- a/vendor/github.com/miekg/dns/generate.go +++ b/vendor/github.com/miekg/dns/generate.go @@ -2,8 +2,8 @@ package dns import ( "bytes" - "errors" "fmt" + "io" "strconv" "strings" ) @@ -18,142 +18,225 @@ import ( // * rhs (rdata) // But we are lazy here, only the range is parsed *all* occurrences // of $ after that are interpreted. -// Any error are returned as a string value, the empty string signals -// "no error". -func generate(l lex, c chan lex, t chan *Token, o string) string { +func (zp *ZoneParser) generate(l lex) (RR, bool) { + token := l.token step := 1 - if i := strings.IndexAny(l.token, "/"); i != -1 { - if i+1 == len(l.token) { - return "bad step in $GENERATE range" + if i := strings.IndexByte(token, '/'); i >= 0 { + if i+1 == len(token) { + return zp.setParseError("bad step in $GENERATE range", l) } - if s, err := strconv.Atoi(l.token[i+1:]); err == nil { - if s < 0 { - return "bad step in $GENERATE range" - } - step = s - } else { - return "bad step in $GENERATE range" + + s, err := strconv.Atoi(token[i+1:]) + if err != nil || s <= 0 { + return zp.setParseError("bad step in $GENERATE range", l) } - l.token = l.token[:i] + + step = s + token = token[:i] } - sx := strings.SplitN(l.token, "-", 2) + + sx := strings.SplitN(token, "-", 2) if len(sx) != 2 { - return "bad start-stop in $GENERATE range" + return zp.setParseError("bad start-stop in $GENERATE range", l) } + start, err := strconv.Atoi(sx[0]) if err != nil { - return "bad start in $GENERATE range" + return zp.setParseError("bad start in $GENERATE range", l) } + end, err := strconv.Atoi(sx[1]) if err != nil { - return "bad stop in $GENERATE range" + return zp.setParseError("bad stop in $GENERATE range", l) } if end < 0 || start < 0 || end < start { - return "bad range in $GENERATE range" + return zp.setParseError("bad range in $GENERATE range", l) } - <-c // _BLANK + zp.c.Next() // _BLANK + // Create a complete new string, which we then parse again. - s := "" -BuildRR: - l = <-c - if l.value != zNewline && l.value != zEOF { - s += l.token - goto BuildRR - } - for i := start; i <= end; i += step { - var ( - escape bool - dom bytes.Buffer - mod string - err error - offset int - ) + var s string + for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() { + if l.err { + return zp.setParseError("bad data in $GENERATE directive", l) + } + if l.value == zNewline { + break + } - for j := 0; j < len(s); j++ { // No 'range' because we need to jump around - switch s[j] { - case '\\': - if escape { - dom.WriteByte('\\') - escape = false - continue - } - escape = true - case '$': - mod = "%d" - offset = 0 - if escape { - dom.WriteByte('$') - escape = false - continue - } - escape = false - if j+1 >= len(s) { // End of the string - dom.WriteString(fmt.Sprintf(mod, i+offset)) - continue - } else { - if s[j+1] == '$' { - dom.WriteByte('$') - j++ - continue - } - } - // Search for { and } - if s[j+1] == '{' { // Modifier block - sep := strings.Index(s[j+2:], "}") - if sep == -1 { - return "bad modifier in $GENERATE" - } - mod, offset, err = modToPrintf(s[j+2 : j+2+sep]) - if err != nil { - return err.Error() - } - j += 2 + sep // Jump to it - } - dom.WriteString(fmt.Sprintf(mod, i+offset)) - default: - if escape { // Pretty useless here - escape = false - continue - } - dom.WriteByte(s[j]) - } - } - // Re-parse the RR and send it on the current channel t - rx, err := NewRR("$ORIGIN " + o + "\n" + dom.String()) - if err != nil { - return err.Error() - } - t <- &Token{RR: rx} - // Its more efficient to first built the rrlist and then parse it in - // one go! But is this a problem? + s += l.token + } + + r := &generateReader{ + s: s, + + cur: start, + start: start, + end: end, + step: step, + + file: zp.file, + lex: &l, + } + zp.sub = NewZoneParser(r, zp.origin, zp.file) + zp.sub.includeDepth, zp.sub.includeAllowed = zp.includeDepth, zp.includeAllowed + zp.sub.SetDefaultTTL(defaultTtl) + return zp.subNext() +} + +type generateReader struct { + s string + si int + + cur int + start int + end int + step int + + mod bytes.Buffer + + escape bool + + eof bool + + file string + lex *lex +} + +func (r *generateReader) parseError(msg string, end int) *ParseError { + r.eof = true // Make errors sticky. + + l := *r.lex + l.token = r.s[r.si-1 : end] + l.column += r.si // l.column starts one zBLANK before r.s + + return &ParseError{r.file, msg, l} +} + +func (r *generateReader) Read(p []byte) (int, error) { + // NewZLexer, through NewZoneParser, should use ReadByte and + // not end up here. + + panic("not implemented") +} + +func (r *generateReader) ReadByte() (byte, error) { + if r.eof { + return 0, io.EOF + } + if r.mod.Len() > 0 { + return r.mod.ReadByte() + } + + if r.si >= len(r.s) { + r.si = 0 + r.cur += r.step + + r.eof = r.cur > r.end || r.cur < 0 + return '\n', nil + } + + si := r.si + r.si++ + + switch r.s[si] { + case '\\': + if r.escape { + r.escape = false + return '\\', nil + } + + r.escape = true + return r.ReadByte() + case '$': + if r.escape { + r.escape = false + return '$', nil + } + + mod := "%d" + + if si >= len(r.s)-1 { + // End of the string + fmt.Fprintf(&r.mod, mod, r.cur) + return r.mod.ReadByte() + } + + if r.s[si+1] == '$' { + r.si++ + return '$', nil + } + + var offset int + + // Search for { and } + if r.s[si+1] == '{' { + // Modifier block + sep := strings.Index(r.s[si+2:], "}") + if sep < 0 { + return 0, r.parseError("bad modifier in $GENERATE", len(r.s)) + } + + var errMsg string + mod, offset, errMsg = modToPrintf(r.s[si+2 : si+2+sep]) + if errMsg != "" { + return 0, r.parseError(errMsg, si+3+sep) + } + if r.start+offset < 0 || r.end+offset > 1<<31-1 { + return 0, r.parseError("bad offset in $GENERATE", si+3+sep) + } + + r.si += 2 + sep // Jump to it + } + + fmt.Fprintf(&r.mod, mod, r.cur+offset) + return r.mod.ReadByte() + default: + if r.escape { // Pretty useless here + r.escape = false + return r.ReadByte() + } + + return r.s[si], nil } - return "" } // Convert a $GENERATE modifier 0,0,d to something Printf can deal with. -func modToPrintf(s string) (string, int, error) { - xs := strings.SplitN(s, ",", 3) - if len(xs) != 3 { - return "", 0, errors.New("bad modifier in $GENERATE") +func modToPrintf(s string) (string, int, string) { + // Modifier is { offset [ ,width [ ,base ] ] } - provide default + // values for optional width and type, if necessary. + var offStr, widthStr, base string + switch xs := strings.Split(s, ","); len(xs) { + case 1: + offStr, widthStr, base = xs[0], "0", "d" + case 2: + offStr, widthStr, base = xs[0], xs[1], "d" + case 3: + offStr, widthStr, base = xs[0], xs[1], xs[2] + default: + return "", 0, "bad modifier in $GENERATE" } - // xs[0] is offset, xs[1] is width, xs[2] is base - if xs[2] != "o" && xs[2] != "d" && xs[2] != "x" && xs[2] != "X" { - return "", 0, errors.New("bad base in $GENERATE") + + switch base { + case "o", "d", "x", "X": + default: + return "", 0, "bad base in $GENERATE" } - offset, err := strconv.Atoi(xs[0]) - if err != nil || offset > 255 { - return "", 0, errors.New("bad offset in $GENERATE") + + offset, err := strconv.Atoi(offStr) + if err != nil { + return "", 0, "bad offset in $GENERATE" } - width, err := strconv.Atoi(xs[1]) - if err != nil || width > 255 { - return "", offset, errors.New("bad width in $GENERATE") + + width, err := strconv.Atoi(widthStr) + if err != nil || width < 0 || width > 255 { + return "", 0, "bad width in $GENERATE" } - switch { - case width < 0: - return "", offset, errors.New("bad width in $GENERATE") - case width == 0: - return "%" + xs[1] + xs[2], offset, nil + + if width == 0 { + return "%" + base, offset, "" } - return "%0" + xs[1] + xs[2], offset, nil + + return "%0" + widthStr + base, offset, "" } diff --git a/vendor/github.com/miekg/dns/labels.go b/vendor/github.com/miekg/dns/labels.go index 760b89e7..577fc59d 100644 --- a/vendor/github.com/miekg/dns/labels.go +++ b/vendor/github.com/miekg/dns/labels.go @@ -178,10 +178,10 @@ func equal(a, b string) bool { ai := a[i] bi := b[i] if ai >= 'A' && ai <= 'Z' { - ai |= ('a' - 'A') + ai |= 'a' - 'A' } if bi >= 'A' && bi <= 'Z' { - bi |= ('a' - 'A') + bi |= 'a' - 'A' } if ai != bi { return false diff --git a/vendor/github.com/miekg/dns/listen_go111.go b/vendor/github.com/miekg/dns/listen_go111.go new file mode 100644 index 00000000..fad195cf --- /dev/null +++ b/vendor/github.com/miekg/dns/listen_go111.go @@ -0,0 +1,44 @@ +// +build go1.11 +// +build aix darwin dragonfly freebsd linux netbsd openbsd + +package dns + +import ( + "context" + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +const supportsReusePort = true + +func reuseportControl(network, address string, c syscall.RawConn) error { + var opErr error + err := c.Control(func(fd uintptr) { + opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + if err != nil { + return err + } + + return opErr +} + +func listenTCP(network, addr string, reuseport bool) (net.Listener, error) { + var lc net.ListenConfig + if reuseport { + lc.Control = reuseportControl + } + + return lc.Listen(context.Background(), network, addr) +} + +func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) { + var lc net.ListenConfig + if reuseport { + lc.Control = reuseportControl + } + + return lc.ListenPacket(context.Background(), network, addr) +} diff --git a/vendor/github.com/miekg/dns/listen_go_not111.go b/vendor/github.com/miekg/dns/listen_go_not111.go new file mode 100644 index 00000000..b9201417 --- /dev/null +++ b/vendor/github.com/miekg/dns/listen_go_not111.go @@ -0,0 +1,23 @@ +// +build !go1.11 !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd + +package dns + +import "net" + +const supportsReusePort = false + +func listenTCP(network, addr string, reuseport bool) (net.Listener, error) { + if reuseport { + // TODO(tmthrgd): return an error? + } + + return net.Listen(network, addr) +} + +func listenUDP(network, addr string, reuseport bool) (net.PacketConn, error) { + if reuseport { + // TODO(tmthrgd): return an error? + } + + return net.ListenPacket(network, addr) +} diff --git a/vendor/github.com/miekg/dns/msg.go b/vendor/github.com/miekg/dns/msg.go index dcd3b6a5..14ec5d10 100644 --- a/vendor/github.com/miekg/dns/msg.go +++ b/vendor/github.com/miekg/dns/msg.go @@ -24,6 +24,18 @@ import ( const ( maxCompressionOffset = 2 << 13 // We have 14 bits for the compression pointer maxDomainNameWireOctets = 255 // See RFC 1035 section 2.3.4 + + // This is the maximum number of compression pointers that should occur in a + // semantically valid message. Each label in a domain name must be at least one + // octet and is separated by a period. The root label won't be represented by a + // compression pointer to a compression pointer, hence the -2 to exclude the + // smallest valid root label. + // + // It is possible to construct a valid message that has more compression pointers + // than this, and still doesn't loop, by pointing to a previous pointer. This is + // not something a well written implementation should ever do, so we leave them + // to trip the maximum compression pointer check. + maxCompressionPointers = (maxDomainNameWireOctets+1)/2 - 2 ) // Errors defined in this package. @@ -46,10 +58,9 @@ var ( ErrRRset error = &Error{err: "bad rrset"} ErrSecret error = &Error{err: "no secrets defined"} ErrShortRead error = &Error{err: "short read"} - ErrSig error = &Error{err: "bad signature"} // ErrSig indicates that a signature can not be cryptographically validated. - ErrSoa error = &Error{err: "no SOA"} // ErrSOA indicates that no SOA RR was seen when doing zone transfers. - ErrTime error = &Error{err: "bad time"} // ErrTime indicates a timing error in TSIG authentication. - ErrTruncated error = &Error{err: "failed to unpack truncated message"} // ErrTruncated indicates that we failed to unpack a truncated message. We unpacked as much as we had so Msg can still be used, if desired. + ErrSig error = &Error{err: "bad signature"} // ErrSig indicates that a signature can not be cryptographically validated. + ErrSoa error = &Error{err: "no SOA"} // ErrSOA indicates that no SOA RR was seen when doing zone transfers. + ErrTime error = &Error{err: "bad time"} // ErrTime indicates a timing error in TSIG authentication. ) // Id by default, returns a 16 bits random number to be used as a @@ -151,7 +162,7 @@ var RcodeToString = map[int]string{ RcodeFormatError: "FORMERR", RcodeServerFailure: "SERVFAIL", RcodeNameError: "NXDOMAIN", - RcodeNotImplemented: "NOTIMPL", + RcodeNotImplemented: "NOTIMP", RcodeRefused: "REFUSED", RcodeYXDomain: "YXDOMAIN", // See RFC 2136 RcodeYXRrset: "YXRRSET", @@ -187,133 +198,167 @@ func packDomainName(s string, msg []byte, off int, compression map[string]int, c if msg != nil { lenmsg = len(msg) } + ls := len(s) if ls == 0 { // Ok, for instance when dealing with update RR without any rdata. return off, 0, nil } - // If not fully qualified, error out, but only if msg == nil #ugly - switch { - case msg == nil: - if s[ls-1] != '.' { - s += "." - ls++ - } - case msg != nil: - if s[ls-1] != '.' { + + // If not fully qualified, error out, but only if msg != nil #ugly + if s[ls-1] != '.' { + if msg != nil { return lenmsg, 0, ErrFqdn } + s += "." + ls++ } + // Each dot ends a segment of the name. // We trade each dot byte for a length byte. // Except for escaped dots (\.), which are normal dots. // There is also a trailing zero. // Compression - nameoffset := -1 pointer := -1 + // Emit sequence of counted strings, chopping at dots. - begin := 0 - bs := []byte(s) - roBs, bsFresh, escapedDot := s, true, false + var ( + begin int + bs []byte + wasDot bool + ) +loop: for i := 0; i < ls; i++ { - if bs[i] == '\\' { - for j := i; j < ls-1; j++ { - bs[j] = bs[j+1] - } - ls-- + var c byte + if bs == nil { + c = s[i] + } else { + c = bs[i] + } + + switch c { + case '\\': if off+1 > lenmsg { return lenmsg, labels, ErrBuf } - // check for \DDD - if i+2 < ls && isDigit(bs[i]) && isDigit(bs[i+1]) && isDigit(bs[i+2]) { - bs[i] = dddToByte(bs[i:]) - for j := i + 1; j < ls-2; j++ { - bs[j] = bs[j+2] - } - ls -= 2 - } - escapedDot = bs[i] == '.' - bsFresh = false - continue - } - if bs[i] == '.' { - if i > 0 && bs[i-1] == '.' && !escapedDot { + if bs == nil { + bs = []byte(s) + } + + // check for \DDD + if i+3 < ls && isDigit(bs[i+1]) && isDigit(bs[i+2]) && isDigit(bs[i+3]) { + bs[i] = dddToByte(bs[i+1:]) + copy(bs[i+1:ls-3], bs[i+4:]) + ls -= 3 + } else { + copy(bs[i:ls-1], bs[i+1:]) + ls-- + } + + wasDot = false + case '.': + if wasDot { // two dots back to back is not legal return lenmsg, labels, ErrRdata } - if i-begin >= 1<<6 { // top two bits of length must be clear + wasDot = true + + labelLen := i - begin + if labelLen >= 1<<6 { // top two bits of length must be clear return lenmsg, labels, ErrRdata } + // off can already (we're in a loop) be bigger than len(msg) // this happens when a name isn't fully qualified - if off+1 > lenmsg { + if off+1+labelLen > lenmsg { return lenmsg, labels, ErrBuf } - if msg != nil { - msg[off] = byte(i - begin) - } - offset := off - off++ - for j := begin; j < i; j++ { - if off+1 > lenmsg { - return lenmsg, labels, ErrBuf - } - if msg != nil { - msg[off] = bs[j] - } - off++ - } - if compress && !bsFresh { - roBs = string(bs) - bsFresh = true - } + // Don't try to compress '.' - // We should only compress when compress it true, but we should also still pick + // We should only compress when compress is true, but we should also still pick // up names that can be used for *future* compression(s). - if compression != nil && roBs[begin:] != "." { - if p, ok := compression[roBs[begin:]]; !ok { - // Only offsets smaller than this can be used. - if offset < maxCompressionOffset { - compression[roBs[begin:]] = offset - } + if compression != nil && !isRootLabel(s, bs, begin, ls) { + var ( + p int + ok bool + ) + if bs == nil { + p, ok = compression[s[begin:]] } else { + p, ok = compression[string(bs[begin:ls])] + } + + if ok { // The first hit is the longest matching dname // keep the pointer offset we get back and store // the offset of the current name, because that's // where we need to insert the pointer later // If compress is true, we're allowed to compress this dname - if pointer == -1 && compress { - pointer = p // Where to point to - nameoffset = offset // Where to point from - break + if compress { + pointer = p // Where to point to + break loop + } + } else if off < maxCompressionOffset { + // Only offsets smaller than maxCompressionOffset can be used. + if bs == nil { + compression[s[begin:]] = off + } else { + compression[string(bs[begin:ls])] = off } } } + + // The following is covered by the length check above. + if msg != nil { + msg[off] = byte(labelLen) + + if bs == nil { + copy(msg[off+1:], s[begin:i]) + } else { + copy(msg[off+1:], bs[begin:i]) + } + } + off += 1 + labelLen + labels++ begin = i + 1 + default: + wasDot = false } - escapedDot = false } + // Root label is special - if len(bs) == 1 && bs[0] == '.' { + if isRootLabel(s, bs, 0, ls) { return off, labels, nil } + // If we did compression and we find something add the pointer here if pointer != -1 { // We have two bytes (14 bits) to put the pointer in // if msg == nil, we will never do compression - binary.BigEndian.PutUint16(msg[nameoffset:], uint16(pointer^0xC000)) - off = nameoffset + 1 - goto End + binary.BigEndian.PutUint16(msg[off:], uint16(pointer^0xC000)) + return off + 2, labels, nil } - if msg != nil && off < len(msg) { + + if msg != nil && off < lenmsg { msg[off] = 0 } -End: - off++ - return off, labels, nil + + return off + 1, labels, nil +} + +// isRootLabel returns whether s or bs, from off to end, is the root +// label ".". +// +// If bs is nil, s will be checked, otherwise bs will be checked. +func isRootLabel(s string, bs []byte, off, end int) bool { + if bs == nil { + return s[off:end] == "." + } + + return end-off == 1 && bs[off] == '.' } // Unpack a domain name. @@ -330,12 +375,16 @@ End: // In theory, the pointers are only allowed to jump backward. // We let them jump anywhere and stop jumping after a while. -// UnpackDomainName unpacks a domain name into a string. +// UnpackDomainName unpacks a domain name into a string. It returns +// the name, the new offset into msg and any error that occurred. +// +// When an error is encountered, the unpacked name will be discarded +// and len(msg) will be returned as the offset. func UnpackDomainName(msg []byte, off int) (string, int, error) { s := make([]byte, 0, 64) off1 := 0 lenmsg := len(msg) - maxLen := maxDomainNameWireOctets + budget := maxDomainNameWireOctets ptr := 0 // number of pointers followed Loop: for { @@ -354,27 +403,25 @@ Loop: if off+c > lenmsg { return "", lenmsg, ErrBuf } + budget -= c + 1 // +1 for the label separator + if budget <= 0 { + return "", lenmsg, ErrLongDomain + } for j := off; j < off+c; j++ { switch b := msg[j]; b { case '.', '(', ')', ';', ' ', '@': fallthrough case '"', '\\': s = append(s, '\\', b) - // presentation-format \X escapes add an extra byte - maxLen++ default: if b < 32 || b >= 127 { // unprintable, use \DDD var buf [3]byte bufs := strconv.AppendInt(buf[:0], int64(b), 10) s = append(s, '\\') - for i := 0; i < 3-len(bufs); i++ { + for i := len(bufs); i < 3; i++ { s = append(s, '0') } - for _, r := range bufs { - s = append(s, r) - } - // presentation-format \DDD escapes add 3 extra bytes - maxLen += 3 + s = append(s, bufs...) } else { s = append(s, b) } @@ -396,7 +443,7 @@ Loop: if ptr == 0 { off1 = off } - if ptr++; ptr > 10 { + if ptr++; ptr > maxCompressionPointers { return "", lenmsg, &Error{err: "too many compression pointers"} } // pointer should guarantee that it advances and points forwards at least @@ -412,10 +459,7 @@ Loop: off1 = off } if len(s) == 0 { - s = []byte(".") - } else if len(s) >= maxLen { - // error if the name is too long, but don't throw it away - return string(s), lenmsg, ErrLongDomain + return ".", off1, nil } return string(s), off1, nil } @@ -512,7 +556,7 @@ func unpackTxt(msg []byte, off0 int) (ss []string, off int, err error) { off = off0 var s string for off < len(msg) && err == nil { - s, off, err = unpackTxtString(msg, off) + s, off, err = unpackString(msg, off) if err == nil { ss = append(ss, s) } @@ -520,43 +564,16 @@ func unpackTxt(msg []byte, off0 int) (ss []string, off int, err error) { return } -func unpackTxtString(msg []byte, offset int) (string, int, error) { - if offset+1 > len(msg) { - return "", offset, &Error{err: "overflow unpacking txt"} - } - l := int(msg[offset]) - if offset+l+1 > len(msg) { - return "", offset, &Error{err: "overflow unpacking txt"} - } - s := make([]byte, 0, l) - for _, b := range msg[offset+1 : offset+1+l] { - switch b { - case '"', '\\': - s = append(s, '\\', b) - default: - if b < 32 || b > 127 { // unprintable - var buf [3]byte - bufs := strconv.AppendInt(buf[:0], int64(b), 10) - s = append(s, '\\') - for i := 0; i < 3-len(bufs); i++ { - s = append(s, '0') - } - for _, r := range bufs { - s = append(s, r) - } - } else { - s = append(s, b) - } - } - } - offset += 1 + l - return string(s), offset, nil -} - // Helpers for dealing with escaped bytes func isDigit(b byte) bool { return b >= '0' && b <= '9' } func dddToByte(s []byte) byte { + _ = s[2] // bounds check hint to compiler; see golang.org/issue/14808 + return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')) +} + +func dddStringToByte(s string) byte { + _ = s[2] // bounds check hint to compiler; see golang.org/issue/14808 return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0')) } @@ -693,32 +710,33 @@ func (dns *Msg) Pack() (msg []byte, err error) { // PackBuffer packs a Msg, using the given buffer buf. If buf is too small a new buffer is allocated. func (dns *Msg) PackBuffer(buf []byte) (msg []byte, err error) { - var compression map[string]int - if dns.Compress { - compression = make(map[string]int) // Compression pointer mappings. + // If this message can't be compressed, avoid filling the + // compression map and creating garbage. + if dns.Compress && dns.isCompressible() { + compression := make(map[string]int) // Compression pointer mappings. + return dns.packBufferWithCompressionMap(buf, compression, true) } - return dns.packBufferWithCompressionMap(buf, compression) + + return dns.packBufferWithCompressionMap(buf, nil, false) } // packBufferWithCompressionMap packs a Msg, using the given buffer buf. -func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression map[string]int) (msg []byte, err error) { - // We use a similar function in tsig.go's stripTsig. - - var dh Header - +func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression map[string]int, compress bool) (msg []byte, err error) { if dns.Rcode < 0 || dns.Rcode > 0xFFF { return nil, ErrRcode } - if dns.Rcode > 0xF { - // Regular RCODE field is 4 bits - opt := dns.IsEdns0() - if opt == nil { - return nil, ErrExtendedRcode - } - opt.SetExtendedRcode(uint8(dns.Rcode >> 4)) + + // Set extended rcode unconditionally if we have an opt, this will allow + // reseting the extended rcode bits if they need to. + if opt := dns.IsEdns0(); opt != nil { + opt.SetExtendedRcode(uint16(dns.Rcode)) + } else if dns.Rcode > 0xF { + // If Rcode is an extended one and opt is nil, error out. + return nil, ErrExtendedRcode } // Convert convenient Msg into wire-like Header. + var dh Header dh.Id = dns.Id dh.Bits = uint16(dns.Opcode)<<11 | uint16(dns.Rcode&0xF) if dns.Response { @@ -746,16 +764,10 @@ func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression map[string] dh.Bits |= _CD } - // Prepare variable sized arrays. - question := dns.Question - answer := dns.Answer - ns := dns.Ns - extra := dns.Extra - - dh.Qdcount = uint16(len(question)) - dh.Ancount = uint16(len(answer)) - dh.Nscount = uint16(len(ns)) - dh.Arcount = uint16(len(extra)) + dh.Qdcount = uint16(len(dns.Question)) + dh.Ancount = uint16(len(dns.Answer)) + dh.Nscount = uint16(len(dns.Ns)) + dh.Arcount = uint16(len(dns.Extra)) // We need the uncompressed length here, because we first pack it and then compress it. msg = buf @@ -766,30 +778,30 @@ func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression map[string] // Pack it in: header and then the pieces. off := 0 - off, err = dh.pack(msg, off, compression, dns.Compress) + off, err = dh.pack(msg, off, compression, compress) if err != nil { return nil, err } - for i := 0; i < len(question); i++ { - off, err = question[i].pack(msg, off, compression, dns.Compress) + for _, r := range dns.Question { + off, err = r.pack(msg, off, compression, compress) if err != nil { return nil, err } } - for i := 0; i < len(answer); i++ { - off, err = PackRR(answer[i], msg, off, compression, dns.Compress) + for _, r := range dns.Answer { + off, err = PackRR(r, msg, off, compression, compress) if err != nil { return nil, err } } - for i := 0; i < len(ns); i++ { - off, err = PackRR(ns[i], msg, off, compression, dns.Compress) + for _, r := range dns.Ns { + off, err = PackRR(r, msg, off, compression, compress) if err != nil { return nil, err } } - for i := 0; i < len(extra); i++ { - off, err = PackRR(extra[i], msg, off, compression, dns.Compress) + for _, r := range dns.Extra { + off, err = PackRR(r, msg, off, compression, compress) if err != nil { return nil, err } @@ -797,28 +809,7 @@ func (dns *Msg) packBufferWithCompressionMap(buf []byte, compression map[string] return msg[:off], nil } -// Unpack unpacks a binary message to a Msg structure. -func (dns *Msg) Unpack(msg []byte) (err error) { - var ( - dh Header - off int - ) - if dh, off, err = unpackMsgHdr(msg, off); err != nil { - return err - } - - dns.Id = dh.Id - dns.Response = (dh.Bits & _QR) != 0 - dns.Opcode = int(dh.Bits>>11) & 0xF - dns.Authoritative = (dh.Bits & _AA) != 0 - dns.Truncated = (dh.Bits & _TC) != 0 - dns.RecursionDesired = (dh.Bits & _RD) != 0 - dns.RecursionAvailable = (dh.Bits & _RA) != 0 - dns.Zero = (dh.Bits & _Z) != 0 - dns.AuthenticatedData = (dh.Bits & _AD) != 0 - dns.CheckingDisabled = (dh.Bits & _CD) != 0 - dns.Rcode = int(dh.Bits & 0xF) - +func (dns *Msg) unpack(dh Header, msg []byte, off int) (err error) { // If we are at the end of the message we should return *just* the // header. This can still be useful to the caller. 9.9.9.9 sends these // when responding with REFUSED for instance. @@ -837,8 +828,6 @@ func (dns *Msg) Unpack(msg []byte) (err error) { var q Question q, off, err = unpackQuestion(msg, off) if err != nil { - // Even if Truncated is set, we only will set ErrTruncated if we - // actually got the questions return err } if off1 == off { // Offset does not increase anymore, dh.Qdcount is a lie! @@ -862,16 +851,29 @@ func (dns *Msg) Unpack(msg []byte) (err error) { // The header counts might have been wrong so we need to update it dh.Arcount = uint16(len(dns.Extra)) + // Set extended Rcode + if opt := dns.IsEdns0(); opt != nil { + dns.Rcode |= opt.ExtendedRcode() + } + if off != len(msg) { // TODO(miek) make this an error? // use PackOpt to let people tell how detailed the error reporting should be? // println("dns: extra bytes in dns packet", off, "<", len(msg)) - } else if dns.Truncated { - // Whether we ran into a an error or not, we want to return that it - // was truncated - err = ErrTruncated } return err + +} + +// Unpack unpacks a binary message to a Msg structure. +func (dns *Msg) Unpack(msg []byte) (err error) { + dh, off, err := unpackMsgHdr(msg, 0) + if err != nil { + return err + } + + dns.setHdr(dh) + return dns.unpack(dh, msg, off) } // Convert a complete message to a string with dig-like output. @@ -923,7 +925,14 @@ func (dns *Msg) String() string { // than packing it, measuring the size and discarding the buffer. func (dns *Msg) Len() int { return compressedLen(dns, dns.Compress) } -func compressedLenWithCompressionMap(dns *Msg, compression map[string]int) int { +// isCompressible returns whether the msg may be compressible. +func (dns *Msg) isCompressible() bool { + // If we only have one question, there is nothing we can ever compress. + return len(dns.Question) > 1 || len(dns.Answer) > 0 || + len(dns.Ns) > 0 || len(dns.Extra) > 0 +} + +func compressedLenWithCompressionMap(dns *Msg, compression map[string]struct{}) int { l := 12 // Message header is always 12 bytes for _, r := range dns.Question { compressionLenHelper(compression, r.Name, l) @@ -939,12 +948,15 @@ func compressedLenWithCompressionMap(dns *Msg, compression map[string]int) int { // when compress is true, otherwise the uncompressed length is returned. func compressedLen(dns *Msg, compress bool) int { // We always return one more than needed. - if compress { - compression := map[string]int{} + + // If this message can't be compressed, avoid filling the + // compression map and creating garbage. + if compress && dns.isCompressible() { + compression := make(map[string]struct{}) return compressedLenWithCompressionMap(dns, compression) } - l := 12 // Message header is always 12 bytes + l := 12 // Message header is always 12 bytes for _, r := range dns.Question { l += r.len() } @@ -967,7 +979,7 @@ func compressedLen(dns *Msg, compress bool) int { return l } -func compressionLenSlice(lenp int, c map[string]int, rs []RR) int { +func compressionLenSlice(lenp int, c map[string]struct{}, rs []RR) int { initLen := lenp for _, r := range rs { if r == nil { @@ -999,7 +1011,7 @@ func compressionLenSlice(lenp int, c map[string]int, rs []RR) int { } // Put the parts of the name in the compression map, return the size in bytes added in payload -func compressionLenHelper(c map[string]int, s string, currentLen int) int { +func compressionLenHelper(c map[string]struct{}, s string, currentLen int) int { if currentLen > maxCompressionOffset { // We won't be able to add any label that could be re-used later anyway return 0 @@ -1018,7 +1030,7 @@ func compressionLenHelper(c map[string]int, s string, currentLen int) int { if _, ok := c[pref]; !ok { // If first byte label is within the first 14bits, it might be re-used later if currentLen < maxCompressionOffset { - c[pref] = currentLen + c[pref] = struct{}{} } } else { added := currentLen - initLen @@ -1036,7 +1048,7 @@ func compressionLenHelper(c map[string]int, s string, currentLen int) int { // keep on searching so we get the longest match. // Will return the size of compression found, whether a match has been // found and the size of record if added in payload -func compressionLenSearch(c map[string]int, s string) (int, bool, int) { +func compressionLenSearch(c map[string]struct{}, s string) (int, bool, int) { off := 0 end := false if s == "" { // don't bork on bogus data @@ -1204,3 +1216,18 @@ func unpackMsgHdr(msg []byte, off int) (Header, int, error) { dh.Arcount, off, err = unpackUint16(msg, off) return dh, off, err } + +// setHdr set the header in the dns using the binary data in dh. +func (dns *Msg) setHdr(dh Header) { + dns.Id = dh.Id + dns.Response = dh.Bits&_QR != 0 + dns.Opcode = int(dh.Bits>>11) & 0xF + dns.Authoritative = dh.Bits&_AA != 0 + dns.Truncated = dh.Bits&_TC != 0 + dns.RecursionDesired = dh.Bits&_RD != 0 + dns.RecursionAvailable = dh.Bits&_RA != 0 + dns.Zero = dh.Bits&_Z != 0 // _Z covers the zero bit, which should be zero; not sure why we set it to the opposite. + dns.AuthenticatedData = dh.Bits&_AD != 0 + dns.CheckingDisabled = dh.Bits&_CD != 0 + dns.Rcode = int(dh.Bits & 0xF) +} diff --git a/vendor/github.com/miekg/dns/msg_helpers.go b/vendor/github.com/miekg/dns/msg_helpers.go index 4a6e878d..a5b342e3 100644 --- a/vendor/github.com/miekg/dns/msg_helpers.go +++ b/vendor/github.com/miekg/dns/msg_helpers.go @@ -6,7 +6,7 @@ import ( "encoding/binary" "encoding/hex" "net" - "strconv" + "strings" ) // helper functions called from the generated zmsg.go @@ -223,8 +223,8 @@ func unpackUint48(msg []byte, off int) (i uint64, off1 int, err error) { return 0, len(msg), &Error{err: "overflow unpacking uint64 as uint48"} } // Used in TSIG where the last 48 bits are occupied, so for now, assume a uint48 (6 bytes) - i = (uint64(uint64(msg[off])<<40 | uint64(msg[off+1])<<32 | uint64(msg[off+2])<<24 | uint64(msg[off+3])<<16 | - uint64(msg[off+4])<<8 | uint64(msg[off+5]))) + i = uint64(msg[off])<<40 | uint64(msg[off+1])<<32 | uint64(msg[off+2])<<24 | uint64(msg[off+3])<<16 | + uint64(msg[off+4])<<8 | uint64(msg[off+5]) off += 6 return i, off, nil } @@ -267,29 +267,21 @@ func unpackString(msg []byte, off int) (string, int, error) { if off+l+1 > len(msg) { return "", off, &Error{err: "overflow unpacking txt"} } - s := make([]byte, 0, l) + var s strings.Builder + s.Grow(l) for _, b := range msg[off+1 : off+1+l] { - switch b { - case '"', '\\': - s = append(s, '\\', b) + switch { + case b == '"' || b == '\\': + s.WriteByte('\\') + s.WriteByte(b) + case b < ' ' || b > '~': // unprintable + writeEscapedByte(&s, b) default: - if b < 32 || b > 127 { // unprintable - var buf [3]byte - bufs := strconv.AppendInt(buf[:0], int64(b), 10) - s = append(s, '\\') - for i := 0; i < 3-len(bufs); i++ { - s = append(s, '0') - } - for _, r := range bufs { - s = append(s, r) - } - } else { - s = append(s, b) - } + s.WriteByte(b) } } off += 1 + l - return string(s), off, nil + return s.String(), off, nil } func packString(s string, msg []byte, off int) (int, error) { @@ -363,7 +355,7 @@ func packStringHex(s string, msg []byte, off int) (int, error) { if err != nil { return len(msg), err } - if off+(len(h)) > len(msg) { + if off+len(h) > len(msg) { return len(msg), &Error{err: "overflow packing hex"} } copy(msg[off:off+len(h)], h) @@ -603,7 +595,7 @@ func packDataNsec(bitmap []uint16, msg []byte, off int) (int, error) { // Setting the octets length msg[off+1] = byte(length) // Setting the bit value for the type in the right octet - msg[off+1+int(length)] |= byte(1 << (7 - (t % 8))) + msg[off+1+int(length)] |= byte(1 << (7 - t%8)) lastwindow, lastlength = window, length } off += int(lastlength) + 2 diff --git a/vendor/github.com/miekg/dns/nsecx.go b/vendor/github.com/miekg/dns/nsecx.go index 9b908c44..7b4c55e2 100644 --- a/vendor/github.com/miekg/dns/nsecx.go +++ b/vendor/github.com/miekg/dns/nsecx.go @@ -63,8 +63,10 @@ func (rr *NSEC3) Cover(name string) bool { } nextHash := rr.NextDomain - if ownerHash == nextHash { // empty interval - return false + + // if empty interval found, try cover wildcard hashes so nameHash shouldn't match with ownerHash + if ownerHash == nextHash && nameHash != ownerHash { // empty interval + return true } if ownerHash > nextHash { // end of zone if nameHash > ownerHash { // covered since there is nothing after ownerHash diff --git a/vendor/github.com/miekg/dns/privaterr.go b/vendor/github.com/miekg/dns/privaterr.go index 41989e7a..74544a74 100644 --- a/vendor/github.com/miekg/dns/privaterr.go +++ b/vendor/github.com/miekg/dns/privaterr.go @@ -105,7 +105,7 @@ func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) return rr, off, err } - setPrivateRR := func(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { + setPrivateRR := func(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := mkPrivateRR(h.Rrtype) rr.Hdr = h @@ -115,7 +115,7 @@ func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) for { // TODO(miek): we could also be returning _QUOTE, this might or might not // be an issue (basically parsing TXT becomes hard) - switch l = <-c; l.value { + switch l, _ = c.Next(); l.value { case zNewline, zEOF: break Fetch case zString: @@ -134,7 +134,7 @@ func PrivateHandle(rtypestr string, rtype uint16, generator func() PrivateRdata) typeToparserFunc[rtype] = parserFunc{setPrivateRR, true} } -// PrivateHandleRemove removes defenitions required to support private RR type. +// PrivateHandleRemove removes definitions required to support private RR type. func PrivateHandleRemove(rtype uint16) { rtypestr, ok := TypeToString[rtype] if ok { @@ -144,5 +144,4 @@ func PrivateHandleRemove(rtype uint16) { delete(StringToType, rtypestr) delete(typeToUnpack, rtype) } - return } diff --git a/vendor/github.com/miekg/dns/reverse.go b/vendor/github.com/miekg/dns/reverse.go index f6e7a47a..1f0e2b2a 100644 --- a/vendor/github.com/miekg/dns/reverse.go +++ b/vendor/github.com/miekg/dns/reverse.go @@ -12,6 +12,11 @@ var StringToOpcode = reverseInt(OpcodeToString) // StringToRcode is a map of rcodes to strings. var StringToRcode = reverseInt(RcodeToString) +func init() { + // Preserve previous NOTIMP typo, see github.com/miekg/dns/issues/733. + StringToRcode["NOTIMPL"] = RcodeNotImplemented +} + // Reverse a map func reverseInt8(m map[uint8]string) map[string]uint8 { n := make(map[string]uint8, len(m)) diff --git a/vendor/github.com/miekg/dns/sanitize.go b/vendor/github.com/miekg/dns/sanitize.go index c415bdd6..cac15787 100644 --- a/vendor/github.com/miekg/dns/sanitize.go +++ b/vendor/github.com/miekg/dns/sanitize.go @@ -5,6 +5,7 @@ package dns // rrs. // m is used to store the RRs temporary. If it is nil a new map will be allocated. func Dedup(rrs []RR, m map[string]RR) []RR { + if m == nil { m = make(map[string]RR) } diff --git a/vendor/github.com/miekg/dns/scan.go b/vendor/github.com/miekg/dns/scan.go index f9cd4740..61ace121 100644 --- a/vendor/github.com/miekg/dns/scan.go +++ b/vendor/github.com/miekg/dns/scan.go @@ -1,6 +1,7 @@ package dns import ( + "bufio" "fmt" "io" "os" @@ -10,7 +11,10 @@ import ( ) const maxTok = 2048 // Largest token we can return. -const maxUint16 = 1<<16 - 1 + +// The maximum depth of $INCLUDE directives supported by the +// ZoneParser API. +const maxIncludeDepth = 7 // Tokinize a RFC 1035 zone file. The tokenizer will normalize it: // * Add ownernames if they are left blank; @@ -75,15 +79,13 @@ func (e *ParseError) Error() (s string) { } type lex struct { - token string // text of the token - tokenUpper string // uppercase text of the token - length int // length of the token - err bool // when true, token text has lexer error - value uint8 // value: zString, _BLANK, etc. - line int // line in the file - column int // column in the file - torc uint16 // type or class as parsed in the lexer, we only need to look this up in the grammar - comment string // any comment text seen + token string // text of the token + err bool // when true, token text has lexer error + value uint8 // value: zString, _BLANK, etc. + torc uint16 // type or class as parsed in the lexer, we only need to look this up in the grammar + line int // line in the file + column int // column in the file + comment string // any comment text seen } // Token holds the token that are returned when a zone file is parsed. @@ -103,10 +105,14 @@ type ttlState struct { } // NewRR reads the RR contained in the string s. Only the first RR is -// returned. If s contains no RR, return nil with no error. The class -// defaults to IN and TTL defaults to 3600. The full zone file syntax -// like $TTL, $ORIGIN, etc. is supported. All fields of the returned -// RR are set, except RR.Header().Rdlength which is set to 0. +// returned. If s contains no records, NewRR will return nil with no +// error. +// +// The class defaults to IN and TTL defaults to 3600. The full zone +// file syntax like $TTL, $ORIGIN, etc. is supported. +// +// All fields of the returned RR are set, except RR.Header().Rdlength +// which is set to 0. func NewRR(s string) (RR, error) { if len(s) > 0 && s[len(s)-1] != '\n' { // We need a closing newline return ReadRR(strings.NewReader(s+"\n"), "") @@ -114,28 +120,31 @@ func NewRR(s string) (RR, error) { return ReadRR(strings.NewReader(s), "") } -// ReadRR reads the RR contained in q. +// ReadRR reads the RR contained in r. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. +// // See NewRR for more documentation. -func ReadRR(q io.Reader, filename string) (RR, error) { - defttl := &ttlState{defaultTtl, false} - r := <-parseZoneHelper(q, ".", filename, defttl, 1) - if r == nil { - return nil, nil - } - - if r.Error != nil { - return nil, r.Error - } - return r.RR, nil +func ReadRR(r io.Reader, file string) (RR, error) { + zp := NewZoneParser(r, ".", file) + zp.SetDefaultTTL(defaultTtl) + zp.SetIncludeAllowed(true) + rr, _ := zp.Next() + return rr, zp.Err() } -// ParseZone reads a RFC 1035 style zonefile from r. It returns *Tokens on the -// returned channel, each consisting of either a parsed RR and optional comment -// or a nil RR and an error. The string file is only used -// in error reporting. The string origin is used as the initial origin, as -// if the file would start with an $ORIGIN directive. -// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are supported. -// The channel t is closed by ParseZone when the end of r is reached. +// ParseZone reads a RFC 1035 style zonefile from r. It returns +// *Tokens on the returned channel, each consisting of either a +// parsed RR and optional comment or a nil RR and an error. The +// channel is closed by ParseZone when the end of r is reached. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. The string origin is used as the initial +// origin, as if the file would start with an $ORIGIN directive. +// +// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all +// supported. // // Basic usage pattern when reading from a string (z) containing the // zone data: @@ -148,91 +157,246 @@ func ReadRR(q io.Reader, filename string) (RR, error) { // } // } // -// Comments specified after an RR (and on the same line!) are returned too: +// Comments specified after an RR (and on the same line!) are +// returned too: // // foo. IN A 10.0.0.1 ; this is a comment // -// The text "; this is comment" is returned in Token.Comment. Comments inside the -// RR are discarded. Comments on a line by themselves are discarded too. +// The text "; this is comment" is returned in Token.Comment. +// Comments inside the RR are returned concatenated along with the +// RR. Comments on a line by themselves are discarded. +// +// To prevent memory leaks it is important to always fully drain the +// returned channel. If an error occurs, it will always be the last +// Token sent on the channel. +// +// Deprecated: New users should prefer the ZoneParser API. func ParseZone(r io.Reader, origin, file string) chan *Token { - return parseZoneHelper(r, origin, file, nil, 10000) -} - -func parseZoneHelper(r io.Reader, origin, file string, defttl *ttlState, chansize int) chan *Token { - t := make(chan *Token, chansize) - go parseZone(r, origin, file, defttl, t, 0) + t := make(chan *Token, 10000) + go parseZone(r, origin, file, t) return t } -func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, include int) { - defer func() { - if include == 0 { - close(t) - } - }() - s, cancel := scanInit(r) - c := make(chan lex) - // Start the lexer - go zlexer(s, c) +func parseZone(r io.Reader, origin, file string, t chan *Token) { + defer close(t) - defer func() { - cancel() - // zlexer can send up to three tokens, the next one and possibly 2 remainders. - // Do a non-blocking read. - _, ok := <-c - _, ok = <-c - _, ok = <-c + zp := NewZoneParser(r, origin, file) + zp.SetIncludeAllowed(true) + + for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { + t <- &Token{RR: rr, Comment: zp.Comment()} + } + + if err := zp.Err(); err != nil { + pe, ok := err.(*ParseError) if !ok { - // too bad + pe = &ParseError{file: file, err: err.Error()} } - }() - // 6 possible beginnings of a line, _ is a space - // 0. zRRTYPE -> all omitted until the rrtype - // 1. zOwner _ zRrtype -> class/ttl omitted - // 2. zOwner _ zString _ zRrtype -> class omitted - // 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class - // 4. zOwner _ zClass _ zRrtype -> ttl omitted - // 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed) - // After detecting these, we know the zRrtype so we can jump to functions - // handling the rdata for each of these types. + t <- &Token{Error: pe} + } +} + +// ZoneParser is a parser for an RFC 1035 style zonefile. +// +// Each parsed RR in the zone is returned sequentially from Next. An +// optional comment can be retrieved with Comment. +// +// The directives $INCLUDE, $ORIGIN, $TTL and $GENERATE are all +// supported. Although $INCLUDE is disabled by default. +// +// Basic usage pattern when reading from a string (z) containing the +// zone data: +// +// zp := NewZoneParser(strings.NewReader(z), "", "") +// +// for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { +// // Do something with rr +// } +// +// if err := zp.Err(); err != nil { +// // log.Println(err) +// } +// +// Comments specified after an RR (and on the same line!) are +// returned too: +// +// foo. IN A 10.0.0.1 ; this is a comment +// +// The text "; this is comment" is returned from Comment. Comments inside +// the RR are returned concatenated along with the RR. Comments on a line +// by themselves are discarded. +type ZoneParser struct { + c *zlexer + + parseErr *ParseError + + origin string + file string + + defttl *ttlState + + h RR_Header + + // sub is used to parse $INCLUDE files and $GENERATE directives. + // Next, by calling subNext, forwards the resulting RRs from this + // sub parser to the calling code. + sub *ZoneParser + osFile *os.File + + com string + + includeDepth uint8 + + includeAllowed bool +} + +// NewZoneParser returns an RFC 1035 style zonefile parser that reads +// from r. +// +// The string file is used in error reporting and to resolve relative +// $INCLUDE directives. The string origin is used as the initial +// origin, as if the file would start with an $ORIGIN directive. +func NewZoneParser(r io.Reader, origin, file string) *ZoneParser { + var pe *ParseError if origin != "" { origin = Fqdn(origin) if _, ok := IsDomainName(origin); !ok { - t <- &Token{Error: &ParseError{f, "bad initial origin name", lex{}}} - return + pe = &ParseError{file, "bad initial origin name", lex{}} } } - st := zExpectOwnerDir // initial state - var h RR_Header - var prevName string - for l := range c { - // Lexer spotted an error already - if l.err == true { - t <- &Token{Error: &ParseError{f, l.token, l}} - return + return &ZoneParser{ + c: newZLexer(r), + parseErr: pe, + + origin: origin, + file: file, + } +} + +// SetDefaultTTL sets the parsers default TTL to ttl. +func (zp *ZoneParser) SetDefaultTTL(ttl uint32) { + zp.defttl = &ttlState{ttl, false} +} + +// SetIncludeAllowed controls whether $INCLUDE directives are +// allowed. $INCLUDE directives are not supported by default. +// +// The $INCLUDE directive will open and read from a user controlled +// file on the system. Even if the file is not a valid zonefile, the +// contents of the file may be revealed in error messages, such as: +// +// /etc/passwd: dns: not a TTL: "root:x:0:0:root:/root:/bin/bash" at line: 1:31 +// /etc/shadow: dns: not a TTL: "root:$6$::0:99999:7:::" at line: 1:125 +func (zp *ZoneParser) SetIncludeAllowed(v bool) { + zp.includeAllowed = v +} + +// Err returns the first non-EOF error that was encountered by the +// ZoneParser. +func (zp *ZoneParser) Err() error { + if zp.parseErr != nil { + return zp.parseErr + } + + if zp.sub != nil { + if err := zp.sub.Err(); err != nil { + return err } + } + + return zp.c.Err() +} + +func (zp *ZoneParser) setParseError(err string, l lex) (RR, bool) { + zp.parseErr = &ParseError{zp.file, err, l} + return nil, false +} + +// Comment returns an optional text comment that occurred alongside +// the RR. +func (zp *ZoneParser) Comment() string { + return zp.com +} + +func (zp *ZoneParser) subNext() (RR, bool) { + if rr, ok := zp.sub.Next(); ok { + zp.com = zp.sub.com + return rr, true + } + + if zp.sub.osFile != nil { + zp.sub.osFile.Close() + zp.sub.osFile = nil + } + + if zp.sub.Err() != nil { + // We have errors to surface. + return nil, false + } + + zp.sub = nil + return zp.Next() +} + +// Next advances the parser to the next RR in the zonefile and +// returns the (RR, true). It will return (nil, false) when the +// parsing stops, either by reaching the end of the input or an +// error. After Next returns (nil, false), the Err method will return +// any error that occurred during parsing. +func (zp *ZoneParser) Next() (RR, bool) { + zp.com = "" + + if zp.parseErr != nil { + return nil, false + } + if zp.sub != nil { + return zp.subNext() + } + + // 6 possible beginnings of a line (_ is a space): + // + // 0. zRRTYPE -> all omitted until the rrtype + // 1. zOwner _ zRrtype -> class/ttl omitted + // 2. zOwner _ zString _ zRrtype -> class omitted + // 3. zOwner _ zString _ zClass _ zRrtype -> ttl/class + // 4. zOwner _ zClass _ zRrtype -> ttl omitted + // 5. zOwner _ zClass _ zString _ zRrtype -> class/ttl (reversed) + // + // After detecting these, we know the zRrtype so we can jump to functions + // handling the rdata for each of these types. + + st := zExpectOwnerDir // initial state + h := &zp.h + + for l, ok := zp.c.Next(); ok; l, ok = zp.c.Next() { + // zlexer spotted an error already + if l.err { + return zp.setParseError(l.token, l) + } + switch st { case zExpectOwnerDir: // We can also expect a directive, like $TTL or $ORIGIN - if defttl != nil { - h.Ttl = defttl.ttl + if zp.defttl != nil { + h.Ttl = zp.defttl.ttl } + h.Class = ClassINET + switch l.value { case zNewline: st = zExpectOwnerDir case zOwner: - h.Name = l.token - name, ok := toAbsoluteName(l.token, origin) + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad owner name", l}} - return + return zp.setParseError("bad owner name", l) } + h.Name = name - prevName = h.Name + st = zExpectOwnerBl case zDirTTL: st = zExpectDirTTLBl @@ -243,12 +407,12 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i case zDirGenerate: st = zExpectDirGenerateBl case zRrtpe: - h.Name = prevName h.Rrtype = l.torc + st = zExpectRdata case zClass: - h.Name = prevName h.Class = l.torc + st = zExpectAnyNoClassBl case zBlank: // Discard, can happen when there is nothing on the @@ -256,297 +420,400 @@ func parseZone(r io.Reader, origin, f string, defttl *ttlState, t chan *Token, i case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } - h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} - } - st = zExpectAnyNoTTLBl + h.Ttl = ttl + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} + } + + st = zExpectAnyNoTTLBl default: - t <- &Token{Error: &ParseError{f, "syntax error at beginning", l}} - return + return zp.setParseError("syntax error at beginning", l) } case zExpectDirIncludeBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $INCLUDE-directive", l}} - return + return zp.setParseError("no blank after $INCLUDE-directive", l) } + st = zExpectDirInclude case zExpectDirInclude: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $INCLUDE value, not this...", l}} - return + return zp.setParseError("expecting $INCLUDE value, not this...", l) } - neworigin := origin // There may be optionally a new origin set after the filename, if not use current one - switch l := <-c; l.value { + + neworigin := zp.origin // There may be optionally a new origin set after the filename, if not use current one + switch l, _ := zp.c.Next(); l.value { case zBlank: - l := <-c + l, _ := zp.c.Next() if l.value == zString { - name, ok := toAbsoluteName(l.token, origin) + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad origin name", l}} - return + return zp.setParseError("bad origin name", l) } + neworigin = name } case zNewline, zEOF: // Ok default: - t <- &Token{Error: &ParseError{f, "garbage after $INCLUDE", l}} - return + return zp.setParseError("garbage after $INCLUDE", l) } + + if !zp.includeAllowed { + return zp.setParseError("$INCLUDE directive not allowed", l) + } + if zp.includeDepth >= maxIncludeDepth { + return zp.setParseError("too deeply nested $INCLUDE", l) + } + // Start with the new file includePath := l.token if !filepath.IsAbs(includePath) { - includePath = filepath.Join(filepath.Dir(f), includePath) + includePath = filepath.Join(filepath.Dir(zp.file), includePath) } + r1, e1 := os.Open(includePath) if e1 != nil { - msg := fmt.Sprintf("failed to open `%s'", l.token) + var as string if !filepath.IsAbs(l.token) { - msg += fmt.Sprintf(" as `%s'", includePath) + as = fmt.Sprintf(" as `%s'", includePath) } - t <- &Token{Error: &ParseError{f, msg, l}} - return + + msg := fmt.Sprintf("failed to open `%s'%s: %v", l.token, as, e1) + return zp.setParseError(msg, l) } - if include+1 > 7 { - t <- &Token{Error: &ParseError{f, "too deeply nested $INCLUDE", l}} - return - } - parseZone(r1, neworigin, includePath, defttl, t, include+1) - st = zExpectOwnerDir + + zp.sub = NewZoneParser(r1, neworigin, includePath) + zp.sub.defttl, zp.sub.includeDepth, zp.sub.osFile = zp.defttl, zp.includeDepth+1, r1 + zp.sub.SetIncludeAllowed(true) + return zp.subNext() case zExpectDirTTLBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $TTL-directive", l}} - return + return zp.setParseError("no blank after $TTL-directive", l) } + st = zExpectDirTTL case zExpectDirTTL: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}} - return + return zp.setParseError("expecting $TTL value, not this...", l) } - if e, _ := slurpRemainder(c, f); e != nil { - t <- &Token{Error: e} - return + + if e, _ := slurpRemainder(zp.c, zp.file); e != nil { + zp.parseErr = e + return nil, false } + ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "expecting $TTL value, not this...", l}} - return + return zp.setParseError("expecting $TTL value, not this...", l) } - defttl = &ttlState{ttl, true} + + zp.defttl = &ttlState{ttl, true} + st = zExpectOwnerDir case zExpectDirOriginBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $ORIGIN-directive", l}} - return + return zp.setParseError("no blank after $ORIGIN-directive", l) } + st = zExpectDirOrigin case zExpectDirOrigin: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $ORIGIN value, not this...", l}} - return + return zp.setParseError("expecting $ORIGIN value, not this...", l) } - if e, _ := slurpRemainder(c, f); e != nil { - t <- &Token{Error: e} + + if e, _ := slurpRemainder(zp.c, zp.file); e != nil { + zp.parseErr = e + return nil, false } - name, ok := toAbsoluteName(l.token, origin) + + name, ok := toAbsoluteName(l.token, zp.origin) if !ok { - t <- &Token{Error: &ParseError{f, "bad origin name", l}} - return + return zp.setParseError("bad origin name", l) } - origin = name + + zp.origin = name + st = zExpectOwnerDir case zExpectDirGenerateBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after $GENERATE-directive", l}} - return + return zp.setParseError("no blank after $GENERATE-directive", l) } + st = zExpectDirGenerate case zExpectDirGenerate: if l.value != zString { - t <- &Token{Error: &ParseError{f, "expecting $GENERATE value, not this...", l}} - return + return zp.setParseError("expecting $GENERATE value, not this...", l) } - if errMsg := generate(l, c, t, origin); errMsg != "" { - t <- &Token{Error: &ParseError{f, errMsg, l}} - return - } - st = zExpectOwnerDir + + return zp.generate(l) case zExpectOwnerBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank after owner", l}} - return + return zp.setParseError("no blank after owner", l) } + st = zExpectAny case zExpectAny: switch l.value { case zRrtpe: - if defttl == nil { - t <- &Token{Error: &ParseError{f, "missing TTL with no previous value", l}} - return + if zp.defttl == nil { + return zp.setParseError("missing TTL with no previous value", l) } + h.Rrtype = l.torc + st = zExpectRdata case zClass: h.Class = l.torc + st = zExpectAnyNoClassBl case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } + h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} } + st = zExpectAnyNoTTLBl default: - t <- &Token{Error: &ParseError{f, "expecting RR type, TTL or class, not this...", l}} - return + return zp.setParseError("expecting RR type, TTL or class, not this...", l) } case zExpectAnyNoClassBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before class", l}} - return + return zp.setParseError("no blank before class", l) } + st = zExpectAnyNoClass case zExpectAnyNoTTLBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before TTL", l}} - return + return zp.setParseError("no blank before TTL", l) } + st = zExpectAnyNoTTL case zExpectAnyNoTTL: switch l.value { case zClass: h.Class = l.torc + st = zExpectRrtypeBl case zRrtpe: h.Rrtype = l.torc + st = zExpectRdata default: - t <- &Token{Error: &ParseError{f, "expecting RR type or class, not this...", l}} - return + return zp.setParseError("expecting RR type or class, not this...", l) } case zExpectAnyNoClass: switch l.value { case zString: ttl, ok := stringToTTL(l.token) if !ok { - t <- &Token{Error: &ParseError{f, "not a TTL", l}} - return + return zp.setParseError("not a TTL", l) } + h.Ttl = ttl - if defttl == nil || !defttl.isByDirective { - defttl = &ttlState{ttl, false} + + if zp.defttl == nil || !zp.defttl.isByDirective { + zp.defttl = &ttlState{ttl, false} } + st = zExpectRrtypeBl case zRrtpe: h.Rrtype = l.torc + st = zExpectRdata default: - t <- &Token{Error: &ParseError{f, "expecting RR type or TTL, not this...", l}} - return + return zp.setParseError("expecting RR type or TTL, not this...", l) } case zExpectRrtypeBl: if l.value != zBlank { - t <- &Token{Error: &ParseError{f, "no blank before RR type", l}} - return + return zp.setParseError("no blank before RR type", l) } + st = zExpectRrtype case zExpectRrtype: if l.value != zRrtpe { - t <- &Token{Error: &ParseError{f, "unknown RR type", l}} - return + return zp.setParseError("unknown RR type", l) } + h.Rrtype = l.torc + st = zExpectRdata case zExpectRdata: - r, e, c1 := setRR(h, c, origin, f) + r, e, c1 := setRR(*h, zp.c, zp.origin, zp.file) if e != nil { // If e.lex is nil than we have encounter a unknown RR type // in that case we substitute our current lex token if e.lex.token == "" && e.lex.value == 0 { e.lex = l // Uh, dirty } - t <- &Token{Error: e} - return + + zp.parseErr = e + return nil, false } - t <- &Token{RR: r, Comment: c1} - st = zExpectOwnerDir + + zp.com = c1 + return r, true } } + // If we get here, we and the h.Rrtype is still zero, we haven't parsed anything, this // is not an error, because an empty zone file is still a zone file. + return nil, false } -// zlexer scans the sourcefile and returns tokens on the channel c. -func zlexer(s *scan, c chan lex) { - var l lex - str := make([]byte, maxTok) // Should be enough for any token - stri := 0 // Offset in str (0 means empty) - com := make([]byte, maxTok) // Hold comment text - comi := 0 - quote := false - escape := false - space := false - commt := false - rrtype := false - owner := true - brace := 0 - x, err := s.tokenText() - defer close(c) - for err == nil { - l.column = s.position.Column - l.line = s.position.Line - if stri >= maxTok { +type zlexer struct { + br io.ByteReader + + readErr error + + line int + column int + + com string + + l lex + + brace int + quote bool + space bool + commt bool + rrtype bool + owner bool + + nextL bool + + eol bool // end-of-line +} + +func newZLexer(r io.Reader) *zlexer { + br, ok := r.(io.ByteReader) + if !ok { + br = bufio.NewReaderSize(r, 1024) + } + + return &zlexer{ + br: br, + + line: 1, + + owner: true, + } +} + +func (zl *zlexer) Err() error { + if zl.readErr == io.EOF { + return nil + } + + return zl.readErr +} + +// readByte returns the next byte from the input +func (zl *zlexer) readByte() (byte, bool) { + if zl.readErr != nil { + return 0, false + } + + c, err := zl.br.ReadByte() + if err != nil { + zl.readErr = err + return 0, false + } + + // delay the newline handling until the next token is delivered, + // fixes off-by-one errors when reporting a parse error. + if zl.eol { + zl.line++ + zl.column = 0 + zl.eol = false + } + + if c == '\n' { + zl.eol = true + } else { + zl.column++ + } + + return c, true +} + +func (zl *zlexer) Next() (lex, bool) { + l := &zl.l + if zl.nextL { + zl.nextL = false + return *l, true + } + if l.err { + // Parsing errors should be sticky. + return lex{value: zEOF}, false + } + + var ( + str [maxTok]byte // Hold string text + com [maxTok]byte // Hold comment text + + stri int // Offset in str (0 means empty) + comi int // Offset in com (0 means empty) + + escape bool + ) + + if zl.com != "" { + comi = copy(com[:], zl.com) + zl.com = "" + } + + for x, ok := zl.readByte(); ok; x, ok = zl.readByte() { + l.line, l.column = zl.line, zl.column + l.comment = "" + + if stri >= len(str) { l.token = "token length insufficient for parsing" l.err = true - c <- l - return + return *l, true } - if comi >= maxTok { + if comi >= len(com) { l.token = "comment length insufficient for parsing" l.err = true - c <- l - return + return *l, true } switch x { case ' ', '\t': - if escape { + if escape || zl.quote { + // Inside quotes or escaped this is legal. + str[stri] = x + stri++ + escape = false - str[stri] = x - stri++ break } - if quote { - // Inside quotes this is legal - str[stri] = x - stri++ - break - } - if commt { + + if zl.commt { com[comi] = x comi++ break } + + var retL lex if stri == 0 { // Space directly in the beginning, handled in the grammar - } else if owner { + } else if zl.owner { // If we have a string and its the first, make it an owner l.value = zOwner l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri + // escape $... start with a \ not a $, so this will work - switch l.tokenUpper { + switch strings.ToUpper(l.token) { case "$TTL": l.value = zDirTTL case "$ORIGIN": @@ -556,259 +823,316 @@ func zlexer(s *scan, c chan lex) { case "$GENERATE": l.value = zDirGenerate } - c <- l + + retL = *l } else { l.value = zString l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri - if !rrtype { - if t, ok := StringToType[l.tokenUpper]; ok { + + if !zl.rrtype { + tokenUpper := strings.ToUpper(l.token) + if t, ok := StringToType[tokenUpper]; ok { l.value = zRrtpe l.torc = t - rrtype = true - } else { - if strings.HasPrefix(l.tokenUpper, "TYPE") { - t, ok := typeToInt(l.token) - if !ok { - l.token = "unknown RR type" - l.err = true - c <- l - return - } - l.value = zRrtpe - rrtype = true - l.torc = t + + zl.rrtype = true + } else if strings.HasPrefix(tokenUpper, "TYPE") { + t, ok := typeToInt(l.token) + if !ok { + l.token = "unknown RR type" + l.err = true + return *l, true } + + l.value = zRrtpe + l.torc = t + + zl.rrtype = true } - if t, ok := StringToClass[l.tokenUpper]; ok { + + if t, ok := StringToClass[tokenUpper]; ok { l.value = zClass l.torc = t - } else { - if strings.HasPrefix(l.tokenUpper, "CLASS") { - t, ok := classToInt(l.token) - if !ok { - l.token = "unknown class" - l.err = true - c <- l - return - } - l.value = zClass - l.torc = t + } else if strings.HasPrefix(tokenUpper, "CLASS") { + t, ok := classToInt(l.token) + if !ok { + l.token = "unknown class" + l.err = true + return *l, true } + + l.value = zClass + l.torc = t } } - c <- l - } - stri = 0 - if !space && !commt { + retL = *l + } + + zl.owner = false + + if !zl.space { + zl.space = true + l.value = zBlank l.token = " " - l.length = 1 - c <- l + + if retL == (lex{}) { + return *l, true + } + + zl.nextL = true + } + + if retL != (lex{}) { + return retL, true } - owner = false - space = true case ';': - if escape { + if escape || zl.quote { + // Inside quotes or escaped this is legal. + str[stri] = x + stri++ + escape = false - str[stri] = x - stri++ break } - if quote { - // Inside quotes this is legal - str[stri] = x - stri++ - break + + zl.commt = true + zl.com = "" + + if comi > 1 { + // A newline was previously seen inside a comment that + // was inside braces and we delayed adding it until now. + com[comi] = ' ' // convert newline to space + comi++ } - if stri > 0 { - l.value = zString - l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri - c <- l - stri = 0 - } - commt = true + com[comi] = ';' comi++ + + if stri > 0 { + zl.com = string(com[:comi]) + + l.value = zString + l.token = string(str[:stri]) + return *l, true + } case '\r': escape = false - if quote { + + if zl.quote { str[stri] = x stri++ - break } + // discard if outside of quotes case '\n': escape = false + // Escaped newline - if quote { + if zl.quote { str[stri] = x stri++ break } - // inside quotes this is legal - if commt { + + if zl.commt { // Reset a comment - commt = false - rrtype = false - stri = 0 + zl.commt = false + zl.rrtype = false + // If not in a brace this ends the comment AND the RR - if brace == 0 { - owner = true - owner = true + if zl.brace == 0 { + zl.owner = true + l.value = zNewline l.token = "\n" - l.tokenUpper = l.token - l.length = 1 l.comment = string(com[:comi]) - c <- l - l.comment = "" - comi = 0 - break + return *l, true } - com[comi] = ' ' // convert newline to space - comi++ + + zl.com = string(com[:comi]) break } - if brace == 0 { + if zl.brace == 0 { // If there is previous text, we should output it here + var retL lex if stri != 0 { l.value = zString l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri - if !rrtype { - if t, ok := StringToType[l.tokenUpper]; ok { + if !zl.rrtype { + tokenUpper := strings.ToUpper(l.token) + if t, ok := StringToType[tokenUpper]; ok { + zl.rrtype = true + l.value = zRrtpe l.torc = t - rrtype = true } } - c <- l + + retL = *l } + l.value = zNewline l.token = "\n" - l.tokenUpper = l.token - l.length = 1 - c <- l - stri = 0 - commt = false - rrtype = false - owner = true - comi = 0 + l.comment = zl.com + + zl.com = "" + zl.rrtype = false + zl.owner = true + + if retL != (lex{}) { + zl.nextL = true + return retL, true + } + + return *l, true } case '\\': // comments do not get escaped chars, everything is copied - if commt { + if zl.commt { com[comi] = x comi++ break } + // something already escaped must be in string if escape { str[stri] = x stri++ + escape = false break } + // something escaped outside of string gets added to string str[stri] = x stri++ + escape = true case '"': - if commt { + if zl.commt { com[comi] = x comi++ break } + if escape { str[stri] = x stri++ + escape = false break } - space = false + + zl.space = false + // send previous gathered text and the quote + var retL lex if stri != 0 { l.value = zString l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri - c <- l - stri = 0 + retL = *l } // send quote itself as separate token l.value = zQuote l.token = "\"" - l.tokenUpper = l.token - l.length = 1 - c <- l - quote = !quote + + zl.quote = !zl.quote + + if retL != (lex{}) { + zl.nextL = true + return retL, true + } + + return *l, true case '(', ')': - if commt { + if zl.commt { com[comi] = x comi++ break } - if escape { + + if escape || zl.quote { + // Inside quotes or escaped this is legal. str[stri] = x stri++ + escape = false break } - if quote { - str[stri] = x - stri++ - break - } + switch x { case ')': - brace-- - if brace < 0 { + zl.brace-- + + if zl.brace < 0 { l.token = "extra closing brace" - l.tokenUpper = l.token l.err = true - c <- l - return + return *l, true } case '(': - brace++ + zl.brace++ } default: escape = false - if commt { + + if zl.commt { com[comi] = x comi++ break } + str[stri] = x stri++ - space = false + + zl.space = false } - x, err = s.tokenText() } + + if zl.readErr != nil && zl.readErr != io.EOF { + // Don't return any tokens after a read error occurs. + return lex{value: zEOF}, false + } + + var retL lex if stri > 0 { - // Send remainder - l.token = string(str[:stri]) - l.tokenUpper = strings.ToUpper(l.token) - l.length = stri + // Send remainder of str l.value = zString - c <- l + l.token = string(str[:stri]) + retL = *l + + if comi <= 0 { + return retL, true + } } - if brace != 0 { + + if comi > 0 { + // Send remainder of com + l.value = zNewline + l.token = "\n" + l.comment = string(com[:comi]) + + if retL != (lex{}) { + zl.nextL = true + return retL, true + } + + return *l, true + } + + if zl.brace != 0 { + l.comment = "" // in case there was left over string and comment l.token = "unbalanced brace" - l.tokenUpper = l.token l.err = true - c <- l + return *l, true } + + return lex{value: zEOF}, false } // Extract the class number from CLASSxx @@ -969,12 +1293,12 @@ func locCheckEast(token string, longitude uint32) (uint32, bool) { } // "Eat" the rest of the "line". Return potential comments -func slurpRemainder(c chan lex, f string) (*ParseError, string) { - l := <-c +func slurpRemainder(c *zlexer, f string) (*ParseError, string) { + l, _ := c.Next() com := "" switch l.value { case zBlank: - l = <-c + l, _ = c.Next() com = l.comment if l.value != zNewline && l.value != zEOF { return &ParseError{f, "garbage after rdata", l}, "" diff --git a/vendor/github.com/miekg/dns/scan_rr.go b/vendor/github.com/miekg/dns/scan_rr.go index fb6f95d1..935d22c3 100644 --- a/vendor/github.com/miekg/dns/scan_rr.go +++ b/vendor/github.com/miekg/dns/scan_rr.go @@ -11,7 +11,7 @@ type parserFunc struct { // Func defines the function that parses the tokens and returns the RR // or an error. The last string contains any comments in the line as // they returned by the lexer as well. - Func func(h RR_Header, c chan lex, origin string, file string) (RR, *ParseError, string) + Func func(h RR_Header, c *zlexer, origin string, file string) (RR, *ParseError, string) // Signals if the RR ending is of variable length, like TXT or records // that have Hexadecimal or Base64 as their last element in the Rdata. Records // that have a fixed ending or for instance A, AAAA, SOA and etc. @@ -23,7 +23,7 @@ type parserFunc struct { // After the rdata there may come a zBlank and then a zNewline // or immediately a zNewline. If this is not the case we flag // an *ParseError: garbage after rdata. -func setRR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRR(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { parserfunc, ok := typeToparserFunc[h.Rrtype] if ok { r, e, cm := parserfunc.Func(h, c, o, f) @@ -45,9 +45,9 @@ func setRR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { // A remainder of the rdata with embedded spaces, return the parsed string (sans the spaces) // or an error -func endingToString(c chan lex, errstr, f string) (string, *ParseError, string) { +func endingToString(c *zlexer, errstr, f string) (string, *ParseError, string) { s := "" - l := <-c // zString + l, _ := c.Next() // zString for l.value != zNewline && l.value != zEOF { if l.err { return s, &ParseError{f, errstr, l}, "" @@ -59,16 +59,16 @@ func endingToString(c chan lex, errstr, f string) (string, *ParseError, string) default: return "", &ParseError{f, errstr, l}, "" } - l = <-c + l, _ = c.Next() } return s, nil, l.comment } // A remainder of the rdata with embedded spaces, split on unquoted whitespace // and return the parsed string slice or an error -func endingToTxtSlice(c chan lex, errstr, f string) ([]string, *ParseError, string) { +func endingToTxtSlice(c *zlexer, errstr, f string) ([]string, *ParseError, string) { // Get the remaining data until we see a zNewline - l := <-c + l, _ := c.Next() if l.err { return nil, &ParseError{f, errstr, l}, "" } @@ -117,7 +117,7 @@ func endingToTxtSlice(c chan lex, errstr, f string) ([]string, *ParseError, stri default: return nil, &ParseError{f, errstr, l}, "" } - l = <-c + l, _ = c.Next() } if quote { return nil, &ParseError{f, errstr, l}, "" @@ -125,12 +125,12 @@ func endingToTxtSlice(c chan lex, errstr, f string) ([]string, *ParseError, stri return s, nil, l.comment } -func setA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(A) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -141,12 +141,12 @@ func setA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setAAAA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setAAAA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(AAAA) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -157,13 +157,13 @@ func setAAAA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setNS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNS(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NS) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Ns = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -175,13 +175,13 @@ func setNS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setPTR(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(PTR) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Ptr = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -193,13 +193,13 @@ func setPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setNSAPPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNSAPPTR(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NSAPPTR) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Ptr = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -211,13 +211,13 @@ func setNSAPPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) return rr, nil, "" } -func setRP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRP(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(RP) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Mbox = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -227,8 +227,8 @@ func setRP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Mbox = mbox - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rr.Txt = l.token txt, txtOk := toAbsoluteName(l.token, o) @@ -240,13 +240,13 @@ func setRP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMR(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MR) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Mr = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -258,13 +258,13 @@ func setMR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMB(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMB(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MB) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Mb = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -276,13 +276,13 @@ func setMB(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMG(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MG) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Mg = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -294,7 +294,7 @@ func setMG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setHINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setHINFO(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(HINFO) rr.Hdr = h @@ -320,13 +320,13 @@ func setHINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMINFO(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MINFO) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Rmail = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -336,8 +336,8 @@ func setMINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Rmail = rmail - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rr.Email = l.token email, emailOk := toAbsoluteName(l.token, o) @@ -349,13 +349,13 @@ func setMINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMF(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMF(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MF) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Mf = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -367,13 +367,13 @@ func setMF(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMD(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMD(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MD) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Md = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -385,12 +385,12 @@ func setMD(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setMX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setMX(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(MX) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -400,8 +400,8 @@ func setMX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Mx = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -413,12 +413,12 @@ func setMX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setRT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRT(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(RT) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -428,8 +428,8 @@ func setRT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Host = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -441,12 +441,12 @@ func setRT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setAFSDB(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setAFSDB(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(AFSDB) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -456,8 +456,8 @@ func setAFSDB(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Subtype = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Hostname = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -468,12 +468,12 @@ func setAFSDB(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setX25(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setX25(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(X25) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -484,12 +484,12 @@ func setX25(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setKX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setKX(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(KX) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -499,8 +499,8 @@ func setKX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Exchanger = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -511,13 +511,13 @@ func setKX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setCNAME(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCNAME(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(CNAME) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Target = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -529,13 +529,13 @@ func setCNAME(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setDNAME(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setDNAME(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(DNAME) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Target = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -547,13 +547,13 @@ func setDNAME(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setSOA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSOA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(SOA) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.Ns = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -563,8 +563,8 @@ func setSOA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Ns = ns - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rr.Mbox = l.token mbox, mboxOk := toAbsoluteName(l.token, o) @@ -573,14 +573,14 @@ func setSOA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Mbox = mbox - <-c // zBlank + c.Next() // zBlank var ( v uint32 ok bool ) for i := 0; i < 5; i++ { - l = <-c + l, _ = c.Next() if l.err { return nil, &ParseError{f, "bad SOA zone parameter", l}, "" } @@ -600,16 +600,16 @@ func setSOA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { switch i { case 0: rr.Serial = v - <-c // zBlank + c.Next() // zBlank case 1: rr.Refresh = v - <-c // zBlank + c.Next() // zBlank case 2: rr.Retry = v - <-c // zBlank + c.Next() // zBlank case 3: rr.Expire = v - <-c // zBlank + c.Next() // zBlank case 4: rr.Minttl = v } @@ -617,12 +617,12 @@ func setSOA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setSRV(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSRV(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(SRV) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -632,24 +632,24 @@ func setSRV(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Priority = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad SRV Weight", l}, "" } rr.Weight = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad SRV Port", l}, "" } rr.Port = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Target = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -660,12 +660,12 @@ func setSRV(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNAPTR(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NAPTR) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -675,8 +675,8 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Order = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad NAPTR Preference", l}, "" @@ -684,15 +684,15 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { rr.Preference = uint16(i) // Flags - <-c // zBlank - l = <-c // _QUOTE + c.Next() // zBlank + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Flags", l}, "" } - l = <-c // Either String or Quote + l, _ = c.Next() // Either String or Quote if l.value == zString { rr.Flags = l.token - l = <-c // _QUOTE + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Flags", l}, "" } @@ -703,15 +703,15 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } // Service - <-c // zBlank - l = <-c // _QUOTE + c.Next() // zBlank + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Service", l}, "" } - l = <-c // Either String or Quote + l, _ = c.Next() // Either String or Quote if l.value == zString { rr.Service = l.token - l = <-c // _QUOTE + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Service", l}, "" } @@ -722,15 +722,15 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } // Regexp - <-c // zBlank - l = <-c // _QUOTE + c.Next() // zBlank + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Regexp", l}, "" } - l = <-c // Either String or Quote + l, _ = c.Next() // Either String or Quote if l.value == zString { rr.Regexp = l.token - l = <-c // _QUOTE + l, _ = c.Next() // _QUOTE if l.value != zQuote { return nil, &ParseError{f, "bad NAPTR Regexp", l}, "" } @@ -741,8 +741,8 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } // After quote no space?? - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Replacement = l.token name, nameOk := toAbsoluteName(l.token, o) @@ -753,13 +753,13 @@ func setNAPTR(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setTALINK(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setTALINK(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(TALINK) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.PreviousName = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -769,8 +769,8 @@ func setTALINK(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.PreviousName = previousName - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rr.NextName = l.token nextName, nextNameOk := toAbsoluteName(l.token, o) @@ -782,7 +782,7 @@ func setTALINK(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setLOC(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(LOC) rr.Hdr = h // Non zero defaults for LOC record, see RFC 1876, Section 3. @@ -792,8 +792,8 @@ func setLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { ok := false // North - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } i, e := strconv.ParseUint(l.token, 10, 32) @@ -802,9 +802,9 @@ func setLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Latitude = 1000 * 60 * 60 * uint32(i) - <-c // zBlank + c.Next() // zBlank // Either number, 'N' or 'S' - l = <-c + l, _ = c.Next() if rr.Latitude, ok = locCheckNorth(l.token, rr.Latitude); ok { goto East } @@ -814,16 +814,16 @@ func setLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Latitude += 1000 * 60 * uint32(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, e := strconv.ParseFloat(l.token, 32); e != nil || l.err { return nil, &ParseError{f, "bad LOC Latitude seconds", l}, "" } else { rr.Latitude += uint32(1000 * i) } - <-c // zBlank + c.Next() // zBlank // Either number, 'N' or 'S' - l = <-c + l, _ = c.Next() if rr.Latitude, ok = locCheckNorth(l.token, rr.Latitude); ok { goto East } @@ -832,16 +832,16 @@ func setLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { East: // East - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, e := strconv.ParseUint(l.token, 10, 32); e != nil || l.err { return nil, &ParseError{f, "bad LOC Longitude", l}, "" } else { rr.Longitude = 1000 * 60 * 60 * uint32(i) } - <-c // zBlank + c.Next() // zBlank // Either number, 'E' or 'W' - l = <-c + l, _ = c.Next() if rr.Longitude, ok = locCheckEast(l.token, rr.Longitude); ok { goto Altitude } @@ -850,16 +850,16 @@ East: } else { rr.Longitude += 1000 * 60 * uint32(i) } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, e := strconv.ParseFloat(l.token, 32); e != nil || l.err { return nil, &ParseError{f, "bad LOC Longitude seconds", l}, "" } else { rr.Longitude += uint32(1000 * i) } - <-c // zBlank + c.Next() // zBlank // Either number, 'E' or 'W' - l = <-c + l, _ = c.Next() if rr.Longitude, ok = locCheckEast(l.token, rr.Longitude); ok { goto Altitude } @@ -867,9 +867,9 @@ East: return nil, &ParseError{f, "bad LOC Longitude East/West", l}, "" Altitude: - <-c // zBlank - l = <-c - if l.length == 0 || l.err { + c.Next() // zBlank + l, _ = c.Next() + if len(l.token) == 0 || l.err { return nil, &ParseError{f, "bad LOC Altitude", l}, "" } if l.token[len(l.token)-1] == 'M' || l.token[len(l.token)-1] == 'm' { @@ -882,7 +882,7 @@ Altitude: } // And now optionally the other values - l = <-c + l, _ = c.Next() count := 0 for l.value != zNewline && l.value != zEOF { switch l.value { @@ -893,19 +893,19 @@ Altitude: if !ok { return nil, &ParseError{f, "bad LOC Size", l}, "" } - rr.Size = (e & 0x0f) | (m << 4 & 0xf0) + rr.Size = e&0x0f | m<<4&0xf0 case 1: // HorizPre e, m, ok := stringToCm(l.token) if !ok { return nil, &ParseError{f, "bad LOC HorizPre", l}, "" } - rr.HorizPre = (e & 0x0f) | (m << 4 & 0xf0) + rr.HorizPre = e&0x0f | m<<4&0xf0 case 2: // VertPre e, m, ok := stringToCm(l.token) if !ok { return nil, &ParseError{f, "bad LOC VertPre", l}, "" } - rr.VertPre = (e & 0x0f) | (m << 4 & 0xf0) + rr.VertPre = e&0x0f | m<<4&0xf0 } count++ case zBlank: @@ -913,18 +913,18 @@ Altitude: default: return nil, &ParseError{f, "bad LOC Size, HorizPre or VertPre", l}, "" } - l = <-c + l, _ = c.Next() } return rr, nil, "" } -func setHIP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setHIP(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(HIP) rr.Hdr = h // HitLength is not represented - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -934,24 +934,24 @@ func setHIP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.PublicKeyAlgorithm = uint8(i) - <-c // zBlank - l = <-c // zString - if l.length == 0 || l.err { + c.Next() // zBlank + l, _ = c.Next() // zString + if len(l.token) == 0 || l.err { return nil, &ParseError{f, "bad HIP Hit", l}, "" } rr.Hit = l.token // This can not contain spaces, see RFC 5205 Section 6. rr.HitLength = uint8(len(rr.Hit)) / 2 - <-c // zBlank - l = <-c // zString - if l.length == 0 || l.err { + c.Next() // zBlank + l, _ = c.Next() // zString + if len(l.token) == 0 || l.err { return nil, &ParseError{f, "bad HIP PublicKey", l}, "" } rr.PublicKey = l.token // This cannot contain spaces rr.PublicKeyLength = uint16(base64.StdEncoding.DecodedLen(len(rr.PublicKey))) // RendezvousServers (if any) - l = <-c + l, _ = c.Next() var xs []string for l.value != zNewline && l.value != zEOF { switch l.value { @@ -966,18 +966,18 @@ func setHIP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { default: return nil, &ParseError{f, "bad HIP RendezvousServers", l}, "" } - l = <-c + l, _ = c.Next() } rr.RendezvousServers = xs return rr, nil, l.comment } -func setCERT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCERT(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(CERT) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -988,15 +988,15 @@ func setCERT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } else { rr.Type = uint16(i) } - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e := strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad CERT KeyTag", l}, "" } rr.KeyTag = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString if v, ok := StringToAlgorithm[l.token]; ok { rr.Algorithm = v } else if i, e := strconv.ParseUint(l.token, 10, 8); e != nil { @@ -1012,7 +1012,7 @@ func setCERT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setOPENPGPKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setOPENPGPKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(OPENPGPKEY) rr.Hdr = h @@ -1024,12 +1024,12 @@ func setOPENPGPKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, strin return rr, nil, c1 } -func setCSYNC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCSYNC(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(CSYNC) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } j, e := strconv.ParseUint(l.token, 10, 32) @@ -1039,9 +1039,9 @@ func setCSYNC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Serial = uint32(j) - <-c // zBlank + c.Next() // zBlank - l = <-c + l, _ = c.Next() j, e = strconv.ParseUint(l.token, 10, 16) if e != nil { // Serial must be a number @@ -1054,14 +1054,15 @@ func setCSYNC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { k uint16 ok bool ) - l = <-c + l, _ = c.Next() for l.value != zNewline && l.value != zEOF { switch l.value { case zBlank: // Ok case zString: - if k, ok = StringToType[l.tokenUpper]; !ok { - if k, ok = typeToInt(l.tokenUpper); !ok { + tokenUpper := strings.ToUpper(l.token) + if k, ok = StringToType[tokenUpper]; !ok { + if k, ok = typeToInt(l.token); !ok { return nil, &ParseError{f, "bad CSYNC TypeBitMap", l}, "" } } @@ -1069,12 +1070,12 @@ func setCSYNC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { default: return nil, &ParseError{f, "bad CSYNC TypeBitMap", l}, "" } - l = <-c + l, _ = c.Next() } return rr, nil, l.comment } -func setSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSIG(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setRRSIG(h, c, o, f) if r != nil { return &SIG{*r.(*RRSIG)}, e, s @@ -1082,18 +1083,19 @@ func setSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, e, s } -func setRRSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRRSIG(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(RRSIG) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } - if t, ok := StringToType[l.tokenUpper]; !ok { - if strings.HasPrefix(l.tokenUpper, "TYPE") { - t, ok = typeToInt(l.tokenUpper) + tokenUpper := strings.ToUpper(l.token) + if t, ok := StringToType[tokenUpper]; !ok { + if strings.HasPrefix(tokenUpper, "TYPE") { + t, ok = typeToInt(l.token) if !ok { return nil, &ParseError{f, "bad RRSIG Typecovered", l}, "" } @@ -1105,32 +1107,32 @@ func setRRSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { rr.TypeCovered = t } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, err := strconv.ParseUint(l.token, 10, 8) if err != nil || l.err { return nil, &ParseError{f, "bad RRSIG Algorithm", l}, "" } rr.Algorithm = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, err = strconv.ParseUint(l.token, 10, 8) if err != nil || l.err { return nil, &ParseError{f, "bad RRSIG Labels", l}, "" } rr.Labels = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, err = strconv.ParseUint(l.token, 10, 32) if err != nil || l.err { return nil, &ParseError{f, "bad RRSIG OrigTtl", l}, "" } rr.OrigTtl = uint32(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, err := StringToTime(l.token); err != nil { // Try to see if all numeric and use it as epoch if i, err := strconv.ParseInt(l.token, 10, 64); err == nil { @@ -1143,8 +1145,8 @@ func setRRSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { rr.Expiration = i } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, err := StringToTime(l.token); err != nil { if i, err := strconv.ParseInt(l.token, 10, 64); err == nil { rr.Inception = uint32(i) @@ -1155,16 +1157,16 @@ func setRRSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { rr.Inception = i } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, err = strconv.ParseUint(l.token, 10, 16) if err != nil || l.err { return nil, &ParseError{f, "bad RRSIG KeyTag", l}, "" } rr.KeyTag = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rr.SignerName = l.token name, nameOk := toAbsoluteName(l.token, o) if l.err || !nameOk { @@ -1181,13 +1183,13 @@ func setRRSIG(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setNSEC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNSEC(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NSEC) rr.Hdr = h - l := <-c + l, _ := c.Next() rr.NextDomain = l.token - if l.length == 0 { // dynamic update rr. + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1202,14 +1204,15 @@ func setNSEC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { k uint16 ok bool ) - l = <-c + l, _ = c.Next() for l.value != zNewline && l.value != zEOF { switch l.value { case zBlank: // Ok case zString: - if k, ok = StringToType[l.tokenUpper]; !ok { - if k, ok = typeToInt(l.tokenUpper); !ok { + tokenUpper := strings.ToUpper(l.token) + if k, ok = StringToType[tokenUpper]; !ok { + if k, ok = typeToInt(l.token); !ok { return nil, &ParseError{f, "bad NSEC TypeBitMap", l}, "" } } @@ -1217,17 +1220,17 @@ func setNSEC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { default: return nil, &ParseError{f, "bad NSEC TypeBitMap", l}, "" } - l = <-c + l, _ = c.Next() } return rr, nil, l.comment } -func setNSEC3(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNSEC3(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NSEC3) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1236,22 +1239,22 @@ func setNSEC3(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad NSEC3 Hash", l}, "" } rr.Hash = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad NSEC3 Flags", l}, "" } rr.Flags = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad NSEC3 Iterations", l}, "" } rr.Iterations = uint16(i) - <-c - l = <-c + c.Next() + l, _ = c.Next() if len(l.token) == 0 || l.err { return nil, &ParseError{f, "bad NSEC3 Salt", l}, "" } @@ -1260,8 +1263,8 @@ func setNSEC3(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { rr.Salt = l.token } - <-c - l = <-c + c.Next() + l, _ = c.Next() if len(l.token) == 0 || l.err { return nil, &ParseError{f, "bad NSEC3 NextDomain", l}, "" } @@ -1273,14 +1276,15 @@ func setNSEC3(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { k uint16 ok bool ) - l = <-c + l, _ = c.Next() for l.value != zNewline && l.value != zEOF { switch l.value { case zBlank: // Ok case zString: - if k, ok = StringToType[l.tokenUpper]; !ok { - if k, ok = typeToInt(l.tokenUpper); !ok { + tokenUpper := strings.ToUpper(l.token) + if k, ok = StringToType[tokenUpper]; !ok { + if k, ok = typeToInt(l.token); !ok { return nil, &ParseError{f, "bad NSEC3 TypeBitMap", l}, "" } } @@ -1288,17 +1292,17 @@ func setNSEC3(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { default: return nil, &ParseError{f, "bad NSEC3 TypeBitMap", l}, "" } - l = <-c + l, _ = c.Next() } return rr, nil, l.comment } -func setNSEC3PARAM(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNSEC3PARAM(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NSEC3PARAM) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1307,22 +1311,22 @@ func setNSEC3PARAM(h RR_Header, c chan lex, o, f string) (RR, *ParseError, strin return nil, &ParseError{f, "bad NSEC3PARAM Hash", l}, "" } rr.Hash = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad NSEC3PARAM Flags", l}, "" } rr.Flags = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad NSEC3PARAM Iterations", l}, "" } rr.Iterations = uint16(i) - <-c - l = <-c + c.Next() + l, _ = c.Next() if l.token != "-" { rr.SaltLength = uint8(len(l.token)) rr.Salt = l.token @@ -1330,16 +1334,16 @@ func setNSEC3PARAM(h RR_Header, c chan lex, o, f string) (RR, *ParseError, strin return rr, nil, "" } -func setEUI48(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setEUI48(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(EUI48) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } - if l.length != 17 || l.err { + if len(l.token) != 17 || l.err { return nil, &ParseError{f, "bad EUI48 Address", l}, "" } addr := make([]byte, 12) @@ -1363,16 +1367,16 @@ func setEUI48(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setEUI64(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setEUI64(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(EUI64) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } - if l.length != 23 || l.err { + if len(l.token) != 23 || l.err { return nil, &ParseError{f, "bad EUI64 Address", l}, "" } addr := make([]byte, 16) @@ -1396,12 +1400,12 @@ func setEUI64(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setSSHFP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSSHFP(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(SSHFP) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1410,14 +1414,14 @@ func setSSHFP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad SSHFP Algorithm", l}, "" } rr.Algorithm = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad SSHFP Type", l}, "" } rr.Type = uint8(i) - <-c // zBlank + c.Next() // zBlank s, e1, c1 := endingToString(c, "bad SSHFP Fingerprint", f) if e1 != nil { return nil, e1, c1 @@ -1426,12 +1430,12 @@ func setSSHFP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setDNSKEYs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, string) { +func setDNSKEYs(h RR_Header, c *zlexer, o, f, typ string) (RR, *ParseError, string) { rr := new(DNSKEY) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1440,15 +1444,15 @@ func setDNSKEYs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, str return nil, &ParseError{f, "bad " + typ + " Flags", l}, "" } rr.Flags = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad " + typ + " Protocol", l}, "" } rr.Protocol = uint8(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad " + typ + " Algorithm", l}, "" @@ -1462,7 +1466,7 @@ func setDNSKEYs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, str return rr, nil, c1 } -func setKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDNSKEYs(h, c, o, f, "KEY") if r != nil { return &KEY{*r.(*DNSKEY)}, e, s @@ -1470,12 +1474,12 @@ func setKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, e, s } -func setDNSKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setDNSKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDNSKEYs(h, c, o, f, "DNSKEY") return r, e, s } -func setCDNSKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCDNSKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDNSKEYs(h, c, o, f, "CDNSKEY") if r != nil { return &CDNSKEY{*r.(*DNSKEY)}, e, s @@ -1483,12 +1487,12 @@ func setCDNSKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) return nil, e, s } -func setRKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(RKEY) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1497,15 +1501,15 @@ func setRKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad RKEY Flags", l}, "" } rr.Flags = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad RKEY Protocol", l}, "" } rr.Protocol = uint8(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad RKEY Algorithm", l}, "" @@ -1519,7 +1523,7 @@ func setRKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setEID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setEID(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(EID) rr.Hdr = h s, e, c1 := endingToString(c, "bad EID Endpoint", f) @@ -1530,7 +1534,7 @@ func setEID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setNIMLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNIMLOC(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NIMLOC) rr.Hdr = h s, e, c1 := endingToString(c, "bad NIMLOC Locator", f) @@ -1541,12 +1545,12 @@ func setNIMLOC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setGPOS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setGPOS(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(GPOS) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1555,15 +1559,15 @@ func setGPOS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad GPOS Longitude", l}, "" } rr.Longitude = l.token - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() _, e = strconv.ParseFloat(l.token, 64) if e != nil || l.err { return nil, &ParseError{f, "bad GPOS Latitude", l}, "" } rr.Latitude = l.token - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() _, e = strconv.ParseFloat(l.token, 64) if e != nil || l.err { return nil, &ParseError{f, "bad GPOS Altitude", l}, "" @@ -1572,12 +1576,12 @@ func setGPOS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setDSs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, string) { +func setDSs(h RR_Header, c *zlexer, o, f, typ string) (RR, *ParseError, string) { rr := new(DS) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1586,10 +1590,11 @@ func setDSs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, string) return nil, &ParseError{f, "bad " + typ + " KeyTag", l}, "" } rr.KeyTag = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, e = strconv.ParseUint(l.token, 10, 8); e != nil { - i, ok := StringToAlgorithm[l.tokenUpper] + tokenUpper := strings.ToUpper(l.token) + i, ok := StringToAlgorithm[tokenUpper] if !ok || l.err { return nil, &ParseError{f, "bad " + typ + " Algorithm", l}, "" } @@ -1597,8 +1602,8 @@ func setDSs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, string) } else { rr.Algorithm = uint8(i) } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad " + typ + " DigestType", l}, "" @@ -1612,12 +1617,12 @@ func setDSs(h RR_Header, c chan lex, o, f, typ string) (RR, *ParseError, string) return rr, nil, c1 } -func setDS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setDS(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDSs(h, c, o, f, "DS") return r, e, s } -func setDLV(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setDLV(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDSs(h, c, o, f, "DLV") if r != nil { return &DLV{*r.(*DS)}, e, s @@ -1625,7 +1630,7 @@ func setDLV(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, e, s } -func setCDS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCDS(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { r, e, s := setDSs(h, c, o, f, "CDS") if r != nil { return &CDS{*r.(*DS)}, e, s @@ -1633,12 +1638,12 @@ func setCDS(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, e, s } -func setTA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setTA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(TA) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1647,10 +1652,11 @@ func setTA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad TA KeyTag", l}, "" } rr.KeyTag = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if i, e := strconv.ParseUint(l.token, 10, 8); e != nil { - i, ok := StringToAlgorithm[l.tokenUpper] + tokenUpper := strings.ToUpper(l.token) + i, ok := StringToAlgorithm[tokenUpper] if !ok || l.err { return nil, &ParseError{f, "bad TA Algorithm", l}, "" } @@ -1658,27 +1664,27 @@ func setTA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } else { rr.Algorithm = uint8(i) } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad TA DigestType", l}, "" } rr.DigestType = uint8(i) - s, e, c1 := endingToString(c, "bad TA Digest", f) - if e != nil { - return nil, e.(*ParseError), c1 + s, err, c1 := endingToString(c, "bad TA Digest", f) + if err != nil { + return nil, err, c1 } rr.Digest = s return rr, nil, c1 } -func setTLSA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setTLSA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(TLSA) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1687,15 +1693,15 @@ func setTLSA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad TLSA Usage", l}, "" } rr.Usage = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad TLSA Selector", l}, "" } rr.Selector = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad TLSA MatchingType", l}, "" @@ -1710,12 +1716,12 @@ func setTLSA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setSMIMEA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSMIMEA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(SMIMEA) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -1724,15 +1730,15 @@ func setSMIMEA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad SMIMEA Usage", l}, "" } rr.Usage = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad SMIMEA Selector", l}, "" } rr.Selector = uint8(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 8) if e != nil || l.err { return nil, &ParseError{f, "bad SMIMEA MatchingType", l}, "" @@ -1747,17 +1753,17 @@ func setSMIMEA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setRFC3597(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setRFC3597(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(RFC3597) rr.Hdr = h - l := <-c + l, _ := c.Next() if l.token != "\\#" { return nil, &ParseError{f, "bad RFC3597 Rdata", l}, "" } - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() rdlength, e := strconv.Atoi(l.token) if e != nil || l.err { return nil, &ParseError{f, "bad RFC3597 Rdata ", l}, "" @@ -1774,7 +1780,7 @@ func setRFC3597(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) return rr, nil, c1 } -func setSPF(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setSPF(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(SPF) rr.Hdr = h @@ -1786,7 +1792,7 @@ func setSPF(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setAVC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setAVC(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(AVC) rr.Hdr = h @@ -1798,7 +1804,7 @@ func setAVC(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setTXT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setTXT(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(TXT) rr.Hdr = h @@ -1812,7 +1818,7 @@ func setTXT(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } // identical to setTXT -func setNINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNINFO(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NINFO) rr.Hdr = h @@ -1824,12 +1830,12 @@ func setNINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setURI(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setURI(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(URI) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1838,15 +1844,15 @@ func setURI(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad URI Priority", l}, "" } rr.Priority = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() i, e = strconv.ParseUint(l.token, 10, 16) if e != nil || l.err { return nil, &ParseError{f, "bad URI Weight", l}, "" } rr.Weight = uint16(i) - <-c // zBlank + c.Next() // zBlank s, err, c1 := endingToTxtSlice(c, "bad URI Target", f) if err != nil { return nil, err, "" @@ -1858,7 +1864,7 @@ func setURI(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setDHCID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setDHCID(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { // awesome record to parse! rr := new(DHCID) rr.Hdr = h @@ -1871,12 +1877,12 @@ func setDHCID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setNID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setNID(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(NID) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1885,8 +1891,8 @@ func setNID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad NID Preference", l}, "" } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString u, err := stringToNodeID(l) if err != nil || l.err { return nil, err, "" @@ -1895,12 +1901,12 @@ func setNID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setL32(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setL32(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(L32) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1909,8 +1915,8 @@ func setL32(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad L32 Preference", l}, "" } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Locator32 = net.ParseIP(l.token) if rr.Locator32 == nil || l.err { return nil, &ParseError{f, "bad L32 Locator", l}, "" @@ -1918,12 +1924,12 @@ func setL32(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setLP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setLP(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(LP) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1933,8 +1939,8 @@ func setLP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Fqdn = l.token name, nameOk := toAbsoluteName(l.token, o) if l.err || !nameOk { @@ -1945,12 +1951,12 @@ func setLP(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setL64(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setL64(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(L64) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1959,8 +1965,8 @@ func setL64(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return nil, &ParseError{f, "bad L64 Preference", l}, "" } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString u, err := stringToNodeID(l) if err != nil || l.err { return nil, err, "" @@ -1969,12 +1975,12 @@ func setL64(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setUID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setUID(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(UID) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -1986,12 +1992,12 @@ func setUID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setGID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setGID(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(GID) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -2003,7 +2009,7 @@ func setGID(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setUINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setUINFO(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(UINFO) rr.Hdr = h @@ -2018,12 +2024,12 @@ func setUINFO(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setPX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setPX(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(PX) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, "" } @@ -2033,8 +2039,8 @@ func setPX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Preference = uint16(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Map822 = l.token map822, map822Ok := toAbsoluteName(l.token, o) if l.err || !map822Ok { @@ -2042,8 +2048,8 @@ func setPX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Map822 = map822 - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString rr.Mapx400 = l.token mapx400, mapx400Ok := toAbsoluteName(l.token, o) if l.err || !mapx400Ok { @@ -2054,12 +2060,12 @@ func setPX(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, "" } -func setCAA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setCAA(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(CAA) rr.Hdr = h - l := <-c - if l.length == 0 { // dynamic update rr. + l, _ := c.Next() + if len(l.token) == 0 { // dynamic update rr. return rr, nil, l.comment } @@ -2069,14 +2075,14 @@ func setCAA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { } rr.Flag = uint8(i) - <-c // zBlank - l = <-c // zString + c.Next() // zBlank + l, _ = c.Next() // zString if l.value != zString { return nil, &ParseError{f, "bad CAA Tag", l}, "" } rr.Tag = l.token - <-c // zBlank + c.Next() // zBlank s, e, c1 := endingToTxtSlice(c, "bad CAA Value", f) if e != nil { return nil, e, "" @@ -2088,43 +2094,43 @@ func setCAA(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { return rr, nil, c1 } -func setTKEY(h RR_Header, c chan lex, o, f string) (RR, *ParseError, string) { +func setTKEY(h RR_Header, c *zlexer, o, f string) (RR, *ParseError, string) { rr := new(TKEY) rr.Hdr = h - l := <-c + l, _ := c.Next() // Algorithm if l.value != zString { return nil, &ParseError{f, "bad TKEY algorithm", l}, "" } rr.Algorithm = l.token - <-c // zBlank + c.Next() // zBlank // Get the key length and key values - l = <-c + l, _ = c.Next() i, err := strconv.ParseUint(l.token, 10, 8) if err != nil || l.err { return nil, &ParseError{f, "bad TKEY key length", l}, "" } rr.KeySize = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if l.value != zString { return nil, &ParseError{f, "bad TKEY key", l}, "" } rr.Key = l.token - <-c // zBlank + c.Next() // zBlank // Get the otherdata length and string data - l = <-c + l, _ = c.Next() i, err = strconv.ParseUint(l.token, 10, 8) if err != nil || l.err { return nil, &ParseError{f, "bad TKEY otherdata length", l}, "" } rr.OtherLen = uint16(i) - <-c // zBlank - l = <-c + c.Next() // zBlank + l, _ = c.Next() if l.value != zString { return nil, &ParseError{f, "bad TKEY otherday", l}, "" } diff --git a/vendor/github.com/miekg/dns/scanner.go b/vendor/github.com/miekg/dns/scanner.go deleted file mode 100644 index 424e5af9..00000000 --- a/vendor/github.com/miekg/dns/scanner.go +++ /dev/null @@ -1,56 +0,0 @@ -package dns - -// Implement a simple scanner, return a byte stream from an io reader. - -import ( - "bufio" - "context" - "io" - "text/scanner" -) - -type scan struct { - src *bufio.Reader - position scanner.Position - eof bool // Have we just seen a eof - ctx context.Context -} - -func scanInit(r io.Reader) (*scan, context.CancelFunc) { - s := new(scan) - s.src = bufio.NewReader(r) - s.position.Line = 1 - - ctx, cancel := context.WithCancel(context.Background()) - s.ctx = ctx - - return s, cancel -} - -// tokenText returns the next byte from the input -func (s *scan) tokenText() (byte, error) { - c, err := s.src.ReadByte() - if err != nil { - return c, err - } - select { - case <-s.ctx.Done(): - return c, context.Canceled - default: - break - } - - // delay the newline handling until the next token is delivered, - // fixes off-by-one errors when reporting a parse error. - if s.eof == true { - s.position.Line++ - s.position.Column = 0 - s.eof = false - } - if c == '\n' { - s.eof = true - return c, nil - } - s.position.Column++ - return c, nil -} diff --git a/vendor/github.com/miekg/dns/serve_mux.go b/vendor/github.com/miekg/dns/serve_mux.go new file mode 100644 index 00000000..ae304db5 --- /dev/null +++ b/vendor/github.com/miekg/dns/serve_mux.go @@ -0,0 +1,147 @@ +package dns + +import ( + "strings" + "sync" +) + +// ServeMux is an DNS request multiplexer. It matches the zone name of +// each incoming request against a list of registered patterns add calls +// the handler for the pattern that most closely matches the zone name. +// +// ServeMux is DNSSEC aware, meaning that queries for the DS record are +// redirected to the parent zone (if that is also registered), otherwise +// the child gets the query. +// +// ServeMux is also safe for concurrent access from multiple goroutines. +// +// The zero ServeMux is empty and ready for use. +type ServeMux struct { + z map[string]Handler + m sync.RWMutex +} + +// NewServeMux allocates and returns a new ServeMux. +func NewServeMux() *ServeMux { + return new(ServeMux) +} + +// DefaultServeMux is the default ServeMux used by Serve. +var DefaultServeMux = NewServeMux() + +func (mux *ServeMux) match(q string, t uint16) Handler { + mux.m.RLock() + defer mux.m.RUnlock() + if mux.z == nil { + return nil + } + + var handler Handler + + // TODO(tmthrgd): Once https://go-review.googlesource.com/c/go/+/137575 + // lands in a go release, replace the following with strings.ToLower. + var sb strings.Builder + for i := 0; i < len(q); i++ { + c := q[i] + if !(c >= 'A' && c <= 'Z') { + continue + } + + sb.Grow(len(q)) + sb.WriteString(q[:i]) + + for ; i < len(q); i++ { + c := q[i] + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + + sb.WriteByte(c) + } + + q = sb.String() + break + } + + for off, end := 0, false; !end; off, end = NextLabel(q, off) { + if h, ok := mux.z[q[off:]]; ok { + if t != TypeDS { + return h + } + // Continue for DS to see if we have a parent too, if so delegate to the parent + handler = h + } + } + + // Wildcard match, if we have found nothing try the root zone as a last resort. + if h, ok := mux.z["."]; ok { + return h + } + + return handler +} + +// Handle adds a handler to the ServeMux for pattern. +func (mux *ServeMux) Handle(pattern string, handler Handler) { + if pattern == "" { + panic("dns: invalid pattern " + pattern) + } + mux.m.Lock() + if mux.z == nil { + mux.z = make(map[string]Handler) + } + mux.z[Fqdn(pattern)] = handler + mux.m.Unlock() +} + +// HandleFunc adds a handler function to the ServeMux for pattern. +func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Msg)) { + mux.Handle(pattern, HandlerFunc(handler)) +} + +// HandleRemove deregisters the handler specific for pattern from the ServeMux. +func (mux *ServeMux) HandleRemove(pattern string) { + if pattern == "" { + panic("dns: invalid pattern " + pattern) + } + mux.m.Lock() + delete(mux.z, Fqdn(pattern)) + mux.m.Unlock() +} + +// ServeDNS dispatches the request to the handler whose pattern most +// closely matches the request message. +// +// ServeDNS is DNSSEC aware, meaning that queries for the DS record +// are redirected to the parent zone (if that is also registered), +// otherwise the child gets the query. +// +// If no handler is found, or there is no question, a standard SERVFAIL +// message is returned +func (mux *ServeMux) ServeDNS(w ResponseWriter, req *Msg) { + var h Handler + if len(req.Question) >= 1 { // allow more than one question + h = mux.match(req.Question[0].Name, req.Question[0].Qtype) + } + + if h != nil { + h.ServeDNS(w, req) + } else { + HandleFailed(w, req) + } +} + +// Handle registers the handler with the given pattern +// in the DefaultServeMux. The documentation for +// ServeMux explains how patterns are matched. +func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } + +// HandleRemove deregisters the handle with the given pattern +// in the DefaultServeMux. +func HandleRemove(pattern string) { DefaultServeMux.HandleRemove(pattern) } + +// HandleFunc registers the handler function with the given pattern +// in the DefaultServeMux. +func HandleFunc(pattern string, handler func(ResponseWriter, *Msg)) { + DefaultServeMux.HandleFunc(pattern, handler) +} diff --git a/vendor/github.com/miekg/dns/server.go b/vendor/github.com/miekg/dns/server.go index 2d98f148..6abbed51 100644 --- a/vendor/github.com/miekg/dns/server.go +++ b/vendor/github.com/miekg/dns/server.go @@ -4,10 +4,13 @@ package dns import ( "bytes" + "context" "crypto/tls" "encoding/binary" + "errors" "io" "net" + "strings" "sync" "sync/atomic" "time" @@ -16,17 +19,39 @@ import ( // Default maximum number of TCP queries before we close the socket. const maxTCPQueries = 128 -// Interval for stop worker if no load +// The maximum number of idle workers. +// +// This controls the maximum number of workers that are allowed to stay +// idle waiting for incoming requests before being torn down. +// +// If this limit is reached, the server will just keep spawning new +// workers (goroutines) for each incoming request. In this case, each +// worker will only be used for a single request. +const maxIdleWorkersCount = 10000 + +// The maximum length of time a worker may idle for before being destroyed. const idleWorkerTimeout = 10 * time.Second -// Maximum number of workers -const maxWorkersCount = 10000 +// aLongTimeAgo is a non-zero time, far in the past, used for +// immediate cancelation of network operations. +var aLongTimeAgo = time.Unix(1, 0) // Handler is implemented by any value that implements ServeDNS. type Handler interface { ServeDNS(w ResponseWriter, r *Msg) } +// The HandlerFunc type is an adapter to allow the use of +// ordinary functions as DNS handlers. If f is a function +// with the appropriate signature, HandlerFunc(f) is a +// Handler object that calls f. +type HandlerFunc func(ResponseWriter, *Msg) + +// ServeDNS calls f(w, r). +func (f HandlerFunc) ServeDNS(w ResponseWriter, r *Msg) { + f(w, r) +} + // A ResponseWriter interface is used by an DNS handler to // construct an DNS response. type ResponseWriter interface { @@ -49,46 +74,25 @@ type ResponseWriter interface { Hijack() } +// A ConnectionStater interface is used by a DNS Handler to access TLS connection state +// when available. +type ConnectionStater interface { + ConnectionState() *tls.ConnectionState +} + type response struct { msg []byte + closed bool // connection has been closed hijacked bool // connection has been hijacked by handler - tsigStatus error tsigTimersOnly bool + tsigStatus error tsigRequestMAC string tsigSecret map[string]string // the tsig secrets udp *net.UDPConn // i/o connection if UDP was used tcp net.Conn // i/o connection if TCP was used udpSession *SessionUDP // oob data to get egress interface right writer Writer // writer to output the raw DNS bits -} - -// ServeMux is an DNS request multiplexer. It matches the -// zone name of each incoming request against a list of -// registered patterns add calls the handler for the pattern -// that most closely matches the zone name. ServeMux is DNSSEC aware, meaning -// that queries for the DS record are redirected to the parent zone (if that -// is also registered), otherwise the child gets the query. -// ServeMux is also safe for concurrent access from multiple goroutines. -type ServeMux struct { - z map[string]Handler - m *sync.RWMutex -} - -// NewServeMux allocates and returns a new ServeMux. -func NewServeMux() *ServeMux { return &ServeMux{z: make(map[string]Handler), m: new(sync.RWMutex)} } - -// DefaultServeMux is the default ServeMux used by Serve. -var DefaultServeMux = NewServeMux() - -// The HandlerFunc type is an adapter to allow the use of -// ordinary functions as DNS handlers. If f is a function -// with the appropriate signature, HandlerFunc(f) is a -// Handler object that calls f. -type HandlerFunc func(ResponseWriter, *Msg) - -// ServeDNS calls f(w, r). -func (f HandlerFunc) ServeDNS(w ResponseWriter, r *Msg) { - f(w, r) + wg *sync.WaitGroup // for gracefull shutdown } // HandleFailed returns a HandlerFunc that returns SERVFAIL for every request it gets. @@ -99,8 +103,6 @@ func HandleFailed(w ResponseWriter, r *Msg) { w.WriteMsg(m) } -func failedHandler() Handler { return HandlerFunc(HandleFailed) } - // ListenAndServe Starts a server on address and network specified Invoke handler // for incoming queries. func ListenAndServe(addr string, network string, handler Handler) error { @@ -139,99 +141,6 @@ func ActivateAndServe(l net.Listener, p net.PacketConn, handler Handler) error { return server.ActivateAndServe() } -func (mux *ServeMux) match(q string, t uint16) Handler { - mux.m.RLock() - defer mux.m.RUnlock() - var handler Handler - b := make([]byte, len(q)) // worst case, one label of length q - off := 0 - end := false - for { - l := len(q[off:]) - for i := 0; i < l; i++ { - b[i] = q[off+i] - if b[i] >= 'A' && b[i] <= 'Z' { - b[i] |= ('a' - 'A') - } - } - if h, ok := mux.z[string(b[:l])]; ok { // causes garbage, might want to change the map key - if t != TypeDS { - return h - } - // Continue for DS to see if we have a parent too, if so delegeate to the parent - handler = h - } - off, end = NextLabel(q, off) - if end { - break - } - } - // Wildcard match, if we have found nothing try the root zone as a last resort. - if h, ok := mux.z["."]; ok { - return h - } - return handler -} - -// Handle adds a handler to the ServeMux for pattern. -func (mux *ServeMux) Handle(pattern string, handler Handler) { - if pattern == "" { - panic("dns: invalid pattern " + pattern) - } - mux.m.Lock() - mux.z[Fqdn(pattern)] = handler - mux.m.Unlock() -} - -// HandleFunc adds a handler function to the ServeMux for pattern. -func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Msg)) { - mux.Handle(pattern, HandlerFunc(handler)) -} - -// HandleRemove deregistrars the handler specific for pattern from the ServeMux. -func (mux *ServeMux) HandleRemove(pattern string) { - if pattern == "" { - panic("dns: invalid pattern " + pattern) - } - mux.m.Lock() - delete(mux.z, Fqdn(pattern)) - mux.m.Unlock() -} - -// ServeDNS dispatches the request to the handler whose -// pattern most closely matches the request message. If DefaultServeMux -// is used the correct thing for DS queries is done: a possible parent -// is sought. -// If no handler is found a standard SERVFAIL message is returned -// If the request message does not have exactly one question in the -// question section a SERVFAIL is returned, unlesss Unsafe is true. -func (mux *ServeMux) ServeDNS(w ResponseWriter, request *Msg) { - var h Handler - if len(request.Question) < 1 { // allow more than one question - h = failedHandler() - } else { - if h = mux.match(request.Question[0].Name, request.Question[0].Qtype); h == nil { - h = failedHandler() - } - } - h.ServeDNS(w, request) -} - -// Handle registers the handler with the given pattern -// in the DefaultServeMux. The documentation for -// ServeMux explains how patterns are matched. -func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } - -// HandleRemove deregisters the handle with the given pattern -// in the DefaultServeMux. -func HandleRemove(pattern string) { DefaultServeMux.HandleRemove(pattern) } - -// HandleFunc registers the handler function with the given pattern -// in the DefaultServeMux. -func HandleFunc(pattern string, handler func(ResponseWriter, *Msg)) { - DefaultServeMux.HandleFunc(pattern, handler) -} - // Writer writes raw DNS messages; each call to Write should send an entire message. type Writer interface { io.Writer @@ -294,9 +203,6 @@ type Server struct { IdleTimeout func() time.Duration // Secret(s) for Tsig map[]. The zonename must be in canonical form (lowercase, fqdn, see RFC 4034 Section 6.2). TsigSecret map[string]string - // Unsafe instructs the server to disregard any sanity checks and directly hand the message to - // the handler. It will specifically not check if the query has the QR bit not set. - Unsafe bool // If NotifyStartedFunc is set it is called once the server has started listening. NotifyStartedFunc func() // DecorateReader is optional, allows customization of the process that reads raw DNS messages. @@ -305,14 +211,33 @@ type Server struct { DecorateWriter DecorateWriter // Maximum number of TCP queries before we close the socket. Default is maxTCPQueries (unlimited if -1). MaxTCPQueries int + // Whether to set the SO_REUSEPORT socket option, allowing multiple listeners to be bound to a single address. + // It is only supported on go1.11+ and when using ListenAndServe. + ReusePort bool + // AcceptMsgFunc will check the incoming message and will reject it early in the process. + // By default DefaultMsgAcceptFunc will be used. + MsgAcceptFunc MsgAcceptFunc // UDP packet or TCP connection queue queue chan *response // Workers count workersCount int32 + // Shutdown handling - lock sync.RWMutex - started bool + lock sync.RWMutex + started bool + shutdown chan struct{} + conns map[net.Conn]struct{} + + // A pool for UDP message buffers. + udpPool sync.Pool +} + +func (srv *Server) isStarted() bool { + srv.lock.RLock() + started := srv.started + srv.lock.RUnlock() + return started } func (srv *Server) worker(w *response) { @@ -320,7 +245,7 @@ func (srv *Server) worker(w *response) { for { count := atomic.LoadInt32(&srv.workersCount) - if count > maxWorkersCount { + if count > maxIdleWorkersCount { return } if atomic.CompareAndSwapInt32(&srv.workersCount, count, count+1) { @@ -360,10 +285,39 @@ func (srv *Server) spawnWorker(w *response) { } } +func makeUDPBuffer(size int) func() interface{} { + return func() interface{} { + return make([]byte, size) + } +} + +func (srv *Server) init() { + srv.queue = make(chan *response) + + srv.shutdown = make(chan struct{}) + srv.conns = make(map[net.Conn]struct{}) + + if srv.UDPSize == 0 { + srv.UDPSize = MinMsgSize + } + if srv.MsgAcceptFunc == nil { + srv.MsgAcceptFunc = defaultMsgAcceptFunc + } + + srv.udpPool.New = makeUDPBuffer(srv.UDPSize) +} + +func unlockOnce(l sync.Locker) func() { + var once sync.Once + return func() { once.Do(l.Unlock) } +} + // ListenAndServe starts a nameserver on the configured address in *Server. func (srv *Server) ListenAndServe() error { + unlock := unlockOnce(&srv.lock) srv.lock.Lock() - defer srv.lock.Unlock() + defer unlock() + if srv.started { return &Error{err: "server already started"} } @@ -372,63 +326,47 @@ func (srv *Server) ListenAndServe() error { if addr == "" { addr = ":domain" } - if srv.UDPSize == 0 { - srv.UDPSize = MinMsgSize - } - srv.queue = make(chan *response) + + srv.init() defer close(srv.queue) + switch srv.Net { case "tcp", "tcp4", "tcp6": - a, err := net.ResolveTCPAddr(srv.Net, addr) - if err != nil { - return err - } - l, err := net.ListenTCP(srv.Net, a) + l, err := listenTCP(srv.Net, addr, srv.ReusePort) if err != nil { return err } srv.Listener = l srv.started = true - srv.lock.Unlock() - err = srv.serveTCP(l) - srv.lock.Lock() // to satisfy the defer at the top - return err + unlock() + return srv.serveTCP(l) case "tcp-tls", "tcp4-tls", "tcp6-tls": - network := "tcp" - if srv.Net == "tcp4-tls" { - network = "tcp4" - } else if srv.Net == "tcp6-tls" { - network = "tcp6" + if srv.TLSConfig == nil || (len(srv.TLSConfig.Certificates) == 0 && srv.TLSConfig.GetCertificate == nil) { + return errors.New("dns: neither Certificates nor GetCertificate set in Config") } - - l, err := tls.Listen(network, addr, srv.TLSConfig) + network := strings.TrimSuffix(srv.Net, "-tls") + l, err := listenTCP(network, addr, srv.ReusePort) if err != nil { return err } + l = tls.NewListener(l, srv.TLSConfig) srv.Listener = l srv.started = true - srv.lock.Unlock() - err = srv.serveTCP(l) - srv.lock.Lock() // to satisfy the defer at the top - return err + unlock() + return srv.serveTCP(l) case "udp", "udp4", "udp6": - a, err := net.ResolveUDPAddr(srv.Net, addr) + l, err := listenUDP(srv.Net, addr, srv.ReusePort) if err != nil { return err } - l, err := net.ListenUDP(srv.Net, a) - if err != nil { - return err - } - if e := setUDPSocketOptions(l); e != nil { + u := l.(*net.UDPConn) + if e := setUDPSocketOptions(u); e != nil { return e } srv.PacketConn = l srv.started = true - srv.lock.Unlock() - err = srv.serveUDP(l) - srv.lock.Lock() // to satisfy the defer at the top - return err + unlock() + return srv.serveUDP(u) } return &Error{err: "bad network"} } @@ -436,20 +374,20 @@ func (srv *Server) ListenAndServe() error { // ActivateAndServe starts a nameserver with the PacketConn or Listener // configured in *Server. Its main use is to start a server from systemd. func (srv *Server) ActivateAndServe() error { + unlock := unlockOnce(&srv.lock) srv.lock.Lock() - defer srv.lock.Unlock() + defer unlock() + if srv.started { return &Error{err: "server already started"} } + srv.init() + defer close(srv.queue) + pConn := srv.PacketConn l := srv.Listener - srv.queue = make(chan *response) - defer close(srv.queue) if pConn != nil { - if srv.UDPSize == 0 { - srv.UDPSize = MinMsgSize - } // Check PacketConn interface's type is valid and value // is not nil if t, ok := pConn.(*net.UDPConn); ok && t != nil { @@ -457,18 +395,14 @@ func (srv *Server) ActivateAndServe() error { return e } srv.started = true - srv.lock.Unlock() - e := srv.serveUDP(t) - srv.lock.Lock() // to satisfy the defer at the top - return e + unlock() + return srv.serveUDP(t) } } if l != nil { srv.started = true - srv.lock.Unlock() - e := srv.serveTCP(l) - srv.lock.Lock() // to satisfy the defer at the top - return e + unlock() + return srv.serveTCP(l) } return &Error{err: "bad listeners"} } @@ -476,23 +410,57 @@ func (srv *Server) ActivateAndServe() error { // Shutdown shuts down a server. After a call to Shutdown, ListenAndServe and // ActivateAndServe will return. func (srv *Server) Shutdown() error { + return srv.ShutdownContext(context.Background()) +} + +// ShutdownContext shuts down a server. After a call to ShutdownContext, +// ListenAndServe and ActivateAndServe will return. +// +// A context.Context may be passed to limit how long to wait for connections +// to terminate. +func (srv *Server) ShutdownContext(ctx context.Context) error { srv.lock.Lock() if !srv.started { srv.lock.Unlock() return &Error{err: "server not started"} } + srv.started = false + + if srv.PacketConn != nil { + srv.PacketConn.SetReadDeadline(aLongTimeAgo) // Unblock reads + } + + if srv.Listener != nil { + srv.Listener.Close() + } + + for rw := range srv.conns { + rw.SetReadDeadline(aLongTimeAgo) // Unblock reads + } + srv.lock.Unlock() + if testShutdownNotify != nil { + testShutdownNotify.Broadcast() + } + + var ctxErr error + select { + case <-srv.shutdown: + case <-ctx.Done(): + ctxErr = ctx.Err() + } + if srv.PacketConn != nil { srv.PacketConn.Close() } - if srv.Listener != nil { - srv.Listener.Close() - } - return nil + + return ctxErr } +var testShutdownNotify *sync.Cond + // getReadTimeout is a helper func to use system timeout if server did not intend to change it. func (srv *Server) getReadTimeout() time.Duration { rtimeout := dnsTimeout @@ -510,22 +478,36 @@ func (srv *Server) serveTCP(l net.Listener) error { srv.NotifyStartedFunc() } - for { + var wg sync.WaitGroup + defer func() { + wg.Wait() + close(srv.shutdown) + }() + + for srv.isStarted() { rw, err := l.Accept() - srv.lock.RLock() - if !srv.started { - srv.lock.RUnlock() - return nil - } - srv.lock.RUnlock() if err != nil { + if !srv.isStarted() { + return nil + } if neterr, ok := err.(net.Error); ok && neterr.Temporary() { continue } return err } - srv.spawnWorker(&response{tsigSecret: srv.TsigSecret, tcp: rw}) + srv.lock.Lock() + // Track the connection to allow unblocking reads on shutdown. + srv.conns[rw] = struct{}{} + srv.lock.Unlock() + wg.Add(1) + srv.spawnWorker(&response{ + tsigSecret: srv.TsigSecret, + tcp: rw, + wg: &wg, + }) } + + return nil } // serveUDP starts a UDP listener for the server. @@ -541,27 +523,42 @@ func (srv *Server) serveUDP(l *net.UDPConn) error { reader = srv.DecorateReader(reader) } + var wg sync.WaitGroup + defer func() { + wg.Wait() + close(srv.shutdown) + }() + rtimeout := srv.getReadTimeout() // deadline is not used here - for { + for srv.isStarted() { m, s, err := reader.ReadUDP(l, rtimeout) - srv.lock.RLock() - if !srv.started { - srv.lock.RUnlock() - return nil - } - srv.lock.RUnlock() if err != nil { + if !srv.isStarted() { + return nil + } if netErr, ok := err.(net.Error); ok && netErr.Temporary() { continue } return err } if len(m) < headerSize { + if cap(m) == srv.UDPSize { + srv.udpPool.Put(m[:srv.UDPSize]) + } continue } - srv.spawnWorker(&response{msg: m, tsigSecret: srv.TsigSecret, udp: l, udpSession: s}) + wg.Add(1) + srv.spawnWorker(&response{ + msg: m, + tsigSecret: srv.TsigSecret, + udp: l, + udpSession: s, + wg: &wg, + }) } + + return nil } func (srv *Server) serve(w *response) { @@ -574,20 +571,28 @@ func (srv *Server) serve(w *response) { if w.udp != nil { // serve UDP srv.serveDNS(w) - return - } - reader := Reader(&defaultReader{srv}) - if srv.DecorateReader != nil { - reader = srv.DecorateReader(reader) + w.wg.Done() + return } defer func() { if !w.hijacked { w.Close() } + + srv.lock.Lock() + delete(srv.conns, w.tcp) + srv.lock.Unlock() + + w.wg.Done() }() + reader := Reader(&defaultReader{srv}) + if srv.DecorateReader != nil { + reader = srv.DecorateReader(reader) + } + idleTimeout := tcpIdleTimeout if srv.IdleTimeout != nil { idleTimeout = srv.IdleTimeout() @@ -600,7 +605,7 @@ func (srv *Server) serve(w *response) { limit = maxTCPQueries } - for q := 0; q < limit || limit == -1; q++ { + for q := 0; (q < limit || limit == -1) && srv.isStarted(); q++ { var err error w.msg, err = reader.ReadTCP(w.tcp, timeout) if err != nil { @@ -620,16 +625,43 @@ func (srv *Server) serve(w *response) { } } +func (srv *Server) disposeBuffer(w *response) { + if w.udp != nil && cap(w.msg) == srv.UDPSize { + srv.udpPool.Put(w.msg[:srv.UDPSize]) + } + w.msg = nil +} + func (srv *Server) serveDNS(w *response) { - req := new(Msg) - err := req.Unpack(w.msg) - if err != nil { // Send a FormatError back - x := new(Msg) - x.SetRcodeFormatError(req) - w.WriteMsg(x) + dh, off, err := unpackMsgHdr(w.msg, 0) + if err != nil { + // Let client hang, they are sending crap; any reply can be used to amplify. return } - if !srv.Unsafe && req.Response { + + req := new(Msg) + req.setHdr(dh) + + switch srv.MsgAcceptFunc(dh) { + case MsgAccept: + case MsgIgnore: + return + case MsgReject: + req.SetRcodeFormatError(req) + // Are we allowed to delete any OPT records here? + req.Ns, req.Answer, req.Extra = nil, nil, nil + + w.WriteMsg(req) + srv.disposeBuffer(w) + return + } + + if err := req.unpack(dh, w.msg, off); err != nil { + req.SetRcodeFormatError(req) + req.Ns, req.Answer, req.Extra = nil, nil, nil + + w.WriteMsg(req) + srv.disposeBuffer(w) return } @@ -646,6 +678,8 @@ func (srv *Server) serveDNS(w *response) { } } + srv.disposeBuffer(w) + handler := srv.Handler if handler == nil { handler = DefaultServeMux @@ -655,7 +689,16 @@ func (srv *Server) serveDNS(w *response) { } func (srv *Server) readTCP(conn net.Conn, timeout time.Duration) ([]byte, error) { - conn.SetReadDeadline(time.Now().Add(timeout)) + // If we race with ShutdownContext, the read deadline may + // have been set in the distant past to unblock the read + // below. We must not override it, otherwise we may block + // ShutdownContext. + srv.lock.RLock() + if srv.started { + conn.SetReadDeadline(time.Now().Add(timeout)) + } + srv.lock.RUnlock() + l := make([]byte, 2) n, err := conn.Read(l) if err != nil || n != 2 { @@ -690,10 +733,17 @@ func (srv *Server) readTCP(conn net.Conn, timeout time.Duration) ([]byte, error) } func (srv *Server) readUDP(conn *net.UDPConn, timeout time.Duration) ([]byte, *SessionUDP, error) { - conn.SetReadDeadline(time.Now().Add(timeout)) - m := make([]byte, srv.UDPSize) + srv.lock.RLock() + if srv.started { + // See the comment in readTCP above. + conn.SetReadDeadline(time.Now().Add(timeout)) + } + srv.lock.RUnlock() + + m := srv.udpPool.Get().([]byte) n, s, err := ReadFromSessionUDP(conn, m) if err != nil { + srv.udpPool.Put(m) return nil, nil, err } m = m[:n] @@ -702,6 +752,10 @@ func (srv *Server) readUDP(conn *net.UDPConn, timeout time.Duration) ([]byte, *S // WriteMsg implements the ResponseWriter.WriteMsg method. func (w *response) WriteMsg(m *Msg) (err error) { + if w.closed { + return &Error{err: "WriteMsg called after Close"} + } + var data []byte if w.tsigSecret != nil { // if no secrets, dont check for the tsig (which is a longer check) if t := m.IsTsig(); t != nil { @@ -723,6 +777,10 @@ func (w *response) WriteMsg(m *Msg) (err error) { // Write implements the ResponseWriter.Write method. func (w *response) Write(m []byte) (int, error) { + if w.closed { + return 0, &Error{err: "Write called after Close"} + } + switch { case w.udp != nil: n, err := WriteToSessionUDP(w.udp, m, w.udpSession) @@ -741,24 +799,33 @@ func (w *response) Write(m []byte) (int, error) { n, err := io.Copy(w.tcp, bytes.NewReader(m)) return int(n), err + default: + panic("dns: internal error: udp and tcp both nil") } - panic("not reached") } // LocalAddr implements the ResponseWriter.LocalAddr method. func (w *response) LocalAddr() net.Addr { - if w.tcp != nil { + switch { + case w.udp != nil: + return w.udp.LocalAddr() + case w.tcp != nil: return w.tcp.LocalAddr() + default: + panic("dns: internal error: udp and tcp both nil") } - return w.udp.LocalAddr() } // RemoteAddr implements the ResponseWriter.RemoteAddr method. func (w *response) RemoteAddr() net.Addr { - if w.tcp != nil { + switch { + case w.udpSession != nil: + return w.udpSession.RemoteAddr() + case w.tcp != nil: return w.tcp.RemoteAddr() + default: + panic("dns: internal error: udpSession and tcp both nil") } - return w.udpSession.RemoteAddr() } // TsigStatus implements the ResponseWriter.TsigStatus method. @@ -772,11 +839,30 @@ func (w *response) Hijack() { w.hijacked = true } // Close implements the ResponseWriter.Close method func (w *response) Close() error { - // Can't close the udp conn, as that is actually the listener. - if w.tcp != nil { - e := w.tcp.Close() - w.tcp = nil - return e + if w.closed { + return &Error{err: "connection already closed"} + } + w.closed = true + + switch { + case w.udp != nil: + // Can't close the udp conn, as that is actually the listener. + return nil + case w.tcp != nil: + return w.tcp.Close() + default: + panic("dns: internal error: udp and tcp both nil") + } +} + +// ConnectionState() implements the ConnectionStater.ConnectionState() interface. +func (w *response) ConnectionState() *tls.ConnectionState { + type tlsConnectionStater interface { + ConnectionState() tls.ConnectionState + } + if v, ok := w.tcp.(tlsConnectionStater); ok { + t := v.ConnectionState() + return &t } return nil } diff --git a/vendor/github.com/miekg/dns/sig0.go b/vendor/github.com/miekg/dns/sig0.go index f31e9e68..92b12b32 100644 --- a/vendor/github.com/miekg/dns/sig0.go +++ b/vendor/github.com/miekg/dns/sig0.go @@ -127,8 +127,7 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error { if offset+1 >= buflen { continue } - var rdlen uint16 - rdlen = binary.BigEndian.Uint16(buf[offset:]) + rdlen := binary.BigEndian.Uint16(buf[offset:]) offset += 2 offset += int(rdlen) } @@ -168,7 +167,7 @@ func (rr *SIG) Verify(k *KEY, buf []byte) error { } // If key has come from the DNS name compression might // have mangled the case of the name - if strings.ToLower(signername) != strings.ToLower(k.Header().Name) { + if !strings.EqualFold(signername, k.Header().Name) { return &Error{err: "signer name doesn't match key name"} } sigend := offset diff --git a/vendor/github.com/miekg/dns/types.go b/vendor/github.com/miekg/dns/types.go index a779ca8a..115f2c7b 100644 --- a/vendor/github.com/miekg/dns/types.go +++ b/vendor/github.com/miekg/dns/types.go @@ -330,7 +330,7 @@ func (rr *MX) String() string { type AFSDB struct { Hdr RR_Header Subtype uint16 - Hostname string `dns:"cdomain-name"` + Hostname string `dns:"domain-name"` } func (rr *AFSDB) String() string { @@ -419,128 +419,130 @@ type TXT struct { func (rr *TXT) String() string { return rr.Hdr.String() + sprintTxt(rr.Txt) } func sprintName(s string) string { - src := []byte(s) - dst := make([]byte, 0, len(src)) - for i := 0; i < len(src); { - if i+1 < len(src) && src[i] == '\\' && src[i+1] == '.' { - dst = append(dst, src[i:i+2]...) + var dst strings.Builder + dst.Grow(len(s)) + for i := 0; i < len(s); { + if i+1 < len(s) && s[i] == '\\' && s[i+1] == '.' { + dst.WriteString(s[i : i+2]) i += 2 - } else { - b, n := nextByte(src, i) - if n == 0 { - i++ // dangling back slash - } else if b == '.' { - dst = append(dst, b) - } else { - dst = appendDomainNameByte(dst, b) - } - i += n + continue } + + b, n := nextByte(s, i) + switch { + case n == 0: + i++ // dangling back slash + case b == '.': + dst.WriteByte('.') + default: + writeDomainNameByte(&dst, b) + } + i += n } - return string(dst) + return dst.String() } func sprintTxtOctet(s string) string { - src := []byte(s) - dst := make([]byte, 0, len(src)) - dst = append(dst, '"') - for i := 0; i < len(src); { - if i+1 < len(src) && src[i] == '\\' && src[i+1] == '.' { - dst = append(dst, src[i:i+2]...) + var dst strings.Builder + dst.Grow(2 + len(s)) + dst.WriteByte('"') + for i := 0; i < len(s); { + if i+1 < len(s) && s[i] == '\\' && s[i+1] == '.' { + dst.WriteString(s[i : i+2]) i += 2 - } else { - b, n := nextByte(src, i) - if n == 0 { - i++ // dangling back slash - } else if b == '.' { - dst = append(dst, b) - } else { - if b < ' ' || b > '~' { - dst = appendByte(dst, b) - } else { - dst = append(dst, b) - } - } - i += n + continue } + + b, n := nextByte(s, i) + switch { + case n == 0: + i++ // dangling back slash + case b == '.': + dst.WriteByte('.') + case b < ' ' || b > '~': + writeEscapedByte(&dst, b) + default: + dst.WriteByte(b) + } + i += n } - dst = append(dst, '"') - return string(dst) + dst.WriteByte('"') + return dst.String() } func sprintTxt(txt []string) string { - var out []byte + var out strings.Builder for i, s := range txt { + out.Grow(3 + len(s)) if i > 0 { - out = append(out, ` "`...) + out.WriteString(` "`) } else { - out = append(out, '"') + out.WriteByte('"') } - bs := []byte(s) - for j := 0; j < len(bs); { - b, n := nextByte(bs, j) + for j := 0; j < len(s); { + b, n := nextByte(s, j) if n == 0 { break } - out = appendTXTStringByte(out, b) + writeTXTStringByte(&out, b) j += n } - out = append(out, '"') + out.WriteByte('"') } - return string(out) + return out.String() } -func appendDomainNameByte(s []byte, b byte) []byte { +func writeDomainNameByte(s *strings.Builder, b byte) { switch b { case '.', ' ', '\'', '@', ';', '(', ')': // additional chars to escape - return append(s, '\\', b) + s.WriteByte('\\') + s.WriteByte(b) + default: + writeTXTStringByte(s, b) } - return appendTXTStringByte(s, b) } -func appendTXTStringByte(s []byte, b byte) []byte { - switch b { - case '"', '\\': - return append(s, '\\', b) +func writeTXTStringByte(s *strings.Builder, b byte) { + switch { + case b == '"' || b == '\\': + s.WriteByte('\\') + s.WriteByte(b) + case b < ' ' || b > '~': + writeEscapedByte(s, b) + default: + s.WriteByte(b) } - if b < ' ' || b > '~' { - return appendByte(s, b) - } - return append(s, b) } -func appendByte(s []byte, b byte) []byte { +func writeEscapedByte(s *strings.Builder, b byte) { var buf [3]byte bufs := strconv.AppendInt(buf[:0], int64(b), 10) - s = append(s, '\\') - for i := 0; i < 3-len(bufs); i++ { - s = append(s, '0') + s.WriteByte('\\') + for i := len(bufs); i < 3; i++ { + s.WriteByte('0') } - for _, r := range bufs { - s = append(s, r) - } - return s + s.Write(bufs) } -func nextByte(b []byte, offset int) (byte, int) { - if offset >= len(b) { +func nextByte(s string, offset int) (byte, int) { + if offset >= len(s) { return 0, 0 } - if b[offset] != '\\' { + if s[offset] != '\\' { // not an escape sequence - return b[offset], 1 + return s[offset], 1 } - switch len(b) - offset { + switch len(s) - offset { case 1: // dangling escape return 0, 0 case 2, 3: // too short to be \ddd default: // maybe \ddd - if isDigit(b[offset+1]) && isDigit(b[offset+2]) && isDigit(b[offset+3]) { - return dddToByte(b[offset+1:]), 4 + if isDigit(s[offset+1]) && isDigit(s[offset+2]) && isDigit(s[offset+3]) { + return dddStringToByte(s[offset+1:]), 4 } } // not \ddd, just an RFC 1035 "quoted" character - return b[offset+1], 2 + return s[offset+1], 2 } // SPF RR. See RFC 4408, Section 3.1.1. @@ -728,7 +730,7 @@ func (rr *LOC) String() string { lat = lat % LOC_DEGREES m := lat / LOC_HOURS lat = lat % LOC_HOURS - s += fmt.Sprintf("%02d %02d %0.3f %s ", h, m, (float64(lat) / 1000), ns) + s += fmt.Sprintf("%02d %02d %0.3f %s ", h, m, float64(lat)/1000, ns) lon := rr.Longitude ew := "E" @@ -742,7 +744,7 @@ func (rr *LOC) String() string { lon = lon % LOC_DEGREES m = lon / LOC_HOURS lon = lon % LOC_HOURS - s += fmt.Sprintf("%02d %02d %0.3f %s ", h, m, (float64(lon) / 1000), ew) + s += fmt.Sprintf("%02d %02d %0.3f %s ", h, m, float64(lon)/1000, ew) var alt = float64(rr.Altitude) / 100 alt -= LOC_ALTITUDEBASE @@ -752,9 +754,9 @@ func (rr *LOC) String() string { s += fmt.Sprintf("%.0fm ", alt) } - s += cmToM((rr.Size&0xf0)>>4, rr.Size&0x0f) + "m " - s += cmToM((rr.HorizPre&0xf0)>>4, rr.HorizPre&0x0f) + "m " - s += cmToM((rr.VertPre&0xf0)>>4, rr.VertPre&0x0f) + "m" + s += cmToM(rr.Size&0xf0>>4, rr.Size&0x0f) + "m " + s += cmToM(rr.HorizPre&0xf0>>4, rr.HorizPre&0x0f) + "m " + s += cmToM(rr.VertPre&0xf0>>4, rr.VertPre&0x0f) + "m" return s } @@ -1306,11 +1308,11 @@ func (rr *CSYNC) len() int { // string representation used when printing the record. // It takes serial arithmetic (RFC 1982) into account. func TimeToString(t uint32) string { - mod := ((int64(t) - time.Now().Unix()) / year68) - 1 + mod := (int64(t)-time.Now().Unix())/year68 - 1 if mod < 0 { mod = 0 } - ti := time.Unix(int64(t)-(mod*year68), 0).UTC() + ti := time.Unix(int64(t)-mod*year68, 0).UTC() return ti.Format("20060102150405") } @@ -1322,11 +1324,11 @@ func StringToTime(s string) (uint32, error) { if err != nil { return 0, err } - mod := (t.Unix() / year68) - 1 + mod := t.Unix()/year68 - 1 if mod < 0 { mod = 0 } - return uint32(t.Unix() - (mod * year68)), nil + return uint32(t.Unix() - mod*year68), nil } // saltToString converts a NSECX salt to uppercase and returns "-" when it is empty. diff --git a/vendor/github.com/miekg/dns/version.go b/vendor/github.com/miekg/dns/version.go index dcc84e4a..98df76d3 100644 --- a/vendor/github.com/miekg/dns/version.go +++ b/vendor/github.com/miekg/dns/version.go @@ -3,7 +3,7 @@ package dns import "fmt" // Version is current version of this library. -var Version = V{1, 0, 8} +var Version = V{1, 1, 0} // V holds the version of this library. type V struct { diff --git a/vendor/github.com/miekg/dns/zcompress.go b/vendor/github.com/miekg/dns/zcompress.go index a2c09dd4..fefdd2a0 100644 --- a/vendor/github.com/miekg/dns/zcompress.go +++ b/vendor/github.com/miekg/dns/zcompress.go @@ -2,7 +2,7 @@ package dns -func compressionLenHelperType(c map[string]int, r RR, initLen int) int { +func compressionLenHelperType(c map[string]struct{}, r RR, initLen int) int { currentLen := initLen switch x := r.(type) { case *AFSDB: @@ -107,11 +107,8 @@ func compressionLenHelperType(c map[string]int, r RR, initLen int) int { return currentLen - initLen } -func compressionLenSearchType(c map[string]int, r RR) (int, bool, int) { +func compressionLenSearchType(c map[string]struct{}, r RR) (int, bool, int) { switch x := r.(type) { - case *AFSDB: - k1, ok1, sz1 := compressionLenSearch(c, x.Hostname) - return k1, ok1, sz1 case *CNAME: k1, ok1, sz1 := compressionLenSearch(c, x.Target) return k1, ok1, sz1 diff --git a/vendor/github.com/miekg/dns/zduplicate.go b/vendor/github.com/miekg/dns/zduplicate.go new file mode 100644 index 00000000..ba9863b2 --- /dev/null +++ b/vendor/github.com/miekg/dns/zduplicate.go @@ -0,0 +1,943 @@ +// Code generated by "go run duplicate_generate.go"; DO NOT EDIT. + +package dns + +// isDuplicateRdata calls the rdata specific functions +func isDuplicateRdata(r1, r2 RR) bool { + switch r1.Header().Rrtype { + case TypeA: + return isDuplicateA(r1.(*A), r2.(*A)) + case TypeAAAA: + return isDuplicateAAAA(r1.(*AAAA), r2.(*AAAA)) + case TypeAFSDB: + return isDuplicateAFSDB(r1.(*AFSDB), r2.(*AFSDB)) + case TypeAVC: + return isDuplicateAVC(r1.(*AVC), r2.(*AVC)) + case TypeCAA: + return isDuplicateCAA(r1.(*CAA), r2.(*CAA)) + case TypeCERT: + return isDuplicateCERT(r1.(*CERT), r2.(*CERT)) + case TypeCNAME: + return isDuplicateCNAME(r1.(*CNAME), r2.(*CNAME)) + case TypeCSYNC: + return isDuplicateCSYNC(r1.(*CSYNC), r2.(*CSYNC)) + case TypeDHCID: + return isDuplicateDHCID(r1.(*DHCID), r2.(*DHCID)) + case TypeDNAME: + return isDuplicateDNAME(r1.(*DNAME), r2.(*DNAME)) + case TypeDNSKEY: + return isDuplicateDNSKEY(r1.(*DNSKEY), r2.(*DNSKEY)) + case TypeDS: + return isDuplicateDS(r1.(*DS), r2.(*DS)) + case TypeEID: + return isDuplicateEID(r1.(*EID), r2.(*EID)) + case TypeEUI48: + return isDuplicateEUI48(r1.(*EUI48), r2.(*EUI48)) + case TypeEUI64: + return isDuplicateEUI64(r1.(*EUI64), r2.(*EUI64)) + case TypeGID: + return isDuplicateGID(r1.(*GID), r2.(*GID)) + case TypeGPOS: + return isDuplicateGPOS(r1.(*GPOS), r2.(*GPOS)) + case TypeHINFO: + return isDuplicateHINFO(r1.(*HINFO), r2.(*HINFO)) + case TypeHIP: + return isDuplicateHIP(r1.(*HIP), r2.(*HIP)) + case TypeKX: + return isDuplicateKX(r1.(*KX), r2.(*KX)) + case TypeL32: + return isDuplicateL32(r1.(*L32), r2.(*L32)) + case TypeL64: + return isDuplicateL64(r1.(*L64), r2.(*L64)) + case TypeLOC: + return isDuplicateLOC(r1.(*LOC), r2.(*LOC)) + case TypeLP: + return isDuplicateLP(r1.(*LP), r2.(*LP)) + case TypeMB: + return isDuplicateMB(r1.(*MB), r2.(*MB)) + case TypeMD: + return isDuplicateMD(r1.(*MD), r2.(*MD)) + case TypeMF: + return isDuplicateMF(r1.(*MF), r2.(*MF)) + case TypeMG: + return isDuplicateMG(r1.(*MG), r2.(*MG)) + case TypeMINFO: + return isDuplicateMINFO(r1.(*MINFO), r2.(*MINFO)) + case TypeMR: + return isDuplicateMR(r1.(*MR), r2.(*MR)) + case TypeMX: + return isDuplicateMX(r1.(*MX), r2.(*MX)) + case TypeNAPTR: + return isDuplicateNAPTR(r1.(*NAPTR), r2.(*NAPTR)) + case TypeNID: + return isDuplicateNID(r1.(*NID), r2.(*NID)) + case TypeNIMLOC: + return isDuplicateNIMLOC(r1.(*NIMLOC), r2.(*NIMLOC)) + case TypeNINFO: + return isDuplicateNINFO(r1.(*NINFO), r2.(*NINFO)) + case TypeNS: + return isDuplicateNS(r1.(*NS), r2.(*NS)) + case TypeNSAPPTR: + return isDuplicateNSAPPTR(r1.(*NSAPPTR), r2.(*NSAPPTR)) + case TypeNSEC: + return isDuplicateNSEC(r1.(*NSEC), r2.(*NSEC)) + case TypeNSEC3: + return isDuplicateNSEC3(r1.(*NSEC3), r2.(*NSEC3)) + case TypeNSEC3PARAM: + return isDuplicateNSEC3PARAM(r1.(*NSEC3PARAM), r2.(*NSEC3PARAM)) + case TypeOPENPGPKEY: + return isDuplicateOPENPGPKEY(r1.(*OPENPGPKEY), r2.(*OPENPGPKEY)) + case TypePTR: + return isDuplicatePTR(r1.(*PTR), r2.(*PTR)) + case TypePX: + return isDuplicatePX(r1.(*PX), r2.(*PX)) + case TypeRKEY: + return isDuplicateRKEY(r1.(*RKEY), r2.(*RKEY)) + case TypeRP: + return isDuplicateRP(r1.(*RP), r2.(*RP)) + case TypeRRSIG: + return isDuplicateRRSIG(r1.(*RRSIG), r2.(*RRSIG)) + case TypeRT: + return isDuplicateRT(r1.(*RT), r2.(*RT)) + case TypeSMIMEA: + return isDuplicateSMIMEA(r1.(*SMIMEA), r2.(*SMIMEA)) + case TypeSOA: + return isDuplicateSOA(r1.(*SOA), r2.(*SOA)) + case TypeSPF: + return isDuplicateSPF(r1.(*SPF), r2.(*SPF)) + case TypeSRV: + return isDuplicateSRV(r1.(*SRV), r2.(*SRV)) + case TypeSSHFP: + return isDuplicateSSHFP(r1.(*SSHFP), r2.(*SSHFP)) + case TypeTA: + return isDuplicateTA(r1.(*TA), r2.(*TA)) + case TypeTALINK: + return isDuplicateTALINK(r1.(*TALINK), r2.(*TALINK)) + case TypeTKEY: + return isDuplicateTKEY(r1.(*TKEY), r2.(*TKEY)) + case TypeTLSA: + return isDuplicateTLSA(r1.(*TLSA), r2.(*TLSA)) + case TypeTSIG: + return isDuplicateTSIG(r1.(*TSIG), r2.(*TSIG)) + case TypeTXT: + return isDuplicateTXT(r1.(*TXT), r2.(*TXT)) + case TypeUID: + return isDuplicateUID(r1.(*UID), r2.(*UID)) + case TypeUINFO: + return isDuplicateUINFO(r1.(*UINFO), r2.(*UINFO)) + case TypeURI: + return isDuplicateURI(r1.(*URI), r2.(*URI)) + case TypeX25: + return isDuplicateX25(r1.(*X25), r2.(*X25)) + } + return false +} + +// isDuplicate() functions + +func isDuplicateA(r1, r2 *A) bool { + if len(r1.A) != len(r2.A) { + return false + } + for i := 0; i < len(r1.A); i++ { + if r1.A[i] != r2.A[i] { + return false + } + } + return true +} + +func isDuplicateAAAA(r1, r2 *AAAA) bool { + if len(r1.AAAA) != len(r2.AAAA) { + return false + } + for i := 0; i < len(r1.AAAA); i++ { + if r1.AAAA[i] != r2.AAAA[i] { + return false + } + } + return true +} + +func isDuplicateAFSDB(r1, r2 *AFSDB) bool { + if r1.Subtype != r2.Subtype { + return false + } + if !isDulicateName(r1.Hostname, r2.Hostname) { + return false + } + return true +} + +func isDuplicateAVC(r1, r2 *AVC) bool { + if len(r1.Txt) != len(r2.Txt) { + return false + } + for i := 0; i < len(r1.Txt); i++ { + if r1.Txt[i] != r2.Txt[i] { + return false + } + } + return true +} + +func isDuplicateCAA(r1, r2 *CAA) bool { + if r1.Flag != r2.Flag { + return false + } + if r1.Tag != r2.Tag { + return false + } + if r1.Value != r2.Value { + return false + } + return true +} + +func isDuplicateCERT(r1, r2 *CERT) bool { + if r1.Type != r2.Type { + return false + } + if r1.KeyTag != r2.KeyTag { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.Certificate != r2.Certificate { + return false + } + return true +} + +func isDuplicateCNAME(r1, r2 *CNAME) bool { + if !isDulicateName(r1.Target, r2.Target) { + return false + } + return true +} + +func isDuplicateCSYNC(r1, r2 *CSYNC) bool { + if r1.Serial != r2.Serial { + return false + } + if r1.Flags != r2.Flags { + return false + } + if len(r1.TypeBitMap) != len(r2.TypeBitMap) { + return false + } + for i := 0; i < len(r1.TypeBitMap); i++ { + if r1.TypeBitMap[i] != r2.TypeBitMap[i] { + return false + } + } + return true +} + +func isDuplicateDHCID(r1, r2 *DHCID) bool { + if r1.Digest != r2.Digest { + return false + } + return true +} + +func isDuplicateDNAME(r1, r2 *DNAME) bool { + if !isDulicateName(r1.Target, r2.Target) { + return false + } + return true +} + +func isDuplicateDNSKEY(r1, r2 *DNSKEY) bool { + if r1.Flags != r2.Flags { + return false + } + if r1.Protocol != r2.Protocol { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.PublicKey != r2.PublicKey { + return false + } + return true +} + +func isDuplicateDS(r1, r2 *DS) bool { + if r1.KeyTag != r2.KeyTag { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.DigestType != r2.DigestType { + return false + } + if r1.Digest != r2.Digest { + return false + } + return true +} + +func isDuplicateEID(r1, r2 *EID) bool { + if r1.Endpoint != r2.Endpoint { + return false + } + return true +} + +func isDuplicateEUI48(r1, r2 *EUI48) bool { + if r1.Address != r2.Address { + return false + } + return true +} + +func isDuplicateEUI64(r1, r2 *EUI64) bool { + if r1.Address != r2.Address { + return false + } + return true +} + +func isDuplicateGID(r1, r2 *GID) bool { + if r1.Gid != r2.Gid { + return false + } + return true +} + +func isDuplicateGPOS(r1, r2 *GPOS) bool { + if r1.Longitude != r2.Longitude { + return false + } + if r1.Latitude != r2.Latitude { + return false + } + if r1.Altitude != r2.Altitude { + return false + } + return true +} + +func isDuplicateHINFO(r1, r2 *HINFO) bool { + if r1.Cpu != r2.Cpu { + return false + } + if r1.Os != r2.Os { + return false + } + return true +} + +func isDuplicateHIP(r1, r2 *HIP) bool { + if r1.HitLength != r2.HitLength { + return false + } + if r1.PublicKeyAlgorithm != r2.PublicKeyAlgorithm { + return false + } + if r1.PublicKeyLength != r2.PublicKeyLength { + return false + } + if r1.Hit != r2.Hit { + return false + } + if r1.PublicKey != r2.PublicKey { + return false + } + if len(r1.RendezvousServers) != len(r2.RendezvousServers) { + return false + } + for i := 0; i < len(r1.RendezvousServers); i++ { + if !isDulicateName(r1.RendezvousServers[i], r2.RendezvousServers[i]) { + return false + } + } + return true +} + +func isDuplicateKX(r1, r2 *KX) bool { + if r1.Preference != r2.Preference { + return false + } + if !isDulicateName(r1.Exchanger, r2.Exchanger) { + return false + } + return true +} + +func isDuplicateL32(r1, r2 *L32) bool { + if r1.Preference != r2.Preference { + return false + } + if len(r1.Locator32) != len(r2.Locator32) { + return false + } + for i := 0; i < len(r1.Locator32); i++ { + if r1.Locator32[i] != r2.Locator32[i] { + return false + } + } + return true +} + +func isDuplicateL64(r1, r2 *L64) bool { + if r1.Preference != r2.Preference { + return false + } + if r1.Locator64 != r2.Locator64 { + return false + } + return true +} + +func isDuplicateLOC(r1, r2 *LOC) bool { + if r1.Version != r2.Version { + return false + } + if r1.Size != r2.Size { + return false + } + if r1.HorizPre != r2.HorizPre { + return false + } + if r1.VertPre != r2.VertPre { + return false + } + if r1.Latitude != r2.Latitude { + return false + } + if r1.Longitude != r2.Longitude { + return false + } + if r1.Altitude != r2.Altitude { + return false + } + return true +} + +func isDuplicateLP(r1, r2 *LP) bool { + if r1.Preference != r2.Preference { + return false + } + if !isDulicateName(r1.Fqdn, r2.Fqdn) { + return false + } + return true +} + +func isDuplicateMB(r1, r2 *MB) bool { + if !isDulicateName(r1.Mb, r2.Mb) { + return false + } + return true +} + +func isDuplicateMD(r1, r2 *MD) bool { + if !isDulicateName(r1.Md, r2.Md) { + return false + } + return true +} + +func isDuplicateMF(r1, r2 *MF) bool { + if !isDulicateName(r1.Mf, r2.Mf) { + return false + } + return true +} + +func isDuplicateMG(r1, r2 *MG) bool { + if !isDulicateName(r1.Mg, r2.Mg) { + return false + } + return true +} + +func isDuplicateMINFO(r1, r2 *MINFO) bool { + if !isDulicateName(r1.Rmail, r2.Rmail) { + return false + } + if !isDulicateName(r1.Email, r2.Email) { + return false + } + return true +} + +func isDuplicateMR(r1, r2 *MR) bool { + if !isDulicateName(r1.Mr, r2.Mr) { + return false + } + return true +} + +func isDuplicateMX(r1, r2 *MX) bool { + if r1.Preference != r2.Preference { + return false + } + if !isDulicateName(r1.Mx, r2.Mx) { + return false + } + return true +} + +func isDuplicateNAPTR(r1, r2 *NAPTR) bool { + if r1.Order != r2.Order { + return false + } + if r1.Preference != r2.Preference { + return false + } + if r1.Flags != r2.Flags { + return false + } + if r1.Service != r2.Service { + return false + } + if r1.Regexp != r2.Regexp { + return false + } + if !isDulicateName(r1.Replacement, r2.Replacement) { + return false + } + return true +} + +func isDuplicateNID(r1, r2 *NID) bool { + if r1.Preference != r2.Preference { + return false + } + if r1.NodeID != r2.NodeID { + return false + } + return true +} + +func isDuplicateNIMLOC(r1, r2 *NIMLOC) bool { + if r1.Locator != r2.Locator { + return false + } + return true +} + +func isDuplicateNINFO(r1, r2 *NINFO) bool { + if len(r1.ZSData) != len(r2.ZSData) { + return false + } + for i := 0; i < len(r1.ZSData); i++ { + if r1.ZSData[i] != r2.ZSData[i] { + return false + } + } + return true +} + +func isDuplicateNS(r1, r2 *NS) bool { + if !isDulicateName(r1.Ns, r2.Ns) { + return false + } + return true +} + +func isDuplicateNSAPPTR(r1, r2 *NSAPPTR) bool { + if !isDulicateName(r1.Ptr, r2.Ptr) { + return false + } + return true +} + +func isDuplicateNSEC(r1, r2 *NSEC) bool { + if !isDulicateName(r1.NextDomain, r2.NextDomain) { + return false + } + if len(r1.TypeBitMap) != len(r2.TypeBitMap) { + return false + } + for i := 0; i < len(r1.TypeBitMap); i++ { + if r1.TypeBitMap[i] != r2.TypeBitMap[i] { + return false + } + } + return true +} + +func isDuplicateNSEC3(r1, r2 *NSEC3) bool { + if r1.Hash != r2.Hash { + return false + } + if r1.Flags != r2.Flags { + return false + } + if r1.Iterations != r2.Iterations { + return false + } + if r1.SaltLength != r2.SaltLength { + return false + } + if r1.Salt != r2.Salt { + return false + } + if r1.HashLength != r2.HashLength { + return false + } + if r1.NextDomain != r2.NextDomain { + return false + } + if len(r1.TypeBitMap) != len(r2.TypeBitMap) { + return false + } + for i := 0; i < len(r1.TypeBitMap); i++ { + if r1.TypeBitMap[i] != r2.TypeBitMap[i] { + return false + } + } + return true +} + +func isDuplicateNSEC3PARAM(r1, r2 *NSEC3PARAM) bool { + if r1.Hash != r2.Hash { + return false + } + if r1.Flags != r2.Flags { + return false + } + if r1.Iterations != r2.Iterations { + return false + } + if r1.SaltLength != r2.SaltLength { + return false + } + if r1.Salt != r2.Salt { + return false + } + return true +} + +func isDuplicateOPENPGPKEY(r1, r2 *OPENPGPKEY) bool { + if r1.PublicKey != r2.PublicKey { + return false + } + return true +} + +func isDuplicatePTR(r1, r2 *PTR) bool { + if !isDulicateName(r1.Ptr, r2.Ptr) { + return false + } + return true +} + +func isDuplicatePX(r1, r2 *PX) bool { + if r1.Preference != r2.Preference { + return false + } + if !isDulicateName(r1.Map822, r2.Map822) { + return false + } + if !isDulicateName(r1.Mapx400, r2.Mapx400) { + return false + } + return true +} + +func isDuplicateRKEY(r1, r2 *RKEY) bool { + if r1.Flags != r2.Flags { + return false + } + if r1.Protocol != r2.Protocol { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.PublicKey != r2.PublicKey { + return false + } + return true +} + +func isDuplicateRP(r1, r2 *RP) bool { + if !isDulicateName(r1.Mbox, r2.Mbox) { + return false + } + if !isDulicateName(r1.Txt, r2.Txt) { + return false + } + return true +} + +func isDuplicateRRSIG(r1, r2 *RRSIG) bool { + if r1.TypeCovered != r2.TypeCovered { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.Labels != r2.Labels { + return false + } + if r1.OrigTtl != r2.OrigTtl { + return false + } + if r1.Expiration != r2.Expiration { + return false + } + if r1.Inception != r2.Inception { + return false + } + if r1.KeyTag != r2.KeyTag { + return false + } + if !isDulicateName(r1.SignerName, r2.SignerName) { + return false + } + if r1.Signature != r2.Signature { + return false + } + return true +} + +func isDuplicateRT(r1, r2 *RT) bool { + if r1.Preference != r2.Preference { + return false + } + if !isDulicateName(r1.Host, r2.Host) { + return false + } + return true +} + +func isDuplicateSMIMEA(r1, r2 *SMIMEA) bool { + if r1.Usage != r2.Usage { + return false + } + if r1.Selector != r2.Selector { + return false + } + if r1.MatchingType != r2.MatchingType { + return false + } + if r1.Certificate != r2.Certificate { + return false + } + return true +} + +func isDuplicateSOA(r1, r2 *SOA) bool { + if !isDulicateName(r1.Ns, r2.Ns) { + return false + } + if !isDulicateName(r1.Mbox, r2.Mbox) { + return false + } + if r1.Serial != r2.Serial { + return false + } + if r1.Refresh != r2.Refresh { + return false + } + if r1.Retry != r2.Retry { + return false + } + if r1.Expire != r2.Expire { + return false + } + if r1.Minttl != r2.Minttl { + return false + } + return true +} + +func isDuplicateSPF(r1, r2 *SPF) bool { + if len(r1.Txt) != len(r2.Txt) { + return false + } + for i := 0; i < len(r1.Txt); i++ { + if r1.Txt[i] != r2.Txt[i] { + return false + } + } + return true +} + +func isDuplicateSRV(r1, r2 *SRV) bool { + if r1.Priority != r2.Priority { + return false + } + if r1.Weight != r2.Weight { + return false + } + if r1.Port != r2.Port { + return false + } + if !isDulicateName(r1.Target, r2.Target) { + return false + } + return true +} + +func isDuplicateSSHFP(r1, r2 *SSHFP) bool { + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.Type != r2.Type { + return false + } + if r1.FingerPrint != r2.FingerPrint { + return false + } + return true +} + +func isDuplicateTA(r1, r2 *TA) bool { + if r1.KeyTag != r2.KeyTag { + return false + } + if r1.Algorithm != r2.Algorithm { + return false + } + if r1.DigestType != r2.DigestType { + return false + } + if r1.Digest != r2.Digest { + return false + } + return true +} + +func isDuplicateTALINK(r1, r2 *TALINK) bool { + if !isDulicateName(r1.PreviousName, r2.PreviousName) { + return false + } + if !isDulicateName(r1.NextName, r2.NextName) { + return false + } + return true +} + +func isDuplicateTKEY(r1, r2 *TKEY) bool { + if !isDulicateName(r1.Algorithm, r2.Algorithm) { + return false + } + if r1.Inception != r2.Inception { + return false + } + if r1.Expiration != r2.Expiration { + return false + } + if r1.Mode != r2.Mode { + return false + } + if r1.Error != r2.Error { + return false + } + if r1.KeySize != r2.KeySize { + return false + } + if r1.Key != r2.Key { + return false + } + if r1.OtherLen != r2.OtherLen { + return false + } + if r1.OtherData != r2.OtherData { + return false + } + return true +} + +func isDuplicateTLSA(r1, r2 *TLSA) bool { + if r1.Usage != r2.Usage { + return false + } + if r1.Selector != r2.Selector { + return false + } + if r1.MatchingType != r2.MatchingType { + return false + } + if r1.Certificate != r2.Certificate { + return false + } + return true +} + +func isDuplicateTSIG(r1, r2 *TSIG) bool { + if !isDulicateName(r1.Algorithm, r2.Algorithm) { + return false + } + if r1.TimeSigned != r2.TimeSigned { + return false + } + if r1.Fudge != r2.Fudge { + return false + } + if r1.MACSize != r2.MACSize { + return false + } + if r1.MAC != r2.MAC { + return false + } + if r1.OrigId != r2.OrigId { + return false + } + if r1.Error != r2.Error { + return false + } + if r1.OtherLen != r2.OtherLen { + return false + } + if r1.OtherData != r2.OtherData { + return false + } + return true +} + +func isDuplicateTXT(r1, r2 *TXT) bool { + if len(r1.Txt) != len(r2.Txt) { + return false + } + for i := 0; i < len(r1.Txt); i++ { + if r1.Txt[i] != r2.Txt[i] { + return false + } + } + return true +} + +func isDuplicateUID(r1, r2 *UID) bool { + if r1.Uid != r2.Uid { + return false + } + return true +} + +func isDuplicateUINFO(r1, r2 *UINFO) bool { + if r1.Uinfo != r2.Uinfo { + return false + } + return true +} + +func isDuplicateURI(r1, r2 *URI) bool { + if r1.Priority != r2.Priority { + return false + } + if r1.Weight != r2.Weight { + return false + } + if r1.Target != r2.Target { + return false + } + return true +} + +func isDuplicateX25(r1, r2 *X25) bool { + if r1.PSDNAddress != r2.PSDNAddress { + return false + } + return true +} diff --git a/vendor/github.com/miekg/dns/zmsg.go b/vendor/github.com/miekg/dns/zmsg.go index 0d1f6f4d..1a68f74d 100644 --- a/vendor/github.com/miekg/dns/zmsg.go +++ b/vendor/github.com/miekg/dns/zmsg.go @@ -42,7 +42,7 @@ func (rr *AFSDB) pack(msg []byte, off int, compression map[string]int, compress if err != nil { return off, err } - off, err = PackDomainName(rr.Hostname, msg, off, compression, compress) + off, err = PackDomainName(rr.Hostname, msg, off, compression, false) if err != nil { return off, err } diff --git a/vendor/github.com/sacloud/libsacloud/api/archive.go b/vendor/github.com/sacloud/libsacloud/api/archive.go index 8d9a1119..561c2c9a 100644 --- a/vendor/github.com/sacloud/libsacloud/api/archive.go +++ b/vendor/github.com/sacloud/libsacloud/api/archive.go @@ -2,10 +2,11 @@ package api import ( "fmt" - "github.com/sacloud/libsacloud/sacloud" - "github.com/sacloud/libsacloud/sacloud/ostype" "strings" "time" + + "github.com/sacloud/libsacloud/sacloud" + "github.com/sacloud/libsacloud/sacloud/ostype" ) // ArchiveAPI アーカイブAPI @@ -25,6 +26,8 @@ var ( archiveLatestStableKusanagiTags = []string{"current-stable", "pkg-kusanagi"} archiveLatestStableSophosUTMTags = []string{"current-stable", "pkg-sophosutm"} archiveLatestStableFreeBSDTags = []string{"current-stable", "distro-freebsd"} + archiveLatestStableNetwiserTags = []string{"current-stable", "pkg-netwiserve"} + archiveLatestStableOPNsenseTags = []string{"current-stable", "distro-opnsense"} archiveLatestStableWindows2012Tags = []string{"os-windows", "distro-ver-2012.2"} archiveLatestStableWindows2012RDSTags = []string{"os-windows", "distro-ver-2012.2", "windows-rds"} archiveLatestStableWindows2012RDSOfficeTags = []string{"os-windows", "distro-ver-2012.2", "windows-rds", "with-office"} @@ -60,6 +63,8 @@ func NewArchiveAPI(client *Client) *ArchiveAPI { ostype.Kusanagi: api.FindLatestStableKusanagi, ostype.SophosUTM: api.FindLatestStableSophosUTM, ostype.FreeBSD: api.FindLatestStableFreeBSD, + ostype.Netwiser: api.FindLatestStableNetwiser, + ostype.OPNsense: api.FindLatestStableOPNsense, ostype.Windows2012: api.FindLatestStableWindows2012, ostype.Windows2012RDS: api.FindLatestStableWindows2012RDS, ostype.Windows2012RDSOffice: api.FindLatestStableWindows2012RDSOffice, @@ -141,6 +146,14 @@ func (api *ArchiveAPI) CanEditDisk(id int64) (bool, error) { if archive.HasTag("pkg-sophosutm") || archive.IsSophosUTM() { return false, nil } + // OPNsenseであれば編集不可 + if archive.HasTag("distro-opnsense") { + return false, nil + } + // Netwiser VEであれば編集不可 + if archive.HasTag("pkg-netwiserve") { + return false, nil + } for _, t := range allowDiskEditTags { if archive.HasTag(t) { @@ -184,6 +197,14 @@ func (api *ArchiveAPI) GetPublicArchiveIDFromAncestors(id int64) (int64, bool) { if archive.HasTag("pkg-sophosutm") || archive.IsSophosUTM() { return emptyID, false } + // OPNsenseであれば編集不可 + if archive.HasTag("distro-opnsense") { + return emptyID, false + } + // Netwiser VEであれば編集不可 + if archive.HasTag("pkg-netwiserve") { + return emptyID, false + } for _, t := range allowDiskEditTags { if archive.HasTag(t) { @@ -253,6 +274,16 @@ func (api *ArchiveAPI) FindLatestStableFreeBSD() (*sacloud.Archive, error) { return api.findByOSTags(archiveLatestStableFreeBSDTags) } +// FindLatestStableNetwiser 安定版最新のNetwiserパブリックアーカイブを取得 +func (api *ArchiveAPI) FindLatestStableNetwiser() (*sacloud.Archive, error) { + return api.findByOSTags(archiveLatestStableNetwiserTags) +} + +// FindLatestStableOPNsense 安定版最新のOPNsenseパブリックアーカイブを取得 +func (api *ArchiveAPI) FindLatestStableOPNsense() (*sacloud.Archive, error) { + return api.findByOSTags(archiveLatestStableOPNsenseTags) +} + // FindLatestStableWindows2012 安定版最新のWindows2012パブリックアーカイブを取得 func (api *ArchiveAPI) FindLatestStableWindows2012() (*sacloud.Archive, error) { return api.findByOSTags(archiveLatestStableWindows2012Tags, map[string]interface{}{ diff --git a/vendor/github.com/sacloud/libsacloud/api/auth_status.go b/vendor/github.com/sacloud/libsacloud/api/auth_status.go index d9fd2c73..e1d9621b 100644 --- a/vendor/github.com/sacloud/libsacloud/api/auth_status.go +++ b/vendor/github.com/sacloud/libsacloud/api/auth_status.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/auto_backup.go b/vendor/github.com/sacloud/libsacloud/api/auto_backup.go index d9944e09..2bd237ab 100644 --- a/vendor/github.com/sacloud/libsacloud/api/auto_backup.go +++ b/vendor/github.com/sacloud/libsacloud/api/auto_backup.go @@ -1,8 +1,8 @@ package api import ( - "encoding/json" - // "strings" + "encoding/json" // "strings" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/base_api.go b/vendor/github.com/sacloud/libsacloud/api/base_api.go index a071869c..9840938c 100644 --- a/vendor/github.com/sacloud/libsacloud/api/base_api.go +++ b/vendor/github.com/sacloud/libsacloud/api/base_api.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "net/url" + + "github.com/sacloud/libsacloud/sacloud" ) type baseAPI struct { diff --git a/vendor/github.com/sacloud/libsacloud/api/bill.go b/vendor/github.com/sacloud/libsacloud/api/bill.go index 2caba0b2..caf252dc 100644 --- a/vendor/github.com/sacloud/libsacloud/api/bill.go +++ b/vendor/github.com/sacloud/libsacloud/api/bill.go @@ -4,10 +4,11 @@ import ( "encoding/csv" "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "io" "strings" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // BillAPI 請求情報API diff --git a/vendor/github.com/sacloud/libsacloud/api/cdrom.go b/vendor/github.com/sacloud/libsacloud/api/cdrom.go index 5bda66b9..49a479c6 100644 --- a/vendor/github.com/sacloud/libsacloud/api/cdrom.go +++ b/vendor/github.com/sacloud/libsacloud/api/cdrom.go @@ -2,8 +2,9 @@ package api import ( "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // CDROMAPI ISOイメージAPI diff --git a/vendor/github.com/sacloud/libsacloud/api/client.go b/vendor/github.com/sacloud/libsacloud/api/client.go index f3ca5a9a..2bf63020 100644 --- a/vendor/github.com/sacloud/libsacloud/api/client.go +++ b/vendor/github.com/sacloud/libsacloud/api/client.go @@ -4,14 +4,15 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/sacloud/libsacloud" - "github.com/sacloud/libsacloud/sacloud" "io" "io/ioutil" "log" "net/http" "strings" "time" + + "github.com/sacloud/libsacloud" + "github.com/sacloud/libsacloud/sacloud" ) var ( diff --git a/vendor/github.com/sacloud/libsacloud/api/database.go b/vendor/github.com/sacloud/libsacloud/api/database.go index 9ebe9a14..5efcb2f7 100644 --- a/vendor/github.com/sacloud/libsacloud/api/database.go +++ b/vendor/github.com/sacloud/libsacloud/api/database.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) //HACK: さくらのAPI側仕様: Applianceの内容によってJSONフォーマットが異なるため diff --git a/vendor/github.com/sacloud/libsacloud/api/disk.go b/vendor/github.com/sacloud/libsacloud/api/disk.go index 198281e5..fa39a22f 100644 --- a/vendor/github.com/sacloud/libsacloud/api/disk.go +++ b/vendor/github.com/sacloud/libsacloud/api/disk.go @@ -2,8 +2,9 @@ package api import ( "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) var ( @@ -56,7 +57,50 @@ func (api *DiskAPI) Create(value *sacloud.Disk) (*sacloud.Disk, error) { Success string `json:",omitempty"` } res := &diskResponse{} - err := api.create(api.createRequest(value), res) + + rawBody := &sacloud.Request{} + rawBody.Disk = value + if len(value.DistantFrom) > 0 { + rawBody.DistantFrom = value.DistantFrom + value.DistantFrom = []int64{} + } + + err := api.create(rawBody, res) + if err != nil { + return nil, err + } + return res.Disk, nil +} + +// CreateWithConfig ディスク作成とディスクの修正、サーバ起動(指定されていれば)を1回のAPI呼び出しで実行 +func (api *DiskAPI) CreateWithConfig(value *sacloud.Disk, config *sacloud.DiskEditValue, bootAtAvailable bool) (*sacloud.Disk, error) { + //HACK: さくらのAPI側仕様: 戻り値:Successがbool値へ変換できないため文字列で受ける("Accepted"などが返る) + type diskResponse struct { + *sacloud.Response + // Success + Success string `json:",omitempty"` + } + res := &diskResponse{} + + type diskRequest struct { + *sacloud.Request + Config *sacloud.DiskEditValue `json:",omitempty"` + BootAtAvailable bool `json:",omitempty"` + } + + rawBody := &diskRequest{ + Request: &sacloud.Request{}, + BootAtAvailable: bootAtAvailable, + } + rawBody.Disk = value + rawBody.Config = config + + if len(value.DistantFrom) > 0 { + rawBody.DistantFrom = value.DistantFrom + value.DistantFrom = []int64{} + } + + err := api.create(rawBody, res) if err != nil { return nil, err } @@ -90,7 +134,14 @@ func (api *DiskAPI) install(id int64, body *sacloud.Disk) (bool, error) { Success string `json:",omitempty"` } res := &diskResponse{} - err := api.baseAPI.request(method, uri, api.createRequest(body), res) + rawBody := &sacloud.Request{} + rawBody.Disk = body + if len(body.DistantFrom) > 0 { + rawBody.DistantFrom = body.DistantFrom + body.DistantFrom = []int64{} + } + + err := api.baseAPI.request(method, uri, rawBody, res) if err != nil { return false, err } @@ -213,6 +264,14 @@ func (api *DiskAPI) CanEditDisk(id int64) (bool, error) { if disk.HasTag("pkg-sophosutm") || disk.IsSophosUTM() { return false, nil } + // OPNsenseであれば編集不可 + if disk.HasTag("distro-opnsense") { + return false, nil + } + // Netwiser VEであれば編集不可 + if disk.HasTag("pkg-netwiserve") { + return false, nil + } // ソースアーカイブ/ソースディスクともに持っていない場合 if disk.SourceArchive == nil && disk.SourceDisk == nil { @@ -263,6 +322,14 @@ func (api *DiskAPI) GetPublicArchiveIDFromAncestors(id int64) (int64, bool) { if disk.HasTag("pkg-sophosutm") || disk.IsSophosUTM() { return emptyID, false } + // OPNsenseであれば編集不可 + if disk.HasTag("distro-opnsense") { + return emptyID, false + } + // Netwiser VEであれば編集不可 + if disk.HasTag("pkg-netwiserve") { + return emptyID, false + } for _, t := range allowDiskEditTags { if disk.HasTag(t) { diff --git a/vendor/github.com/sacloud/libsacloud/api/dns.go b/vendor/github.com/sacloud/libsacloud/api/dns.go index 3483a835..b81e9612 100644 --- a/vendor/github.com/sacloud/libsacloud/api/dns.go +++ b/vendor/github.com/sacloud/libsacloud/api/dns.go @@ -2,8 +2,9 @@ package api import ( "encoding/json" - "github.com/sacloud/libsacloud/sacloud" "strings" + + "github.com/sacloud/libsacloud/sacloud" ) //HACK: さくらのAPI側仕様: CommonServiceItemsの内容によってJSONフォーマットが異なるため diff --git a/vendor/github.com/sacloud/libsacloud/api/error.go b/vendor/github.com/sacloud/libsacloud/api/error.go index 5e0ca2a8..366dd3f9 100644 --- a/vendor/github.com/sacloud/libsacloud/api/error.go +++ b/vendor/github.com/sacloud/libsacloud/api/error.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/gslb.go b/vendor/github.com/sacloud/libsacloud/api/gslb.go index bce4cc2d..976f297c 100644 --- a/vendor/github.com/sacloud/libsacloud/api/gslb.go +++ b/vendor/github.com/sacloud/libsacloud/api/gslb.go @@ -1,8 +1,8 @@ package api import ( - "encoding/json" - // "strings" + "encoding/json" // "strings" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/interface.go b/vendor/github.com/sacloud/libsacloud/api/interface.go index 21282c6c..bcfa1bf0 100644 --- a/vendor/github.com/sacloud/libsacloud/api/interface.go +++ b/vendor/github.com/sacloud/libsacloud/api/interface.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) @@ -81,3 +82,26 @@ func (api *InterfaceAPI) DisconnectFromPacketFilter(interfaceID int64) (bool, er ) return api.modify(method, uri, nil) } + +// SetDisplayIPAddress 表示用IPアドレス 設定 +func (api *InterfaceAPI) SetDisplayIPAddress(interfaceID int64, ipaddress string) (bool, error) { + var ( + method = "PUT" + uri = fmt.Sprintf("/%s/%d", api.getResourceURL(), interfaceID) + ) + body := map[string]interface{}{ + "Interface": map[string]string{ + "UserIPAddress": ipaddress, + }, + } + return api.modify(method, uri, body) +} + +// DeleteDisplayIPAddress 表示用IPアドレス 削除 +func (api *InterfaceAPI) DeleteDisplayIPAddress(interfaceID int64) (bool, error) { + var ( + method = "DELETE" + uri = fmt.Sprintf("/%s/%d", api.getResourceURL(), interfaceID) + ) + return api.modify(method, uri, nil) +} diff --git a/vendor/github.com/sacloud/libsacloud/api/internet.go b/vendor/github.com/sacloud/libsacloud/api/internet.go index 01513414..64a21fd5 100644 --- a/vendor/github.com/sacloud/libsacloud/api/internet.go +++ b/vendor/github.com/sacloud/libsacloud/api/internet.go @@ -2,8 +2,9 @@ package api import ( "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // InternetAPI ルーターAPI diff --git a/vendor/github.com/sacloud/libsacloud/api/ipaddress.go b/vendor/github.com/sacloud/libsacloud/api/ipaddress.go index 3de95259..42a07b6d 100644 --- a/vendor/github.com/sacloud/libsacloud/api/ipaddress.go +++ b/vendor/github.com/sacloud/libsacloud/api/ipaddress.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/ipv6addr.go b/vendor/github.com/sacloud/libsacloud/api/ipv6addr.go index 7186d3e0..5884742b 100644 --- a/vendor/github.com/sacloud/libsacloud/api/ipv6addr.go +++ b/vendor/github.com/sacloud/libsacloud/api/ipv6addr.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/load_balancer.go b/vendor/github.com/sacloud/libsacloud/api/load_balancer.go index c5873c69..14042bd8 100644 --- a/vendor/github.com/sacloud/libsacloud/api/load_balancer.go +++ b/vendor/github.com/sacloud/libsacloud/api/load_balancer.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) //HACK: さくらのAPI側仕様: Applianceの内容によってJSONフォーマットが異なるため diff --git a/vendor/github.com/sacloud/libsacloud/api/mobile_gateway.go b/vendor/github.com/sacloud/libsacloud/api/mobile_gateway.go index 1e0bdc39..6a298427 100644 --- a/vendor/github.com/sacloud/libsacloud/api/mobile_gateway.go +++ b/vendor/github.com/sacloud/libsacloud/api/mobile_gateway.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // SearchMobileGatewayResponse モバイルゲートウェイ検索レスポンス @@ -41,6 +42,14 @@ type mobileGatewaySIMResponse struct { Success interface{} `json:",omitempty"` //HACK: さくらのAPI側仕様: 戻り値:Successがbool値へ変換できないためinterface{} } +type trafficMonitoringBody struct { + TrafficMonitoring *sacloud.TrafficMonitoringConfig `json:"traffic_monitoring_config"` +} + +type trafficStatusBody struct { + TrafficStatus *sacloud.TrafficStatus `json:"traffic_status"` +} + // MobileGatewayAPI モバイルゲートウェイAPI type MobileGatewayAPI struct { *baseAPI @@ -412,3 +421,55 @@ func (api *MobileGatewayAPI) Logs(id int64, body interface{}) ([]sacloud.SIMLog, } return res.Logs, nil } + +// GetTrafficMonitoringConfig トラフィックコントロール 取得 +func (api *MobileGatewayAPI) GetTrafficMonitoringConfig(id int64) (*sacloud.TrafficMonitoringConfig, error) { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%d/mobilegateway/traffic_monitoring", api.getResourceURL(), id) + ) + + res := &trafficMonitoringBody{} + err := api.baseAPI.request(method, uri, nil, res) + if err != nil { + return nil, err + } + return res.TrafficMonitoring, nil +} + +// SetTrafficMonitoringConfig トラフィックコントロール 設定 +func (api *MobileGatewayAPI) SetTrafficMonitoringConfig(id int64, trafficMonConfig *sacloud.TrafficMonitoringConfig) (bool, error) { + var ( + method = "PUT" + uri = fmt.Sprintf("%s/%d/mobilegateway/traffic_monitoring", api.getResourceURL(), id) + ) + + req := &trafficMonitoringBody{ + TrafficMonitoring: trafficMonConfig, + } + return api.modify(method, uri, req) +} + +// DisableTrafficMonitoringConfig トラフィックコントロール 解除 +func (api *MobileGatewayAPI) DisableTrafficMonitoringConfig(id int64) (bool, error) { + var ( + method = "DELETE" + uri = fmt.Sprintf("%s/%d/mobilegateway/traffic_monitoring", api.getResourceURL(), id) + ) + return api.modify(method, uri, nil) +} + +// GetTrafficStatus 当月通信量 取得 +func (api *MobileGatewayAPI) GetTrafficStatus(id int64) (*sacloud.TrafficStatus, error) { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%d/mobilegateway/traffic_status", api.getResourceURL(), id) + ) + + res := &trafficStatusBody{} + err := api.baseAPI.request(method, uri, nil, res) + if err != nil { + return nil, err + } + return res.TrafficStatus, nil +} diff --git a/vendor/github.com/sacloud/libsacloud/api/newsfeed.go b/vendor/github.com/sacloud/libsacloud/api/newsfeed.go index 4adab1ca..f2dad265 100644 --- a/vendor/github.com/sacloud/libsacloud/api/newsfeed.go +++ b/vendor/github.com/sacloud/libsacloud/api/newsfeed.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/nfs.go b/vendor/github.com/sacloud/libsacloud/api/nfs.go index f044fc86..b65fa5ad 100644 --- a/vendor/github.com/sacloud/libsacloud/api/nfs.go +++ b/vendor/github.com/sacloud/libsacloud/api/nfs.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // SearchNFSResponse NFS検索レスポンス diff --git a/vendor/github.com/sacloud/libsacloud/api/product_server.go b/vendor/github.com/sacloud/libsacloud/api/product_server.go index f5efff13..63807eee 100644 --- a/vendor/github.com/sacloud/libsacloud/api/product_server.go +++ b/vendor/github.com/sacloud/libsacloud/api/product_server.go @@ -2,8 +2,8 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" - "strconv" ) // ProductServerAPI サーバープランAPI @@ -24,48 +24,50 @@ func NewProductServerAPI(client *Client) *ProductServerAPI { } } -func (api *ProductServerAPI) getPlanIDBySpec(core int, memGB int) (int64, error) { - //assert args - if core <= 0 { - return -1, fmt.Errorf("Invalid Parameter: CPU Core") - } - if memGB <= 0 { - return -1, fmt.Errorf("Invalid Parameter: Memory Size(GB)") - } - - return strconv.ParseInt(fmt.Sprintf("%d%03d", memGB, core), 10, 64) -} - -// IsValidPlan 指定のコア数/メモリサイズのプランが存在し、有効であるか判定 -func (api *ProductServerAPI) IsValidPlan(core int, memGB int) (bool, error) { - - planID, err := api.getPlanIDBySpec(core, memGB) - if err != nil { - return false, err - } - productServer, err := api.Read(planID) - - if err != nil { - return false, err - } - - if productServer != nil { - return true, nil - } - - return false, fmt.Errorf("Server Plan[%d] Not Found", planID) - -} - -// GetBySpec 指定のコア数/メモリサイズのサーバープランを取得 -func (api *ProductServerAPI) GetBySpec(core int, memGB int) (*sacloud.ProductServer, error) { - planID, err := api.getPlanIDBySpec(core, memGB) - - productServer, err := api.Read(planID) - +// GetBySpec 指定のコア数/メモリサイズ/世代のプランを取得 +func (api *ProductServerAPI) GetBySpec(core int, memGB int, gen sacloud.PlanGenerations) (*sacloud.ProductServer, error) { + plans, err := api.Reset().Find() if err != nil { return nil, err } + var res sacloud.ProductServer + var found bool + for _, plan := range plans.ServerPlans { + if plan.CPU == core && plan.GetMemoryGB() == memGB { + if gen == sacloud.PlanDefault || gen == plan.Generation { + // PlanDefaultの場合は複数ヒットしうる。 + // この場合より新しい世代を優先する。 + if found && plan.Generation <= res.Generation { + continue + } + res = plan + found = true + } + } + } - return productServer, nil + if !found { + return nil, fmt.Errorf("Server Plan[core:%d, memory:%d, gen:%d] is not found", core, memGB, gen) + } + return &res, nil +} + +// IsValidPlan 指定のコア数/メモリサイズ/世代のプランが存在し、有効であるか判定 +func (api *ProductServerAPI) IsValidPlan(core int, memGB int, gen sacloud.PlanGenerations) (bool, error) { + + productServer, err := api.GetBySpec(core, memGB, gen) + + if err != nil { + return false, err + } + + if productServer == nil { + return false, fmt.Errorf("Server Plan[core:%d, memory:%d, gen:%d] is not found", core, memGB, gen) + } + + if productServer.Availability != sacloud.EAAvailable { + return false, fmt.Errorf("Server Plan[core:%d, memory:%d, gen:%d] is not available", core, memGB, gen) + } + + return true, nil } diff --git a/vendor/github.com/sacloud/libsacloud/api/server.go b/vendor/github.com/sacloud/libsacloud/api/server.go index 16d81fe0..4572c6bd 100644 --- a/vendor/github.com/sacloud/libsacloud/api/server.go +++ b/vendor/github.com/sacloud/libsacloud/api/server.go @@ -2,8 +2,9 @@ package api import ( "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) // ServerAPI サーバーAPI @@ -150,14 +151,18 @@ func (api *ServerAPI) SleepUntilDown(id int64, timeout time.Duration) error { } // ChangePlan サーバープラン変更(サーバーIDが変更となるため注意) -func (api *ServerAPI) ChangePlan(serverID int64, planID string) (*sacloud.Server, error) { +func (api *ServerAPI) ChangePlan(serverID int64, plan *sacloud.ProductServer) (*sacloud.Server, error) { var ( method = "PUT" - uri = fmt.Sprintf("%s/%d/to/plan/%s", api.getResourceURL(), serverID, planID) + uri = fmt.Sprintf("%s/%d/plan", api.getResourceURL(), serverID) + body = &sacloud.ProductServer{} ) + body.CPU = plan.CPU + body.MemoryMB = plan.MemoryMB + body.Generation = plan.Generation return api.request(func(res *sacloud.Response) error { - return api.baseAPI.request(method, uri, nil, res) + return api.baseAPI.request(method, uri, body, res) }) } diff --git a/vendor/github.com/sacloud/libsacloud/api/simple_monitor.go b/vendor/github.com/sacloud/libsacloud/api/simple_monitor.go index 8e1a45b2..881cdfdd 100644 --- a/vendor/github.com/sacloud/libsacloud/api/simple_monitor.go +++ b/vendor/github.com/sacloud/libsacloud/api/simple_monitor.go @@ -1,9 +1,9 @@ package api import ( - "encoding/json" - // "strings" + "encoding/json" // "strings" "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/ssh_key.go b/vendor/github.com/sacloud/libsacloud/api/ssh_key.go index 63148286..1ad48254 100644 --- a/vendor/github.com/sacloud/libsacloud/api/ssh_key.go +++ b/vendor/github.com/sacloud/libsacloud/api/ssh_key.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/switch.go b/vendor/github.com/sacloud/libsacloud/api/switch.go index b646c10d..c98d9623 100644 --- a/vendor/github.com/sacloud/libsacloud/api/switch.go +++ b/vendor/github.com/sacloud/libsacloud/api/switch.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/sacloud/libsacloud/sacloud" ) diff --git a/vendor/github.com/sacloud/libsacloud/api/vpc_router.go b/vendor/github.com/sacloud/libsacloud/api/vpc_router.go index 43ad1513..5a6df64e 100644 --- a/vendor/github.com/sacloud/libsacloud/api/vpc_router.go +++ b/vendor/github.com/sacloud/libsacloud/api/vpc_router.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "time" + + "github.com/sacloud/libsacloud/sacloud" ) //HACK: さくらのAPI側仕様: Applianceの内容によってJSONフォーマットが異なるため diff --git a/vendor/github.com/sacloud/libsacloud/api/webaccel.go b/vendor/github.com/sacloud/libsacloud/api/webaccel.go index e3b924b2..81708afd 100644 --- a/vendor/github.com/sacloud/libsacloud/api/webaccel.go +++ b/vendor/github.com/sacloud/libsacloud/api/webaccel.go @@ -3,8 +3,9 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "strings" + + "github.com/sacloud/libsacloud/sacloud" ) // WebAccelAPI ウェブアクセラレータAPI diff --git a/vendor/github.com/sacloud/libsacloud/api/webaccel_search.go b/vendor/github.com/sacloud/libsacloud/api/webaccel_search.go index fc8884b6..28e02163 100644 --- a/vendor/github.com/sacloud/libsacloud/api/webaccel_search.go +++ b/vendor/github.com/sacloud/libsacloud/api/webaccel_search.go @@ -3,9 +3,10 @@ package api import ( "encoding/json" "fmt" - "github.com/sacloud/libsacloud/sacloud" "net/url" "strings" + + "github.com/sacloud/libsacloud/sacloud" ) // Reset 検索条件のリセット diff --git a/vendor/github.com/sacloud/libsacloud/libsacloud.go b/vendor/github.com/sacloud/libsacloud/libsacloud.go index 1981b502..8403b2a3 100644 --- a/vendor/github.com/sacloud/libsacloud/libsacloud.go +++ b/vendor/github.com/sacloud/libsacloud/libsacloud.go @@ -2,4 +2,4 @@ package libsacloud // Version バージョン -const Version = "1.0.0-rc5" +const Version = "1.0.0" diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/common_types.go b/vendor/github.com/sacloud/libsacloud/sacloud/common_types.go index 35e88afe..7b709f43 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/common_types.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/common_types.go @@ -213,7 +213,7 @@ type Request struct { Filter map[string]interface{} `json:",omitempty"` // フィルタ Exclude []string `json:",omitempty"` // 除外する項目 Include []string `json:",omitempty"` // 取得する項目 - + DistantFrom []int64 `json:",omitempty"` // ストレージ隔離対象ディスク } // AddFilter フィルタの追加 @@ -324,3 +324,15 @@ var ( // DatetimeLayout さくらのクラウドAPIで利用される日付型のレイアウト(RFC3339) var DatetimeLayout = "2006-01-02T15:04:05-07:00" + +// PlanGenerations サーバプラン世代 +type PlanGenerations int + +var ( + // PlanDefault デフォルト + PlanDefault = PlanGenerations(0) + // PlanG1 第1世代(Generation:100) + PlanG1 = PlanGenerations(100) + // PlanG2 第2世代(Generation:200) + PlanG2 = PlanGenerations(200) +) diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/database.go b/vendor/github.com/sacloud/libsacloud/sacloud/database.go index 447f9cd9..0f989f2f 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/database.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/database.go @@ -2,9 +2,15 @@ package sacloud import ( "encoding/json" + "fmt" "strings" ) +// AllowDatabaseBackupWeekdays データベースバックアップ実行曜日リスト +func AllowDatabaseBackupWeekdays() []string { + return []string{"mon", "tue", "wed", "thu", "fri", "sat", "sun"} +} + // Database データベース(appliance) type Database struct { *Appliance // アプライアンス共通属性 @@ -64,8 +70,6 @@ type DatabaseCommonRemark struct { DatabaseRevision string `json:",omitempty"` // リビジョン DatabaseTitle string `json:",omitempty"` // タイトル DatabaseVersion string `json:",omitempty"` // バージョン - ReplicaPassword string `json:",omitempty"` // レプリケーションパスワード - ReplicaUser string `json:",omitempty"` // レプリケーションユーザー } // DatabaseSettings データベース設定リスト @@ -75,8 +79,9 @@ type DatabaseSettings struct { // DatabaseSetting データベース設定 type DatabaseSetting struct { - Backup *DatabaseBackupSetting `json:",omitempty"` // バックアップ設定 - Common *DatabaseCommonSetting `json:",oitempty"` // 共通設定 + Backup *DatabaseBackupSetting `json:",omitempty"` // バックアップ設定 + Common *DatabaseCommonSetting `json:",oitempty"` // 共通設定 + Replication *DatabaseReplicationSetting `json:",omitempty"` // レプリケーション設定 } // DatabaseServer データベースサーバー情報 @@ -122,17 +127,20 @@ func AllowDatabasePlans() []int { // DatabaseBackupSetting バックアップ設定 type DatabaseBackupSetting struct { - Rotate int `json:",omitempty"` // ローテーション世代数 - Time string `json:",omitempty"` // 開始時刻 + Rotate int `json:",omitempty"` // ローテーション世代数 + Time string `json:",omitempty"` // 開始時刻 + DayOfWeek []string `json:",omitempty"` // 取得曜日 } // DatabaseCommonSetting 共通設定 type DatabaseCommonSetting struct { - DefaultUser string `json:",omitempty"` // ユーザー名 - UserPassword string `json:",omitempty"` // ユーザーパスワード - WebUI interface{} `json:",omitempty"` // WebUIのIPアドレス or FQDN - ServicePort string // ポート番号 - SourceNetwork SourceNetwork // 接続許可ネットワーク + DefaultUser string `json:",omitempty"` // ユーザー名 + UserPassword string `json:",omitempty"` // ユーザーパスワード + WebUI interface{} `json:",omitempty"` // WebUIのIPアドレス or FQDN + ReplicaPassword string `json:",omitempty"` // レプリケーションパスワード + ReplicaUser string `json:",omitempty"` // レプリケーションユーザー + ServicePort json.Number `json:",omitempty"` // ポート番号 + SourceNetwork SourceNetwork // 接続許可ネットワーク } // SourceNetwork 接続許可ネットワーク @@ -168,32 +176,82 @@ func (s *SourceNetwork) MarshalJSON() ([]byte, error) { return json.Marshal(list) } +// DatabaseReplicationSetting レプリケーション設定 +type DatabaseReplicationSetting struct { + // Model レプリケーションモデル + Model DatabaseReplicationModels `json:",omitempty"` + // Appliance マスター側アプライアンス + Appliance *Resource `json:",omitempty"` + // IPAddress IPアドレス + IPAddress string `json:",omitempty"` + // Port ポート + Port int `json:",omitempty"` + // User ユーザー + User string `json:",omitempty"` + // Password パスワード + Password string `json:",omitempty"` +} + +// DatabaseReplicationModels データベースのレプリケーションモデル +type DatabaseReplicationModels string + +const ( + // DatabaseReplicationModelMasterSlave レプリケーションモデル: Master-Slave(マスター側) + DatabaseReplicationModelMasterSlave = "Master-Slave" + // DatabaseReplicationModelAsyncReplica レプリケーションモデル: Async-Replica(スレーブ側) + DatabaseReplicationModelAsyncReplica = "Async-Replica" +) + // CreateDatabaseValue データベース作成用パラメータ type CreateDatabaseValue struct { - Plan DatabasePlan // プラン - AdminPassword string // 管理者パスワード - DefaultUser string // ユーザー名 - UserPassword string // パスワード - SourceNetwork []string // 接続許可ネットワーク - ServicePort string // ポート - // BackupRotate int // バックアップ世代数 - BackupTime string // バックアップ開始時間 - SwitchID string // 接続先スイッチ - IPAddress1 string // IPアドレス1 - MaskLen int // ネットワークマスク長 - DefaultRoute string // デフォルトルート - Name string // 名称 - Description string // 説明 - Tags []string // タグ - Icon *Resource // アイコン - WebUI bool // WebUI有効 - DatabaseName string // データベース名 - DatabaseRevision string // リビジョン - DatabaseTitle string // データベースタイトル - DatabaseVersion string // データベースバージョン - ReplicaUser string // ReplicaUser レプリケーションユーザー - SourceAppliance *Resource // クローン元DB - //ReplicaPassword string // in current API version , setted admin password + Plan DatabasePlan // プラン + AdminPassword string // 管理者パスワード + DefaultUser string // ユーザー名 + UserPassword string // パスワード + SourceNetwork []string // 接続許可ネットワーク + ServicePort int // ポート + EnableBackup bool // バックアップ有効化 + BackupRotate int // バックアップ世代数 + BackupTime string // バックアップ開始時間 + BackupDayOfWeek []string // バックアップ取得曜日 + SwitchID string // 接続先スイッチ + IPAddress1 string // IPアドレス1 + MaskLen int // ネットワークマスク長 + DefaultRoute string // デフォルトルート + Name string // 名称 + Description string // 説明 + Tags []string // タグ + Icon *Resource // アイコン + WebUI bool // WebUI有効 + DatabaseName string // データベース名 + DatabaseRevision string // リビジョン + DatabaseTitle string // データベースタイトル + DatabaseVersion string // データベースバージョン + // ReplicaUser string // レプリケーションユーザー 現在はreplica固定 + ReplicaPassword string // レプリケーションパスワード + SourceAppliance *Resource // クローン元DB +} + +// SlaveDatabaseValue スレーブデータベース作成用パラメータ +type SlaveDatabaseValue struct { + Plan DatabasePlan // プラン + DefaultUser string // ユーザー名 + UserPassword string // パスワード + SwitchID string // 接続先スイッチ + IPAddress1 string // IPアドレス1 + MaskLen int // ネットワークマスク長 + DefaultRoute string // デフォルトルート + Name string // 名称 + Description string // 説明 + Tags []string // タグ + Icon *Resource // アイコン + DatabaseName string // データベース名 + DatabaseVersion string // データベースバージョン + // ReplicaUser string // レプリケーションユーザー 現在はreplica固定 + ReplicaPassword string // レプリケーションパスワード + MasterApplianceID int64 // クローン元DB + MasterIPAddress string // マスターIPアドレス + MasterPort int // マスターポート } // NewCreatePostgreSQLDatabaseValue PostgreSQL作成用パラメーター @@ -267,10 +325,6 @@ func CreateNewDatabase(values *CreateDatabaseValue) *Database { DatabaseTitle: values.DatabaseTitle, // DatabaseVersion DatabaseVersion: values.DatabaseVersion, - // ReplicaUser - // ReplicaUser: values.ReplicaUser, - // ReplicaPassword - // ReplicaPassword: values.AdminPassword, }, }, // Plan @@ -288,6 +342,8 @@ func CreateNewDatabase(values *CreateDatabaseValue) *Database { Rotate: 8, // Time Time: values.BackupTime, + // DayOfWeek + DayOfWeek: values.BackupDayOfWeek, }, // Common Common: &DatabaseCommonSetting{ @@ -297,13 +353,19 @@ func CreateNewDatabase(values *CreateDatabaseValue) *Database { UserPassword: values.UserPassword, // SourceNetwork SourceNetwork: SourceNetwork(values.SourceNetwork), - // ServicePort - ServicePort: values.ServicePort, }, }, }, } + if values.ServicePort > 0 { + db.Settings.DBConf.Common.ServicePort = json.Number(fmt.Sprintf("%d", values.ServicePort)) + } + + if !values.EnableBackup { + db.Settings.DBConf.Backup = nil + } + db.Remark.Switch = &ApplianceRemarkSwitch{ // ID ID: values.SwitchID, @@ -323,11 +385,19 @@ func CreateNewDatabase(values *CreateDatabaseValue) *Database { db.Settings.DBConf.Common.WebUI = values.WebUI } + if values.ReplicaPassword != "" { + db.Settings.DBConf.Common.ReplicaUser = "replica" + db.Settings.DBConf.Common.ReplicaPassword = values.ReplicaPassword + db.Settings.DBConf.Replication = &DatabaseReplicationSetting{ + Model: DatabaseReplicationModelMasterSlave, + } + } + return db } -// CloneNewDatabase データベース作成 -func CloneNewDatabase(values *CreateDatabaseValue) *Database { +// NewSlaveDatabaseValue スレーブ向けパラメータ作成 +func NewSlaveDatabaseValue(values *SlaveDatabaseValue) *Database { db := &Database{ // Appliance Appliance: &Appliance{ @@ -363,32 +433,34 @@ func CloneNewDatabase(values *CreateDatabaseValue) *Database { DBConf: &DatabaseCommonRemarks{ // Common Common: &DatabaseCommonRemark{ - DatabaseName: values.DatabaseName, + // DatabaseName + DatabaseName: values.DatabaseName, + // DatabaseVersion DatabaseVersion: values.DatabaseVersion, }, }, // Plan - propPlanID: propPlanID{Plan: &Resource{ID: int64(values.Plan)}}, - SourceAppliance: values.SourceAppliance, + propPlanID: propPlanID{Plan: &Resource{ID: int64(values.Plan)}}, }, // Settings Settings: &DatabaseSettings{ // DBConf DBConf: &DatabaseSetting{ - // Backup - Backup: &DatabaseBackupSetting{ - // Rotate - // Rotate: values.BackupRotate, - Rotate: 8, - // Time - Time: values.BackupTime, - }, // Common Common: &DatabaseCommonSetting{ - // SourceNetwork - SourceNetwork: SourceNetwork(values.SourceNetwork), - // ServicePort - ServicePort: values.ServicePort, + // DefaultUser + DefaultUser: values.DefaultUser, + // UserPassword + UserPassword: values.UserPassword, + }, + // Replication + Replication: &DatabaseReplicationSetting{ + Model: DatabaseReplicationModelAsyncReplica, + Appliance: NewResource(values.MasterApplianceID), + IPAddress: values.MasterIPAddress, + Port: values.MasterPort, + User: "replica", + Password: values.ReplicaPassword, }, }, }, @@ -409,10 +481,6 @@ func CloneNewDatabase(values *CreateDatabaseValue) *Database { map[string]interface{}{"IPAddress": values.IPAddress1}, } - if values.WebUI { - db.Settings.DBConf.Common.WebUI = values.WebUI - } - return db } @@ -433,3 +501,8 @@ func (s *Database) DeleteSourceNetwork(nw string) { } s.Settings.DBConf.Common.SourceNetwork = SourceNetwork(res) } + +// IsReplicationMaster レプリケーションが有効かつマスターとして構成されているか +func (s *Database) IsReplicationMaster() bool { + return s.Settings.DBConf.Replication != nil && s.Settings.DBConf.Replication.Model == DatabaseReplicationModelMasterSlave +} diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/disk.go b/vendor/github.com/sacloud/libsacloud/sacloud/disk.go index 9deb36ef..4f42a984 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/disk.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/disk.go @@ -4,22 +4,23 @@ import "fmt" // Disk ディスク type Disk struct { - *Resource // ID - propAvailability // 有功状態 - propName // 名称 - propDescription // 説明 - propSizeMB // サイズ(MB単位) - propMigratedMB // コピー済みデータサイズ(MB単位) - propCopySource // コピー元情報 - propJobStatus // マイグレーションジョブステータス - propBundleInfo // バンドル情報 - propServer // サーバー - propIcon // アイコン - propTags // タグ - propCreatedAt // 作成日時 - propPlanID // プランID - propDiskConnection // ディスク接続情報 - propDistantFrom // ストレージ隔離対象ディスク + *Resource // ID + propAvailability // 有功状態 + propName // 名称 + propDescription // 説明 + propSizeMB // サイズ(MB単位) + propMigratedMB // コピー済みデータサイズ(MB単位) + propCopySource // コピー元情報 + propJobStatus // マイグレーションジョブステータス + propBundleInfo // バンドル情報 + propServer // サーバー + propIcon // アイコン + propTags // タグ + propCreatedAt // 作成日時 + propPlanID // プランID + propDiskConnection // ディスク接続情報 + propDistantFrom // ストレージ隔離対象ディスク + Generation PlanGenerations `json:",omitempty"` // プラン世代 ReinstallCount int `json:",omitempty"` // 再インストール回数 diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/dns.go b/vendor/github.com/sacloud/libsacloud/sacloud/dns.go index edfbbc63..35182573 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/dns.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/dns.go @@ -50,7 +50,9 @@ func CreateNewDNS(zoneName string) *DNS { Class: "dns", }, Settings: DNSSettings{ - DNS: DNSRecordSets{}, + DNS: DNSRecordSets{ + ResourceRecordSets: []DNSRecordSet{}, + }, }, } } @@ -135,7 +137,9 @@ func (d *DNS) AddRecord(record *DNSRecordSet) { // ClearRecords レコード クリア func (d *DNS) ClearRecords() { - d.Settings.DNS = DNSRecordSets{} + d.Settings.DNS = DNSRecordSets{ + ResourceRecordSets: []DNSRecordSet{}, + } } // DNSRecordSets DNSレコード設定リスト diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/mobile_gateway.go b/vendor/github.com/sacloud/libsacloud/sacloud/mobile_gateway.go index bad13871..b3b8ccbf 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/mobile_gateway.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/mobile_gateway.go @@ -301,3 +301,54 @@ func (m *MobileGatewaySIMRoutes) FindSIMRoute(simID int64, prefix string) (int, } return -1, nil } + +// TrafficStatus トラフィックコントロール 当月通信量 +type TrafficStatus struct { + UplinkBytes uint64 `json:"uplink_bytes,omitempty"` + DownlinkBytes uint64 `json:"downlink_bytes,omitempty"` + TrafficShaping bool `json:"traffic_shaping"` // 帯域制限 +} + +// UnmarshalJSON JSONアンマーシャル(uint64文字列対応) +func (s *TrafficStatus) UnmarshalJSON(data []byte) error { + tmp := &struct { + UplinkBytes string `json:"uplink_bytes,omitempty"` + DownlinkBytes string `json:"downlink_bytes,omitempty"` + TrafficShaping bool `json:"traffic_shaping"` + }{} + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + var err error + s.UplinkBytes, err = strconv.ParseUint(tmp.UplinkBytes, 10, 64) + if err != nil { + return err + } + s.DownlinkBytes, err = strconv.ParseUint(tmp.DownlinkBytes, 10, 64) + if err != nil { + return err + } + s.TrafficShaping = tmp.TrafficShaping + return nil +} + +// TrafficMonitoringConfig トラフィックコントロール 設定 +type TrafficMonitoringConfig struct { + TrafficQuotaInMB int `json:"traffic_quota_in_mb"` + BandWidthLimitInKbps int `json:"bandwidth_limit_in_kbps"` + EMailConfig *TrafficMonitoringNotifyEmail `json:"email_config"` + SlackConfig *TrafficMonitoringNotifySlack `json:"slack_config"` + AutoTrafficShaping bool `json:"auto_traffic_shaping"` +} + +// TrafficMonitoringNotifyEmail トラフィックコントロール通知設定 +type TrafficMonitoringNotifyEmail struct { + Enabled bool `json:"enabled"` // 有効/無効 +} + +// TrafficMonitoringNotifySlack トラフィックコントロール通知設定 +type TrafficMonitoringNotifySlack struct { + Enabled bool `json:"enabled"` // 有効/無効 + IncomingWebhooksURL string `json:"slack_url,omitempty"` // Slack通知の場合のWebhook URL +} diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archive_ostype.go b/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archive_ostype.go index c9deb698..51c63ce0 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archive_ostype.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archive_ostype.go @@ -27,6 +27,10 @@ const ( SophosUTM // FreeBSD OS種別:FreeBSD FreeBSD + // Netwiser OS種別: Netwiser Virtual Edition + Netwiser + // OPNsense OS種別: OPNsense + OPNsense // Windows2012 OS種別:Windows Server 2012 R2 Datacenter Edition Windows2012 // Windows2012RDS OS種別:Windows Server 2012 R2 for RDS @@ -57,6 +61,7 @@ const ( var OSTypeShortNames = []string{ "centos", "centos6", "ubuntu", "debian", "vyos", "coreos", "rancheros", "kusanagi", "sophos-utm", "freebsd", + "netwiser", "opnsense", "windows2012", "windows2012-rds", "windows2012-rds-office", "windows2016", "windows2016-rds", "windows2016-rds-office", "windows2016-sql-web", "windows2016-sql-standard", "windows2016-sql-standard-all", @@ -109,6 +114,10 @@ func StrToOSType(osType string) ArchiveOSTypes { return SophosUTM case "freebsd": return FreeBSD + case "netwiser": + return Netwiser + case "opnsense": + return OPNsense case "windows2012": return Windows2012 case "windows2012-rds": diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archiveostypes_string.go b/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archiveostypes_string.go index 3ed2fde3..b59eca73 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archiveostypes_string.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/ostype/archiveostypes_string.go @@ -4,9 +4,9 @@ package ostype import "strconv" -const _ArchiveOSTypes_name = "CentOSCentOS6UbuntuDebianVyOSCoreOSRancherOSKusanagiSophosUTMFreeBSDWindows2012Windows2012RDSWindows2012RDSOfficeWindows2016Windows2016RDSWindows2016RDSOfficeWindows2016SQLServerWebWindows2016SQLServerStandardWindows2016SQLServer2017StandardWindows2016SQLServerStandardAllWindows2016SQLServer2017StandardAllCustom" +const _ArchiveOSTypes_name = "CentOSCentOS6UbuntuDebianVyOSCoreOSRancherOSKusanagiSophosUTMFreeBSDNetwiserOPNsenseWindows2012Windows2012RDSWindows2012RDSOfficeWindows2016Windows2016RDSWindows2016RDSOfficeWindows2016SQLServerWebWindows2016SQLServerStandardWindows2016SQLServer2017StandardWindows2016SQLServerStandardAllWindows2016SQLServer2017StandardAllCustom" -var _ArchiveOSTypes_index = [...]uint16{0, 6, 13, 19, 25, 29, 35, 44, 52, 61, 68, 79, 93, 113, 124, 138, 158, 181, 209, 241, 272, 307, 313} +var _ArchiveOSTypes_index = [...]uint16{0, 6, 13, 19, 25, 29, 35, 44, 52, 61, 68, 76, 84, 95, 109, 129, 140, 154, 174, 197, 225, 257, 288, 323, 329} func (i ArchiveOSTypes) String() string { if i < 0 || i >= ArchiveOSTypes(len(_ArchiveOSTypes_index)-1) { diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/product_server.go b/vendor/github.com/sacloud/libsacloud/sacloud/product_server.go index 9427ec0d..593c342f 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/product_server.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/product_server.go @@ -2,11 +2,12 @@ package sacloud // ProductServer サーバープラン type ProductServer struct { - *Resource // ID - propName // 名称 - propDescription // 説明 - propAvailability // 有功状態 - propCPU // CPUコア数 - propMemoryMB // メモリサイズ(MB単位) - propServiceClass // サービスクラス + *Resource // ID + propName // 名称 + propDescription // 説明 + propAvailability // 有功状態 + propCPU // CPUコア数 + propMemoryMB // メモリサイズ(MB単位) + propServiceClass // サービスクラス + Generation PlanGenerations `json:",omitempty"` // 世代 } diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/prop_memory.go b/vendor/github.com/sacloud/libsacloud/sacloud/prop_memory.go index 5c503a33..da08b53b 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/prop_memory.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/prop_memory.go @@ -17,3 +17,8 @@ func (p *propMemoryMB) GetMemoryGB() int { } return p.MemoryMB / 1024 } + +// SetMemoryGB サイズ(GB単位) 設定 +func (p *propMemoryMB) SetMemoryGB(memoryGB int) { + p.MemoryMB = memoryGB * 1024 +} diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/prop_server_plan.go b/vendor/github.com/sacloud/libsacloud/sacloud/prop_server_plan.go index b5fa3454..3b33862f 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/prop_server_plan.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/prop_server_plan.go @@ -23,6 +23,15 @@ func (p *propServerPlan) SetServerPlanByID(planID string) { p.ServerPlan.Resource = NewResourceByStringID(planID) } +// SetServerPlanByValue サーバープラン設定(値指定) +func (p *propServerPlan) SetServerPlanByValue(cpu int, memoryGB int, gen PlanGenerations) { + plan := &ProductServer{} + plan.CPU = cpu + plan.SetMemoryGB(memoryGB) + plan.Generation = gen + p.ServerPlan = plan +} + // GetCPU CPUコア数 取得 func (p *propServerPlan) GetCPU() int { if p.ServerPlan == nil { @@ -49,3 +58,7 @@ func (p *propServerPlan) GetMemoryGB() int { return p.ServerPlan.GetMemoryGB() } + +func (p *propServerPlan) SetMemoryGB(memoryGB int) { + p.ServerPlan.SetMemoryGB(memoryGB) +} diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/prop_wait_disk_migration.go b/vendor/github.com/sacloud/libsacloud/sacloud/prop_wait_disk_migration.go new file mode 100644 index 00000000..ed2da17b --- /dev/null +++ b/vendor/github.com/sacloud/libsacloud/sacloud/prop_wait_disk_migration.go @@ -0,0 +1,16 @@ +package sacloud + +// propWaitDiskMigration ディスク作成待ちフラグ内包型 +type propWaitDiskMigration struct { + WaitDiskMigration bool `json:",omitempty"` +} + +// GetWaitDiskMigration ディスク作成待ちフラグ 取得 +func (p *propWaitDiskMigration) GetWaitDiskMigration() bool { + return p.WaitDiskMigration +} + +// SetWaitDiskMigration ディスク作成待ちフラグ 設定 +func (p *propWaitDiskMigration) SetWaitDiskMigration(f bool) { + p.WaitDiskMigration = f +} diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/server.go b/vendor/github.com/sacloud/libsacloud/sacloud/server.go index 37ed43b6..477966dc 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/server.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/server.go @@ -21,6 +21,7 @@ type Server struct { propIcon // アイコン propTags // タグ propCreatedAt // 作成日時 + propWaitDiskMigration // サーバ作成時のディスク作成待ち } // DNSServers サーバの所属するリージョンの推奨ネームサーバリスト @@ -43,10 +44,13 @@ func (s *Server) IPAddress() string { // Gateway デフォルトゲートウェイアドレス func (s *Server) Gateway() string { - if len(s.Interfaces) == 0 || s.Interfaces[0].Switch == nil || s.Interfaces[0].Switch.UserSubnet == nil { + if len(s.Interfaces) == 0 || s.Interfaces[0].Switch == nil { return "" } - return s.Interfaces[0].Switch.UserSubnet.DefaultRoute + if s.Interfaces[0].Switch.UserSubnet != nil { + return s.Interfaces[0].Switch.UserSubnet.DefaultRoute + } + return s.Interfaces[0].Switch.Subnet.DefaultRoute } // DefaultRoute デフォルトゲートウェイアドレス(Gatewayのエイリアス) @@ -56,10 +60,13 @@ func (s *Server) DefaultRoute() string { // NetworkMaskLen サーバの1番目のNIC(eth0)のネットワークマスク長 func (s *Server) NetworkMaskLen() int { - if len(s.Interfaces) == 0 || s.Interfaces[0].Switch == nil || s.Interfaces[0].Switch.UserSubnet == nil { + if len(s.Interfaces) == 0 || s.Interfaces[0].Switch == nil { return 0 } - return s.Interfaces[0].Switch.UserSubnet.NetworkMaskLen + if s.Interfaces[0].Switch.UserSubnet != nil { + return s.Interfaces[0].Switch.UserSubnet.NetworkMaskLen + } + return s.Interfaces[0].Switch.Subnet.NetworkMaskLen } // NetworkAddress サーバの1番目のNIC(eth0)のネットワークアドレス diff --git a/vendor/github.com/sacloud/libsacloud/sacloud/sim.go b/vendor/github.com/sacloud/libsacloud/sacloud/sim.go index e513a1e8..e4367d0f 100644 --- a/vendor/github.com/sacloud/libsacloud/sacloud/sim.go +++ b/vendor/github.com/sacloud/libsacloud/sacloud/sim.go @@ -2,6 +2,7 @@ package sacloud import ( "encoding/json" + "strconv" "strings" "time" ) @@ -49,8 +50,8 @@ type SIMInfo struct { // SIMTrafficBytes 当月通信量 type SIMTrafficBytes struct { - UplinkBytes int64 `json:"uplink_bytes,omitempty"` - DownlinkBytes int64 `json:"downlink_bytes,omitempty"` + UplinkBytes uint64 `json:"uplink_bytes,omitempty"` + DownlinkBytes uint64 `json:"downlink_bytes,omitempty"` } // UnmarshalJSON JSONアンマーシャル(配列、オブジェクトが混在するためここで対応) @@ -60,15 +61,22 @@ func (s *SIMTrafficBytes) UnmarshalJSON(data []byte) error { return nil } tmp := &struct { - UplinkBytes int64 `json:"uplink_bytes,omitempty"` - DownlinkBytes int64 `json:"downlink_bytes,omitempty"` + UplinkBytes string `json:"uplink_bytes,omitempty"` + DownlinkBytes string `json:"downlink_bytes,omitempty"` }{} if err := json.Unmarshal(data, &tmp); err != nil { return err } - s.UplinkBytes = tmp.UplinkBytes - s.DownlinkBytes = tmp.DownlinkBytes + var err error + s.UplinkBytes, err = strconv.ParseUint(tmp.UplinkBytes, 10, 64) + if err != nil { + return err + } + s.DownlinkBytes, err = strconv.ParseUint(tmp.DownlinkBytes, 10, 64) + if err != nil { + return err + } return nil } diff --git a/vendor/gopkg.in/square/go-jose.v2/jwk.go b/vendor/gopkg.in/square/go-jose.v2/jwk.go index 8081d5ad..2dfd5d50 100644 --- a/vendor/gopkg.in/square/go-jose.v2/jwk.go +++ b/vendor/gopkg.in/square/go-jose.v2/jwk.go @@ -489,6 +489,16 @@ func fromRsaPrivateKey(rsa *rsa.PrivateKey) (*rawJSONWebKey, error) { raw.P = newBuffer(rsa.Primes[0].Bytes()) raw.Q = newBuffer(rsa.Primes[1].Bytes()) + if rsa.Precomputed.Dp != nil { + raw.Dp = newBuffer(rsa.Precomputed.Dp.Bytes()) + } + if rsa.Precomputed.Dq != nil { + raw.Dq = newBuffer(rsa.Precomputed.Dq.Bytes()) + } + if rsa.Precomputed.Qinv != nil { + raw.Qi = newBuffer(rsa.Precomputed.Qinv.Bytes()) + } + return raw, nil }