573 lines
11 KiB
Go
573 lines
11 KiB
Go
package routing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
msg "github.com/openware/rango/pkg/message"
|
|
"github.com/openware/rango/pkg/metrics"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/streadway/amqp"
|
|
)
|
|
|
|
type Request struct {
|
|
client IClient
|
|
msg.Request
|
|
}
|
|
|
|
// Hub maintains the set of active clients and broadcasts messages to the
|
|
// clients.
|
|
type Hub struct {
|
|
// Register Requests from the clients.
|
|
Requests chan Request
|
|
|
|
// Unregister requests from clients.
|
|
Unregister chan IClient
|
|
|
|
// List of clients registered to public topics
|
|
PublicTopics map[string]*Topic
|
|
|
|
// List of clients registered to private topics
|
|
PrivateTopics map[string]map[string]*Topic
|
|
|
|
// map[prefix -> map[topic -> *Topic]]
|
|
PrefixedTopics map[string]map[string]*Topic
|
|
|
|
// Storage for incremental objects
|
|
IncrementalObjects map[string]*IncrementalObject
|
|
|
|
// map[prefix -> allowed roles]
|
|
RBAC map[string][]string
|
|
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
type Event struct {
|
|
Scope string // global, public, private
|
|
Stream string // channel routing key
|
|
Type string // event type
|
|
Topic string // topic routing key (stream.type)
|
|
Body interface{} // event json body
|
|
}
|
|
|
|
type IncrementalObject struct {
|
|
Snapshot string
|
|
Increments []string
|
|
}
|
|
|
|
func NewHub(rbac map[string][]string) *Hub {
|
|
return &Hub{
|
|
Requests: make(chan Request),
|
|
Unregister: make(chan IClient),
|
|
PublicTopics: make(map[string]*Topic, 100),
|
|
PrivateTopics: make(map[string]map[string]*Topic, 1000),
|
|
PrefixedTopics: make(map[string]map[string]*Topic, 100),
|
|
IncrementalObjects: make(map[string]*IncrementalObject, 5),
|
|
RBAC: rbac,
|
|
}
|
|
}
|
|
|
|
func isIncrementObject(s string) bool {
|
|
return strings.HasSuffix(s, "-inc")
|
|
}
|
|
|
|
func isSnapshotObject(s string) bool {
|
|
return strings.HasSuffix(s, "-snap")
|
|
}
|
|
|
|
func isDebug() bool {
|
|
return log.Logger.GetLevel() <= zerolog.DebugLevel
|
|
}
|
|
|
|
func isTrace() bool {
|
|
return log.Logger.GetLevel() <= zerolog.TraceLevel
|
|
}
|
|
|
|
func getTopic(scope, stream, typ string) string {
|
|
if isSnapshotObject(typ) {
|
|
typ = strings.Replace(typ, "-snap", "-inc", 1)
|
|
}
|
|
if scope == "private" {
|
|
return typ
|
|
}
|
|
return stream + "." + typ
|
|
}
|
|
|
|
func (h *Hub) ListenWebsocketEvents() {
|
|
for {
|
|
select {
|
|
case req := <-h.Requests:
|
|
h.handleRequest(&req)
|
|
|
|
case client := <-h.Unregister:
|
|
log.Info().Msgf("Unregistering client (%s)", client.GetAuth().UID)
|
|
h.unsubscribeAll(client)
|
|
client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReceiveMsg handles AMQP messages
|
|
func (h *Hub) ReceiveMsg(delivery amqp.Delivery) {
|
|
if isTrace() {
|
|
log.Trace().Msgf("AMQP msg received: %s -> %s", delivery.RoutingKey, delivery.Body)
|
|
}
|
|
s := strings.Split(delivery.RoutingKey, ".")
|
|
|
|
var o interface{}
|
|
err := json.Unmarshal(delivery.Body, &o)
|
|
|
|
if err != nil {
|
|
log.Error().Msgf("JSON parse error: %s, msg: %s", err.Error(), delivery.Body)
|
|
return
|
|
}
|
|
|
|
switch len(s) {
|
|
case 2:
|
|
msg := Event{
|
|
Scope: s[0],
|
|
Stream: "",
|
|
Type: s[1],
|
|
Topic: getTopic(s[0], s[0], s[1]),
|
|
Body: o,
|
|
}
|
|
|
|
h.routeMessage(&msg)
|
|
|
|
case 3:
|
|
msg := Event{
|
|
Scope: s[0],
|
|
Stream: s[1],
|
|
Type: s[2],
|
|
Topic: getTopic(s[0], s[1], s[2]),
|
|
Body: o,
|
|
}
|
|
|
|
h.routeMessage(&msg)
|
|
|
|
default:
|
|
log.Error().Msgf("Bad routing key: %s", delivery.RoutingKey)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) SkipPrivateMsg(delivery amqp.Delivery) {
|
|
if strings.HasPrefix(delivery.RoutingKey, "private") {
|
|
return
|
|
}
|
|
|
|
h.ReceiveMsg(delivery)
|
|
}
|
|
|
|
func (h *Hub) handleSnapshot(msg *Event) (string, error) {
|
|
topic := msg.Stream + "." + msg.Type
|
|
body, err := json.Marshal(map[string]interface{}{
|
|
topic: msg.Body,
|
|
})
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
o, ok := h.IncrementalObjects[msg.Topic]
|
|
if !ok {
|
|
o = &IncrementalObject{}
|
|
h.IncrementalObjects[msg.Topic] = o
|
|
}
|
|
o.Snapshot = string(body)
|
|
o.Increments = []string{}
|
|
|
|
return string(body), nil
|
|
}
|
|
|
|
func (h *Hub) handleIncrement(msg *Event) (string, error) {
|
|
body, err := json.Marshal(map[string]interface{}{
|
|
msg.Topic: msg.Body,
|
|
})
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
o, ok := h.IncrementalObjects[msg.Topic]
|
|
if !ok {
|
|
return "", fmt.Errorf("No snapshot received before the increment for topic %s, ignoring", msg.Topic)
|
|
}
|
|
o.Increments = append(o.Increments, string(body))
|
|
return string(body), nil
|
|
|
|
}
|
|
|
|
func (h *Hub) handleMessage(topic *Topic, ok bool, msg *Event) {
|
|
switch {
|
|
case isIncrementObject(msg.Type):
|
|
rm, err := h.handleIncrement(msg)
|
|
if err != nil {
|
|
log.Error().Msgf("handleIncrement failed: %s", err.Error())
|
|
return
|
|
}
|
|
if ok {
|
|
topic.broadcastRaw(rm)
|
|
}
|
|
|
|
case isSnapshotObject(msg.Type):
|
|
_, err := h.handleSnapshot(msg)
|
|
if err != nil {
|
|
log.Error().Msgf("handleSnapshot failed: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
default:
|
|
if ok {
|
|
topic.broadcast(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Hub) routeMessage(msg *Event) {
|
|
if isTrace() {
|
|
log.Trace().Msgf("Routing message %v", msg)
|
|
}
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
switch msg.Scope {
|
|
case "public", "global":
|
|
topic, ok := h.PublicTopics[msg.Topic]
|
|
h.handleMessage(topic, ok, msg)
|
|
|
|
if !ok {
|
|
if isTrace() {
|
|
log.Trace().Msgf("No public registration to %s", msg.Topic)
|
|
log.Trace().Msgf("Public topics: %v", h.PublicTopics)
|
|
}
|
|
}
|
|
|
|
case "private":
|
|
uid := msg.Stream
|
|
uTopic, ok := h.PrivateTopics[uid]
|
|
if ok {
|
|
topic, ok := uTopic[msg.Topic]
|
|
if ok {
|
|
topic.broadcast(msg)
|
|
break
|
|
}
|
|
}
|
|
if isTrace() {
|
|
log.Trace().Msgf("No private registration to %s", msg.Topic)
|
|
log.Trace().Msgf("Private topics: %v", h.PrivateTopics)
|
|
}
|
|
|
|
default:
|
|
scope, ok := h.PrefixedTopics[msg.Scope]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
topic, ok := scope[msg.Topic]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
topic.broadcast(msg)
|
|
|
|
log.Trace().Msgf("Broadcasted message scope %s", msg.Scope)
|
|
}
|
|
|
|
}
|
|
|
|
func (h *Hub) unsubscribeAll(client IClient) {
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
for t, topic := range h.PublicTopics {
|
|
if topic.unsubscribe(client) {
|
|
metrics.RecordHubUnsubscription("public", t)
|
|
}
|
|
if topic.len() == 0 {
|
|
delete(h.PublicTopics, t)
|
|
}
|
|
}
|
|
|
|
for k, scope := range h.PrefixedTopics {
|
|
for t, topic := range scope {
|
|
if topic.unsubscribe(client) {
|
|
metrics.RecordHubUnsubscription("prefixed", t)
|
|
}
|
|
|
|
if topic.len() == 0 {
|
|
delete(scope, t)
|
|
}
|
|
}
|
|
|
|
if len(scope) == 0 {
|
|
delete(h.PrefixedTopics, k)
|
|
}
|
|
}
|
|
|
|
uid := client.GetAuth().UID
|
|
topics, ok := h.PrivateTopics[uid]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for t, topic := range topics {
|
|
if topic.unsubscribe(client) {
|
|
metrics.RecordHubUnsubscription("private", t)
|
|
}
|
|
if topic.len() == 0 {
|
|
delete(topics, t)
|
|
}
|
|
}
|
|
|
|
if len(topics) == 0 {
|
|
delete(h.PrivateTopics, uid)
|
|
}
|
|
|
|
}
|
|
|
|
func responseMust(e error, r interface{}) string {
|
|
res, err := msg.PackOutgoingResponse(e, r)
|
|
if err != nil {
|
|
log.Panic().Msg("responseMust failed:" + err.Error())
|
|
panic(err.Error())
|
|
}
|
|
|
|
return string(res)
|
|
}
|
|
|
|
func isPrivateStream(s string) bool {
|
|
return strings.Count(s, ".") == 0
|
|
}
|
|
func isPrefixedStream(s string) bool {
|
|
return strings.Count(s, ".") == 2
|
|
}
|
|
|
|
func (h *Hub) handleRequest(req *Request) {
|
|
switch req.Method {
|
|
case "subscribe":
|
|
h.handleSubscribe(req)
|
|
case "unsubscribe":
|
|
h.handleUnsubscribe(req)
|
|
default:
|
|
req.client.Send(responseMust(errors.New("unsupported method"), nil))
|
|
}
|
|
}
|
|
|
|
func (h *Hub) subscribePrivate(t string, req *Request) {
|
|
uid := req.client.GetAuth().UID
|
|
if uid == "" {
|
|
log.Error().Msgf("Anonymous user tried to subscribe to private stream %s", t)
|
|
return
|
|
}
|
|
|
|
uTopics, ok := h.PrivateTopics[uid]
|
|
if !ok {
|
|
uTopics = make(map[string]*Topic, 3)
|
|
h.PrivateTopics[uid] = uTopics
|
|
}
|
|
|
|
topic, ok := uTopics[t]
|
|
if !ok {
|
|
topic = NewTopic(h)
|
|
uTopics[t] = topic
|
|
}
|
|
|
|
if topic.subscribe(req.client) {
|
|
metrics.RecordHubSubscription("private", t)
|
|
req.client.SubscribePrivate(t)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) subscribePublic(t string, req *Request) {
|
|
topic, ok := h.PublicTopics[t]
|
|
if !ok {
|
|
topic = NewTopic(h)
|
|
h.PublicTopics[t] = topic
|
|
}
|
|
|
|
// Replay snapshot and buffered increments before joining the topic, so live
|
|
// broadcasts cannot interleave ahead of the initial ob-snap on market switch.
|
|
if isIncrementObject(t) {
|
|
o, ok := h.IncrementalObjects[t]
|
|
if ok && o.Snapshot != "" {
|
|
req.client.Send(o.Snapshot)
|
|
for _, inc := range o.Increments {
|
|
req.client.Send(inc)
|
|
}
|
|
}
|
|
}
|
|
|
|
if topic.subscribe(req.client) {
|
|
metrics.RecordHubSubscription("public", t)
|
|
req.client.SubscribePublic(t)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) premittedRBAC(prefix string, auth Auth) bool {
|
|
rbac := h.RBAC[prefix]
|
|
|
|
for _, role := range rbac {
|
|
if role == auth.Role {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func splitPrefixedTopic(prefixed string) (string, string) {
|
|
spl := strings.Split(prefixed, ".")
|
|
prefix := spl[0]
|
|
t := strings.TrimPrefix(prefixed, prefix+".")
|
|
|
|
return prefix, t
|
|
}
|
|
|
|
func (h *Hub) subscribePrefixed(prefixed string, req *Request) {
|
|
prefix, t := splitPrefixedTopic(prefixed)
|
|
|
|
if !h.premittedRBAC(prefix, req.client.GetAuth()) {
|
|
req.client.Send(responseMust(nil, map[string]interface{}{
|
|
"message": "cannot subscribe to " + prefixed,
|
|
}))
|
|
|
|
return
|
|
}
|
|
|
|
topics, ok := h.PrefixedTopics[prefix]
|
|
if !ok {
|
|
topics := make(map[string]*Topic, 0)
|
|
h.PrefixedTopics[prefix] = topics
|
|
}
|
|
|
|
topic, ok := topics[t]
|
|
if !ok {
|
|
topic = NewTopic(h)
|
|
h.PrefixedTopics[prefix][t] = topic
|
|
}
|
|
|
|
if isIncrementObject(t) {
|
|
o, ok := h.IncrementalObjects[t]
|
|
if ok && o.Snapshot != "" {
|
|
req.client.Send(o.Snapshot)
|
|
for _, inc := range o.Increments {
|
|
req.client.Send(inc)
|
|
}
|
|
}
|
|
}
|
|
|
|
if topic.subscribe(req.client) {
|
|
metrics.RecordHubSubscription("prefixed", prefixed)
|
|
req.client.SubscribePublic(prefixed)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) handleSubscribe(req *Request) {
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
for _, t := range req.Streams {
|
|
switch {
|
|
case isPrivateStream(t):
|
|
h.subscribePrivate(t, req)
|
|
case isPrefixedStream(t):
|
|
h.subscribePrefixed(t, req)
|
|
default:
|
|
h.subscribePublic(t, req)
|
|
}
|
|
}
|
|
|
|
req.client.Send(responseMust(nil, map[string]interface{}{
|
|
"message": "subscribed",
|
|
"streams": req.client.GetSubscriptions(),
|
|
}))
|
|
}
|
|
|
|
func (h *Hub) unsubscribePrivate(t string, req *Request) {
|
|
uid := req.client.GetAuth().UID
|
|
if uid == "" {
|
|
return
|
|
}
|
|
uTopics, ok := h.PrivateTopics[uid]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
topic, ok := uTopics[t]
|
|
if ok {
|
|
if topic.unsubscribe(req.client) {
|
|
metrics.RecordHubUnsubscription("private", t)
|
|
req.client.UnsubscribePrivate(t)
|
|
}
|
|
|
|
if topic.len() == 0 {
|
|
delete(uTopics, t)
|
|
}
|
|
}
|
|
|
|
uTopics, ok = h.PrivateTopics[uid]
|
|
if ok && len(uTopics) == 0 {
|
|
delete(h.PrivateTopics, uid)
|
|
}
|
|
}
|
|
|
|
func (h *Hub) unsubscribePrefixed(prefixed string, req *Request) {
|
|
scope, t := splitPrefixedTopic(prefixed)
|
|
topics, ok := h.PrefixedTopics[scope]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
topic, ok := topics[t]
|
|
if ok {
|
|
if topic.unsubscribe(req.client) {
|
|
metrics.RecordHubUnsubscription("prefixed", t)
|
|
req.client.UnsubscribePublic(t)
|
|
}
|
|
|
|
if topic.len() == 0 {
|
|
delete(topics, t)
|
|
h.PrefixedTopics[scope] = topics
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Hub) unsubscribePublic(t string, req *Request) {
|
|
topic, ok := h.PublicTopics[t]
|
|
if ok {
|
|
if topic.unsubscribe(req.client) {
|
|
metrics.RecordHubUnsubscription("public", t)
|
|
req.client.UnsubscribePublic(t)
|
|
}
|
|
|
|
if topic.len() == 0 {
|
|
delete(h.PublicTopics, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Hub) handleUnsubscribe(req *Request) {
|
|
h.mutex.Lock()
|
|
defer h.mutex.Unlock()
|
|
|
|
for _, t := range req.Streams {
|
|
switch {
|
|
case isPrivateStream(t):
|
|
h.unsubscribePrivate(t, req)
|
|
case isPrefixedStream(t):
|
|
h.unsubscribePrefixed(t, req)
|
|
default:
|
|
h.unsubscribePublic(t, req)
|
|
}
|
|
}
|
|
|
|
req.client.Send(responseMust(nil, map[string]interface{}{
|
|
"message": "unsubscribed",
|
|
"streams": req.client.GetSubscriptions(),
|
|
}))
|
|
}
|