From 3c4e6994c291c69461e99846e74a7a7e96f8e2b9 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 16 Jan 2015 21:53:22 -0800 Subject: [PATCH] chat.Channel->chat.Room, /ban, /whois, chat.User.Identifier - 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 --- auth.go | 80 ++++++---- chat/channel.go | 188 ----------------------- chat/command.go | 53 +++---- chat/doc.go | 2 +- chat/message.go | 6 +- chat/room.go | 200 +++++++++++++++++++++++++ chat/{channel_test.go => room_test.go} | 22 +-- chat/set.go | 53 ++++--- chat/theme.go | 4 +- chat/user.go | 45 +++--- host.go | 98 ++++++++---- host_test.go | 2 +- identity.go | 51 +++++++ sshd/auth.go | 10 +- sshd/net.go | 2 +- sshd/terminal.go | 14 +- 16 files changed, 486 insertions(+), 344 deletions(-) delete mode 100644 chat/channel.go create mode 100644 chat/room.go rename chat/{channel_test.go => room_test.go} (90%) create mode 100644 identity.go diff --git a/auth.go b/auth.go index d218366..bce92bf 100644 --- 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 index a5fd0c3..0000000 --- a/chat/channel.go +++ /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 -} diff --git a/chat/command.go b/chat/command.go index 351d090..aa6d3f5 100644 --- a/chat/command.go +++ b/chat/command.go @@ -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") } diff --git a/chat/doc.go b/chat/doc.go index 7c80e02..22760e7 100644 --- a/chat/doc.go +++ b/chat/doc.go @@ -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. diff --git a/chat/message.go b/chat/message.go index 955bc23..2a804a9 100644 --- a/chat/message.go +++ b/chat/message.go @@ -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 index 0000000..896f92f --- /dev/null +++ b/chat/room.go @@ -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 +} diff --git a/chat/channel_test.go b/chat/room_test.go similarity index 90% rename from chat/channel_test.go rename to chat/room_test.go index 13c9bdc..4f39dd7 100644 --- a/chat/channel_test.go +++ b/chat/room_test.go @@ -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() diff --git a/chat/set.go b/chat/set.go index 2248de8..d032a34 100644 --- a/chat/set.go +++ b/chat/set.go @@ -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() diff --git a/chat/theme.go b/chat/theme.go index 9ada33f..dbf9e82 100644 --- a/chat/theme.go +++ b/chat/theme.go @@ -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 diff --git a/chat/user.go b/chat/user.go index 75ea330..b7ab4a8 100644 --- a/chat/user.go +++ b/chat/user.go @@ -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 936e8d7..5747310 100644 --- 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 }, }) diff --git a/host_test.go b/host_test.go index fb47a61..9c57439 100644 --- a/host_test.go +++ b/host_test.go @@ -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 index 0000000..7df812b --- /dev/null +++ b/identity.go @@ -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) +} diff --git a/sshd/auth.go b/sshd/auth.go index 3cf0855..163caa0 100644 --- a/sshd/auth.go +++ b/sshd/auth.go @@ -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 }, } diff --git a/sshd/net.go b/sshd/net.go index 5e782d8..69a30da 100644 --- a/sshd/net.go +++ b/sshd/net.go @@ -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 diff --git a/sshd/terminal.go b/sshd/terminal.go index bfd16a0..3da2d65 100644 --- a/sshd/terminal.go +++ b/sshd/terminal.go @@ -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 {