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
This commit is contained in:
Andrey Petrov 2015-01-16 21:53:22 -08:00
parent cc25d17bdc
commit 3c4e6994c2
16 changed files with 486 additions and 344 deletions

80
auth.go
View File

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

View File

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

View 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")
}

View 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.

View 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 {

200
chat/room.go Normal file
View File

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

View 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()

View 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()

View 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

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

98
host.go
View File

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

View 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

51
identity.go Normal file
View File

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

View 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
},
}

View 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

View 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 {