mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-13 15:47:17 +03:00
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/sshd"
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,27 +17,38 @@ var ErrBanned = errors.New("banned")
|
|||||||
// AuthKey is the type that our lookups are keyed against.
|
// AuthKey is the type that our lookups are keyed against.
|
||||||
type AuthKey string
|
type AuthKey string
|
||||||
|
|
||||||
// NewAuthKey returns an AuthKey from an ssh.PublicKey.
|
// NewAuthKey returns string from an ssh.PublicKey.
|
||||||
func NewAuthKey(key ssh.PublicKey) AuthKey {
|
func NewAuthKey(key ssh.PublicKey) string {
|
||||||
|
if key == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
// FIXME: Is there a way to index pubkeys without marshal'ing them into strings?
|
// 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
|
// Auth stores fingerprint lookups
|
||||||
|
// TODO: Add timed auth by using a time.Time instead of struct{} for values.
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
sshd.Auth
|
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
whitelist map[AuthKey]struct{}
|
bannedAddr map[string]struct{}
|
||||||
banned map[AuthKey]struct{}
|
banned map[string]struct{}
|
||||||
ops map[AuthKey]struct{}
|
whitelist map[string]struct{}
|
||||||
|
ops map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuth creates a new default Auth.
|
// NewAuth creates a new default Auth.
|
||||||
func NewAuth() *Auth {
|
func NewAuth() *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
whitelist: make(map[AuthKey]struct{}),
|
bannedAddr: make(map[string]struct{}),
|
||||||
banned: make(map[AuthKey]struct{}),
|
banned: make(map[string]struct{}),
|
||||||
ops: make(map[AuthKey]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.
|
// 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)
|
authkey := NewAuthKey(key)
|
||||||
|
|
||||||
a.RLock()
|
a.RLock()
|
||||||
@ -63,9 +74,13 @@ func (a Auth) Check(key ssh.PublicKey) (bool, error) {
|
|||||||
if !whitelisted {
|
if !whitelisted {
|
||||||
return false, ErrNotWhitelisted
|
return false, ErrNotWhitelisted
|
||||||
}
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, banned := a.banned[authkey]
|
_, banned := a.banned[authkey]
|
||||||
|
if !banned {
|
||||||
|
_, banned = a.bannedAddr[NewAuthAddr(addr)]
|
||||||
|
}
|
||||||
if banned {
|
if banned {
|
||||||
return false, ErrBanned
|
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.
|
// Op will set a fingerprint as a known operator.
|
||||||
func (a *Auth) Op(key ssh.PublicKey) {
|
func (a *Auth) Op(key ssh.PublicKey) {
|
||||||
|
if key == nil {
|
||||||
|
// Don't process empty keys.
|
||||||
|
return
|
||||||
|
}
|
||||||
authkey := NewAuthKey(key)
|
authkey := NewAuthKey(key)
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.ops[authkey] = struct{}{}
|
a.ops[authkey] = struct{}{}
|
||||||
@ -83,6 +102,9 @@ func (a *Auth) Op(key ssh.PublicKey) {
|
|||||||
|
|
||||||
// IsOp checks if a public key is an op.
|
// IsOp checks if a public key is an op.
|
||||||
func (a Auth) IsOp(key ssh.PublicKey) bool {
|
func (a Auth) IsOp(key ssh.PublicKey) bool {
|
||||||
|
if key == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
authkey := NewAuthKey(key)
|
authkey := NewAuthKey(key)
|
||||||
a.RLock()
|
a.RLock()
|
||||||
_, ok := a.ops[authkey]
|
_, 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.
|
// Whitelist will set a public key as a whitelisted user.
|
||||||
func (a *Auth) Whitelist(key ssh.PublicKey) {
|
func (a *Auth) Whitelist(key ssh.PublicKey) {
|
||||||
|
if key == nil {
|
||||||
|
// Don't process empty keys.
|
||||||
|
return
|
||||||
|
}
|
||||||
authkey := NewAuthKey(key)
|
authkey := NewAuthKey(key)
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.whitelist[authkey] = struct{}{}
|
a.whitelist[authkey] = struct{}{}
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWhitelisted checks if a public key is whitelisted.
|
// Ban will set a public key as banned.
|
||||||
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.
|
|
||||||
func (a *Auth) Ban(key ssh.PublicKey) {
|
func (a *Auth) Ban(key ssh.PublicKey) {
|
||||||
|
if key == nil {
|
||||||
|
// Don't process empty keys.
|
||||||
|
return
|
||||||
|
}
|
||||||
authkey := NewAuthKey(key)
|
authkey := NewAuthKey(key)
|
||||||
|
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.banned[authkey] = struct{}{}
|
a.banned[authkey] = struct{}{}
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBanned will set a fingerprint as banned.
|
// Ban will set an IP address as banned.
|
||||||
func (a Auth) IsBanned(key ssh.PublicKey) bool {
|
func (a *Auth) BanAddr(addr net.Addr) {
|
||||||
authkey := NewAuthKey(key)
|
key := NewAuthAddr(addr)
|
||||||
a.RLock()
|
|
||||||
_, ok := a.whitelist[authkey]
|
a.Lock()
|
||||||
a.RUnlock()
|
a.bannedAddr[key] = struct{}{}
|
||||||
return ok
|
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
|
PrefixHelp string
|
||||||
// If omitted, command is hidden from /help
|
// If omitted, command is hidden from /help
|
||||||
Help string
|
Help string
|
||||||
Handler func(*Channel, CommandMsg) error
|
Handler func(*Room, CommandMsg) error
|
||||||
// Command requires Op permissions
|
// Command requires Op permissions
|
||||||
Op bool
|
Op bool
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ func (c Commands) Alias(command string, alias string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run executes a command message.
|
// 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 {
|
if msg.From == nil {
|
||||||
return ErrNoOwner
|
return ErrNoOwner
|
||||||
}
|
}
|
||||||
@ -69,7 +69,7 @@ func (c Commands) Run(channel *Channel, msg CommandMsg) error {
|
|||||||
return ErrInvalidCommand
|
return ErrInvalidCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd.Handler(channel, msg)
|
return cmd.Handler(room, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help will return collated help text as one string.
|
// Help will return collated help text as one string.
|
||||||
@ -102,16 +102,16 @@ func init() {
|
|||||||
func InitCommands(c *Commands) {
|
func InitCommands(c *Commands) {
|
||||||
c.Add(Command{
|
c.Add(Command{
|
||||||
Prefix: "/help",
|
Prefix: "/help",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
op := channel.IsOp(msg.From())
|
op := room.IsOp(msg.From())
|
||||||
channel.Send(NewSystemMsg(channel.commands.Help(op), msg.From()))
|
room.Send(NewSystemMsg(room.commands.Help(op), msg.From()))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Add(Command{
|
c.Add(Command{
|
||||||
Prefix: "/me",
|
Prefix: "/me",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
me := strings.TrimLeft(msg.body, "/me")
|
me := strings.TrimLeft(msg.body, "/me")
|
||||||
if me == "" {
|
if me == "" {
|
||||||
me = " is at a loss for words."
|
me = " is at a loss for words."
|
||||||
@ -119,7 +119,7 @@ func InitCommands(c *Commands) {
|
|||||||
me = me[1:]
|
me = me[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.Send(NewEmoteMsg(me, msg.From()))
|
room.Send(NewEmoteMsg(me, msg.From()))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -127,7 +127,7 @@ func InitCommands(c *Commands) {
|
|||||||
c.Add(Command{
|
c.Add(Command{
|
||||||
Prefix: "/exit",
|
Prefix: "/exit",
|
||||||
Help: "Exit the chat.",
|
Help: "Exit the chat.",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
msg.From().Close()
|
msg.From().Close()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -138,17 +138,20 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/nick",
|
Prefix: "/nick",
|
||||||
PrefixHelp: "NAME",
|
PrefixHelp: "NAME",
|
||||||
Help: "Rename yourself.",
|
Help: "Rename yourself.",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
args := msg.Args()
|
args := msg.Args()
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return ErrMissingArg
|
return ErrMissingArg
|
||||||
}
|
}
|
||||||
u := msg.From()
|
u := msg.From()
|
||||||
oldName := u.Name()
|
oldId := u.Id()
|
||||||
u.SetName(args[0])
|
u.SetId(Id(args[0]))
|
||||||
|
|
||||||
body := fmt.Sprintf("%s is now known as %s.", oldName, u.Name())
|
err := room.Rename(oldId, u)
|
||||||
channel.Send(NewAnnounceMsg(body))
|
if err != nil {
|
||||||
|
u.SetId(oldId)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -156,11 +159,11 @@ func InitCommands(c *Commands) {
|
|||||||
c.Add(Command{
|
c.Add(Command{
|
||||||
Prefix: "/names",
|
Prefix: "/names",
|
||||||
Help: "List users who are connected.",
|
Help: "List users who are connected.",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
// TODO: colorize
|
// TODO: colorize
|
||||||
names := channel.NamesPrefix("")
|
names := room.NamesPrefix("")
|
||||||
body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", "))
|
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
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -170,7 +173,7 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/theme",
|
Prefix: "/theme",
|
||||||
PrefixHelp: "[mono|colors]",
|
PrefixHelp: "[mono|colors]",
|
||||||
Help: "Set your color theme.",
|
Help: "Set your color theme.",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
user := msg.From()
|
user := msg.From()
|
||||||
args := msg.Args()
|
args := msg.Args()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@ -179,7 +182,7 @@ func InitCommands(c *Commands) {
|
|||||||
theme = user.Config.Theme.Id()
|
theme = user.Config.Theme.Id()
|
||||||
}
|
}
|
||||||
body := fmt.Sprintf("Current theme: %s", theme)
|
body := fmt.Sprintf("Current theme: %s", theme)
|
||||||
channel.Send(NewSystemMsg(body, user))
|
room.Send(NewSystemMsg(body, user))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,7 +191,7 @@ func InitCommands(c *Commands) {
|
|||||||
if t.Id() == id {
|
if t.Id() == id {
|
||||||
user.Config.Theme = &t
|
user.Config.Theme = &t
|
||||||
body := fmt.Sprintf("Set theme: %s", id)
|
body := fmt.Sprintf("Set theme: %s", id)
|
||||||
channel.Send(NewSystemMsg(body, user))
|
room.Send(NewSystemMsg(body, user))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,7 +202,7 @@ func InitCommands(c *Commands) {
|
|||||||
c.Add(Command{
|
c.Add(Command{
|
||||||
Prefix: "/quiet",
|
Prefix: "/quiet",
|
||||||
Help: "Silence announcement-type messages (join, part, rename, etc).",
|
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 := msg.From()
|
||||||
u.ToggleQuietMode()
|
u.ToggleQuietMode()
|
||||||
|
|
||||||
@ -209,7 +212,7 @@ func InitCommands(c *Commands) {
|
|||||||
} else {
|
} else {
|
||||||
body = "Quiet mode is toggled OFF"
|
body = "Quiet mode is toggled OFF"
|
||||||
}
|
}
|
||||||
channel.Send(NewSystemMsg(body, u))
|
room.Send(NewSystemMsg(body, u))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -219,8 +222,8 @@ func InitCommands(c *Commands) {
|
|||||||
Prefix: "/op",
|
Prefix: "/op",
|
||||||
PrefixHelp: "USER",
|
PrefixHelp: "USER",
|
||||||
Help: "Mark user as admin.",
|
Help: "Mark user as admin.",
|
||||||
Handler: func(channel *Channel, msg CommandMsg) error {
|
Handler: func(room *Room, msg CommandMsg) error {
|
||||||
if !channel.IsOp(msg.From()) {
|
if !room.IsOp(msg.From()) {
|
||||||
return errors.New("must be op")
|
return errors.New("must be op")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +234,7 @@ func InitCommands(c *Commands) {
|
|||||||
|
|
||||||
// TODO: Add support for fingerprint-based op'ing. This will
|
// TODO: Add support for fingerprint-based op'ing. This will
|
||||||
// probably need to live in host land.
|
// probably need to live in host land.
|
||||||
member, ok := channel.MemberById(Id(args[0]))
|
member, ok := room.MemberById(Id(args[0]))
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("user not found")
|
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.
|
ssh-chat.
|
||||||
|
|
||||||
This package should not know anything about sockets. It should expose io-style
|
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.
|
TODO: Add usage examples here.
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ func (m *Msg) Command() string {
|
|||||||
return ""
|
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 {
|
type PublicMsg struct {
|
||||||
Msg
|
Msg
|
||||||
from *User
|
from *User
|
||||||
@ -105,7 +105,7 @@ func (m *PublicMsg) String() string {
|
|||||||
return fmt.Sprintf("%s: %s", m.from.Name(), m.body)
|
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
|
// extend PublicMsg because it doesn't implement MessageFrom to allow the
|
||||||
// sender to see the emote.
|
// sender to see the emote.
|
||||||
type EmoteMsg struct {
|
type EmoteMsg struct {
|
||||||
@ -212,7 +212,7 @@ type CommandMsg struct {
|
|||||||
*PublicMsg
|
*PublicMsg
|
||||||
command string
|
command string
|
||||||
args []string
|
args []string
|
||||||
channel *Channel
|
room *Room
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *CommandMsg) Command() string {
|
func (m *CommandMsg) Command() string {
|
||||||
|
200
chat/room.go
Normal file
200
chat/room.go
Normal 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
|
||||||
|
}
|
@ -5,8 +5,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChannelServe(t *testing.T) {
|
func TestRoomServe(t *testing.T) {
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
ch.Send(NewAnnounceMsg("hello"))
|
ch.Send(NewAnnounceMsg("hello"))
|
||||||
|
|
||||||
received := <-ch.broadcast
|
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
|
var expected, actual []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &MockScreen{}
|
||||||
u := NewUser("foo")
|
u := NewUser("foo")
|
||||||
|
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
defer ch.Close()
|
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 := NewUser("foo")
|
||||||
u.Config = UserConfig{
|
u.Config = UserConfig{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
defer ch.Close()
|
defer ch.Close()
|
||||||
|
|
||||||
_, err := ch.Join(u)
|
_, err := ch.Join(u)
|
||||||
@ -92,13 +92,13 @@ func TestChannelDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
|
|||||||
ch.HandleMsg(NewPublicMsg("hello", u))
|
ch.HandleMsg(NewPublicMsg("hello", u))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChannelQuietToggleBroadcasts(t *testing.T) {
|
func TestRoomQuietToggleBroadcasts(t *testing.T) {
|
||||||
u := NewUser("foo")
|
u := NewUser("foo")
|
||||||
u.Config = UserConfig{
|
u.Config = UserConfig{
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
defer ch.Close()
|
defer ch.Close()
|
||||||
|
|
||||||
_, err := ch.Join(u)
|
_, err := ch.Join(u)
|
||||||
@ -134,7 +134,7 @@ func TestQuietToggleDisplayState(t *testing.T) {
|
|||||||
s := &MockScreen{}
|
s := &MockScreen{}
|
||||||
u := NewUser("foo")
|
u := NewUser("foo")
|
||||||
|
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
defer ch.Close()
|
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
|
var expected, actual []byte
|
||||||
|
|
||||||
s := &MockScreen{}
|
s := &MockScreen{}
|
||||||
u := NewUser("foo")
|
u := NewUser("foo")
|
||||||
|
|
||||||
ch := NewChannel()
|
ch := NewRoom()
|
||||||
go ch.Serve()
|
go ch.Serve()
|
||||||
defer ch.Close()
|
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.
|
// The error returned when a requested item does not exist in the set.
|
||||||
var ErrItemMissing = errors.New("item does not exist")
|
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.
|
// Set with string lookup.
|
||||||
// TODO: Add trie for efficient prefix lookup?
|
// TODO: Add trie for efficient prefix lookup?
|
||||||
type Set struct {
|
type Set struct {
|
||||||
lookup map[Id]Item
|
lookup map[Id]Identifier
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSet creates a new set.
|
// NewSet creates a new set.
|
||||||
func NewSet() *Set {
|
func NewSet() *Set {
|
||||||
return &Set{
|
return &Set{
|
||||||
lookup: map[Id]Item{},
|
lookup: map[Id]Identifier{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +30,7 @@ func NewSet() *Set {
|
|||||||
func (s *Set) Clear() int {
|
func (s *Set) Clear() int {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
n := len(s.lookup)
|
n := len(s.lookup)
|
||||||
s.lookup = map[Id]Item{}
|
s.lookup = map[Id]Identifier{}
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
@ -49,7 +41,7 @@ func (s *Set) Len() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In checks if an item exists in this set.
|
// 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()
|
s.RLock()
|
||||||
_, ok := s.lookup[item.Id()]
|
_, ok := s.lookup[item.Id()]
|
||||||
s.RUnlock()
|
s.RUnlock()
|
||||||
@ -57,7 +49,7 @@ func (s *Set) In(item Item) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get returns an item with the given Id.
|
// 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()
|
s.RLock()
|
||||||
item, ok := s.lookup[id]
|
item, ok := s.lookup[id]
|
||||||
s.RUnlock()
|
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.
|
// 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()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
@ -84,7 +76,7 @@ func (s *Set) Add(item Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove item from this set.
|
// Remove item from this set.
|
||||||
func (s *Set) Remove(item Item) error {
|
func (s *Set) Remove(item Identifier) error {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
id := item.Id()
|
id := item.Id()
|
||||||
@ -96,9 +88,34 @@ func (s *Set) Remove(item Item) error {
|
|||||||
return nil
|
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
|
// Each loops over every item while holding a read lock and applies fn to each
|
||||||
// element.
|
// element.
|
||||||
func (s *Set) Each(fn func(item Item)) {
|
func (s *Set) Each(fn func(item Identifier)) {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
for _, item := range s.lookup {
|
for _, item := range s.lookup {
|
||||||
fn(item)
|
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.
|
// ListPrefix returns a list of items with a prefix, case insensitive.
|
||||||
func (s *Set) ListPrefix(prefix string) []Item {
|
func (s *Set) ListPrefix(prefix string) []Identifier {
|
||||||
r := []Item{}
|
r := []Identifier{}
|
||||||
prefix = strings.ToLower(prefix)
|
prefix = strings.ToLower(prefix)
|
||||||
|
|
||||||
s.RLock()
|
s.RLock()
|
||||||
|
@ -98,10 +98,10 @@ func (t Theme) Id() string {
|
|||||||
// Colorize name string given some index
|
// Colorize name string given some index
|
||||||
func (t Theme) ColorName(u *User) string {
|
func (t Theme) ColorName(u *User) string {
|
||||||
if t.names == nil {
|
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
|
// 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")
|
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
|
// User definition, implemented set Item interface and io.Writer
|
||||||
type User struct {
|
type User struct {
|
||||||
|
Identifier
|
||||||
Config UserConfig
|
Config UserConfig
|
||||||
name string
|
|
||||||
colorIdx int
|
colorIdx int
|
||||||
joined time.Time
|
joined time.Time
|
||||||
msg chan Message
|
msg chan Message
|
||||||
@ -25,38 +35,29 @@ type User struct {
|
|||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(name string) *User {
|
func NewUser(identity Identifier) *User {
|
||||||
u := User{
|
u := User{
|
||||||
Config: *DefaultUserConfig,
|
Identifier: identity,
|
||||||
joined: time.Now(),
|
Config: *DefaultUserConfig,
|
||||||
msg: make(chan Message, messageBuffer),
|
joined: time.Now(),
|
||||||
done: make(chan struct{}, 1),
|
msg: make(chan Message, messageBuffer),
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
u.SetName(name)
|
u.SetColorIdx(rand.Int())
|
||||||
|
|
||||||
return &u
|
return &u
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserScreen(name string, screen io.Writer) *User {
|
func NewUserScreen(identity Identifier, screen io.Writer) *User {
|
||||||
u := NewUser(name)
|
u := NewUser(identity)
|
||||||
go u.Consume(screen)
|
go u.Consume(screen)
|
||||||
|
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id of the user, a unique identifier within a set
|
// Rename the user with a new Identifier.
|
||||||
func (u *User) Id() Id {
|
func (u *User) SetId(id Id) {
|
||||||
return Id(u.name)
|
u.Identifier.SetId(id)
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
u.SetColorIdx(rand.Int())
|
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
|
// 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 {
|
type Host struct {
|
||||||
|
*chat.Room
|
||||||
listener *sshd.SSHListener
|
listener *sshd.SSHListener
|
||||||
channel *chat.Channel
|
|
||||||
commands *chat.Commands
|
commands *chat.Commands
|
||||||
|
|
||||||
motd string
|
motd string
|
||||||
@ -36,19 +36,19 @@ type Host struct {
|
|||||||
|
|
||||||
// NewHost creates a Host on top of an existing listener.
|
// NewHost creates a Host on top of an existing listener.
|
||||||
func NewHost(listener *sshd.SSHListener) *Host {
|
func NewHost(listener *sshd.SSHListener) *Host {
|
||||||
ch := chat.NewChannel()
|
room := chat.NewRoom()
|
||||||
h := Host{
|
h := Host{
|
||||||
|
Room: room,
|
||||||
listener: listener,
|
listener: listener,
|
||||||
channel: ch,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make our own commands registry instance.
|
// Make our own commands registry instance.
|
||||||
commands := chat.Commands{}
|
commands := chat.Commands{}
|
||||||
chat.InitCommands(&commands)
|
chat.InitCommands(&commands)
|
||||||
h.InitCommands(&commands)
|
h.InitCommands(&commands)
|
||||||
ch.SetCommands(commands)
|
room.SetCommands(commands)
|
||||||
|
|
||||||
go ch.Serve()
|
go room.Serve()
|
||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,19 +58,19 @@ func (h *Host) SetMotd(motd string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h Host) isOp(conn sshd.Connection) bool {
|
func (h Host) isOp(conn sshd.Connection) bool {
|
||||||
key, ok := conn.PublicKey()
|
key := conn.PublicKey()
|
||||||
if !ok {
|
if key == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return h.auth.IsOp(key)
|
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) {
|
func (h *Host) Connect(term *sshd.Terminal) {
|
||||||
name := term.Conn.Name()
|
id := NewIdentity(term.Conn)
|
||||||
term.AutoCompleteCallback = h.AutoCompleteFunction
|
term.AutoCompleteCallback = h.AutoCompleteFunction
|
||||||
|
|
||||||
user := chat.NewUserScreen(name, term)
|
user := chat.NewUserScreen(id, term)
|
||||||
user.Config.Theme = h.theme
|
user.Config.Theme = h.theme
|
||||||
go func() {
|
go func() {
|
||||||
// Close term once user is closed.
|
// Close term once user is closed.
|
||||||
@ -79,11 +79,11 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
}()
|
}()
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|
||||||
member, err := h.channel.Join(user)
|
member, err := h.Join(user)
|
||||||
if err == chat.ErrIdTaken {
|
if err == chat.ErrIdTaken {
|
||||||
// Try again...
|
// Try again...
|
||||||
user.SetName(fmt.Sprintf("Guest%d", h.count))
|
id.SetName(fmt.Sprintf("Guest%d", h.count))
|
||||||
member, err = h.channel.Join(user)
|
member, err = h.Join(user)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to join: %s", err)
|
logger.Errorf("Failed to join: %s", err)
|
||||||
@ -108,13 +108,13 @@ func (h *Host) Connect(term *sshd.Terminal) {
|
|||||||
}
|
}
|
||||||
m := chat.ParseInput(line, user)
|
m := chat.ParseInput(line, user)
|
||||||
|
|
||||||
// FIXME: Any reason to use h.channel.Send(m) instead?
|
// FIXME: Any reason to use h.room.Send(m) instead?
|
||||||
h.channel.HandleMsg(m)
|
h.HandleMsg(m)
|
||||||
|
|
||||||
cmd := m.Command()
|
cmd := m.Command()
|
||||||
if cmd == "/nick" || cmd == "/theme" {
|
if cmd == "/nick" || cmd == "/theme" {
|
||||||
// Hijack /nick command to update terminal synchronously. Wouldn't
|
// 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
|
// FIXME: This is hacky, how do we improve the API to allow for
|
||||||
// this? Chat module shouldn't know about terminals.
|
// 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 {
|
if err != nil {
|
||||||
logger.Errorf("Failed to leave: %s", err)
|
logger.Errorf("Failed to leave: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve our chat channel onto the listener
|
// Serve our chat room onto the listener
|
||||||
func (h *Host) Serve() {
|
func (h *Host) Serve() {
|
||||||
terminals := h.listener.ServeTerminal()
|
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])
|
fields := strings.Fields(line[:pos])
|
||||||
partial := fields[len(fields)-1]
|
partial := fields[len(fields)-1]
|
||||||
names := h.channel.NamesPrefix(partial)
|
names := h.NamesPrefix(partial)
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
// Didn't find anything
|
// Didn't find anything
|
||||||
return
|
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.
|
// GetUser returns a chat.User based on a name.
|
||||||
func (h *Host) GetUser(name string) (*chat.User, bool) {
|
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 {
|
if !ok {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
Prefix: "/msg",
|
Prefix: "/msg",
|
||||||
PrefixHelp: "USER MESSAGE",
|
PrefixHelp: "USER MESSAGE",
|
||||||
Help: "Send MESSAGE to USER.",
|
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()
|
args := msg.Args()
|
||||||
switch len(args) {
|
switch len(args) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -201,7 +201,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m := chat.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
|
m := chat.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
|
||||||
channel.Send(m)
|
room.Send(m)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -212,8 +212,8 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
Prefix: "/kick",
|
Prefix: "/kick",
|
||||||
PrefixHelp: "USER",
|
PrefixHelp: "USER",
|
||||||
Help: "Kick USER from the server.",
|
Help: "Kick USER from the server.",
|
||||||
Handler: func(channel *chat.Channel, msg chat.CommandMsg) error {
|
Handler: func(room *chat.Room, msg chat.CommandMsg) error {
|
||||||
if !channel.IsOp(msg.From()) {
|
if !room.IsOp(msg.From()) {
|
||||||
return errors.New("must be op")
|
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())
|
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()
|
target.Close()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -239,10 +239,9 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
Prefix: "/ban",
|
Prefix: "/ban",
|
||||||
PrefixHelp: "USER",
|
PrefixHelp: "USER",
|
||||||
Help: "Ban USER from the server.",
|
Help: "Ban USER from the server.",
|
||||||
Handler: func(channel *chat.Channel, msg chat.CommandMsg) error {
|
Handler: func(room *chat.Room, msg chat.CommandMsg) error {
|
||||||
// TODO: This only bans their pubkey if they have one. Would be
|
// TODO: Would be nice to specify what to ban. Key? Ip? etc.
|
||||||
// nice to IP-ban too while we're at it.
|
if !room.IsOp(msg.From()) {
|
||||||
if !channel.IsOp(msg.From()) {
|
|
||||||
return errors.New("must be op")
|
return errors.New("must be op")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,11 +255,44 @@ func (h *Host) InitCommands(c *chat.Commands) {
|
|||||||
return errors.New("user not found")
|
return errors.New("user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Figure out how to link a public key to a target.
|
id := target.Identifier.(*Identity)
|
||||||
//h.auth.Ban(target.Conn.PublicKey())
|
h.auth.Ban(id.PublicKey())
|
||||||
|
h.auth.BanAddr(id.RemoteAddr())
|
||||||
|
|
||||||
body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
|
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()
|
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
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -184,7 +184,7 @@ func TestHostKick(t *testing.T) {
|
|||||||
// First client
|
// First client
|
||||||
err = sshd.ConnectShell(addr, "foo", func(r io.Reader, w io.WriteCloser) {
|
err = sshd.ConnectShell(addr, "foo", func(r io.Reader, w io.WriteCloser) {
|
||||||
// Make op
|
// Make op
|
||||||
member, _ := host.channel.MemberById("foo")
|
member, _ := host.Room.MemberById("foo")
|
||||||
member.Op = true
|
member.Op = true
|
||||||
|
|
||||||
// Block until second client is here
|
// Block until second client is here
|
||||||
|
51
identity.go
Normal file
51
identity.go
Normal 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)
|
||||||
|
}
|
10
sshd/auth.go
10
sshd/auth.go
@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
@ -12,8 +13,8 @@ import (
|
|||||||
type Auth interface {
|
type Auth interface {
|
||||||
// Whether to allow connections without a public key.
|
// Whether to allow connections without a public key.
|
||||||
AllowAnonymous() bool
|
AllowAnonymous() bool
|
||||||
// Given public key, return if the connection should be permitted.
|
// Given address and public key, return if the connection should be permitted.
|
||||||
Check(ssh.PublicKey) (bool, error)
|
Check(net.Addr, ssh.PublicKey) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation.
|
// MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation.
|
||||||
@ -22,7 +23,7 @@ func MakeAuth(auth Auth) *ssh.ServerConfig {
|
|||||||
NoClientAuth: false,
|
NoClientAuth: false,
|
||||||
// Auth-related things should be constant-time to avoid timing attacks.
|
// Auth-related things should be constant-time to avoid timing attacks.
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
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 {
|
if !ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -35,7 +36,8 @@ func MakeAuth(auth Auth) *ssh.ServerConfig {
|
|||||||
if !auth.AllowAnonymous() {
|
if !auth.AllowAnonymous() {
|
||||||
return nil, errors.New("public key authentication required")
|
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) {
|
func (l *SSHListener) handleConn(conn net.Conn) (*Terminal, error) {
|
||||||
if l.RateLimit {
|
if l.RateLimit {
|
||||||
// TODO: Configurable Limiter?
|
// 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
|
// Upgrade TCP connection to SSH connection
|
||||||
|
@ -3,6 +3,7 @@ package sshd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/crypto/ssh/terminal"
|
||||||
@ -10,7 +11,8 @@ import (
|
|||||||
|
|
||||||
// Connection is an interface with fields necessary to operate an sshd host.
|
// Connection is an interface with fields necessary to operate an sshd host.
|
||||||
type Connection interface {
|
type Connection interface {
|
||||||
PublicKey() (ssh.PublicKey, bool)
|
PublicKey() ssh.PublicKey
|
||||||
|
RemoteAddr() net.Addr
|
||||||
Name() string
|
Name() string
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
@ -19,22 +21,22 @@ type sshConn struct {
|
|||||||
*ssh.ServerConn
|
*ssh.ServerConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c sshConn) PublicKey() (ssh.PublicKey, bool) {
|
func (c sshConn) PublicKey() ssh.PublicKey {
|
||||||
if c.Permissions == nil {
|
if c.Permissions == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ok := c.Permissions.Extensions["pubkey"]
|
s, ok := c.Permissions.Extensions["pubkey"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := ssh.ParsePublicKey([]byte(s))
|
key, err := ssh.ParsePublicKey([]byte(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, true
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c sshConn) Name() string {
|
func (c sshConn) Name() string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user