mirror of
				https://github.com/therootcompany/telebit.git
				synced 2025-10-26 19:02:48 +00:00 
			
		
		
		
	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" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/internal/acmeroutes" | ||||
| 	"git.rootprojects.org/root/telebit/internal/mgmt" | ||||
| 	"git.rootprojects.org/root/telebit/internal/mgmt/authstore" | ||||
| 
 | ||||
| @ -34,7 +35,7 @@ var ( | ||||
| 	serviceName = "telebit-mgmt" | ||||
| 
 | ||||
| 	// serviceDesc | ||||
| 	serviceDesc = "Telebit Device Management" | ||||
| 	//serviceDesc = "Telebit Device Management" | ||||
| ) | ||||
| 
 | ||||
| func ver() string { | ||||
| @ -151,7 +152,12 @@ func main() { | ||||
| 	_ = store.SetMaster(secret) | ||||
| 	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 { | ||||
| 		go func() { | ||||
| @ -178,7 +184,7 @@ func main() { | ||||
| 	r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Write([]byte("TODO (json): " + ver() + "\n")) | ||||
| 	}) | ||||
| 	mgmt.RouteAll(r) | ||||
| 	mgmt.RouteAll(r, authURL) | ||||
| 	fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, r)) | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -709,7 +709,7 @@ func fetchDirectivesAndRun() { | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		err = mgmt.Ping(config.authURL, token) | ||||
| 		err = authutil.Ping(config.authURL, token) | ||||
| 		if nil != err { | ||||
| 			fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err) | ||||
| 			//os.Exit(exitRetry) | ||||
| @ -722,7 +722,7 @@ func fetchDirectivesAndRun() { | ||||
| 					// re-create token unless no secret was supplied | ||||
| 					token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway) | ||||
| 				} | ||||
| 				err = mgmt.Ping(config.authURL, token) | ||||
| 				err = authutil.Ping(config.authURL, token) | ||||
| 				if nil != err { | ||||
| 					fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err) | ||||
| 					//os.Exit(exitRetry) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| package mgmt | ||||
| package acmeroutes | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| @ -6,8 +6,10 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/internal/authutil" | ||||
| 	"git.rootprojects.org/root/telebit/internal/http01fs" | ||||
| 
 | ||||
| 	"github.com/go-acme/lego/v3/challenge" | ||||
| @ -37,11 +39,50 @@ type Identifier struct { | ||||
| 	Value string `json:"value"` | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| type acmeProvider struct { | ||||
| 	BaseURL  string | ||||
| 	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 { | ||||
| 	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 { | ||||
| 	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) { | ||||
| 		r.Post("/{domain}", createChallenge) | ||||
| 
 | ||||
| @ -78,7 +144,7 @@ func createChallenge(w http.ResponseWriter, r *http.Request) { | ||||
| 	domain := chi.URLParam(r, "domain") | ||||
| 
 | ||||
| 	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) { | ||||
| 		msg := `{ "error": "invalid domain", "code":"E_BAD_REQUEST"}` | ||||
| 		http.Error(w, msg+"\n", http.StatusUnprocessableEntity) | ||||
| @ -103,8 +169,8 @@ 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") { | ||||
| 		provider := &http01fs.Provider | ||||
| 		provider.Present(context.Background(), acme.Challenge{ | ||||
| 		http01Provider := &http01fs.Provider | ||||
| 		http01Provider.Present(context.Background(), acme.Challenge{ | ||||
| 			Token:            ch.Token, | ||||
| 			KeyAuthorization: ch.KeyAuth, | ||||
| 			Identifier: acme.Identifier{ | ||||
| @ -146,8 +212,8 @@ 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") { | ||||
| 		provider := &http01fs.Provider | ||||
| 		provider.CleanUp(context.Background(), acme.Challenge{ | ||||
| 		http01Provider := &http01fs.Provider | ||||
| 		http01Provider.CleanUp(context.Background(), acme.Challenge{ | ||||
| 			Token:            ch.Token, | ||||
| 			KeyAuthorization: ch.KeyAuth, | ||||
| 			Identifier: acme.Identifier{ | ||||
							
								
								
									
										85
									
								
								internal/authutil/authorizer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								internal/authutil/authorizer.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										12
									
								
								internal/authutil/authutil.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/authutil/authutil.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										68
									
								
								internal/authutil/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								internal/authutil/request.go
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/internal/authutil" | ||||
| 	"git.rootprojects.org/root/telebit/internal/dbg" | ||||
| 	"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) { | ||||
| 	pub := authstore.ToPublicKeyString(ppid) | ||||
| 	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 { | ||||
| 		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 { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| @ -10,6 +10,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/internal/authutil" | ||||
| 	"git.rootprojects.org/root/telebit/internal/mgmt/authstore" | ||||
| 
 | ||||
| 	"github.com/go-chi/chi" | ||||
| @ -21,7 +22,7 @@ func handleDeviceRoutes(r chi.Router) { | ||||
| 		r.Use(func(next http.Handler) http.Handler { | ||||
| 			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 				ctx := r.Context() | ||||
| 				claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) | ||||
| 				claims, ok := ctx.Value(authutil.MWKey("claims")).(*authutil.Claims) | ||||
| 				if !ok || "*" != claims.Slug { | ||||
| 					msg := `{"error":"missing or invalid authorization token", "code":"E_TOKEN"}` | ||||
| 					http.Error(w, msg+"\n", http.StatusUnprocessableEntity) | ||||
|  | ||||
| @ -2,14 +2,10 @@ package mgmt | ||||
| 
 | ||||
| import ( | ||||
| 	"git.rootprojects.org/root/telebit/internal/mgmt/authstore" | ||||
| 
 | ||||
| 	"github.com/go-acme/lego/v3/challenge" | ||||
| ) | ||||
| 
 | ||||
| var store authstore.Store | ||||
| 
 | ||||
| var provider challenge.Provider = nil | ||||
| 
 | ||||
| // DeviceDomain is the base hostname used for devices, such as devices.example.com | ||||
| // which has devices as foo.devices.example.com | ||||
| var DeviceDomain string | ||||
| @ -18,11 +14,7 @@ var DeviceDomain string | ||||
| // ( currently NOT used, but will be used for wss://RELAY_DOMAIN/ ) | ||||
| var RelayDomain string | ||||
| 
 | ||||
| // MWKey is a type guard | ||||
| type MWKey string | ||||
| 
 | ||||
| // Init initializes some package variables | ||||
| func Init(s authstore.Store, p challenge.Provider) { | ||||
| func Init(s authstore.Store) { | ||||
| 	store = s | ||||
| 	provider = p | ||||
| } | ||||
|  | ||||
| @ -7,10 +7,11 @@ import ( | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"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/mgmt/authstore" | ||||
| 
 | ||||
| @ -19,15 +20,7 @@ import ( | ||||
| 	"github.com/go-chi/chi/middleware" | ||||
| ) | ||||
| 
 | ||||
| // MgmtClaims includes a Slug, for backwards compatibility | ||||
| type MgmtClaims struct { | ||||
| 	Slug string `json:"slug"` | ||||
| 	jwt.StandardClaims | ||||
| } | ||||
| 
 | ||||
| var presenters = make(chan *Challenge) | ||||
| var cleanups = make(chan *Challenge) | ||||
| 
 | ||||
| // RouteStatic handles the not-so-dynamic routes | ||||
| func RouteStatic(r chi.Router) 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.Recoverer) | ||||
| 
 | ||||
| 		r.Get("/.well-known/acme-challenge/{token}", getACMEChallenges) | ||||
| 		r.Get("/.well-known/acme-challenge/{token}", acmeroutes.GetACMEChallenges) | ||||
| 	}) | ||||
| 
 | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| } | ||||
| 
 | ||||
| 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") | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| // RouteAll will generate the routes | ||||
| func RouteAll(r chi.Router, authURL string) { | ||||
| 	r.Route("/", func(r chi.Router) { | ||||
| 		r.Use(middleware.Logger) | ||||
| 		r.Use(middleware.Timeout(15 * time.Second)) | ||||
| 		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.Use(func(next http.Handler) http.Handler { | ||||
| 				return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 					w.Header().Set("Content-Type", "application/json") | ||||
| 			r.Use(newClaimsGetter(authURL)) | ||||
| 
 | ||||
| 					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, | ||||
| 						&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) | ||||
| 			acmeroutes.HandleACMEChallengeRoutes(r) | ||||
| 			handleDeviceRoutes(r) | ||||
| 
 | ||||
| 			r.Get("/inspect", func(w http.ResponseWriter, r *http.Request) { | ||||
| 				ctx := r.Context() | ||||
| 				claims, ok := ctx.Value(MWKey("claims")).(*MgmtClaims) | ||||
| 				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, | ||||
| 				))) | ||||
| 			}) | ||||
| 
 | ||||
| 			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")) | ||||
| 			}) | ||||
| 			if len(authURL) > 0 { | ||||
| 				fmt.Printf("Delegating auth to %s\nIgnoring '/api/inspect'\n", authURL) | ||||
| 			} else { | ||||
| 				r.Get("/inspect", inspectToken) | ||||
| 			} | ||||
| 			r.Route("/register-device", registerDevice) | ||||
| 			r.Post("/ping", pong) | ||||
| 			r.Get("/", hello) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // 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" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/assets/admin" | ||||
| 	"git.rootprojects.org/root/telebit/internal/authutil" | ||||
| 	"git.rootprojects.org/root/telebit/internal/dbg" | ||||
| 
 | ||||
| 	"github.com/go-chi/chi" | ||||
| @ -88,14 +89,14 @@ func RouteAdmin(authURL string, r chi.Router) { | ||||
| 					grants, err := authorizer(r) | ||||
| 					if nil != err { | ||||
| 						log.Println("authorization failed", err) | ||||
| 						w.Write(apiNotAuthorizedContent) | ||||
| 						w.Write(authutil.NotAuthorizedContent) | ||||
| 						return | ||||
| 					} | ||||
| 
 | ||||
| 					// TODO define Admins in a better way | ||||
| 					if "*" != grants.Subject { | ||||
| 						log.Println("only admins allowed", err) | ||||
| 						w.Write(apiNotAuthorizedContent) | ||||
| 						w.Write(authutil.NotAuthorizedContent) | ||||
| 						return | ||||
| 					} | ||||
| 
 | ||||
| @ -121,7 +122,6 @@ func RouteAdmin(authURL string, r chi.Router) { | ||||
| } | ||||
| 
 | ||||
| var apiNotFoundContent = []byte("{ \"error\": \"not found\" }\n") | ||||
| var apiNotAuthorizedContent = []byte("{ \"error\": \"not authorized\" }\n") | ||||
| 
 | ||||
| func apiNotFoundHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	w.Write(apiNotFoundContent) | ||||
| @ -232,7 +232,7 @@ func upgradeWebsocket(w http.ResponseWriter, r *http.Request) { | ||||
| 	grants, err := authorizer(r) | ||||
| 	if nil != err { | ||||
| 		log.Println("WebSocket authorization failed", err) | ||||
| 		w.Write(apiNotAuthorizedContent) | ||||
| 		w.Write(authutil.NotAuthorizedContent) | ||||
| 		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 | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httputil" | ||||
| @ -16,7 +13,6 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/telebit/internal/dbg" | ||||
| 	httpshim "git.rootprojects.org/root/telebit/internal/tunnel" | ||||
| 
 | ||||
| 	"github.com/coolaj86/certmagic" | ||||
| @ -48,9 +44,6 @@ type Handler interface { | ||||
| // HandlerFunc should handle, proxy, or terminate the connection | ||||
| 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). | ||||
| func (f HandlerFunc) Serve(conn net.Conn) error { | ||||
| 	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) { | ||||
| 	if !acme.Agree { | ||||
| 		fmt.Fprintf( | ||||
| @ -458,68 +452,3 @@ func NewCertMagic(acme *ACME) (*certmagic.Config, error) { | ||||
| 	magic.Issuer = certmagic.NewACMEManager(magic, manager) | ||||
| 	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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user