add ability to relay http-01 challenges
This commit is contained in:
parent
d433b987cb
commit
1d71b24ccf
|
@ -61,6 +61,7 @@ ACME_AGREE=true
|
|||
ACME_EMAIL=letsencrypt@example.com
|
||||
|
||||
# For Let's Encrypt / ACME challenges
|
||||
ACME_HTTP_01_RELAY_URL=http://localhost:3010/api/http
|
||||
ACME_RELAY_URL=http://localhost:3010/api/dns
|
||||
SECRET=xxxxxxxxxxxxxxxx
|
||||
GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
"method": "GET",
|
||||
"pathname": ""
|
||||
},
|
||||
"acme_dns_01_proxy": {
|
||||
"_todo_acme_dns_01_proxy": {
|
||||
"pathname": "dns"
|
||||
},
|
||||
"acme_http_01_proxy": {
|
||||
"pathname": "http"
|
||||
},
|
||||
"pair_request": {
|
||||
"method": "POST",
|
||||
"pathname": "telebit.app/pair_request"
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
dns01 "git.rootprojects.org/root/telebit/dns01"
|
||||
"git.rootprojects.org/root/telebit/internal/dns01"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-acme/lego/v3/challenge"
|
||||
|
|
|
@ -2,20 +2,38 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v3/challenge"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// A Challenge has the data necessary to create an ACME DNS-01 Key Authorization Digest.
|
||||
const (
|
||||
tmpBase = "acme-tmp"
|
||||
challengeDir = ".well-known/acme-challenge"
|
||||
)
|
||||
|
||||
// Challenge is an ACME http-01 challenge
|
||||
type Challenge struct {
|
||||
Domain string `json:"domain"`
|
||||
Token string `json:"token"`
|
||||
KeyAuth string `json:"key_authorization"`
|
||||
error chan error
|
||||
Type string `json:"type"`
|
||||
Token string `json:"token"`
|
||||
KeyAuth string `json:"key_authorization"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
// for the old one
|
||||
Domain string `json:"domain"`
|
||||
error chan error
|
||||
}
|
||||
|
||||
// Identifier is restricted to DNS Domain Names for now
|
||||
type Identifier struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type acmeProvider struct {
|
||||
|
@ -32,68 +50,97 @@ func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
}
|
||||
|
||||
func handleDNSRoutes(r chi.Router) {
|
||||
r.Route("/dns", func(r chi.Router) {
|
||||
r.Post("/{domain}", func(w http.ResponseWriter, r *http.Request) {
|
||||
domain := chi.URLParam(r, "domain")
|
||||
|
||||
ctx := r.Context()
|
||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
||||
if !ok || !strings.HasPrefix(domain+".", claims.Slug) {
|
||||
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
ch := Challenge{}
|
||||
|
||||
// TODO prevent slow loris
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&ch)
|
||||
if nil != err || "" == ch.Token || "" == ch.KeyAuth {
|
||||
msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
//domain := chi.URLParam(r, "*")
|
||||
ch.Domain = domain
|
||||
|
||||
// TODO some additional error checking before the handoff
|
||||
//ch.error = make(chan error, 1)
|
||||
ch.error = make(chan error)
|
||||
presenters <- &ch
|
||||
err = <-ch.error
|
||||
if nil != err || "" == ch.Token || "" == ch.KeyAuth {
|
||||
fmt.Println("presenter err", err, ch.Token, ch.KeyAuth)
|
||||
msg := `{"error":"ACME dns-01 error", "code":"E_SERVER"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("{\"success\":true}\n"))
|
||||
})
|
||||
handleACMEChallenges := func(r chi.Router) {
|
||||
r.Post("/{domain}", createChallenge)
|
||||
|
||||
// TODO ugly Delete, but whatever
|
||||
r.Delete("/{domain}/{token}/{keyAuth}", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO authenticate
|
||||
r.Delete("/{domain}/{token}/{keyAuth}", deleteChallenge)
|
||||
r.Delete("/{domain}/{token}/{keyAuth}/{challengeType}", deleteChallenge)
|
||||
}
|
||||
|
||||
ch := Challenge{
|
||||
Domain: chi.URLParam(r, "domain"),
|
||||
Token: chi.URLParam(r, "token"),
|
||||
KeyAuth: chi.URLParam(r, "keyAuth"),
|
||||
error: make(chan error),
|
||||
//error: make(chan error, 1),
|
||||
}
|
||||
|
||||
cleanups <- &ch
|
||||
err := <-ch.error
|
||||
if nil != err || "" == ch.Token || "" == ch.KeyAuth {
|
||||
msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("{\"success\":true}\n"))
|
||||
})
|
||||
})
|
||||
r.Route("/dns", handleACMEChallenges)
|
||||
r.Route("/http", handleACMEChallenges)
|
||||
}
|
||||
|
||||
func createChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
domain := chi.URLParam(r, "domain")
|
||||
|
||||
ctx := r.Context()
|
||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
||||
if !ok || !strings.HasPrefix(domain+".", claims.Slug) {
|
||||
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
ch := Challenge{}
|
||||
|
||||
// TODO prevent slow loris
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&ch)
|
||||
if nil != err || "" == ch.Token || "" == ch.KeyAuth {
|
||||
msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
//domain := chi.URLParam(r, "*")
|
||||
ch.Domain = domain
|
||||
ch.Identifier.Value = domain
|
||||
|
||||
if "" == ch.Token || "" == ch.KeyAuth {
|
||||
err = errors.New("missing token and/or key auth")
|
||||
} else if strings.Contains(ch.Type, "http") {
|
||||
challengeBase := filepath.Join(tmpBase, ch.Domain, ".well-known/acme-challenge")
|
||||
_ = os.MkdirAll(challengeBase, 0700)
|
||||
tokenPath := filepath.Join(challengeBase, ch.Token)
|
||||
err = ioutil.WriteFile(tokenPath, []byte(ch.KeyAuth), 0600)
|
||||
} else {
|
||||
// TODO some additional error checking before the handoff
|
||||
//ch.error = make(chan error, 1)
|
||||
ch.error = make(chan error)
|
||||
presenters <- &ch
|
||||
err = <-ch.error
|
||||
}
|
||||
|
||||
if nil != err {
|
||||
fmt.Println("presenter err", err, ch.Token, ch.KeyAuth)
|
||||
msg := `{"error":"ACME dns-01 error", "code":"E_SERVER"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("{\"success\":true}\n"))
|
||||
}
|
||||
|
||||
func deleteChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO authenticate
|
||||
|
||||
ch := Challenge{
|
||||
Domain: chi.URLParam(r, "domain"),
|
||||
Token: chi.URLParam(r, "token"),
|
||||
KeyAuth: chi.URLParam(r, "keyAuth"),
|
||||
error: make(chan error),
|
||||
//error: make(chan error, 1),
|
||||
}
|
||||
|
||||
var err error
|
||||
if "" == ch.Token || "" == ch.KeyAuth {
|
||||
err = errors.New("missing token and/or key auth")
|
||||
} else if strings.Contains(ch.Type, "http") {
|
||||
// always try to remove, as there's no harm
|
||||
tokenPath := filepath.Join(tmpBase, ch.Domain, challengeDir, ch.Token)
|
||||
_ = os.Remove(tokenPath)
|
||||
} else {
|
||||
cleanups <- &ch
|
||||
err = <-ch.error
|
||||
}
|
||||
|
||||
if nil != err {
|
||||
msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}", "code":"E_BAD_REQUEST"}`
|
||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("{\"success\":true}\n"))
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ var (
|
|||
GitTimestamp = "0000-00-00T00:00:00+0000"
|
||||
)
|
||||
|
||||
// MWKey is a type guard
|
||||
type MWKey string
|
||||
|
||||
var store authstore.Store
|
||||
|
@ -44,6 +45,7 @@ func main() {
|
|||
|
||||
addr := flag.String("address", "", "IPv4 or IPv6 bind address")
|
||||
port := flag.String("port", "3000", "port to listen to")
|
||||
challengesPort := flag.String("challenges-port", "80", "port to use to respond to .well-known/acme-challenge tokens")
|
||||
dbURL := flag.String(
|
||||
"db-url",
|
||||
"postgres://postgres:postgres@localhost/postgres",
|
||||
|
@ -101,6 +103,10 @@ func main() {
|
|||
_ = store.SetMaster(secret)
|
||||
defer store.Close()
|
||||
|
||||
go func() {
|
||||
http.ListenAndServe(":"+challengesPort, routeStatic())
|
||||
}()
|
||||
|
||||
bind := *addr + ":" + *port
|
||||
fmt.Println("Listening on", bind)
|
||||
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(bind, routeAll()))
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -25,6 +26,29 @@ type MgmtClaims struct {
|
|||
var presenters = make(chan *Challenge)
|
||||
var cleanups = make(chan *Challenge)
|
||||
|
||||
func routeStatic() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Timeout(15 * time.Second))
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
r.Get("/.well-known/acme-challenge/{token}", func(w http.ResponseWriter, r *http.Request) {
|
||||
//token := chi.URLParam(r, "token")
|
||||
host := r.Header.Get("Host")
|
||||
|
||||
if strings.ContainsAny(host, "/:|\\") {
|
||||
host = ""
|
||||
}
|
||||
tokenPath := filepath.Join(tmpBase, host)
|
||||
|
||||
fsrv := http.FileServer(http.Dir(tokenPath))
|
||||
fsrv.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func routeAll() chi.Router {
|
||||
|
||||
go func() {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -21,14 +20,14 @@ import (
|
|||
|
||||
telebit "git.rootprojects.org/root/telebit"
|
||||
"git.rootprojects.org/root/telebit/dbg"
|
||||
tbDns01 "git.rootprojects.org/root/telebit/dns01"
|
||||
tbDns01 "git.rootprojects.org/root/telebit/internal/dns01"
|
||||
"git.rootprojects.org/root/telebit/internal/http01"
|
||||
"git.rootprojects.org/root/telebit/iplist"
|
||||
"git.rootprojects.org/root/telebit/mgmt"
|
||||
"git.rootprojects.org/root/telebit/mgmt/authstore"
|
||||
"git.rootprojects.org/root/telebit/table"
|
||||
"git.rootprojects.org/root/telebit/tunnel"
|
||||
legoDns01 "github.com/go-acme/lego/v3/challenge/dns01"
|
||||
"github.com/mholt/acmez/acme"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
|
@ -81,36 +80,6 @@ var VendorID string
|
|||
// ClientSecret may be baked in, or supplied via ENVs or --args
|
||||
var ClientSecret string
|
||||
|
||||
type legoWrapper struct {
|
||||
provider challenge.Provider
|
||||
//option legoDns01.ChallengeOption
|
||||
dnsSolver certmagic.DNS01Solver
|
||||
}
|
||||
|
||||
func (lw *legoWrapper) Present(ctx context.Context, ch acme.Challenge) error {
|
||||
return lw.provider.Present(ch.Identifier.Value, ch.Token, ch.KeyAuthorization)
|
||||
}
|
||||
|
||||
func (lw *legoWrapper) CleanUp(ctx context.Context, ch acme.Challenge) error {
|
||||
c := make(chan error)
|
||||
go func() {
|
||||
c <- lw.provider.CleanUp(ch.Identifier.Value, ch.Token, ch.KeyAuthorization)
|
||||
}()
|
||||
select {
|
||||
case err := <-c:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait blocks until the TXT record created in Present() appears in
|
||||
// authoritative lookups, i.e. until it has propagated, or until
|
||||
// timeout, whichever is first.
|
||||
func (lw *legoWrapper) Wait(ctx context.Context, challenge acme.Challenge) error {
|
||||
return lw.dnsSolver.Wait(ctx, challenge)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var domains []string
|
||||
var forwards []Forward
|
||||
|
@ -128,6 +97,7 @@ func main() {
|
|||
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
|
||||
enableTLSALPN01 := flag.Bool("acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
|
||||
acmeRelay := flag.String("acme-relay-url", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay")
|
||||
acmeHTTP01Relay := flag.String("acme-http-01-relay-url", "", "the base url of the ACME HTTP-01 relay, if not the same as the DNS-01 relay")
|
||||
var dnsPropagationDelay time.Duration
|
||||
flag.DurationVar(&dnsPropagationDelay, "dns-01-delay", 0, "add an extra delay after dns self-check to allow DNS-01 challenges to propagate")
|
||||
resolverList := flag.String("dns-resolvers", "", "a list of resolvers in the format 8.8.8.8:53,8.8.4.4:53")
|
||||
|
@ -181,6 +151,9 @@ func main() {
|
|||
if 0 == len(*acmeRelay) {
|
||||
*acmeRelay = os.Getenv("ACME_RELAY_URL")
|
||||
}
|
||||
if 0 == len(*acmeHTTP01Relay) {
|
||||
*acmeHTTP01Relay = os.Getenv("ACME_HTTP_01_RELAY_URL")
|
||||
}
|
||||
|
||||
if 0 == len(*email) {
|
||||
*email = os.Getenv("ACME_EMAIL")
|
||||
|
@ -428,34 +401,52 @@ func main() {
|
|||
}
|
||||
authorizer = NewAuthorizer(*authURL)
|
||||
|
||||
provider, err := getACMEProvider(acmeRelay, token)
|
||||
if nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
// it's possible for some providers this could be a failed network request,
|
||||
// but I think in the case of what we specifically support it's bad arguments
|
||||
os.Exit(exitBadArguments)
|
||||
return
|
||||
var dns01Solver *tbDns01.Solver
|
||||
if len(*acmeRelay) > 0 {
|
||||
provider, err := getACMEProvider(acmeRelay, token)
|
||||
if nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
// it's possible for some providers this could be a failed network request,
|
||||
// but I think in the case of what we specifically support it's bad arguments
|
||||
os.Exit(exitBadArguments)
|
||||
return
|
||||
}
|
||||
dns01Solver = tbDns01.NewSolver(provider)
|
||||
}
|
||||
|
||||
var http01Solver *http01.Solver
|
||||
if len(*acmeHTTP01Relay) > 0 {
|
||||
endpoint, err := url.Parse(*acmeHTTP01Relay)
|
||||
if nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(exitBadArguments)
|
||||
return
|
||||
}
|
||||
http01Solver, err = http01.NewSolver(&http01.Config{
|
||||
Endpoint: endpoint,
|
||||
Token: *token,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Email: %q\n", *email)
|
||||
|
||||
acme := &telebit.ACME{
|
||||
Email: *email,
|
||||
StoragePath: *certpath,
|
||||
Agree: *acmeAgree,
|
||||
Directory: *acmeDirectory,
|
||||
DNS01Solver: &legoWrapper{
|
||||
provider: provider,
|
||||
/*
|
||||
options: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||
ok, err := orig(fqdn, value)
|
||||
if ok && dnsPropagationDelay > 0 {
|
||||
fmt.Printf("[Telebit-ACME-DNS] sleeping an additional %s\n", dnsPropagationDelay)
|
||||
time.Sleep(dnsPropagationDelay)
|
||||
}
|
||||
return ok, err
|
||||
}),
|
||||
*/
|
||||
dnsSolver: certmagic.DNS01Solver{},
|
||||
},
|
||||
DNS01Solver: dns01Solver,
|
||||
/*
|
||||
options: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||
ok, err := orig(fqdn, value)
|
||||
if ok && dnsPropagationDelay > 0 {
|
||||
fmt.Printf("[Telebit-ACME-DNS] sleeping an additional %s\n", dnsPropagationDelay)
|
||||
time.Sleep(dnsPropagationDelay)
|
||||
}
|
||||
return ok, err
|
||||
}),
|
||||
*/
|
||||
HTTP01Solver: http01Solver,
|
||||
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
||||
EnableHTTPChallenge: *enableHTTP01,
|
||||
EnableTLSALPNChallenge: *enableTLSALPN01,
|
||||
|
|
|
@ -12,16 +12,14 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
telebit "git.rootprojects.org/root/telebit"
|
||||
tbDns01 "git.rootprojects.org/root/telebit/dns01"
|
||||
tbDns01 "git.rootprojects.org/root/telebit/internal/dns01"
|
||||
"git.rootprojects.org/root/telebit/table"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-acme/lego/v3/challenge"
|
||||
legoDns01 "github.com/go-acme/lego/v3/challenge/dns01"
|
||||
"github.com/go-acme/lego/v3/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v3/providers/dns/godaddy"
|
||||
"github.com/go-chi/chi"
|
||||
|
@ -89,16 +87,18 @@ func main() {
|
|||
StoragePath: *certpath,
|
||||
Agree: *acmeAgree,
|
||||
Directory: *acmeDirectory,
|
||||
DNSProvider: provider,
|
||||
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
||||
DNSChallengeOption: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||
ok, err := orig(fqdn, value)
|
||||
if ok {
|
||||
fmt.Println("[Telebit-ACME-DNS] sleeping an additional 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
return ok, err
|
||||
}),
|
||||
DNS01Solver: tbDns01.NewSolver(provider),
|
||||
/*
|
||||
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
||||
DNSChallengeOption: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||
ok, err := orig(fqdn, value)
|
||||
if ok {
|
||||
fmt.Println("[Telebit-ACME-DNS] sleeping an additional 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
return ok, err
|
||||
}),
|
||||
*/
|
||||
EnableHTTPChallenge: *enableHTTP01,
|
||||
EnableTLSALPNChallenge: *enableTLSALPN01,
|
||||
}
|
||||
|
|
|
@ -2,5 +2,6 @@ CLIENT_SUBJECT=newbie
|
|||
TUNNEL_RELAY_URL=https://devices.example.com/
|
||||
CLIENT_SECRET=xxxxxxxxxxxxxxxx
|
||||
LOCALS=https:$CLIENT_SUBJECT.devices.example.com:3000,https:*.$CLIENT_SUBJECT.devices.example.com:3000
|
||||
#ACME_HTTP_01_RELAY_URL=http://localhost:4200/api/http
|
||||
#PORT_FORWARDS=3443:3001,8443:3002
|
||||
#DUCKDNS_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
|
|
|
@ -5,6 +5,7 @@ package dns01
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -14,8 +15,11 @@ import (
|
|||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/coolaj86/certmagic"
|
||||
"github.com/go-acme/lego/v3/challenge"
|
||||
"github.com/go-acme/lego/v3/challenge/dns01"
|
||||
"github.com/go-acme/lego/v3/platform/config/env"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
// Environment variables names.
|
||||
|
@ -180,3 +184,44 @@ func (d *DNSProvider) doRequest(method, uri string, msg interface{}) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSolver creates a new Solver
|
||||
func NewSolver(provider challenge.Provider) (lego *Solver) {
|
||||
return &Solver{
|
||||
provider: provider,
|
||||
dnsChecker: certmagic.DNS01Solver{},
|
||||
}
|
||||
}
|
||||
|
||||
// Solver wraps a Lego DNS Provider for CertMagic
|
||||
type Solver struct {
|
||||
provider challenge.Provider
|
||||
//option legoDns01.ChallengeOption
|
||||
dnsChecker certmagic.DNS01Solver
|
||||
}
|
||||
|
||||
// Present creates a DNS-01 Challenge Token
|
||||
func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error {
|
||||
return s.provider.Present(ch.Identifier.Value, ch.Token, ch.KeyAuthorization)
|
||||
}
|
||||
|
||||
// CleanUp deletes a DNS-01 Challenge Token
|
||||
func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error {
|
||||
c := make(chan error)
|
||||
go func() {
|
||||
c <- s.provider.CleanUp(ch.Identifier.Value, ch.Token, ch.KeyAuthorization)
|
||||
}()
|
||||
select {
|
||||
case err := <-c:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait blocks until the TXT record created in Present() appears in
|
||||
// authoritative lookups, i.e. until it has propagated, or until
|
||||
// timeout, whichever is first.
|
||||
func (s *Solver) Wait(ctx context.Context, challenge acme.Challenge) error {
|
||||
return s.dnsChecker.Wait(ctx, challenge)
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package http01
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
// Config is used to configure the creation of the HTTP-01 Solver.
|
||||
type Config struct {
|
||||
Endpoint *url.URL
|
||||
Token string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// Solver implements the challenge.Provider interface.
|
||||
type Solver struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Challenge is an ACME http-01 challenge
|
||||
type Challenge struct {
|
||||
Type string `json:"type"`
|
||||
Token string `json:"token"`
|
||||
KeyAuthorization string `json:"key_authorization"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
}
|
||||
|
||||
// Identifier is restricted to DNS Domain Names for now
|
||||
type Identifier struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// NewSolver return a new HTTP-01 Solver.
|
||||
func NewSolver(config *Config) (*Solver, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("api: the configuration of the DNS provider is nil")
|
||||
}
|
||||
|
||||
if config.Endpoint == nil {
|
||||
return nil, errors.New("api: the endpoint is missing")
|
||||
}
|
||||
|
||||
if nil == config.HTTPClient {
|
||||
config.HTTPClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
return &Solver{config: config}, nil
|
||||
}
|
||||
|
||||
// Present creates a DNS-01 Challenge Token
|
||||
func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error {
|
||||
msg := &Challenge{
|
||||
Type: "http-01",
|
||||
Token: ch.Token,
|
||||
KeyAuthorization: ch.KeyAuthorization,
|
||||
Identifier: Identifier{
|
||||
Type: ch.Identifier.Type,
|
||||
Value: ch.Identifier.Value,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.doRequest(http.MethodPost, fmt.Sprintf("/%s", msg.Identifier.Value), msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp deletes an HTTP-01 Challenge Token
|
||||
func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error {
|
||||
msg := &Challenge{
|
||||
Type: "http-01",
|
||||
Token: ch.Token,
|
||||
KeyAuthorization: ch.KeyAuthorization,
|
||||
Identifier: Identifier{
|
||||
Type: ch.Identifier.Type,
|
||||
Value: ch.Identifier.Value,
|
||||
},
|
||||
}
|
||||
|
||||
err := s.doRequest(
|
||||
http.MethodDelete,
|
||||
fmt.Sprintf("/%s/%s/%s/%s", msg.Identifier.Value, msg.Token, msg.KeyAuthorization, msg.Type),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Solver) doRequest(method, uri string, msg interface{}) error {
|
||||
data, _ := json.MarshalIndent(msg, "", " ")
|
||||
reqBody := bytes.NewBuffer(data)
|
||||
|
||||
newURI := path.Join(s.config.Endpoint.EscapedPath(), uri)
|
||||
endpoint, err := s.config.Endpoint.Parse(newURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, endpoint.String(), reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if len(s.config.Token) > 0 {
|
||||
req.Header.Set("Authorization", "Bearer "+s.config.Token)
|
||||
}
|
||||
|
||||
resp, err := s.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: %w", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
// +build tools
|
||||
|
||||
// tools is a faux package for tracking dependencies that don't make it into the code
|
||||
// Package tools is a faux package for tracking dependencies that don't make it into the code
|
||||
package tools
|
||||
|
||||
import (
|
||||
// these are binaries
|
||||
_ "git.rootprojects.org/root/go-gitver"
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
_ "github.com/shurcooL/vfsgen/cmd/vfsgendev"
|
|
@ -14,11 +14,13 @@ import (
|
|||
// which will provide the tunnel endpoint. However, for the sake of testing,
|
||||
// these things may happen out-of-order.
|
||||
type Endpoints struct {
|
||||
ToS string `json:"terms_of_service"`
|
||||
APIHost string `json:"api_host"`
|
||||
Tunnel Endpoint `json:"tunnel"`
|
||||
Authenticate Endpoint `json:"authn"`
|
||||
DNS01Proxy Endpoint `json:"acme_dns_01_proxy"`
|
||||
ToS string `json:"terms_of_service"`
|
||||
APIHost string `json:"api_host"`
|
||||
Tunnel Endpoint `json:"tunnel"`
|
||||
Authenticate Endpoint `json:"authn"`
|
||||
ChallengeProxy Endpoint `json:"acme_challenge_proxy"`
|
||||
DNS01Proxy Endpoint `json:"acme_dns_01_proxy"`
|
||||
HTTP01Proxy Endpoint `json:"acme_http_01_proxy"`
|
||||
/*
|
||||
{
|
||||
"terms_of_service": ":hostname/tos/",
|
||||
|
@ -77,7 +79,13 @@ func Discover(relay string) (*Endpoints, error) {
|
|||
}
|
||||
|
||||
directives.Authenticate.URL = endpointToURLString(directives.APIHost, directives.Authenticate)
|
||||
if len(directives.ChallengeProxy.Pathname) > 0 {
|
||||
directives.ChallengeProxy.URL = endpointToURLString(directives.APIHost, directives.ChallengeProxy)
|
||||
}
|
||||
directives.DNS01Proxy.URL = endpointToURLString(directives.APIHost, directives.DNS01Proxy)
|
||||
if len(directives.HTTP01Proxy.Pathname) > 0 {
|
||||
directives.HTTP01Proxy.URL = endpointToURLString(directives.APIHost, directives.HTTP01Proxy)
|
||||
}
|
||||
|
||||
return directives, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue