From 5627f57ebb03d4dbbdc3784c4c8d98d04fe0985c Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 3 Feb 2024 13:04:52 +0100 Subject: [PATCH] Tesla Command: implement token storage (#12021) --- cmd/settings-get.go | 62 +++++++++++ cmd/settings.go | 15 +++ server/db/settings/setting.go | 13 +++ vehicle/tesla-command.go | 16 +-- vehicle/tesla-vehicle-command/helper.go | 19 ++++ vehicle/tesla-vehicle-command/identity.go | 124 ++++++++++++++++------ 6 files changed, 203 insertions(+), 46 deletions(-) create mode 100644 cmd/settings-get.go create mode 100644 cmd/settings.go diff --git a/cmd/settings-get.go b/cmd/settings-get.go new file mode 100644 index 000000000..325db4a07 --- /dev/null +++ b/cmd/settings-get.go @@ -0,0 +1,62 @@ +package cmd + +import ( + "fmt" + "os" + "os/signal" + "regexp" + "syscall" + "text/tabwriter" + + "github.com/evcc-io/evcc/server/db/settings" + "github.com/spf13/cobra" +) + +// settingsGetCmd represents the configure command +var settingsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get configuration settings", + Run: runSettingsGet, + Args: cobra.MaximumNArgs(1), +} + +func init() { + settingsCmd.AddCommand(settingsGetCmd) +} + +func runSettingsGet(cmd *cobra.Command, args []string) { + // load config + if err := loadConfigFile(&conf); err != nil { + log.FATAL.Fatal(err) + } + + // setup environment + if err := configureEnvironment(cmd, conf); err != nil { + log.FATAL.Fatal(err) + } + + var re *regexp.Regexp + if len(args) > 0 { + re = regexp.MustCompile(args[0]) + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + for _, s := range settings.All() { + if re != nil && !re.MatchString(s.Key) { + continue + } + + fmt.Fprintf(w, "%s:\t%s\n", s.Key, s.Value) + } + w.Flush() + + // catch signals + go func() { + signalC := make(chan os.Signal, 1) + signal.Notify(signalC, os.Interrupt, syscall.SIGTERM) + + <-signalC // wait for signal + + os.Exit(1) + }() +} diff --git a/cmd/settings.go b/cmd/settings.go new file mode 100644 index 000000000..7efd6ba46 --- /dev/null +++ b/cmd/settings.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// settingsCmd represents the configure command +var settingsCmd = &cobra.Command{ + Use: "settings", + Short: "Manage configuration settings", +} + +func init() { + rootCmd.AddCommand(settingsCmd) +} diff --git a/server/db/settings/setting.go b/server/db/settings/setting.go index d45b71dbc..2d3451ade 100644 --- a/server/db/settings/setting.go +++ b/server/db/settings/setting.go @@ -1,6 +1,7 @@ package settings import ( + "cmp" "encoding/json" "errors" "slices" @@ -43,6 +44,18 @@ func Persist() error { return db.Instance.Save(settings).Error } +func All() []setting { + mu.RLock() + defer mu.RUnlock() + + res := slices.Clone(settings) + slices.SortFunc(res, func(i, j setting) int { + return cmp.Compare(i.Key, j.Key) + }) + + return res +} + func SetString(key string, val string) { mu.Lock() defer mu.Unlock() diff --git a/vehicle/tesla-command.go b/vehicle/tesla-command.go index bb5605a67..b550bdb9a 100644 --- a/vehicle/tesla-command.go +++ b/vehicle/tesla-command.go @@ -1,7 +1,6 @@ package vehicle import ( - "context" "os" "time" @@ -9,8 +8,6 @@ import ( "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command" - "github.com/golang-jwt/jwt/v5" - "golang.org/x/oauth2" ) // TeslaCommand is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api. @@ -55,23 +52,12 @@ func NewTeslaCommandFromConfig(other map[string]interface{}) (api.Vehicle, error return nil, err } - if t := cc.Tokens.Access; t != "" { - var claims jwt.RegisteredClaims - if _, _, err := jwt.NewParser().ParseUnverified(t, &claims); err != nil { - return nil, err - } - token.Expiry = claims.ExpiresAt.Time - } - log := util.NewLogger("tesla-command").Redact( cc.Tokens.Access, cc.Tokens.Refresh, vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret, ) - ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log)) - ts := vc.OAuth2Config.TokenSource(ctx, token) - - identity, err := vc.NewIdentity(log, ts) + identity, err := vc.NewIdentity(log, token) if err != nil { return nil, err } diff --git a/vehicle/tesla-vehicle-command/helper.go b/vehicle/tesla-vehicle-command/helper.go index 8a0420724..3178c3b52 100644 --- a/vehicle/tesla-vehicle-command/helper.go +++ b/vehicle/tesla-vehicle-command/helper.go @@ -3,11 +3,30 @@ package vc import ( "errors" "strings" + "sync" "github.com/evcc-io/evcc/api" "github.com/teslamotors/vehicle-command/pkg/connector/inet" ) +var ( + mu sync.Mutex + identities = make(map[string]*Identity) +) + +func getInstance(subject string) *Identity { + mu.Lock() + defer mu.Unlock() + v, _ := identities[subject] + return v +} + +func addInstance(subject string, identity *Identity) { + mu.Lock() + defer mu.Unlock() + identities[subject] = identity +} + // apiError converts HTTP 408 error to ErrTimeout func apiError(err error) error { if err != nil && (errors.Is(err, inet.ErrVehicleNotAwake) || diff --git a/vehicle/tesla-vehicle-command/identity.go b/vehicle/tesla-vehicle-command/identity.go index f4bc9e4cb..c2d0073c2 100644 --- a/vehicle/tesla-vehicle-command/identity.go +++ b/vehicle/tesla-vehicle-command/identity.go @@ -1,8 +1,16 @@ package vc import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" - "github.com/teslamotors/vehicle-command/pkg/account" + "github.com/evcc-io/evcc/util/oauth" + "github.com/evcc-io/evcc/util/request" + "github.com/golang-jwt/jwt/v5" "golang.org/x/oauth2" ) @@ -19,7 +27,7 @@ var OAuth2Config = &oauth2.Config{ Scopes: []string{"openid", "email", "offline_access"}, } -const userAgent = "evcc/evcc-io" +// const userAgent = "evcc/evcc-io" var TESLA_CLIENT_ID, TESLA_CLIENT_SECRET string @@ -34,45 +42,99 @@ func init() { type Identity struct { oauth2.TokenSource - log *util.Logger - token *oauth2.Token - acct *account.Account + mu sync.Mutex + log *util.Logger + subject string + // acct *account.Account } -func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) { +func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) { + // serialise instance handling + mu.Lock() + defer mu.Unlock() + + // determine tesla identity + var claims jwt.RegisteredClaims + if _, _, err := jwt.NewParser().ParseUnverified(token.AccessToken, &claims); err != nil { + return nil, err + } + + // reuse identity instance + if instance := getInstance(claims.Subject); instance != nil { + return instance, nil + } + + if !token.Valid() { + token.Expiry = claims.ExpiresAt.Time + } + + v := &Identity{ + log: log, + subject: claims.Subject, + // acct: acct, + } + + // database token + if !token.Valid() { + if err := settings.Json(v.settingsKey(), &token); err != nil { + return nil, fmt.Errorf("missing token setting for %s: %w", claims.Subject, err) + } + + if !token.Valid() { + return nil, errors.New("token expired") + } + } + + // acct, err := account.New(token.AccessToken, userAgent) + // if err != nil { + // return nil, err + // } + + v.TokenSource = oauth.RefreshTokenSource(token, v) + + // add instance + addInstance(claims.Subject, v) + + return v, nil +} + +func (v *Identity) settingsKey() string { + return fmt.Sprintf("tesla-command.%s", v.subject) +} + +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { + v.mu.Lock() + defer v.mu.Unlock() + + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(v.log)) + ts := OAuth2Config.TokenSource(ctx, token) + token, err := ts.Token() if err != nil { return nil, err } - acct, err := account.New(token.AccessToken, userAgent) - if err != nil { - return nil, err - } + err = settings.SetJson(v.settingsKey(), token) - return &Identity{ - TokenSource: ts, - token: token, - acct: acct, - }, nil + return token, err } -func (v *Identity) Account() *account.Account { - token, err := v.Token() - if err != nil { - v.log.ERROR.Println(err) - return v.acct - } +// func (v *Identity) Account() *account.Account { +// token, err := v.Token() +// if err != nil { +// v.log.ERROR.Println(err) +// return v.acct +// } - if token.AccessToken != v.token.AccessToken { - acct, err := account.New(token.AccessToken, userAgent) - if err != nil { - v.log.ERROR.Println(err) - return v.acct - } +// if token.AccessToken != v.token.AccessToken { +// acct, err := account.New(token.AccessToken, userAgent) +// if err != nil { +// v.log.ERROR.Println(err) +// return v.acct +// } - v.acct = acct - } +// v.acct = acct +// } - return v.acct -} +// return v.acct +// }