From 1d71b24ccf5038e9e2ef16fb7a448bb8ff2e3acc Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 5 Nov 2020 02:11:17 -0700 Subject: [PATCH] add ability to relay http-01 challenges --- README.md | 1 + .../assets/.well-known/telebit.app/index.json | 5 +- cmd/dnsclient/dnsclient.go | 2 +- cmd/mgmt/acmeroutes.go | 179 +++++++++++------- cmd/mgmt/mgmt.go | 6 + cmd/mgmt/route.go | 24 +++ cmd/telebit/telebit.go | 99 +++++----- cmd/wsserve/wsserve.go | 26 +-- examples/client.env | 1 + {dns01 => internal/dns01}/LICENSE | 0 {dns01 => internal/dns01}/dns01.go | 45 +++++ {dns01 => internal/dns01}/dns01_test.go | 0 internal/http01/http01.go | 142 ++++++++++++++ {tools => internal/tools}/tools.go | 3 +- tunnel/discover.go | 18 +- 15 files changed, 410 insertions(+), 141 deletions(-) rename {dns01 => internal/dns01}/LICENSE (100%) rename {dns01 => internal/dns01}/dns01.go (78%) rename {dns01 => internal/dns01}/dns01_test.go (100%) create mode 100644 internal/http01/http01.go rename {tools => internal/tools}/tools.go (58%) diff --git a/README.md b/README.md index a94e98e..1600eff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/admin/assets/.well-known/telebit.app/index.json b/admin/assets/.well-known/telebit.app/index.json index b4b535a..19508d9 100644 --- a/admin/assets/.well-known/telebit.app/index.json +++ b/admin/assets/.well-known/telebit.app/index.json @@ -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" diff --git a/cmd/dnsclient/dnsclient.go b/cmd/dnsclient/dnsclient.go index 2bc5cb3..0ab8254 100644 --- a/cmd/dnsclient/dnsclient.go +++ b/cmd/dnsclient/dnsclient.go @@ -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" diff --git a/cmd/mgmt/acmeroutes.go b/cmd/mgmt/acmeroutes.go index bda5e02..bbb944e 100644 --- a/cmd/mgmt/acmeroutes.go +++ b/cmd/mgmt/acmeroutes.go @@ -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")) } diff --git a/cmd/mgmt/mgmt.go b/cmd/mgmt/mgmt.go index 9482a26..300c730 100644 --- a/cmd/mgmt/mgmt.go +++ b/cmd/mgmt/mgmt.go @@ -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())) diff --git a/cmd/mgmt/route.go b/cmd/mgmt/route.go index 11a67d8..b654bd0 100644 --- a/cmd/mgmt/route.go +++ b/cmd/mgmt/route.go @@ -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() { diff --git a/cmd/telebit/telebit.go b/cmd/telebit/telebit.go index 34fa7dd..59df3da 100644 --- a/cmd/telebit/telebit.go +++ b/cmd/telebit/telebit.go @@ -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, diff --git a/cmd/wsserve/wsserve.go b/cmd/wsserve/wsserve.go index 8d4f6c0..7b070bd 100644 --- a/cmd/wsserve/wsserve.go +++ b/cmd/wsserve/wsserve.go @@ -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, } diff --git a/examples/client.env b/examples/client.env index 06f80ab..e53b292 100644 --- a/examples/client.env +++ b/examples/client.env @@ -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 diff --git a/dns01/LICENSE b/internal/dns01/LICENSE similarity index 100% rename from dns01/LICENSE rename to internal/dns01/LICENSE diff --git a/dns01/dns01.go b/internal/dns01/dns01.go similarity index 78% rename from dns01/dns01.go rename to internal/dns01/dns01.go index 6ca183a..d0bc41d 100644 --- a/dns01/dns01.go +++ b/internal/dns01/dns01.go @@ -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) +} diff --git a/dns01/dns01_test.go b/internal/dns01/dns01_test.go similarity index 100% rename from dns01/dns01_test.go rename to internal/dns01/dns01_test.go diff --git a/internal/http01/http01.go b/internal/http01/http01.go new file mode 100644 index 0000000..400b903 --- /dev/null +++ b/internal/http01/http01.go @@ -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 +} diff --git a/tools/tools.go b/internal/tools/tools.go similarity index 58% rename from tools/tools.go rename to internal/tools/tools.go index 4ebcd31..11757ff 100644 --- a/tools/tools.go +++ b/internal/tools/tools.go @@ -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" diff --git a/tunnel/discover.go b/tunnel/discover.go index a843625..b88e65c 100644 --- a/tunnel/discover.go +++ b/tunnel/discover.go @@ -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 }