diff --git a/go.mod b/go.mod index 03882f2..ce32607 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 31337d4..045734c 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/pkg/account/account.go b/pkg/account/account.go index e038a49..b76d6db 100644 --- a/pkg/account/account.go +++ b/pkg/account/account.go @@ -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) @@ -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 @@ -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" @@ -114,8 +114,12 @@ func (p *oauthPayload) domain() string { // New returns an [Account] that can be used to fetch a [vehicle.Vehicle]. // 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) { + 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") } @@ -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 } @@ -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() @@ -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) @@ -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. diff --git a/pkg/account/account_test.go b/pkg/account/account_test.go index 16fd9ce..256ce50 100644 --- a/pkg/account/account_test.go +++ b/pkg/account/account_test.go @@ -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) } @@ -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) } @@ -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) } diff --git a/pkg/connector/inet/inet.go b/pkg/connector/inet/inet.go index a60f26b..22a5f38 100644 --- a/pkg/connector/inet/inet.go +++ b/pkg/connector/inet/inet.go @@ -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 { @@ -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) @@ -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 { @@ -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 { 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 } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 20a3225..c40b881 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -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 ( @@ -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.