Add BMW CarData api (#23972)
This commit is contained in:
parent
3139499b0f
commit
9dd4599fc2
17 changed files with 731 additions and 37 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
|
|
@ -26,6 +27,30 @@ type OAuth struct {
|
|||
subject string
|
||||
cv string
|
||||
ctx context.Context
|
||||
|
||||
deviceFlow bool
|
||||
tokenRetriever func(string, *oauth2.Token) error
|
||||
tokenStorer func(*oauth2.Token) any
|
||||
}
|
||||
|
||||
type oauthOption func(*OAuth)
|
||||
|
||||
func WithOauthDeviceFlowOption() func(o *OAuth) {
|
||||
return func(o *OAuth) {
|
||||
o.deviceFlow = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithTokenStorerOption(ts func(*oauth2.Token) any) func(o *OAuth) {
|
||||
return func(o *OAuth) {
|
||||
o.tokenStorer = ts
|
||||
}
|
||||
}
|
||||
|
||||
func WithTokenRetrieverOption(tr func(string, *oauth2.Token) error) func(o *OAuth) {
|
||||
return func(o *OAuth) {
|
||||
o.tokenRetriever = tr
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -59,20 +84,23 @@ func NewOauthFromConfig(ctx context.Context, other map[string]any) (oauth2.Token
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return NewOauth(ctx, &cc.Config, cc.Name)
|
||||
return NewOauth(ctx, cc.Name, &cc.Config)
|
||||
}
|
||||
|
||||
func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oauth2.TokenSource, error) {
|
||||
log := util.NewLogger("oauth-generic")
|
||||
var (
|
||||
_ oauth.TokenRefresher = (*OAuth)(nil)
|
||||
_ api.AuthProvider = (*OAuth)(nil)
|
||||
)
|
||||
|
||||
if instanceName == "" {
|
||||
func NewOauth(ctx context.Context, name string, oc *oauth2.Config, opts ...oauthOption) (oauth2.TokenSource, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("instance name must not be empty")
|
||||
}
|
||||
|
||||
// hash oauth2 config
|
||||
h := sha256.Sum256(fmt.Append(nil, oc))
|
||||
hash := hex.EncodeToString(h[:])[:8]
|
||||
subject := instanceName + " (" + hash + ")"
|
||||
subject := name + " (" + hash + ")"
|
||||
|
||||
// reuse instance
|
||||
if instance := getInstance(subject); instance != nil {
|
||||
|
|
@ -83,21 +111,36 @@ func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oaut
|
|||
o := &OAuth{
|
||||
subject: subject,
|
||||
oc: oc,
|
||||
log: log,
|
||||
log: util.NewLogger("oauth"),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
// load token from db
|
||||
var tok oauth2.Token
|
||||
var token oauth2.Token
|
||||
if settings.Exists(o.subject) {
|
||||
o.log.DEBUG.Printf("loading token for %s from database", o.subject)
|
||||
|
||||
if err := settings.Json(o.subject, &tok); err != nil {
|
||||
return nil, err
|
||||
if o.tokenRetriever != nil {
|
||||
plain, err := settings.String(o.subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := o.tokenRetriever(plain, &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := settings.Json(o.subject, &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.TokenSource = oauth.RefreshTokenSource(&tok, o)
|
||||
o.TokenSource = oauth.RefreshTokenSource(&token, o)
|
||||
|
||||
// add instance
|
||||
addInstance(o.subject, o)
|
||||
|
|
@ -108,69 +151,116 @@ func NewOauth(ctx context.Context, oc *oauth2.Config, instanceName string) (oaut
|
|||
return o, nil
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.RefreshTokenSource.
|
||||
// RefreshToken implements oauth.TokenRefresher.
|
||||
func (o *OAuth) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
if token.RefreshToken == "" {
|
||||
return nil, api.ErrMissingToken
|
||||
}
|
||||
|
||||
// log token before refresh
|
||||
o.log.DEBUG.Printf("refreshing token for %s", o.subject)
|
||||
|
||||
// refresh token source
|
||||
token, err := o.oc.TokenSource(o.ctx, token).Token()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid_grant") {
|
||||
if settings.Exists(o.subject) {
|
||||
settings.Delete(o.subject)
|
||||
}
|
||||
if strings.Contains(err.Error(), "invalid_grant") && settings.Exists(o.subject) {
|
||||
settings.Delete(o.subject)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = settings.SetJson(o.subject, token)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
// updateToken must only be called when lock is held
|
||||
func (o *OAuth) updateToken(token *oauth2.Token) {
|
||||
var store any = token
|
||||
|
||||
// tokenStorer allows persisting the token together with it's extra properties
|
||||
if o.tokenStorer != nil {
|
||||
store = o.tokenStorer(token)
|
||||
}
|
||||
|
||||
if err := settings.SetJson(o.subject, store); err != nil {
|
||||
o.log.ERROR.Printf("error saving token: %v", err)
|
||||
}
|
||||
|
||||
o.TokenSource = oauth.RefreshTokenSource(token, o)
|
||||
}
|
||||
|
||||
// HandleCallback implements api.AuthProvider.
|
||||
func (o *OAuth) HandleCallback(responseValues url.Values) error {
|
||||
code := responseValues.Get("code")
|
||||
func (o *OAuth) HandleCallback(params url.Values) error {
|
||||
code := params.Get("code")
|
||||
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
token, err := o.oc.Exchange(o.ctx, code, oauth2.VerifierOption(o.cv))
|
||||
if err != nil {
|
||||
o.log.ERROR.Printf("error during oauth exchange: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := settings.SetJson(o.subject, token); err != nil {
|
||||
o.log.ERROR.Printf("error saving token: %s", err)
|
||||
}
|
||||
o.updateToken(token)
|
||||
|
||||
o.TokenSource = oauth.RefreshTokenSource(token, o)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login implements api.AuthProvider.
|
||||
func (o *OAuth) Login(state string) string {
|
||||
func (o *OAuth) Login(state string) (string, error) {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
o.cv = oauth2.GenerateVerifier()
|
||||
return o.oc.AuthCodeURL(state, oauth2.S256ChallengeOption(o.cv))
|
||||
|
||||
if o.deviceFlow {
|
||||
ctx := context.Background()
|
||||
|
||||
da, err := o.oc.DeviceAuth(ctx, oauth2.S256ChallengeOption(o.cv))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
defer cancel()
|
||||
|
||||
token, err := o.oc.DeviceAccessToken(ctx, da, oauth2.VerifierOption(o.cv))
|
||||
if err != nil {
|
||||
o.log.ERROR.Printf("error retrieving token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
o.updateToken(token)
|
||||
}()
|
||||
|
||||
return da.VerificationURIComplete, nil
|
||||
}
|
||||
|
||||
if o.oc.Endpoint.AuthURL == "" {
|
||||
return "", errors.New("missing auth url")
|
||||
}
|
||||
|
||||
return o.oc.AuthCodeURL(state, oauth2.S256ChallengeOption(o.cv)), nil
|
||||
}
|
||||
|
||||
// Logout implements api.AuthProvider.
|
||||
func (o *OAuth) Logout() error {
|
||||
o.log.INFO.Printf("removing %s from database", o.subject)
|
||||
o.log.DEBUG.Printf("removing %s from database", o.subject)
|
||||
|
||||
if settings.Exists(o.subject) {
|
||||
settings.Delete(o.subject)
|
||||
if err := settings.Delete(o.subject); err != nil {
|
||||
o.log.ERROR.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
o.TokenSource = oauth.RefreshTokenSource(nil, o)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -182,6 +272,6 @@ func (o *OAuth) DisplayName() string {
|
|||
|
||||
// Authenticated implements api.AuthProvider.
|
||||
func (o *OAuth) Authenticated() bool {
|
||||
token, err := o.TokenSource.Token()
|
||||
token, err := o.Token()
|
||||
return err == nil && token.Valid()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue