// replyTopicDescBasic loads minimal topic Desc when the requester is not subscribed to the topic func replyTopicDescBasic(sess *Session, topic string, get *MsgClientGet) { log.Printf("hub.replyTopicDescBasic: topic %s", topic) now := time.Now().UTC().Round(time.Millisecond) desc := &MsgTopicDesc{} if strings.HasPrefix(topic, "grp") { stopic, err := store.Topics.Get(topic) if err == nil { desc.CreatedAt = &stopic.CreatedAt desc.UpdatedAt = &stopic.UpdatedAt desc.Public = stopic.Public } else { sess.queueOut(ErrUnknown(get.Id, get.Topic, now)) return } } else { // 'me' and p2p topics var uid types.Uid if strings.HasPrefix(topic, "usr") { // User specified as usrXXX uid = types.ParseUserId(topic) } else if strings.HasPrefix(topic, "p2p") { // User specified as p2pXXXYYY uid1, uid2, _ := types.ParseP2P(topic) if uid1 == sess.uid { uid = uid2 } else if uid2 == sess.uid { uid = uid1 } } if uid.IsZero() { sess.queueOut(ErrMalformed(get.Id, get.Topic, now)) return } suser, err := store.Users.Get(uid) if err == nil { desc.CreatedAt = &suser.CreatedAt desc.UpdatedAt = &suser.UpdatedAt desc.Public = suser.Public } else { log.Printf("hub.replyTopicInfoBasic: sending error 3") sess.queueOut(ErrUnknown(get.Id, get.Topic, now)) return } } log.Printf("hub.replyTopicDescBasic: sending desc -- OK") sess.queueOut(&ServerComMessage{ Meta: &MsgServerMeta{Id: get.Id, Topic: get.Topic, Timestamp: &now, Desc: desc}}) }
// validateTopicName expands session specific topic name to global name // Returns // topic: session-specific topic name the message recepient should see // routeTo: routable global topic name // err: *ServerComMessage with an error to return to the sender func (s *Session) validateTopicName(msgId, topic string, timestamp time.Time) (string, string, *ServerComMessage) { if topic == "" { return "", "", ErrMalformed(msgId, "", timestamp) } if !strings.HasPrefix(topic, "grp") && s.uid.IsZero() { // me and p2p topics require authentication return "", "", ErrAuthRequired(msgId, topic, timestamp) } // Topic to route to i.e. rcptto: or s.subs[routeTo] routeTo := topic if topic == "me" { routeTo = s.uid.UserId() } else if topic == "fnd" { routeTo = s.uid.FndName() } else if strings.HasPrefix(topic, "usr") { // Initiating a p2p topic uid2 := types.ParseUserId(topic) if uid2.IsZero() { // Ensure the user id is valid return "", "", ErrMalformed(msgId, topic, timestamp) } else if uid2 == s.uid { // Use 'me' to access self-topic return "", "", ErrPermissionDenied(msgId, topic, timestamp) } routeTo = s.uid.P2PName(uid2) topic = routeTo } else if strings.HasPrefix(topic, "p2p") { uid1, uid2, err := types.ParseP2P(topic) if err != nil || uid1.IsZero() || uid2.IsZero() || uid1 == uid2 { // Ensure the user ids are valid return "", "", ErrMalformed(msgId, topic, timestamp) } else if uid1 != s.uid && uid2 != s.uid { // One can't access someone else's p2p topic return "", "", ErrPermissionDenied(msgId, topic, timestamp) } } return topic, routeTo, nil }
// Save message func (MessagesObjMapper) Save(msg *types.Message) error { msg.InitTimes() // Need a transaction here, RethinkDB does not support transactions // An invite (message to 'me') may have a zero SeqId if 'me' was inactive at the time of generating the invite if msg.SeqId == 0 { if user, err := adaptr.UserGet(types.ParseUserId(msg.Topic)); err != nil { return err } else { msg.SeqId = user.SeqId + 1 } } if err := adaptr.TopicUpdateOnMessage(msg.Topic, msg); err != nil { return err } return adaptr.MessageSave(msg) }
// replySetSub is a response to new subscription request or an update to a subscription {set.sub}: // update topic metadata cache, save/update subs, reply to the caller as {ctrl} message, generate an invite func (t *Topic) replySetSub(h *Hub, sess *Session, set *MsgClientSet) error { now := time.Now().UTC().Round(time.Millisecond) var uid types.Uid if uid = types.ParseUserId(set.Sub.User); uid.IsZero() && set.Sub.User != "" { // Invalid user ID sess.queueOut(ErrMalformed(set.Id, set.Topic, now)) return errors.New("invalid user id") } // if set.User is not set, request is for the current user if !uid.IsZero() { uid = sess.uid } if uid == sess.uid { return t.requestSub(h, sess, set.Id, set.Sub.Mode, set.Sub.Info, nil, false) } else { return t.approveSub(h, sess, uid, set) } }
func (t *Topic) run(hub *Hub) { log.Printf("Topic started: '%s'", t.name) keepAlive := TOPICTIMEOUT // TODO(gene): read keepalive value from the command line killTimer := time.NewTimer(time.Hour) killTimer.Stop() // 'me' only var uaTimer *time.Timer var currentUA string uaTimer = time.NewTimer(time.Minute) uaTimer.Stop() for { select { case sreg := <-t.reg: // Request to add a conection to this topic // The topic is alive, so stop the kill timer, if it's ticking. We don't want the topic to die // while processing the call killTimer.Stop() if err := t.handleSubscription(hub, sreg); err == nil { // give a broadcast channel to the connection (.read) // give channel to use when shutting down (.done) sreg.sess.subs[t.name] = &Subscription{ broadcast: t.broadcast, done: t.unreg, meta: t.meta, ping: t.uaChange} t.sessions[sreg.sess] = true } else if len(t.sessions) == 0 { // Failed to subscribe, the topic is still inactive killTimer.Reset(keepAlive) } case leave := <-t.unreg: // Remove connection from topic; session may continue to function if leave.unsub { // User wants to leave and unsubscribe. if err := t.replyLeaveUnsub(hub, leave.sess, leave.reqId, leave.topic); err != nil { log.Panicln(err) continue } } else { now := time.Now().UTC().Round(time.Millisecond) // Just leaving the topic without unsubscribing delete(t.sessions, leave.sess) pud := t.perUser[leave.sess.uid] pud.online-- if t.cat == types.TopicCat_Me { mrs := t.mostRecentSession() if mrs == nil { // Last session mrs = leave.sess } else { // Change UA to the most recent live session currentUA = mrs.userAgent uaTimer.Reset(UA_TIMER_DELAY) } // Update user's last online timestamp & user agent if err := store.Users.UpdateLastSeen(mrs.uid, mrs.userAgent, now); err != nil { log.Println(err) } } else if t.cat == types.TopicCat_Grp && pud.online == 0 { t.presPubChange(leave.sess.uid, "off") } t.perUser[leave.sess.uid] = pud if leave.reqId != "" { leave.sess.queueOut(NoErr(leave.reqId, leave.topic, now)) } } // If there are no more subscriptions to this topic, start a kill timer if len(t.sessions) == 0 { killTimer.Reset(keepAlive) } case msg := <-t.broadcast: // Content message intended for broadcasting to recepients // log.Printf("topic[%s].run: got message '%v'", t.name, msg) // Record last message timestamp if msg.Data != nil { from := types.ParseUserId(msg.Data.From) // msg.sessFrom is not nil when the message originated at the client. // for internally generated messages, like invites, the akn is nil if msg.sessFrom != nil { userData := t.perUser[from] if userData.modeWant&userData.modeGiven&types.ModePub == 0 { msg.sessFrom.queueOut(ErrPermissionDenied(msg.id, t.original, msg.timestamp)) continue } } if err := store.Messages.Save(&types.Message{ ObjHeader: types.ObjHeader{CreatedAt: msg.Data.Timestamp}, SeqId: t.lastId + 1, Topic: t.name, From: from.String(), Content: msg.Data.Content}); err != nil { log.Printf("topic[%s].run: failed to save message: %v", t.name, err) msg.sessFrom.queueOut(ErrUnknown(msg.id, t.original, msg.timestamp)) continue } t.lastId++ msg.Data.SeqId = t.lastId if msg.id != "" { reply := NoErrAccepted(msg.id, t.original, msg.timestamp) reply.Ctrl.Params = map[string]int{"seq": t.lastId} msg.sessFrom.queueOut(reply) } t.presPubMessageSent(t.lastId) } else if msg.Pres != nil { // log.Printf("topic[%s].run: pres.src='%s' what='%s'", t.name, msg.Pres.Src, msg.Pres.What) t.presProcReq(msg.Pres.Src, (msg.Pres.What == "on"), msg.Pres.wantReply) if t.original != msg.Pres.Topic { // This is just a request for status, don't forward it to sessions continue } } else if msg.Info != nil { if msg.Info.SeqId > t.lastId { // Skip bogus read notification continue } if msg.Info.What == "read" || msg.Info.What == "recv" { uid := types.ParseUserId(msg.Info.From) pud := t.perUser[uid] var read, recv int if msg.Info.What == "read" { if msg.Info.SeqId > pud.readId { pud.readId = msg.Info.SeqId read = pud.readId } else { // No need to report stale or bogus read status continue } } else if msg.Info.What == "recv" { if msg.Info.SeqId > pud.recvId { pud.recvId = msg.Info.SeqId recv = pud.recvId } else { continue } } if pud.readId > pud.recvId { pud.recvId = pud.readId recv = pud.recvId } if err := store.Subs.Update(t.name, uid, map[string]interface{}{ "RecvSeqId": pud.recvId, "ReadSeqId": pud.readId}); err != nil { log.Printf("topic[%s].run: failed to update SeqRead/Recv counter: %v", t.name, err) continue } t.presPubMessageCount(msg.sessSkip, 0, recv, read) t.perUser[uid] = pud } } // Broadcast the message. Only {data}, {pres}, {ping} are broadcastable. // {meta} and {ctrl} are sent to the session only if msg.Data != nil || msg.Pres != nil || msg.Info != nil { var packet, _ = json.Marshal(msg) for sess := range t.sessions { if sess == msg.sessSkip { continue } select { case sess.send <- packet: default: log.Printf("topic[%s].run: connection stuck, detaching", t.name) t.unreg <- &sessionLeave{sess: sess, unsub: false} } } } else { log.Printf("topic[%s].run: wrong message type for broadcasting", t.name) } case meta := <-t.meta: log.Printf("topic[%s].run: got meta message '%v'", t.name, meta) // Request to get/set topic metadata if meta.pkt.Get != nil { // Get request if meta.what&constMsgMetaDesc != 0 { t.replyGetDesc(meta.sess, meta.pkt.Get.Id, false, meta.pkt.Get.Desc) } if meta.what&constMsgMetaSub != 0 { t.replyGetSub(meta.sess, meta.pkt.Get.Id, meta.pkt.Get.Sub) } if meta.what&constMsgMetaData != 0 { t.replyGetData(meta.sess, meta.pkt.Get.Id, meta.pkt.Get.Data) } } else if meta.pkt.Set != nil { // Set request if meta.what&constMsgMetaDesc != 0 { t.replySetDesc(meta.sess, meta.pkt.Set) } if meta.what&constMsgMetaSub != 0 { t.replySetSub(hub, meta.sess, meta.pkt.Set) } } else if meta.pkt.Del != nil { // Del request if meta.what == constMsgDelMsg { t.replyDelMsg(meta.sess, meta.pkt.Del) } else if meta.what == constMsgDelTopic { t.replyDelTopic(hub, meta.sess, meta.pkt.Del) } } case ua := <-t.uaChange: // process an update to user agent from one of the sessions currentUA = ua uaTimer.Reset(UA_TIMER_DELAY) case <-uaTimer.C: // Publish user agent changes after a delay t.presPubUAChange(currentUA) case <-killTimer.C: // Topic timeout log.Println("Topic timeout: ", t.name) hub.unreg <- &topicUnreg{topic: t.name} if t.cat == types.TopicCat_Me { t.presPubMeChange("off", currentUA) } return case sd := <-t.exit: // Perform cleanup and database updates before server shuts down. // FIXME(gene): save lastMessage value; if t.cat == types.TopicCat_Me { t.presPubMeChange("off", currentUA) } else if t.cat == types.TopicCat_Grp { if sd.del { t.presPubTopicOnline("gone") } else { t.presPubTopicOnline("off") } } // not publishing online/offline to P2P topics // Report completion back to sender, if 'done' is not nil. if sd.done != nil { sd.done <- true } return } } }
// topicInit reads an existing topic from database or creates a new topic func topicInit(sreg *sessionJoin, h *Hub) { var t *Topic timestamp := time.Now().UTC().Round(time.Millisecond) t = &Topic{name: sreg.topic, original: sreg.pkt.Topic, sessions: make(map[*Session]bool), broadcast: make(chan *ServerComMessage, 256), reg: make(chan *sessionJoin, 32), unreg: make(chan *sessionLeave, 32), meta: make(chan *metaReq, 32), perUser: make(map[types.Uid]perUserData), exit: make(chan *shutDown, 1), } // Request to load a 'me' topic. The topic always axists. if t.original == "me" { log.Println("hub: loading 'me' topic") t.cat = types.TopicCat_Me // 'me' has no owner, t.owner = nil // Ensure all requests to subscribe are automatically rejected t.accessAuth = types.ModeBanned t.accessAnon = types.ModeBanned user, err := store.Users.Get(sreg.sess.uid) if err != nil { log.Println("hub: cannot load user object for 'me'='" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } if err = t.loadSubscribers(); err != nil { log.Println("hub: cannot load subscribers for '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } t.public = user.Public t.created = user.CreatedAt t.updated = user.UpdatedAt t.lastId = user.SeqId t.clearId = user.ClearId // Initiate User Agent with the UA of the creating session to report it later t.userAgent = sreg.sess.userAgent // Request to load a 'find' topic. The topic always exists. } else if t.original == "fnd" { log.Println("hub: loading 'fnd' topic") t.cat = types.TopicCat_Fnd // 'fnd' has no owner, t.owner = nil // The p2p topic cannot be accessed by anyone but the 2 participants. Access is checked at the session level. // Set default access in case user leaves - joins. t.accessAuth = types.ModeBanned t.accessAnon = types.ModeBanned if err := t.loadSubscribers(); err != nil { log.Println("hub: cannot load subscribers for '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } // Public is not set. // t.public = nil t.created = t.perUser[sreg.sess.uid].created t.updated = t.perUser[sreg.sess.uid].updated // Publishing to Find is not supported // t.lastId = 0 // Request to attach to a firehause topic. Must be root. } else if t.original == "fh" { log.Println("hub: loading 'fh' topic") t.cat = types.TopicCat_Fh // 'fh' has no owner, t.owner = nil // The fh can be accessed by anyone with root access // t.accessAuth = types.ModeBanned // t.accessAnon = types.ModeBanned // Subscriptions are ephemeral, no need to load subscribers. // TODO(gene): handle public more intelligently // t.public = nil // Publishing to Fh is not supported, history is not supported. // t.lastId = 0 // Request to load an existing or create a new p2p topic, then attach to it. } else if strings.HasPrefix(t.original, "usr") || strings.HasPrefix(t.original, "p2p") { log.Println("hub: loading or creating p2p topic") // Handle the following cases: // 1. Neither topic nor subscriptions exist: create a new p2p topic & subscriptions. // 2. Topic exists, one of the subscriptions is missing: // 2.1 Requester's subscription is missing, recreate it. // 2.2 Other user's subscription is missing, treat like a new request for user 2. // 3. Topic exists, both subscriptions are missing: should not happen, fail. // 4. Topic and both subscriptions exist: attach to topic t.cat = types.TopicCat_P2P // Check if the topic already exists stopic, err := store.Topics.Get(t.name) if err != nil { log.Println("hub: error while loading topic '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } // If topic exists, load subscriptions var subs []types.Subscription if stopic != nil { // Subs already have Public swapped if subs, err = store.Topics.GetSubs(t.name); err != nil { log.Println("hub: cannot load subscritions for '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } // Case 3, fail if len(subs) == 0 { log.Println("hub: missing both subscriptions for '" + t.name + "' (SHOULD NEVER HAPPEN!)") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } t.created = stopic.CreatedAt t.updated = stopic.UpdatedAt t.lastId = stopic.SeqId } // t.owner is blank for p2p topics // Ensure that other users are automatically rejected t.accessAuth = types.ModeP2P t.accessAnon = types.ModeBanned // t.public is not used for p2p topics since each user get a different public // Custom default access levels set in sreg.pkt.Init.DefaultAcs are ignored if stopic != nil && len(subs) == 2 { // Case 4. log.Println("hub: existing p2p topic") // t.clearId is not used in P2P topics, may change later for i := 0; i < 2; i++ { uid := types.ParseUid(subs[i].User) t.perUser[uid] = perUserData{ // Adapter already swapped the public values public: subs[i].GetPublic(), private: subs[i].Private, // lastSeenTag: subs[i].LastSeen, modeWant: subs[i].ModeWant, modeGiven: subs[i].ModeGiven} } } else { // Cases 1, 2 log.Println("hub: p2p new topic or one of the subs missing") var userData perUserData // Fetching records for both users. // Requester. userId1 := sreg.sess.uid // The other user. userId2 := types.ParseUserId(t.original) var u1, u2 int users, err := store.Users.GetAll(userId1, userId2) if err != nil { log.Println("hub: failed to load users for '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } else if len(users) != 2 { // Invited user does not exist log.Println("hub: missing user for '" + t.name + "'") sreg.sess.queueOut(ErrUserNotFound(sreg.pkt.Id, t.name, timestamp)) return } else { // User records are unsorted, make sure we know who is who. if users[0].Uid() == userId1 { u1, u2 = 0, 1 } else { u1, u2 = 1, 0 } } // Figure out which subscriptions are missing: User1's, User2's or both. var sub1, sub2 *types.Subscription // Set to true if only requester's subscription was created var user1only bool if len(subs) == 1 { if subs[0].Uid() == userId1 { // User2's subscription is missing sub1 = &subs[0] } else { // User1's is missing sub2 = &subs[0] user1only = true } } if sub1 == nil { if sreg.pkt.Set != nil && sreg.pkt.Set.Desc != nil && !isNullValue(sreg.pkt.Set.Desc.Private) { userData.private = sreg.pkt.Set.Desc.Private // Init.DefaultAcs and Init.Public are ignored for p2p topics } // User may set non-default access to topic, just make sure it's no higher than the default if sreg.pkt.Set != nil && sreg.pkt.Set.Sub != nil && sreg.pkt.Set.Sub.Mode != "" { if err := userData.modeWant.UnmarshalText([]byte(sreg.pkt.Set.Sub.Mode)); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + sreg.pkt.Set.Sub.Mode + "'") userData.modeWant = types.ModeP2P } else { // FIXME(gene): check for Mode.Banned userData.modeWant &= types.ModeP2P } } else if users[u1].Access.Auth != types.ModeNone { userData.modeWant = users[u1].Access.Auth } else { userData.modeWant = types.ModeP2P } sub1 = &types.Subscription{ User: userId1.String(), Topic: t.name, ModeWant: userData.modeWant, ModeGiven: types.ModeP2P, Private: userData.private} // Swap Public to match swapped Public in subs returned from store.Topics.GetSubs sub1.SetPublic(users[u2].Public) } if sub2 == nil { sub2 = &types.Subscription{ User: userId2.String(), Topic: t.name, ModeWant: users[u2].Access.Auth, ModeGiven: types.ModeP2P, Private: nil} // Swap Public to match swapped Public in subs returned from store.Topics.GetSubs sub2.SetPublic(users[u1].Public) } // Create everything if stopic == nil { if err = store.Topics.CreateP2P(sub1, sub2); err != nil { log.Println("hub: databse error in creating subscriptions '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } t.created = sub1.CreatedAt t.updated = sub1.UpdatedAt // t.lastId is not set (default 0) for new topics } else { // Recreate one of the subscriptions var subToMake *types.Subscription if user1only { subToMake = sub1 } else { subToMake = sub2 } if err = store.Subs.Create(subToMake); err != nil { log.Println("hub: databse error in re-subscribing user '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } } // t.clearId is not currently used for p2p topics // Publics is already swapped userData.public = sub1.GetPublic() userData.modeWant = sub1.ModeWant userData.modeGiven = sub1.ModeGiven t.perUser[userId1] = userData t.perUser[userId2] = perUserData{ public: sub2.GetPublic(), modeWant: sub2.ModeWant, modeGiven: sub2.ModeGiven, } sreg.created = !user1only } t.original = t.name // Processing request to create a new generic (group) topic: } else if t.original == "new" { log.Println("hub: new group topic") t.cat = types.TopicCat_Grp // Generic topics have parameters stored in the topic object t.owner = sreg.sess.uid t.accessAuth = DEFAULT_AUTH_ACCESS t.accessAnon = DEFAULT_ANON_ACCESS // Owner/creator gets full access to the topic. Owner may change the default modeWant through 'set'. userData := perUserData{ modeGiven: types.ModeFull, modeWant: types.ModeFull} if sreg.pkt.Set != nil { // User sent initialization parameters if sreg.pkt.Set.Desc != nil { if !isNullValue(sreg.pkt.Set.Desc.Public) { t.public = sreg.pkt.Set.Desc.Public } if !isNullValue(sreg.pkt.Set.Desc.Private) { userData.private = sreg.pkt.Set.Desc.Private } // set default access if sreg.pkt.Set.Desc.DefaultAcs != nil { if auth, anon, err := parseTopicAccess(sreg.pkt.Set.Desc.DefaultAcs, t.accessAuth, t.accessAnon); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + err.Error() + "'") } else if auth&types.ModeOwner != 0 || anon&types.ModeOwner != 0 { log.Println("hub: OWNER default access in topic '" + t.name) } else { t.accessAuth, t.accessAnon = auth, anon } } } // Owner/creator may restrict own access to topic if sreg.pkt.Set.Sub == nil || sreg.pkt.Set.Sub.Mode == "" { userData.modeWant = types.ModeFull } else { if err := userData.modeWant.UnmarshalText([]byte(sreg.pkt.Set.Sub.Mode)); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + sreg.pkt.Set.Sub.Mode + "'") } } } t.perUser[t.owner] = userData t.created = timestamp t.updated = timestamp // t.lastId & t.clearId are not set for new topics stopic := &types.Topic{ ObjHeader: types.ObjHeader{Id: sreg.topic, CreatedAt: timestamp}, Access: types.DefaultAccess{Auth: t.accessAuth, Anon: t.accessAnon}, Public: t.public} // store.Topics.Create will add a subscription record for the topic creator stopic.GiveAccess(t.owner, userData.modeWant, userData.modeGiven) err := store.Topics.Create(stopic, t.owner, t.perUser[t.owner].private) if err != nil { log.Println("hub: cannot save new topic '" + t.name + "' (" + err.Error() + ")") // Error sent on "new" topic sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } t.original = t.name // keeping 'new' as original has no value to the client sreg.created = true } else { log.Println("hub: existing group topic") t.cat = types.TopicCat_Grp // TODO(gene): check and validate topic name stopic, err := store.Topics.Get(t.name) if err != nil { log.Println("hub: error while loading topic '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } else if stopic == nil { log.Println("hub: topic '" + t.name + "' does not exist") sreg.sess.queueOut(ErrTopicNotFound(sreg.pkt.Id, t.original, timestamp)) return } if err = t.loadSubscribers(); err != nil { log.Println("hub: cannot load subscribers for '" + t.name + "' (" + err.Error() + ")") sreg.sess.queueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } // t.owner is set by loadSubscriptions t.accessAuth = stopic.Access.Auth t.accessAnon = stopic.Access.Anon t.public = stopic.Public t.created = stopic.CreatedAt t.updated = stopic.UpdatedAt t.lastId = stopic.SeqId t.clearId = stopic.ClearId } log.Println("hub: topic created or loaded: " + t.name) h.topicPut(t.name, t) h.topicsLive.Add(1) go t.run(h) sreg.loaded = true // Topic will check access rights, send invite to p2p user, send {ctrl} message to the initiator session t.reg <- sreg }
func (h *Hub) run() { for { select { case sreg := <-h.join: // Handle a subscription request: // 1. Init topic // 1.1 If a new topic is requested, create it // 1.2 If a new subscription to an existing topic is requested: // 1.2.1 check if topic is already loaded // 1.2.2 if not, load it // 1.2.3 if it cannot be loaded (not found), fail // 2. Check access rights and reject, if appropriate // 3. Attach session to the topic t := h.topicGet(sreg.topic) // is the topic already loaded? if t == nil { // Topic does not exist or not loaded go topicInit(sreg, h) } else { // Topic found. // Topic will check access rights and send appropriate {ctrl} t.reg <- sreg } case msg := <-h.route: // This is a message from a connection not subscribed to topic // Route incoming message to topic if topic permits such routing timestamp := time.Now().UTC().Round(time.Millisecond) if dst := h.topicGet(msg.rcptto); dst != nil { // Everything is OK, sending packet to known topic //log.Printf("Hub. Sending message to '%s'", dst.name) if dst.broadcast != nil { select { case dst.broadcast <- msg: default: log.Printf("hub: topic's broadcast queue is full '%s'", dst.name) } } } else { if msg.Data != nil { // Normally the message is persisted at the topic. If the topic is offline, // persist message here. The only case of sending to offline topics is invites/info to 'me' // The 'me' must receive them, so ignore access settings if err := store.Messages.Save(&types.Message{ ObjHeader: types.ObjHeader{CreatedAt: msg.Data.Timestamp}, Topic: msg.rcptto, // SeqId is assigned by the store.Mesages.Save From: types.ParseUserId(msg.Data.From).String(), Content: msg.Data.Content}); err != nil { msg.sessFrom.queueOut(ErrUnknown(msg.id, msg.Data.Topic, timestamp)) return } // TODO(gene): validate topic name, discarding invalid topics log.Printf("Hub. Topic[%s] is unknown or offline", msg.rcptto) for tt, _ := range h.topics { log.Printf("Hub contains topic '%s'", tt) } msg.sessFrom.queueOut(NoErrAccepted(msg.id, msg.rcptto, timestamp)) } } case meta := <-h.meta: log.Println("hub.meta: got message") // Request for topic info from a user who is not subscribed to the topic if dst := h.topicGet(meta.topic); dst != nil { // If topic is already in memory, pass request to topic dst.meta <- meta } else if meta.pkt.Get != nil { // If topic is not in memory, fetch requested description from DB and reply here go replyTopicDescBasic(meta.sess, meta.topic, meta.pkt.Get) } case unreg := <-h.unreg: // The topic is being garbage collected or deleted. h.topicUnreg(unreg.sess, unreg.topic, unreg.msg, unreg.fromSession, unreg.del) case hubdone := <-h.shutdown: topicsdone := make(chan bool) for _, topic := range h.topics { topic.exit <- &shutDown{done: topicsdone} } for i := 0; i < len(h.topics); i++ { <-topicsdone } log.Printf("Hub shutdown: terminated %d topics", len(h.topics)) // let the main goroutine know we are done with the cleanup hubdone <- true return case <-time.After(IDLETIMEOUT): } } }
// topicInit reads an existing topic from database or creates a new topic func topicInit(sreg *sessionJoin, h *Hub) { var t *Topic timestamp := time.Now().UTC().Round(time.Millisecond) t = &Topic{name: sreg.topic, original: sreg.pkt.Topic, sessions: make(map[*Session]bool), broadcast: make(chan *ServerComMessage, 256), reg: make(chan *sessionJoin, 32), unreg: make(chan *sessionLeave, 32), meta: make(chan *metaReq, 32), perUser: make(map[types.Uid]perUserData), shutdown: make(chan chan<- bool, 1), } // Request to load a me topic. The topic must exist if t.original == "me" { log.Println("hub: loading me topic") t.cat = TopicCat_Me // 'me' has no owner, t.owner = nil // Ensure all requests to subscribe are automatically rejected t.accessAuth = types.ModeBanned t.accessAnon = types.ModeBanned user, err := store.Users.Get(sreg.sess.uid) if err != nil { log.Println("hub: cannot load user object for 'me'='" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } if err = t.loadSubscribers(); err != nil { log.Println("hub: cannot load subscribers for '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } t.public = user.Public t.created = user.CreatedAt t.updated = user.UpdatedAt t.lastId = user.SeqId t.clearId = user.ClearId // Initiate User Agent with the UA of the creating session so we don't report it later t.userAgent = sreg.sess.userAgent // Request to create a new p2p topic, then attach to it } else if strings.HasPrefix(t.original, "usr") { log.Println("hub: new p2p topic") t.cat = TopicCat_P2P // t.owner is blank for p2p topics // Ensure that other users are automatically rejected t.accessAuth = types.ModeBanned t.accessAnon = types.ModeBanned var userData perUserData if sreg.pkt.Init != nil && !isNullValue(sreg.pkt.Init.Private) { // t.public is not used for p2p topics since each user get a different public userData.private = sreg.pkt.Init.Private // Init.DefaultAcs and Init.Public are ignored for p2p topics } // Custom default access levels set in sreg.pkt.Init.DefaultAcs are ignored // Fetch user's public and default access userId1 := sreg.sess.uid userId2 := types.ParseUserId(t.original) users, err := store.Users.GetAll(userId1, userId2) if err != nil { log.Println("hub: failed to load users for '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } else if len(users) != 2 { // invited user does not exist log.Println("hub: missing user for '" + t.name + "'") sreg.sess.QueueOut(ErrUserNotFound(sreg.pkt.Id, t.name, timestamp)) return } var u1, u2 int if users[0].Uid() == userId1 { u1 = 0 u2 = 1 } else { u1 = 1 u2 = 0 } // User may set non-default access to topic, just make sure it's no higher than the default if sreg.pkt.Sub != nil && sreg.pkt.Sub.Mode != "" { if err := userData.modeWant.UnmarshalText([]byte(sreg.pkt.Sub.Mode)); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + sreg.pkt.Sub.Mode + "'") userData.modeWant = types.ModeP2P } else { userData.modeWant &= types.ModeP2P } } else if users[u1].Access.Auth != types.ModeNone { userData.modeWant = users[u1].Access.Auth } else { userData.modeWant = types.ModeP2P } user1 := &types.Subscription{ User: userId1.String(), Topic: t.name, ModeWant: userData.modeWant, ModeGiven: types.ModeP2P, Private: userData.private} user1.SetPublic(users[u1].Public) user2 := &types.Subscription{ User: userId2.String(), Topic: t.name, ModeWant: users[u2].Access.Auth, ModeGiven: types.ModeP2P, Private: nil} user2.SetPublic(users[u2].Public) err = store.Topics.CreateP2P(user1, user2) if err != nil { log.Println("hub: databse error in creating subscriptions '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } // t.public is not used for p2p topics since each user gets a different public t.created = user1.CreatedAt t.updated = user1.UpdatedAt // t.lastId is not set (default 0) for new topics // t.clearId is not currently used for p2p topics userData.public = user2.GetPublic() userData.modeWant = user1.ModeWant userData.modeGiven = user1.ModeGiven t.perUser[userId1] = userData t.perUser[userId2] = perUserData{ public: user1.GetPublic(), modeWant: user2.ModeWant, modeGiven: user2.ModeGiven, } t.original = t.name sreg.created = true // Load an existing p2p topic } else if strings.HasPrefix(t.name, "p2p") { log.Println("hub: existing p2p topic") t.cat = TopicCat_P2P // t.owner no valid owner for p2p topics, leave blank // Ensure that other users are automatically rejected t.accessAuth = types.ModeBanned t.accessAnon = types.ModeBanned // Load the topic object stopic, err := store.Topics.Get(t.name) if err != nil { log.Println("hub: error while loading topic '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } else if stopic == nil { log.Println("hub: topic '" + t.name + "' does not exist") sreg.sess.QueueOut(ErrTopicNotFound(sreg.pkt.Id, t.original, timestamp)) return } subs, err := store.Topics.GetSubs(t.name) if err != nil { log.Println("hub: cannot load subscritions for '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.name, timestamp)) return } else if len(subs) != 2 { log.Println("hub: invalid number of subscriptions for '" + t.name + "'") sreg.sess.QueueOut(ErrTopicNotFound(sreg.pkt.Id, t.name, timestamp)) return } // t.public is not used for p2p topics since each user gets a different public t.created = stopic.CreatedAt t.updated = stopic.UpdatedAt t.lastId = stopic.SeqId // t.clearId is not used in P2P topics, may change later for i := 0; i < 2; i++ { uid := types.ParseUid(subs[i].User) t.perUser[uid] = perUserData{ // Based on other user public: subs[(i+1)%2].GetPublic(), private: subs[i].Private, // lastSeenTag: subs[i].LastSeen, modeWant: subs[i].ModeWant, modeGiven: subs[i].ModeGiven} } // Processing request to create a new generic (group) topic: } else if t.original == "new" { log.Println("hub: new group topic") t.cat = TopicCat_Grp // Generic topics have parameters stored in the topic object t.owner = sreg.sess.uid t.accessAuth = DEFAULT_AUTH_ACCESS t.accessAnon = DEFAULT_ANON_ACCESS // Owner/creator gets full access to topic userData := perUserData{modeGiven: types.ModeFull} // User sent initialization parameters if sreg.pkt.Init != nil { if !isNullValue(sreg.pkt.Init.Public) { t.public = sreg.pkt.Init.Public } if !isNullValue(sreg.pkt.Init.Private) { userData.private = sreg.pkt.Init.Private } // set default access if sreg.pkt.Init.DefaultAcs != nil { if auth, anon, err := parseTopicAccess(sreg.pkt.Init.DefaultAcs, t.accessAuth, t.accessAnon); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + err.Error() + "'") } else if auth&types.ModeOwner != 0 || anon&types.ModeOwner != 0 { log.Println("hub: OWNER default access in topic '" + t.name) } else { t.accessAuth, t.accessAnon = auth, anon } } } // Owner/creator may restrict own access to topic if sreg.pkt.Sub == nil || sreg.pkt.Sub.Mode == "" { userData.modeWant = types.ModeFull } else { if err := userData.modeWant.UnmarshalText([]byte(sreg.pkt.Sub.Mode)); err != nil { log.Println("hub: invalid access mode for topic '" + t.name + "': '" + sreg.pkt.Sub.Mode + "'") } } t.perUser[t.owner] = userData t.created = timestamp t.updated = timestamp // t.lastId & t.clearId are not set for new topics stopic := &types.Topic{ ObjHeader: types.ObjHeader{CreatedAt: timestamp}, Name: sreg.topic, Access: types.DefaultAccess{Auth: t.accessAuth, Anon: t.accessAnon}, Public: t.public} // store.Topics.Create will add a subscription record for the topic creator stopic.GiveAccess(t.owner, userData.modeWant, userData.modeGiven) err := store.Topics.Create(stopic, t.owner, t.perUser[t.owner].private) if err != nil { log.Println("hub: cannot save new topic '" + t.name + "' (" + err.Error() + ")") // Error sent on "new" topic sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } t.original = t.name // keeping 'new' as original has no value to the client sreg.created = true } else { log.Println("hub: existing group topic") t.cat = TopicCat_Grp // TODO(gene): check and validate topic name stopic, err := store.Topics.Get(t.name) if err != nil { log.Println("hub: error while loading topic '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } else if stopic == nil { log.Println("hub: topic '" + t.name + "' does not exist") sreg.sess.QueueOut(ErrTopicNotFound(sreg.pkt.Id, t.original, timestamp)) return } if err = t.loadSubscribers(); err != nil { log.Println("hub: cannot load subscribers for '" + t.name + "' (" + err.Error() + ")") sreg.sess.QueueOut(ErrUnknown(sreg.pkt.Id, t.original, timestamp)) return } // t.owner is set by loadSubscriptions t.accessAuth = stopic.Access.Auth t.accessAnon = stopic.Access.Anon t.public = stopic.Public t.created = stopic.CreatedAt t.updated = stopic.UpdatedAt t.lastId = stopic.SeqId t.clearId = stopic.ClearId } log.Println("hub: topic created or loaded: " + t.name) h.topicPut(t.name, t) h.topicsLive.Add(1) go t.run(h) sreg.loaded = true // Topic will check access rights, send invite to p2p user, send {ctrl} message to the initiator session t.reg <- sreg }