From 5f2a230ecc5638bbba56dbf4938d84d60660c50d Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Wed, 7 Sep 2016 15:33:44 -0400
Subject: [PATCH] progress: broken: Multi-client support, tests fail, Whois
 factored

---
 client.go |  36 +-------
 host.go   | 269 +++++++++++++++++++++++++++++++-----------------------
 whois.go  |  41 +++++++++
 3 files changed, 200 insertions(+), 146 deletions(-)
 create mode 100644 whois.go

diff --git a/client.go b/client.go
index 5776cac..9f3b9a7 100644
--- a/client.go
+++ b/client.go
@@ -1,45 +1,15 @@
 package sshchat
 
 import (
-	"net"
 	"time"
 
-	humanize "github.com/dustin/go-humanize"
 	"github.com/shazow/ssh-chat/chat/message"
 	"github.com/shazow/ssh-chat/sshd"
 )
 
 type Client struct {
-	sshd.Connection
-	message.User
+	user *message.User
+	conn sshd.Connection
 
-	connected time.Time
-}
-
-// Whois returns a whois description for non-admin users.
-func (client Client) Whois() string {
-	conn, u := client.Connection, client.User
-	fingerprint := "(no public key)"
-	if conn.PublicKey() != nil {
-		fingerprint = sshd.Fingerprint(conn.PublicKey())
-	}
-	return "name: " + u.Name() + message.Newline +
-		" > fingerprint: " + fingerprint + message.Newline +
-		" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
-		" > joined: " + humanize.Time(u.Joined())
-}
-
-// WhoisAdmin returns a whois description for admin users.
-func (client Client) WhoisAdmin() string {
-	conn, u := client.Connection, client.User
-	ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
-	fingerprint := "(no public key)"
-	if conn.PublicKey() != nil {
-		fingerprint = sshd.Fingerprint(conn.PublicKey())
-	}
-	return "name: " + u.Name() + message.Newline +
-		" > ip: " + ip + message.Newline +
-		" > fingerprint: " + fingerprint + message.Newline +
-		" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
-		" > joined: " + humanize.Time(u.Joined())
+	timestamp time.Time
 }
diff --git a/host.go b/host.go
index 55f7bdc..117af55 100644
--- a/host.go
+++ b/host.go
@@ -32,9 +32,10 @@ type Host struct {
 	// Default theme
 	theme message.Theme
 
-	mu    sync.Mutex
-	motd  string
-	count int
+	mu      sync.Mutex
+	motd    string
+	count   int
+	clients map[*message.User][]Client
 }
 
 // NewHost creates a Host on top of an existing listener.
@@ -45,6 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
 		listener: listener,
 		commands: chat.Commands{},
 		auth:     auth,
+		clients:  map[*message.User][]Client{},
 	}
 
 	// Make our own commands registry instance.
@@ -70,26 +72,13 @@ func (h *Host) SetMotd(motd string) {
 	h.mu.Unlock()
 }
 
-func (h *Host) isOp(conn sshd.Connection) bool {
-	key := conn.PublicKey()
-	if key == nil {
-		return false
-	}
-	return h.auth.IsOp(key)
-}
-
 // Connect a specific Terminal to this host and its room.
 func (h *Host) Connect(term *sshd.Terminal) {
 	requestedName := term.Conn.Name()
 	user := message.NewUserScreen(requestedName, term)
-	cfg := user.Config()
-	cfg.Theme = &h.theme
-	user.SetConfig(cfg)
-	go user.Consume()
 
-	// Close term once user is closed.
-	defer user.Close()
-	defer term.Close()
+	client := h.addClient(user, term.Conn)
+	defer h.removeClient(user, client)
 
 	h.mu.Lock()
 	motd := h.motd
@@ -97,6 +86,16 @@ func (h *Host) Connect(term *sshd.Terminal) {
 	h.count++
 	h.mu.Unlock()
 
+	cfg := user.Config()
+	cfg.Theme = &h.theme
+	user.SetConfig(cfg)
+
+	// Close term once user is closed.
+	defer user.Close()
+	defer term.Close()
+
+	go user.Consume()
+
 	// Send MOTD
 	if motd != "" {
 		user.Send(message.NewAnnounceMsg(motd))
@@ -119,11 +118,17 @@ func (h *Host) Connect(term *sshd.Terminal) {
 	user.SetHighlight(user.Name())
 
 	// Should the user be op'd on join?
-	if h.isOp(term.Conn) {
-		h.Room.Ops.Add(set.Keyize(member.ID()))
+	if key := term.Conn.PublicKey(); key != nil {
+		authItem, err := h.auth.ops.Get(newAuthKey(key))
+		if err != nil {
+			err = h.Room.Ops.Add(set.Rename(authItem, member.ID()))
+		}
+	}
+	if err != nil {
+		logger.Warningf("[%s] Failed to op: %s", term.Conn.RemoteAddr(), err)
 	}
-	ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
 
+	ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
 	logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
 
 	for {
@@ -175,6 +180,41 @@ func (h *Host) Connect(term *sshd.Terminal) {
 	logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name())
 }
 
+func (h *Host) addClient(user *message.User, conn sshd.Connection) *Client {
+	client := Client{
+		user:      user,
+		conn:      conn,
+		timestamp: time.Now(),
+	}
+	h.mu.Lock()
+	h.clients[user] = append(h.clients[user], client)
+	h.mu.Unlock()
+	return &client
+}
+
+func (h *Host) removeClient(user *message.User, client *Client) {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	clients := h.clients[user]
+	for i, c := range clients {
+		// Find the user
+		if &c != client {
+			continue
+		}
+		// Delete corresponding client
+		clients[i] = clients[len(clients)-1]
+		clients = clients[:len(clients)-1]
+		break
+	}
+}
+
+func (h *Host) findClients(user *message.User) []Client {
+	h.mu.Lock()
+	defer h.mu.Unlock()
+	return h.clients[user]
+}
+
 // Serve our chat room onto the listener
 func (h *Host) Serve() {
 	h.listener.HandlerFunc = h.Connect
@@ -331,37 +371,35 @@ func (h *Host) InitCommands(c *chat.Commands) {
 		},
 	})
 
-	// XXX: Temporarily disable whois
-	/*
-		c.Add(chat.Command{
-			Prefix:     "/whois",
-			PrefixHelp: "USER",
-			Help:       "Information about USER.",
-			Handler: func(room *chat.Room, msg message.CommandMsg) error {
-				args := msg.Args()
-				if len(args) == 0 {
-					return errors.New("must specify user")
-				}
+	c.Add(chat.Command{
+		Prefix:     "/whois",
+		PrefixHelp: "USER",
+		Help:       "Information about USER.",
+		Handler: func(room *chat.Room, msg message.CommandMsg) error {
+			args := msg.Args()
+			if len(args) == 0 {
+				return errors.New("must specify user")
+			}
 
-				target, ok := h.GetUser(args[0])
-				if !ok {
-					return errors.New("user not found")
-				}
+			target, ok := h.GetUser(args[0])
+			if !ok {
+				return errors.New("user not found")
+			}
 
-				id := target.Identifier.(*identity)
-				var whois string
-				switch room.IsOp(msg.From()) {
-				case true:
-					whois = id.WhoisAdmin()
-				case false:
-					whois = id.Whois()
-				}
-				room.Send(message.NewSystemMsg(whois, msg.From()))
+			// FIXME: Handle many clients
+			clients := h.findClients(target)
+			var whois string
+			switch room.IsOp(msg.From()) {
+			case true:
+				whois = whoisAdmin(clients)
+			case false:
+				whois = whoisPublic(clients)
+			}
+			room.Send(message.NewSystemMsg(whois, msg.From()))
 
-				return nil
-			},
-		})
-	*/
+			return nil
+		},
+	})
 
 	// Hidden commands
 	c.Add(chat.Command{
@@ -381,85 +419,90 @@ func (h *Host) InitCommands(c *chat.Commands) {
 		},
 	})
 
-	// XXX: Temporarily disable op and ban
-	/*
+	c.Add(chat.Command{
+		Op:         true,
+		Prefix:     "/op",
+		PrefixHelp: "USER [DURATION]",
+		Help:       "Set USER as admin.",
+		Handler: func(room *chat.Room, msg message.CommandMsg) error {
+			if !room.IsOp(msg.From()) {
+				return errors.New("must be op")
+			}
 
-		c.Add(chat.Command{
-			Op:         true,
-			Prefix:     "/op",
-			PrefixHelp: "USER [DURATION]",
-			Help:       "Set USER as admin.",
-			Handler: func(room *chat.Room, msg message.CommandMsg) error {
-				if !room.IsOp(msg.From()) {
-					return errors.New("must be op")
-				}
+			args := msg.Args()
+			if len(args) == 0 {
+				return errors.New("must specify user")
+			}
 
-				args := msg.Args()
-				if len(args) == 0 {
-					return errors.New("must specify user")
-				}
+			var until time.Duration = 0
+			if len(args) > 1 {
+				until, _ = time.ParseDuration(args[1])
+			}
 
-				var until time.Duration = 0
-				if len(args) > 1 {
-					until, _ = time.ParseDuration(args[1])
-				}
-
-				user, ok := h.GetUser(args[0])
-				if !ok {
-					return errors.New("user not found")
-				}
+			user, ok := h.GetUser(args[0])
+			if !ok {
+				return errors.New("user not found")
+			}
+			if until != 0 {
+				room.Ops.Add(set.Expire(set.Keyize(user.ID()), until))
+			} else {
 				room.Ops.Add(set.Keyize(user.ID()))
+			}
 
-				h.auth.Op(user.Identifier.(*identity).PublicKey(), until)
+			for _, client := range h.findClients(user) {
+				h.auth.Op(client.conn.PublicKey(), until)
+			}
 
-				body := fmt.Sprintf("Made op by %s.", msg.From().Name())
-				room.Send(message.NewSystemMsg(body, user))
+			body := fmt.Sprintf("Made op by %s.", msg.From().Name())
+			room.Send(message.NewSystemMsg(body, user))
 
-				return nil
-			},
-		})
+			return nil
+		},
+	})
 
-		// Op commands
-		c.Add(chat.Command{
-			Op:         true,
-			Prefix:     "/ban",
-			PrefixHelp: "USER [DURATION]",
-			Help:       "Ban USER from the server.",
-			Handler: func(room *chat.Room, msg message.CommandMsg) error {
-				// TODO: Would be nice to specify what to ban. Key? Ip? etc.
-				if !room.IsOp(msg.From()) {
-					return errors.New("must be op")
-				}
+	// Op commands
+	c.Add(chat.Command{
+		Op:         true,
+		Prefix:     "/ban",
+		PrefixHelp: "USER [DURATION]",
+		Help:       "Ban USER from the server.",
+		Handler: func(room *chat.Room, msg message.CommandMsg) error {
+			// TODO: Would be nice to specify what to ban. Key? Ip? etc.
+			if !room.IsOp(msg.From()) {
+				return errors.New("must be op")
+			}
 
-				args := msg.Args()
-				if len(args) == 0 {
-					return errors.New("must specify user")
-				}
+			args := msg.Args()
+			if len(args) == 0 {
+				return errors.New("must specify user")
+			}
 
-				target, ok := h.GetUser(args[0])
-				if !ok {
-					return errors.New("user not found")
-				}
+			target, ok := h.GetUser(args[0])
+			if !ok {
+				return errors.New("user not found")
+			}
 
-				var until time.Duration = 0
-				if len(args) > 1 {
-					until, _ = time.ParseDuration(args[1])
-				}
+			var until time.Duration = 0
+			if len(args) > 1 {
+				until, _ = time.ParseDuration(args[1])
+			}
 
-				id := target.Identifier.(*identity)
-				h.auth.Ban(id.PublicKey(), until)
-				h.auth.BanAddr(id.RemoteAddr(), until)
+			clients := h.findClients(target)
+			for _, client := range clients {
+				h.auth.Ban(client.conn.PublicKey(), until)
+				h.auth.BanAddr(client.conn.RemoteAddr(), until)
+			}
 
-				body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
-				room.Send(message.NewAnnounceMsg(body))
-				target.Close()
+			body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
+			room.Send(message.NewAnnounceMsg(body))
+			target.Close()
 
-				logger.Debugf("Banned: \n-> %s", id.Whois())
+			logger.Debugf("Banned: \n-> %s", whoisAdmin(clients))
+
+			return nil
+		},
+	})
 
-				return nil
-			},
-		})
-	*/
 	c.Add(chat.Command{
 		Op:         true,
 		Prefix:     "/kick",
diff --git a/whois.go b/whois.go
new file mode 100644
index 0000000..7089667
--- /dev/null
+++ b/whois.go
@@ -0,0 +1,41 @@
+package sshchat
+
+import (
+	"net"
+
+	humanize "github.com/dustin/go-humanize"
+	"github.com/shazow/ssh-chat/chat/message"
+	"github.com/shazow/ssh-chat/sshd"
+)
+
+// Helpers for printing whois messages
+
+func whoisPublic(clients []Client) string {
+	// FIXME: Handle many clients
+	conn, u := clients[0].conn, clients[0].user
+
+	fingerprint := "(no public key)"
+	if conn.PublicKey() != nil {
+		fingerprint = sshd.Fingerprint(conn.PublicKey())
+	}
+	return "name: " + u.Name() + message.Newline +
+		" > fingerprint: " + fingerprint + message.Newline +
+		" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
+		" > joined: " + humanize.Time(u.Joined())
+}
+
+func whoisAdmin(clients []Client) string {
+	// FIXME: Handle many clients
+	conn, u := clients[0].conn, clients[0].user
+
+	ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
+	fingerprint := "(no public key)"
+	if conn.PublicKey() != nil {
+		fingerprint = sshd.Fingerprint(conn.PublicKey())
+	}
+	return "name: " + u.Name() + message.Newline +
+		" > ip: " + ip + message.Newline +
+		" > fingerprint: " + fingerprint + message.Newline +
+		" > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline +
+		" > joined: " + humanize.Time(u.Joined())
+}