Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
package websocket
import (
"fmt"
"net/http"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/utils/envtool"
"github.com/pkg/errors"
)
var authURL string = envtool.GetEnv("VERIFY_URL", "https://dev-auth.fiskerdps.com/auth/verify/")
// AuthEvent is the authentication message sent over websocket
type AuthEvent struct {
Topic string `json:"topic"`
Key string `json:"key"`
Payload AuthPayload `json:"payload"`
}
// AuthPayload describes the payload received
type AuthPayload struct {
Handler string `json:"handler"`
Data AuthData `json:"data"`
}
// AuthData describes the data received
type AuthData struct {
Token string `json:"token"`
}
// AuthResponse provides format for auth response
type AuthResponse struct {
Handler string `json:"handler"`
Data AuthResponseData `json:"data"`
}
// AuthResponseData provides data for auth response
type AuthResponseData struct {
Authenticated bool `json:"authenticated"`
}
// AuthenticateRequest checks for valid authentication message
func AuthenticateRequest(ae AuthEvent) (bool, error) {
if ae.Topic != "auth_service" || len(ae.Key) == 0 {
return false, errors.New("incorrect format")
}
switch ae.Payload.Handler {
case "verify":
return verifyToken(ae.Payload.Data)
}
return false, errors.New("invalid request")
}
func verifyToken(ad AuthData) (bool, error) {
tokenString := []string{fmt.Sprintf("bearer %s", ad.Token)}
resp, err := httpclient.Get(authURL, http.Header{"authorization": tokenString})
if err != nil {
return false, errors.WithStack(err)
}
return resp.StatusCode == 200, nil
}
func parseIDFromToken(token string) (string, error) {
payload, err := jwt.GetPayload(token)
if err != nil {
return fmt.Sprintf("%+v", payload), err
}
return fmt.Sprintf("%+v", payload), nil
}

View File

@@ -0,0 +1,105 @@
package websocket
import (
"net/http"
"testing"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/testhelper"
)
func TestVerifyTokenAuthorized(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
ad := AuthData{
Token: "validtoken",
}
ok, err := verifyToken(ad)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenAuthorized", nil, err)
}
if !ok {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenAuthorized", true, ok)
}
}
func TestVerifyTokenUnauthorized(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 401}, nil
},
}
httpclient.Client = &c
ad := AuthData{
Token: "invalidtoken",
}
ok, err := verifyToken(ad)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenUnauthorized", nil, err)
}
if ok {
t.Errorf(testhelper.TestErrorTemplate, "TestVerifyTokenUnauthorized", false, ok)
}
}
func TestAuthenticateRequest(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
ae := AuthEvent{
Topic: "auth_service",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
ok, err := AuthenticateRequest(ae)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequest", nil, err)
}
if !ok {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequest", true, ok)
}
}
func TestAuthenticateRequestInvalid(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 401}, nil
},
}
httpclient.Client = &c
ae := AuthEvent{
Topic: "invalid_topic",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
_, err := AuthenticateRequest(ae)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestAuthenticateRequestInvalid", "error", nil)
}
}

View File

@@ -0,0 +1,166 @@
package websocket
import (
"context"
"encoding/json"
"sync"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/scheduler"
"github.com/pkg/errors"
"github.com/robfig/cron"
)
func NewConnections() *Connections {
return &Connections{
sessions: make(map[string]SessionInterface),
expiration: scheduler.Bucket[SessionInterface]{},
}
}
type Connections struct {
sessions map[string]SessionInterface
expiration scheduler.Bucket[SessionInterface]
mu sync.RWMutex
}
func (c *Connections) getSession(id string) (SessionInterface, bool) {
c.mu.RLock()
session, ok := c.sessions[id]
c.mu.RUnlock()
return session, ok
}
func (c *Connections) addSession(key string, session SessionInterface) {
c.mu.Lock()
c.sessions[key] = session
logger.At(logger.Info(), key, "conn").
Str("ip", session.GetIP()).
Int("connections", c.length()).
Msgf("added connection %s", key)
c.mu.Unlock()
}
func (c *Connections) deleteSession(session SessionInterface) {
c.mu.Lock()
key := session.Key()
delete(c.sessions, key)
logger.At(logger.Info(), key, "conn").
Str("ip", session.GetIP()).
Int("connections", c.length()).
Msgf("removed connection %s", key)
c.mu.Unlock()
}
// Add connection to map
func (c *Connections) Add(session SessionInterface) error {
key := session.Key()
expiredSession, exists := c.getSession(key)
if exists {
expiredSession.SendMsgToClient(DuplicateConnectionMessage())
// if connection already exists, skip teardown when closing connection
// otherwise the car status will be changed to offline
expiredSession.SkipTeardown(true)
c.Remove(expiredSession)
}
c.addSession(key, session)
if exists && expiredSession != nil {
if expiredSession.GetType() == common.HMI.String() { // workaround for HMI sessions
// if connection already exists, skip teardown when closing connection
// otherwise the car status will be changed to offline
logger.At(logger.Info(), "Connections::checkIfExists schedule", key).Send()
c.Schedule(expiredSession)
} else {
expiredSession.Close()
logger.At(logger.Info(), key, "conn").Msgf("existing connection %s is closed", key)
}
logger.At(logger.Info(), key, "conn").Msgf("removing duplicate connection %s", key)
}
return nil
}
// Remove connection from map
//
// if connection is not equal to the connection in map,
// does not remove
func (c *Connections) Remove(session SessionInterface) error {
id := session.Key()
expiredSesssion, ok := c.getSession(id)
if !ok {
return missingWebsocketError(id)
}
if expiredSesssion != session {
return wrongSessionError(id)
}
c.deleteSession(session)
return nil
}
// Send to websocket connection
func (c *Connections) SendMsgToClient(id string, message []byte) error {
session, ok := c.getSession(id)
if !ok {
return missingWebsocketError(id)
}
return session.SendMsgToClient(message)
}
func (c *Connections) length() int {
return len(c.sessions)
}
func missingWebsocketError(id string) error {
return errors.Errorf("no websocket connection found for ID: %v", id)
}
func wrongSessionError(id string) error {
return errors.Errorf("%v does not match with existing connection", id)
}
func DuplicateConnectionMessage() []byte {
m := common.Message{
Handler: "error",
Data: common.MessageString{
Message: "disconnected by duplicate ID",
},
}
p, _ := json.Marshal(m)
return p
}
func (c *Connections) Schedule(session SessionInterface) error {
logger.Info().Msgf("Scheduling session to expire in 6 min %s. type %s", session.GetID(), session.GetType())
c.expiration.Schedule(session)
return nil
}
func (c *Connections) RunExpiration(ctx context.Context) {
cr := cron.New()
cr.AddFunc("@every 30s", func() {
c.expiration.Process(func(session SessionInterface) {
logger.Debug().Msgf("RunExpiration::closing session %s ", session.Key())
session.Close()
})
})
cr.Start()
<-ctx.Done()
cr.Stop()
}

View File

@@ -0,0 +1,73 @@
package websocket
import (
"fmt"
"net"
"sync"
"testing"
"time"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
)
func AddRemoveRedisListeners(add bool, id string, pubsub *redis.PubSub, queueRef *redis.Queues) {
if !add {
if err := pubsub.Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis PubSub conn remove failed").Err(err).Send()
}
if err := queueRef.Remove(id); err != nil {
logger.At(logger.Warn(), id, "Redis Queue conn remove failed").Err(err).Send()
}
} else {
if err := pubsub.Add(id); err != nil {
logger.At(logger.Error(), id, "Redis PubSub conn add failed").Err(err).Send()
}
if err := queueRef.Add(id); err != nil {
logger.At(logger.Error(), id, "Redis Queue conn add failed").Err(err).Send()
}
}
}
func TestSecureSessionTRexConnections(t *testing.T) {
redis.MockRedisConnection()
kafka.GetKafkaMock(nil)
pubSub := redis.NewPubSub(redis.GetMockPool().Get())
queueRef := redis.NewQueues(redis.GetMockPool())
var wg sync.WaitGroup
connections := NewConnections()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
ws, _ := net.Pipe()
id := fmt.Sprintf("fisker123")
if i%2 == 0 {
id = fmt.Sprintf("%s%d", id, i)
}
s := &SessionTRex{
Session: &Session{
Websocket: ws,
ID: id,
},
DBC: id,
ICCID: "!23454523453",
}
connections.Add(s)
AddRemoveRedisListeners(true, id, pubSub, queueRef)
time.Sleep(200 * time.Millisecond)
connections.Remove(s)
s.Close()
ws.Close()
wg.Done()
}(i)
}
wg.Wait()
t.Log("success")
}

View File

@@ -0,0 +1,23 @@
package websocket
import (
"fmt"
"strings"
"github.com/gobwas/ws"
"github.com/pkg/errors"
)
func isNormalClosure(err error) bool {
return strings.Contains(err.Error(), fmt.Sprintf("%d", ws.StatusNormalClosure))
}
var ErrFailedAuthentication = errors.New("failed authentication")
var ErrFailedToLoad = errors.New("failed loading")
var ErrInvalidHeaders = errors.New("request missing header Ssl-Client-Subject-Dn")
var ErrInvalidToken = errors.New("token missing username field")
func ErrInvalidHandler(handler string) error {
return errors.Errorf("%s is an invalid message handler", handler)
}

View File

@@ -0,0 +1,50 @@
package websocket
import (
"net/http"
"strings"
)
// ParseDBCFromRequest retrieves DBC version from the "fisker-dbc" field
//
// located in header of request
func ParseDBCFromRequest(r *http.Request) string {
return r.Header.Get("Fisker-Dbc-Sha256")
}
func ParseICCIDFromRequest(r *http.Request) (string, error) {
iccid := strings.TrimSpace(r.Header.Get("X-ICCID"))
// ok, err := validator.ValidateICCIDSimple(iccid)
// if err != nil {
// return iccid, err
// } else if !ok {
// return iccid, errors.Errorf("%s failed to pass ICCID validation", iccid)
// }
return iccid, nil
}
// ParseDeviceAndVersionFromRequest parses device type and version
//
// of client from User-Agent field in header
func ParseDeviceAndVersionFromRequest(r *http.Request) (string, string) {
var device string
var version string
userAgent := r.Header.Get("User-Agent")
specs := strings.Split(userAgent, " ")
device = strings.ToLower(specs[0])
switch len(specs) {
case 5:
version = specs[3]
case 4:
version = specs[2]
case 2:
version = specs[1]
}
return device, version
}

View File

@@ -0,0 +1,42 @@
package websocket_test
import (
"net/http"
"testing"
"gateway/websocket"
"fiskerinc.com/modules/testhelper"
)
func TestParseDeviceAndVersionFromRequest(t *testing.T) {
req := &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "Fisker T.Rex 1.2.3 [abc123]")
device, version := websocket.ParseDeviceAndVersionFromRequest(req)
if device != "fisker" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "fisker", device)
}
if version != "1.2.3" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3", version)
}
req = &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "HMI 1.2.3.4")
_, version = websocket.ParseDeviceAndVersionFromRequest(req)
if version != "1.2.3.4" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3.4", version)
}
req = &http.Request{Header: http.Header{}}
req.Header.Add("User-Agent", "Fisker T.Rex Ocean 1.2.3 [abc123]")
device, version = websocket.ParseDeviceAndVersionFromRequest(req)
if device != "fisker" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "fisker", device)
}
if version != "1.2.3" {
t.Errorf(testhelper.TestErrorTemplate, "TestParseDeviceAndVersionFromRequest", "1.2.3", version)
}
}

View File

@@ -0,0 +1,17 @@
package websocket
import (
"net"
"time"
)
type MockConn struct{}
func (c *MockConn) Read(b []byte) (n int, err error) { return 0, nil }
func (c *MockConn) Write(b []byte) (n int, err error) { return 0, nil }
func (c *MockConn) Close() error { return nil }
func (c *MockConn) LocalAddr() net.Addr { return &net.IPAddr{} }
func (c *MockConn) RemoteAddr() net.Addr { return &net.IPAddr{} }
func (c *MockConn) SetDeadline(t time.Time) error { return nil }
func (c *MockConn) SetReadDeadline(t time.Time) error { return nil }
func (c *MockConn) SetWriteDeadline(t time.Time) error { return nil }

View File

@@ -0,0 +1,413 @@
package websocket
import (
"compress/flate"
"context"
"encoding/json"
"fmt"
"gateway/sloppy"
"io"
"net"
"net/http"
"strings"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/validator"
"google.golang.org/protobuf/proto"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsflate"
"github.com/gobwas/ws/wsutil"
"github.com/pkg/errors"
)
var deadline = time.Duration(envtool.GetEnvInt("WS_TIMEOUT", 30)) * time.Second
// SessionInterface provides methods for connection
type SessionInterface interface {
Authenticate() error
Key() string
SendMsgToClient(message []byte) error
Receive() ([]byte, ws.OpCode, error)
Listen(context.Context, kafka.ProducerInterface) error
Load(kafka.ProducerInterface) error
Teardown(kafka.ProducerInterface) error
Close() error
GetWebsocket() net.Conn
GetIP() string
GetType() string
IsDevice(device common.Device) bool
GetID() string
GetUUID() int64
GetVIN() (string)
SkipTeardown(skip bool)
}
// NewSecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
// ex: "Fisker Ocean T.Rex 1.2.3.4 abc123" - T.Rex
// ex: "HMI 2.0.0.0" - HMI
func NewSecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
// HERE, get the vin and block the request
vin, err := utils.ParseVINFromRequest(r)
if err != nil {
logger.At(logger.Error(), "no vin from request", "conn").Send()
return s, err
}
ok := validator.ValidateVINSimple(vin)
if !ok {
logger.Error().Str("type", "conn").Str("VIN", vin).Msg("NewSecureSession failed to validate vin")
return s, errors.Errorf("%s failed to validate VIN", vin)
}
vin = strings.ToUpper(vin)
if !sloppy.GetVINBlocker().IsVINAllowed(vin){
return s, errors.Errorf("%s is not an allowed VIN, please contact support", vin)
}
device, version := ParseDeviceAndVersionFromRequest(r)
switch device {
case "fisker":
logger.At(logger.Info(), "1:"+vin, "conn")
iccid, err := ParseICCIDFromRequest(r)
if err != nil {
logger.At(logger.Warn(), "1:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to parse ICCID from request %s", vin)).Send()
}
s, err = NewTRexSession(w, r, vin, version, iccid)
if err != nil {
logger.At(logger.Warn(), "1:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to create Trex session %s", vin)).Send()
return s, err
}
logger.At(logger.Info(), "1:"+vin, "conn").Send()
case "hmi":
s, err = NewHMISession(w, r, vin, version)
if err != nil {
logger.At(logger.Warn(), "2:"+vin, "conn").
Err(errors.WithMessagef(err, "failed to create HMI session %s", vin)).Send()
return s, err
}
logger.At(logger.Info(), "2:"+vin, "conn").Send()
default:
return s, ErrFailedToLoad
}
return s, nil
}
// NewInsecureSession creates session w/ websocket based off user-agent
// given in HTTP request
//
// ex: "Mobile 1.2.3.4" - Mobile
func NewInsecureSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
var err error
device, version := ParseDeviceAndVersionFromRequest(r)
switch device {
case "mobile", "android", "ios":
s, err = NewMobileSession(w, r, version)
if err != nil {
return s, err
}
logger.At(logger.Info(), "3: "+s.GetID(), "conn").Send()
default:
return s, ErrFailedToLoad
}
return s, nil
}
// NewSession is used when device is unknown
func NewSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &Session{
Websocket: conn,
Type: common.Unknown,
epoch: time.Now().UnixNano(),
}, nil
}
// Session contains websocket info
type Session struct {
Websocket net.Conn
Type common.Device
ID string // used for key generation to kafka
Version string
epoch int64
skipteardown bool
}
// Authenticate returns id if proper authentication, else returns error
func (s *Session) Authenticate() error {
msg, _, err := s.Receive()
if err != nil {
return err
}
var ae AuthEvent
err = json.Unmarshal(msg, &ae)
if err != nil {
return errors.WithStack(err)
}
authenticated, err := AuthenticateRequest(ae)
if err != nil {
return err
} else if !authenticated {
return errors.New("failed authentication")
}
s.ID = ae.Key
return nil
}
// Key generates key based on type of session and ID
func (s *Session) Key() string {
if s.Type == common.Unknown {
return s.ID
}
return s.Type.Key(s.ID)
}
// SendMsgToClient: Send a message to client
func (s *Session) SendMsgToClient(message []byte) error {
vin := s.GetVIN()
logger.Debug().Str("type", s.GetType()).Str("VIN", vin).Int64("SessionID", s.GetUUID()).Str("value", string(message)).Msg("SendMsgToClient")
err := wsutil.WriteServerMessage(s.Websocket, ws.OpText, message)
if err != nil {
err = errors.WithStack(err)
}
return err
}
func (s *Session) extendDeadline() error {
return s.Websocket.SetDeadline(time.Now().Add(deadline))
}
func (s *Session) receive(postFrame func() error) ([]byte, ws.OpCode, error) {
var (
err error
h ws.Header
msg wsflate.MessageState
)
// Using nil as a source io.Reader since we will Reset() it in the loop
// below.
fr := wsflate.NewReader(nil, func(r io.Reader) wsflate.Decompressor {
return flate.NewReader(r)
})
controlHandler := wsutil.ControlFrameHandler(s.Websocket, ws.StateServerSide)
rd := wsutil.Reader{
Source: s.Websocket,
State: ws.StateServerSide | ws.StateExtended,
OnIntermediate: controlHandler,
Extensions: []wsutil.RecvExtension{&msg},
}
for {
h, err = rd.NextFrame()
if err != nil {
return nil, h.OpCode, err
}
if postFrame != nil {
err = postFrame()
if err != nil {
return nil, 0, err
}
}
if h.OpCode.IsControl() {
if err := controlHandler(h, &rd); err != nil {
return nil, h.OpCode, err
}
continue
}
var src io.Reader = &rd
if msg.IsCompressed() {
fr.Reset(&rd)
src = fr
}
data, err := io.ReadAll(src)
if err != nil {
return nil, h.OpCode, err
}
return data, h.OpCode, err
}
}
func (s *Session) Receive() ([]byte, ws.OpCode, error) {
return s.receive(nil)
}
// Listen to websocket session and use handler upon message received
func (s *Session) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), "Socket:Listen::EOF closing session ", key).Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Error(), "Socket:Listen::err during receiving session ", key).Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), "Socket:Listen:: failed route session ", key).Err(err).Send()
}
}
}
// Route messages
// - this allows other structs to override the behavior of messages received
func (s *Session) Route(producer kafka.ProducerInterface, data []byte) error {
var e common.EventRawJSON
err := e.Unmarshal(data)
if err != nil {
return errors.WithStack(err)
}
key := s.Key()
return producer.Produce(e.Topic, key, e.Payload, nil)
}
// Load the session - distributes messages to system notifying of new connection
func (s *Session) Load(producer kafka.ProducerInterface) error {
key := s.Key()
logger.At(logger.Info(), "Session::Load connection start notification", key).
Msgf("session.Load %s", key)
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}
// Teardown the session - distributes messages to system notifying of removed connection
func (s *Session) Teardown(producer kafka.ProducerInterface) error {
// Go to send del message to depot service if connection was a duplicate
if s.skipteardown {
return nil
}
key := s.Key()
logger.At(logger.Debug(), "Session::Teardown: Notify services ", key).
Msgf("session.Teardown %s", key)
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "del",
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}
// Close the session
func (s *Session) Close() error {
key := s.Key()
logger.At(logger.Debug(), "Session:Close connection for ", key)
return s.Websocket.Close()
}
// GetWebsocket returns session's websocket
func (s *Session) GetWebsocket() net.Conn {
return s.Websocket
}
// GetIP returns session's websocket's IP
func (s *Session) GetIP() string {
return s.Websocket.RemoteAddr().String()
}
// GetType returns Device type in string form
func (s *Session) GetType() string {
return s.Type.String()
}
func (s *Session) IsDevice(device common.Device) bool {
return s.Type == device
}
// GetID returns ID of session (not to be mistaken with key)
func (s *Session) GetID() string {
return s.ID
}
// GetUUID returns a unique identifier for the session
func (s *Session) GetUUID() int64 {
return s.epoch
}
func (s *Session) GetVIN() (vin string) {
// For somereason code was changed to do some kind of parsing from session, but VIN is added directly
return s.ID
}
func (s *Session) SkipTeardown(skip bool) {
s.skipteardown = skip
}
func PrintRequest(r *http.Request) string {
// Create return string
var request []string
// Add the request string
url := fmt.Sprintf("%v %v %v", r.Method, r.URL, r.Proto)
request = append(request, url)
// Add the host
request = append(request, fmt.Sprintf("Host: %v", r.Host))
// Loop through headers
for name, headers := range r.Header {
name = strings.ToLower(name)
for _, h := range headers {
request = append(request, fmt.Sprintf("%v: %v", name, h))
}
}
// If this is a POST, add post data
if r.Method == "POST" {
r.ParseForm()
request = append(request, "\n")
request = append(request, r.Form.Encode())
}
// Return the request as a string
return strings.Join(request, "\n")
}

View File

@@ -0,0 +1,242 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/security"
"github.com/gobwas/ws"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewHMISession serves as the constructor for HMI sessions
func NewHMISession(w http.ResponseWriter, r *http.Request, vin string, version string) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &SessionHMI{
Session: &Session{
Websocket: conn,
Type: common.HMI,
Version: version,
ID: vin,
epoch: time.Now().UnixNano(),
},
InAuthentication: false,
}, nil
}
// SessionHMI contains websocket info
type SessionHMI struct {
*Session
SessionID string
InAuthentication bool
}
// Authenticate returns id if proper authentication, else returns error
//
// validates VIN inputted through "key" field of message
func (s *SessionHMI) Authenticate() error {
s.InAuthentication = true
defer func() { s.InAuthentication = false }()
err := s.authenticate()
data, _ := json.Marshal(AuthResponse{
Handler: "verify",
Data: AuthResponseData{
Authenticated: err == nil,
},
})
s.SendMsgToClient(data)
if err != nil {
errors.Errorf("Unable to send to session %s", s.SessionID)
return errors.WithStack(err)
}
return nil
}
func (s *SessionHMI) SendMsgToClient(message []byte) error {
if !s.InAuthentication && len(s.SessionID) == 0 {
return fmt.Errorf("session is not authorized, %s", s.SessionID)
}
return s.Session.SendMsgToClient(message)
}
func (s *SessionHMI) authenticate() error {
s.InAuthentication = true
defer func() { s.InAuthentication = false }()
msg, _, err := s.Receive()
if err != nil {
errors.Errorf("unable to read socket %s", s.ID)
return err
}
var m common.HMISessionMessage
err = json.Unmarshal(msg, &m)
if err != nil {
return errors.WithStack(err)
} else if m.Handler != "verify" {
return errors.Errorf("incorrect auth handler specified %v", m.Handler)
}
switch {
case m.Data.SessionID != "":
salter, err := security.NewSalter(s.ID)
if err != nil {
errors.Errorf("unable to generate salt %s", s.ID)
return err
}
err = salter.ValidateSessionID(m.Data.SessionID)
if err != nil {
errors.Errorf("unable to validate session %s", m.Data.SessionID)
return err
}
s.SessionID = m.Data.SessionID
case m.Data.Salt != "":
s.SessionID = m.Data.Salt
default:
return ErrFailedAuthentication
}
return nil
}
// Listen to websocket session and use handler upon message received
func (s *SessionHMI) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
// logger.At(logger.Info(), key, "conn").Msg("OpClose") //commented out because reported by the call in websocket.go
return nil
} else if err != nil {
// logger.At(logger.Info(), key, "conn").Err(err).Send() //commented out because reported by the call in websocket.go
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").Err(err).Send()
}
}
}
// Route HMI messages
func (s *SessionHMI) Route(producer kafka.ProducerInterface, data []byte) error {
var m common.MessageRawJSON
err := m.Unmarshal(data)
if err != nil {
return err
}
if m.Verify == "ack"{
// Throw out ACK messages
return nil
}
key := s.Key()
logger.Debug().
Str("id", key).
Str("handler", m.Handler).
Int("size", len(data)).
Msgf("received from %v %s", key, string(data))
topic, ok := kafka.HMIHandlerTopics[m.Handler]
if !ok {
err = ErrInvalidHandler(m.Handler)
logger.Err(err).Str("Byte Data", string(data)).Str("Parsed Data", string(m.Data)).Msg("No Handler Insights")
return err
}
switch topic {
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteHMIGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteHMIToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteHMIPayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
}
if err != nil {
return err
}
return nil
}
func (s *SessionHMI) Load(producer kafka.ProducerInterface) error {
key := s.Key()
hmiSession := &kafka_grpc.GRPC_DepotPayload_HmiSession{
HmiSession: &kafka_grpc.HMISessionData{
SessionId: s.SessionID,
},
}
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: hmiSession,
}
binaryPayload, _ := proto.Marshal(&payload)
err := producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, binaryPayload, nil)
return err
}

View File

@@ -0,0 +1,196 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionHMI(t *testing.T) {
ws, _ := net.Pipe()
s := SessionHMI{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "websocket.SessionHMI" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMI", "websocket.SessionHMI", fmt.Sprintf("%T", s))
}
}
func TestNewHMISession(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewTRexSession(w, r, "1F15K3R45N1234567", "2.0.0", "123456789123456789123456789")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionHMIAuthenticate(t *testing.T) {
userAgent := "HMI 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.HMISessionMessage{
Handler: "verify",
Data: common.HMISessionData{
Salt: "XXXXXX",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionHMIListen(t *testing.T) {
userAgent := "HMI 1.2.3.4"
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIListen", nil, err)
}
}
func TestSessionHMIRoute(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
},
}
msg := common.Message{
Handler: "update_approve",
Data: "hello fisker!",
}
data, err := json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
msg = common.Message{
Handler: "invalid_handler",
Data: "false",
}
data, err = json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMIRoute", "error", err)
}
}
func TestSessionHMILoadSession(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
SessionID: "abc123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMILoadSession", nil, err)
}
}
func TestSessionHMITeardownSession(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionHMI{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
SessionID: "abc123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMILoadSession", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionHMITeardownSession", nil, err)
}
}

View File

@@ -0,0 +1,194 @@
package websocket
import (
"context"
"encoding/json"
"net/http"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/validator"
"google.golang.org/protobuf/proto"
"github.com/gobwas/ws"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewMobileSession serves as the constructor for HMI sessions
func NewMobileSession(w http.ResponseWriter, r *http.Request, version string) (SessionInterface, error) {
var s SessionInterface
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return s, errors.WithStack(err)
}
return &SessionMobile{
Session: &Session{
Websocket: conn,
Type: common.Mobile,
Version: version,
epoch: time.Now().UnixNano(),
},
}, nil
}
// SessionMobile contains websocket info
type SessionMobile struct {
*Session
}
// Authenticate returns id if proper authentication, else returns error
func (s *SessionMobile) Authenticate() error {
err := s.authenticate()
data, _ := json.Marshal(AuthResponse{
Handler: "verify",
Data: AuthResponseData{
Authenticated: err == nil,
},
})
s.SendMsgToClient(data)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *SessionMobile) authenticate() error {
msg, _, err := s.Receive()
if err != nil {
return err
}
var m common.MobileSessionMessage
err = json.Unmarshal(msg, &m)
if err != nil {
return errors.WithStack(err)
} else if m.Handler != "verify" {
return errors.Errorf("incorrect auth handler specified %v", m.Handler)
}
err = validator.ValidateStruct(m)
if err != nil {
return errors.WithStack(err)
}
valid := jwt.NewJWTValidator("")
payload, err := valid.ValidateToken(m.Data.Token)
if err != nil {
return err
}
id, ok := payload["username"].(string)
if !ok {
return ErrInvalidToken
}
s.ID = id
return nil
}
// Listen to websocket session and use handler upon message received
func (s *SessionMobile) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
key := s.Key()
for {
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), key, "conn").Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Info(), key, "conn").Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").
Err(err).Send()
}
}
}
// Route Mobile messages
func (s *SessionMobile) Route(producer kafka.ProducerInterface, data []byte) error {
var m common.MessageRawJSON
err := m.Unmarshal(data)
if err != nil {
return err
}
key := s.Key()
logger.At(logger.Debug(), key, "route").
Str("handler", m.Handler).
Int("size", len(data)).
Msgf("received from %v", key)
topic, ok := kafka.MobileHandlerTopics[m.Handler]
if !ok {
return ErrInvalidHandler(m.Handler)
}
switch topic {
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteMobileGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteMobileToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteMobilePayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
}
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,86 @@
package websocket
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionMobile(t *testing.T) {
ws, _ := net.Pipe()
s := SessionMobile{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobile", "websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
func TestNewSessionMobile(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewMobileSession(w, r, "1.2.3.4")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", "*websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionMobileAuthenticate(t *testing.T) {
userAgent := "Mobile 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewInsecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", "error", nil)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.MobileSessionMessage{
Handler: "verify",
Data: common.MobileSessionData{
Token: "validtoken",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionMobileAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}

View File

@@ -0,0 +1,420 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
m "fiskerinc.com/modules/common"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/httphead"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func createMockWebsocketClient(url, userAgent string) net.Conn {
u := "ws" + strings.TrimPrefix(url, "http")
header := make(http.Header)
header.Add("User-Agent", userAgent)
header.Add("X-ICCID", "12345678912345678923456789")
header.Add("Ssl-Client-Subject-Dn", "CN=1F15K3R45N1234567")
dialer := ws.Dialer{
Header: ws.HandshakeHeaderHTTP(header),
Extensions: []httphead.Option{httphead.NewOption("permessage-deflate", map[string]string{})},
}
ctx := context.Background()
ws, _, _, err := dialer.Dial(ctx, u)
if err != nil {
return nil
}
return ws
}
func TestSecureSessionTRex(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSecureSessionHMI(t *testing.T) {
userAgent := "HMI 2.0.0.0"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionHMI", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionHMI" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionHMI", "*websocket.SessionHMI", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestInsecureSessionMobile(t *testing.T) {
userAgent := "Mobile 1.2.3.4"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewInsecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionMobile" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionMobile", "*websocket.SessionMobile", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSession(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
}
if fmt.Sprintf("%T", s) != "*websocket.Session" {
t.Errorf(testhelper.TestErrorTemplate, "TestSession", "*websocket.Session", fmt.Sprintf("%T", s))
}
}
func TestNewSession(t *testing.T) {
userAgent := ""
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSession", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.Session" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSession", "*websocket.Session", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
}
func TestSessionAuthenticate(t *testing.T) {
c := mock.Client{
DoFunc: func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
httpclient.Client = &c
userAgent := ""
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
ae := AuthEvent{
Topic: "auth_service",
Key: "FISKER123",
Payload: AuthPayload{
Handler: "verify",
Data: AuthData{
Token: "validtoken",
},
},
}
msg, err := json.Marshal(ae)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionKey(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
key := s.Key()
if key != "1:FISKER123" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionKey", "1:FISKER123", key)
}
}
func TestSessionKeyUnknown(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.Unknown,
ID: "FISKER123",
}
key := s.Key()
if key != "FISKER123" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionKeyUnknown", "FISKER123", key)
}
}
func TestSessionReceive(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
defer s.Close()
data, _, err := s.Receive()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
msg := string(data)
if msg != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", payload, msg)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionReceive", nil, err)
}
}
func TestSessionListen(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionListen", nil, err)
}
}
func TestSessionSend(t *testing.T) {
userAgent := ""
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
defer s.Close()
data, _, err := s.Receive()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
msg := string(data)
if msg != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", payload, msg)
}
err = s.SendMsgToClient(data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
echo, _, err := wsutil.ReadServerData(conn)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", nil, err)
}
message := string(echo)
if message != payload {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionSend", payload, message)
}
}
func TestSessionLoad(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
}
func TestSessionTeardown(t *testing.T) {
ws, _ := net.Pipe()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", nil, err)
}
}
func TestSessionGetUUID(t *testing.T) {
ws, _ := net.Pipe()
currentNanoSeconds := time.Now().UnixNano()
s := &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
epoch: currentNanoSeconds,
}
uuid := s.GetUUID()
if uuid != currentNanoSeconds {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionLoad", currentNanoSeconds, uuid)
}
}
type SessionGetVINTestCase struct {
Name string
Session *Session
VIN string
ExpectedErrorMsg string
}
func TestSessionGetVIN(t *testing.T) {
ws, _ := net.Pipe()
tests := []SessionGetVINTestCase{
{
Name: "Parsable",
Session: &Session{
Websocket: ws,
Type: m.TRex,
ID: "VCF1EBU22PG001732",
},
VIN: "VCF1EBU22PG001732",
},
{
Name: "Not Parsable",
Session: &Session{
Websocket: ws,
Type: m.TRex,
ID: "FISKER123",
},
VIN: "FISKER123",
ExpectedErrorMsg: "could not get VIN from session",
},
}
for _, tt := range tests {
vin := tt.Session.GetID()
if vin != tt.VIN {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionGetVIN", tt.VIN, vin)
}
}
}

View File

@@ -0,0 +1,307 @@
package websocket
import (
"context"
"fmt"
"net/http"
"time"
"fiskerinc.com/modules/dbc/models"
"google.golang.org/protobuf/proto"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/kafka"
"fiskerinc.com/modules/logger"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsflate"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
// NewTRexSession serves as the constructor for TRex sessions
func NewTRexSession(w http.ResponseWriter, r *http.Request, vin, version, iccid string) (SessionInterface, error) {
var s SessionInterface
var compressionNegotiator = wsflate.Extension{
Parameters: wsflate.DefaultParameters,
}
var websocketUpgrader = ws.HTTPUpgrader{
Negotiate: compressionNegotiator.Negotiate,
}
conn, _, _, err := websocketUpgrader.Upgrade(r, w)
if err != nil {
return s, errors.WithStack(err)
}
if _, ok := compressionNegotiator.Accepted(); !ok {
conn.Close()
return s, errors.Errorf("didn't negotiate compression for %s", conn.RemoteAddr())
}
dbc := ParseDBCFromRequest(r)
return &SessionTRex{
Session: &Session{
Websocket: conn,
Type: common.TRex,
Version: version,
ID: vin,
epoch: time.Now().UnixNano(),
},
DBC: dbc,
ICCID: iccid,
}, nil
}
// SessionTRex utilizes a specialized listener
type SessionTRex struct {
*Session
DBC string
ICCID string
}
// Authenticate returns id if proper authentication, else returns error
func (s *SessionTRex) Authenticate() error {
return nil
}
func (s *SessionTRex) Receive() ([]byte, ws.OpCode, error) {
return s.receive(s.extendDeadline)
}
// Listen to websocket session and use handler upon message received
func (s *SessionTRex) Listen(ctx context.Context, producer kafka.ProducerInterface) error {
span, _ := tracer.StartSpanFromContext(ctx, "listen")
defer span.Finish()
defer s.Close()
for {
key := s.Key()
msg, op, err := s.Receive()
if op == ws.OpClose {
logger.At(logger.Info(), key, "conn").Msg("OpClose")
return nil
} else if err != nil {
logger.At(logger.Info(), key, "conn").Err(err).Send()
return err
}
err = s.Route(producer, msg)
if err != nil {
logger.At(logger.Warn(), key, "route").Err(err).Send()
}
}
}
// Route TRex messages
func (s *SessionTRex) Route(producer kafka.ProducerInterface, data []byte) error {
// TODO Unmarshal message and extract CAN frames into Kafka
var m common.MessageRawJSON
var err error
err = m.Unmarshal(data)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("msg %s", string(data)))
}
key := s.Key()
topic, ok := kafka.TRexHandlerTopics[m.Handler]
if !ok {
return ErrInvalidHandler(m.Handler)
}
switch topic {
case kafka.VehicleData:
m.Version = models.GetShortKey(s.DBC)
car := common.CarDataBatchPayload{}
grpcCan, err := car.ToGrpc(m, s.GetID())
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert to GRPC")).Send()
return err
}
if s.ID == "VCF1ZBU29PG004227" {
ids := ""
event := logger.Warn()
for _, f := range grpcCan.Data.Frames{
// f.Value
ids = fmt.Sprintf("%s, %d", ids, f.ID)
event.Str(fmt.Sprintf("%d",f.ID), fmt.Sprintf("%X", f.Value))
}
event.Msg(s.ID)
}
grpcData, err := proto.Marshal(grpcCan)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal GRPC")).Send()
return err
}
err = producer.ProduceBinary(kafka.VehicleData, s.GetID(), grpcData, nil)
grpcData = nil
grpcCan = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer failed")).Send()
return err
}
case kafka.LogService:
var grpcLogs *kafka_grpc.TRexLogs_BatchPayload
switch m.Handler {
case "trex_log":
logs := common.TRexLogs{}
grpcLogs, err = logs.ToGrpc(m)
case "error":
errorr := common.TRexError{}
grpcLogs, err = errorr.ToGrpc(m)
}
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert trex logs to GRPC"+string(m.Data[:]))).Send()
return err
}
grpcData, err := proto.Marshal(grpcLogs)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC"+string(m.Data[:]))).Send()
return err
}
err = producer.ProduceBinary(kafka.LogServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
grpcLogs = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.LogServiceGRPCKafka: // This case should not be necessary, but just in case someone gets confused, it is in here
var grpcLogs *kafka_grpc.TRexLogs_BatchPayload
switch m.Handler {
case "trex_log":
logs := common.TRexLogs{}
grpcLogs, err = logs.ToGrpc(m)
case "error":
errorr := common.TRexError{}
grpcLogs, err = errorr.ToGrpc(m)
}
// TODO: unable to convert trex logs msg {"code":-32601,"message":"The handler does not exist or is not available"}: json: cannot unmarshal string into Go struct field TRexError.message of type []common.TRexErrorMessage
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to convert trex logs msg "+string(m.Data[:]))).Send()
return err
}
grpcData, err := proto.Marshal(grpcLogs)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC"+string(m.Data[:]))).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.LogServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
grpcLogs = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.ValetServiceGRPCKafka:
valetData := common.ValetRouteTRexPayloadGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.ValetServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.DepotServiceGRPCKafka:
valetData := common.DepotRouteTRexToGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.DepotServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
case kafka.AttendantServiceGRPCKafka:
valetData := common.AttendantRouteTRexGRPC(m)
grpcData, err := proto.Marshal(valetData)
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "unable to marshal trexlogs GRPC "+topic)).Send()
return errors.WithStack(err)
}
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, key, grpcData, nil)
grpcData = nil
if err != nil {
logger.At(logger.Error(), key, "route").
Err(errors.Wrap(err, "Producer for trex logs failed")).Send()
return err
}
default:
err = producer.Produce(topic, key, m, nil)
if err != nil {
return err
}
}
return nil
}
func (s *SessionTRex) KafkaEndSessionMarker(producer kafka.ProducerInterface) error {
can := kafka_grpc.GRPC_BatchPayload{
Handler: "",
Data: nil,
Version: models.GetShortKey(s.DBC),
}
grpcData, _ := proto.Marshal(&can)
key := s.Key()
logger.At(logger.Info(), key, "conn").
Msgf("closing connection %s", key)
return producer.ProduceBinary(kafka.VehicleData, s.GetID(), grpcData, nil)
}
func (s *SessionTRex) Teardown(producer kafka.ProducerInterface) error {
s.KafkaEndSessionMarker(producer)
return s.Session.Teardown(producer)
}
// Load the session - distributes messages to system notifing of new connection
func (s *SessionTRex) Load(producer kafka.ProducerInterface) error {
m := &kafka_grpc.GRPC_DepotPayload_InitPayload{
InitPayload: &kafka_grpc.InitPayload{
Data: map[string]string{
"version": s.Version,
"iccid": s.ICCID,
"ip": s.GetIP(),
"dbc_version": s.DBC,
},
},
}
payload := kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: m,
}
binaryPayload, _ := proto.Marshal(&payload)
return producer.ProduceBinary(kafka.DepotServiceGRPCKafka, s.Key(), binaryPayload, nil)
}

View File

@@ -0,0 +1,191 @@
package websocket
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/grpc/kafka_grpc"
kafka "fiskerinc.com/modules/kafka/mock"
"fiskerinc.com/modules/testhelper"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func TestSessionTRex(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
},
}
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
func TestNewTRexSession(t *testing.T) {
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewTRexSession(w, r, "1F15K3R45N1234567", "1.2.3.4", "12345678912346789123456789")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", nil, err)
}
defer s.Close()
if fmt.Sprintf("%T", s) != "*websocket.SessionTRex" {
t.Errorf(testhelper.TestErrorTemplate, "TestNewSessionTRex", "*websocket.SessionTRex", fmt.Sprintf("%T", s))
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, "")
defer conn.Close()
}
func TestSessionTRexAuthenticate(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
defer s.Close()
err = s.Authenticate()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
am := common.TRexSessionMessage{
Handler: "verify",
Data: common.TRexSessionData{
VIN: "1HD1CGP134K410769",
},
}
msg, err := json.Marshal(am)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexAuthenticate", nil, err)
}
wsutil.WriteClientMessage(conn, ws.OpText, msg)
}
func TestSessionTRexListen(t *testing.T) {
userAgent := "Fisker Ocean T.Rex 1.2.3.4 abc123"
payload := "hello fisker!"
createNewSession := func(w http.ResponseWriter, r *http.Request) {
s, err := NewSecureSession(w, r)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
defer s.Close()
ctx := context.Background()
err = s.Listen(ctx, kafka.GetKafkaMock(nil))
if err.Error() != "EOF" {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
}
server := httptest.NewServer(http.HandlerFunc(createNewSession))
defer server.Close()
conn := createMockWebsocketClient(server.URL, userAgent)
defer conn.Close()
err := wsutil.WriteClientMessage(conn, ws.OpText, []byte(payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexListen", nil, err)
}
}
func TestSessionTRexRoute(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
},
}
msg := common.Message{
Handler: "canbus",
Data: &kafka_grpc.GRPC_CANData{
EpochUsec: 1653255445,
Dropped: 10,
Filtered: 20,
Frames: []*kafka_grpc.GRPC_CANFrame{
{
Epoch: 1642455023642165,
ID: 832,
Value: []byte("AAAAAAAAIAE="),
},
},
},
}
data, err := json.Marshal(msg)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexRoute", nil, err)
}
err = s.Route(kafka.GetKafkaMock(nil), data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexRoute", nil, err)
}
}
func TestSessionTRexLoad(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexLoad", nil, err)
}
}
func TestSessionTRexTeardown(t *testing.T) {
ws, _ := net.Pipe()
s := &SessionTRex{
Session: &Session{
Websocket: ws,
Type: common.TRex,
ID: "FISKER123",
},
}
err := s.Load(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexTeardown", nil, err)
}
err = s.Teardown(kafka.GetKafkaMock(nil))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestSessionTRexTeardown", nil, err)
}
}