chat.Channel->chat.Room, /ban, /whois, chat.User.Identifier
authorAndrey Petrov <andrey.petrov@shazow.net>
Sat, 17 Jan 2015 05:53:22 +0000 (21:53 -0800)
committerAndrey Petrov <andrey.petrov@shazow.net>
Sat, 17 Jan 2015 05:53:22 +0000 (21:53 -0800)
- Renamed chat.Channel -> chat.Room
- /ban works, supports IP also
- /whois works
- chat.User now accepts an Identifier interface rather than name
- Tweaked rate limiting

16 files changed:
auth.go
chat/channel.go [deleted file]
chat/command.go
chat/doc.go
chat/message.go
chat/room.go [new file with mode: 0644]
chat/room_test.go [moved from chat/channel_test.go with 90% similarity]
chat/set.go
chat/theme.go
chat/user.go
host.go
host_test.go
identity.go [new file with mode: 0644]
sshd/auth.go
sshd/net.go
sshd/terminal.go

diff --git a/auth.go b/auth.go
index d218366c4a3c32b3303a9478b7a9675c6cc21038..bce92bf092bfe8cb5f8166b396bd8a785c2fba7b 100644 (file)
--- a/auth.go
+++ b/auth.go
@@ -2,9 +2,9 @@ package main
 
 import (
        "errors"
+       "net"
        "sync"
 
-       "github.com/shazow/ssh-chat/sshd"
        "golang.org/x/crypto/ssh"
 )
 
@@ -17,27 +17,38 @@ var ErrBanned = errors.New("banned")
 // AuthKey is the type that our lookups are keyed against.
 type AuthKey string
 
-// NewAuthKey returns an AuthKey from an ssh.PublicKey.
-func NewAuthKey(key ssh.PublicKey) AuthKey {
+// NewAuthKey returns string from an ssh.PublicKey.
+func NewAuthKey(key ssh.PublicKey) string {
+       if key == nil {
+               return ""
+       }
        // FIXME: Is there a way to index pubkeys without marshal'ing them into strings?
-       return AuthKey(string(key.Marshal()))
+       return string(key.Marshal())
+}
+
+// NewAuthAddr returns a string from a net.Addr
+func NewAuthAddr(addr net.Addr) string {
+       host, _, _ := net.SplitHostPort(addr.String())
+       return host
 }
 
 // Auth stores fingerprint lookups
+// TODO: Add timed auth by using a time.Time instead of struct{} for values.
 type Auth struct {
-       sshd.Auth
        sync.RWMutex
-       whitelist map[AuthKey]struct{}
-       banned    map[AuthKey]struct{}
-       ops       map[AuthKey]struct{}
+       bannedAddr map[string]struct{}
+       banned     map[string]struct{}
+       whitelist  map[string]struct{}
+       ops        map[string]struct{}
 }
 
 // NewAuth creates a new default Auth.
 func NewAuth() *Auth {
        return &Auth{
-               whitelist: make(map[AuthKey]struct{}),
-               banned:    make(map[AuthKey]struct{}),
-               ops:       make(map[AuthKey]struct{}),
+               bannedAddr: make(map[string]struct{}),
+               banned:     make(map[string]struct{}),
+               whitelist:  make(map[string]struct{}),
+               ops:        make(map[string]struct{}),
        }
 }
 
@@ -50,7 +61,7 @@ func (a Auth) AllowAnonymous() bool {
 }
 
 // Check determines if a pubkey fingerprint is permitted.
-func (a Auth) Check(key ssh.PublicKey) (bool, error) {
+func (a Auth) Check(addr net.Addr, key ssh.PublicKey) (bool, error) {
        authkey := NewAuthKey(key)
 
        a.RLock()
@@ -63,9 +74,13 @@ func (a Auth) Check(key ssh.PublicKey) (bool, error) {
                if !whitelisted {
                        return false, ErrNotWhitelisted
                }
+               return true, nil
        }
 
        _, banned := a.banned[authkey]
+       if !banned {
+               _, banned = a.bannedAddr[NewAuthAddr(addr)]
+       }
        if banned {
                return false, ErrBanned
        }
@@ -75,6 +90,10 @@ func (a Auth) Check(key ssh.PublicKey) (bool, error) {
 
 // Op will set a fingerprint as a known operator.
 func (a *Auth) Op(key ssh.PublicKey) {
+       if key == nil {
+               // Don't process empty keys.
+               return
+       }
        authkey := NewAuthKey(key)
        a.Lock()
        a.ops[authkey] = struct{}{}
@@ -83,6 +102,9 @@ func (a *Auth) Op(key ssh.PublicKey) {
 
 // IsOp checks if a public key is an op.
 func (a Auth) IsOp(key ssh.PublicKey) bool {
+       if key == nil {
+               return false
+       }
        authkey := NewAuthKey(key)
        a.RLock()
        _, ok := a.ops[authkey]
@@ -92,34 +114,34 @@ func (a Auth) IsOp(key ssh.PublicKey) bool {
 
 // Whitelist will set a public key as a whitelisted user.
 func (a *Auth) Whitelist(key ssh.PublicKey) {
+       if key == nil {
+               // Don't process empty keys.
+               return
+       }
        authkey := NewAuthKey(key)
        a.Lock()
        a.whitelist[authkey] = struct{}{}
        a.Unlock()
 }
 
-// IsWhitelisted checks if a public key is whitelisted.
-func (a Auth) IsWhitelisted(key ssh.PublicKey) bool {
-       authkey := NewAuthKey(key)
-       a.RLock()
-       _, ok := a.whitelist[authkey]
-       a.RUnlock()
-       return ok
-}
-
-// Ban will set a fingerprint as banned.
+// Ban will set a public key as banned.
 func (a *Auth) Ban(key ssh.PublicKey) {
+       if key == nil {
+               // Don't process empty keys.
+               return
+       }
        authkey := NewAuthKey(key)
+
        a.Lock()
        a.banned[authkey] = struct{}{}
        a.Unlock()
 }
 
-// IsBanned will set a fingerprint as banned.
-func (a Auth) IsBanned(key ssh.PublicKey) bool {
-       authkey := NewAuthKey(key)
-       a.RLock()
-       _, ok := a.whitelist[authkey]
-       a.RUnlock()
-       return ok
+// Ban will set an IP address as banned.
+func (a *Auth) BanAddr(addr net.Addr) {
+       key := NewAuthAddr(addr)
+
+       a.Lock()
+       a.bannedAddr[key] = struct{}{}
+       a.Unlock()
 }
diff --git a/chat/channel.go b/chat/channel.go
deleted file mode 100644 (file)
index a5fd0c3..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-package chat
-
-import (
-       "errors"
-       "fmt"
-       "sync"
-)
-
-const historyLen = 20
-const channelBuffer = 10
-
-// The error returned when a message is sent to a channel that is already
-// closed.
-var ErrChannelClosed = errors.New("channel closed")
-
-// Member is a User with per-Channel metadata attached to it.
-type Member struct {
-       *User
-       Op bool
-}
-
-// Channel definition, also a Set of User Items
-type Channel struct {
-       topic     string
-       history   *History
-       members   *Set
-       broadcast chan Message
-       commands  Commands
-       closed    bool
-       closeOnce sync.Once
-}
-
-// NewChannel creates a new channel.
-func NewChannel() *Channel {
-       broadcast := make(chan Message, channelBuffer)
-
-       return &Channel{
-               broadcast: broadcast,
-               history:   NewHistory(historyLen),
-               members:   NewSet(),
-               commands:  *defaultCommands,
-       }
-}
-
-// SetCommands sets the channel's command handlers.
-func (ch *Channel) SetCommands(commands Commands) {
-       ch.commands = commands
-}
-
-// Close the channel and all the users it contains.
-func (ch *Channel) Close() {
-       ch.closeOnce.Do(func() {
-               ch.closed = true
-               ch.members.Each(func(m Item) {
-                       m.(*Member).Close()
-               })
-               ch.members.Clear()
-               close(ch.broadcast)
-       })
-}
-
-// HandleMsg reacts to a message, will block until done.
-func (ch *Channel) HandleMsg(m Message) {
-       switch m := m.(type) {
-       case *CommandMsg:
-               cmd := *m
-               err := ch.commands.Run(ch, cmd)
-               if err != nil {
-                       m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
-                       go ch.HandleMsg(m)
-               }
-       case MessageTo:
-               user := m.To()
-               user.Send(m)
-       default:
-               fromMsg, skip := m.(MessageFrom)
-               var skipUser *User
-               if skip {
-                       skipUser = fromMsg.From()
-               }
-
-               ch.members.Each(func(u Item) {
-                       user := u.(*Member).User
-                       if skip && skipUser == user {
-                               // Skip
-                               return
-                       }
-                       if _, ok := m.(*AnnounceMsg); ok {
-                               if user.Config.Quiet {
-                                       // Skip
-                                       return
-                               }
-                       }
-                       err := user.Send(m)
-                       if err != nil {
-                               user.Close()
-                       }
-               })
-       }
-}
-
-// Serve will consume the broadcast channel and handle the messages, should be
-// run in a goroutine.
-func (ch *Channel) Serve() {
-       for m := range ch.broadcast {
-               go ch.HandleMsg(m)
-       }
-}
-
-// Send message, buffered by a chan.
-func (ch *Channel) Send(m Message) {
-       ch.broadcast <- m
-}
-
-// Join the channel as a user, will announce.
-func (ch *Channel) Join(u *User) (*Member, error) {
-       if ch.closed {
-               return nil, ErrChannelClosed
-       }
-       member := Member{u, false}
-       err := ch.members.Add(&member)
-       if err != nil {
-               return nil, err
-       }
-       s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.members.Len())
-       ch.Send(NewAnnounceMsg(s))
-       return &member, nil
-}
-
-// Leave the channel as a user, will announce. Mostly used during setup.
-func (ch *Channel) Leave(u *User) error {
-       err := ch.members.Remove(u)
-       if err != nil {
-               return err
-       }
-       s := fmt.Sprintf("%s left.", u.Name())
-       ch.Send(NewAnnounceMsg(s))
-       return nil
-}
-
-// Member returns a corresponding Member object to a User if the Member is
-// present in this channel.
-func (ch *Channel) Member(u *User) (*Member, bool) {
-       m, ok := ch.MemberById(u.Id())
-       if !ok {
-               return nil, false
-       }
-       // Check that it's the same user
-       if m.User != u {
-               return nil, false
-       }
-       return m, true
-}
-
-func (ch *Channel) MemberById(id Id) (*Member, bool) {
-       m, err := ch.members.Get(id)
-       if err != nil {
-               return nil, false
-       }
-       return m.(*Member), true
-}
-
-// IsOp returns whether a user is an operator in this channel.
-func (ch *Channel) IsOp(u *User) bool {
-       m, ok := ch.Member(u)
-       return ok && m.Op
-}
-
-// Topic of the channel.
-func (ch *Channel) Topic() string {
-       return ch.topic
-}
-
-// SetTopic will set the topic of the channel.
-func (ch *Channel) SetTopic(s string) {
-       ch.topic = s
-}
-
-// NamesPrefix lists all members' names with a given prefix, used to query
-// for autocompletion purposes.
-func (ch *Channel) NamesPrefix(prefix string) []string {
-       members := ch.members.ListPrefix(prefix)
-       names := make([]string, len(members))
-       for i, u := range members {
-               names[i] = u.(*Member).User.Name()
-       }
-       return names
-}
index 351d090360237352368ef6516a183833096fe1a5..aa6d3f5ab7c5e9483fd49df58c1b8df1cc9c35e1 100644 (file)
@@ -29,7 +29,7 @@ type Command struct {
        PrefixHelp string
        // If omitted, command is hidden from /help
        Help    string
-       Handler func(*Channel, CommandMsg) error
+       Handler func(*Room, CommandMsg) error
        // Command requires Op permissions
        Op bool
 }
@@ -59,7 +59,7 @@ func (c Commands) Alias(command string, alias string) error {
 }
 
 // Run executes a command message.
-func (c Commands) Run(channel *Channel, msg CommandMsg) error {
+func (c Commands) Run(room *Room, msg CommandMsg) error {
        if msg.From == nil {
                return ErrNoOwner
        }
@@ -69,7 +69,7 @@ func (c Commands) Run(channel *Channel, msg CommandMsg) error {
                return ErrInvalidCommand
        }
 
-       return cmd.Handler(channel, msg)
+       return cmd.Handler(room, msg)
 }
 
 // Help will return collated help text as one string.
@@ -102,16 +102,16 @@ func init() {
 func InitCommands(c *Commands) {
        c.Add(Command{
                Prefix: "/help",
-               Handler: func(channel *Channel, msg CommandMsg) error {
-                       op := channel.IsOp(msg.From())
-                       channel.Send(NewSystemMsg(channel.commands.Help(op), msg.From()))
+               Handler: func(room *Room, msg CommandMsg) error {
+                       op := room.IsOp(msg.From())
+                       room.Send(NewSystemMsg(room.commands.Help(op), msg.From()))
                        return nil
                },
        })
 
        c.Add(Command{
                Prefix: "/me",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        me := strings.TrimLeft(msg.body, "/me")
                        if me == "" {
                                me = " is at a loss for words."
@@ -119,7 +119,7 @@ func InitCommands(c *Commands) {
                                me = me[1:]
                        }
 
-                       channel.Send(NewEmoteMsg(me, msg.From()))
+                       room.Send(NewEmoteMsg(me, msg.From()))
                        return nil
                },
        })
@@ -127,7 +127,7 @@ func InitCommands(c *Commands) {
        c.Add(Command{
                Prefix: "/exit",
                Help:   "Exit the chat.",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        msg.From().Close()
                        return nil
                },
@@ -138,17 +138,20 @@ func InitCommands(c *Commands) {
                Prefix:     "/nick",
                PrefixHelp: "NAME",
                Help:       "Rename yourself.",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        args := msg.Args()
                        if len(args) != 1 {
                                return ErrMissingArg
                        }
                        u := msg.From()
-                       oldName := u.Name()
-                       u.SetName(args[0])
+                       oldId := u.Id()
+                       u.SetId(Id(args[0]))
 
-                       body := fmt.Sprintf("%s is now known as %s.", oldName, u.Name())
-                       channel.Send(NewAnnounceMsg(body))
+                       err := room.Rename(oldId, u)
+                       if err != nil {
+                               u.SetId(oldId)
+                               return err
+                       }
                        return nil
                },
        })
@@ -156,11 +159,11 @@ func InitCommands(c *Commands) {
        c.Add(Command{
                Prefix: "/names",
                Help:   "List users who are connected.",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        // TODO: colorize
-                       names := channel.NamesPrefix("")
+                       names := room.NamesPrefix("")
                        body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", "))
-                       channel.Send(NewSystemMsg(body, msg.From()))
+                       room.Send(NewSystemMsg(body, msg.From()))
                        return nil
                },
        })
@@ -170,7 +173,7 @@ func InitCommands(c *Commands) {
                Prefix:     "/theme",
                PrefixHelp: "[mono|colors]",
                Help:       "Set your color theme.",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        user := msg.From()
                        args := msg.Args()
                        if len(args) == 0 {
@@ -179,7 +182,7 @@ func InitCommands(c *Commands) {
                                        theme = user.Config.Theme.Id()
                                }
                                body := fmt.Sprintf("Current theme: %s", theme)
-                               channel.Send(NewSystemMsg(body, user))
+                               room.Send(NewSystemMsg(body, user))
                                return nil
                        }
 
@@ -188,7 +191,7 @@ func InitCommands(c *Commands) {
                                if t.Id() == id {
                                        user.Config.Theme = &t
                                        body := fmt.Sprintf("Set theme: %s", id)
-                                       channel.Send(NewSystemMsg(body, user))
+                                       room.Send(NewSystemMsg(body, user))
                                        return nil
                                }
                        }
@@ -199,7 +202,7 @@ func InitCommands(c *Commands) {
        c.Add(Command{
                Prefix: "/quiet",
                Help:   "Silence announcement-type messages (join, part, rename, etc).",
-               Handler: func(channel *Channel, msg CommandMsg) error {
+               Handler: func(room *Room, msg CommandMsg) error {
                        u := msg.From()
                        u.ToggleQuietMode()
 
@@ -209,7 +212,7 @@ func InitCommands(c *Commands) {
                        } else {
                                body = "Quiet mode is toggled OFF"
                        }
-                       channel.Send(NewSystemMsg(body, u))
+                       room.Send(NewSystemMsg(body, u))
                        return nil
                },
        })
@@ -219,8 +222,8 @@ func InitCommands(c *Commands) {
                Prefix:     "/op",
                PrefixHelp: "USER",
                Help:       "Mark user as admin.",
-               Handler: func(channel *Channel, msg CommandMsg) error {
-                       if !channel.IsOp(msg.From()) {
+               Handler: func(room *Room, msg CommandMsg) error {
+                       if !room.IsOp(msg.From()) {
                                return errors.New("must be op")
                        }
 
@@ -231,7 +234,7 @@ func InitCommands(c *Commands) {
 
                        // TODO: Add support for fingerprint-based op'ing. This will
                        // probably need to live in host land.
-                       member, ok := channel.MemberById(Id(args[0]))
+                       member, ok := room.MemberById(Id(args[0]))
                        if !ok {
                                return errors.New("user not found")
                        }
index 7c80e020d543f5ab0033f3a3b9d1dcaa67820c25..22760e7c83d009ea38c5f4667c4b6cb1e23701ae 100644 (file)
@@ -4,7 +4,7 @@ with the intention of using with the intention of using as the backend for
 ssh-chat.
 
 This package should not know anything about sockets. It should expose io-style
-interfaces and channels for communicating with any method of transnport.
+interfaces and rooms for communicating with any method of transnport.
 
 TODO: Add usage examples here.
 
index 955bc2317057f50f167806030c441aa841f3dde3..2a804a9b458f72adc91480246a1385345184cb4e 100644 (file)
@@ -55,7 +55,7 @@ func (m *Msg) Command() string {
        return ""
 }
 
-// PublicMsg is any message from a user sent to the channel.
+// PublicMsg is any message from a user sent to the room.
 type PublicMsg struct {
        Msg
        from *User
@@ -105,7 +105,7 @@ func (m *PublicMsg) String() string {
        return fmt.Sprintf("%s: %s", m.from.Name(), m.body)
 }
 
-// EmoteMsg is a /me message sent to the channel. It specifically does not
+// EmoteMsg is a /me message sent to the room. It specifically does not
 // extend PublicMsg because it doesn't implement MessageFrom to allow the
 // sender to see the emote.
 type EmoteMsg struct {
@@ -212,7 +212,7 @@ type CommandMsg struct {
        *PublicMsg
        command string
        args    []string
-       channel *Channel
+       room *Room
 }
 
 func (m *CommandMsg) Command() string {
diff --git a/chat/room.go b/chat/room.go
new file mode 100644 (file)
index 0000000..896f92f
--- /dev/null
@@ -0,0 +1,200 @@
+package chat
+
+import (
+       "errors"
+       "fmt"
+       "sync"
+)
+
+const historyLen = 20
+const roomBuffer = 10
+
+// The error returned when a message is sent to a room that is already
+// closed.
+var ErrRoomClosed = errors.New("room closed")
+
+// Member is a User with per-Room metadata attached to it.
+type Member struct {
+       *User
+       Op bool
+}
+
+// Room definition, also a Set of User Items
+type Room struct {
+       topic     string
+       history   *History
+       members   *Set
+       broadcast chan Message
+       commands  Commands
+       closed    bool
+       closeOnce sync.Once
+}
+
+// NewRoom creates a new room.
+func NewRoom() *Room {
+       broadcast := make(chan Message, roomBuffer)
+
+       return &Room{
+               broadcast: broadcast,
+               history:   NewHistory(historyLen),
+               members:   NewSet(),
+               commands:  *defaultCommands,
+       }
+}
+
+// SetCommands sets the room's command handlers.
+func (r *Room) SetCommands(commands Commands) {
+       r.commands = commands
+}
+
+// Close the room and all the users it contains.
+func (r *Room) Close() {
+       r.closeOnce.Do(func() {
+               r.closed = true
+               r.members.Each(func(m Identifier) {
+                       m.(*Member).Close()
+               })
+               r.members.Clear()
+               close(r.broadcast)
+       })
+}
+
+// HandleMsg reacts to a message, will block until done.
+func (r *Room) HandleMsg(m Message) {
+       switch m := m.(type) {
+       case *CommandMsg:
+               cmd := *m
+               err := r.commands.Run(r, cmd)
+               if err != nil {
+                       m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
+                       go r.HandleMsg(m)
+               }
+       case MessageTo:
+               user := m.To()
+               user.Send(m)
+       default:
+               fromMsg, skip := m.(MessageFrom)
+               var skipUser *User
+               if skip {
+                       skipUser = fromMsg.From()
+               }
+
+               r.members.Each(func(u Identifier) {
+                       user := u.(*Member).User
+                       if skip && skipUser == user {
+                               // Skip
+                               return
+                       }
+                       if _, ok := m.(*AnnounceMsg); ok {
+                               if user.Config.Quiet {
+                                       // Skip
+                                       return
+                               }
+                       }
+                       err := user.Send(m)
+                       if err != nil {
+                               user.Close()
+                       }
+               })
+       }
+}
+
+// Serve will consume the broadcast room and handle the messages, should be
+// run in a goroutine.
+func (r *Room) Serve() {
+       for m := range r.broadcast {
+               go r.HandleMsg(m)
+       }
+}
+
+// Send message, buffered by a chan.
+func (r *Room) Send(m Message) {
+       r.broadcast <- m
+}
+
+// Join the room as a user, will announce.
+func (r *Room) Join(u *User) (*Member, error) {
+       if r.closed {
+               return nil, ErrRoomClosed
+       }
+       member := Member{u, false}
+       err := r.members.Add(&member)
+       if err != nil {
+               return nil, err
+       }
+       s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.members.Len())
+       r.Send(NewAnnounceMsg(s))
+       return &member, nil
+}
+
+// Leave the room as a user, will announce. Mostly used during setup.
+func (r *Room) Leave(u *User) error {
+       err := r.members.Remove(u)
+       if err != nil {
+               return err
+       }
+       s := fmt.Sprintf("%s left.", u.Name())
+       r.Send(NewAnnounceMsg(s))
+       return nil
+}
+
+// Rename member with a new identity. This will not call rename on the member.
+func (r *Room) Rename(oldId Id, identity Identifier) error {
+       err := r.members.Replace(oldId, identity)
+       if err != nil {
+               return err
+       }
+
+       s := fmt.Sprintf("%s is now known as %s.", oldId, identity.Id())
+       r.Send(NewAnnounceMsg(s))
+       return nil
+}
+
+// Member returns a corresponding Member object to a User if the Member is
+// present in this room.
+func (r *Room) Member(u *User) (*Member, bool) {
+       m, ok := r.MemberById(u.Id())
+       if !ok {
+               return nil, false
+       }
+       // Check that it's the same user
+       if m.User != u {
+               return nil, false
+       }
+       return m, true
+}
+
+func (r *Room) MemberById(id Id) (*Member, bool) {
+       m, err := r.members.Get(id)
+       if err != nil {
+               return nil, false
+       }
+       return m.(*Member), true
+}
+
+// IsOp returns whether a user is an operator in this room.
+func (r *Room) IsOp(u *User) bool {
+       m, ok := r.Member(u)
+       return ok && m.Op
+}
+
+// Topic of the room.
+func (r *Room) Topic() string {
+       return r.topic
+}
+
+// SetTopic will set the topic of the room.
+func (r *Room) SetTopic(s string) {
+       r.topic = s
+}
+
+// NamesPrefix lists all members' names with a given prefix, used to query
+// for autocompletion purposes.
+func (r *Room) NamesPrefix(prefix string) []string {
+       members := r.members.ListPrefix(prefix)
+       names := make([]string, len(members))
+       for i, u := range members {
+               names[i] = u.(*Member).User.Name()
+       }
+       return names
+}
similarity index 90%
rename from chat/channel_test.go
rename to chat/room_test.go
index 13c9bdcbd2b79b6b21daf35fb13162f34901cf5f..4f39dd727806af242fb2ac208d78911eba37f4cc 100644 (file)
@@ -5,8 +5,8 @@ import (
        "testing"
 )
 
-func TestChannelServe(t *testing.T) {
-       ch := NewChannel()
+func TestRoomServe(t *testing.T) {
+       ch := NewRoom()
        ch.Send(NewAnnounceMsg("hello"))
 
        received := <-ch.broadcast
@@ -18,13 +18,13 @@ func TestChannelServe(t *testing.T) {
        }
 }
 
-func TestChannelJoin(t *testing.T) {
+func TestRoomJoin(t *testing.T) {
        var expected, actual []byte
 
        s := &MockScreen{}
        u := NewUser("foo")
 
-       ch := NewChannel()
+       ch := NewRoom()
        go ch.Serve()
        defer ch.Close()
 
@@ -57,13 +57,13 @@ func TestChannelJoin(t *testing.T) {
        }
 }
 
-func TestChannelDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
+func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
        u := NewUser("foo")
        u.Config = UserConfig{
                Quiet: true,
        }
 
-       ch := NewChannel()
+       ch := NewRoom()
        defer ch.Close()
 
        _, err := ch.Join(u)
@@ -92,13 +92,13 @@ func TestChannelDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
        ch.HandleMsg(NewPublicMsg("hello", u))
 }
 
-func TestChannelQuietToggleBroadcasts(t *testing.T) {
+func TestRoomQuietToggleBroadcasts(t *testing.T) {
        u := NewUser("foo")
        u.Config = UserConfig{
                Quiet: true,
        }
 
-       ch := NewChannel()
+       ch := NewRoom()
        defer ch.Close()
 
        _, err := ch.Join(u)
@@ -134,7 +134,7 @@ func TestQuietToggleDisplayState(t *testing.T) {
        s := &MockScreen{}
        u := NewUser("foo")
 
-       ch := NewChannel()
+       ch := NewRoom()
        go ch.Serve()
        defer ch.Close()
 
@@ -164,13 +164,13 @@ func TestQuietToggleDisplayState(t *testing.T) {
        }
 }
 
-func TestChannelNames(t *testing.T) {
+func TestRoomNames(t *testing.T) {
        var expected, actual []byte
 
        s := &MockScreen{}
        u := NewUser("foo")
 
-       ch := NewChannel()
+       ch := NewRoom()
        go ch.Serve()
        defer ch.Close()
 
index 2248de8dff4d0e492360065b23f44bade0bebffa..d032a34d5aa57c34d1b237b07bfac7c2f3304b61 100644 (file)
@@ -12,25 +12,17 @@ var ErrIdTaken = errors.New("id already taken")
 // The error returned when a requested item does not exist in the set.
 var ErrItemMissing = errors.New("item does not exist")
 
-// Id is a unique identifier for an item.
-type Id string
-
-// Item is an interface for items to store-able in the set
-type Item interface {
-       Id() Id
-}
-
 // Set with string lookup.
 // TODO: Add trie for efficient prefix lookup?
 type Set struct {
-       lookup map[Id]Item
+       lookup map[Id]Identifier
        sync.RWMutex
 }
 
 // NewSet creates a new set.
 func NewSet() *Set {
        return &Set{
-               lookup: map[Id]Item{},
+               lookup: map[Id]Identifier{},
        }
 }
 
@@ -38,7 +30,7 @@ func NewSet() *Set {
 func (s *Set) Clear() int {
        s.Lock()
        n := len(s.lookup)
-       s.lookup = map[Id]Item{}
+       s.lookup = map[Id]Identifier{}
        s.Unlock()
        return n
 }
@@ -49,7 +41,7 @@ func (s *Set) Len() int {
 }
 
 // In checks if an item exists in this set.
-func (s *Set) In(item Item) bool {
+func (s *Set) In(item Identifier) bool {
        s.RLock()
        _, ok := s.lookup[item.Id()]
        s.RUnlock()
@@ -57,7 +49,7 @@ func (s *Set) In(item Item) bool {
 }
 
 // Get returns an item with the given Id.
-func (s *Set) Get(id Id) (Item, error) {
+func (s *Set) Get(id Id) (Identifier, error) {
        s.RLock()
        item, ok := s.lookup[id]
        s.RUnlock()
@@ -70,7 +62,7 @@ func (s *Set) Get(id Id) (Item, error) {
 }
 
 // Add item to this set if it does not exist already.
-func (s *Set) Add(item Item) error {
+func (s *Set) Add(item Identifier) error {
        s.Lock()
        defer s.Unlock()
 
@@ -84,7 +76,7 @@ func (s *Set) Add(item Item) error {
 }
 
 // Remove item from this set.
-func (s *Set) Remove(item Item) error {
+func (s *Set) Remove(item Identifier) error {
        s.Lock()
        defer s.Unlock()
        id := item.Id()
@@ -96,9 +88,34 @@ func (s *Set) Remove(item Item) error {
        return nil
 }
 
+// Replace item from old Id with new Identifier.
+// Used for moving the same identifier to a new Id, such as a rename.
+func (s *Set) Replace(oldId Id, item Identifier) error {
+       s.Lock()
+       defer s.Unlock()
+
+       // Check if it already exists
+       _, found := s.lookup[item.Id()]
+       if found {
+               return ErrIdTaken
+       }
+
+       // Remove oldId
+       _, found = s.lookup[oldId]
+       if !found {
+               return ErrItemMissing
+       }
+       delete(s.lookup, oldId)
+
+       // Add new identifier
+       s.lookup[item.Id()] = item
+
+       return nil
+}
+
 // Each loops over every item while holding a read lock and applies fn to each
 // element.
-func (s *Set) Each(fn func(item Item)) {
+func (s *Set) Each(fn func(item Identifier)) {
        s.RLock()
        for _, item := range s.lookup {
                fn(item)
@@ -107,8 +124,8 @@ func (s *Set) Each(fn func(item Item)) {
 }
 
 // ListPrefix returns a list of items with a prefix, case insensitive.
-func (s *Set) ListPrefix(prefix string) []Item {
-       r := []Item{}
+func (s *Set) ListPrefix(prefix string) []Identifier {
+       r := []Identifier{}
        prefix = strings.ToLower(prefix)
 
        s.RLock()
index 9ada33f79a4b32dc74056fc56b2ca0e87e0060cc..dbf9e82c918032422a11dbb06959b09e52d0e716 100644 (file)
@@ -98,10 +98,10 @@ func (t Theme) Id() string {
 // Colorize name string given some index
 func (t Theme) ColorName(u *User) string {
        if t.names == nil {
-               return u.name
+               return u.Name()
        }
 
-       return t.names.Get(u.colorIdx).Format(u.name)
+       return t.names.Get(u.colorIdx).Format(u.Name())
 }
 
 // Colorize the PM string
index 75ea3307fbcb18dc1c293abcc45d1a1d2eae29a1..b7ab4a88b50802c1d97d13fe4f1b1b2b7f1808ff 100644 (file)
@@ -12,10 +12,20 @@ const messageBuffer = 20
 
 var ErrUserClosed = errors.New("user closed")
 
+// Id is a unique immutable identifier for a user.
+type Id string
+
+// Identifier is an interface that can uniquely identify itself.
+type Identifier interface {
+       Id() Id
+       SetId(Id)
+       Name() string
+}
+
 // User definition, implemented set Item interface and io.Writer
 type User struct {
+       Identifier
        Config    UserConfig
-       name      string
        colorIdx  int
        joined    time.Time
        msg       chan Message
@@ -25,38 +35,29 @@ type User struct {
        closeOnce sync.Once
 }
 
-func NewUser(name string) *User {
+func NewUser(identity Identifier) *User {
        u := User{
-               Config: *DefaultUserConfig,
-               joined: time.Now(),
-               msg:    make(chan Message, messageBuffer),
-               done:   make(chan struct{}, 1),
+               Identifier: identity,
+               Config:     *DefaultUserConfig,
+               joined:     time.Now(),
+               msg:        make(chan Message, messageBuffer),
+               done:       make(chan struct{}, 1),
        }
-       u.SetName(name)
+       u.SetColorIdx(rand.Int())
 
        return &u
 }
 
-func NewUserScreen(name string, screen io.Writer) *User {
-       u := NewUser(name)
+func NewUserScreen(identity Identifier, screen io.Writer) *User {
+       u := NewUser(identity)
        go u.Consume(screen)
 
        return u
 }
 
-// Id of the user, a unique identifier within a set
-func (u *User) Id() Id {
-       return Id(u.name)
-}
-
-// Name of the user
-func (u *User) Name() string {
-       return u.name
-}
-
-// SetName will change the name of the user and reset the colorIdx
-func (u *User) SetName(name string) {
-       u.name = name
+// Rename the user with a new Identifier.
+func (u *User) SetId(id Id) {
+       u.Identifier.SetId(id)
        u.SetColorIdx(rand.Int())
 }
 
diff --git a/host.go b/host.go
index 936e8d77afc0807f7d6ac1a2883c6ba4ac7ade13..5747310f83b17c732fbe667bd98998952a4b4fdc 100644 (file)
--- a/host.go
+++ b/host.go
@@ -20,10 +20,10 @@ func GetPrompt(user *chat.User) string {
 }
 
 // Host is the bridge between sshd and chat modules
-// TODO: Should be easy to add support for multiple channels, if we want.
+// TODO: Should be easy to add support for multiple rooms, if we want.
 type Host struct {
+       *chat.Room
        listener *sshd.SSHListener
-       channel  *chat.Channel
        commands *chat.Commands
 
        motd  string
@@ -36,19 +36,19 @@ type Host struct {
 
 // NewHost creates a Host on top of an existing listener.
 func NewHost(listener *sshd.SSHListener) *Host {
-       ch := chat.NewChannel()
+       room := chat.NewRoom()
        h := Host{
+               Room:     room,
                listener: listener,
-               channel:  ch,
        }
 
        // Make our own commands registry instance.
        commands := chat.Commands{}
        chat.InitCommands(&commands)
        h.InitCommands(&commands)
-       ch.SetCommands(commands)
+       room.SetCommands(commands)
 
-       go ch.Serve()
+       go room.Serve()
        return &h
 }
 
@@ -58,19 +58,19 @@ func (h *Host) SetMotd(motd string) {
 }
 
 func (h Host) isOp(conn sshd.Connection) bool {
-       key, ok := conn.PublicKey()
-       if !ok {
+       key := conn.PublicKey()
+       if key == nil {
                return false
        }
        return h.auth.IsOp(key)
 }
 
-// Connect a specific Terminal to this host and its channel.
+// Connect a specific Terminal to this host and its room.
 func (h *Host) Connect(term *sshd.Terminal) {
-       name := term.Conn.Name()
+       id := NewIdentity(term.Conn)
        term.AutoCompleteCallback = h.AutoCompleteFunction
 
-       user := chat.NewUserScreen(name, term)
+       user := chat.NewUserScreen(id, term)
        user.Config.Theme = h.theme
        go func() {
                // Close term once user is closed.
@@ -79,11 +79,11 @@ func (h *Host) Connect(term *sshd.Terminal) {
        }()
        defer user.Close()
 
-       member, err := h.channel.Join(user)
+       member, err := h.Join(user)
        if err == chat.ErrIdTaken {
                // Try again...
-               user.SetName(fmt.Sprintf("Guest%d", h.count))
-               member, err = h.channel.Join(user)
+               id.SetName(fmt.Sprintf("Guest%d", h.count))
+               member, err = h.Join(user)
        }
        if err != nil {
                logger.Errorf("Failed to join: %s", err)
@@ -108,13 +108,13 @@ func (h *Host) Connect(term *sshd.Terminal) {
                }
                m := chat.ParseInput(line, user)
 
-               // FIXME: Any reason to use h.channel.Send(m) instead?
-               h.channel.HandleMsg(m)
+               // FIXME: Any reason to use h.room.Send(m) instead?
+               h.HandleMsg(m)
 
                cmd := m.Command()
                if cmd == "/nick" || cmd == "/theme" {
                        // Hijack /nick command to update terminal synchronously. Wouldn't
-                       // work if we use h.channel.Send(m) above.
+                       // work if we use h.room.Send(m) above.
                        //
                        // FIXME: This is hacky, how do we improve the API to allow for
                        // this? Chat module shouldn't know about terminals.
@@ -122,14 +122,14 @@ func (h *Host) Connect(term *sshd.Terminal) {
                }
        }
 
-       err = h.channel.Leave(user)
+       err = h.Leave(user)
        if err != nil {
                logger.Errorf("Failed to leave: %s", err)
                return
        }
 }
 
-// Serve our chat channel onto the listener
+// Serve our chat room onto the listener
 func (h *Host) Serve() {
        terminals := h.listener.ServeTerminal()
 
@@ -146,7 +146,7 @@ func (h *Host) AutoCompleteFunction(line string, pos int, key rune) (newLine str
 
        fields := strings.Fields(line[:pos])
        partial := fields[len(fields)-1]
-       names := h.channel.NamesPrefix(partial)
+       names := h.NamesPrefix(partial)
        if len(names) == 0 {
                // Didn't find anything
                return
@@ -172,7 +172,7 @@ func (h *Host) AutoCompleteFunction(line string, pos int, key rune) (newLine str
 
 // GetUser returns a chat.User based on a name.
 func (h *Host) GetUser(name string) (*chat.User, bool) {
-       m, ok := h.channel.MemberById(chat.Id(name))
+       m, ok := h.MemberById(chat.Id(name))
        if !ok {
                return nil, false
        }
@@ -186,7 +186,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
                Prefix:     "/msg",
                PrefixHelp: "USER MESSAGE",
                Help:       "Send MESSAGE to USER.",
-               Handler: func(channel *chat.Channel, msg chat.CommandMsg) error {
+               Handler: func(room *chat.Room, msg chat.CommandMsg) error {
                        args := msg.Args()
                        switch len(args) {
                        case 0:
@@ -201,7 +201,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
                        }
 
                        m := chat.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
-                       channel.Send(m)
+                       room.Send(m)
                        return nil
                },
        })
@@ -212,8 +212,8 @@ func (h *Host) InitCommands(c *chat.Commands) {
                Prefix:     "/kick",
                PrefixHelp: "USER",
                Help:       "Kick USER from the server.",
-               Handler: func(channel *chat.Channel, msg chat.CommandMsg) error {
-                       if !channel.IsOp(msg.From()) {
+               Handler: func(room *chat.Room, msg chat.CommandMsg) error {
+                       if !room.IsOp(msg.From()) {
                                return errors.New("must be op")
                        }
 
@@ -228,7 +228,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
                        }
 
                        body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name())
-                       channel.Send(chat.NewAnnounceMsg(body))
+                       room.Send(chat.NewAnnounceMsg(body))
                        target.Close()
                        return nil
                },
@@ -239,10 +239,9 @@ func (h *Host) InitCommands(c *chat.Commands) {
                Prefix:     "/ban",
                PrefixHelp: "USER",
                Help:       "Ban USER from the server.",
-               Handler: func(channel *chat.Channel, msg chat.CommandMsg) error {
-                       // TODO: This only bans their pubkey if they have one. Would be
-                       // nice to IP-ban too while we're at it.
-                       if !channel.IsOp(msg.From()) {
+               Handler: func(room *chat.Room, msg chat.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")
                        }
 
@@ -256,11 +255,44 @@ func (h *Host) InitCommands(c *chat.Commands) {
                                return errors.New("user not found")
                        }
 
-                       // XXX: Figure out how to link a public key to a target.
-                       //h.auth.Ban(target.Conn.PublicKey())
+                       id := target.Identifier.(*Identity)
+                       h.auth.Ban(id.PublicKey())
+                       h.auth.BanAddr(id.RemoteAddr())
+
                        body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
-                       channel.Send(chat.NewAnnounceMsg(body))
+                       room.Send(chat.NewAnnounceMsg(body))
                        target.Close()
+
+                       logger.Debugf("Banned: \n-> %s", id.Whois())
+
+                       return nil
+               },
+       })
+
+       c.Add(chat.Command{
+               Op:         true,
+               Prefix:     "/whois",
+               PrefixHelp: "USER",
+               Help:       "Information about USER.",
+               Handler: func(room *chat.Room, msg chat.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")
+                       }
+
+                       target, ok := h.GetUser(args[0])
+                       if !ok {
+                               return errors.New("user not found")
+                       }
+
+                       id := target.Identifier.(*Identity)
+                       room.Send(chat.NewSystemMsg(id.Whois(), msg.From()))
+
                        return nil
                },
        })
index fb47a6111197cf4cf2fcf0d09f7a561ab68b0a5e..9c57439f0d1ffc09da844e49920ef56c16d9b0ae 100644 (file)
@@ -184,7 +184,7 @@ func TestHostKick(t *testing.T) {
                // First client
                err = sshd.ConnectShell(addr, "foo", func(r io.Reader, w io.WriteCloser) {
                        // Make op
-                       member, _ := host.channel.MemberById("foo")
+                       member, _ := host.Room.MemberById("foo")
                        member.Op = true
 
                        // Block until second client is here
diff --git a/identity.go b/identity.go
new file mode 100644 (file)
index 0000000..7df812b
--- /dev/null
@@ -0,0 +1,51 @@
+package main
+
+import (
+       "fmt"
+       "net"
+
+       "github.com/shazow/ssh-chat/chat"
+       "github.com/shazow/ssh-chat/sshd"
+)
+
+// Identity is a container for everything that identifies a client.
+type Identity struct {
+       sshd.Connection
+       id chat.Id
+}
+
+// NewIdentity returns a new identity object from an sshd.Connection.
+func NewIdentity(conn sshd.Connection) *Identity {
+       id := chat.Id(conn.Name())
+       return &Identity{
+               Connection: conn,
+               id:         id,
+       }
+}
+
+func (i Identity) Id() chat.Id {
+       return chat.Id(i.id)
+}
+
+func (i *Identity) SetId(id chat.Id) {
+       i.id = id
+}
+
+func (i *Identity) SetName(name string) {
+       i.SetId(chat.Id(name))
+}
+
+func (i Identity) Name() string {
+       return string(i.id)
+}
+
+func (i Identity) Whois() string {
+       ip, _, _ := net.SplitHostPort(i.RemoteAddr().String())
+       fingerprint := "(no public key)"
+       if i.PublicKey() != nil {
+               fingerprint = sshd.Fingerprint(i.PublicKey())
+       }
+       return fmt.Sprintf("name: %s"+chat.Newline+
+               " > ip: %s"+chat.Newline+
+               " > fingerprint: %s", i.Name(), ip, fingerprint)
+}
index 3cf085568a4be2fb1cf3e84d3ce2fcf3e14b42ac..163caa0085d477e99f694d15762bb7ca9ea9d382 100644 (file)
@@ -4,6 +4,7 @@ import (
        "crypto/sha256"
        "encoding/base64"
        "errors"
+       "net"
 
        "golang.org/x/crypto/ssh"
 )
@@ -12,8 +13,8 @@ import (
 type Auth interface {
        // Whether to allow connections without a public key.
        AllowAnonymous() bool
-       // Given public key, return if the connection should be permitted.
-       Check(ssh.PublicKey) (bool, error)
+       // Given address and public key, return if the connection should be permitted.
+       Check(net.Addr, ssh.PublicKey) (bool, error)
 }
 
 // MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation.
@@ -22,7 +23,7 @@ func MakeAuth(auth Auth) *ssh.ServerConfig {
                NoClientAuth: false,
                // Auth-related things should be constant-time to avoid timing attacks.
                PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
-                       ok, err := auth.Check(key)
+                       ok, err := auth.Check(conn.RemoteAddr(), key)
                        if !ok {
                                return nil, err
                        }
@@ -35,7 +36,8 @@ func MakeAuth(auth Auth) *ssh.ServerConfig {
                        if !auth.AllowAnonymous() {
                                return nil, errors.New("public key authentication required")
                        }
-                       return nil, nil
+                       _, err := auth.Check(conn.RemoteAddr(), nil)
+                       return nil, err
                },
        }
 
index 5e782d8c98a7211a531233c489815d2a1cad059d..69a30da4b6ce5acfddb1bd88b297a25c5d7efdac 100644 (file)
@@ -28,7 +28,7 @@ func ListenSSH(laddr string, config *ssh.ServerConfig) (*SSHListener, error) {
 func (l *SSHListener) handleConn(conn net.Conn) (*Terminal, error) {
        if l.RateLimit {
                // TODO: Configurable Limiter?
-               conn = ReadLimitConn(conn, rateio.NewGracefulLimiter(1000, time.Minute*2, time.Second*3))
+               conn = ReadLimitConn(conn, rateio.NewGracefulLimiter(1024*10, time.Minute*2, time.Second*3))
        }
 
        // Upgrade TCP connection to SSH connection
index bfd16a044011929a67cbe14114f2b0c66d3c01cd..3da2d651bd2381060bb7ac69402a004bba33b360 100644 (file)
@@ -3,6 +3,7 @@ package sshd
 import (
        "errors"
        "fmt"
+       "net"
 
        "golang.org/x/crypto/ssh"
        "golang.org/x/crypto/ssh/terminal"
@@ -10,7 +11,8 @@ import (
 
 // Connection is an interface with fields necessary to operate an sshd host.
 type Connection interface {
-       PublicKey() (ssh.PublicKey, bool)
+       PublicKey() ssh.PublicKey
+       RemoteAddr() net.Addr
        Name() string
        Close() error
 }
@@ -19,22 +21,22 @@ type sshConn struct {
        *ssh.ServerConn
 }
 
-func (c sshConn) PublicKey() (ssh.PublicKey, bool) {
+func (c sshConn) PublicKey() ssh.PublicKey {
        if c.Permissions == nil {
-               return nil, false
+               return nil
        }
 
        s, ok := c.Permissions.Extensions["pubkey"]
        if !ok {
-               return nil, false
+               return nil
        }
 
        key, err := ssh.ParsePublicKey([]byte(s))
        if err != nil {
-               return nil, false
+               return nil
        }
 
-       return key, true
+       return key
 }
 
 func (c sshConn) Name() string {