refactor!: external auth for ACME Challenges, and other mgmt routes
This commit is contained in:
parent
8536e20406
commit
a7f1398ba4
|
@ -10,6 +10,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/acmeroutes"
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt"
|
"git.rootprojects.org/root/telebit/internal/mgmt"
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ var (
|
||||||
serviceName = "telebit-mgmt"
|
serviceName = "telebit-mgmt"
|
||||||
|
|
||||||
// serviceDesc
|
// serviceDesc
|
||||||
serviceDesc = "Telebit Device Management"
|
//serviceDesc = "Telebit Device Management"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ver() string {
|
func ver() string {
|
||||||
|
@ -151,7 +152,12 @@ func main() {
|
||||||
_ = store.SetMaster(secret)
|
_ = store.SetMaster(secret)
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
mgmt.Init(store, provider)
|
var authURL string
|
||||||
|
if 0 == len(authURL) {
|
||||||
|
authURL = os.Getenv("AUTH_URL")
|
||||||
|
}
|
||||||
|
mgmt.Init(store)
|
||||||
|
acmeroutes.Init(provider)
|
||||||
|
|
||||||
if len(challengesPort) > 0 {
|
if len(challengesPort) > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -178,7 +184,7 @@ func main() {
|
||||||
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("TODO (json): " + ver() + "\n"))
|
w.Write([]byte("TODO (json): " + ver() + "\n"))
|
||||||
})
|
})
|
||||||
mgmt.RouteAll(r)
|
mgmt.RouteAll(r, authURL)
|
||||||
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, r))
|
fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -709,7 +709,7 @@ func fetchDirectivesAndRun() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mgmt.Ping(config.authURL, token)
|
err = authutil.Ping(config.authURL, token)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
|
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
|
||||||
//os.Exit(exitRetry)
|
//os.Exit(exitRetry)
|
||||||
|
@ -722,7 +722,7 @@ func fetchDirectivesAndRun() {
|
||||||
// re-create token unless no secret was supplied
|
// re-create token unless no secret was supplied
|
||||||
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
|
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
|
||||||
}
|
}
|
||||||
err = mgmt.Ping(config.authURL, token)
|
err = authutil.Ping(config.authURL, token)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
|
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
|
||||||
//os.Exit(exitRetry)
|
//os.Exit(exitRetry)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package mgmt
|
package acmeroutes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -6,8 +6,10 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/authutil"
|
||||||
"git.rootprojects.org/root/telebit/internal/http01fs"
|
"git.rootprojects.org/root/telebit/internal/http01fs"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
|
@ -37,11 +39,50 @@ type Identifier struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
type acmeProvider struct {
|
type acmeProvider struct {
|
||||||
BaseURL string
|
BaseURL string
|
||||||
provider challenge.Provider
|
provider challenge.Provider
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var provider challenge.Provider = nil
|
||||||
|
var presenters = make(chan *Challenge)
|
||||||
|
var cleanups = make(chan *Challenge)
|
||||||
|
|
||||||
|
// Init initializes some package variables
|
||||||
|
func Init(p challenge.Provider) {
|
||||||
|
provider = p
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
// TODO make parallel?
|
||||||
|
// TODO make cancellable?
|
||||||
|
ch := <-presenters
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
// TODO make parallel?
|
||||||
|
// TODO make cancellable?
|
||||||
|
ch := <-cleanups
|
||||||
|
if nil != provider {
|
||||||
|
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
|
||||||
|
} else {
|
||||||
|
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
func (p *acmeProvider) Present(domain, token, keyAuth string) error {
|
func (p *acmeProvider) Present(domain, token, keyAuth string) error {
|
||||||
return p.provider.Present(domain, token, keyAuth)
|
return p.provider.Present(domain, token, keyAuth)
|
||||||
}
|
}
|
||||||
|
@ -49,8 +90,33 @@ func (p *acmeProvider) Present(domain, token, keyAuth string) error {
|
||||||
func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error {
|
func (p *acmeProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return p.provider.CleanUp(domain, token, keyAuth)
|
return p.provider.CleanUp(domain, token, keyAuth)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
func handleACMEChallengeRoutes(r chi.Router) {
|
// GetACMEChallenges fetches stored HTTP-01 challenges
|
||||||
|
func GetACMEChallenges(w http.ResponseWriter, r *http.Request) {
|
||||||
|
//token := chi.URLParam(r, "token")
|
||||||
|
host := r.Host
|
||||||
|
/*
|
||||||
|
// TODO TrustProxy option?
|
||||||
|
xHost := r.Header.Get("X-Forwarded-Host")
|
||||||
|
//log.Printf("[debug] Host: %q\n[debug] X-Host: %q", host, xHost)
|
||||||
|
if len(xHost) > 0 {
|
||||||
|
host = xHost
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// disallow FS characters
|
||||||
|
if strings.ContainsAny(host, "/:|\\") {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
tokenPath := filepath.Join(tmpBase, host)
|
||||||
|
|
||||||
|
fsrv := http.FileServer(http.Dir(tokenPath))
|
||||||
|
fsrv.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleACMEChallengeRoutes allows storing ACME challenges for relay
|
||||||
|
func HandleACMEChallengeRoutes(r chi.Router) {
|
||||||
handleACMEChallenges := func(r chi.Router) {
|
handleACMEChallenges := func(r chi.Router) {
|
||||||
r.Post("/{domain}", createChallenge)
|
r.Post("/{domain}", createChallenge)
|
||||||
|
|
||||||
|
@ -78,7 +144,7 @@ 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(authutil.MWKey("claims")).(*authutil.Claims)
|
||||||
if !ok || !isSlugAllowed(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)
|
||||||
|
@ -103,8 +169,8 @@ 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") {
|
||||||
provider := &http01fs.Provider
|
http01Provider := &http01fs.Provider
|
||||||
provider.Present(context.Background(), acme.Challenge{
|
http01Provider.Present(context.Background(), acme.Challenge{
|
||||||
Token: ch.Token,
|
Token: ch.Token,
|
||||||
KeyAuthorization: ch.KeyAuth,
|
KeyAuthorization: ch.KeyAuth,
|
||||||
Identifier: acme.Identifier{
|
Identifier: acme.Identifier{
|
||||||
|
@ -146,8 +212,8 @@ 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") {
|
||||||
provider := &http01fs.Provider
|
http01Provider := &http01fs.Provider
|
||||||
provider.CleanUp(context.Background(), acme.Challenge{
|
http01Provider.CleanUp(context.Background(), acme.Challenge{
|
||||||
Token: ch.Token,
|
Token: ch.Token,
|
||||||
KeyAuthorization: ch.KeyAuth,
|
KeyAuthorization: ch.KeyAuth,
|
||||||
Identifier: acme.Identifier{
|
Identifier: acme.Identifier{
|
|
@ -0,0 +1,85 @@
|
||||||
|
package authutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorizer is called when a new client connects and we need to know something about it
|
||||||
|
type Authorizer func(*http.Request) (*Grants, error)
|
||||||
|
|
||||||
|
var NotAuthorizedContent = []byte("{ \"error\": \"not authorized\" }\n")
|
||||||
|
|
||||||
|
// NewAuthorizer will create a new (proxiable) token verifier
|
||||||
|
func NewAuthorizer(authURL string) Authorizer {
|
||||||
|
return func(r *http.Request) (*Grants, error) {
|
||||||
|
// do we have a valid wss_client?
|
||||||
|
|
||||||
|
fmt.Printf("[authz] Authorization = %s\n", r.Header.Get("Authorization"))
|
||||||
|
var tokenString string
|
||||||
|
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
|
||||||
|
// TODO handle Basic auth tokens as well
|
||||||
|
tokenString = auth[1]
|
||||||
|
}
|
||||||
|
if "" == tokenString {
|
||||||
|
// Browsers do not allow Authorization Headers and must use access_token query string
|
||||||
|
tokenString = r.URL.Query().Get("access_token")
|
||||||
|
}
|
||||||
|
if "" != r.URL.Query().Get("access_token") {
|
||||||
|
r.URL.Query().Set("access_token", "[redacted]")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[authz] authURL = %s\n", authURL)
|
||||||
|
fmt.Printf("[authz] token = %s\n", tokenString)
|
||||||
|
grants, err := Inspect(authURL, tokenString)
|
||||||
|
|
||||||
|
if nil != err {
|
||||||
|
fmt.Printf("[authorizer] error inspecting %q: %s\ntoken: %s\n", authURL, err, tokenString)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if "" != r.URL.Query().Get("access_token") {
|
||||||
|
r.URL.Query().Set("access_token", "[redacted:"+grants.Subject+"]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return grants, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grants are verified token Claims
|
||||||
|
type Grants struct {
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Audience string `json:"aud"`
|
||||||
|
Domains []string `json:"domains"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect will verify a token and return its details, decoded
|
||||||
|
func Inspect(authURL, token string) (*Grants, error) {
|
||||||
|
inspectURL := strings.TrimSuffix(authURL, "/inspect") + "/inspect"
|
||||||
|
if dbg.Debug {
|
||||||
|
fmt.Fprintf(os.Stderr, "[debug] telebit.Inspect(\n\tinspectURL = %s,\n\ttoken = %s,\n)\n", inspectURL, token)
|
||||||
|
}
|
||||||
|
msg, err := Request("GET", inspectURL, token, nil)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if nil == msg {
|
||||||
|
return nil, fmt.Errorf("invalid response")
|
||||||
|
}
|
||||||
|
|
||||||
|
grants := &Grants{}
|
||||||
|
err = json.NewDecoder(msg).Decode(grants)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if "" == grants.Subject {
|
||||||
|
fmt.Fprintf(os.Stderr, "TODO update mgmt server to show Subject: %q\n", msg)
|
||||||
|
grants.Subject = strings.Split(grants.Domains[0], ".")[0]
|
||||||
|
}
|
||||||
|
return grants, nil
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package authutil
|
||||||
|
|
||||||
|
import "github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
|
// MWKey is a type guard for context.Value
|
||||||
|
type MWKey string
|
||||||
|
|
||||||
|
// Claims includes a Slug, for backwards compatibility
|
||||||
|
type Claims struct {
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
jwt.StandardClaims
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package authutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SuccessResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request makes an HTTP request the way we like...
|
||||||
|
func Request(method, fullurl, token string, payload io.Reader) (io.Reader, error) {
|
||||||
|
HTTPClient := &http.Client{
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, fullurl, payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(token) > 0 {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
if nil != payload {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return nil, fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewBuffer(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Ping(authURL, token string) error {
|
||||||
|
msg, err := Request("POST", authURL+"/ping", token, nil)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if nil == msg {
|
||||||
|
return fmt.Errorf("invalid response")
|
||||||
|
}
|
||||||
|
resp := SuccessResponse{}
|
||||||
|
err = json.NewDecoder(msg).Decode(&resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if true != resp.Success {
|
||||||
|
return fmt.Errorf("expected successful response")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -7,34 +7,11 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/authutil"
|
||||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||||
"git.rootprojects.org/root/telebit/internal/telebit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SuccessResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ping(authURL, token string) error {
|
|
||||||
msg, err := telebit.Request("POST", authURL+"/ping", token, nil)
|
|
||||||
if nil != err {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if nil == msg {
|
|
||||||
return fmt.Errorf("invalid response")
|
|
||||||
}
|
|
||||||
resp := SuccessResponse{}
|
|
||||||
err = json.NewDecoder(msg).Decode(&resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if true != resp.Success {
|
|
||||||
return fmt.Errorf("expected successful response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Register(authURL, secret, ppid string) (kid string, err error) {
|
func Register(authURL, secret, ppid string) (kid string, err error) {
|
||||||
pub := authstore.ToPublicKeyString(ppid)
|
pub := authstore.ToPublicKeyString(ppid)
|
||||||
jsons := fmt.Sprintf(`{ "machine_ppid": "%s", "public_key": "%s" }`, ppid, pub)
|
jsons := fmt.Sprintf(`{ "machine_ppid": "%s", "public_key": "%s" }`, ppid, pub)
|
||||||
|
@ -43,7 +20,7 @@ func Register(authURL, secret, ppid string) (kid string, err error) {
|
||||||
if dbg.Debug {
|
if dbg.Debug {
|
||||||
fmt.Fprintf(os.Stderr, "[debug] authURL=%s, secret=%s, ppid=%s\n", fullURL, secret, jsons)
|
fmt.Fprintf(os.Stderr, "[debug] authURL=%s, secret=%s, ppid=%s\n", fullURL, secret, jsons)
|
||||||
}
|
}
|
||||||
msg, err := telebit.Request("POST", fullURL, "", jsonb)
|
msg, err := authutil.Request("POST", fullURL, "", jsonb)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/authutil"
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
@ -21,7 +22,7 @@ func handleDeviceRoutes(r chi.Router) {
|
||||||
r.Use(func(next http.Handler) http.Handler {
|
r.Use(func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
|
||||||
if !ok || "*" != claims.Slug {
|
if !ok || "*" != claims.Slug {
|
||||||
msg := `{"error":"missing or invalid authorization token", "code":"E_TOKEN"}`
|
msg := `{"error":"missing or invalid authorization token", "code":"E_TOKEN"}`
|
||||||
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
http.Error(w, msg+"\n", http.StatusUnprocessableEntity)
|
||||||
|
|
|
@ -2,14 +2,10 @@ package mgmt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var store authstore.Store
|
var store authstore.Store
|
||||||
|
|
||||||
var provider challenge.Provider = nil
|
|
||||||
|
|
||||||
// DeviceDomain is the base hostname used for devices, such as devices.example.com
|
// DeviceDomain is the base hostname used for devices, such as devices.example.com
|
||||||
// which has devices as foo.devices.example.com
|
// which has devices as foo.devices.example.com
|
||||||
var DeviceDomain string
|
var DeviceDomain string
|
||||||
|
@ -18,11 +14,7 @@ var DeviceDomain string
|
||||||
// ( currently NOT used, but will be used for wss://RELAY_DOMAIN/ )
|
// ( currently NOT used, but will be used for wss://RELAY_DOMAIN/ )
|
||||||
var RelayDomain string
|
var RelayDomain string
|
||||||
|
|
||||||
// MWKey is a type guard
|
|
||||||
type MWKey string
|
|
||||||
|
|
||||||
// Init initializes some package variables
|
// Init initializes some package variables
|
||||||
func Init(s authstore.Store, p challenge.Provider) {
|
func Init(s authstore.Store) {
|
||||||
store = s
|
store = s
|
||||||
provider = p
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,11 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/telebit/internal/acmeroutes"
|
||||||
|
"git.rootprojects.org/root/telebit/internal/authutil"
|
||||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||||
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
|
||||||
|
|
||||||
|
@ -19,15 +20,7 @@ import (
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MgmtClaims includes a Slug, for backwards compatibility
|
// RouteStatic handles the not-so-dynamic routes
|
||||||
type MgmtClaims struct {
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
jwt.StandardClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
var presenters = make(chan *Challenge)
|
|
||||||
var cleanups = make(chan *Challenge)
|
|
||||||
|
|
||||||
func RouteStatic(r chi.Router) chi.Router {
|
func RouteStatic(r chi.Router) chi.Router {
|
||||||
r.Route("/", func(r chi.Router) {
|
r.Route("/", func(r chi.Router) {
|
||||||
|
|
||||||
|
@ -35,276 +28,270 @@ func RouteStatic(r chi.Router) chi.Router {
|
||||||
r.Use(middleware.Timeout(15 * time.Second))
|
r.Use(middleware.Timeout(15 * time.Second))
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
r.Get("/.well-known/acme-challenge/{token}", getACMEChallenges)
|
r.Get("/.well-known/acme-challenge/{token}", acmeroutes.GetACMEChallenges)
|
||||||
})
|
})
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func getACMEChallenges(w http.ResponseWriter, r *http.Request) {
|
// RouteAll will generate the routes
|
||||||
//token := chi.URLParam(r, "token")
|
func RouteAll(r chi.Router, authURL string) {
|
||||||
host := r.Host
|
|
||||||
/*
|
|
||||||
// TODO TrustProxy option?
|
|
||||||
xHost := r.Header.Get("X-Forwarded-Host")
|
|
||||||
//log.Printf("[debug] Host: %q\n[debug] X-Host: %q", host, xHost)
|
|
||||||
if len(xHost) > 0 {
|
|
||||||
host = xHost
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// disallow FS characters
|
|
||||||
if strings.ContainsAny(host, "/:|\\") {
|
|
||||||
host = ""
|
|
||||||
}
|
|
||||||
tokenPath := filepath.Join(tmpBase, host)
|
|
||||||
|
|
||||||
fsrv := http.FileServer(http.Dir(tokenPath))
|
|
||||||
fsrv.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RouteAll(r chi.Router) {
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// TODO make parallel?
|
|
||||||
// TODO make cancellable?
|
|
||||||
ch := <-presenters
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// TODO make parallel?
|
|
||||||
// TODO make cancellable?
|
|
||||||
ch := <-cleanups
|
|
||||||
if nil != provider {
|
|
||||||
ch.error <- provider.CleanUp(ch.Domain, ch.Token, ch.KeyAuth)
|
|
||||||
} else {
|
|
||||||
ch.error <- fmt.Errorf("missing acme challenge provider for cleanup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r.Route("/", func(r chi.Router) {
|
r.Route("/", func(r chi.Router) {
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Timeout(15 * time.Second))
|
r.Use(middleware.Timeout(15 * time.Second))
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
r.Get("/.well-known/acme-challenge/{token}", getACMEChallenges)
|
r.Get("/.well-known/acme-challenge/{token}", acmeroutes.GetACMEChallenges)
|
||||||
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
r.Route("/api", func(r chi.Router) {
|
||||||
r.Use(func(next http.Handler) http.Handler {
|
r.Use(newClaimsGetter(authURL))
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
var tokenString string
|
acmeroutes.HandleACMEChallengeRoutes(r)
|
||||||
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
|
|
||||||
// TODO handle Basic auth tokens as well
|
|
||||||
tokenString = auth[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
//var err2 error = nil
|
|
||||||
tok, err := jwt.ParseWithClaims(
|
|
||||||
tokenString,
|
|
||||||
&MgmtClaims{},
|
|
||||||
func(token *jwt.Token) (interface{}, error) {
|
|
||||||
if dbg.Debug {
|
|
||||||
fmt.Println("parsed jwt", token)
|
|
||||||
}
|
|
||||||
kid, ok := token.Header["kid"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("missing jwt header 'kid' (key id)")
|
|
||||||
}
|
|
||||||
auth, err := store.Get(kid)
|
|
||||||
if nil != err {
|
|
||||||
return nil, fmt.Errorf("invalid jwt header 'kid' (key id)")
|
|
||||||
}
|
|
||||||
|
|
||||||
claims := token.Claims.(*MgmtClaims)
|
|
||||||
jti := claims.Id
|
|
||||||
if "" == jti {
|
|
||||||
return nil, fmt.Errorf("missing jwt payload 'jti' (jwt id / nonce)")
|
|
||||||
}
|
|
||||||
iat := claims.IssuedAt
|
|
||||||
if 0 == iat {
|
|
||||||
return nil, fmt.Errorf("missing jwt payload 'iat' (issued at)")
|
|
||||||
}
|
|
||||||
exp := claims.ExpiresAt
|
|
||||||
if 0 == exp {
|
|
||||||
return nil, fmt.Errorf("missing jwt payload 'exp' (expires at)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if "" != claims.Slug {
|
|
||||||
return nil, fmt.Errorf("extra jwt payload 'slug' (unknown)")
|
|
||||||
}
|
|
||||||
claims.Slug = auth.Slug
|
|
||||||
if "" != claims.Subject && auth.Slug != claims.Subject {
|
|
||||||
return nil, fmt.Errorf("invalid jwt payload 'sub' (mismatch)")
|
|
||||||
}
|
|
||||||
claims.Subject = claims.Slug
|
|
||||||
claims.Issuer = DeviceDomain
|
|
||||||
claims.Audience = fmt.Sprintf("wss://%s/ws", RelayDomain)
|
|
||||||
|
|
||||||
/*
|
|
||||||
// a little misdirection there
|
|
||||||
mac := hmac.New(sha256.New, auth.MachinePPID)
|
|
||||||
_ = mac.Write([]byte(auth.SharedKey))
|
|
||||||
_ = mac.Write([]byte(fmt.Sprintf("%d", exp)))
|
|
||||||
return []byte(auth.SharedKey), nil
|
|
||||||
*/
|
|
||||||
|
|
||||||
//fmt.Println("ppid:", auth.MachinePPID)
|
|
||||||
|
|
||||||
return []byte(auth.MachinePPID), nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
if nil != tok {
|
|
||||||
if tok.Valid {
|
|
||||||
ctx = context.WithValue(ctx, MWKey("token"), tok)
|
|
||||||
ctx = context.WithValue(ctx, MWKey("claims"), tok.Claims)
|
|
||||||
ctx = context.WithValue(ctx, MWKey("valid"), true)
|
|
||||||
fmt.Println("[auth] Token is fully valid:")
|
|
||||||
fmt.Println(ctx.Value(MWKey("claims")))
|
|
||||||
} else {
|
|
||||||
fmt.Println("[auth] Token was parsed, but NOT valid:", tok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nil != err {
|
|
||||||
fmt.Println("[auth] Token is NOT valid due to error:", err)
|
|
||||||
ctx = context.WithValue(ctx, MWKey("error"), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
handleACMEChallengeRoutes(r)
|
|
||||||
handleDeviceRoutes(r)
|
handleDeviceRoutes(r)
|
||||||
|
if len(authURL) > 0 {
|
||||||
r.Get("/inspect", func(w http.ResponseWriter, r *http.Request) {
|
fmt.Printf("Delegating auth to %s\nIgnoring '/api/inspect'\n", authURL)
|
||||||
ctx := r.Context()
|
} else {
|
||||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
r.Get("/inspect", inspectToken)
|
||||||
if !ok {
|
}
|
||||||
msg := `{"error":"failure to ping: 3", "code":"E_TOKEN"}`
|
r.Route("/register-device", registerDevice)
|
||||||
fmt.Println("touch no claims", claims)
|
r.Post("/ping", pong)
|
||||||
http.Error(w, msg+"\n", http.StatusBadRequest)
|
r.Get("/", hello)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(fmt.Sprintf(
|
|
||||||
`{ "sub": "%s", "aud": "%s", "domains": [ "%s.%s" ], "ports": [] }`+"\n",
|
|
||||||
claims.Subject,
|
|
||||||
claims.Audience,
|
|
||||||
claims.Slug,
|
|
||||||
DeviceDomain,
|
|
||||||
)))
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Route("/register-device", func(r chi.Router) {
|
|
||||||
// r.Use() // must NOT have slug '*'
|
|
||||||
|
|
||||||
r.Post("/{otp}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sharedKey := chi.URLParam(r, "otp")
|
|
||||||
original, err := store.Get(sharedKey)
|
|
||||||
if nil != err {
|
|
||||||
msg := `{"error":"not found", "code":"E_NOT_FOUND"}`
|
|
||||||
log.Printf("/api/register-device/\n")
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, msg, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(original.MachinePPID) > 0 {
|
|
||||||
msg := `{"error":"the presented key has already been used", "code":"E_EXIST"}`
|
|
||||||
log.Printf("/api/register-device/\n")
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, msg, http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := &authstore.Authorization{}
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
err = decoder.Decode(&auth)
|
|
||||||
// MachinePPID and PublicKey are required. ID must NOT be set. Slug is ignored.
|
|
||||||
epoch := time.Time{}
|
|
||||||
auth.SharedKey = sharedKey
|
|
||||||
if nil != err || "" != auth.ID || "" == auth.MachinePPID ||
|
|
||||||
"" == auth.PublicKey || "" == auth.SharedKey ||
|
|
||||||
epoch != auth.CreatedAt || epoch != auth.UpdatedAt || epoch != auth.DeletedAt {
|
|
||||||
msg, _ := json.Marshal(&struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: "expected JSON in the format {\"machine_ppid\":\"\",\"public_key\":\"\"}",
|
|
||||||
})
|
|
||||||
http.Error(w, string(msg), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO hash the PPID and check against the Public Key?
|
|
||||||
pub := authstore.ToPublicKeyString(auth.MachinePPID)
|
|
||||||
if pub != auth.PublicKey {
|
|
||||||
msg, _ := json.Marshal(&struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}{
|
|
||||||
Error: "expected `public_key` to be the first 24 bytes of the hash of the `machine_ppid`",
|
|
||||||
})
|
|
||||||
http.Error(w, string(msg), http.StatusUnprocessableEntity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
original.PublicKey = auth.PublicKey
|
|
||||||
original.MachinePPID = auth.MachinePPID
|
|
||||||
err = store.Set(original)
|
|
||||||
if nil != err {
|
|
||||||
msg := `{"error":"not really sure what happened, but it didn't go well (check the logs)", "code":"E_SERVER"}`
|
|
||||||
log.Printf("/api/register-device/\n")
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, msg, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _ := json.Marshal(auth)
|
|
||||||
w.Write(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Post("/ping", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims)
|
|
||||||
if !ok {
|
|
||||||
msg := `{"error":"failure to ping: 1", "code":"E_TOKEN"}`
|
|
||||||
http.Error(w, msg+"\n", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("ping pong??", claims)
|
|
||||||
err := store.Touch(claims.Slug)
|
|
||||||
if nil != err {
|
|
||||||
fmt.Fprintf(os.Stderr, "touch err %s\n", err)
|
|
||||||
var msg string
|
|
||||||
if err == authstore.ErrNotFound {
|
|
||||||
// if the token is valid, touch should ALWAYS work
|
|
||||||
msg = `{"error":"failure to ping: inconsistent database data", "code":"E_SERVER"}`
|
|
||||||
} else {
|
|
||||||
msg = `{"error":"failure to ping: 2", "code":"E_SERVER"}`
|
|
||||||
}
|
|
||||||
http.Error(w, msg+"\n", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Write([]byte(`{ "success": true }` + "\n"))
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Write([]byte("Hello\n"))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO consolidate 'newClaimsGetter' and 'getVerifiedClaims'
|
||||||
|
func newClaimsGetter(authURL string) func(next http.Handler) http.Handler {
|
||||||
|
if 0 == len(authURL) {
|
||||||
|
return getVerifiedClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizer := authutil.NewAuthorizer(authURL)
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
grants, err := authorizer(r)
|
||||||
|
if nil != err {
|
||||||
|
log.Println("authorization failed", err)
|
||||||
|
w.Write(authutil.NotAuthorizedContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use Claims rather than Grants
|
||||||
|
claims := &authutil.Claims{
|
||||||
|
Slug: grants.Subject,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
Subject: grants.Subject,
|
||||||
|
Audience: grants.Audience,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, authutil.MWKey("claims"), claims)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVerifiedClaims(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
var tokenString string
|
||||||
|
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
|
||||||
|
// TODO handle Basic auth tokens as well
|
||||||
|
tokenString = auth[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
//var err2 error = nil
|
||||||
|
tok, err := jwt.ParseWithClaims(
|
||||||
|
tokenString,
|
||||||
|
&authutil.Claims{},
|
||||||
|
func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if dbg.Debug {
|
||||||
|
fmt.Println("parsed jwt", token)
|
||||||
|
}
|
||||||
|
kid, ok := token.Header["kid"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("missing jwt header 'kid' (key id)")
|
||||||
|
}
|
||||||
|
auth, err := store.Get(kid)
|
||||||
|
if nil != err {
|
||||||
|
return nil, fmt.Errorf("invalid jwt header 'kid' (key id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := token.Claims.(*authutil.Claims)
|
||||||
|
jti := claims.Id
|
||||||
|
if "" == jti {
|
||||||
|
return nil, fmt.Errorf("missing jwt payload 'jti' (jwt id / nonce)")
|
||||||
|
}
|
||||||
|
iat := claims.IssuedAt
|
||||||
|
if 0 == iat {
|
||||||
|
return nil, fmt.Errorf("missing jwt payload 'iat' (issued at)")
|
||||||
|
}
|
||||||
|
exp := claims.ExpiresAt
|
||||||
|
if 0 == exp {
|
||||||
|
return nil, fmt.Errorf("missing jwt payload 'exp' (expires at)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" != claims.Slug {
|
||||||
|
return nil, fmt.Errorf("extra jwt payload 'slug' (unknown)")
|
||||||
|
}
|
||||||
|
claims.Slug = auth.Slug
|
||||||
|
if "" != claims.Subject && auth.Slug != claims.Subject {
|
||||||
|
return nil, fmt.Errorf("invalid jwt payload 'sub' (mismatch)")
|
||||||
|
}
|
||||||
|
claims.Subject = claims.Slug
|
||||||
|
claims.Issuer = DeviceDomain
|
||||||
|
claims.Audience = fmt.Sprintf("wss://%s/ws", RelayDomain)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// a little misdirection there
|
||||||
|
mac := hmac.New(sha256.New, auth.MachinePPID)
|
||||||
|
_ = mac.Write([]byte(auth.SharedKey))
|
||||||
|
_ = mac.Write([]byte(fmt.Sprintf("%d", exp)))
|
||||||
|
return []byte(auth.SharedKey), nil
|
||||||
|
*/
|
||||||
|
|
||||||
|
//fmt.Println("ppid:", auth.MachinePPID)
|
||||||
|
|
||||||
|
return []byte(auth.MachinePPID), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if nil != tok {
|
||||||
|
if tok.Valid {
|
||||||
|
ctx = context.WithValue(ctx, authutil.MWKey("token"), tok)
|
||||||
|
ctx = context.WithValue(ctx, authutil.MWKey("claims"), tok.Claims)
|
||||||
|
ctx = context.WithValue(ctx, authutil.MWKey("valid"), true)
|
||||||
|
fmt.Println("[auth] Token is fully valid:")
|
||||||
|
fmt.Println(ctx.Value(authutil.MWKey("claims")))
|
||||||
|
} else {
|
||||||
|
fmt.Println("[auth] Token was parsed, but NOT valid:", tok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
fmt.Println("[auth] Token is NOT valid due to error:", err)
|
||||||
|
ctx = context.WithValue(ctx, authutil.MWKey("error"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func inspectToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
|
||||||
|
if !ok {
|
||||||
|
msg := `{"error":"failure to ping: 3", "code":"E_TOKEN"}`
|
||||||
|
fmt.Println("touch no claims", claims)
|
||||||
|
http.Error(w, msg+"\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(fmt.Sprintf(
|
||||||
|
`{ "sub": "%s", "aud": "%s", "domains": [ "%s.%s" ], "ports": [] }`+"\n",
|
||||||
|
claims.Subject,
|
||||||
|
claims.Audience,
|
||||||
|
claims.Slug,
|
||||||
|
DeviceDomain,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDevice(r chi.Router) {
|
||||||
|
// r.Use() // must NOT have slug '*'
|
||||||
|
|
||||||
|
r.Post("/{otp}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sharedKey := chi.URLParam(r, "otp")
|
||||||
|
original, err := store.Get(sharedKey)
|
||||||
|
if nil != err {
|
||||||
|
msg := `{"error":"not found", "code":"E_NOT_FOUND"}`
|
||||||
|
log.Printf("/api/register-device/\n")
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, msg, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(original.MachinePPID) > 0 {
|
||||||
|
msg := `{"error":"the presented key has already been used", "code":"E_EXIST"}`
|
||||||
|
log.Printf("/api/register-device/\n")
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, msg, http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := &authstore.Authorization{}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
err = decoder.Decode(&auth)
|
||||||
|
// MachinePPID and PublicKey are required. ID must NOT be set. Slug is ignored.
|
||||||
|
epoch := time.Time{}
|
||||||
|
auth.SharedKey = sharedKey
|
||||||
|
if nil != err || "" != auth.ID || "" == auth.MachinePPID ||
|
||||||
|
"" == auth.PublicKey || "" == auth.SharedKey ||
|
||||||
|
epoch != auth.CreatedAt || epoch != auth.UpdatedAt || epoch != auth.DeletedAt {
|
||||||
|
msg, _ := json.Marshal(&struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: "expected JSON in the format {\"machine_ppid\":\"\",\"public_key\":\"\"}",
|
||||||
|
})
|
||||||
|
http.Error(w, string(msg), http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO hash the PPID and check against the Public Key?
|
||||||
|
pub := authstore.ToPublicKeyString(auth.MachinePPID)
|
||||||
|
if pub != auth.PublicKey {
|
||||||
|
msg, _ := json.Marshal(&struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}{
|
||||||
|
Error: "expected `public_key` to be the first 24 bytes of the hash of the `machine_ppid`",
|
||||||
|
})
|
||||||
|
http.Error(w, string(msg), http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
original.PublicKey = auth.PublicKey
|
||||||
|
original.MachinePPID = auth.MachinePPID
|
||||||
|
err = store.Set(original)
|
||||||
|
if nil != err {
|
||||||
|
msg := `{"error":"not really sure what happened, but it didn't go well (check the logs)", "code":"E_SERVER"}`
|
||||||
|
log.Printf("/api/register-device/\n")
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, msg, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _ := json.Marshal(auth)
|
||||||
|
w.Write(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func pong(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims)
|
||||||
|
if !ok {
|
||||||
|
msg := `{"error":"failure to ping: 1", "code":"E_TOKEN"}`
|
||||||
|
http.Error(w, msg+"\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("ping pong??", claims)
|
||||||
|
err := store.Touch(claims.Slug)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "touch err %s\n", err)
|
||||||
|
var msg string
|
||||||
|
if err == authstore.ErrNotFound {
|
||||||
|
// if the token is valid, touch should ALWAYS work
|
||||||
|
msg = `{"error":"failure to ping: inconsistent database data", "code":"E_SERVER"}`
|
||||||
|
} else {
|
||||||
|
msg = `{"error":"failure to ping: 2", "code":"E_SERVER"}`
|
||||||
|
}
|
||||||
|
http.Error(w, msg+"\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte(`{ "success": true }` + "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hello(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("Hello\n"))
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/telebit/assets/admin"
|
"git.rootprojects.org/root/telebit/assets/admin"
|
||||||
|
"git.rootprojects.org/root/telebit/internal/authutil"
|
||||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
"git.rootprojects.org/root/telebit/internal/dbg"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
@ -88,14 +89,14 @@ func RouteAdmin(authURL string, r chi.Router) {
|
||||||
grants, err := authorizer(r)
|
grants, err := authorizer(r)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
log.Println("authorization failed", err)
|
log.Println("authorization failed", err)
|
||||||
w.Write(apiNotAuthorizedContent)
|
w.Write(authutil.NotAuthorizedContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO define Admins in a better way
|
// TODO define Admins in a better way
|
||||||
if "*" != grants.Subject {
|
if "*" != grants.Subject {
|
||||||
log.Println("only admins allowed", err)
|
log.Println("only admins allowed", err)
|
||||||
w.Write(apiNotAuthorizedContent)
|
w.Write(authutil.NotAuthorizedContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +122,6 @@ func RouteAdmin(authURL string, r chi.Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiNotFoundContent = []byte("{ \"error\": \"not found\" }\n")
|
var apiNotFoundContent = []byte("{ \"error\": \"not found\" }\n")
|
||||||
var apiNotAuthorizedContent = []byte("{ \"error\": \"not authorized\" }\n")
|
|
||||||
|
|
||||||
func apiNotFoundHandler(w http.ResponseWriter, r *http.Request) {
|
func apiNotFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(apiNotFoundContent)
|
w.Write(apiNotFoundContent)
|
||||||
|
@ -232,7 +232,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
grants, err := authorizer(r)
|
grants, err := authorizer(r)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
log.Println("WebSocket authorization failed", err)
|
log.Println("WebSocket authorization failed", err)
|
||||||
w.Write(apiNotAuthorizedContent)
|
w.Write(authutil.NotAuthorizedContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
package telebit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewAuthorizer(authURL string) Authorizer {
|
|
||||||
return func(r *http.Request) (*Grants, error) {
|
|
||||||
// do we have a valid wss_client?
|
|
||||||
|
|
||||||
fmt.Printf("[authz] Authorization = %s\n", r.Header.Get("Authorization"))
|
|
||||||
var tokenString string
|
|
||||||
if auth := strings.Split(r.Header.Get("Authorization"), " "); len(auth) > 1 {
|
|
||||||
// TODO handle Basic auth tokens as well
|
|
||||||
tokenString = auth[1]
|
|
||||||
}
|
|
||||||
if "" == tokenString {
|
|
||||||
// Browsers do not allow Authorization Headers and must use access_token query string
|
|
||||||
tokenString = r.URL.Query().Get("access_token")
|
|
||||||
}
|
|
||||||
if "" != r.URL.Query().Get("access_token") {
|
|
||||||
r.URL.Query().Set("access_token", "[redacted]")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("[authz] authURL = %s\n", authURL)
|
|
||||||
fmt.Printf("[authz] token = %s\n", tokenString)
|
|
||||||
grants, err := Inspect(authURL, tokenString)
|
|
||||||
|
|
||||||
if nil != err {
|
|
||||||
fmt.Printf("[authorizer] error inspecting %q: %s\ntoken: %s\n", authURL, err, tokenString)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if "" != r.URL.Query().Get("access_token") {
|
|
||||||
r.URL.Query().Set("access_token", "[redacted:"+grants.Subject+"]")
|
|
||||||
}
|
|
||||||
|
|
||||||
return grants, err
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,10 @@
|
||||||
package telebit
|
package telebit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
@ -16,7 +13,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/telebit/internal/dbg"
|
|
||||||
httpshim "git.rootprojects.org/root/telebit/internal/tunnel"
|
httpshim "git.rootprojects.org/root/telebit/internal/tunnel"
|
||||||
|
|
||||||
"github.com/coolaj86/certmagic"
|
"github.com/coolaj86/certmagic"
|
||||||
|
@ -48,9 +44,6 @@ type Handler interface {
|
||||||
// HandlerFunc should handle, proxy, or terminate the connection
|
// HandlerFunc should handle, proxy, or terminate the connection
|
||||||
type HandlerFunc func(net.Conn) error
|
type HandlerFunc func(net.Conn) error
|
||||||
|
|
||||||
// Authorizer is called when a new client connects and we need to know something about it
|
|
||||||
type Authorizer func(*http.Request) (*Grants, error)
|
|
||||||
|
|
||||||
// Serve calls f(conn).
|
// Serve calls f(conn).
|
||||||
func (f HandlerFunc) Serve(conn net.Conn) error {
|
func (f HandlerFunc) Serve(conn net.Conn) error {
|
||||||
return f(conn)
|
return f(conn)
|
||||||
|
@ -415,6 +408,7 @@ func TerminateTLS(client net.Conn, acme *ACME) net.Conn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCertMagic creates our flavor of a CertMagic instance
|
||||||
func NewCertMagic(acme *ACME) (*certmagic.Config, error) {
|
func NewCertMagic(acme *ACME) (*certmagic.Config, error) {
|
||||||
if !acme.Agree {
|
if !acme.Agree {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
|
@ -458,68 +452,3 @@ func NewCertMagic(acme *ACME) (*certmagic.Config, error) {
|
||||||
magic.Issuer = certmagic.NewACMEManager(magic, manager)
|
magic.Issuer = certmagic.NewACMEManager(magic, manager)
|
||||||
return magic, nil
|
return magic, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Grants struct {
|
|
||||||
Subject string `json:"sub"`
|
|
||||||
Audience string `json:"aud"`
|
|
||||||
Domains []string `json:"domains"`
|
|
||||||
Ports []int `json:"ports"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Inspect(authURL, token string) (*Grants, error) {
|
|
||||||
inspectURL := strings.TrimSuffix(authURL, "/inspect") + "/inspect"
|
|
||||||
if dbg.Debug {
|
|
||||||
fmt.Fprintf(os.Stderr, "[debug] telebit.Inspect(\n\tinspectURL = %s,\n\ttoken = %s,\n)\n", inspectURL, token)
|
|
||||||
}
|
|
||||||
msg, err := Request("GET", inspectURL, token, nil)
|
|
||||||
if nil != err {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if nil == msg {
|
|
||||||
return nil, fmt.Errorf("invalid response")
|
|
||||||
}
|
|
||||||
|
|
||||||
grants := &Grants{}
|
|
||||||
err = json.NewDecoder(msg).Decode(grants)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if "" == grants.Subject {
|
|
||||||
fmt.Fprintf(os.Stderr, "TODO update mgmt server to show Subject: %q\n", msg)
|
|
||||||
grants.Subject = strings.Split(grants.Domains[0], ".")[0]
|
|
||||||
}
|
|
||||||
return grants, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Request(method, fullurl, token string, payload io.Reader) (io.Reader, error) {
|
|
||||||
HTTPClient := &http.Client{
|
|
||||||
Timeout: 15 * time.Second,
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(method, fullurl, payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(token) > 0 {
|
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
}
|
|
||||||
if nil != payload {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode >= http.StatusBadRequest {
|
|
||||||
return nil, fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes.NewBuffer(body), nil
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue