func (TokenAuth) Authenticate(token string) (types.Uid, time.Time, int) { var zeroTime time.Time // [8:UID][4:expires][32:signature] == 44 bytes data, err := base64.URLEncoding.DecodeString(token) if err != nil { return types.ZeroUid, zeroTime, auth.ErrMalformed } if len(data) != token_len_decoded { return types.ZeroUid, zeroTime, auth.ErrMalformed } var uid types.Uid if err := uid.UnmarshalBinary(data[0:8]); err != nil { return types.ZeroUid, zeroTime, auth.ErrMalformed } hasher := hmac.New(sha256.New, hmac_salt) hasher.Write(data[:12]) if !hmac.Equal(data[12:], hasher.Sum(nil)) { return types.ZeroUid, zeroTime, auth.ErrFailed } expires := time.Unix(int64(binary.LittleEndian.Uint32(data[8:12])), 0).UTC() if expires.Before(time.Now()) { return types.ZeroUid, zeroTime, auth.ErrExpired } return uid, expires, auth.NoErr }
// evictUser evicts given user's sessions from the topic and clears user's cached data, if requested func (t *Topic) evictUser(uid types.Uid, clear bool, ignore *Session) { now := time.Now().UTC().Round(time.Millisecond) note := NoErrEvicted("", t.original, now) if clear { // Delete per-user data delete(t.perUser, uid) } else { // Clear online status pud := t.perUser[uid] pud.online = 0 t.perUser[uid] = pud } // Notify topic subscribers that the user has left the topic if t.cat == TopicCat_Grp { t.presPubChange(uid.UserId(), "off") } // Detach all user's sessions for sess, _ := range t.sessions { if sess.uid == uid { delete(t.sessions, sess) sess.detach <- t.name if sess != ignore { sess.QueueOut(note) } } } }
func (a *RethinkDbAdapter) UserUpdateStatus(uid t.Uid, status interface{}) error { update := map[string]interface{}{"Status": status} _, err := rdb.DB(a.dbName).Table("users").Get(uid.String()). Update(update, rdb.UpdateOpts{Durability: "soft"}).RunWrite(a.conn) return err }
// Announce to a single user on 'me' topic func (t *Topic) presAnnounceToUser(uid types.Uid, what string, seq int, skip *Session) { if pud, ok := t.perUser[uid]; ok { update := &MsgServerPres{Topic: "me", What: what, Src: t.original, SeqId: seq} if pud.modeGiven&pud.modeWant&types.ModePres != 0 { globals.hub.route <- &ServerComMessage{Pres: update, rcptto: uid.UserId(), sessSkip: skip} } } }
// Get a subscription of a user to a topic func (a *RethinkDbAdapter) SubscriptionGet(topic string, user t.Uid) (*t.Subscription, error) { rows, err := rdb.DB(a.dbName).Table("subscriptions").Get(topic + ":" + user.String()).Run(a.conn) if err != nil { return nil, err } var sub t.Subscription err = rows.One(&sub) return &sub, rows.Err() }
func (a *RethinkDbAdapter) UserUpdateLastSeen(uid t.Uid, userAgent string, when time.Time) error { update := struct { LastSeen time.Time UserAgent string }{when, userAgent} _, err := rdb.DB(a.dbName).Table("users").Get(uid.String()). Update(update, rdb.UpdateOpts{Durability: "soft"}).RunWrite(a.conn) return err }
// UserGet fetches a single user by user id. If user is not found it returns (nil, nil) func (a *RethinkDbAdapter) UserGet(uid t.Uid) (*t.User, error) { if row, err := rdb.DB(a.dbName).Table("users").Get(uid.String()).Run(a.conn); err == nil && !row.IsNil() { var user t.User if err = row.One(&user); err == nil { return &user, nil } return nil, err } else { // If user does not exist, it returns nil, nil return nil, err } }
// 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}}) }
func (TokenAuth) GenSecret(uid types.Uid, expires time.Time) (string, error) { // [8:UID][4:expires][32:signature] == 44 bytes buf := new(bytes.Buffer) uidbits, _ := uid.MarshalBinary() binary.Write(buf, binary.LittleEndian, uidbits) binary.Write(buf, binary.LittleEndian, uint32(expires.Unix())) hasher := hmac.New(sha256.New, hmac_salt) hasher.Write(buf.Bytes()) binary.Write(buf, binary.LittleEndian, hasher.Sum(nil)) return base64.URLEncoding.EncodeToString(buf.Bytes()), nil }
// Add user's authentication record func (a *RethinkDbAdapter) AddAuthRecord(uid t.Uid, unique string, secret []byte, expires time.Time) (error, bool) { _, err := rdb.DB(a.dbName).Table("auth").Insert( map[string]interface{}{ "unique": unique, "userid": uid.String(), "secret": secret, "expires": expires}).RunWrite(a.conn) if err != nil { if rdb.IsConflictErr(err) { return errors.New("duplicate credential"), true } return err, false } return nil, false }
// SubsForUser loads a list of user's subscriptions to topics func (a *RethinkDbAdapter) SubsForUser(forUser t.Uid) ([]t.Subscription, error) { if forUser.IsZero() { return nil, errors.New("RethinkDb adapter: invalid user ID in TopicGetAll") } q := rdb.DB(a.dbName).Table("subscriptions").GetAllByIndex("User", forUser.String()).Limit(MAX_RESULTS) rows, err := q.Run(a.conn) if err != nil { return nil, err } var subs []t.Subscription var ss t.Subscription for rows.Next(&ss) { subs = append(subs, ss) } return subs, rows.Err() }
// 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) } }
// Create creates a topic and owner's subscription to topic func (TopicsObjMapper) Create(topic *types.Topic, owner types.Uid, private interface{}) error { topic.InitTimes() err := adaptr.TopicCreate(topic) if err != nil { return err } if !owner.IsZero() { err = Subs.Create(&types.Subscription{ ObjHeader: types.ObjHeader{CreatedAt: topic.CreatedAt}, User: owner.String(), Topic: topic.Name, ModeGiven: types.ModeFull, ModeWant: topic.GetAccess(owner), Private: private}) } return err }
func (t *Topic) makeInvite(notify, target, from types.Uid, act types.InviteAction, modeWant, modeGiven types.AccessMode, info interface{}) *ServerComMessage { // FIXME(gene): this is a workaround for gorethink's broken way of marshalling json inv, err := json.Marshal(MsgInvitation{ Topic: t.name, User: target.UserId(), Action: act.String(), Acs: MsgAccessMode{modeWant.String(), modeGiven.String()}, Info: info}) if err != nil { log.Println(err) } converted := map[string]interface{}{} err = json.Unmarshal(inv, &converted) if err != nil { log.Println(err) } // endof workaround msg := &ServerComMessage{Data: &MsgServerData{ Topic: "me", From: from.UserId(), Timestamp: time.Now().UTC().Round(time.Millisecond), Content: converted}, rcptto: notify.UserId()} log.Printf("Invite generated: %#+v", msg.Data) return msg }
// Authenticate func (s *Session) login(msg *ClientComMessage) { var uid types.Uid var err error if !s.uid.IsZero() { s.QueueOut(ErrAlreadyAuthenticated(msg.Login.Id, "", msg.timestamp)) return } else if msg.Login.Scheme == "" || msg.Login.Scheme == "basic" { uid, err = store.Users.Login(msg.Login.Scheme, msg.Login.Secret) if err != nil { // DB error log.Println(err) s.QueueOut(ErrUnknown(msg.Login.Id, "", msg.timestamp)) return } else if uid.IsZero() { // Invalid login or password s.QueueOut(ErrAuthFailed(msg.Login.Id, "", msg.timestamp)) return } } else { s.QueueOut(ErrAuthUnknownScheme(msg.Login.Id, "", msg.timestamp)) return } s.uid = uid s.userAgent = msg.Login.UserAgent s.QueueOut(&ServerComMessage{Ctrl: &MsgServerCtrl{ Id: msg.Login.Id, Code: http.StatusOK, Text: http.StatusText(http.StatusOK), Timestamp: msg.timestamp, Params: map[string]interface{}{"uid": uid.UserId()}}}) }
// SubsUpdate updates a single subscription. func (a *RethinkDbAdapter) SubsUpdate(topic string, user t.Uid, update map[string]interface{}) error { _, err := rdb.DB(a.dbName).Table("subscriptions").Get(topic + ":" + user.String()).Update(update).RunWrite(a.conn) return err }
// Delete user's all authentication records func (a *RethinkDbAdapter) DelAllAuthRecords(uid t.Uid) (int, error) { res, err := rdb.DB(a.dbName).Table("auth").GetAllByIndex("userid", uid.String()).Delete().RunWrite(a.conn) return res.Deleted, err }
// approveSub processes a request to initiate an invite or approve a subscription request from another user: // Handle these cases: // A. Manager is inviting another user for the first time (no prior subscription) // B. Manager is re-inviting another user (adjusting modeGiven, modeWant is still "N") // C. Manager is changing modeGiven for another user, modeWant != "N" func (t *Topic) approveSub(h *Hub, sess *Session, target types.Uid, set *MsgClientSet) error { now := time.Now().UTC().Round(time.Millisecond) // Check if requester actually has permission to manage sharing if userData, ok := t.perUser[sess.uid]; !ok || !userData.modeGiven.IsManager() || !userData.modeWant.IsManager() { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("topic access denied") } // Parse the access mode granted var modeGiven types.AccessMode if set.Sub.Mode != "" { modeGiven.UnmarshalText([]byte(set.Sub.Mode)) } // If the user is banned from topic, make sute it's the only change if modeGiven.IsBanned() { modeGiven = types.ModeBanned } // Make sure no one but the owner can do an ownership transfer if modeGiven.IsOwner() && t.owner != sess.uid { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("attempt to transfer ownership by non-owner") } var givenBefore types.AccessMode // Check if it's a new invite. If so, save it to database as a subscription. // Saved subscription does not mean the user is allowed to post/read userData, ok := t.perUser[target] if !ok { if modeGiven == types.ModeNone { if t.accessAuth != types.ModeNone { // Request to use default access mode for the new subscriptions. modeGiven = t.accessAuth } else { sess.queueOut(ErrMalformed(set.Id, t.original, now)) return errors.New("cannot invite without giving any access rights") } } // Add subscription to database sub := &types.Subscription{ User: target.String(), Topic: t.name, ModeWant: types.ModeNone, ModeGiven: modeGiven, } if err := store.Subs.Create(sub); err != nil { sess.queueOut(ErrUnknown(set.Id, t.original, now)) return err } userData = perUserData{ modeGiven: sub.ModeGiven, modeWant: sub.ModeWant, private: nil, } t.perUser[target] = userData } else { // Action on an existing subscription (re-invite or confirm/decline) givenBefore = userData.modeGiven // Request to re-send invite without changing the access mode if modeGiven == types.ModeNone { modeGiven = userData.modeGiven } else if modeGiven != userData.modeGiven { userData.modeGiven = modeGiven // Save changed value to database if err := store.Subs.Update(t.name, sess.uid, map[string]interface{}{"ModeGiven": modeGiven}); err != nil { return err } t.perUser[target] = userData } } // The user does not want to be bothered, no further action is needed if userData.modeWant.IsBanned() { sess.queueOut(ErrPermissionDenied(set.Id, t.original, now)) return errors.New("topic access denied") } // Handle the following cases: // * a ban of the user, modeGive.IsBanned = true (if user is banned no need to invite anyone) // * regular invite: modeWant = "N", modeGiven > 0 // * access rights update: old modeGiven != new modeGiven if !modeGiven.IsBanned() { if userData.modeWant == types.ModeNone { // (re-)Send the invite to target h.route <- t.makeInvite(target, target, sess.uid, types.InvJoin, userData.modeWant, modeGiven, set.Sub.Info) } else if givenBefore != modeGiven { // Inform target that the access has changed h.route <- t.makeInvite(target, target, sess.uid, types.InvInfo, userData.modeWant, modeGiven, set.Sub.Info) } } // Has anything actually changed? if givenBefore != modeGiven { // inform requester of the change made h.route <- t.makeInvite(sess.uid, target, sess.uid, types.InvInfo, userData.modeWant, modeGiven, map[string]string{"before": givenBefore.String()}) } return nil }
// User subscribed to a new topic. Let all user's other sessions know. // Case 11 func (t *Topic) presTopicSubscribed(user types.Uid, skip *Session) { t.presAnnounceToUser(user, "on", 0, skip) log.Printf("Pres 11: from '%s' (src: %s) [subbed/on]", t.name, user.UserId()) }
func (a *RethinkDbAdapter) UserUpdate(uid t.Uid, update map[string]interface{}) error { _, err := rdb.DB(a.dbName).Table("users").Get(uid.String()).Update(update).RunWrite(a.conn) return err }
// Announce topic disappearance just to the affected user // Case 4.b func (t *Topic) presTopicGone(user types.Uid) { t.presAnnounceToUser(user, "gone", 0, nil) log.Printf("Pres 4.b: from '%s' (src: %s) [gone]", t.name, user.UserId()) }
// Publish announcement to topic // Cases 4.a, 7 func (t *Topic) presPubChange(src types.Uid, what string) { // Announce to topic subscribers. 4.a, 7 t.presAnnounceToTopic(src.UserId(), what, 0, nil) //log.Printf("Pres 4.a,7: from '%s' (src: %s) [%s]", t.name, src, what) }
// TopicsForUser loads user's contact list: p2p and grp topics, except for 'me' subscription. func (a *RethinkDbAdapter) TopicsForUser(uid t.Uid) ([]t.Subscription, error) { // Fetch user's subscriptions // Subscription have Topic.UpdatedAt denormalized into Subscription.UpdatedAt q := rdb.DB(a.dbName).Table("subscriptions").GetAllByIndex("User", uid.String()).Limit(MAX_RESULTS) //log.Printf("RethinkDbAdapter.TopicsForUser q: %+v", q) rows, err := q.Run(a.conn) if err != nil { return nil, err } // Fetch subscriptions. Two queries are needed: users table (me & p2p) and topics table (p2p and grp). // Prepare a list of Separate subscriptions to users vs topics var sub t.Subscription join := make(map[string]t.Subscription) // Keeping these to make a join with table for .private and .access topq := make([]interface{}, 0, 16) usrq := make([]interface{}, 0, 16) for rows.Next(&sub) { tcat := t.GetTopicCat(sub.Topic) // 'me' subscription, skip if tcat == t.TopicCat_Me || tcat == t.TopicCat_Fnd { continue // p2p subscription, find the other user to get user.Public } else if tcat == t.TopicCat_P2P { uid1, uid2, _ := t.ParseP2P(sub.Topic) if uid1 == uid { usrq = append(usrq, uid2.String()) } else { usrq = append(usrq, uid1.String()) } topq = append(topq, sub.Topic) // grp subscription } else { topq = append(topq, sub.Topic) } join[sub.Topic] = sub } //log.Printf("RethinkDbAdapter.TopicsForUser topq, usrq: %+v, %+v", topq, usrq) var subs []t.Subscription if len(topq) > 0 || len(usrq) > 0 { subs = make([]t.Subscription, 0, len(join)) } if len(topq) > 0 { // Fetch grp & p2p topics rows, err = rdb.DB(a.dbName).Table("topics").GetAll(topq...).Run(a.conn) if err != nil { return nil, err } var top t.Topic for rows.Next(&top) { sub = join[top.Id] sub.ObjHeader.MergeTimes(&top.ObjHeader) sub.SetSeqId(top.SeqId) sub.SetHardClearId(top.ClearId) if t.GetTopicCat(sub.Topic) == t.TopicCat_Grp { // all done with a grp topic sub.SetPublic(top.Public) subs = append(subs, sub) } else { // put back the updated value of a p2p subsription, will process further below join[top.Id] = sub } } //log.Printf("RethinkDbAdapter.TopicsForUser 1: %#+v", subs) } // Fetch p2p users and join to p2p tables if len(usrq) > 0 { rows, err = rdb.DB(a.dbName).Table("users").GetAll(usrq...).Run(a.conn) if err != nil { return nil, err } var usr t.User for rows.Next(&usr) { uid2 := t.ParseUid(usr.Id) topic := uid.P2PName(uid2) if sub, ok := join[topic]; ok { sub.ObjHeader.MergeTimes(&usr.ObjHeader) sub.SetWith(uid2.UserId()) sub.SetPublic(usr.Public) sub.SetLastSeenAndUA(usr.LastSeen, usr.UserAgent) subs = append(subs, sub) } } //log.Printf("RethinkDbAdapter.TopicsForUser 2: %#+v", subs) } return subs, nil }
// Update time when the user was last attached to the topic func (a *RethinkDbAdapter) SubsLastSeen(topic string, user t.Uid, lastSeen map[string]time.Time) error { _, err := rdb.DB(a.dbName).Table("subscriptions").Get(topic+":"+user.String()). Update(map[string]interface{}{"LastSeen": lastSeen}, rdb.UpdateOpts{Durability: "soft"}).RunWrite(a.conn) return err }
// SubsDelete deletes a subscription. func (a *RethinkDbAdapter) SubsDelete(topic string, user t.Uid) error { _, err := rdb.DB(a.dbName).Table("subscriptions").Get(topic + ":" + user.String()).Delete().RunWrite(a.conn) return err }