Update vendor dir

This commit is contained in:
Ken-Håvard Lieng 2018-11-10 08:04:36 +01:00
parent 9c9b05d479
commit a219e689c1
124 changed files with 9583 additions and 2752 deletions

View file

@ -7,7 +7,11 @@ const (
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
TLSALPN01 = Challenge("tls-alpn-01")
)

View file

@ -5,6 +5,7 @@ import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
@ -25,6 +26,9 @@ const (
// “new-reg”, “new-authz” and “new-cert” endpoints. From the documentation the
// limitation is 20 requests per second, but using 20 as value doesn't work but 18 do
overallRequestLimit = 18
statusValid = "valid"
statusInvalid = "invalid"
)
// User interface is to be implemented by users of this library.
@ -40,6 +44,17 @@ type solver interface {
Solve(challenge challenge, domain string) error
}
// Interface for challenges like dns, where we can set a record in advance for ALL challenges.
// This saves quite a bit of time vs creating the records and solving them serially.
type preSolver interface {
PreSolve(challenge challenge, domain string) error
}
// Interface for challenges like dns, where we can solve all the challenges before to delete them.
type cleanup interface {
CleanUp(challenge challenge, domain string) error
}
type validateFunc func(j *jws, domain, uri string, chlng challenge) error
// Client is the user-friendy way to ACME
@ -81,8 +96,10 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*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[Challenge]solver)
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
solvers := map[Challenge]solver{
HTTP01: &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}},
TLSALPN01: &tlsALPNChallenge{jws: jws, validate: validate, provider: &TLSALPNProviderServer{}},
}
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
}
@ -94,8 +111,10 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider)
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
case DNS01:
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
case TLSALPN01:
c.solvers[challenge] = &tlsALPNChallenge{jws: c.jws, validate: validate, provider: p}
default:
return fmt.Errorf("Unknown challenge %v", challenge)
return fmt.Errorf("unknown challenge %v", challenge)
}
return nil
}
@ -119,6 +138,24 @@ func (c *Client) SetHTTPAddress(iface string) error {
return nil
}
// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
// If this option is not used, the default port 443 and all interfaces will be used.
// To only specify a port and no interface use the ":port" notation.
//
// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling
// c.SetChallengeProvider with the default TLS-ALPN challenge provider.
func (c *Client) SetTLSAddress(iface string) error {
host, port, err := net.SplitHostPort(iface)
if err != nil {
return err
}
if chlng, ok := c.solvers[TLSALPN01]; ok {
chlng.(*tlsALPNChallenge).provider = NewTLSALPNProviderServer(host, port)
}
return nil
}
// ExcludeChallenges explicitly removes challenges from the pool for solving.
func (c *Client) ExcludeChallenges(challenges []Challenge) {
// Loop through all challenges and delete the requested one if found.
@ -142,7 +179,7 @@ func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
if c == nil || c.user == nil {
return nil, errors.New("acme: cannot register a nil client or user")
}
log.Printf("[INFO] acme: Registering account for %s", c.user.GetEmail())
log.Infof("acme: Registering account for %s", c.user.GetEmail())
accMsg := accountMessage{}
if c.user.GetEmail() != "" {
@ -176,7 +213,7 @@ func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string,
if c == nil || c.user == nil {
return nil, errors.New("acme: cannot register a nil client or user")
}
log.Printf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail())
log.Infof("acme: Registering account (EAB) for %s", c.user.GetEmail())
accMsg := accountMessage{}
if c.user.GetEmail() != "" {
@ -222,7 +259,7 @@ func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string,
// ResolveAccountByKey will attempt to look up an account using the given account key
// and return its registration resource.
func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) {
log.Printf("[INFO] acme: Trying to resolve account by key")
log.Infof("acme: Trying to resolve account by key")
acc := accountMessage{OnlyReturnExisting: true}
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, acc, nil)
@ -251,7 +288,7 @@ func (c *Client) DeleteRegistration() error {
if c == nil || c.user == nil {
return errors.New("acme: cannot unregister a nil client or user")
}
log.Printf("[INFO] acme: Deleting account for %s", c.user.GetEmail())
log.Infof("acme: Deleting account for %s", c.user.GetEmail())
accMsg := accountMessage{
Status: "deactivated",
@ -271,7 +308,7 @@ func (c *Client) QueryRegistration() (*RegistrationResource, error) {
return nil, errors.New("acme: cannot query the registration of a nil client or user")
}
// Log the URL here instead of the email as the email may not be set
log.Printf("[INFO] acme: Querying account for %s", c.user.GetRegistration().URI)
log.Infof("acme: Querying account for %s", c.user.GetRegistration().URI)
accMsg := accountMessage{}
@ -317,9 +354,9 @@ DNSNames:
}
if bundle {
log.Printf("[INFO][%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
log.Infof("[%s] acme: Obtaining bundled SAN certificate given a CSR", strings.Join(domains, ", "))
} else {
log.Printf("[INFO][%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
log.Infof("[%s] acme: Obtaining SAN certificate given a CSR", strings.Join(domains, ", "))
}
order, err := c.createOrderForIdentifiers(domains)
@ -341,7 +378,7 @@ DNSNames:
return nil, err
}
log.Printf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(ObtainError)
cert, err := c.requestCertificateForCsr(order, bundle, csr.Raw, nil)
@ -351,8 +388,10 @@ DNSNames:
}
}
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals.
cert.CSR = pemEncode(&csr)
}
// do not return an empty failures map, because
// it would still be a non-nil error value
@ -373,13 +412,13 @@ DNSNames:
// the whole certificate will fail.
func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey, mustStaple bool) (*CertificateResource, error) {
if len(domains) == 0 {
return nil, errors.New("No domains to obtain a certificate for")
return nil, errors.New("no domains to obtain a certificate for")
}
if bundle {
log.Printf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
log.Infof("[%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", "))
} else {
log.Printf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
log.Infof("[%s] acme: Obtaining SAN certificate", strings.Join(domains, ", "))
}
order, err := c.createOrderForIdentifiers(domains)
@ -401,7 +440,7 @@ func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto
return nil, err
}
log.Printf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
log.Infof("[%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", "))
failures := make(ObtainError)
cert, err := c.requestCertificateForOrder(order, bundle, privKey, mustStaple)
@ -460,15 +499,15 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
// This is just meant to be informal for the user.
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
log.Printf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
log.Infof("[%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours()))
// We always need to request a new certificate to renew.
// Start by checking to see if the certificate was based off a CSR, and
// use that if it's defined.
if len(cert.CSR) > 0 {
csr, err := pemDecodeTox509CSR(cert.CSR)
if err != nil {
return nil, err
csr, errP := pemDecodeTox509CSR(cert.CSR)
if errP != nil {
return nil, errP
}
newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
return newCert, failures
@ -501,7 +540,6 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle, mustStaple b
}
func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, error) {
var identifiers []identifier
for _, domain := range domains {
identifiers = append(identifiers, identifier{Type: "dns", Value: domain})
@ -525,29 +563,75 @@ func (c *Client) createOrderForIdentifiers(domains []string) (orderResource, err
return orderRes, nil
}
// an authz with the solver we have chosen and the index of the challenge associated with it
type selectedAuthSolver struct {
authz authorization
challengeIndex int
solver solver
}
// Looks through the challenge combinations to find a solvable match.
// Then solves the challenges in series and returns.
func (c *Client) solveChallengeForAuthz(authorizations []authorization) error {
failures := make(ObtainError)
// loop through the resources, basically through the domains.
authSolvers := []*selectedAuthSolver{}
// loop through the resources, basically through the domains. First pass just selects a solver for each authz.
for _, authz := range authorizations {
if authz.Status == "valid" {
if authz.Status == statusValid {
// Boulder might recycle recent validated authz (see issue #267)
log.Printf("[INFO][%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
log.Infof("[%s] acme: Authorization already valid; skipping challenge", authz.Identifier.Value)
continue
}
if i, solvr := c.chooseSolver(authz, authz.Identifier.Value); solvr != nil {
authSolvers = append(authSolvers, &selectedAuthSolver{
authz: authz,
challengeIndex: i,
solver: solvr,
})
} else {
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
}
}
// no solvers - no solving
if i, solver := c.chooseSolver(authz, authz.Identifier.Value); solver != nil {
err := solver.Solve(authz.Challenges[i], authz.Identifier.Value)
if err != nil {
//c.disableAuthz(authz.Identifier)
// for all valid presolvers, first submit the challenges so they have max time to propagate
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if presolver, ok := item.solver.(preSolver); ok {
if err := presolver.PreSolve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
}
} else {
//c.disableAuthz(authz)
failures[authz.Identifier.Value] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Identifier.Value)
}
}
defer func() {
// clean all created TXT records
for _, item := range authSolvers {
if clean, ok := item.solver.(cleanup); ok {
if failures[item.authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
err := clean.CleanUp(item.authz.Challenges[item.challengeIndex], item.authz.Identifier.Value)
if err != nil {
log.Warnf("Error cleaning up %s: %v ", item.authz.Identifier.Value, err)
}
}
}
}()
// finally solve all challenges for real
for _, item := range authSolvers {
authz := item.authz
i := item.challengeIndex
if failures[authz.Identifier.Value] != nil {
// already failed in previous loop
continue
}
if err := item.solver.Solve(authz.Challenges[i], authz.Identifier.Value); err != nil {
failures[authz.Identifier.Value] = err
}
}
@ -565,7 +649,7 @@ func (c *Client) chooseSolver(auth authorization, domain string) (int, solver) {
if solver, ok := c.solvers[Challenge(challenge.Type)]; ok {
return i, solver
}
log.Printf("[INFO][%s] acme: Could not find solver for: %s", domain, challenge.Type)
log.Infof("[%s] acme: Could not find solver for: %s", domain, challenge.Type)
}
return 0, nil
}
@ -617,7 +701,7 @@ func (c *Client) getAuthzForOrder(order orderResource) ([]authorization, error)
func logAuthz(order orderResource) {
for i, auth := range order.Authorizations {
log.Printf("[INFO][%s] AuthURL: %s", order.Identifiers[i].Value, auth)
log.Infof("[%s] AuthURL: %s", order.Identifiers[i].Value, auth)
}
}
@ -640,9 +724,18 @@ func (c *Client) requestCertificateForOrder(order orderResource, bundle bool, pr
// determine certificate name(s) based on the authorization resources
commonName := order.Domains[0]
var san []string
// ACME draft Section 7.4 "Applying for Certificate Issuance"
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
// says:
// Clients SHOULD NOT make any assumptions about the sort order of
// "identifiers" or "authorizations" elements in the returned order
// object.
san := []string{commonName}
for _, auth := range order.Identifiers {
san = append(san, auth.Value)
if auth.Value != commonName {
san = append(san, auth.Value)
}
}
// TODO: should the CSR be customizable?
@ -659,13 +752,13 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
csrString := base64.RawURLEncoding.EncodeToString(csr)
var retOrder orderMessage
_, error := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder)
if error != nil {
return nil, error
_, err := postJSON(c.jws, order.Finalize, csrMessage{Csr: csrString}, &retOrder)
if err != nil {
return nil, err
}
if retOrder.Status == "invalid" {
return nil, error
if retOrder.Status == statusInvalid {
return nil, err
}
certRes := CertificateResource{
@ -674,7 +767,7 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
PrivateKey: privateKeyPem,
}
if retOrder.Status == "valid" {
if retOrder.Status == statusValid {
// if the certificate is available right away, short cut!
ok, err := c.checkCertResponse(retOrder, &certRes, bundle)
if err != nil {
@ -686,25 +779,30 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
}
}
maxChecks := 1000
for i := 0; i < maxChecks; i++ {
_, err := getJSON(order.URL, &retOrder)
if err != nil {
return nil, err
}
done, err := c.checkCertResponse(retOrder, &certRes, bundle)
if err != nil {
return nil, err
}
if done {
break
}
if i == maxChecks-1 {
return nil, fmt.Errorf("polled for certificate %d times; giving up", i)
stopTimer := time.NewTimer(30 * time.Second)
defer stopTimer.Stop()
retryTick := time.NewTicker(500 * time.Millisecond)
defer retryTick.Stop()
for {
select {
case <-stopTimer.C:
return nil, errors.New("certificate polling timed out")
case <-retryTick.C:
_, err := getJSON(order.URL, &retOrder)
if err != nil {
return nil, err
}
done, err := c.checkCertResponse(retOrder, &certRes, bundle)
if err != nil {
return nil, err
}
if done {
return &certRes, nil
}
}
}
return &certRes, nil
}
// checkCertResponse checks to see if the certificate is ready and a link is contained in the
@ -713,9 +811,8 @@ func (c *Client) requestCertificateForCsr(order orderResource, bundle bool, csr
// should already have the Domain (common name) field populated. If bundle is
// true, the certificate will be bundled with the issuer's cert.
func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResource, bundle bool) (bool, error) {
switch order.Status {
case "valid":
case statusValid:
resp, err := httpGet(order.Certificate)
if err != nil {
return false, err
@ -726,15 +823,16 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou
return false, err
}
// The issuer certificate link is always supplied via an "up" link
// in the response headers of a new certificate.
// The issuer certificate link may be supplied via an "up" link
// in the response headers of a new certificate. See
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2
links := parseLinks(resp.Header["Link"])
if link, ok := links["up"]; ok {
issuerCert, err := c.getIssuerCertificate(link)
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
log.Printf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
log.Warnf("[%s] acme: Could not bundle issuer certificate: %v", certRes.Domain, err)
} else {
issuerCert = pemEncode(derCertificateBytes(issuerCert))
@ -746,26 +844,33 @@ func (c *Client) checkCertResponse(order orderMessage, certRes *CertificateResou
certRes.IssuerCertificate = issuerCert
}
} else {
// Get issuerCert from bundled response from Let's Encrypt
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
_, rest := pem.Decode(cert)
if rest != nil {
certRes.IssuerCertificate = rest
}
}
certRes.Certificate = cert
certRes.CertURL = order.Certificate
certRes.CertStableURL = order.Certificate
log.Printf("[INFO][%s] Server responded with a certificate.", certRes.Domain)
log.Infof("[%s] Server responded with a certificate.", certRes.Domain)
return true, nil
case "processing":
return false, nil
case "invalid":
return false, errors.New("Order has invalid state: invalid")
case statusInvalid:
return false, errors.New("order has invalid state: invalid")
default:
return false, nil
}
return false, nil
}
// getIssuerCertificate requests the issuer certificate
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
log.Printf("[INFO] acme: Requesting issuer cert from %s", url)
log.Infof("acme: Requesting issuer cert from %s", url)
resp, err := httpGet(url)
if err != nil {
return nil, err
@ -818,12 +923,12 @@ func validate(j *jws, domain, uri string, c challenge) error {
// Repeatedly check the server for an updated status on our request.
for {
switch chlng.Status {
case "valid":
log.Printf("[INFO][%s] The server validated our request", domain)
case statusValid:
log.Infof("[%s] The server validated our request", domain)
return nil
case "pending":
case "processing":
case "invalid":
case statusInvalid:
return handleChallengeError(chlng)
default:
return errors.New("the server returned an unexpected state")

View file

@ -81,20 +81,20 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
return nil, nil, errors.New("no issuing certificate URL")
}
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, err
resp, errC := httpGet(issuedCert.IssuingCertificateURL[0])
if errC != nil {
return nil, nil, errC
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if err != nil {
return nil, nil, err
issuerBytes, errC := ioutil.ReadAll(limitReader(resp.Body, 1024*1024))
if errC != nil {
return nil, nil, errC
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, nil, err
issuerCert, errC := x509.ParseCertificate(issuerBytes)
if errC != nil {
return nil, nil, errC
}
// Insert it into the slice on position 0
@ -215,9 +215,7 @@ func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
template := x509.CertificateRequest{
Subject: pkix.Name{
CommonName: domain,
},
Subject: pkix.Name{CommonName: domain},
}
if len(san) > 0 {
@ -260,15 +258,6 @@ func pemDecode(data []byte) (*pem.Block, error) {
return pemBlock, nil
}
func pemDecodeTox509(pem []byte) (*x509.Certificate, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
return nil, err
}
return x509.ParseCertificate(pemBlock.Bytes)
}
func pemDecodeTox509CSR(pem []byte) (*x509.CertificateRequest, error) {
pemBlock, err := pemDecode(pem)
if pemBlock == nil {
@ -303,8 +292,8 @@ func getCertExpiration(cert []byte) (time.Time, error) {
return pCert.NotAfter, nil
}
func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain)
func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions)
if err != nil {
return nil, err
}
@ -312,7 +301,7 @@ func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
}
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
@ -334,6 +323,7 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin
KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{domain},
ExtraExtensions: extensions,
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)

View file

@ -7,6 +7,7 @@ import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/miekg/dns"
@ -18,18 +19,30 @@ 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{}
PreCheckDNS preCheckDNSFunc = checkDNSPropagation
fqdnToZone = map[string]string{}
muFqdnToZone sync.Mutex
)
const defaultResolvConf = "/etc/resolv.conf"
const (
// DefaultPropagationTimeout default propagation timeout
DefaultPropagationTimeout = 60 * time.Second
// DefaultPollingInterval default polling interval
DefaultPollingInterval = 2 * time.Second
// DefaultTTL default TTL
DefaultTTL = 120
)
var defaultNameservers = []string{
"google-public-dns-a.google.com:53",
"google-public-dns-b.google.com:53",
}
// RecursiveNameservers are used to pre-check DNS propagations
// RecursiveNameservers are used to pre-check DNS propagation
var RecursiveNameservers = getNameservers(defaultResolvConf, defaultNameservers)
// DNSTimeout is used to override the default DNS timeout of 10 seconds.
@ -59,7 +72,7 @@ func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
// base64URL encoding without padding
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
ttl = 120
ttl = DefaultTTL
fqdn = fmt.Sprintf("_acme-challenge.%s.", domain)
return
}
@ -71,11 +84,13 @@ type dnsChallenge struct {
provider ChallengeProvider
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
log.Printf("[INFO][%s] acme: Trying to solve DNS-01", domain)
// PreSolve just submits the txt record to the dns provider. It does not validate record propagation, or
// do anything at all with the acme server.
func (s *dnsChallenge) PreSolve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
if s.provider == nil {
return errors.New("No DNS Provider configured")
return errors.New("no DNS Provider configured")
}
// Generate the Key Authorization for the challenge
@ -86,25 +101,31 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
err = s.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("Error presenting token: %s", err)
return fmt.Errorf("error presenting token: %s", err)
}
return nil
}
func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
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)
log.Printf("[INFO][%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
log.Infof("[%s] Checking DNS record propagation using %+v", domain, RecursiveNameservers)
var timeout, interval time.Duration
switch provider := s.provider.(type) {
case ChallengeProviderTimeout:
timeout, interval = provider.Timeout()
default:
timeout, interval = 60*time.Second, 2*time.Second
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
}
err = WaitFor(timeout, interval, func() (bool, error) {
@ -117,6 +138,15 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
return s.validate(s.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// CleanUp cleans the challenge
func (s *dnsChallenge) CleanUp(chlng challenge, domain string) error {
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
if err != nil {
return err
}
return s.provider.CleanUp(domain, chlng.Token, keyAuth)
}
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn, value string) (bool, error) {
// Initial attempt to resolve at the recursive NS
@ -124,6 +154,7 @@ func checkDNSPropagation(fqdn, value string) (bool, error) {
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 {
@ -167,7 +198,7 @@ func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, erro
}
if !found {
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
return false, fmt.Errorf("NS %s did not return the expected TXT record [fqdn: %s]", ns, fqdn)
}
}
@ -210,7 +241,7 @@ func lookupNameservers(fqdn string) ([]string, error) {
zone, err := FindZoneByFqdn(fqdn, RecursiveNameservers)
if err != nil {
return nil, fmt.Errorf("Could not determine the zone: %v", err)
return nil, fmt.Errorf("could not determine the zone: %v", err)
}
r, err := dnsQuery(zone, dns.TypeNS, RecursiveNameservers, true)
@ -227,12 +258,15 @@ func lookupNameservers(fqdn string) ([]string, error) {
if len(authoritativeNss) > 0 {
return authoritativeNss, nil
}
return nil, fmt.Errorf("Could not determine authoritative nameservers")
return nil, fmt.Errorf("could not determine authoritative nameservers")
}
// FindZoneByFqdn determines the zone apex for the given fqdn by recursing up the
// domain labels until the nameserver returns a SOA record in the answer section.
func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
muFqdnToZone.Lock()
defer muFqdnToZone.Unlock()
// Do we have it cached?
if zone, ok := fqdnToZone[fqdn]; ok {
return zone, nil
@ -249,7 +283,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
// 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",
return "", fmt.Errorf("unexpected response code '%s' for %s",
dns.RcodeToString[in.Rcode], domain)
}
@ -272,7 +306,7 @@ func FindZoneByFqdn(fqdn string, nameservers []string) (string, error) {
}
}
return "", fmt.Errorf("Could not find the start of authority")
return "", fmt.Errorf("could not find the start of authority")
}
// dnsMsgContainsCNAME checks for a CNAME answer in msg

View file

@ -30,9 +30,9 @@ func (*DNSProviderManual) Present(domain, token, keyAuth string) error {
return err
}
log.Printf("[INFO] acme: Please create the following TXT record in your %s zone:", authZone)
log.Printf("[INFO] acme: %s", dnsRecord)
log.Printf("[INFO] acme: Press 'Enter' when you are done")
log.Infof("acme: Please create the following TXT record in your %s zone:", authZone)
log.Infof("acme: %s", dnsRecord)
log.Infof("acme: Press 'Enter' when you are done")
reader := bufio.NewReader(os.Stdin)
_, _ = reader.ReadString('\n')
@ -49,7 +49,7 @@ func (*DNSProviderManual) CleanUp(domain, token, keyAuth string) error {
return err
}
log.Printf("[INFO] acme: You can now remove this TXT record from your %s zone:", authZone)
log.Printf("[INFO] acme: %s", dnsRecord)
log.Infof("acme: You can now remove this TXT record from your %s zone:", authZone)
log.Infof("acme: %s", dnsRecord)
return nil
}

View file

@ -1,33 +1,45 @@
package acme
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"runtime"
"strings"
"time"
)
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
var UserAgent string
var (
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
UserAgent string
// HTTPClient is an HTTP client with a reasonable timeout value.
var HTTPClient = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
// HTTPClient is an HTTP client with a reasonable timeout value and
// potentially a custom *x509.CertPool based on the caCertificatesEnvVar
// environment variable (see the `initCertPool` function)
HTTPClient = http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
ServerName: os.Getenv(caServerNameEnvVar),
RootCAs: initCertPool(),
},
},
}
)
const (
// defaultGoUserAgent is the Go HTTP package user agent string. Too
@ -36,12 +48,46 @@ const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme"
// caCertificatesEnvVar is the environment variable name that can be used to
// specify the path to PEM encoded CA Certificates that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caCertificatesEnvVar = "LEGO_CA_CERTIFICATES"
// caServerNameEnvVar is the environment variable name that can be used to
// specify the CA server name that can be used to
// authenticate an ACME server with a HTTPS certificate not issued by a CA in
// the system-wide trusted root list.
caServerNameEnvVar = "LEGO_CA_SERVER_NAME"
)
// initCertPool creates a *x509.CertPool populated with the PEM certificates
// found in the filepath specified in the caCertificatesEnvVar OS environment
// variable. If the caCertificatesEnvVar is not set then initCertPool will
// return nil. If there is an error creating a *x509.CertPool from the provided
// caCertificatesEnvVar value then initCertPool will panic.
func initCertPool() *x509.CertPool {
if customCACertsPath := os.Getenv(caCertificatesEnvVar); customCACertsPath != "" {
customCAs, err := ioutil.ReadFile(customCACertsPath)
if err != nil {
panic(fmt.Sprintf("error reading %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(customCAs); !ok {
panic(fmt.Sprintf("error creating x509 cert pool from %s=%q: %v",
caCertificatesEnvVar, customCACertsPath, err))
}
return certPool
}
return nil
}
// httpHead performs a HEAD request with a proper User-Agent string.
// The response body (resp.Body) is already closed when this function returns.
func httpHead(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil)
req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to head %q: %v", url, err)
}
@ -59,7 +105,7 @@ func httpHead(url string) (resp *http.Response, err error) {
// httpPost performs a POST request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it.
func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body)
req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return nil, fmt.Errorf("failed to post %q: %v", url, err)
}
@ -72,7 +118,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response,
// httpGet performs a GET request with a proper User-Agent string.
// Callers should close resp.Body when done reading from it.
func httpGet(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to get %q: %v", url, err)
}
@ -102,29 +148,25 @@ func getJSON(uri string, respBody interface{}) (http.Header, error) {
func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) {
jsonBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("Failed to marshal network message")
return nil, errors.New("failed to marshal network message")
}
resp, err := j.post(uri, jsonBytes)
if err != nil {
return nil, fmt.Errorf("Failed to post JWS message. -> %v", err)
return nil, fmt.Errorf("failed to post JWS message. -> %v", err)
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
err := 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)
retryResp, errP := j.post(uri, jsonBytes)
if errP != nil {
return nil, fmt.Errorf("failed to post JWS message. -> %v", errP)
}
defer retryResp.Body.Close()
@ -155,6 +197,6 @@ func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, e
// userAgent builds and returns the User-Agent string to use in requests.
func userAgent() string {
ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent)
ua := fmt.Sprintf("%s %s (%s; %s) %s", UserAgent, ourUserAgent, runtime.GOOS, runtime.GOARCH, defaultGoUserAgent)
return strings.TrimSpace(ua)
}

View file

@ -19,7 +19,7 @@ func HTTP01ChallengePath(token string) string {
func (s *httpChallenge) Solve(chlng challenge, domain string) error {
log.Printf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
log.Infof("[%s] acme: Trying to solve HTTP-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
@ -34,7 +34,7 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
defer func() {
err := s.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("[%s] error cleaning up: %v", domain, err)
log.Warnf("[%s] error cleaning up: %v", domain, err)
}
}()

View file

@ -35,7 +35,7 @@ func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
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)
return fmt.Errorf("could not start HTTP server for challenge -> %v", err)
}
s.done = make(chan bool)
@ -60,22 +60,33 @@ func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
// 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" {
if strings.HasPrefix(r.Host, domain) && r.Method == http.MethodGet {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(keyAuth))
log.Printf("[INFO][%s] Served key authentication", domain)
_, err := w.Write([]byte(keyAuth))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("[%s] Served key authentication", domain)
} else {
log.Printf("[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"))
log.Warnf("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)
_, err := w.Write([]byte("TEST"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
})
httpServer := &http.Server{
Handler: mux,
}
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)
err := httpServer.Serve(s.listener)
if err != nil {
log.Println(err)
}
s.done <- true
}

View file

@ -0,0 +1,104 @@
package acme
import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"github.com/xenolf/lego/log"
)
// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension OID referencing the ACME extension.
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
type tlsALPNChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}
// Solve manages the provider to validate and solve the challenge.
func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error {
log.Infof("[%s] acme: Trying to solve TLS-ALPN-01", domain)
// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
if err != nil {
return err
}
err = t.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
}
defer func() {
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Warnf("[%s] error cleaning up: %v", domain, err)
}
}()
return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}
// TLSALPNChallengeBlocks returns PEM blocks (certPEMBlock, keyPEMBlock) with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func TLSALPNChallengeBlocks(domain, keyAuth string) ([]byte, []byte, error) {
// Compute the SHA-256 digest of the key authorization.
zBytes := sha256.Sum256([]byte(keyAuth))
value, err := asn1.Marshal(zBytes[:sha256.Size])
if err != nil {
return nil, nil, err
}
// Add the keyAuth digest as the acmeValidation-v1 extension
// (marked as critical such that it won't be used by non-ACME software).
// Reference: https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3
extensions := []pkix.Extension{
{
Id: idPeAcmeIdentifierV1,
Critical: true,
Value: value,
},
}
// Generate a new RSA key for the certificates.
tempPrivKey, err := generatePrivateKey(RSA2048)
if err != nil {
return nil, nil, err
}
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
// Generate the PEM certificate using the provided private key, domain, and extra extensions.
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions)
if err != nil {
return nil, nil, err
}
// Encode the private key into a PEM format. We'll need to use it to generate the x509 keypair.
rsaPrivPEM := pemEncode(rsaPrivKey)
return tempCertPEM, rsaPrivPEM, nil
}
// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1 extension
// and domain name for the `tls-alpn-01` challenge.
func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
tempCertPEM, rsaPrivPEM, err := TLSALPNChallengeBlocks(domain, keyAuth)
if err != nil {
return nil, err
}
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return nil, err
}
return &certificate, nil
}

View file

@ -0,0 +1,90 @@
package acme
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
)
const (
// ACMETLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol.
ACMETLS1Protocol = "acme-tls/1"
// defaultTLSPort is the port that the TLSALPNProviderServer will default to
// when no other port is provided.
defaultTLSPort = "443"
)
// TLSALPNProviderServer implements ChallengeProvider for `TLS-ALPN-01`
// challenge. It may be instantiated without using the NewTLSALPNProviderServer
// if you want only to use the default values.
type TLSALPNProviderServer struct {
iface string
port string
listener net.Listener
}
// NewTLSALPNProviderServer creates a new TLSALPNProviderServer on the selected
// interface and port. Setting iface and / or port to an empty string will make
// the server fall back to the "any" interface and port 443 respectively.
func NewTLSALPNProviderServer(iface, port string) *TLSALPNProviderServer {
return &TLSALPNProviderServer{iface: iface, port: port}
}
// Present generates a certificate with a SHA-256 digest of the keyAuth provided
// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN
// spec.
func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error {
if t.port == "" {
// Fallback to port 443 if the port was not provided.
t.port = defaultTLSPort
}
// Generate the challenge certificate using the provided keyAuth and domain.
cert, err := TLSALPNChallengeCert(domain, keyAuth)
if err != nil {
return err
}
// Place the generated certificate with the extension into the TLS config
// so that it can serve the correct details.
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{*cert}
// We must set that the `acme-tls/1` application level protocol is supported
// so that the protocol negotiation can succeed. Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
tlsConf.NextProtos = []string{ACMETLS1Protocol}
// Create the listener with the created tls.Config.
t.listener, err = tls.Listen("tcp", net.JoinHostPort(t.iface, t.port), tlsConf)
if err != nil {
return fmt.Errorf("could not start HTTPS server for challenge -> %v", err)
}
// Shut the server down when we're finished.
go func() {
err := http.Serve(t.listener, nil)
if err != nil {
log.Println(err)
}
}()
return nil
}
// CleanUp closes the HTTPS server.
func (t *TLSALPNProviderServer) CleanUp(domain, token, keyAuth string) error {
if t.listener == nil {
return nil
}
// Server was created, close it.
if err := t.listener.Close(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}

View file

@ -3,16 +3,20 @@ package acme
import (
"fmt"
"time"
"github.com/xenolf/lego/log"
)
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
log.Infof("Wait [timeout: %s, interval: %s]", timeout, interval)
var lastErr string
timeup := time.After(timeout)
timeUp := time.After(timeout)
for {
select {
case <-timeup:
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
case <-timeUp:
return fmt.Errorf("time limit exceeded: last error: %s", lastErr)
default:
}

View file

@ -6,54 +6,54 @@ import (
)
// Logger is an optional custom logger.
var Logger *log.Logger
var Logger StdLogger = log.New(os.Stdout, "", log.LstdFlags)
// StdLogger interface for Standard Logger.
type StdLogger interface {
Fatal(args ...interface{})
Fatalln(args ...interface{})
Fatalf(format string, args ...interface{})
Print(args ...interface{})
Println(args ...interface{})
Printf(format string, args ...interface{})
}
// Fatal writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatal(args ...interface{}) {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
Logger.Fatal(args...)
}
// Fatalf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Fatalf(format string, args ...interface{}) {
if Logger == nil {
Logger = log.New(os.Stderr, "", log.LstdFlags)
}
Logger.Fatalf(format, args...)
}
// Print writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Print(args ...interface{}) {
if Logger == nil {
Logger = log.New(os.Stdout, "", log.LstdFlags)
}
Logger.Print(args...)
}
// Println writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Println(args ...interface{}) {
if Logger == nil {
Logger = log.New(os.Stdout, "", log.LstdFlags)
}
Logger.Println(args...)
}
// Printf writes a log entry.
// It uses Logger if not nil, otherwise it uses the default log.Logger.
func Printf(format string, args ...interface{}) {
if Logger == nil {
Logger = log.New(os.Stdout, "", log.LstdFlags)
}
Logger.Printf(format, args...)
}
// Warnf writes a log entry.
func Warnf(format string, args ...interface{}) {
Printf("[WARN] "+format, args...)
}
// Infof writes a log entry.
func Infof(format string, args ...interface{}) {
Printf("[INFO] "+format, args...)
}