diff --git a/.gitignore b/.gitignore index fcb744d..8e27fef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ certs acme.d xversion.go +/mplexer/cmd/mgmt/mgmt /mplexer/cmd/telebit/telebit /telebit /cmd/telebit/telebit diff --git a/go.mod b/go.mod index e0cf9d1..df3317f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/caddyserver/certmagic v0.10.12 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-acme/lego/v3 v3.7.0 + github.com/go-chi/chi v4.1.1+incompatible github.com/gorilla/mux v1.7.4 github.com/gorilla/websocket v1.4.2 github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index ade5cec..e93830c 100644 --- a/go.sum +++ b/go.sum @@ -93,6 +93,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-acme/lego/v3 v3.4.0/go.mod h1:xYbLDuxq3Hy4bMUT1t9JIuz6GWIWb3m5X+TeTHYaT7M= github.com/go-acme/lego/v3 v3.7.0 h1:qC5/8/CbltyAE8fGLE6bGlqucj7pXc/vBxiLwLOsmAQ= github.com/go-acme/lego/v3 v3.7.0/go.mod h1:4eDjjYkAsDXyNcwN8IhhZAwxz9Ltiks1Zmpv0q20J7A= +github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4= +github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= diff --git a/mplexer/cmd/mgmt/mgmt.go b/mplexer/cmd/mgmt/mgmt.go new file mode 100644 index 0000000..d8841d3 --- /dev/null +++ b/mplexer/cmd/mgmt/mgmt.go @@ -0,0 +1,179 @@ +//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "time" + + "github.com/go-acme/lego/v3/challenge" + "github.com/go-acme/lego/v3/providers/dns/duckdns" + "github.com/go-acme/lego/v3/providers/dns/godaddy" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + _ "github.com/joho/godotenv/autoload" +) + +var ( + // GitRev refers to the abbreviated commit hash + GitRev = "0000000" + // GitVersion refers to the most recent tag, plus any commits made since then + GitVersion = "v0.0.0-pre0+0000000" + // GitTimestamp refers to the timestamp of the most recent commit + GitTimestamp = "0000-00-00T00:00:00+0000" +) + +func main() { + var err error + var provider challenge.Provider = nil // TODO is this concurrency-safe? + var presenters = make(chan *Challenge) + var cleanups = make(chan *Challenge) + + addr := flag.String("address", "", "IPv4 or IPv6 bind address") + port := flag.String("port", "3000", "port to listen to") + flag.Parse() + + if "" != os.Getenv("GODADDY_API_KEY") { + id := os.Getenv("GODADDY_API_KEY") + secret := os.Getenv("GODADDY_API_SECRET") + if provider, err = newGoDaddyDNSProvider(id, secret); nil != err { + panic(err) + } + } else if "" != os.Getenv("DUCKDNS_TOKEN") { + if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err { + panic(err) + } + } else { + panic("Must provide either DUCKDNS or GODADDY credentials") + } + + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Timeout(15 * time.Second)) + r.Use(middleware.Recoverer) + + // TODO add authorization header and validation + //r.Post("/api/dns/{domain:[a-z0-9\\.-]+}", func(w http.ResponseWriter, r *http.Request) { + //r.Post("/api/dns/*", func(w http.ResponseWriter, r *http.Request) { + r.Post("/api/dns/{domain}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + 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\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } + + domain := chi.URLParam(r, "domain") + //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 { + msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } + + w.Write([]byte("{\"success\":true}\n")) + }) + + // TODO ugly Delete, but wahtever + r.Delete("/api/dns/{domain}/{token}/{keyAuth}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + 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\"}"}` + http.Error(w, msg, http.StatusUnprocessableEntity) + return + } + + w.Write([]byte("{\"success\":true}\n")) + }) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome\n")) + }) + + go func() { + for { + // TODO make parallel? + // TODO make cancellable? + ch := <-presenters + err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth) + ch.error <- err + } + }() + + go func() { + for { + // TODO make parallel? + // TODO make cancellable? + ch := <-cleanups + ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth) + } + }() + + bind := *addr + ":" + *port + fmt.Println("Listening on", bind) + fmt.Fprintf(os.Stderr, "failed:", http.ListenAndServe(bind, r)) +} + +// A Challenge has the data necessary to create an ACME DNS-01 Key Authorization Digest. +type Challenge struct { + Domain string `json:"domain"` + Token string `json:"token"` + KeyAuth string `json:"key_authorization"` + error chan error +} + +type acmeProvider struct { + BaseURL string + provider challenge.Provider +} + +func (p *acmeProvider) Present(domain, token, keyAuth string) error { + return p.provider.Present(domain, token, keyAuth) +} + +func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error { + return p.provider.CleanUp(domain, token, keyAuth) +} + +// newDuckDNSProvider is for the sake of demoing the tunnel +func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) { + config := duckdns.NewDefaultConfig() + config.Token = token + return duckdns.NewDNSProviderConfig(config) +} + +// newGoDaddyDNSProvider is for the sake of demoing the tunnel +func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) { + config := godaddy.NewDefaultConfig() + config.APIKey = id + config.APISecret = secret + return godaddy.NewDNSProviderConfig(config) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1837729..e27119a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -31,6 +31,10 @@ github.com/go-acme/lego/v3/platform/wait github.com/go-acme/lego/v3/providers/dns/duckdns github.com/go-acme/lego/v3/providers/dns/godaddy github.com/go-acme/lego/v3/registration +# github.com/go-chi/chi v4.1.1+incompatible +## explicit +github.com/go-chi/chi +github.com/go-chi/chi/middleware # github.com/gorilla/mux v1.7.4 ## explicit github.com/gorilla/mux