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
|
ACME_EMAIL=letsencrypt@example.com
|
||||||
|
|
||||||
# For Let's Encrypt / ACME challenges
|
# For Let's Encrypt / ACME challenges
|
||||||
|
ACME_HTTP_01_RELAY_URL=http://localhost:3010/api/http
|
||||||
ACME_RELAY_URL=http://localhost:3010/api/dns
|
ACME_RELAY_URL=http://localhost:3010/api/dns
|
||||||
SECRET=xxxxxxxxxxxxxxxx
|
SECRET=xxxxxxxxxxxxxxxx
|
||||||
GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
|
@ -5,9 +5,12 @@
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"pathname": ""
|
"pathname": ""
|
||||||
},
|
},
|
||||||
"acme_dns_01_proxy": {
|
"_todo_acme_dns_01_proxy": {
|
||||||
"pathname": "dns"
|
"pathname": "dns"
|
||||||
},
|
},
|
||||||
|
"acme_http_01_proxy": {
|
||||||
|
"pathname": "http"
|
||||||
|
},
|
||||||
"pair_request": {
|
"pair_request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"pathname": "telebit.app/pair_request"
|
"pathname": "telebit.app/pair_request"
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
dns01 "git.rootprojects.org/root/telebit/dns01"
|
"git.rootprojects.org/root/telebit/internal/dns01"
|
||||||
|
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
|
|
|
@ -2,20 +2,38 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
"github.com/go-chi/chi"
|
"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 {
|
type Challenge struct {
|
||||||
Domain string `json:"domain"`
|
Type string `json:"type"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
KeyAuth string `json:"key_authorization"`
|
KeyAuth string `json:"key_authorization"`
|
||||||
error chan error
|
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 {
|
type acmeProvider struct {
|
||||||
|
@ -32,68 +50,97 @@ func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDNSRoutes(r chi.Router) {
|
func handleDNSRoutes(r chi.Router) {
|
||||||
r.Route("/dns", func(r chi.Router) {
|
handleACMEChallenges := func(r chi.Router) {
|
||||||
r.Post("/{domain}", func(w http.ResponseWriter, r *http.Request) {
|
r.Post("/{domain}", createChallenge)
|
||||||
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"))
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO ugly Delete, but whatever
|
// TODO ugly Delete, but whatever
|
||||||
r.Delete("/{domain}/{token}/{keyAuth}", func(w http.ResponseWriter, r *http.Request) {
|
r.Delete("/{domain}/{token}/{keyAuth}", deleteChallenge)
|
||||||
// TODO authenticate
|
r.Delete("/{domain}/{token}/{keyAuth}/{challengeType}", deleteChallenge)
|
||||||
|
}
|
||||||
|
|
||||||
ch := Challenge{
|
r.Route("/dns", handleACMEChallenges)
|
||||||
Domain: chi.URLParam(r, "domain"),
|
r.Route("/http", handleACMEChallenges)
|
||||||
Token: chi.URLParam(r, "token"),
|
}
|
||||||
KeyAuth: chi.URLParam(r, "keyAuth"),
|
|
||||||
error: make(chan error),
|
func createChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
//error: make(chan error, 1),
|
domain := chi.URLParam(r, "domain")
|
||||||
}
|
|
||||||
|
ctx := r.Context()
|
||||||
cleanups <- &ch
|
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
||||||
err := <-ch.error
|
if !ok || !strings.HasPrefix(domain+".", claims.Slug) {
|
||||||
if nil != err || "" == ch.Token || "" == ch.KeyAuth {
|
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
|
||||||
msg := `{"error":"expected json in the format {\"token\":\"xxx\",\"key_authorization\":\"yyy\"}", "code":"E_BAD_REQUEST"}`
|
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
||||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
ch := Challenge{}
|
||||||
w.Write([]byte("{\"success\":true}\n"))
|
|
||||||
})
|
// 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"
|
GitTimestamp = "0000-00-00T00:00:00+0000"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MWKey is a type guard
|
||||||
type MWKey string
|
type MWKey string
|
||||||
|
|
||||||
var store authstore.Store
|
var store authstore.Store
|
||||||
|
@ -44,6 +45,7 @@ func main() {
|
||||||
|
|
||||||
addr := flag.String("address", "", "IPv4 or IPv6 bind address")
|
addr := flag.String("address", "", "IPv4 or IPv6 bind address")
|
||||||
port := flag.String("port", "3000", "port to listen to")
|
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(
|
dbURL := flag.String(
|
||||||
"db-url",
|
"db-url",
|
||||||
"postgres://postgres:postgres@localhost/postgres",
|
"postgres://postgres:postgres@localhost/postgres",
|
||||||
|
@ -101,6 +103,10 @@ func main() {
|
||||||
_ = store.SetMaster(secret)
|
_ = store.SetMaster(secret)
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe(":"+challengesPort, routeStatic())
|
||||||
|
}()
|
||||||
|
|
||||||
bind := *addr + ":" + *port
|
bind := *addr + ":" + *port
|
||||||
fmt.Println("Listening on", bind)
|
fmt.Println("Listening on", bind)
|
||||||
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(bind, routeAll()))
|
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(bind, routeAll()))
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,6 +26,29 @@ type MgmtClaims struct {
|
||||||
var presenters = make(chan *Challenge)
|
var presenters = make(chan *Challenge)
|
||||||
var cleanups = 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 {
|
func routeAll() chi.Router {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -21,14 +20,14 @@ import (
|
||||||
|
|
||||||
telebit "git.rootprojects.org/root/telebit"
|
telebit "git.rootprojects.org/root/telebit"
|
||||||
"git.rootprojects.org/root/telebit/dbg"
|
"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/iplist"
|
||||||
"git.rootprojects.org/root/telebit/mgmt"
|
"git.rootprojects.org/root/telebit/mgmt"
|
||||||
"git.rootprojects.org/root/telebit/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/mgmt/authstore"
|
||||||
"git.rootprojects.org/root/telebit/table"
|
"git.rootprojects.org/root/telebit/table"
|
||||||
"git.rootprojects.org/root/telebit/tunnel"
|
"git.rootprojects.org/root/telebit/tunnel"
|
||||||
legoDns01 "github.com/go-acme/lego/v3/challenge/dns01"
|
legoDns01 "github.com/go-acme/lego/v3/challenge/dns01"
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
|
|
||||||
"github.com/coolaj86/certmagic"
|
"github.com/coolaj86/certmagic"
|
||||||
"github.com/denisbrodbeck/machineid"
|
"github.com/denisbrodbeck/machineid"
|
||||||
|
@ -81,36 +80,6 @@ var VendorID string
|
||||||
// ClientSecret may be baked in, or supplied via ENVs or --args
|
// ClientSecret may be baked in, or supplied via ENVs or --args
|
||||||
var ClientSecret string
|
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() {
|
func main() {
|
||||||
var domains []string
|
var domains []string
|
||||||
var forwards []Forward
|
var forwards []Forward
|
||||||
|
@ -128,6 +97,7 @@ func main() {
|
||||||
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
|
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")
|
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")
|
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
|
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")
|
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")
|
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) {
|
if 0 == len(*acmeRelay) {
|
||||||
*acmeRelay = os.Getenv("ACME_RELAY_URL")
|
*acmeRelay = os.Getenv("ACME_RELAY_URL")
|
||||||
}
|
}
|
||||||
|
if 0 == len(*acmeHTTP01Relay) {
|
||||||
|
*acmeHTTP01Relay = os.Getenv("ACME_HTTP_01_RELAY_URL")
|
||||||
|
}
|
||||||
|
|
||||||
if 0 == len(*email) {
|
if 0 == len(*email) {
|
||||||
*email = os.Getenv("ACME_EMAIL")
|
*email = os.Getenv("ACME_EMAIL")
|
||||||
|
@ -428,34 +401,52 @@ func main() {
|
||||||
}
|
}
|
||||||
authorizer = NewAuthorizer(*authURL)
|
authorizer = NewAuthorizer(*authURL)
|
||||||
|
|
||||||
provider, err := getACMEProvider(acmeRelay, token)
|
var dns01Solver *tbDns01.Solver
|
||||||
if nil != err {
|
if len(*acmeRelay) > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
provider, err := getACMEProvider(acmeRelay, token)
|
||||||
// it's possible for some providers this could be a failed network request,
|
if nil != err {
|
||||||
// but I think in the case of what we specifically support it's bad arguments
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(exitBadArguments)
|
// it's possible for some providers this could be a failed network request,
|
||||||
return
|
// 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)
|
fmt.Printf("Email: %q\n", *email)
|
||||||
|
|
||||||
acme := &telebit.ACME{
|
acme := &telebit.ACME{
|
||||||
Email: *email,
|
Email: *email,
|
||||||
StoragePath: *certpath,
|
StoragePath: *certpath,
|
||||||
Agree: *acmeAgree,
|
Agree: *acmeAgree,
|
||||||
Directory: *acmeDirectory,
|
Directory: *acmeDirectory,
|
||||||
DNS01Solver: &legoWrapper{
|
DNS01Solver: dns01Solver,
|
||||||
provider: provider,
|
/*
|
||||||
/*
|
options: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||||
options: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
ok, err := orig(fqdn, value)
|
||||||
ok, err := orig(fqdn, value)
|
if ok && dnsPropagationDelay > 0 {
|
||||||
if ok && dnsPropagationDelay > 0 {
|
fmt.Printf("[Telebit-ACME-DNS] sleeping an additional %s\n", dnsPropagationDelay)
|
||||||
fmt.Printf("[Telebit-ACME-DNS] sleeping an additional %s\n", dnsPropagationDelay)
|
time.Sleep(dnsPropagationDelay)
|
||||||
time.Sleep(dnsPropagationDelay)
|
}
|
||||||
}
|
return ok, err
|
||||||
return ok, err
|
}),
|
||||||
}),
|
*/
|
||||||
*/
|
HTTP01Solver: http01Solver,
|
||||||
dnsSolver: certmagic.DNS01Solver{},
|
|
||||||
},
|
|
||||||
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
||||||
EnableHTTPChallenge: *enableHTTP01,
|
EnableHTTPChallenge: *enableHTTP01,
|
||||||
EnableTLSALPNChallenge: *enableTLSALPN01,
|
EnableTLSALPNChallenge: *enableTLSALPN01,
|
||||||
|
|
|
@ -12,16 +12,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
telebit "git.rootprojects.org/root/telebit"
|
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"
|
"git.rootprojects.org/root/telebit/table"
|
||||||
|
|
||||||
"github.com/coolaj86/certmagic"
|
"github.com/coolaj86/certmagic"
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"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/duckdns"
|
||||||
"github.com/go-acme/lego/v3/providers/dns/godaddy"
|
"github.com/go-acme/lego/v3/providers/dns/godaddy"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
@ -89,16 +87,18 @@ func main() {
|
||||||
StoragePath: *certpath,
|
StoragePath: *certpath,
|
||||||
Agree: *acmeAgree,
|
Agree: *acmeAgree,
|
||||||
Directory: *acmeDirectory,
|
Directory: *acmeDirectory,
|
||||||
DNSProvider: provider,
|
DNS01Solver: tbDns01.NewSolver(provider),
|
||||||
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
/*
|
||||||
DNSChallengeOption: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
//DNSChallengeOption: legoDns01.DNSProviderOption,
|
||||||
ok, err := orig(fqdn, value)
|
DNSChallengeOption: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) {
|
||||||
if ok {
|
ok, err := orig(fqdn, value)
|
||||||
fmt.Println("[Telebit-ACME-DNS] sleeping an additional 5 seconds")
|
if ok {
|
||||||
time.Sleep(5 * time.Second)
|
fmt.Println("[Telebit-ACME-DNS] sleeping an additional 5 seconds")
|
||||||
}
|
time.Sleep(5 * time.Second)
|
||||||
return ok, err
|
}
|
||||||
}),
|
return ok, err
|
||||||
|
}),
|
||||||
|
*/
|
||||||
EnableHTTPChallenge: *enableHTTP01,
|
EnableHTTPChallenge: *enableHTTP01,
|
||||||
EnableTLSALPNChallenge: *enableTLSALPN01,
|
EnableTLSALPNChallenge: *enableTLSALPN01,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@ CLIENT_SUBJECT=newbie
|
||||||
TUNNEL_RELAY_URL=https://devices.example.com/
|
TUNNEL_RELAY_URL=https://devices.example.com/
|
||||||
CLIENT_SECRET=xxxxxxxxxxxxxxxx
|
CLIENT_SECRET=xxxxxxxxxxxxxxxx
|
||||||
LOCALS=https:$CLIENT_SUBJECT.devices.example.com:3000,https:*.$CLIENT_SUBJECT.devices.example.com:3000
|
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
|
#PORT_FORWARDS=3443:3001,8443:3002
|
||||||
#DUCKDNS_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
#DUCKDNS_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
|
|
@ -5,6 +5,7 @@ package dns01
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -14,8 +15,11 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"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/challenge/dns01"
|
||||||
"github.com/go-acme/lego/v3/platform/config/env"
|
"github.com/go-acme/lego/v3/platform/config/env"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Environment variables names.
|
// Environment variables names.
|
||||||
|
@ -180,3 +184,44 @@ func (d *DNSProvider) doRequest(method, uri string, msg interface{}) error {
|
||||||
|
|
||||||
return nil
|
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
|
// +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
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// these are binaries
|
||||||
_ "git.rootprojects.org/root/go-gitver"
|
_ "git.rootprojects.org/root/go-gitver"
|
||||||
_ "github.com/shurcooL/vfsgen"
|
_ "github.com/shurcooL/vfsgen"
|
||||||
_ "github.com/shurcooL/vfsgen/cmd/vfsgendev"
|
_ "github.com/shurcooL/vfsgen/cmd/vfsgendev"
|
|
@ -14,11 +14,13 @@ import (
|
||||||
// which will provide the tunnel endpoint. However, for the sake of testing,
|
// which will provide the tunnel endpoint. However, for the sake of testing,
|
||||||
// these things may happen out-of-order.
|
// these things may happen out-of-order.
|
||||||
type Endpoints struct {
|
type Endpoints struct {
|
||||||
ToS string `json:"terms_of_service"`
|
ToS string `json:"terms_of_service"`
|
||||||
APIHost string `json:"api_host"`
|
APIHost string `json:"api_host"`
|
||||||
Tunnel Endpoint `json:"tunnel"`
|
Tunnel Endpoint `json:"tunnel"`
|
||||||
Authenticate Endpoint `json:"authn"`
|
Authenticate Endpoint `json:"authn"`
|
||||||
DNS01Proxy Endpoint `json:"acme_dns_01_proxy"`
|
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/",
|
"terms_of_service": ":hostname/tos/",
|
||||||
|
@ -77,7 +79,13 @@ func Discover(relay string) (*Endpoints, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
directives.Authenticate.URL = endpointToURLString(directives.APIHost, directives.Authenticate)
|
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)
|
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
|
return directives, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue