Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use token source instead of non-refreshable token #151

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ require (
github.com/99designs/keyring v1.2.2
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
golang.org/x/term v0.5.0
google.golang.org/protobuf v1.28.1
golang.org/x/oauth2 v0.16.0
golang.org/x/term v0.16.0
google.golang.org/protobuf v1.31.0
)

require (
Expand All @@ -16,7 +17,7 @@ require (
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
Expand All @@ -27,7 +28,9 @@ require (
github.com/pkg/errors v0.8.1 // indirect
github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 // indirect
github.com/sirupsen/logrus v1.5.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
)

replace github.com/JuulLabs-OSS/cbgo => github.com/tinygo-org/cbgo v0.0.4
31 changes: 23 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633 h1:ZrzoZQz1CF33SPHLkjRp
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
Expand Down Expand Up @@ -58,18 +60,31 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
44 changes: 26 additions & 18 deletions pkg/account/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ import (
"github.com/teslamotors/vehicle-command/pkg/connector"
"github.com/teslamotors/vehicle-command/pkg/connector/inet"
"github.com/teslamotors/vehicle-command/pkg/vehicle"
"golang.org/x/oauth2"
)

var (
//go:embed version.txt
libraryVersion string
)
//go:embed version.txt
var libraryVersion string

func buildUserAgent(app string) string {
library := strings.TrimSpace("tesla-sdk/" + libraryVersion)
Expand Down Expand Up @@ -64,10 +63,9 @@ func buildUserAgent(app string) string {
// Account allows interaction with a Tesla account.
type Account struct {
// The default UserAgent is constructed from the global UserAgent, but can be overridden.
UserAgent string
authHeader string
Host string
client http.Client
UserAgent string
Host string
client *http.Client
}

// We don't parse JWTs beyond what's required to extract the API server domain name
Expand All @@ -76,8 +74,10 @@ type oauthPayload struct {
OUCode string `json:"ou_code"`
}

var domainRegEx = regexp.MustCompile(`^[A-Za-z0-9-.]+$`) // We're mostly interested in stopping paths; the http package handles the rest.
var remappedDomains = map[string]string{} // For use during development; populate in an init() function.
var (
domainRegEx = regexp.MustCompile(`^[A-Za-z0-9-.]+$`) // We're mostly interested in stopping paths; the http package handles the rest.
remappedDomains = map[string]string{} // For use during development; populate in an init() function.
)

const defaultDomain = "fleet-api.prd.na.vn.cloud.tesla.com"

Expand Down Expand Up @@ -114,8 +114,12 @@ func (p *oauthPayload) domain() string {

// New returns an [Account] that can be used to fetch a [vehicle.Vehicle].
andig marked this conversation as resolved.
Show resolved Hide resolved
// Optional userAgent can be passed in - otherwise it will be generated from code
func New(oauthToken, userAgent string) (*Account, error) {
parts := strings.Split(oauthToken, ".")
func New(ts oauth2.TokenSource, userAgent string) (*Account, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently tokens are invalidated immediately after another refresh token get issued. I'm afraid that building this functionality is going to create downstream errors for folks who manages their tokens cross app/manually

Copy link
Author

@andig andig Feb 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, this PR is from before that change. Actually, I think this makes this PR even more important. How you build your token source is up to you.

oauthToken, err := ts.Token()
if err != nil {
return nil, err
}
parts := strings.Split(oauthToken.AccessToken, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("client provided malformed OAuth token")
}
Expand All @@ -132,10 +136,15 @@ func New(oauthToken, userAgent string) (*Account, error) {
if domain == "" {
return nil, fmt.Errorf("client provided OAuth token with invalid audiences")
}
client := http.DefaultClient
client.Transport = &oauth2.Transport{
Source: ts,
Base: http.DefaultClient.Transport,
}
return &Account{
UserAgent: buildUserAgent(userAgent),
authHeader: "Bearer " + strings.TrimSpace(oauthToken),
Host: domain,
UserAgent: buildUserAgent(userAgent),
client: client,
Host: domain,
}, nil
}

Expand All @@ -147,7 +156,7 @@ func New(oauthToken, userAgent string) (*Account, error) {
// sessions parameter may also be nil, but providing a cache.SessionCache avoids a round-trip
// handshake with the Vehicle in subsequent connections.
func (a *Account) GetVehicle(ctx context.Context, vin string, privateKey authentication.ECDHPrivateKey, sessions *cache.SessionCache) (*vehicle.Vehicle, error) {
conn := inet.NewConnection(vin, a.authHeader, a.Host, a.UserAgent)
conn := inet.NewConnection(vin, a.client, a.Host, a.UserAgent)
car, err := vehicle.NewVehicle(conn, privateKey, sessions)
if err != nil {
conn.Close()
Expand All @@ -168,7 +177,6 @@ func (a *Account) Get(ctx context.Context, endpoint string) ([]byte, error) {
log.Debug("Requesting %s...", url)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", a.UserAgent)
request.Header.Set("Authorization", a.authHeader)
response, err := a.client.Do(request)
if err != nil {
return nil, fmt.Errorf("error fetching %s: %w", endpoint, err)
Expand All @@ -188,7 +196,7 @@ func (a *Account) Get(ctx context.Context, endpoint string) ([]byte, error) {
}

func (a *Account) sendFleetAPICommand(ctx context.Context, endpoint string, command interface{}) ([]byte, error) {
return inet.SendFleetAPICommand(ctx, &a.client, a.UserAgent, a.authHeader, fmt.Sprintf("https://%s/%s", a.Host, endpoint), command)
return inet.SendFleetAPICommand(ctx, a.client, a.UserAgent, fmt.Sprintf("https://%s/%s", a.Host, endpoint), command)
}

// Post sends an HTTP POST request to endpoint.
Expand Down
26 changes: 16 additions & 10 deletions pkg/account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,43 @@ import (
"encoding/json"
"fmt"
"testing"

"golang.org/x/oauth2"
)

func b64Encode(payload string) string {
return base64.RawStdEncoding.EncodeToString([]byte(payload))
}

func ts(token string) oauth2.TokenSource {
return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
}

func TestNewAccount(t *testing.T) {
validDomain := "fleet-api.example.tesla.com"
if _, err := New("", ""); err == nil {
if _, err := New(ts(""), ""); err == nil {
t.Error("Returned success empty JWT")
}
if _, err := New(b64Encode(validDomain), ""); err == nil {
if _, err := New(ts(b64Encode(validDomain)), ""); err == nil {
t.Error("Returned success on one-field JWT")
}
if _, err := New("x."+b64Encode(validDomain), ""); err == nil {
if _, err := New(ts("x."+b64Encode(validDomain)), ""); err == nil {
t.Error("Returned success on two-field JWT")
}
if _, err := New("x."+b64Encode(validDomain)+"y.z", ""); err == nil {
if _, err := New(ts("x."+b64Encode(validDomain)+"y.z"), ""); err == nil {
t.Error("Returned success on four-field JWT")
}
if _, err := New("x."+validDomain+".y", ""); err == nil {
if _, err := New(ts("x."+validDomain+".y"), ""); err == nil {
t.Error("Returned success on non-base64 encoded JWT")
}
if _, err := New("x."+b64Encode("{\"aud\": \"example.com\"}")+".y", ""); err == nil {
if _, err := New(ts("x."+b64Encode("{\"aud\": \"example.com\"}")+".y"), ""); err == nil {
t.Error("Returned success on untrusted domain")
}
if _, err := New("x."+b64Encode(fmt.Sprintf("{\"aud\": \"%s\"}", validDomain))+".y", ""); err == nil {
if _, err := New(ts("x."+b64Encode(fmt.Sprintf("{\"aud\": \"%s\"}", validDomain))+".y"), ""); err == nil {
t.Error("Returned when aud field not a list")
}

acct, err := New("x."+b64Encode(fmt.Sprintf("{\"aud\": [\"%s\"]}", validDomain))+".y", "")
acct, err := New(ts("x."+b64Encode(fmt.Sprintf("{\"aud\": [\"%s\"]}", validDomain))+".y"), "")
if err != nil {
t.Fatalf("Returned error on valid JWT: %s", err)
}
Expand All @@ -49,7 +55,7 @@ func TestDomainDefault(t *testing.T) {
Audiences: []string{"https://auth.tesla.com/nts"},
}

acct, err := New(makeTestJWT(payload), "")
acct, err := New(ts(makeTestJWT(payload)), "")
if err != nil {
t.Fatalf("Returned error on valid JWT: %s", err)
}
Expand All @@ -64,7 +70,7 @@ func TestDomainExtraction(t *testing.T) {
OUCode: "EU",
}

acct, err := New(makeTestJWT(payload), "")
acct, err := New(ts(makeTestJWT(payload)), "")
if err != nil {
t.Fatalf("Returned error on valid JWT: %s", err)
}
Expand Down
29 changes: 13 additions & 16 deletions pkg/connector/inet/inet.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (e *HttpError) Temporary() bool {
e.Code == http.StatusTooManyRequests
}

func SendFleetAPICommand(ctx context.Context, client *http.Client, userAgent, authHeader string, url string, command interface{}) ([]byte, error) {
func SendFleetAPICommand(ctx context.Context, client *http.Client, userAgent, url string, command interface{}) ([]byte, error) {
var body []byte
var ok bool
if body, ok = command.([]byte); !ok {
Expand All @@ -96,7 +96,6 @@ func SendFleetAPICommand(ctx context.Context, client *http.Client, userAgent, au

request.Header.Set("User-Agent", userAgent)
request.Header.Set("Content-type", "application/json")
request.Header.Set("Authorization", authHeader)
request.Header.Set("Accept", "*/*")

result, err := client.Do(request)
Expand Down Expand Up @@ -139,7 +138,7 @@ func ValidTeslaDomainSuffix(domain string) bool {
// response body is not necessarily nil if the error is set.
func (c *Connection) SendFleetAPICommand(ctx context.Context, endpoint string, command interface{}) ([]byte, error) {
url := fmt.Sprintf("https://%s/%s", c.serverURL, endpoint)
rsp, err := SendFleetAPICommand(ctx, &c.client, c.UserAgent, c.authHeader, url, command)
rsp, err := SendFleetAPICommand(ctx, c.client, c.UserAgent, url, command)
if err != nil {
var httpErr *HttpError
if errors.As(err, &httpErr) && httpErr.Code == http.StatusMisdirectedRequest {
Expand All @@ -155,26 +154,24 @@ func (c *Connection) SendFleetAPICommand(ctx context.Context, endpoint string, c

// Connection implements the connector.Connector interface by POSTing commands to a server.
type Connection struct {
UserAgent string
vin string
client http.Client
serverURL string
inbox chan []byte
authHeader string
UserAgent string
vin string
client *http.Client
serverURL string
inbox chan []byte

wakeLock sync.Mutex
lastPoke time.Time
}

// NewConnection creates a Connection.
func NewConnection(vin string, authHeader, serverURL, userAgent string) *Connection {
func NewConnection(vin string, client *http.Client, serverURL, userAgent string) *Connection {
Copy link
Collaborator

@sethterashima sethterashima Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requirements for client need to be clearly documented, ideally with an example of setting up a token source. Alternatively, we could create a NewConnectionWithClient(client *http.Client, vin string ...) *Connection function (with documentation + example).

This could be mirrored in a account.NewWithClient(...) function.

conn := Connection{
UserAgent: userAgent,
vin: vin,
client: http.Client{},
serverURL: serverURL,
authHeader: authHeader,
inbox: make(chan []byte, connector.BufferSize),
UserAgent: userAgent,
vin: vin,
client: client,
serverURL: serverURL,
inbox: make(chan []byte, connector.BufferSize),
}
return &conn
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/teslamotors/vehicle-command/pkg/connector/inet"
"github.com/teslamotors/vehicle-command/pkg/protocol"
"github.com/teslamotors/vehicle-command/pkg/vehicle"
"golang.org/x/oauth2"
)

const (
Expand All @@ -33,7 +34,8 @@ func getAccount(req *http.Request) (*account.Account, error) {
if !ok {
return nil, fmt.Errorf("client did not provide an OAuth token")
}
return account.New(token, proxyProtocolVersion)
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
return account.New(ts, proxyProtocolVersion)
}

// Proxy exposes an HTTP API for sending vehicle commands.
Expand Down