Upgrade server dependencies, manage them with govendor
This commit is contained in:
parent
ebee2746d6
commit
971278e7e5
1748 changed files with 196165 additions and 194500 deletions
21
vendor/github.com/xenolf/lego/LICENSE
generated
vendored
Normal file
21
vendor/github.com/xenolf/lego/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Sebastian Erhart
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
16
vendor/github.com/xenolf/lego/acme/challenges.go
generated
vendored
Normal file
16
vendor/github.com/xenolf/lego/acme/challenges.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
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")
|
||||
// TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni
|
||||
// Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge
|
||||
TLSSNI01 = Challenge("tls-sni-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")
|
||||
)
|
493
vendor/github.com/xenolf/lego/acme/client.go
generated
vendored
493
vendor/github.com/xenolf/lego/acme/client.go
generated
vendored
|
@ -1,8 +1,8 @@
|
|||
// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers.
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
@ -11,6 +11,7 @@ import (
|
|||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -22,6 +23,16 @@ var (
|
|||
Logger *log.Logger
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
// logf writes a log entry. It uses Logger if not
|
||||
// nil, otherwise it uses the default log.Logger.
|
||||
func logf(format string, args ...interface{}) {
|
||||
|
@ -37,7 +48,7 @@ func logf(format string, args ...interface{}) {
|
|||
type User interface {
|
||||
GetEmail() string
|
||||
GetRegistration() *RegistrationResource
|
||||
GetPrivateKey() *rsa.PrivateKey
|
||||
GetPrivateKey() crypto.PrivateKey
|
||||
}
|
||||
|
||||
// Interface for all challenge solvers to implement.
|
||||
|
@ -49,27 +60,23 @@ 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
|
||||
keyBits int
|
||||
issuerCert []byte
|
||||
solvers map[string]solver
|
||||
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. It will
|
||||
// generate private keys for certificates of size keyBits.
|
||||
func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
|
||||
// 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")
|
||||
}
|
||||
|
||||
if err := privKey.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid private key: %v", err)
|
||||
}
|
||||
|
||||
var dir directory
|
||||
if _, err := getJSON(caDirURL, &dir); err != nil {
|
||||
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
||||
|
@ -93,25 +100,42 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
|
|||
// 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 := make(map[string]solver)
|
||||
solvers["http-01"] = &httpChallenge{jws: jws, validate: validate}
|
||||
solvers["tls-sni-01"] = &tlsSNIChallenge{jws: jws, validate: validate}
|
||||
solvers := make(map[Challenge]solver)
|
||||
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
|
||||
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}}
|
||||
|
||||
return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
|
||||
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 TLSSNI01:
|
||||
c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p}
|
||||
case DNS01:
|
||||
c.solvers[challenge] = &dnsChallenge{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["http-01"]; ok {
|
||||
chlng.(*httpChallenge).iface = host
|
||||
chlng.(*httpChallenge).port = port
|
||||
if chlng, ok := c.solvers[HTTP01]; ok {
|
||||
chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -120,27 +144,26 @@ func (c *Client) SetHTTPAddress(iface string) error {
|
|||
// 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-SNI provider previously set by calling
|
||||
// c.SetChallengeProvider with the default TLS-SNI 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["tls-sni-01"]; ok {
|
||||
chlng.(*tlsSNIChallenge).iface = host
|
||||
chlng.(*tlsSNIChallenge).port = port
|
||||
if chlng, ok := c.solvers[TLSSNI01]; ok {
|
||||
chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExcludeChallenges explicitly removes challenges from the pool for solving.
|
||||
func (c *Client) ExcludeChallenges(challenges []string) {
|
||||
func (c *Client) ExcludeChallenges(challenges []Challenge) {
|
||||
// Loop through all challenges and delete the requested one if found.
|
||||
for _, challenge := range challenges {
|
||||
if _, ok := c.solvers[challenge]; ok {
|
||||
delete(c.solvers, challenge)
|
||||
}
|
||||
delete(c.solvers, challenge)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,15 +184,31 @@ func (c *Client) Register() (*RegistrationResource, error) {
|
|||
}
|
||||
|
||||
var serverReg Registration
|
||||
var regURI string
|
||||
hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
remoteErr, ok := err.(RemoteError)
|
||||
if ok && remoteErr.StatusCode == 409 {
|
||||
regURI = hdr.Get("Location")
|
||||
regMsg = registrationMessage{
|
||||
Resource: "reg",
|
||||
}
|
||||
if hdr, err = postJSON(c.jws, regURI, regMsg, &serverReg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reg := &RegistrationResource{Body: serverReg}
|
||||
|
||||
links := parseLinks(hdr["Link"])
|
||||
reg.URI = hdr.Get("Location")
|
||||
|
||||
if regURI == "" {
|
||||
regURI = hdr.Get("Location")
|
||||
}
|
||||
reg.URI = regURI
|
||||
if links["terms-of-service"] != "" {
|
||||
reg.TosURL = links["terms-of-service"]
|
||||
}
|
||||
|
@ -183,15 +222,138 @@ func (c *Client) Register() (*RegistrationResource, error) {
|
|||
return reg, 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")
|
||||
}
|
||||
logf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
|
||||
|
||||
regMsg := registrationMessage{
|
||||
Resource: "reg",
|
||||
Delete: true,
|
||||
}
|
||||
|
||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return 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 (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
|
||||
logf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
|
||||
|
||||
regMsg := registrationMessage{
|
||||
Resource: "reg",
|
||||
}
|
||||
|
||||
var serverReg Registration
|
||||
hdr, err := postJSON(c.jws, c.user.GetRegistration().URI, regMsg, &serverReg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reg := &RegistrationResource{Body: serverReg}
|
||||
|
||||
links := parseLinks(hdr["Link"])
|
||||
// Location: header is not returned so this needs to be populated off of
|
||||
// existing URI
|
||||
reg.URI = c.user.GetRegistration().URI
|
||||
if links["terms-of-service"] != "" {
|
||||
reg.TosURL = links["terms-of-service"]
|
||||
}
|
||||
|
||||
if links["next"] != "" {
|
||||
reg.NewAuthzURL = links["next"]
|
||||
} else {
|
||||
return nil, errors.New("acme: No new-authz link in response to registration query")
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// AgreeToTOS updates the Client registration and sends the agreement to
|
||||
// the server.
|
||||
func (c *Client) AgreeToTOS() error {
|
||||
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL
|
||||
c.user.GetRegistration().Body.Resource = "reg"
|
||||
reg := c.user.GetRegistration()
|
||||
|
||||
reg.Body.Agreement = c.user.GetRegistration().TosURL
|
||||
reg.Body.Resource = "reg"
|
||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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, map[string]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 {
|
||||
logf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
} else {
|
||||
logf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
|
||||
}
|
||||
|
||||
challenges, failures := c.getChallenges(domains)
|
||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
||||
if len(failures) > 0 {
|
||||
return CertificateResource{}, failures
|
||||
}
|
||||
|
||||
errs := c.solveChallenges(challenges)
|
||||
// If any challenge fails - return. Do not generate partial SAN certificates.
|
||||
if len(errs) > 0 {
|
||||
return CertificateResource{}, errs
|
||||
}
|
||||
|
||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
cert, err := c.requestCertificateForCsr(challenges, bundle, csr.Raw, nil)
|
||||
if err != nil {
|
||||
for _, chln := range challenges {
|
||||
failures[chln.Domain] = err
|
||||
}
|
||||
}
|
||||
|
||||
// Add the CSR to the certificate so that it can be used for renewals.
|
||||
cert.CSR = pemEncode(&csr)
|
||||
|
||||
return cert, failures
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -201,7 +363,7 @@ func (c *Client) AgreeToTOS() error {
|
|||
// 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) (CertificateResource, map[string]error) {
|
||||
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, map[string]error) {
|
||||
if bundle {
|
||||
logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
|
||||
} else {
|
||||
|
@ -222,7 +384,7 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto
|
|||
|
||||
logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
|
||||
|
||||
cert, err := c.requestCertificate(challenges, bundle, privKey)
|
||||
cert, err := c.requestCertificate(challenges, bundle, privKey, mustStaple)
|
||||
if err != nil {
|
||||
for _, chln := range challenges {
|
||||
failures[chln.Domain] = err
|
||||
|
@ -258,7 +420,7 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
|
|||
// 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 bool) (CertificateResource, error) {
|
||||
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)
|
||||
|
@ -275,48 +437,16 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif
|
|||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
||||
logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
|
||||
|
||||
// The first step of renewal is to check if we get a renewed cert
|
||||
// directly from the cert URL.
|
||||
resp, err := httpGet(cert.CertURL)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
serverCertBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
serverCert, err := x509.ParseCertificate(serverCertBytes)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
// If the server responds with a different certificate we are effectively renewed.
|
||||
// TODO: Further test if we can actually use the new certificate (Our private key works)
|
||||
if !x509Cert.Equal(serverCert) {
|
||||
logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
|
||||
issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we need the issuer certificate.
|
||||
if bundle {
|
||||
// The issuer certificate link is always supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
|
||||
} else {
|
||||
// Success - append the issuer cert to the issued cert.
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
issuedCert = append(issuedCert, issuerCert...)
|
||||
cert.Certificate = issuedCert
|
||||
}
|
||||
// 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, err := pemDecodeTox509CSR(cert.CSR)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
cert.Certificate = issuedCert
|
||||
return cert, nil
|
||||
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
|
||||
return newCert, failures[cert.Domain]
|
||||
}
|
||||
|
||||
var privKey crypto.PrivateKey
|
||||
|
@ -327,7 +457,22 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif
|
|||
}
|
||||
}
|
||||
|
||||
newCert, failures := c.ObtainCertificate([]string{cert.Domain}, bundle, privKey)
|
||||
var domains []string
|
||||
var failures map[string]error
|
||||
// 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, failures := c.ObtainCertificate(domains, bundle, privKey, mustStaple)
|
||||
return newCert, failures[cert.Domain]
|
||||
}
|
||||
|
||||
|
@ -337,6 +482,11 @@ func (c *Client) solveChallenges(challenges []authorizationResource) map[string]
|
|||
// loop through the resources, basically through the domains.
|
||||
failures := make(map[string]error)
|
||||
for _, authz := range challenges {
|
||||
if authz.Body.Status == "valid" {
|
||||
// Boulder might recycle recent validated authz (see issue #267)
|
||||
logf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Domain)
|
||||
continue
|
||||
}
|
||||
// no solvers - no solving
|
||||
if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil {
|
||||
for i, solver := range solvers {
|
||||
|
@ -379,7 +529,11 @@ func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver
|
|||
func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) {
|
||||
resc, errc := make(chan authorizationResource), make(chan domainError)
|
||||
|
||||
delay := time.Second / overallRequestLimit
|
||||
|
||||
for _, domain := range domains {
|
||||
time.Sleep(delay)
|
||||
|
||||
go func(domain string) {
|
||||
authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}}
|
||||
var authz authorization
|
||||
|
@ -392,6 +546,7 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
|
|||
links := parseLinks(hdr["Link"])
|
||||
if links["next"] == "" {
|
||||
logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain)
|
||||
errc <- domainError{Domain: domain, Error: errors.New("Server did not provide next link to proceed")}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -417,39 +572,57 @@ func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[s
|
|||
}
|
||||
}
|
||||
|
||||
logAuthz(challenges)
|
||||
|
||||
close(resc)
|
||||
close(errc)
|
||||
|
||||
return challenges, failures
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) {
|
||||
func logAuthz(authz []authorizationResource) {
|
||||
for _, auth := range authz {
|
||||
logf("[INFO][%s] AuthURL: %s", auth.Domain, auth.AuthURL)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (CertificateResource, error) {
|
||||
if len(authz) == 0 {
|
||||
return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!")
|
||||
}
|
||||
|
||||
commonName := authz[0]
|
||||
var err error
|
||||
if privKey == nil {
|
||||
privKey, err = generatePrivateKey(rsakey, c.keyBits)
|
||||
privKey, err = generatePrivateKey(c.keyType)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// determine certificate name(s) based on the authorization resources
|
||||
commonName := authz[0]
|
||||
var san []string
|
||||
var authURLs []string
|
||||
for _, auth := range authz[1:] {
|
||||
san = append(san, auth.Domain)
|
||||
authURLs = append(authURLs, auth.AuthURL)
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san)
|
||||
csr, err := generateCsr(privKey, commonName.Domain, san, mustStaple)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
return c.requestCertificateForCsr(authz, bundle, csr, pemEncode(privKey))
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificateForCsr(authz []authorizationResource, bundle bool, csr []byte, privateKeyPem []byte) (CertificateResource, error) {
|
||||
commonName := authz[0]
|
||||
|
||||
var authURLs []string
|
||||
for _, auth := range authz[1:] {
|
||||
authURLs = append(authURLs, auth.AuthURL)
|
||||
}
|
||||
|
||||
csrString := base64.URLEncoding.EncodeToString(csr)
|
||||
jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs})
|
||||
if err != nil {
|
||||
|
@ -461,89 +634,108 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
privateKeyPem := pemEncode(privKey)
|
||||
cerRes := CertificateResource{
|
||||
certRes := CertificateResource{
|
||||
Domain: commonName.Domain,
|
||||
CertURL: resp.Header.Get("Location"),
|
||||
PrivateKey: privateKeyPem}
|
||||
PrivateKey: privateKeyPem,
|
||||
}
|
||||
|
||||
for {
|
||||
switch resp.StatusCode {
|
||||
case 201, 202:
|
||||
cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
// The server returns a body with a length of zero if the
|
||||
// certificate was not ready at the time this request completed.
|
||||
// Otherwise the body is the certificate.
|
||||
if len(cert) > 0 {
|
||||
|
||||
cerRes.CertStableURL = resp.Header.Get("Content-Location")
|
||||
|
||||
issuedCert := pemEncode(derCertificateBytes(cert))
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we need the issuer certificate.
|
||||
if bundle {
|
||||
// The issuer certificate link is always supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
|
||||
} else {
|
||||
// Success - append the issuer cert to the issued cert.
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
issuedCert = append(issuedCert, issuerCert...)
|
||||
}
|
||||
}
|
||||
|
||||
cerRes.Certificate = issuedCert
|
||||
logf("[INFO][%s] Server responded with a certificate.", commonName.Domain)
|
||||
return cerRes, nil
|
||||
}
|
||||
|
||||
// The certificate was granted but is not yet issued.
|
||||
// Check retry-after and loop.
|
||||
ra := resp.Header.Get("Retry-After")
|
||||
retryAfter, err := strconv.Atoi(ra)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter)
|
||||
time.Sleep(time.Duration(retryAfter) * time.Second)
|
||||
|
||||
break
|
||||
default:
|
||||
return CertificateResource{}, handleHTTPError(resp)
|
||||
maxChecks := 1000
|
||||
for i := 0; i < maxChecks; i++ {
|
||||
done, err := c.checkCertResponse(resp, &certRes, bundle)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
resp, err = httpGet(cerRes.CertURL)
|
||||
if done {
|
||||
break
|
||||
}
|
||||
if i == maxChecks-1 {
|
||||
return CertificateResource{}, fmt.Errorf("polled for certificate %d times; giving up", i)
|
||||
}
|
||||
resp, err = httpGet(certRes.CertURL)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return certRes, nil
|
||||
}
|
||||
|
||||
// getIssuerCertificate requests the issuer certificate and caches it for
|
||||
// subsequent requests.
|
||||
// checkCertResponse checks resp to see if a certificate is contained in the
|
||||
// response, and if so, loads it into certRes and returns true. If the cert
|
||||
// is not yet ready, it returns false. This function honors the waiting period
|
||||
// required by the Retry-After header of the response, if specified. This
|
||||
// function may read from resp.Body but does NOT close it. 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(resp *http.Response, certRes *CertificateResource, bundle bool) (bool, error) {
|
||||
switch resp.StatusCode {
|
||||
case 201, 202:
|
||||
cert, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The server returns a body with a length of zero if the
|
||||
// certificate was not ready at the time this request completed.
|
||||
// Otherwise the body is the certificate.
|
||||
if len(cert) > 0 {
|
||||
certRes.CertStableURL = resp.Header.Get("Content-Location")
|
||||
certRes.AccountRef = c.user.GetRegistration().URI
|
||||
|
||||
issuedCert := pemEncode(derCertificateBytes(cert))
|
||||
|
||||
// The issuer certificate link is always supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
logf("[WARNING][%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 {
|
||||
issuedCert = append(issuedCert, issuerCert...)
|
||||
}
|
||||
}
|
||||
|
||||
certRes.Certificate = issuedCert
|
||||
certRes.IssuerCertificate = issuerCert
|
||||
logf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// The certificate was granted but is not yet issued.
|
||||
// Check retry-after and loop.
|
||||
ra := resp.Header.Get("Retry-After")
|
||||
retryAfter, err := strconv.Atoi(ra)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", certRes.Domain, retryAfter)
|
||||
time.Sleep(time.Duration(retryAfter) * time.Second)
|
||||
|
||||
return false, nil
|
||||
default:
|
||||
return false, handleHTTPError(resp)
|
||||
}
|
||||
}
|
||||
|
||||
// getIssuerCertificate requests the issuer certificate
|
||||
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
||||
logf("[INFO] acme: Requesting issuer cert from %s", url)
|
||||
if c.issuerCert != nil {
|
||||
return c.issuerCert, nil
|
||||
}
|
||||
|
||||
resp, err := httpGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
|
||||
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -553,7 +745,6 @@ func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
c.issuerCert = issuerBytes
|
||||
return issuerBytes, err
|
||||
}
|
||||
|
||||
|
|
196
vendor/github.com/xenolf/lego/acme/client_test.go
generated
vendored
196
vendor/github.com/xenolf/lego/acme/client_test.go
generated
vendored
|
@ -1,196 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
keyBits := 32 // small value keeps test fast
|
||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
t.Fatal("Could not generate test key:", err)
|
||||
}
|
||||
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{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
||||
w.Write(data)
|
||||
}))
|
||||
|
||||
client, err := NewClient(ts.URL, user, keyBits)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create client: %v", err)
|
||||
}
|
||||
|
||||
if client.jws == nil {
|
||||
t.Fatalf("Expected client.jws to not be nil")
|
||||
}
|
||||
if expected, actual := key, client.jws.privKey; actual != expected {
|
||||
t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual)
|
||||
}
|
||||
|
||||
if client.keyBits != keyBits {
|
||||
t.Errorf("Expected keyBits to be %d but was %d", keyBits, client.keyBits)
|
||||
}
|
||||
|
||||
if expected, actual := 2, len(client.solvers); actual != expected {
|
||||
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientOptPort(t *testing.T) {
|
||||
keyBits := 32 // small value keeps test fast
|
||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
t.Fatal("Could not generate test key:", err)
|
||||
}
|
||||
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{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"})
|
||||
w.Write(data)
|
||||
}))
|
||||
|
||||
optPort := "1234"
|
||||
optHost := ""
|
||||
client, err := NewClient(ts.URL, user, keyBits)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create client: %v", err)
|
||||
}
|
||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
||||
|
||||
httpSolver, ok := client.solvers["http-01"].(*httpChallenge)
|
||||
if !ok {
|
||||
t.Fatal("Expected http-01 solver to be httpChallenge type")
|
||||
}
|
||||
if httpSolver.jws != client.jws {
|
||||
t.Error("Expected http-01 to have same jws as client")
|
||||
}
|
||||
if httpSolver.port != optPort {
|
||||
t.Errorf("Expected http-01 to have port %s but was %s", optPort, httpSolver.port)
|
||||
}
|
||||
if httpSolver.iface != optHost {
|
||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, httpSolver.iface)
|
||||
}
|
||||
|
||||
httpsSolver, ok := client.solvers["tls-sni-01"].(*tlsSNIChallenge)
|
||||
if !ok {
|
||||
t.Fatal("Expected tls-sni-01 solver to be httpChallenge type")
|
||||
}
|
||||
if httpsSolver.jws != client.jws {
|
||||
t.Error("Expected tls-sni-01 to have same jws as client")
|
||||
}
|
||||
if httpsSolver.port != optPort {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, httpSolver.port)
|
||||
}
|
||||
if httpsSolver.port != optPort {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, httpSolver.iface)
|
||||
}
|
||||
|
||||
// test setting different host
|
||||
optHost = "127.0.0.1"
|
||||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
||||
|
||||
if httpSolver.iface != optHost {
|
||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, httpSolver.iface)
|
||||
}
|
||||
if httpsSolver.port != optPort {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, httpSolver.iface)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
var statuses []string
|
||||
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 "HEAD":
|
||||
case "POST":
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
||||
|
||||
case "GET":
|
||||
st := statuses[0]
|
||||
statuses = statuses[1:]
|
||||
writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"})
|
||||
|
||||
default:
|
||||
http.Error(w, r.Method, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
|
||||
|
||||
tsts := []struct {
|
||||
name string
|
||||
statuses []string
|
||||
want string
|
||||
}{
|
||||
{"POST-unexpected", []string{"weird"}, "unexpected"},
|
||||
{"POST-valid", []string{"valid"}, ""},
|
||||
{"POST-invalid", []string{"invalid"}, "Error Detail"},
|
||||
{"GET-unexpected", []string{"pending", "weird"}, "unexpected"},
|
||||
{"GET-valid", []string{"pending", "valid"}, ""},
|
||||
{"GET-invalid", []string{"pending", "invalid"}, "Error Detail"},
|
||||
}
|
||||
|
||||
for _, tst := range tsts {
|
||||
statuses = tst.statuses
|
||||
if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" {
|
||||
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
|
||||
} else if err != nil && !strings.Contains(err.Error(), tst.want) {
|
||||
t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(j *jws, domain, uri string, chlng 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() *rsa.PrivateKey { return u.privatekey }
|
137
vendor/github.com/xenolf/lego/acme/crypto.go
generated
vendored
137
vendor/github.com/xenolf/lego/acme/crypto.go
generated
vendored
|
@ -10,7 +10,6 @@ import (
|
|||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -21,16 +20,22 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"encoding/asn1"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
type keyType int
|
||||
// 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 (
|
||||
eckey keyType = iota
|
||||
rsakey
|
||||
EC256 = KeyType("P256")
|
||||
EC384 = KeyType("P384")
|
||||
RSA2048 = KeyType("2048")
|
||||
RSA4096 = KeyType("4096")
|
||||
RSA8192 = KeyType("8192")
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -44,6 +49,12 @@ const (
|
|||
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
|
||||
|
@ -56,14 +67,22 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We only got one certificate, means we have no issuer certificate - get it.
|
||||
// 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(certificates[0].IssuingCertificateURL) == 0 {
|
||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
||||
return nil, nil, errors.New("no issuing certificate URL")
|
||||
}
|
||||
|
||||
resp, err := httpGet(certificates[0].IssuingCertificateURL[0])
|
||||
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -83,11 +102,6 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
// We want it ordered right SRV CRT -> CA
|
||||
certificates = append(certificates, issuerCert)
|
||||
}
|
||||
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the cert and issuer cert out of it,
|
||||
// which should always be the last two certificates.
|
||||
issuedCert := certificates[0]
|
||||
issuerCert := certificates[1]
|
||||
|
||||
// Finally kick off the OCSP request.
|
||||
|
@ -109,19 +123,20 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
if ocspRes.Certificate == nil {
|
||||
err = ocspRes.CheckSignatureFrom(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 := keyAsJWK(key)
|
||||
jwk := keyAsJWK(publicKey)
|
||||
if jwk == nil {
|
||||
return "", errors.New("Could not generate JWK from key.")
|
||||
}
|
||||
|
@ -140,39 +155,6 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
|||
return token + "." + keyThumb, nil
|
||||
}
|
||||
|
||||
// Derive the shared secret according to acme spec 5.6
|
||||
func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label string) []byte {
|
||||
// Derive Z from the private and public keys according to SEC 1 Ver. 2.0 - 3.3.1
|
||||
Z, _ := priv.PublicKey.ScalarMult(pub.X, pub.Y, priv.D.Bytes())
|
||||
|
||||
if len(Z.Bytes())+len(label)+4 > 384 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if outLen < 384*(2^32-1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Derive the shared secret key using the ANS X9.63 KDF - SEC 1 Ver. 2.0 - 3.6.1
|
||||
hasher := sha3.New384()
|
||||
buffer := make([]byte, outLen)
|
||||
bufferLen := 0
|
||||
for i := 0; i < outLen/384; i++ {
|
||||
hasher.Reset()
|
||||
|
||||
// Ki = Hash(Z || Counter || [SharedInfo])
|
||||
hasher.Write(Z.Bytes())
|
||||
binary.Write(hasher, binary.BigEndian, i)
|
||||
hasher.Write([]byte(label))
|
||||
|
||||
hash := hasher.Sum(nil)
|
||||
copied := copy(buffer[bufferLen:], hash)
|
||||
bufferLen += copied
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -214,18 +196,25 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) {
|
||||
switch t {
|
||||
case eckey:
|
||||
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 rsakey:
|
||||
return rsa.GenerateKey(rand.Reader, keyLength)
|
||||
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: %d", t)
|
||||
return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
|
||||
}
|
||||
|
||||
func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byte, error) {
|
||||
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: domain,
|
||||
|
@ -236,15 +225,28 @@ func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byt
|
|||
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)}
|
||||
break
|
||||
case *x509.CertificateRequest:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: key.Raw}
|
||||
break
|
||||
case derCertificateBytes:
|
||||
pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))}
|
||||
}
|
||||
|
@ -270,6 +272,19 @@ func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
|
|||
return x509.ParseCertificate(pemBlock.Bytes)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
92
vendor/github.com/xenolf/lego/acme/crypto_test.go
generated
vendored
92
vendor/github.com/xenolf/lego/acme/crypto_test.go
generated
vendored
|
@ -1,92 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGeneratePrivateKey(t *testing.T) {
|
||||
key, err := generatePrivateKey(rsakey, 32)
|
||||
if err != nil {
|
||||
t.Error("Error generating private key:", err)
|
||||
}
|
||||
if key == nil {
|
||||
t.Error("Expected key to not be nil, but it was")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCSR(t *testing.T) {
|
||||
key, err := generatePrivateKey(rsakey, 512)
|
||||
if err != nil {
|
||||
t.Fatal("Error generating private key:", err)
|
||||
}
|
||||
|
||||
csr, err := generateCsr(key.(*rsa.PrivateKey), "fizz.buzz", nil)
|
||||
if err != nil {
|
||||
t.Error("Error generating CSR:", err)
|
||||
}
|
||||
if csr == nil || len(csr) == 0 {
|
||||
t.Error("Expected CSR with data, but it was nil or length 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPEMEncode(t *testing.T) {
|
||||
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
|
||||
|
||||
reader := MockRandReader{b: buf}
|
||||
key, err := rsa.GenerateKey(reader, 32)
|
||||
if err != nil {
|
||||
t.Fatal("Error generating private key:", err)
|
||||
}
|
||||
|
||||
data := pemEncode(key)
|
||||
|
||||
if data == nil {
|
||||
t.Fatal("Expected result to not be nil, but it was")
|
||||
}
|
||||
if len(data) != 127 {
|
||||
t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPEMCertExpiration(t *testing.T) {
|
||||
privKey, err := generatePrivateKey(rsakey, 2048)
|
||||
if err != nil {
|
||||
t.Fatal("Error generating private key:", err)
|
||||
}
|
||||
|
||||
expiration := time.Now().Add(365)
|
||||
expiration = expiration.Round(time.Second)
|
||||
certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com")
|
||||
if err != nil {
|
||||
t.Fatal("Error generating cert:", err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBufferString("TestingRSAIsSoMuchFun")
|
||||
|
||||
// Some random string should return an error.
|
||||
if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil {
|
||||
t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime)
|
||||
}
|
||||
|
||||
// A DER encoded certificate should return an error.
|
||||
if _, err := GetPEMCertExpiration(certBytes); err == nil {
|
||||
t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.")
|
||||
}
|
||||
|
||||
// A PEM encoded certificate should work ok.
|
||||
pemCert := pemEncode(derCertificateBytes(certBytes))
|
||||
if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) {
|
||||
t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err)
|
||||
}
|
||||
}
|
||||
|
||||
type MockRandReader struct {
|
||||
b *bytes.Buffer
|
||||
}
|
||||
|
||||
func (r MockRandReader) Read(p []byte) (int, error) {
|
||||
return r.b.Read(p)
|
||||
}
|
304
vendor/github.com/xenolf/lego/acme/dns_challenge.go
generated
vendored
304
vendor/github.com/xenolf/lego/acme/dns_challenge.go
generated
vendored
|
@ -1 +1,305 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
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{}
|
||||
)
|
||||
|
||||
const defaultResolvConf = "/etc/resolv.conf"
|
||||
|
||||
var defaultNameservers = []string{
|
||||
"google-public-dns-a.google.com:53",
|
||||
"google-public-dns-b.google.com:53",
|
||||
}
|
||||
|
||||
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
|
||||
keyAuthSha := base64.URLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
||||
value = strings.TrimRight(keyAuthSha, "=")
|
||||
ttl = 120
|
||||
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
|
||||
}
|
||||
|
||||
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
||||
logf("[INFO][%s] acme: Trying 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)
|
||||
}
|
||||
defer func() {
|
||||
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up %s: %v ", domain, err)
|
||||
}
|
||||
}()
|
||||
|
||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
||||
|
||||
logf("[INFO][%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 = 60*time.Second, 2*time.Second
|
||||
}
|
||||
|
||||
err = WaitFor(timeout, interval, func() (bool, error) {
|
||||
return PreCheckDNS(fqdn, value)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: 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", ns)
|
||||
}
|
||||
}
|
||||
|
||||
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 suceeds, 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) {
|
||||
// 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:]
|
||||
// Give up if we have reached the TLD
|
||||
if isTLD(domain) {
|
||||
break
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
func isTLD(domain string) bool {
|
||||
publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(domain))
|
||||
if publicsuffix == UnFqdn(domain) {
|
||||
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
|
||||
}
|
||||
|
|
53
vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
generated
vendored
Normal file
53
vendor/github.com/xenolf/lego/acme/dns_challenge_manual.go
generated
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
package acme
|
||||
|
||||
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, ttl := DNS01Record(domain, keyAuth)
|
||||
dnsRecord := fmt.Sprintf(dnsTemplate, fqdn, ttl, value)
|
||||
|
||||
authZone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
|
||||
logf("[INFO] acme: %s", dnsRecord)
|
||||
logf("[INFO] 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
|
||||
}
|
||||
|
||||
logf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
|
||||
logf("[INFO] acme: %s", dnsRecord)
|
||||
return nil
|
||||
}
|
29
vendor/github.com/xenolf/lego/acme/error.go
generated
vendored
29
vendor/github.com/xenolf/lego/acme/error.go
generated
vendored
|
@ -3,12 +3,14 @@ package acme
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
tosAgreementError = "Must agree to subscriber agreement before any further actions"
|
||||
invalidNonceError = "JWS has invalid anti-replay nonce"
|
||||
)
|
||||
|
||||
// RemoteError is the base type for all errors specific to the ACME protocol.
|
||||
|
@ -29,6 +31,12 @@ 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
|
||||
|
@ -52,10 +60,19 @@ func (c challengeError) Error() string {
|
|||
|
||||
func handleHTTPError(resp *http.Response) error {
|
||||
var errorDetail RemoteError
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err := decoder.Decode(&errorDetail)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "application/json" || 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
|
||||
|
@ -65,6 +82,10 @@ func handleHTTPError(resp *http.Response) error {
|
|||
return TOSError{errorDetail}
|
||||
}
|
||||
|
||||
if errorDetail.StatusCode == http.StatusBadRequest && strings.HasPrefix(errorDetail.Detail, invalidNonceError) {
|
||||
return NonceError{errorDetail}
|
||||
}
|
||||
|
||||
return errorDetail
|
||||
}
|
||||
|
||||
|
|
61
vendor/github.com/xenolf/lego/acme/http.go
generated
vendored
61
vendor/github.com/xenolf/lego/acme/http.go
generated
vendored
|
@ -8,11 +8,15 @@ import (
|
|||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserAgent, if non-empty, will be tacked onto the User-Agent string in requests.
|
||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
||||
var UserAgent string
|
||||
|
||||
// HTTPClient is an HTTP client with a reasonable timeout value.
|
||||
var HTTPClient = http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
const (
|
||||
// defaultGoUserAgent is the Go HTTP package user agent string. Too
|
||||
// bad it isn't exported. If it changes, we should update it here, too.
|
||||
|
@ -27,16 +31,16 @@ const (
|
|||
func httpHead(url string) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to head %q: %v", url, err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
client := http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
if resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -45,13 +49,12 @@ func httpHead(url string) (resp *http.Response, err error) {
|
|||
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to post %q: %v", url, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", bodyType)
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
client := http.Client{}
|
||||
return client.Do(req)
|
||||
return HTTPClient.Do(req)
|
||||
}
|
||||
|
||||
// httpGet performs a GET request with a proper User-Agent string.
|
||||
|
@ -59,12 +62,11 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response,
|
|||
func httpGet(url string) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to get %q: %v", url, err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
client := http.Client{}
|
||||
return client.Do(req)
|
||||
return HTTPClient.Do(req)
|
||||
}
|
||||
|
||||
// getJSON performs an HTTP GET request and parses the response body
|
||||
|
@ -72,7 +74,7 @@ func httpGet(url string) (resp *http.Response, err error) {
|
|||
func getJSON(uri string, respBody interface{}) (http.Header, error) {
|
||||
resp, err := httpGet(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get %q: %v", uri, err)
|
||||
return nil, fmt.Errorf("failed to get json %q: %v", uri, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
@ -95,10 +97,41 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
return resp.Header, handleHTTPError(resp)
|
||||
|
||||
err := handleHTTPError(resp)
|
||||
|
||||
switch err.(type) {
|
||||
|
||||
case NonceError:
|
||||
|
||||
// Retry once if the nonce was invalidated
|
||||
|
||||
retryResp, err := j.post(uri, jsonBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
|
||||
}
|
||||
|
||||
defer retryResp.Body.Close()
|
||||
|
||||
if retryResp.StatusCode >= http.StatusBadRequest {
|
||||
return retryResp.Header, handleHTTPError(retryResp)
|
||||
}
|
||||
|
||||
if respBody == nil {
|
||||
return retryResp.Header, nil
|
||||
}
|
||||
|
||||
return retryResp.Header, json.NewDecoder(retryResp.Body).Decode(respBody)
|
||||
|
||||
default:
|
||||
return resp.Header, err
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if respBody == nil {
|
||||
|
|
65
vendor/github.com/xenolf/lego/acme/http_challenge.go
generated
vendored
65
vendor/github.com/xenolf/lego/acme/http_challenge.go
generated
vendored
|
@ -2,73 +2,40 @@ package acme
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"log"
|
||||
)
|
||||
|
||||
type httpChallenge struct {
|
||||
jws *jws
|
||||
validate validateFunc
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
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 {
|
||||
|
||||
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
|
||||
|
||||
s.done = make(chan bool)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Allow for CLI port override
|
||||
port := "80"
|
||||
if s.port != "" {
|
||||
port = s.port
|
||||
}
|
||||
|
||||
iface := ""
|
||||
if s.iface != "" {
|
||||
iface = s.iface
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort(iface, port))
|
||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
}
|
||||
|
||||
path := "/.well-known/acme-challenge/" + chlng.Token
|
||||
|
||||
go s.serve(listener, path, keyAuth, domain)
|
||||
|
||||
err = s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
listener.Close()
|
||||
<-s.done
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *httpChallenge) serve(listener net.Listener, path, keyAuth, domain string) {
|
||||
// The handler validates the HOST header and request type.
|
||||
// For validation it then writes the token the server returned with the challenge
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte(keyAuth))
|
||||
logf("[INFO][%s] Served key authentication", domain)
|
||||
} else {
|
||||
logf("[INFO] Received request for domain %s with method %s", r.Host, r.Method)
|
||||
w.Write([]byte("TEST"))
|
||||
defer func() {
|
||||
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error cleaning up: %v", domain, err)
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
http.Serve(listener, mux)
|
||||
s.done <- true
|
||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
||||
|
|
79
vendor/github.com/xenolf/lego/acme/http_challenge_server.go
generated
vendored
Normal file
79
vendor/github.com/xenolf/lego/acme/http_challenge_server.go
generated
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
||||
// It may be instantiated without using the NewHTTPProviderServer function if
|
||||
// you want only to use the default values.
|
||||
type HTTPProviderServer struct {
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewHTTPProviderServer creates a new HTTPProviderServer 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}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if s.port == "" {
|
||||
s.port = "80"
|
||||
}
|
||||
|
||||
var err error
|
||||
s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not start HTTP server for challenge -> %v", err)
|
||||
}
|
||||
|
||||
s.done = make(chan bool)
|
||||
go s.serve(domain, token, keyAuth)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
|
||||
func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
s.listener.Close()
|
||||
<-s.done
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
||||
path := HTTP01ChallengePath(token)
|
||||
|
||||
// The handler validates the HOST header and request type.
|
||||
// For validation it then writes the token the server returned with the challenge
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.Host, domain) && r.Method == "GET" {
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte(keyAuth))
|
||||
logf("[INFO][%s] Served key authentication", domain)
|
||||
} else {
|
||||
logf("[WARN] Received request for domain %s with method %s but the domain did not match any challenge. Please ensure your are passing the HOST header properly.", r.Host, r.Method)
|
||||
w.Write([]byte("TEST"))
|
||||
}
|
||||
})
|
||||
|
||||
httpServer := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
// Once httpServer is shut down we don't want any lingering
|
||||
// connections, so disable KeepAlives.
|
||||
httpServer.SetKeepAlivesEnabled(false)
|
||||
httpServer.Serve(s.listener)
|
||||
s.done <- true
|
||||
}
|
56
vendor/github.com/xenolf/lego/acme/http_challenge_test.go
generated
vendored
56
vendor/github.com/xenolf/lego/acme/http_challenge_test.go
generated
vendored
|
@ -1,56 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPChallenge(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
clientChallenge := challenge{Type: "http-01", Token: "http1"}
|
||||
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
|
||||
}
|
||||
solver := &httpChallenge{jws: j, validate: mockValidate, port: "23457"}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
||||
t.Errorf("Solve error: got %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 128)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
clientChallenge := challenge{Type: "http-01", Token: "http2"}
|
||||
solver := &httpChallenge{jws: j, validate: stubValidate, port: "123456"}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||
t.Errorf("Solve error: got %v, want error", err)
|
||||
} else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
|
||||
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
|
||||
}
|
||||
}
|
100
vendor/github.com/xenolf/lego/acme/http_test.go
generated
vendored
100
vendor/github.com/xenolf/lego/acme/http_test.go
generated
vendored
|
@ -1,100 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPHeadUserAgent(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()
|
||||
|
||||
_, err := httpHead(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if method != "HEAD" {
|
||||
t.Errorf("Expected method to be HEAD, got %s", method)
|
||||
}
|
||||
if !strings.Contains(ua, ourUserAgent) {
|
||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPGetUserAgent(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()
|
||||
|
||||
res, err := httpGet(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
if method != "GET" {
|
||||
t.Errorf("Expected method to be GET, got %s", method)
|
||||
}
|
||||
if !strings.Contains(ua, ourUserAgent) {
|
||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPPostUserAgent(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()
|
||||
|
||||
res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
if method != "POST" {
|
||||
t.Errorf("Expected method to be POST, got %s", method)
|
||||
}
|
||||
if !strings.Contains(ua, ourUserAgent) {
|
||||
t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAgent(t *testing.T) {
|
||||
ua := userAgent()
|
||||
|
||||
if !strings.Contains(ua, defaultGoUserAgent) {
|
||||
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
|
||||
}
|
||||
if !strings.Contains(ua, ourUserAgent) {
|
||||
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
|
||||
}
|
||||
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()
|
||||
if !strings.Contains(ua, defaultGoUserAgent) {
|
||||
t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua)
|
||||
}
|
||||
if !strings.Contains(ua, ourUserAgent) {
|
||||
t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua)
|
||||
}
|
||||
if !strings.Contains(ua, UserAgent) {
|
||||
t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua)
|
||||
}
|
||||
}
|
112
vendor/github.com/xenolf/lego/acme/jws.go
generated
vendored
112
vendor/github.com/xenolf/lego/acme/jws.go
generated
vendored
|
@ -2,18 +2,21 @@ package acme
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/square/go-jose"
|
||||
"gopkg.in/square/go-jose.v1"
|
||||
)
|
||||
|
||||
type jws struct {
|
||||
directoryURL string
|
||||
privKey *rsa.PrivateKey
|
||||
nonces []string
|
||||
privKey crypto.PrivateKey
|
||||
nonces nonceManager
|
||||
}
|
||||
|
||||
func keyAsJWK(key interface{}) *jose.JsonWebKey {
|
||||
|
@ -28,66 +31,101 @@ func keyAsJWK(key interface{}) *jose.JsonWebKey {
|
|||
}
|
||||
}
|
||||
|
||||
// Posts a JWS signed message to the specified URL
|
||||
// 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(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
||||
}
|
||||
|
||||
resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize())))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to HTTP POST to %s -> %s", url, err.Error())
|
||||
}
|
||||
|
||||
j.getNonceFromResponse(resp)
|
||||
nonce, nonceErr := getNonceFromResponse(resp)
|
||||
if nonceErr == nil {
|
||||
j.nonces.Push(nonce)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
||||
// TODO: support other algorithms - RS512
|
||||
signer, err := jose.NewSigner(jose.RS256, j.privKey)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(alg, j.privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to create jose signer -> %s", err.Error())
|
||||
}
|
||||
signer.SetNonceSource(j)
|
||||
|
||||
signed, err := signer.Sign(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Failed to sign content -> %s", err.Error())
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
func (j *jws) getNonceFromResponse(resp *http.Response) error {
|
||||
func (j *jws) Nonce() (string, error) {
|
||||
if nonce, ok := j.nonces.Pop(); ok {
|
||||
return nonce, nil
|
||||
}
|
||||
|
||||
return getNonce(j.directoryURL)
|
||||
}
|
||||
|
||||
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 "", fmt.Errorf("Server did not respond with a proper nonce header.")
|
||||
}
|
||||
|
||||
j.nonces = append(j.nonces, nonce)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *jws) getNonce() error {
|
||||
resp, err := httpHead(j.directoryURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return j.getNonceFromResponse(resp)
|
||||
}
|
||||
|
||||
func (j *jws) Nonce() (string, error) {
|
||||
nonce := ""
|
||||
if len(j.nonces) == 0 {
|
||||
err := j.getNonce()
|
||||
if err != nil {
|
||||
return nonce, err
|
||||
}
|
||||
}
|
||||
|
||||
nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1]
|
||||
return nonce, nil
|
||||
}
|
||||
|
|
52
vendor/github.com/xenolf/lego/acme/messages.go
generated
vendored
52
vendor/github.com/xenolf/lego/acme/messages.go
generated
vendored
|
@ -3,7 +3,7 @@ package acme
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/square/go-jose"
|
||||
"gopkg.in/square/go-jose.v1"
|
||||
)
|
||||
|
||||
type directory struct {
|
||||
|
@ -13,33 +13,22 @@ type directory struct {
|
|||
RevokeCertURL string `json:"revoke-cert"`
|
||||
}
|
||||
|
||||
type recoveryKeyMessage struct {
|
||||
Length int `json:"length,omitempty"`
|
||||
Client jose.JsonWebKey `json:"client,omitempty"`
|
||||
Server jose.JsonWebKey `json:"client,omitempty"`
|
||||
}
|
||||
|
||||
type registrationMessage struct {
|
||||
Resource string `json:"resource"`
|
||||
Contact []string `json:"contact"`
|
||||
// RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
|
||||
Delete bool `json:"delete,omitempty"`
|
||||
}
|
||||
|
||||
// Registration is returned by the ACME server after the registration
|
||||
// The client implementation should save this registration somewhere.
|
||||
type Registration struct {
|
||||
Resource string `json:"resource,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Key struct {
|
||||
Kty string `json:"kty"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
} `json:"key"`
|
||||
Contact []string `json:"contact"`
|
||||
Agreement string `json:"agreement,omitempty"`
|
||||
Authorizations string `json:"authorizations,omitempty"`
|
||||
Certificates string `json:"certificates,omitempty"`
|
||||
// RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Key jose.JsonWebKey `json:"key"`
|
||||
Contact []string `json:"contact"`
|
||||
Agreement string `json:"agreement,omitempty"`
|
||||
Authorizations string `json:"authorizations,omitempty"`
|
||||
Certificates string `json:"certificates,omitempty"`
|
||||
}
|
||||
|
||||
// RegistrationResource represents all important informations about a registration
|
||||
|
@ -82,7 +71,7 @@ type validationRecord struct {
|
|||
|
||||
type challenge struct {
|
||||
Resource string `json:"resource,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Type Challenge `json:"type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
|
@ -105,14 +94,17 @@ type revokeCertMessage struct {
|
|||
}
|
||||
|
||||
// CertificateResource represents a CA issued certificate.
|
||||
// PrivateKey and Certificate are both already PEM encoded
|
||||
// and can be directly written to disk. Certificate may
|
||||
// be a certificate bundle, depending on the options supplied
|
||||
// to create it.
|
||||
// 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"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
Certificate []byte `json:"-"`
|
||||
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:"-"`
|
||||
}
|
||||
|
|
28
vendor/github.com/xenolf/lego/acme/provider.go
generated
vendored
Normal file
28
vendor/github.com/xenolf/lego/acme/provider.go
generated
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
package acme
|
||||
|
||||
import "time"
|
||||
|
||||
// ChallengeProvider 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 {
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
Timeout() (timeout, interval time.Duration)
|
||||
}
|
60
vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
generated
vendored
60
vendor/github.com/xenolf/lego/acme/tls_sni_challenge.go
generated
vendored
|
@ -6,15 +6,13 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"log"
|
||||
)
|
||||
|
||||
type tlsSNIChallenge struct {
|
||||
jws *jws
|
||||
validate validateFunc
|
||||
iface string
|
||||
port string
|
||||
provider ChallengeProvider
|
||||
}
|
||||
|
||||
func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
||||
|
@ -24,64 +22,46 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
|||
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, &t.jws.privKey.PublicKey)
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := t.generateCertificate(keyAuth)
|
||||
err = t.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
}
|
||||
|
||||
// Allow for CLI port override
|
||||
port := "443"
|
||||
if t.port != "" {
|
||||
port = t.port
|
||||
}
|
||||
|
||||
iface := ""
|
||||
if t.iface != "" {
|
||||
iface = t.iface
|
||||
}
|
||||
|
||||
tlsConf := new(tls.Config)
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
|
||||
listener, err := tls.Listen("tcp", net.JoinHostPort(iface, port), tlsConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
go http.Serve(listener, nil)
|
||||
|
||||
defer func() {
|
||||
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
log.Printf("[%s] error cleaning up: %v", domain, err)
|
||||
}
|
||||
}()
|
||||
return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
||||
|
||||
func (t *tlsSNIChallenge) generateCertificate(keyAuth string) (tls.Certificate, error) {
|
||||
|
||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||
|
||||
// TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge
|
||||
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, string, error) {
|
||||
// generate a new RSA key for the certificates
|
||||
tempPrivKey, err := generatePrivateKey(rsakey, 2048)
|
||||
tempPrivKey, err := generatePrivateKey(RSA2048)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return tls.Certificate{}, "", err
|
||||
}
|
||||
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
|
||||
rsaPrivPEM := pemEncode(rsaPrivKey)
|
||||
|
||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return tls.Certificate{}, "", err
|
||||
}
|
||||
|
||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return tls.Certificate{}, "", err
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
return certificate, domain, nil
|
||||
}
|
||||
|
|
62
vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
generated
vendored
Normal file
62
vendor/github.com/xenolf/lego/acme/tls_sni_challenge_server.go
generated
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
|
||||
// It may be instantiated without using the NewTLSProviderServer function if
|
||||
// you want only to use the default values.
|
||||
type TLSProviderServer struct {
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTLSProviderServer creates a new TLSProviderServer 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 NewTLSProviderServer(iface, port string) *TLSProviderServer {
|
||||
return &TLSProviderServer{iface: iface, port: port}
|
||||
}
|
||||
|
||||
// Present makes the keyAuth available as a cert
|
||||
func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
|
||||
if s.port == "" {
|
||||
s.port = "443"
|
||||
}
|
||||
|
||||
cert, _, err := TLSSNI01ChallengeCert(keyAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlsConf := new(tls.Config)
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
|
||||
s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err)
|
||||
}
|
||||
|
||||
s.done = make(chan bool)
|
||||
go func() {
|
||||
http.Serve(s.listener, nil)
|
||||
s.done <- true
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp closes the HTTP server.
|
||||
func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
s.listener.Close()
|
||||
<-s.done
|
||||
return nil
|
||||
}
|
64
vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
generated
vendored
64
vendor/github.com/xenolf/lego/acme/tls_sni_challenge_test.go
generated
vendored
|
@ -1,64 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTLSSNIChallenge(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni1"}
|
||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
||||
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Expected to connect to challenge server without an error. %s", err.Error())
|
||||
}
|
||||
|
||||
// Expect the server to only return one certificate
|
||||
connState := conn.ConnectionState()
|
||||
if count := len(connState.PeerCertificates); count != 1 {
|
||||
t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count)
|
||||
}
|
||||
|
||||
remoteCert := connState.PeerCertificates[0]
|
||||
if count := len(remoteCert.DNSNames); count != 1 {
|
||||
t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count)
|
||||
}
|
||||
|
||||
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
|
||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
||||
|
||||
if remoteCert.DNSNames[0] != domain {
|
||||
t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, port: "23457"}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
||||
t.Errorf("Solve error: got %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 128)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
clientChallenge := challenge{Type: "tls-sni-01", Token: "tlssni2"}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, port: "123456"}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||
t.Errorf("Solve error: got %v, want error", err)
|
||||
} else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) {
|
||||
t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want)
|
||||
}
|
||||
}
|
29
vendor/github.com/xenolf/lego/acme/utils.go
generated
vendored
Normal file
29
vendor/github.com/xenolf/lego/acme/utils.go
generated
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
|
||||
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
|
||||
var lastErr string
|
||||
timeup := time.After(timeout)
|
||||
for {
|
||||
select {
|
||||
case <-timeup:
|
||||
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
|
||||
default:
|
||||
}
|
||||
|
||||
stop, err := f()
|
||||
if stop {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err.Error()
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue