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.
|
||||
|
||||
## 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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
Loading…
Reference in New Issue