import (
"errors"
+ "net"
"sync"
- "github.com/shazow/ssh-chat/sshd"
"golang.org/x/crypto/ssh"
)
// 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{}),
}
}
}
// 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()
if !whitelisted {
return false, ErrNotWhitelisted
}
+ return true, nil
}
_, banned := a.banned[authkey]
+ if !banned {
+ _, banned = a.bannedAddr[NewAuthAddr(addr)]
+ }
if banned {
return false, ErrBanned
}
// 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{}{}
// 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]
// 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()
}
+++ /dev/null
-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
-}
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
}
}
// 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
}
return ErrInvalidCommand
}
- return cmd.Handler(channel, msg)
+ return cmd.Handler(room, msg)
}
// Help will return collated help text as one string.
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."
me = me[1:]
}
- channel.Send(NewEmoteMsg(me, msg.From()))
+ room.Send(NewEmoteMsg(me, msg.From()))
return nil
},
})
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
},
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
},
})
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
},
})
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 {
theme = user.Config.Theme.Id()
}
body := fmt.Sprintf("Current theme: %s", theme)
- channel.Send(NewSystemMsg(body, user))
+ room.Send(NewSystemMsg(body, user))
return nil
}
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
}
}
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()
} else {
body = "Quiet mode is toggled OFF"
}
- channel.Send(NewSystemMsg(body, u))
+ room.Send(NewSystemMsg(body, u))
return nil
},
})
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")
}
// 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")
}
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.
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
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 {
*PublicMsg
command string
args []string
- channel *Channel
+ room *Room
}
func (m *CommandMsg) Command() string {
--- /dev/null
+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
+}
"testing"
)
-func TestChannelServe(t *testing.T) {
- ch := NewChannel()
+func TestRoomServe(t *testing.T) {
+ ch := NewRoom()
ch.Send(NewAnnounceMsg("hello"))
received := <-ch.broadcast
}
}
-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()
}
}
-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)
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)
s := &MockScreen{}
u := NewUser("foo")
- ch := NewChannel()
+ ch := NewRoom()
go ch.Serve()
defer ch.Close()
}
}
-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()
// 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{},
}
}
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
}
}
// 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()
}
// 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()
}
// 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()
}
// 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()
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)
}
// 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()
// 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
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
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())
}
}
// 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
// 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
}
}
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.
}()
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)
}
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.
}
}
- 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()
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
// 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
}
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:
}
m := chat.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
- channel.Send(m)
+ room.Send(m)
return nil
},
})
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")
}
}
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
},
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")
}
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
},
})
// 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
--- /dev/null
+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)
+}
"crypto/sha256"
"encoding/base64"
"errors"
+ "net"
"golang.org/x/crypto/ssh"
)
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.
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
}
if !auth.AllowAnonymous() {
return nil, errors.New("public key authentication required")
}
- return nil, nil
+ _, err := auth.Check(conn.RemoteAddr(), nil)
+ return nil, err
},
}
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
import (
"errors"
"fmt"
+ "net"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
// 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
}
*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 {