Adapted my repo to shazow. Changes...

- Added new rank: Administators (or masters). This rank can add or delete operators, but cannot add or remove another administrators. Administrators rank is designed by public key on server startup.
- Added "vim mode". With this mode you can use commands as ':' instead of '/'. As you can see in the code, the Prefix parameter of Command structure was changed by a neutral parameter for future introductions of prefixes.
- New Help function. This function add help for admin rank.
- Added the following commands:
  - setnick: Administrators can change the nick of another user.
  - private: Users can stablish privates conversations. This conversations are permanent until they execute endprivate command.
  - endprivate: Finish private conversation.
  - welcome: Prints motd. Only admins can execute this.
  - whois: In whois only admins can see ip.
  - delop: Allow admins to delete an operator (or moderator as is mentioned in some parts of the code).
- Changed historyLen and roomBuffer vars.
- In cmd tool have been added fsnotify to update public key files (admin and moderator public key files). This is to prevent to restart the server everytime we want to add an administrator.
- Added new prompt mode. In this mode you can see in which room you are talking. As default is 'general'. If you start private conversation the room will be the name of the another user. This part has been introduced for future implementations.
- As I said before now exists private conversations (unlike msg command).
This commit is contained in:
themester 2018-01-12 20:04:39 +01:00
parent fd77009f8d
commit 229c2d793d
8 changed files with 340 additions and 58 deletions

24
auth.go
View File

@ -44,6 +44,7 @@ type Auth struct {
banned *set.Set banned *set.Set
whitelist *set.Set whitelist *set.Set
ops *set.Set ops *set.Set
masters *set.Set
} }
// NewAuth creates a new empty Auth. // NewAuth creates a new empty Auth.
@ -53,6 +54,7 @@ func NewAuth() *Auth {
banned: set.New(), banned: set.New(),
whitelist: set.New(), whitelist: set.New(),
ops: set.New(), ops: set.New(),
masters: set.New(),
} }
} }
@ -99,6 +101,20 @@ func (a *Auth) Op(key ssh.PublicKey, d time.Duration) {
logger.Debugf("Added to ops: %s (for %s)", authItem.Key(), d) logger.Debugf("Added to ops: %s (for %s)", authItem.Key(), d)
} }
// Master sets a public key as a known admin master.
func (a *Auth) Master(key ssh.PublicKey, d time.Duration) {
if key == nil {
return
}
authItem := newAuthItem(key)
if d != 0 {
a.masters.Add(set.Expire(authItem, d))
} else {
a.masters.Add(authItem)
}
logger.Debugf("Added to masters: %s (for %s)", authItem.Key(), d)
}
// 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 { if key == nil {
@ -108,6 +124,14 @@ func (a *Auth) IsOp(key ssh.PublicKey) bool {
return a.ops.In(authkey) return a.ops.In(authkey)
} }
func (a *Auth) IsMaster(key ssh.PublicKey) bool {
if key == nil {
return false
}
authkey := newAuthKey(key)
return a.masters.In(authkey)
}
// 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, d time.Duration) { func (a *Auth) Whitelist(key ssh.PublicKey, d time.Duration) {
if key == nil { if key == nil {

View File

@ -36,6 +36,8 @@ type Command struct {
Handler func(*Room, message.CommandMsg) error Handler func(*Room, message.CommandMsg) error
// Command requires Op permissions // Command requires Op permissions
Op bool Op bool
// command requires Admin permissions
Admin bool
} }
// Commands is a registry of available commands. // Commands is a registry of available commands.
@ -68,7 +70,7 @@ func (c Commands) Run(room *Room, msg message.CommandMsg) error {
return ErrNoOwner return ErrNoOwner
} }
cmd, ok := c[msg.Command()] cmd, ok := c[msg.Command()[1:]]
if !ok { if !ok {
return ErrInvalidCommand return ErrInvalidCommand
} }
@ -77,14 +79,19 @@ func (c Commands) Run(room *Room, msg message.CommandMsg) error {
} }
// Help will return collated help text as one string. // Help will return collated help text as one string.
func (c Commands) Help(showOp bool) string { func (c Commands) Help(showOp, showAdmin bool) string {
// Filter by op // Filter by op
op := []*Command{} op := []*Command{}
admin := []*Command{}
normal := []*Command{} normal := []*Command{}
for _, cmd := range c { for _, cmd := range c {
if cmd.Op { if cmd.Op {
op = append(op, cmd) op = append(op, cmd)
} else { }
if cmd.Admin {
admin = append(admin, cmd)
}
if !cmd.Op && !cmd.Admin {
normal = append(normal, cmd) normal = append(normal, cmd)
} }
} }
@ -92,6 +99,9 @@ func (c Commands) Help(showOp bool) string {
if showOp { if showOp {
help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String() help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String()
} }
if showAdmin {
help += message.Newline + "-> Admin commands:" + message.Newline + NewCommandsHelp(admin).String()
}
return help return help
} }
@ -105,16 +115,17 @@ func init() {
// InitCommands injects default commands into a Commands registry. // InitCommands injects default commands into a Commands registry.
func InitCommands(c *Commands) { func InitCommands(c *Commands) {
c.Add(Command{ c.Add(Command{
Prefix: "/help", Prefix: "help",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
op := room.IsOp(msg.From()) op := room.IsOp(msg.From())
room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From())) admin := room.IsMaster(msg.From())
room.Send(message.NewSystemMsg(room.commands.Help(op, admin), msg.From()))
return nil return nil
}, },
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/me", Prefix: "me",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
me := strings.TrimLeft(msg.Body(), "/me") me := strings.TrimLeft(msg.Body(), "/me")
if me == "" { if me == "" {
@ -129,17 +140,18 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/exit", Prefix: "exit",
Help: "Exit the chat.", Help: "Exit the chat.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
msg.From().Close() msg.From().Close()
return nil return nil
}, },
}) })
c.Alias("/exit", "/quit") c.Alias("exit", "quit")
c.Alias("q", "quit")
c.Add(Command{ c.Add(Command{
Prefix: "/nick", Prefix: "nick",
PrefixHelp: "NAME", PrefixHelp: "NAME",
Help: "Rename yourself.", Help: "Rename yourself.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
@ -170,7 +182,38 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/names", Admin: true,
Prefix: "setnick",
PrefixHelp: "NAME ANOTHER",
Help: "Rename NAME to ANOTHER username",
Handler: func(room *Room, msg message.CommandMsg) error {
args := msg.Args()
if len(args) < 2 {
return errors.New("invalid arguments")
}
member, ok := room.MemberByID(args[0])
if !ok {
return errors.New("failed to find member")
}
oldID := member.ID()
newID := SanitizeName(args[1])
if newID == oldID {
return errors.New("new name is the same as the original")
}
member.SetID(newID)
err := room.Rename(oldID, member)
if err != nil {
member.SetID(oldID)
return err
}
return nil
},
})
c.Add(Command{
Prefix: "names",
Help: "List users who are connected.", Help: "List users who are connected.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
theme := msg.From().Config().Theme theme := msg.From().Config().Theme
@ -196,10 +239,10 @@ func InitCommands(c *Commands) {
return nil return nil
}, },
}) })
c.Alias("/names", "/list") c.Alias("names", "list")
c.Add(Command{ c.Add(Command{
Prefix: "/theme", Prefix: "theme",
PrefixHelp: "[colors|...]", PrefixHelp: "[colors|...]",
Help: "Set your color theme.", Help: "Set your color theme.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
@ -239,7 +282,7 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/quiet", Prefix: "quiet",
Help: "Silence room announcements.", Help: "Silence room announcements.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
u := msg.From() u := msg.From()
@ -259,7 +302,7 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/slap", Prefix: "slap",
PrefixHelp: "NAME", PrefixHelp: "NAME",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
var me string var me string
@ -276,7 +319,7 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/ignore", Prefix: "ignore",
PrefixHelp: "[USER]", PrefixHelp: "[USER]",
Help: "Hide messages from USER, /unignore USER to stop hiding.", Help: "Hide messages from USER, /unignore USER to stop hiding.",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
@ -321,7 +364,7 @@ func InitCommands(c *Commands) {
}) })
c.Add(Command{ c.Add(Command{
Prefix: "/unignore", Prefix: "unignore",
PrefixHelp: "USER", PrefixHelp: "USER",
Handler: func(room *Room, msg message.CommandMsg) error { Handler: func(room *Room, msg message.CommandMsg) error {
id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore")) id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))

View File

@ -5,6 +5,8 @@ type Identifier interface {
ID() string ID() string
SetID(string) SetID(string)
Name() string Name() string
Chat() string
SetChat(string)
} }
// SimpleID is a simple Identifier implementation used for testing. // SimpleID is a simple Identifier implementation used for testing.
@ -24,3 +26,12 @@ func (i SimpleID) SetID(s string) {
func (i SimpleID) Name() string { func (i SimpleID) Name() string {
return i.ID() return i.ID()
} }
// Chat returns the chat name
func (i SimpleID) Chat() string {
return i.Chat()
}
func (i SimpleID) SetChat(s string) {
}

View File

@ -88,7 +88,7 @@ func (m PublicMsg) From() *User {
func (m PublicMsg) ParseCommand() (*CommandMsg, bool) { func (m PublicMsg) ParseCommand() (*CommandMsg, bool) {
// Check if the message is a command // Check if the message is a command
if !strings.HasPrefix(m.body, "/") { if !strings.HasPrefix(m.body, "/") && !strings.HasPrefix(m.body, ":") {
return nil, false return nil, false
} }

View File

@ -10,8 +10,8 @@ import (
"github.com/shazow/ssh-chat/set" "github.com/shazow/ssh-chat/set"
) )
const historyLen = 20 const historyLen = 5
const roomBuffer = 10 const roomBuffer = 250
// The error returned when a message is sent to a room that is already // The error returned when a message is sent to a room that is already
// closed. // closed.
@ -37,6 +37,7 @@ type Room struct {
Members *set.Set Members *set.Set
Ops *set.Set Ops *set.Set
Masters *set.Set
} }
// NewRoom creates a new room. // NewRoom creates a new room.
@ -50,6 +51,7 @@ func NewRoom() *Room {
Members: set.New(), Members: set.New(),
Ops: set.New(), Ops: set.New(),
Masters: set.New(),
} }
} }
@ -165,6 +167,7 @@ func (r *Room) Leave(u message.Identifier) error {
return err return err
} }
r.Ops.Remove(u.ID()) r.Ops.Remove(u.ID())
r.Masters.Remove(u.ID())
s := fmt.Sprintf("%s left.", u.Name()) s := fmt.Sprintf("%s left.", u.Name())
r.Send(message.NewAnnounceMsg(s)) r.Send(message.NewAnnounceMsg(s))
return nil return nil
@ -212,6 +215,11 @@ func (r *Room) IsOp(u *message.User) bool {
return r.Ops.In(u.ID()) return r.Ops.In(u.ID())
} }
// IsMaster returns whether a user is an admin master in this room.
func (r *Room) IsMaster(u *message.User) bool {
return r.Masters.In(u.ID())
}
// Topic of the room. // Topic of the room.
func (r *Room) Topic() string { func (r *Room) Topic() string {
return r.topic return r.topic

View File

@ -12,6 +12,7 @@ import (
"github.com/alexcesaro/log" "github.com/alexcesaro/log"
"github.com/alexcesaro/log/golog" "github.com/alexcesaro/log/golog"
"github.com/fsnotify/fsnotify"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@ -31,7 +32,8 @@ type Options struct {
Version bool `long:"version" description:"Print version and exit."` Version bool `long:"version" description:"Print version and exit."`
Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
Admin string `long:"admin" description:"File of public keys who are admins."` Mods string `long:"moderators" description:"File of public keys who are moderators."`
Admins string `long:"admins" description:"File of public keys who are admins."`
Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
Motd string `long:"motd" description:"Optional Message of the Day file."` Motd string `long:"motd" description:"Optional Message of the Day file."`
Log string `long:"log" description:"Write chat log to this file."` Log string `long:"log" description:"Write chat log to this file."`
@ -123,7 +125,18 @@ func main() {
host.SetTheme(message.Themes[0]) host.SetTheme(message.Themes[0])
host.Version = Version host.Version = Version
err = fromFile(options.Admin, func(line []byte) error { err = fromFile(options.Admins, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
if err != nil {
return err
}
auth.Master(key, 0)
return nil
})
if err != nil {
fail(5, "Failed to load admins: %v\n", err)
}
err = fromFile(options.Mods, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line) key, _, _, _, err := ssh.ParseAuthorizedKey(line)
if err != nil { if err != nil {
return err return err
@ -132,7 +145,7 @@ func main() {
return nil return nil
}) })
if err != nil { if err != nil {
fail(5, "Failed to load admins: %v\n", err) fail(5, "Failed to load mods: %v\n", err)
} }
err = fromFile(options.Whitelist, func(line []byte) error { err = fromFile(options.Whitelist, func(line []byte) error {
@ -169,7 +182,45 @@ func main() {
host.SetLogging(fp) host.SetLogging(fp)
} }
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
defer watcher.Close()
go host.Serve() go host.Serve()
go func() {
for event := range watcher.Events {
if event.Op&fsnotify.Write == fsnotify.Write {
if event.Name == options.Admins {
err = fromFile(options.Admins, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
if err != nil {
return err
}
auth.Master(key, 0)
return nil
})
}
if event.Name == options.Mods {
err = fromFile(options.Mods, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
if err != nil {
return err
}
auth.Op(key, 0)
return nil
})
}
}
}
}()
if len(options.Admins) > 0 {
watcher.Add(options.Admins)
}
if len(options.Mods) > 0 {
watcher.Add(options.Mods)
}
// Construct interrupt handler // Construct interrupt handler
sig := make(chan os.Signal, 1) sig := make(chan os.Signal, 1)
@ -177,7 +228,6 @@ func main() {
<-sig // Wait for ^C signal <-sig // Wait for ^C signal
fmt.Fprintln(os.Stderr, "Interrupt signal detected, shutting down.") fmt.Fprintln(os.Stderr, "Interrupt signal detected, shutting down.")
os.Exit(0)
} }
func fromFile(path string, handler func(line []byte) error) error { func fromFile(path string, handler func(line []byte) error) error {

198
host.go
View File

@ -25,7 +25,7 @@ func GetPrompt(user *message.User) string {
if cfg.Theme != nil { if cfg.Theme != nil {
name = cfg.Theme.ColorName(user) name = cfg.Theme.ColorName(user)
} }
return fmt.Sprintf("[%s] ", name) return fmt.Sprintf("[%s:%s] ", name, user.Chat())
} }
// Host is the bridge between sshd and chat modules // Host is the bridge between sshd and chat modules
@ -42,8 +42,11 @@ type Host struct {
// Default theme // Default theme
theme message.Theme theme message.Theme
mu sync.Mutex mu sync.Mutex
motd string motd string
// start private mode
private map[string]*message.User
count int count int
} }
@ -88,6 +91,14 @@ func (h *Host) isOp(conn sshd.Connection) bool {
return h.auth.IsOp(key) return h.auth.IsOp(key)
} }
func (h *Host) isMaster(conn sshd.Connection) bool {
key := conn.PublicKey()
if key == nil {
return false
}
return h.auth.IsMaster(key)
}
// Connect a specific Terminal to this host and its room. // Connect a specific Terminal to this host and its room.
func (h *Host) Connect(term *sshd.Terminal) { func (h *Host) Connect(term *sshd.Terminal) {
id := NewIdentity(term.Conn) id := NewIdentity(term.Conn)
@ -124,6 +135,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
} }
// Successfully joined. // Successfully joined.
user.SetChat("general")
term.SetPrompt(GetPrompt(user)) term.SetPrompt(GetPrompt(user))
term.AutoCompleteCallback = h.AutoCompleteFunction(user) term.AutoCompleteCallback = h.AutoCompleteFunction(user)
user.SetHighlight(user.Name()) user.SetHighlight(user.Name())
@ -132,10 +144,15 @@ func (h *Host) Connect(term *sshd.Terminal) {
if h.isOp(term.Conn) { if h.isOp(term.Conn) {
h.Room.Ops.Add(set.Itemize(member.ID(), member)) h.Room.Ops.Add(set.Itemize(member.ID(), member))
} }
if h.isMaster(term.Conn) {
h.Room.Masters.Add(set.Itemize(member.ID(), member))
}
ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name()) logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
var args []string
h.private = make(map[string]*message.User)
for { for {
line, err := term.ReadLine() line, err := term.ReadLine()
if err == io.EOF { if err == io.EOF {
@ -162,19 +179,35 @@ func (h *Host) Connect(term *sshd.Terminal) {
m := message.ParseInput(line, user) m := message.ParseInput(line, user)
// FIXME: Any reason to use h.room.Send(m) instead? switch c := m.(type) {
h.HandleMsg(m) case *message.CommandMsg:
args = c.Args()
h.HandleMsg(m)
default:
to, ok := h.private[user.Name()]
if ok {
m = message.NewPrivateMsg(
m.String(), user, to,
)
}
h.HandleMsg(m)
}
cmd := m.Command() if cmd := m.Command(); len(cmd) > 0 {
if cmd == "/nick" || cmd == "/theme" { switch cmd[1:] {
// Hijack /nick command to update terminal synchronously. Wouldn't case "private":
// work if we use h.room.Send(m) above. if len(args) > 0 {
// user.SetChat(args[0])
// FIXME: This is hacky, how do we improve the API to allow for }
// this? Chat module shouldn't know about terminals. case "endprivate":
user.SetChat("general")
case "nick":
case "theme":
}
term.SetPrompt(GetPrompt(user)) term.SetPrompt(GetPrompt(user))
user.SetHighlight(user.Name()) user.SetHighlight(user.Name())
} }
args = nil
} }
err = h.Leave(user) err = h.Leave(user)
@ -280,7 +313,7 @@ func (h *Host) GetUser(name string) (*message.User, bool) {
// override any existing commands. // override any existing commands.
func (h *Host) InitCommands(c *chat.Commands) { func (h *Host) InitCommands(c *chat.Commands) {
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/msg", Prefix: "msg",
PrefixHelp: "USER MESSAGE", PrefixHelp: "USER MESSAGE",
Help: "Send MESSAGE to USER.", Help: "Send MESSAGE to USER.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
@ -309,7 +342,59 @@ func (h *Host) InitCommands(c *chat.Commands) {
}) })
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/reply", Prefix: "private",
PrefixHelp: "USER",
Help: "Start private chat with USER",
Handler: func(room *chat.Room, msg message.CommandMsg) error {
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")
}
h.private[msg.From().Name()] = target
txt := fmt.Sprintf("[Private mode started with %s]", target.Name())
ms := message.NewSystemMsg(txt, msg.From())
room.Send(ms)
target.SetReplyTo(msg.From())
return nil
},
})
c.Alias("p", "private")
c.Add(chat.Command{
Admin: true,
Prefix: "welcome",
Handler: func(room *chat.Room, msg message.CommandMsg) error {
if !room.IsMaster(msg.From()) {
return errors.New("must be admin")
}
room.Send(message.NewMsg(h.motd))
return nil
},
})
c.Add(chat.Command{
Prefix: "endprivate",
Help: "Stop private chat",
Handler: func(room *chat.Room, msg message.CommandMsg) error {
delete(h.private, msg.From().Name())
txt := "[Private mode ended]"
ms := message.NewSystemMsg(txt, msg.From())
room.Send(ms)
return nil
},
})
c.Add(chat.Command{
Prefix: "reply",
PrefixHelp: "MESSAGE", PrefixHelp: "MESSAGE",
Help: "Reply with MESSAGE to the previous private message.", Help: "Reply with MESSAGE to the previous private message.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
@ -341,7 +426,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
}) })
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/whois", Prefix: "whois",
PrefixHelp: "USER", PrefixHelp: "USER",
Help: "Information about USER.", Help: "Information about USER.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
@ -357,11 +442,15 @@ func (h *Host) InitCommands(c *chat.Commands) {
id := target.Identifier.(*Identity) id := target.Identifier.(*Identity)
var whois string var whois string
switch room.IsOp(msg.From()) { switch room.IsMaster(msg.From()) {
case true: case true:
whois = id.WhoisAdmin() whois = id.WhoisMaster()
case false: case false:
whois = id.Whois() if room.IsOp(msg.From()) {
whois = id.WhoisAdmin()
} else {
whois = id.Whois()
}
} }
room.Send(message.NewSystemMsg(whois, msg.From())) room.Send(message.NewSystemMsg(whois, msg.From()))
@ -371,7 +460,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
// Hidden commands // Hidden commands
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/version", Prefix: "version",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
room.Send(message.NewSystemMsg(h.Version, msg.From())) room.Send(message.NewSystemMsg(h.Version, msg.From()))
return nil return nil
@ -380,7 +469,7 @@ func (h *Host) InitCommands(c *chat.Commands) {
timeStarted := time.Now() timeStarted := time.Now()
c.Add(chat.Command{ c.Add(chat.Command{
Prefix: "/uptime", Prefix: "uptime",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
room.Send(message.NewSystemMsg(humanize.Time(timeStarted), msg.From())) room.Send(message.NewSystemMsg(humanize.Time(timeStarted), msg.From()))
return nil return nil
@ -390,11 +479,12 @@ func (h *Host) InitCommands(c *chat.Commands) {
// Op commands // Op commands
c.Add(chat.Command{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/kick", Admin: true,
Prefix: "kick",
PrefixHelp: "USER", PrefixHelp: "USER",
Help: "Kick USER from the server.", Help: "Kick USER from the server.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
if !room.IsOp(msg.From()) { if !room.IsOp(msg.From()) && !room.IsMaster(msg.From()) {
return errors.New("must be op") return errors.New("must be op")
} }
@ -408,6 +498,10 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("user not found") return errors.New("user not found")
} }
if room.IsMaster(target) {
return errors.New("you cannot kick master")
}
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())
room.Send(message.NewAnnounceMsg(body)) room.Send(message.NewAnnounceMsg(body))
target.Close() target.Close()
@ -416,13 +510,14 @@ func (h *Host) InitCommands(c *chat.Commands) {
}) })
c.Add(chat.Command{ c.Add(chat.Command{
Admin: true,
Op: true, Op: true,
Prefix: "/ban", Prefix: "ban",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION]",
Help: "Ban USER from the server.", Help: "Ban USER from the server.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
// TODO: Would be nice to specify what to ban. Key? Ip? etc. // TODO: Would be nice to specify what to ban. Key? Ip? etc.
if !room.IsOp(msg.From()) { if !room.IsMaster(msg.From()) {
return errors.New("must be op") return errors.New("must be op")
} }
@ -436,6 +531,10 @@ func (h *Host) InitCommands(c *chat.Commands) {
return errors.New("user not found") return errors.New("user not found")
} }
if room.IsMaster(target) {
return errors.New("you cannot ban master.")
}
var until time.Duration = 0 var until time.Duration = 0
if len(args) > 1 { if len(args) > 1 {
until, _ = time.ParseDuration(args[1]) until, _ = time.ParseDuration(args[1])
@ -456,13 +555,18 @@ func (h *Host) InitCommands(c *chat.Commands) {
}) })
c.Add(chat.Command{ c.Add(chat.Command{
Admin: true,
Op: true, Op: true,
Prefix: "/motd", Prefix: "motd",
PrefixHelp: "[MESSAGE]", PrefixHelp: "[MESSAGE]",
Help: "Set a new MESSAGE of the day, print the current motd without parameters.", Help: "Set a new MESSAGE of the day, print the current motd without parameters.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
args := msg.Args()
user := msg.From() user := msg.From()
if !room.IsMaster(user) && !room.IsOp(user) {
return errors.New("must be OP to modify the MOTD")
}
args := msg.Args()
h.mu.Lock() h.mu.Lock()
motd := h.motd motd := h.motd
@ -472,9 +576,6 @@ func (h *Host) InitCommands(c *chat.Commands) {
room.Send(message.NewSystemMsg(motd, user)) room.Send(message.NewSystemMsg(motd, user))
return nil return nil
} }
if !room.IsOp(user) {
return errors.New("must be OP to modify the MOTD")
}
motd = strings.Join(args, " ") motd = strings.Join(args, " ")
h.SetMotd(motd) h.SetMotd(motd)
@ -486,13 +587,13 @@ func (h *Host) InitCommands(c *chat.Commands) {
}) })
c.Add(chat.Command{ c.Add(chat.Command{
Op: true, Admin: true,
Prefix: "/op", Prefix: "op",
PrefixHelp: "USER [DURATION]", PrefixHelp: "USER [DURATION]",
Help: "Set USER as admin.", Help: "Set USER as admin.",
Handler: func(room *chat.Room, msg message.CommandMsg) error { Handler: func(room *chat.Room, msg message.CommandMsg) error {
if !room.IsOp(msg.From()) { if !room.IsMaster(msg.From()) {
return errors.New("must be op") return errors.New("must be admin")
} }
args := msg.Args() args := msg.Args()
@ -520,4 +621,35 @@ func (h *Host) InitCommands(c *chat.Commands) {
return nil return nil
}, },
}) })
c.Add(chat.Command{
Admin: true,
Prefix: "delop",
PrefixHelp: "USER",
Help: "Remove USER as admin.",
Handler: func(room *chat.Room, msg message.CommandMsg) error {
if !room.IsMaster(msg.From()) {
return errors.New("must be master")
}
args := msg.Args()
if len(args) == 0 {
return errors.New("must specify user")
}
member, ok := room.MemberByID(args[0])
if !ok {
return errors.New("user not found")
}
err := room.Ops.Remove(args[0])
if err != nil {
return err
}
body := fmt.Sprintf("Deleted op by %s.", msg.From().Name())
room.Send(message.NewSystemMsg(body, member.User))
return nil
},
})
} }

View File

@ -14,6 +14,7 @@ import (
type Identity struct { type Identity struct {
sshd.Connection sshd.Connection
id string id string
chat string
created time.Time created time.Time
} }
@ -42,8 +43,22 @@ func (i Identity) Name() string {
return i.id return i.id
} }
func (i Identity) Chat() string {
return i.chat
}
func (i *Identity) SetChat(c string) {
i.chat = c
}
// Whois returns a whois description for non-admin users. // Whois returns a whois description for non-admin users.
func (i Identity) Whois() string { func (i Identity) Whois() string {
return "name: " + i.Name() + message.Newline +
" > joined: " + humanize.Time(i.created)
}
// WhoisAdmin returns a whois description for admin users.
func (i Identity) WhoisAdmin() string {
fingerprint := "(no public key)" fingerprint := "(no public key)"
if i.PublicKey() != nil { if i.PublicKey() != nil {
fingerprint = sshd.Fingerprint(i.PublicKey()) fingerprint = sshd.Fingerprint(i.PublicKey())
@ -54,8 +69,7 @@ func (i Identity) Whois() string {
" > joined: " + humanize.Time(i.created) " > joined: " + humanize.Time(i.created)
} }
// WhoisAdmin returns a whois description for admin users. func (i Identity) WhoisMaster() string {
func (i Identity) WhoisAdmin() string {
ip, _, _ := net.SplitHostPort(i.RemoteAddr().String()) ip, _, _ := net.SplitHostPort(i.RemoteAddr().String())
fingerprint := "(no public key)" fingerprint := "(no public key)"
if i.PublicKey() != nil { if i.PublicKey() != nil {