traefik/pkg/provider/acme/challenge_http.go

152 lines
3.7 KiB
Go
Raw Normal View History

2018-03-05 19:54:04 +00:00
package acme
import (
2018-11-14 09:18:03 +00:00
"context"
"errors"
"fmt"
2018-07-03 10:44:04 +00:00
"net"
"net/http"
"net/url"
"regexp"
"sync"
2018-03-05 19:54:04 +00:00
"time"
2020-02-26 09:36:05 +00:00
"github.com/cenkalti/backoff/v4"
2020-09-04 08:52:03 +00:00
"github.com/go-acme/lego/v4/challenge/http01"
2022-11-21 17:36:05 +00:00
"github.com/rs/zerolog/log"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/safe"
2018-03-05 19:54:04 +00:00
)
// ChallengeHTTP HTTP challenge provider implements challenge.Provider.
type ChallengeHTTP struct {
httpChallenges map[string]map[string][]byte
lock sync.RWMutex
}
2018-07-03 10:44:04 +00:00
// NewChallengeHTTP creates a new ChallengeHTTP.
func NewChallengeHTTP() *ChallengeHTTP {
return &ChallengeHTTP{
httpChallenges: make(map[string]map[string][]byte),
}
2018-07-03 10:44:04 +00:00
}
2018-11-14 09:18:03 +00:00
// Present presents a challenge to obtain new ACME certificate.
func (c *ChallengeHTTP) Present(domain, token, keyAuth string) error {
c.lock.Lock()
defer c.lock.Unlock()
if _, ok := c.httpChallenges[token]; !ok {
c.httpChallenges[token] = map[string][]byte{}
}
c.httpChallenges[token][domain] = []byte(keyAuth)
return nil
2018-07-03 10:44:04 +00:00
}
2018-03-05 19:54:04 +00:00
2018-11-14 09:18:03 +00:00
// CleanUp cleans the challenges when certificate is obtained.
func (c *ChallengeHTTP) CleanUp(domain, token, _ string) error {
c.lock.Lock()
defer c.lock.Unlock()
if c.httpChallenges == nil && len(c.httpChallenges) == 0 {
return nil
}
if _, ok := c.httpChallenges[token]; ok {
delete(c.httpChallenges[token], domain)
if len(c.httpChallenges[token]) == 0 {
delete(c.httpChallenges, token)
}
}
return nil
2018-03-05 19:54:04 +00:00
}
2018-11-14 09:18:03 +00:00
// Timeout calculates the maximum of time allowed to resolved an ACME challenge.
func (c *ChallengeHTTP) Timeout() (timeout, interval time.Duration) {
2018-07-03 10:44:04 +00:00
return 60 * time.Second, 5 * time.Second
}
func (c *ChallengeHTTP) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
2022-11-21 17:36:05 +00:00
logger := log.Ctx(req.Context()).With().Str(logs.ProviderName, "acme").Logger()
token, err := getPathParam(req.URL)
if err != nil {
2022-11-21 17:36:05 +00:00
logger.Error().Err(err).Msg("Unable to get token")
rw.WriteHeader(http.StatusNotFound)
return
}
if token != "" {
domain, _, err := net.SplitHostPort(req.Host)
if err != nil {
2022-11-21 17:36:05 +00:00
logger.Debug().Err(err).Msg("Unable to split host and port. Fallback to request host.")
domain = req.Host
}
2022-11-21 17:36:05 +00:00
tokenValue := c.getTokenValue(logger.WithContext(req.Context()), token, domain)
if len(tokenValue) > 0 {
rw.WriteHeader(http.StatusOK)
_, err = rw.Write(tokenValue)
if err != nil {
2022-11-21 17:36:05 +00:00
logger.Error().Err(err).Msg("Unable to write token")
2018-07-03 10:44:04 +00:00
}
return
}
}
rw.WriteHeader(http.StatusNotFound)
2018-03-05 19:54:04 +00:00
}
2018-11-14 09:18:03 +00:00
func (c *ChallengeHTTP) getTokenValue(ctx context.Context, token, domain string) []byte {
2022-11-21 17:36:05 +00:00
logger := log.Ctx(ctx)
logger.Debug().Msgf("Retrieving the ACME challenge for %s (token %q)...", domain, token)
2018-11-14 09:18:03 +00:00
var result []byte
operation := func() error {
c.lock.RLock()
defer c.lock.RUnlock()
if _, ok := c.httpChallenges[token]; !ok {
2022-01-27 09:58:04 +00:00
return fmt.Errorf("cannot find challenge for token %q (%s)", token, domain)
}
var ok bool
result, ok = c.httpChallenges[token][domain]
if !ok {
2022-01-27 09:58:04 +00:00
return fmt.Errorf("cannot find challenge for %s (token %q)", domain, token)
}
return nil
2018-11-14 09:18:03 +00:00
}
notify := func(err error, time time.Duration) {
2022-11-21 17:36:05 +00:00
logger.Error().Msgf("Error getting challenge for token retrying in %s", time)
2018-11-14 09:18:03 +00:00
}
ebo := backoff.NewExponentialBackOff()
ebo.MaxElapsedTime = 60 * time.Second
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
if err != nil {
2022-11-21 17:36:05 +00:00
logger.Error().Err(err).Msgf("Cannot retrieve the ACME challenge for %s (token %q)", domain, token)
2018-11-14 09:18:03 +00:00
return []byte{}
}
return result
}
func getPathParam(uri *url.URL) (string, error) {
exp := regexp.MustCompile(fmt.Sprintf(`^%s([^/]+)/?$`, http01.ChallengePath("")))
parts := exp.FindStringSubmatch(uri.Path)
if len(parts) != 2 {
return "", errors.New("missing token")
}
return parts[1], nil
}