diff --git a/bin/build-mgmt.sh b/bin/build-mgmt.sh new file mode 100644 index 0000000..3dd258b --- /dev/null +++ b/bin/build-mgmt.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +go generate -mod vendor ./... +go build -mod vendor -race -o ./telebit-mgmt cmd/mgmt/*.go + +my_version="$(./telebit-mgmt version | cut -d' ' -f4 | sd ':' '.' | sd 'T' '_')" + +rm -rf ~/srv/telebit-mgmt/bin/telebit-mgmt +rsync -avhP telebit-mgmt ~/srv/telebit-mgmt/bin/"telebit-mgmt-${my_version}" +ln -s ~/srv/telebit-mgmt/bin/"telebit-mgmt-${my_version}" ~/srv/telebit-mgmt/bin/telebit-mgmt diff --git a/bin/build-relay.sh b/bin/build-relay.sh new file mode 100644 index 0000000..c20c0f6 --- /dev/null +++ b/bin/build-relay.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +go generate -mod vendor ./... +go build -mod vendor -race -o ./telebit-relay cmd/telebit/*.go + +my_version="$(./telebit-relay version | cut -d' ' -f4 | sd ':' '.' | sd 'T' '_')" + +rm -rf ~/srv/telebit-relay/bin/telebit-relay +rsync -avhP telebit-relay ~/srv/telebit-relay/bin/"telebit-relay-${my_version}" +ln -s ~/srv/telebit-relay/bin/"telebit-relay-${my_version}" ~/srv/telebit-relay/bin/telebit-relay +sudo setcap 'cap_net_bind_service=+ep' ~/srv/telebit-relay/bin/"telebit-relay-${my_version}" diff --git a/bin/deploy-postgres.sh b/bin/deploy-postgres.sh new file mode 100644 index 0000000..4789dad --- /dev/null +++ b/bin/deploy-postgres.sh @@ -0,0 +1 @@ +webi postgres diff --git a/bin/psql-connect.sh b/bin/psql-connect.sh new file mode 100644 index 0000000..0a0dca6 --- /dev/null +++ b/bin/psql-connect.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +set -u + +source .env +psql "${DB_URL}" diff --git a/cmd/telebit/README.md b/cmd/telebit/README.md index 38f6ccf..509f6b2 100644 --- a/cmd/telebit/README.md +++ b/cmd/telebit/README.md @@ -47,6 +47,19 @@ See [`examples/relay.env`][relay-env] for detail explanations. Note: It is not necessary to specify the `--flags` when using the ENVs. +## API + +### Discovery + +Each telebit relay with expose its discovery endpoint at + +- `.well-known/telebit.app/index.json` + +The response will look something like + +```json +``` + ## System Services You can use `serviceman` to run `postgres`, `telebit`, and `telebit-mgmt` as system services diff --git a/cmd/telebit/telebit.go b/cmd/telebit/telebit.go index d83822b..0c31f8a 100644 --- a/cmd/telebit/telebit.go +++ b/cmd/telebit/telebit.go @@ -314,7 +314,7 @@ func parseFlagsAndENVs() { flag.BoolVar(&config.enableHTTP01, "acme-http-01", false, "enable HTTP-01 ACME challenges") flag.BoolVar(&config.enableTLSALPN01, "acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges") flag.StringVar(&config.logPath, "outfile", "", "where to direct output (default system logger or OS stdout)") - flag.StringVar(&config.acmeRelay, "acme-relay-url", "", "the base url of the ACME relay, if different from relay's directives") + flag.StringVar(&config.acmeRelay, "acme-relay-url", "", "the base url of the ACME relay, if different from telebit relay's directives") flag.StringVar(&config.acmeDNS01Relay, "acme-dns-01-relay-url", "", "the base url of the ACME DNS-01 relay, if different from ACME relay") flag.StringVar(&config.acmeHTTP01Relay, "acme-http-01-relay-url", "", "the base url of the ACME HTTP-01 relay, if different from ACME relay") flag.StringVar(&config.authURL, "auth-url", "", "the base url for authentication, if not the same as the tunnel relay") @@ -592,10 +592,15 @@ func parseFlagsAndENVs() { } func tokener() string { + secret := config.pairwiseSecret + if 0 == len(config.tunnelRelay) { + secret = ClientSecret + } + token := config.token if 0 == len(token) { var err error - token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway) + token, err = authstore.HMACToken(secret, config.leeway) if dbg.Debug { fmt.Printf("[debug] app_id: %q\n", VendorID) //fmt.Printf("[debug] client_secret: %q\n", ClientSecret) diff --git a/examples/mgmt-ping-as-admin.sh b/examples/mgmt-ping-as-admin.sh index 5a5d489..1993145 100644 --- a/examples/mgmt-ping-as-admin.sh +++ b/examples/mgmt-ping-as-admin.sh @@ -4,13 +4,15 @@ set -e set -u source .env -MGMT_URL="${MGMT_URL:-"http://localhost:3000/api"}" +MGMT_PORT="${MGMT_PORT:-3000}" +MGMT_URL="${MGMT_URL:-"http://localhost:${MGMT_PORT}/api"}" -TOKEN=$(go run cmd/signjwt/*.go \ - --expires-in 1m \ - --vendor-id "$VENDOR_ID" \ - --secret "$RELAY_SECRET" \ - --machine-ppid "$RELAY_SECRET" +TOKEN=$( + go run cmd/signjwt/*.go \ + --expires-in 1m \ + --vendor-id "$VENDOR_ID" \ + --secret "$RELAY_SECRET" \ + --machine-ppid "$RELAY_SECRET" ) echo "MGMT URL: $MGMT_URL" diff --git a/examples/mgmt.env b/examples/mgmt.env index 1d7843e..6a42f19 100644 --- a/examples/mgmt.env +++ b/examples/mgmt.env @@ -9,6 +9,7 @@ TUNNEL_DOMAIN=tunnel.example.com # For mgmt server itself SECRET=XxxxxxxxxxxxxxxX +VENDOR_ID='example.com' DB_URL=postgres://postgres:postgres@localhost:5432/postgres # PORT diff --git a/examples/relay.env b/examples/relay.env index e058efa..9e7c447 100644 --- a/examples/relay.env +++ b/examples/relay.env @@ -4,6 +4,7 @@ # This should be the same as the MGMT server secret # It is used for JWT token creation and verification SECRET=xxxxxxxxxxxxxxxx +VENDOR_ID='example.com' # VERBOSE=true # This will cause more verbose logs diff --git a/examples/signjwt-admin.sh b/examples/signjwt-admin.sh index acc7eac..175812c 100644 --- a/examples/signjwt-admin.sh +++ b/examples/signjwt-admin.sh @@ -2,20 +2,21 @@ source .env -TOKEN=$(go run cmd/signjwt/*.go \ - --expires-in 1m \ - --vendor-id "$VENDOR_ID" \ - --secret "$MGMT_SECRET" \ - --machine-ppid "$MGMT_SECRET" +TOKEN=$( + go run cmd/signjwt/*.go \ + --expires-in 1m \ + --vendor-id "$VENDOR_ID" \ + --secret "$MGMT_SECRET" \ + --machine-ppid "$MGMT_SECRET" ) echo "MGMT_TOKEN: $TOKEN" my_parts=$( -go run cmd/signjwt/*.go \ - --vendor-id "$VENDOR_ID" \ - --secret "$MGMT_SECRET" \ - --machine-ppid "$MGMT_SECRET" \ - --machine-ppid-only + go run cmd/signjwt/*.go \ + --vendor-id "$VENDOR_ID" \ + --secret "$MGMT_SECRET" \ + --machine-ppid "$MGMT_SECRET" \ + --machine-ppid-only ) my_ppid=$(echo $my_parts | cut -d' ' -f1) my_keyid=$(echo $my_parts | cut -d' ' -f2) diff --git a/internal/http01fs/http01fs.go b/internal/http01fs/http01fs.go new file mode 100644 index 0000000..bfb6268 --- /dev/null +++ b/internal/http01fs/http01fs.go @@ -0,0 +1,54 @@ +package http01fs + +import ( + "context" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/mholt/acmez/acme" +) + +const ( + challengeDir = ".well-known/acme-challenge" + tmpBase = "acme-tmp" +) + +var Provider Solver + +// Solver implements the challenge.Provider interface. +type Solver struct { + Path string +} + +// Present creates a HTTP-01 Challenge Token +func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error { + log.Println("Present HTTP-01 challenge solution for", ch.Identifier.Value) + + myBase := s.Path + if 0 == len(tmpBase) { + myBase = "acme-tmp" + } + + challengeBase := filepath.Join(myBase, ch.Identifier.Value, ".well-known/acme-challenge") + _ = os.MkdirAll(challengeBase, 0700) + tokenPath := filepath.Join(challengeBase, ch.Token) + return ioutil.WriteFile(tokenPath, []byte(ch.KeyAuthorization), 0600) +} + +// CleanUp deletes an HTTP-01 Challenge Token +func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error { + log.Println("CleanUp HTTP-01 challenge solution for", ch.Identifier.Value) + + myBase := s.Path + if 0 == len(tmpBase) { + myBase = "acme-tmp" + } + + // always try to remove, as there's no harm + tokenPath := filepath.Join(myBase, ch.Identifier.Value, challengeDir, ch.Token) + _ = os.Remove(tokenPath) + + return nil +} diff --git a/internal/mgmt/acmeroutes.go b/internal/mgmt/acmeroutes.go index 61a5455..091c968 100644 --- a/internal/mgmt/acmeroutes.go +++ b/internal/mgmt/acmeroutes.go @@ -1,17 +1,18 @@ package mgmt import ( + "context" "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" - "os" - "path/filepath" "strings" + "git.rootprojects.org/root/telebit/internal/http01fs" + "github.com/go-acme/lego/v3/challenge" "github.com/go-chi/chi" + "github.com/mholt/acmez/acme" ) const ( @@ -65,12 +66,20 @@ func handleDNSRoutes(r chi.Router) { r.Route("/http", handleACMEChallenges) } +func isSlugAllowed(domain, slug string) bool { + if "*" == slug { + return true + } + // ex: "abc.devices.example.com" has prefix "abc." + return strings.HasPrefix(domain, slug+".") +} + 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) { + if !ok || !isSlugAllowed(domain, claims.Slug) { msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}` http.Error(w, msg+"\n", http.StatusUnprocessableEntity) return @@ -94,10 +103,15 @@ func createChallenge(w http.ResponseWriter, r *http.Request) { 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) + provider := &http01fs.Provider + provider.Present(context.Background(), acme.Challenge{ + Token: ch.Token, + KeyAuthorization: ch.KeyAuth, + Identifier: acme.Identifier{ + Value: ch.Domain, + Type: "dns", // TODO is this correct?? + }, + }) } else { // TODO some additional error checking before the handoff //ch.error = make(chan error, 1) @@ -120,6 +134,7 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) { // TODO authenticate ch := Challenge{ + Type: chi.URLParam(r, "challengeType"), Domain: chi.URLParam(r, "domain"), Token: chi.URLParam(r, "token"), KeyAuth: chi.URLParam(r, "keyAuth"), @@ -131,10 +146,17 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) { 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) + provider := &http01fs.Provider + provider.CleanUp(context.Background(), acme.Challenge{ + Token: ch.Token, + KeyAuthorization: ch.KeyAuth, + Identifier: acme.Identifier{ + Value: ch.Domain, + Type: "dns", // TODO is this correct?? + }, + }) } else { + // TODO what if DNS-01 is not enabled? cleanups <- &ch err = <-ch.error } diff --git a/internal/mgmt/authstore/postgresql.go b/internal/mgmt/authstore/postgresql.go index bddd41c..6e67d8b 100644 --- a/internal/mgmt/authstore/postgresql.go +++ b/internal/mgmt/authstore/postgresql.go @@ -83,7 +83,7 @@ func (s *PGStore) SetMaster(secret string) error { machine_ppid=$1, shared_key=$1, public_key=$2, - deleted_at='1970-01-01 00:00:00' + deleted_at='1970-01-01 00:00:00' WHERE slug = '*' ` _, err = s.dbx.ExecContext(ctx, query, auth.MachinePPID, auth.PublicKey) @@ -223,6 +223,7 @@ func (s *PGStore) Get(id string) (*Authorization, error) { ` // if the id is actually the secret, we want the public form // (we do this to protect against a timing attack) + kid := id pubby := ToPublicKeyString(id) if len(id) > 24 { id = id[:24] diff --git a/internal/mgmt/route.go b/internal/mgmt/route.go index 53db559..95e963b 100644 --- a/internal/mgmt/route.go +++ b/internal/mgmt/route.go @@ -69,8 +69,12 @@ func RouteAll(r chi.Router) { // TODO make parallel? // TODO make cancellable? ch := <-presenters - err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth) - ch.error <- err + if nil != provider { + err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth) + ch.error <- err + } else { + ch.error <- fmt.Errorf("missing acme challenge provider for present") + } } }() @@ -79,7 +83,11 @@ func RouteAll(r chi.Router) { // TODO make parallel? // TODO make cancellable? ch := <-cleanups - ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth) + if nil != provider { + ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth) + } else { + ch.error <- fmt.Errorf("missing acme challenge provider for cleanup") + } } }()