progress: broken: Multi-client support, tests fail, Whois factored

This commit is contained in:
Andrey Petrov 2016-09-07 15:33:44 -04:00
parent 810ef13bea
commit 5f2a230ecc
3 changed files with 200 additions and 146 deletions

View File

@ -1,45 +1,15 @@
package sshchat package sshchat
import ( import (
"net"
"time" "time"
humanize "github.com/dustin/go-humanize"
"github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
type Client struct { type Client struct {
sshd.Connection user *message.User
message.User conn sshd.Connection
connected time.Time timestamp 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())
} }

269
host.go
View File

@ -32,9 +32,10 @@ type Host struct {
// Default theme // Default theme
theme message.Theme theme message.Theme
mu sync.Mutex mu sync.Mutex
motd string motd string
count int count int
clients map[*message.User][]Client
} }
// NewHost creates a Host on top of an existing listener. // NewHost creates a Host on top of an existing listener.
@ -45,6 +46,7 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
listener: listener, listener: listener,
commands: chat.Commands{}, commands: chat.Commands{},
auth: auth, auth: auth,
clients: map[*message.User][]Client{},
} }
// Make our own commands registry instance. // Make our own commands registry instance.
@ -70,26 +72,13 @@ func (h *Host) SetMotd(motd string) {
h.mu.Unlock() 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. // Connect a specific Terminal to this host and its room.
func (h *Host) Connect(term *sshd.Terminal) { func (h *Host) Connect(term *sshd.Terminal) {
requestedName := term.Conn.Name() requestedName := term.Conn.Name()
user := message.NewUserScreen(requestedName, term) user := message.NewUserScreen(requestedName, term)
cfg := user.Config()
cfg.Theme = &h.theme
user.SetConfig(cfg)
go user.Consume()
// Close term once user is closed. client := h.addClient(user, term.Conn)
defer user.Close() defer h.removeClient(user, client)
defer term.Close()
h.mu.Lock() h.mu.Lock()
motd := h.motd motd := h.motd
@ -97,6 +86,16 @@ func (h *Host) Connect(term *sshd.Terminal) {
h.count++ h.count++
h.mu.Unlock() 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 // Send MOTD
if motd != "" { if motd != "" {
user.Send(message.NewAnnounceMsg(motd)) user.Send(message.NewAnnounceMsg(motd))
@ -119,11 +118,17 @@ func (h *Host) Connect(term *sshd.Terminal) {
user.SetHighlight(user.Name()) user.SetHighlight(user.Name())
// Should the user be op'd on join? // Should the user be op'd on join?
if h.isOp(term.Conn) { if key := term.Conn.PublicKey(); key != nil {
h.Room.Ops.Add(set.Keyize(member.ID())) 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()) logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
for { for {
@ -175,6 +180,41 @@ func (h *Host) Connect(term *sshd.Terminal) {
logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) 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 // Serve our chat room onto the listener
func (h *Host) Serve() { func (h *Host) Serve() {
h.listener.HandlerFunc = h.Connect 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",
c.Add(chat.Command{ PrefixHelp: "USER",
Prefix: "/whois", Help: "Information about USER.",
PrefixHelp: "USER", Handler: func(room *chat.Room, msg message.CommandMsg) error {
Help: "Information about USER.", args := msg.Args()
Handler: func(room *chat.Room, msg message.CommandMsg) error { if len(args) == 0 {
args := msg.Args() return errors.New("must specify user")
if len(args) == 0 { }
return errors.New("must specify user")
}
target, ok := h.GetUser(args[0]) target, ok := h.GetUser(args[0])
if !ok { if !ok {
return errors.New("user not found") return errors.New("user not found")
} }
id := target.Identifier.(*identity) // FIXME: Handle many clients
var whois string clients := h.findClients(target)
switch room.IsOp(msg.From()) { var whois string
case true: switch room.IsOp(msg.From()) {
whois = id.WhoisAdmin() case true:
case false: whois = whoisAdmin(clients)
whois = id.Whois() case false:
} whois = whoisPublic(clients)
room.Send(message.NewSystemMsg(whois, msg.From())) }
room.Send(message.NewSystemMsg(whois, msg.From()))
return nil return nil
}, },
}) })
*/
// Hidden commands // Hidden commands
c.Add(chat.Command{ 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{ args := msg.Args()
Op: true, if len(args) == 0 {
Prefix: "/op", return errors.New("must specify user")
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() var until time.Duration = 0
if len(args) == 0 { if len(args) > 1 {
return errors.New("must specify user") until, _ = time.ParseDuration(args[1])
} }
var until time.Duration = 0 user, ok := h.GetUser(args[0])
if len(args) > 1 { if !ok {
until, _ = time.ParseDuration(args[1]) return errors.New("user not found")
} }
if until != 0 {
user, ok := h.GetUser(args[0]) room.Ops.Add(set.Expire(set.Keyize(user.ID()), until))
if !ok { } else {
return errors.New("user not found")
}
room.Ops.Add(set.Keyize(user.ID())) 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()) body := fmt.Sprintf("Made op by %s.", msg.From().Name())
room.Send(message.NewSystemMsg(body, user)) room.Send(message.NewSystemMsg(body, user))
return nil return nil
}, },
}) })
// Op commands // Op commands
c.Add(chat.Command{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/ban", Prefix: "/ban",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION]",
Help: "Ban USER from the server.", Help: "Ban USER from the server.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
// TODO: Would be nice to specify what to ban. Key? Ip? etc. // TODO: Would be nice to specify what to ban. Key? Ip? etc.
if !room.IsOp(msg.From()) { if !room.IsOp(msg.From()) {
return errors.New("must be op") return errors.New("must be op")
} }
args := msg.Args() args := msg.Args()
if len(args) == 0 { if len(args) == 0 {
return errors.New("must specify user") return errors.New("must specify user")
} }
target, ok := h.GetUser(args[0]) target, ok := h.GetUser(args[0])
if !ok { if !ok {
return errors.New("user not found") return errors.New("user not found")
} }
var until time.Duration = 0 var until time.Duration = 0
if len(args) > 1 { if len(args) > 1 {
until, _ = time.ParseDuration(args[1]) until, _ = time.ParseDuration(args[1])
} }
id := target.Identifier.(*identity) clients := h.findClients(target)
h.auth.Ban(id.PublicKey(), until) for _, client := range clients {
h.auth.BanAddr(id.RemoteAddr(), until) 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()) body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
room.Send(message.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
target.Close() target.Close()
logger.Debugf("Banned: \n-> %s", id.Whois()) logger.Debugf("Banned: \n-> %s", whoisAdmin(clients))
return nil
},
})
return nil
},
})
*/
c.Add(chat.Command{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/kick", Prefix: "/kick",

41
whois.go Normal file
View File

@ -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())
}