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:
parent
cc25d17bdc
commit
3c4e6994c2
80
auth.go
80
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()
|
||||
}
|
||||
|
|
188
chat/channel.go
188
chat/channel.go
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
53
chat/set.go
53
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()
|
||||
|
|
|
@ -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
|
||||
|
|
45
chat/user.go
45
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())
|
||||
}
|
||||
|
||||
|
|
98
host.go
98
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
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
10
sshd/auth.go
10
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
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue