Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
75
services/gateway/websocket/auth.go
Normal file
75
services/gateway/websocket/auth.go
Normal 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
|
||||
}
|
||||
105
services/gateway/websocket/auth_test.go
Normal file
105
services/gateway/websocket/auth_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
166
services/gateway/websocket/connections.go
Normal file
166
services/gateway/websocket/connections.go
Normal 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()
|
||||
}
|
||||
73
services/gateway/websocket/connections_test.go
Normal file
73
services/gateway/websocket/connections_test.go
Normal 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")
|
||||
|
||||
}
|
||||
23
services/gateway/websocket/errors.go
Normal file
23
services/gateway/websocket/errors.go
Normal 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)
|
||||
}
|
||||
50
services/gateway/websocket/helpers.go
Normal file
50
services/gateway/websocket/helpers.go
Normal 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
|
||||
}
|
||||
42
services/gateway/websocket/helpers_test.go
Normal file
42
services/gateway/websocket/helpers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
17
services/gateway/websocket/mock_conn.go
Normal file
17
services/gateway/websocket/mock_conn.go
Normal 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 }
|
||||
413
services/gateway/websocket/session.go
Normal file
413
services/gateway/websocket/session.go
Normal 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")
|
||||
|
||||
}
|
||||
242
services/gateway/websocket/session_hmi.go
Normal file
242
services/gateway/websocket/session_hmi.go
Normal 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
|
||||
}
|
||||
196
services/gateway/websocket/session_hmi_test.go
Normal file
196
services/gateway/websocket/session_hmi_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
194
services/gateway/websocket/session_mobile.go
Normal file
194
services/gateway/websocket/session_mobile.go
Normal 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
|
||||
}
|
||||
86
services/gateway/websocket/session_mobile_test.go
Normal file
86
services/gateway/websocket/session_mobile_test.go
Normal 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)
|
||||
}
|
||||
420
services/gateway/websocket/session_test.go
Normal file
420
services/gateway/websocket/session_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
307
services/gateway/websocket/session_trex.go
Normal file
307
services/gateway/websocket/session_trex.go
Normal 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)
|
||||
}
|
||||
191
services/gateway/websocket/session_trex_test.go
Normal file
191
services/gateway/websocket/session_trex_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user