Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
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")
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user