// Must be called in a transaction, holding session and waiting locks. func handleAuthComplete(conn *userConn, idemChanges []store.Change) { // We're no longer waiting for authentication to complete. delete(waiting, conn.waitingAuth.requestId) waitingAuth := conn.waitingAuth conn.waitingAuth = nil // Find the created session entity. newSessionId := uint64(0) for i := range idemChanges { if idemChanges[i].Key == "kind" && idemChanges[i].Value == "session" { newSessionId = idemChanges[i].TargetEntity break } } // Check session exists. session := store.GetEntity(newSessionId) if session == nil || session.Value("kind") != "session" { sendAuthFail(conn, "Session Deleted") return } // Check it is attached to a user. attachedTo := store.AllAttachedTo(newSessionId) if len(attachedTo) == 0 { sendAuthFail(conn, "User Deleted") return } // Check that user has a username. user := store.GetEntity(attachedTo[0]) if user.Value("name username") == "" { sendAuthFail(conn, "Username Already In Use") // TODO: Should request deletion of user, leaning on timeout. return } // This connection is successfully authenticated with that session. conn.session = newSessionId // If there is an existing connection associated with that session, // drop it. if existingConn := sessions[newSessionId]; existingConn != nil { existingConn.conn.Close() } sessions[conn.session] = conn sendAuthSuccess(conn, waitingAuth.password) }
// Can only be called from the handling goroutine for conn, // inside a transaction. func followUser(conn *userConn, followId uint64) { conn.following = append(conn.following, followId) user := store.GetEntity(followId) // Send the user's username. var dataMsg cliproto_down.UserData dataMsg.UserId = new(uint64) dataMsg.Key = new(string) dataMsg.Value = new(string) dataMsg.FirstUnapplied = new(uint64) *dataMsg.UserId = followId *dataMsg.Key = "name username" *dataMsg.Value = user.Value(*dataMsg.Key) *dataMsg.FirstUnapplied = store.InstructionFirstUnapplied() conn.conn.SendProto(6, &dataMsg) // Send information on all the user's sessions. user.Attached(func(key string) { *dataMsg.Key = key *dataMsg.Value = "true" conn.conn.SendProto(6, &dataMsg) }) // Send a done message to indicate that we are finished. var doneMsg cliproto_down.UserDataDone doneMsg.UserId = dataMsg.UserId doneMsg.FirstUnapplied = dataMsg.FirstUnapplied conn.conn.SendProto(7, &doneMsg) }
// Must be called while holding no locks and not in a transaction. func deliver(msg *relay.UserMessage) { // If we have a connection to the recipient session, // deliver the message. sessionsLock.Lock() userConn, exists := sessions[msg.Recipient] if exists { // Try to deliver the message to this connection, // without blocking. // If its delivery channel is full, just drop it. select { case userConn.deliver <- msg: break default: break } } sessionsLock.Unlock() if exists { return } // Otherwise, decrement TTL. If it's 0, discard the message. msg.Ttl-- if msg.Ttl == 0 { return } // Otherwise, look for another node which is connected to this session. store.StartTransaction() session := store.GetEntity(msg.Recipient) if session == nil || session.Value("kind") != "session" { // Unknown session; discard the message. store.EndTransaction() return } nodes := session.AllAttached() store.EndTransaction() if len(nodes) == 0 { // Shouldn't happen, sessions are transient, // should be deleted before reaching here. // Just discard the message. return } // Forward the message to a random one of those nodes. r := rand.Intn(len(nodes)) relay.Forward(uint16(nodes[r]), msg) }
func namelessTimeout(id uint64) { store.StartTransaction() defer store.EndTransaction() // If the timeout has been removed, do nothing. if namelessRemoveTimeouts[id] == nil { return } // Remove nameless user. // TODO: Should make sure we only do this once. user := store.GetEntity(id) if user != nil { chset := make([]store.Change, 1) chset[0].TargetEntity = id chset[0].Key = "id" chset[0].Value = "" req := makeRequest(chset) go chrequest.Request(req) } }
func orphanTimeout(id uint64) { store.StartTransaction() defer store.EndTransaction() // If the timeout has been removed, do nothing. if namelessRemoveTimeouts[id] == nil { return } // Detach ourselves from the session. // TODO: Should make sure we only do this once. ourAttachStr := "attach " + strconv.FormatUint(uint64(config.Id()), 10) session := store.GetEntity(id) if session != nil { chset := make([]store.Change, 1) chset[0].TargetEntity = id chset[0].Key = ourAttachStr chset[0].Value = "" req := makeRequest(chset) go chrequest.Request(req) } }
// Can only be called from the handling goroutine for conn. func handleFollowUser(conn *userConn, content []byte) { var msg cliproto_up.FollowUser if err := proto.Unmarshal(content, &msg); err != nil { conn.conn.Close() return } // Start transaction. store.StartTransaction() defer store.EndTransaction() sessionsLock.Lock() defer sessionsLock.Unlock() // Authentication check. if conn.session == 0 { conn.conn.Close() return } // Check we're not already following this user. // If we are, discard the message. for _, existing := range conn.following { if existing == *msg.UserId { return } } // Check this ID is actually a user entity. otherUser := store.GetEntity(*msg.UserId) if otherUser == nil || otherUser.Value("kind") != "user" { sendFollowUserIdFail(conn, *msg.UserId, "No Such User") return } // Start following this user. followUser(conn, *msg.UserId) }
func handleApplied(slot uint64, idemChanges []store.Change) { relativeSlot := int(slot - store.InstructionStart()) slots := store.InstructionSlots() requestNode := slots[relativeSlot][0].ChangeRequest().RequestNode requestId := slots[relativeSlot][0].ChangeRequest().RequestId sessionsLock.Lock() defer sessionsLock.Unlock() waitingLock.Lock() defer waitingLock.Unlock() // If there is a connection which has this as its waiting request, // have it complete authentication. if requestNode == config.Id() { waitingConn := waiting[requestId] if waitingConn != nil { handleAuthComplete(waitingConn, idemChanges) } } ourAttachStr := "attach " + strconv.FormatUint(uint64(config.Id()), 10) for i := range idemChanges { key := idemChanges[i].Key value := idemChanges[i].Value if strings.HasPrefix(key, "attach ") { // If we were just detached from a session, // drop any connections associated with it, // and if it was in our attach timeouts, remove it. if key == ourAttachStr && value == "" { sessionId := idemChanges[i].TargetEntity if conn := sessions[sessionId]; conn != nil { log.Print("client/listener: " + "detached from existing " + "session") conn.conn.Close() } if orphanAttachTimeouts[sessionId] != nil { orphanAttachTimeouts[sessionId].Stop() delete(orphanAttachTimeouts, sessionId) } } // If we were just attached to a session, // but have no connection associated with that session, // add an attach timeout. // and if it was in our attach timeouts, remove it. if key == ourAttachStr && value != "" { sessionId := idemChanges[i].TargetEntity if sessions[sessionId] == nil { startOrphanTimeout(sessionId) } } // Handle a session attaching/detaching from a user. entityId := idemChanges[i].TargetEntity entity := store.GetEntity(entityId) if entity != nil && entity.Value("kind") == "user" { // Tell following connections when a session // attaches or detaches from a followed user. // TODO: Expensive loop, // should maintain index to avoid this. for conn, _ := range connections { following := false for _, fw := range conn.following { if fw == entityId { following = true break } } if !following { continue } sendUserChange(conn, entityId, key, value) } } } } // If we've created a nameless user, start a timer to delete it. for i := range idemChanges { if idemChanges[i].Key != "kind" || idemChanges[i].Value != "user" { continue } newUserId := idemChanges[i].TargetEntity newUser := store.GetEntity(newUserId) if newUser != nil && newUser.Value("name username") == "" { startNamelessTimeout(newUserId) } } }
func handleDeleting(entityId uint64) { entity := store.GetEntity(entityId) kind := entity.Value("kind") switch kind { // If it's a user deletion, find any sessions attached to the user, // and drop any connections attached to those. // Cancel any follows of that user. // Additionally, remove any corresponding nameless user delete timeout. case "user": connectionsLock.Lock() defer connectionsLock.Unlock() // TODO: Expensive loop, should maintain index to avoid this. for conn, _ := range connections { for i, followed := range conn.following { if followed != entityId { continue } fwing := conn.following fwing = append(fwing[:i], fwing[i+1:]...) conn.following = fwing sendStoppedFollowing(conn, entityId, "User Deleted") } } sessionIds := entity.AllAttached() if len(sessionIds) > 0 { sessionsLock.Lock() defer sessionsLock.Unlock() for _, sessionId := range sessionIds { if conn := sessions[sessionId]; conn != nil { log.Print("client/listener: " + "connected user deleted") conn.conn.Close() } } } if namelessRemoveTimeouts[entityId] != nil { namelessRemoveTimeouts[entityId].Stop() delete(namelessRemoveTimeouts, entityId) } // If it's a session, and we have a connection associated to it, // close that connection and remove it from our map. case "session": sessionsLock.Lock() defer sessionsLock.Unlock() if conn := sessions[entityId]; conn != nil { log.Print("client/listener: connected session deleted") conn.conn.Close() delete(sessions, entityId) } // If we had a timeout waiting to detach ourselves from this // session, cancel it. if orphanAttachTimeouts[entityId] != nil { orphanAttachTimeouts[entityId].Stop() delete(orphanAttachTimeouts, entityId) } } }
// Can only be called from the handling goroutine for conn. func handleAuth(conn *userConn, content []byte) { var msg cliproto_up.Authenticate if err := proto.Unmarshal(content, &msg); err != nil { conn.conn.Close() return } // Try to get information, then release locks and hash the password. // If the situation changes we may have to hash it again anyway, // but scrypt hashing is extremely expensive and we want to try to // do this without holding our locks in the vast majority of cases. var tryPassGenerated bool var trySaltGenerated bool var tryPass, trySalt, tryKey string if *msg.Password != "" { tryPass = *msg.Password store.StartTransaction() userId := store.NameLookup("user", "name username", *msg.Username) if userId != 0 { user := store.GetEntity(userId) if user != nil { trySalt = user.Value("auth salt") } } store.EndTransaction() if trySalt == "" { trySaltGenerated = true var err error trySalt, tryKey, err = genRandomSalt(conn, []byte(tryPass)) if err != nil { return } } else { var err error tryPass, err = genKey(conn, []byte(tryPass), []byte(trySalt)) if err != nil { return } } } else { tryPassGenerated = true trySaltGenerated = true var err error tryPass, trySalt, tryKey, err = genRandomPass(conn) if err != nil { return } } // TODO: Validate username and password against constraints. // We hold this through quite a lot of logic. // Would be good to be locked less. store.StartTransaction() defer store.EndTransaction() sessionsLock.Lock() defer sessionsLock.Unlock() waitingLock.Lock() defer waitingLock.Unlock() if conn.session != 0 || conn.waitingAuth != nil { conn.conn.Close() return } userId := store.NameLookup("user", "name username", *msg.Username) if userId != 0 { if *msg.Password == "" { sendAuthFail(conn, "Invalid Password") return } // The user already exists. user := store.GetEntity(userId) // Try to authenticate them to it. var key string // If their salt and password matches our attempt above, // we can just take that key. salt := user.Value("auth salt") if trySalt == salt && tryPass == *msg.Password { key = tryKey } else { saltBytes := []byte(user.Value("auth salt")) passBytes := []byte(*msg.Password) var err error key, err = genKey(conn, passBytes, saltBytes) if err != nil { return } } if user.Value("auth password") != string(key) { sendAuthFail(conn, "Invalid Password") return } // It's the real user. if *msg.SessionId != 0 { // They are attaching to an existing session. // Check it exists and is attached to this user. strSessionId := strconv.FormatUint(*msg.SessionId, 10) if user.Value("attach "+strSessionId) == "" { sendAuthFail(conn, "Invalid Session") return } // The session does exist. conn.session = *msg.SessionId // If this node is already attached to // the session, drop the other connection. if sessions[conn.session] != nil { sessions[conn.session].conn.Close() } else { // Create change attaching this node ID // to the session. id := config.Id() idStr := strconv.FormatUint(uint64(id), 10) chset := make([]store.Change, 1) chset[0].TargetEntity = conn.session chset[0].Key = "attach " + idStr chset[0].Value = "true" req := makeRequest(chset) go chrequest.Request(req) } // Put us in the sessions map. sessions[conn.session] = conn // Tell the client they authenticated successfully. sendAuthSuccess(conn, "") } else { // They are creating a new session. req := makeNewSessionRequest(userId) go chrequest.Request(req) // Stuff details in waiting auth. conn.waitingAuth = new(authData) conn.waitingAuth.msg = msg conn.waitingAuth.requestId = req.RequestId waiting[conn.waitingAuth.requestId] = conn } } else { // The user does not already exist. // Check they weren't trying to attach to a session. if *msg.SessionId != 0 { sendAuthFail(conn, "User Does Not Exist") return } // We're creating a new user. newUser := *msg.Username newPass := *msg.Password if !strings.HasPrefix(newUser, "Guest-") { // We're creating a new non-guest user. // Make sure they have a password. if newPass == "" { sendAuthFail(conn, "No Password") return } var salt string var hash string if tryPass == newPass && trySaltGenerated { salt = trySalt hash = tryKey } else { passBytes := []byte(newPass) var err error salt, hash, err = genRandomSalt(conn, passBytes) if err != nil { return } } // Create the new user. req := makeNewUserRequest(newUser, hash, salt, false) go chrequest.Request(req) // Stuff details in waiting auth. conn.waitingAuth = new(authData) conn.waitingAuth.msg = msg conn.waitingAuth.requestId = req.RequestId waiting[conn.waitingAuth.requestId] = conn return } // We're creating a new guest user. // Guests get automatic passwords, and can't set them. if newPass != "" { sendAuthFail(conn, "Cannot Set Password For Guest User") return } var hash string var salt string if tryPassGenerated && trySaltGenerated { newPass = tryPass salt = trySalt hash = tryKey } else { var err error newPass, salt, hash, err = genRandomPass(conn) if err != nil { return } } waitingLock.Lock() // Create the new user. req := makeNewUserRequest(newUser, hash, salt, true) go chrequest.Request(req) // Stuff details in waiting auth. conn.waitingAuth = new(authData) conn.waitingAuth.msg = msg conn.waitingAuth.requestId = req.RequestId waiting[conn.waitingAuth.requestId] = conn waitingLock.Unlock() return } }