forked from TrueCloudLab/lego
Merge pull request #710 from ldez/feature/httpreq
Add DNS provider for "HTTP request".
This commit is contained in:
commit
fac6e4995c
5 changed files with 519 additions and 0 deletions
2
cli.go
2
cli.go
|
@ -226,6 +226,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
|||
fmt.Fprintln(w, "\tglesys:\tGLESYS_API_USER, GLESYS_API_KEY")
|
||||
fmt.Fprintln(w, "\tgodaddy:\tGODADDY_API_KEY, GODADDY_API_SECRET")
|
||||
fmt.Fprintln(w, "\thostingde:\tHOSTINGDE_API_KEY, HOSTINGDE_ZONE_NAME")
|
||||
fmt.Fprintln(w, "\thttpreq:\tHTTPREQ_ENDPOINT, HTTPREQ_MODE, HTTPREQ_USERNAME, HTTPREQ_PASSWORD")
|
||||
fmt.Fprintln(w, "\tiij:\tIIJ_API_ACCESS_KEY, IIJ_API_SECRET_KEY, IIJ_DO_SERVICE_CODE")
|
||||
fmt.Fprintln(w, "\tinwx:\tINWX_USERNAME, INWX_PASSWORD")
|
||||
fmt.Fprintln(w, "\tlightsail:\tAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, DNS_ZONE")
|
||||
|
@ -275,6 +276,7 @@ Here is an example bash command using the CloudFlare DNS provider:
|
|||
fmt.Fprintln(w, "\tglesys:\tGLESYS_POLLING_INTERVAL, GLESYS_PROPAGATION_TIMEOUT, GLESYS_TTL, GLESYS_HTTP_TIMEOUT")
|
||||
fmt.Fprintln(w, "\tgodaddy:\tGODADDY_POLLING_INTERVAL, GODADDY_PROPAGATION_TIMEOUT, GODADDY_TTL, GODADDY_HTTP_TIMEOUT")
|
||||
fmt.Fprintln(w, "\thostingde:\tHOSTINGDE_POLLING_INTERVAL, HOSTINGDE_PROPAGATION_TIMEOUT, HOSTINGDE_TTL, HOSTINGDE_HTTP_TIMEOUT")
|
||||
fmt.Fprintln(w, "\thttpreq:\t,HTTPREQ_POLLING_INTERVAL, HTTPREQ_PROPAGATION_TIMEOUT, HTTPREQ_HTTP_TIMEOUT")
|
||||
fmt.Fprintln(w, "\tiij:\tIIJ_POLLING_INTERVAL, IIJ_PROPAGATION_TIMEOUT, IIJ_TTL")
|
||||
fmt.Fprintln(w, "\tinwx:\tINWX_POLLING_INTERVAL, INWX_PROPAGATION_TIMEOUT, INWX_TTL, INWX_SANDBOX")
|
||||
fmt.Fprintln(w, "\tlightsail:\tLIGHTSAIL_POLLING_INTERVAL, LIGHTSAIL_PROPAGATION_TIMEOUT")
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/xenolf/lego/providers/dns/glesys"
|
||||
"github.com/xenolf/lego/providers/dns/godaddy"
|
||||
"github.com/xenolf/lego/providers/dns/hostingde"
|
||||
"github.com/xenolf/lego/providers/dns/httpreq"
|
||||
"github.com/xenolf/lego/providers/dns/iij"
|
||||
"github.com/xenolf/lego/providers/dns/inwx"
|
||||
"github.com/xenolf/lego/providers/dns/lightsail"
|
||||
|
@ -105,6 +106,8 @@ func NewDNSChallengeProviderByName(name string) (acme.ChallengeProvider, error)
|
|||
return godaddy.NewDNSProvider()
|
||||
case "hostingde":
|
||||
return hostingde.NewDNSProvider()
|
||||
case "httpreq":
|
||||
return httpreq.NewDNSProvider()
|
||||
case "iij":
|
||||
return iij.NewDNSProvider()
|
||||
case "inwx":
|
||||
|
|
193
providers/dns/httpreq/httpreq.go
Normal file
193
providers/dns/httpreq/httpreq.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
// Package httpreq implements a DNS provider for solving the DNS-01 challenge through a HTTP server.
|
||||
package httpreq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/platform/config/env"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
FQDN string `json:"fqdn"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type messageRaw struct {
|
||||
Domain string `json:"domain"`
|
||||
Token string `json:"token"`
|
||||
KeyAuth string `json:"keyAuth"`
|
||||
}
|
||||
|
||||
// Config is used to configure the creation of the DNSProvider
|
||||
type Config struct {
|
||||
Endpoint *url.URL
|
||||
Mode string
|
||||
Username string
|
||||
Password string
|
||||
PropagationTimeout time.Duration
|
||||
PollingInterval time.Duration
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewDefaultConfig returns a default configuration for the DNSProvider
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
PropagationTimeout: env.GetOrDefaultSecond("HTTPREQ_PROPAGATION_TIMEOUT", acme.DefaultPropagationTimeout),
|
||||
PollingInterval: env.GetOrDefaultSecond("HTTPREQ_POLLING_INTERVAL", acme.DefaultPollingInterval),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: env.GetOrDefaultSecond("HTTPREQ_HTTP_TIMEOUT", 30*time.Second),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DNSProvider describes a provider for acme-proxy
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance.
|
||||
func NewDNSProvider() (*DNSProvider, error) {
|
||||
values, err := env.Get("HTTPREQ_ENDPOINT")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
|
||||
endpoint, err := url.Parse(values["HTTPREQ_ENDPOINT"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Mode = os.Getenv("HTTPREQ_MODE")
|
||||
config.Username = os.Getenv("HTTPREQ_USERNAME")
|
||||
config.Password = os.Getenv("HTTPREQ_PASSWORD")
|
||||
config.Endpoint = endpoint
|
||||
return NewDNSProviderConfig(config)
|
||||
}
|
||||
|
||||
// NewDNSProviderConfig return a DNSProvider .
|
||||
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("httpreq: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
if config.Endpoint == nil {
|
||||
return nil, errors.New("httpreq: the endpoint is missing")
|
||||
}
|
||||
|
||||
return &DNSProvider{config: config}, nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||
// Adjusting here to cope with spikes in propagation times.
|
||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfill the dns-01 challenge
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
if d.config.Mode == "RAW" {
|
||||
msg := &messageRaw{
|
||||
Domain: domain,
|
||||
Token: token,
|
||||
KeyAuth: keyAuth,
|
||||
}
|
||||
|
||||
err := d.doPost("/present", msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
msg := &message{
|
||||
FQDN: fqdn,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err := d.doPost("/present", msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
if d.config.Mode == "RAW" {
|
||||
msg := &messageRaw{
|
||||
Domain: domain,
|
||||
Token: token,
|
||||
KeyAuth: keyAuth,
|
||||
}
|
||||
|
||||
err := d.doPost("/cleanup", msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
msg := &message{
|
||||
FQDN: fqdn,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err := d.doPost("/cleanup", msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpreq: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) doPost(uri string, msg interface{}) error {
|
||||
reqBody := &bytes.Buffer{}
|
||||
err := json.NewEncoder(reqBody).Encode(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := d.config.Endpoint.Parse(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, endpoint.String(), reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if len(d.config.Username) > 0 && len(d.config.Password) > 0 {
|
||||
req.SetBasicAuth(d.config.Username, d.config.Password)
|
||||
}
|
||||
|
||||
resp, err := d.config.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= http.StatusBadRequest {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%d: failed to read response body: %v", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
288
providers/dns/httpreq/httpreq_test.go
Normal file
288
providers/dns/httpreq/httpreq_test.go
Normal file
|
@ -0,0 +1,288 @@
|
|||
package httpreq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xenolf/lego/platform/tester"
|
||||
)
|
||||
|
||||
var envTest = tester.NewEnvTest("HTTPREQ_ENDPOINT", "HTTPREQ_MODE", "HTTPREQ_USERNAME", "HTTPREQ_PASSWORD")
|
||||
|
||||
func TestNewDNSProvider(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
envVars: map[string]string{
|
||||
"HTTPREQ_ENDPOINT": "http://localhost:8090",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "invalid URL",
|
||||
envVars: map[string]string{
|
||||
"HTTPREQ_ENDPOINT": ":",
|
||||
},
|
||||
expected: "httpreq: parse :: missing protocol scheme",
|
||||
},
|
||||
{
|
||||
desc: "missing endpoint",
|
||||
envVars: map[string]string{
|
||||
"HTTPREQ_ENDPOINT": "",
|
||||
},
|
||||
expected: "httpreq: some credentials information are missing: HTTPREQ_ENDPOINT",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
defer envTest.RestoreEnv()
|
||||
envTest.ClearEnv()
|
||||
|
||||
envTest.Apply(test.envVars)
|
||||
|
||||
p, err := NewDNSProvider()
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
endpoint *url.URL
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
endpoint: mustParse("http://localhost:8090"),
|
||||
},
|
||||
{
|
||||
desc: "missing endpoint",
|
||||
expected: "httpreq: the endpoint is missing",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
config := NewDefaultConfig()
|
||||
config.Endpoint = test.endpoint
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
|
||||
if len(test.expected) == 0 {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.config)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProvider_Present(t *testing.T) {
|
||||
envTest.RestoreEnv()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
mode string
|
||||
username string
|
||||
password string
|
||||
handler http.HandlerFunc
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
handler: successHandler,
|
||||
},
|
||||
{
|
||||
desc: "error",
|
||||
handler: http.NotFound,
|
||||
expectedError: "httpreq: 404: request failed: 404 page not found\n",
|
||||
},
|
||||
{
|
||||
desc: "success raw mode",
|
||||
mode: "RAW",
|
||||
handler: successRawModeHandler,
|
||||
},
|
||||
{
|
||||
desc: "error raw mode",
|
||||
mode: "RAW",
|
||||
handler: http.NotFound,
|
||||
expectedError: "httpreq: 404: request failed: 404 page not found\n",
|
||||
},
|
||||
{
|
||||
desc: "basic auth",
|
||||
username: "bar",
|
||||
password: "foo",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
username, password, ok := req.BasicAuth()
|
||||
if username != "bar" || password != "foo" || !ok {
|
||||
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password."))
|
||||
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(rw, "lego")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/present", test.handler)
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Endpoint = mustParse(server.URL)
|
||||
config.Mode = test.mode
|
||||
config.Username = test.username
|
||||
config.Password = test.password
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.Present("domain", "token", "key")
|
||||
if test.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDNSProvider_Cleanup(t *testing.T) {
|
||||
envTest.RestoreEnv()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
mode string
|
||||
username string
|
||||
password string
|
||||
handler http.HandlerFunc
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
desc: "success",
|
||||
handler: successHandler,
|
||||
},
|
||||
{
|
||||
desc: "error",
|
||||
handler: http.NotFound,
|
||||
expectedError: "httpreq: 404: request failed: 404 page not found\n",
|
||||
},
|
||||
{
|
||||
desc: "success raw mode",
|
||||
mode: "RAW",
|
||||
handler: successRawModeHandler,
|
||||
},
|
||||
{
|
||||
desc: "error raw mode",
|
||||
mode: "RAW",
|
||||
handler: http.NotFound,
|
||||
expectedError: "httpreq: 404: request failed: 404 page not found\n",
|
||||
},
|
||||
{
|
||||
desc: "basic auth",
|
||||
username: "bar",
|
||||
password: "foo",
|
||||
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||
username, password, ok := req.BasicAuth()
|
||||
if username != "bar" || password != "foo" || !ok {
|
||||
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password."))
|
||||
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
fmt.Fprint(rw, "lego")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/cleanup", test.handler)
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
config := NewDefaultConfig()
|
||||
config.Endpoint = mustParse(server.URL)
|
||||
config.Mode = test.mode
|
||||
config.Username = test.username
|
||||
config.Password = test.password
|
||||
|
||||
p, err := NewDNSProviderConfig(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = p.CleanUp("domain", "token", "key")
|
||||
if test.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.EqualError(t, err, test.expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func successHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &message{}
|
||||
err := json.NewDecoder(req.Body).Decode(msg)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(rw, "lego")
|
||||
}
|
||||
|
||||
func successRawModeHandler(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
msg := &messageRaw{}
|
||||
err := json.NewDecoder(req.Body).Decode(msg)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(rw, "lego")
|
||||
}
|
||||
|
||||
func mustParse(rawURL string) *url.URL {
|
||||
uri, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return uri
|
||||
}
|
33
providers/dns/httpreq/readme.md
Normal file
33
providers/dns/httpreq/readme.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
# HTTP request
|
||||
|
||||
The server must provide:
|
||||
- `POST` `/present`
|
||||
- `POST` `/cleanup`
|
||||
|
||||
The URL of the server must be define by `HTTPREQ_ENDPOINT`.
|
||||
|
||||
## Mode
|
||||
|
||||
There are 2 modes (`HTTPREQ_MODE`):
|
||||
- default mode:
|
||||
```json
|
||||
{
|
||||
"fqdn": "_acme-challenge.domain.",
|
||||
"value": "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"
|
||||
}
|
||||
```
|
||||
|
||||
- `RAW`
|
||||
```json
|
||||
{
|
||||
"domain": "domain",
|
||||
"token": "token",
|
||||
"keyAuth": "key"
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Basic authentication (optional) can be set with some environment variables:
|
||||
- `HTTPREQ_USERNAME` and `HTTPREQ_PASSWORD`
|
||||
- both values must be set, otherwise basic authentication is not defined.
|
Loading…
Reference in a new issue