WIP: fix http-01 challenges
This commit is contained in:
parent
79231a6de8
commit
975e6bab3e
|
@ -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
|
|
@ -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}"
|
|
@ -0,0 +1 @@
|
||||||
|
webi postgres
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
|
||||||
|
source .env
|
||||||
|
psql "${DB_URL}"
|
|
@ -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.
|
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
|
## System Services
|
||||||
|
|
||||||
You can use `serviceman` to run `postgres`, `telebit`, and `telebit-mgmt` as system services
|
You can use `serviceman` to run `postgres`, `telebit`, and `telebit-mgmt` as system services
|
||||||
|
|
|
@ -314,7 +314,7 @@ func parseFlagsAndENVs() {
|
||||||
flag.BoolVar(&config.enableHTTP01, "acme-http-01", false, "enable HTTP-01 ACME challenges")
|
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.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.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.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.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")
|
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 {
|
func tokener() string {
|
||||||
|
secret := config.pairwiseSecret
|
||||||
|
if 0 == len(config.tunnelRelay) {
|
||||||
|
secret = ClientSecret
|
||||||
|
}
|
||||||
|
|
||||||
token := config.token
|
token := config.token
|
||||||
if 0 == len(token) {
|
if 0 == len(token) {
|
||||||
var err error
|
var err error
|
||||||
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
|
token, err = authstore.HMACToken(secret, config.leeway)
|
||||||
if dbg.Debug {
|
if dbg.Debug {
|
||||||
fmt.Printf("[debug] app_id: %q\n", VendorID)
|
fmt.Printf("[debug] app_id: %q\n", VendorID)
|
||||||
//fmt.Printf("[debug] client_secret: %q\n", ClientSecret)
|
//fmt.Printf("[debug] client_secret: %q\n", ClientSecret)
|
||||||
|
|
|
@ -4,9 +4,11 @@ set -e
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
source .env
|
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 \
|
TOKEN=$(
|
||||||
|
go run cmd/signjwt/*.go \
|
||||||
--expires-in 1m \
|
--expires-in 1m \
|
||||||
--vendor-id "$VENDOR_ID" \
|
--vendor-id "$VENDOR_ID" \
|
||||||
--secret "$RELAY_SECRET" \
|
--secret "$RELAY_SECRET" \
|
||||||
|
|
|
@ -9,6 +9,7 @@ TUNNEL_DOMAIN=tunnel.example.com
|
||||||
|
|
||||||
# For mgmt server itself
|
# For mgmt server itself
|
||||||
SECRET=XxxxxxxxxxxxxxxX
|
SECRET=XxxxxxxxxxxxxxxX
|
||||||
|
VENDOR_ID='example.com'
|
||||||
DB_URL=postgres://postgres:postgres@localhost:5432/postgres
|
DB_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||||
|
|
||||||
# PORT
|
# PORT
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
# This should be the same as the MGMT server secret
|
# This should be the same as the MGMT server secret
|
||||||
# It is used for JWT token creation and verification
|
# It is used for JWT token creation and verification
|
||||||
SECRET=xxxxxxxxxxxxxxxx
|
SECRET=xxxxxxxxxxxxxxxx
|
||||||
|
VENDOR_ID='example.com'
|
||||||
|
|
||||||
# VERBOSE=true
|
# VERBOSE=true
|
||||||
# This will cause more verbose logs
|
# This will cause more verbose logs
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
source .env
|
source .env
|
||||||
|
|
||||||
TOKEN=$(go run cmd/signjwt/*.go \
|
TOKEN=$(
|
||||||
|
go run cmd/signjwt/*.go \
|
||||||
--expires-in 1m \
|
--expires-in 1m \
|
||||||
--vendor-id "$VENDOR_ID" \
|
--vendor-id "$VENDOR_ID" \
|
||||||
--secret "$MGMT_SECRET" \
|
--secret "$MGMT_SECRET" \
|
||||||
|
@ -11,7 +12,7 @@ TOKEN=$(go run cmd/signjwt/*.go \
|
||||||
echo "MGMT_TOKEN: $TOKEN"
|
echo "MGMT_TOKEN: $TOKEN"
|
||||||
|
|
||||||
my_parts=$(
|
my_parts=$(
|
||||||
go run cmd/signjwt/*.go \
|
go run cmd/signjwt/*.go \
|
||||||
--vendor-id "$VENDOR_ID" \
|
--vendor-id "$VENDOR_ID" \
|
||||||
--secret "$MGMT_SECRET" \
|
--secret "$MGMT_SECRET" \
|
||||||
--machine-ppid "$MGMT_SECRET" \
|
--machine-ppid "$MGMT_SECRET" \
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
package mgmt
|
package mgmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/http01fs"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -65,12 +66,20 @@ func handleDNSRoutes(r chi.Router) {
|
||||||
r.Route("/http", handleACMEChallenges)
|
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) {
|
func createChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
domain := chi.URLParam(r, "domain")
|
domain := chi.URLParam(r, "domain")
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
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"}`
|
msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}`
|
||||||
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
||||||
return
|
return
|
||||||
|
@ -94,10 +103,15 @@ func createChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
if "" == ch.Token || "" == ch.KeyAuth {
|
if "" == ch.Token || "" == ch.KeyAuth {
|
||||||
err = errors.New("missing token and/or key auth")
|
err = errors.New("missing token and/or key auth")
|
||||||
} else if strings.Contains(ch.Type, "http") {
|
} else if strings.Contains(ch.Type, "http") {
|
||||||
challengeBase := filepath.Join(tmpBase, ch.Domain, ".well-known/acme-challenge")
|
provider := &http01fs.Provider
|
||||||
_ = os.MkdirAll(challengeBase, 0700)
|
provider.Present(context.Background(), acme.Challenge{
|
||||||
tokenPath := filepath.Join(challengeBase, ch.Token)
|
Token: ch.Token,
|
||||||
err = ioutil.WriteFile(tokenPath, []byte(ch.KeyAuth), 0600)
|
KeyAuthorization: ch.KeyAuth,
|
||||||
|
Identifier: acme.Identifier{
|
||||||
|
Value: ch.Domain,
|
||||||
|
Type: "dns", // TODO is this correct??
|
||||||
|
},
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// TODO some additional error checking before the handoff
|
// TODO some additional error checking before the handoff
|
||||||
//ch.error = make(chan error, 1)
|
//ch.error = make(chan error, 1)
|
||||||
|
@ -120,6 +134,7 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO authenticate
|
// TODO authenticate
|
||||||
|
|
||||||
ch := Challenge{
|
ch := Challenge{
|
||||||
|
Type: chi.URLParam(r, "challengeType"),
|
||||||
Domain: chi.URLParam(r, "domain"),
|
Domain: chi.URLParam(r, "domain"),
|
||||||
Token: chi.URLParam(r, "token"),
|
Token: chi.URLParam(r, "token"),
|
||||||
KeyAuth: chi.URLParam(r, "keyAuth"),
|
KeyAuth: chi.URLParam(r, "keyAuth"),
|
||||||
|
@ -131,10 +146,17 @@ func deleteChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
if "" == ch.Token || "" == ch.KeyAuth {
|
if "" == ch.Token || "" == ch.KeyAuth {
|
||||||
err = errors.New("missing token and/or key auth")
|
err = errors.New("missing token and/or key auth")
|
||||||
} else if strings.Contains(ch.Type, "http") {
|
} else if strings.Contains(ch.Type, "http") {
|
||||||
// always try to remove, as there's no harm
|
provider := &http01fs.Provider
|
||||||
tokenPath := filepath.Join(tmpBase, ch.Domain, challengeDir, ch.Token)
|
provider.CleanUp(context.Background(), acme.Challenge{
|
||||||
_ = os.Remove(tokenPath)
|
Token: ch.Token,
|
||||||
|
KeyAuthorization: ch.KeyAuth,
|
||||||
|
Identifier: acme.Identifier{
|
||||||
|
Value: ch.Domain,
|
||||||
|
Type: "dns", // TODO is this correct??
|
||||||
|
},
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
// TODO what if DNS-01 is not enabled?
|
||||||
cleanups <- &ch
|
cleanups <- &ch
|
||||||
err = <-ch.error
|
err = <-ch.error
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,6 +223,7 @@ func (s *PGStore) Get(id string) (*Authorization, error) {
|
||||||
`
|
`
|
||||||
// if the id is actually the secret, we want the public form
|
// if the id is actually the secret, we want the public form
|
||||||
// (we do this to protect against a timing attack)
|
// (we do this to protect against a timing attack)
|
||||||
|
kid := id
|
||||||
pubby := ToPublicKeyString(id)
|
pubby := ToPublicKeyString(id)
|
||||||
if len(id) > 24 {
|
if len(id) > 24 {
|
||||||
id = id[:24]
|
id = id[:24]
|
||||||
|
|
|
@ -69,8 +69,12 @@ func RouteAll(r chi.Router) {
|
||||||
// TODO make parallel?
|
// TODO make parallel?
|
||||||
// TODO make cancellable?
|
// TODO make cancellable?
|
||||||
ch := <-presenters
|
ch := <-presenters
|
||||||
|
if nil != provider {
|
||||||
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
|
err := provider.Present(ch.Domain, ch.Token, ch.KeyAuth)
|
||||||
ch.error <- err
|
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 parallel?
|
||||||
// TODO make cancellable?
|
// TODO make cancellable?
|
||||||
ch := <-cleanups
|
ch := <-cleanups
|
||||||
|
if nil != provider {
|
||||||
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
|
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
|
||||||
|
} else {
|
||||||
|
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue