evcc-io/vehicle/smart/hello/identity.go

217 lines
6.2 KiB
Go

package hello
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
type Identity struct {
*request.Helper
oauth2.TokenSource
user, password string
userID, deviceID string
}
func NewIdentity(log *util.Logger, user, password string) (*Identity, error) {
v := &Identity{
Helper: request.NewHelper(log),
user: user,
password: password,
deviceID: lo.RandomString(16, lo.AlphanumericCharset),
}
v.TokenSource = oauth2.ReuseTokenSource(nil, oauth.BootstrapTokenSource(v.refreshToken))
_, err := v.Token()
return v, err
}
func (v *Identity) refreshToken() (*oauth2.Token, error) {
token, err := v.login()
if err != nil {
return nil, err
}
appToken, userID, err := v.appToken(token)
if err != nil {
return nil, err
}
v.userID = userID
return appToken, nil
}
func (v *Identity) DeviceID() string {
return v.deviceID
}
func (v *Identity) UserID() (string, error) {
var err error
if v.userID == "" {
err = errors.New("missing user id")
}
return v.userID, err
}
func (v *Identity) login() (*oauth2.Token, error) {
uri := "https://awsapi.future.smart.com/login-app/api/v1/authorize?uiLocales=de-DE&uiLocales=de-DE"
req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
"user-agent": userAgent,
"x-requested-with": "com.smart.hellosmart",
})
resp, err := v.Do(req)
if err == nil && resp.StatusCode != 200 {
err = fmt.Errorf("status: %d", resp.StatusCode)
}
if err != nil {
return nil, fmt.Errorf("authorize: %w", err)
}
defer resp.Body.Close()
context := resp.Request.URL.Query().Get("context")
if context == "" {
return nil, fmt.Errorf("missing context: %s", resp.Request.URL.String())
}
data := url.Values{
"loginID": {v.user},
"password": {v.password},
"sessionExpiration": {"2592000"},
"targetEnv": {"jssdk"},
"include": {"profile,data,emails,subscriptions,preferences"},
"includeUserInfo": {"true"},
"loginMode": {"standard"},
"lang": {"de"},
"APIKey": {ApiKey},
"source": {"showScreenSet"},
"sdk": {"js_latest"},
"authMode": {"cookie"},
"pageURL": {"https://app.id.smart.com/login?gig_ui_locales=de-DE"},
"sdkBuild": {"15482"},
"format": {"json"},
}
uri = "https://auth.smart.com/accounts.login"
req, _ = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), map[string]string{
"user-agent": userAgent,
"x-requested-with": "com.smart.hellosmart",
"content-type": request.FormContent,
"cookie": "gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4",
})
var login struct {
ErrorCode int
ErrorDetails, ErrorMessage string
SessionInfo struct {
LoginToken string `json:"login_token"`
ExpiresIn int `json:"expires_in,string"`
}
UserInfo struct {
UID string
FirstName, LastName, NickName string
}
}
if err := v.DoJSON(req, &login); err != nil {
return nil, fmt.Errorf("accounts.login: %w", err)
}
if login.ErrorCode != 0 {
return nil, fmt.Errorf("%s: %s", login.ErrorMessage, login.ErrorDetails)
}
defer resp.Body.Close()
var param request.InterceptResult
v.Client.CheckRedirect, param = request.InterceptRedirect("access_token", true)
uri = fmt.Sprintf("https://auth.smart.com/oidc/op/v1.0/%s/authorize/continue?context=%s&login_token=%s", ApiKey, context, login.SessionInfo.LoginToken)
req, _ = request.New(http.MethodGet, uri, nil, map[string]string{
"user-agent": userAgent,
"x-requested-with": "com.smart.hellosmart",
"content-type": request.FormContent,
"cookie": "gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4;glt_" + ApiKey + "=" + login.SessionInfo.LoginToken,
})
resp, err = v.Do(req)
if err != nil {
return nil, fmt.Errorf("token exchange: %w", err)
}
defer resp.Body.Close()
if _, err := param(); err != nil {
return nil, err
}
u, err := url.Parse(resp.Header.Get("location"))
if err != nil {
return nil, err
}
token := oauth2.Token{
AccessToken: u.Query().Get("access_token"),
RefreshToken: u.Query().Get("refresh_token"),
}
return &token, nil
}
func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error) {
params := url.Values{
"identity_type": {"smart"},
}
data := map[string]string{
"accessToken": token.AccessToken,
}
path := "/auth/account/session/secure"
nonce, ts, sign, err := createSignature(http.MethodPost, path, params, request.MarshalJSON(data))
if err != nil {
return nil, "", err
}
uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode())
req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{
"Accept": "application/json;responseformat=3",
"Content-Type": "application/json; charset=utf-8",
"X-Api-Signature-Version": "1.0",
"X-Api-Signature-Nonce": nonce,
"X-App-Id": appID,
"X-Device-Identifier": v.deviceID,
"X-Operator-Code": operatorCode,
"X-Signature": sign,
"X-Timestamp": ts,
})
var res struct {
Code Int
Message string
Data AppToken
}
if err := v.DoJSON(req, &res); err != nil {
return nil, "", err
} else if res.Code != ResponseOK {
return nil, "", fmt.Errorf("%d: %s", res.Code, res.Message)
}
tok := oauth2.Token{
AccessToken: res.Data.AccessToken,
RefreshToken: res.Data.RefreshToken,
Expiry: time.Now().Add(time.Duration(res.Data.ExpiresIn) * time.Second),
}
return &tok, res.Data.UserId, nil
}