httpreq.go raw

   1  // Package httpreq implements a DNS provider for solving the DNS-01 challenge through an HTTP server.
   2  package httpreq
   3  
   4  import (
   5  	"bytes"
   6  	"context"
   7  	"encoding/json"
   8  	"errors"
   9  	"fmt"
  10  	"net/http"
  11  	"net/url"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/challenge"
  15  	"github.com/go-acme/lego/v4/challenge/dns01"
  16  	"github.com/go-acme/lego/v4/platform/config/env"
  17  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  18  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  19  )
  20  
  21  // Environment variables names.
  22  const (
  23  	envNamespace = "HTTPREQ_"
  24  
  25  	EnvEndpoint = envNamespace + "ENDPOINT"
  26  	EnvMode     = envNamespace + "MODE"
  27  	EnvUsername = envNamespace + "USERNAME"
  28  	EnvPassword = envNamespace + "PASSWORD"
  29  
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  )
  34  
  35  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  36  
  37  type message struct {
  38  	FQDN  string `json:"fqdn"`
  39  	Value string `json:"value"`
  40  }
  41  
  42  type messageRaw struct {
  43  	Domain  string `json:"domain"`
  44  	Token   string `json:"token"`
  45  	KeyAuth string `json:"keyAuth"`
  46  }
  47  
  48  // Config is used to configure the creation of the DNSProvider.
  49  type Config struct {
  50  	Endpoint           *url.URL
  51  	Mode               string
  52  	Username           string
  53  	Password           string
  54  	PropagationTimeout time.Duration
  55  	PollingInterval    time.Duration
  56  	HTTPClient         *http.Client
  57  }
  58  
  59  // NewDefaultConfig returns a default configuration for the DNSProvider.
  60  func NewDefaultConfig() *Config {
  61  	return &Config{
  62  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  63  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  64  		HTTPClient: &http.Client{
  65  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  66  		},
  67  	}
  68  }
  69  
  70  // DNSProvider implements the challenge.Provider interface.
  71  type DNSProvider struct {
  72  	config *Config
  73  }
  74  
  75  // NewDNSProvider returns a DNSProvider instance.
  76  func NewDNSProvider() (*DNSProvider, error) {
  77  	values, err := env.Get(EnvEndpoint)
  78  	if err != nil {
  79  		return nil, fmt.Errorf("httpreq: %w", err)
  80  	}
  81  
  82  	endpoint, err := url.Parse(values[EnvEndpoint])
  83  	if err != nil {
  84  		return nil, fmt.Errorf("httpreq: %w", err)
  85  	}
  86  
  87  	config := NewDefaultConfig()
  88  	config.Mode = env.GetOrFile(EnvMode)
  89  	config.Username = env.GetOrFile(EnvUsername)
  90  	config.Password = env.GetOrFile(EnvPassword)
  91  	config.Endpoint = endpoint
  92  
  93  	return NewDNSProviderConfig(config)
  94  }
  95  
  96  // NewDNSProviderConfig return a DNSProvider.
  97  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  98  	if config == nil {
  99  		return nil, errors.New("httpreq: the configuration of the DNS provider is nil")
 100  	}
 101  
 102  	if config.Endpoint == nil {
 103  		return nil, errors.New("httpreq: the endpoint is missing")
 104  	}
 105  
 106  	config.HTTPClient = clientdebug.Wrap(config.HTTPClient)
 107  
 108  	return &DNSProvider{config: config}, nil
 109  }
 110  
 111  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 112  // Adjusting here to cope with spikes in propagation times.
 113  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 114  	return d.config.PropagationTimeout, d.config.PollingInterval
 115  }
 116  
 117  // Present creates a TXT record to fulfill the dns-01 challenge.
 118  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 119  	ctx := context.Background()
 120  
 121  	if d.config.Mode == "RAW" {
 122  		msg := &messageRaw{
 123  			Domain:  domain,
 124  			Token:   token,
 125  			KeyAuth: keyAuth,
 126  		}
 127  
 128  		err := d.doPost(ctx, "/present", msg)
 129  		if err != nil {
 130  			return fmt.Errorf("httpreq: %w", err)
 131  		}
 132  
 133  		return nil
 134  	}
 135  
 136  	info := dns01.GetChallengeInfo(domain, keyAuth)
 137  	msg := &message{
 138  		FQDN:  info.EffectiveFQDN,
 139  		Value: info.Value,
 140  	}
 141  
 142  	err := d.doPost(ctx, "/present", msg)
 143  	if err != nil {
 144  		return fmt.Errorf("httpreq: %w", err)
 145  	}
 146  
 147  	return nil
 148  }
 149  
 150  // CleanUp removes the TXT record matching the specified parameters.
 151  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 152  	ctx := context.Background()
 153  
 154  	if d.config.Mode == "RAW" {
 155  		msg := &messageRaw{
 156  			Domain:  domain,
 157  			Token:   token,
 158  			KeyAuth: keyAuth,
 159  		}
 160  
 161  		err := d.doPost(ctx, "/cleanup", msg)
 162  		if err != nil {
 163  			return fmt.Errorf("httpreq: %w", err)
 164  		}
 165  
 166  		return nil
 167  	}
 168  
 169  	info := dns01.GetChallengeInfo(domain, keyAuth)
 170  	msg := &message{
 171  		FQDN:  info.EffectiveFQDN,
 172  		Value: info.Value,
 173  	}
 174  
 175  	err := d.doPost(ctx, "/cleanup", msg)
 176  	if err != nil {
 177  		return fmt.Errorf("httpreq: %w", err)
 178  	}
 179  
 180  	return nil
 181  }
 182  
 183  func (d *DNSProvider) doPost(ctx context.Context, uri string, msg any) error {
 184  	reqBody := new(bytes.Buffer)
 185  
 186  	err := json.NewEncoder(reqBody).Encode(msg)
 187  	if err != nil {
 188  		return fmt.Errorf("failed to create request JSON body: %w", err)
 189  	}
 190  
 191  	endpoint := d.config.Endpoint.JoinPath(uri)
 192  
 193  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), reqBody)
 194  	if err != nil {
 195  		return fmt.Errorf("unable to create request: %w", err)
 196  	}
 197  
 198  	req.Header.Set("Accept", "application/json")
 199  	req.Header.Set("Content-Type", "application/json")
 200  
 201  	if d.config.Username != "" && d.config.Password != "" {
 202  		req.SetBasicAuth(d.config.Username, d.config.Password)
 203  	}
 204  
 205  	resp, err := d.config.HTTPClient.Do(req)
 206  	if err != nil {
 207  		return errutils.NewHTTPDoError(req, err)
 208  	}
 209  
 210  	defer func() { _ = resp.Body.Close() }()
 211  
 212  	if resp.StatusCode/100 != 2 {
 213  		return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
 214  	}
 215  
 216  	return nil
 217  }
 218