From 91718a511b7e977a5bcf347c1cad563de7a788fb Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sun, 4 Sep 2016 16:40:51 -0400 Subject: [PATCH] progress: refactor: Remove Identifier interface, reverse ownership of sshchat identity and message.User --- chat/message/identity.go | 26 ---- chat/message/message_test.go | 2 +- chat/{ => message}/sanitize.go | 10 +- chat/message/theme_test.go | 2 +- chat/message/user.go | 38 ++++-- chat/message/user_test.go | 2 +- chat/room.go | 8 +- chat/room_test.go | 12 +- chat/set_test.go | 4 +- client.go | 45 +++++++ host.go | 213 +++++++++++++++++---------------- host_test.go | 2 +- identity.go | 65 ---------- sanitize.go | 10 ++ 14 files changed, 211 insertions(+), 228 deletions(-) delete mode 100644 chat/message/identity.go rename chat/{ => message}/sanitize.go (59%) create mode 100644 client.go delete mode 100644 identity.go create mode 100644 sanitize.go diff --git a/chat/message/identity.go b/chat/message/identity.go deleted file mode 100644 index 5b3b18c..0000000 --- a/chat/message/identity.go +++ /dev/null @@ -1,26 +0,0 @@ -package message - -// Identifier is an interface that can uniquely identify itself. -type Identifier interface { - ID() string - Name() string - SetName(string) -} - -// SimpleID is a simple Identifier implementation used for testing. -type SimpleID string - -// ID returns the ID as a string. -func (i SimpleID) ID() string { - return string(i) -} - -// Name returns the ID -func (i SimpleID) Name() string { - return i.ID() -} - -// SetName is a no-op -func (i SimpleID) SetName(s string) { - // no-op -} diff --git a/chat/message/message_test.go b/chat/message/message_test.go index d7ef285..cd0e3a3 100644 --- a/chat/message/message_test.go +++ b/chat/message/message_test.go @@ -11,7 +11,7 @@ func TestMessage(t *testing.T) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } - u := NewUser(SimpleID("foo")) + u := NewUser("foo") expected = "foo: hello" actual = NewPublicMsg("hello", u).String() if actual != expected { diff --git a/chat/sanitize.go b/chat/message/sanitize.go similarity index 59% rename from chat/sanitize.go rename to chat/message/sanitize.go index b567825..fc13873 100644 --- a/chat/sanitize.go +++ b/chat/message/sanitize.go @@ -1,8 +1,9 @@ -package chat +package message import "regexp" var reStripName = regexp.MustCompile("[^\\w.-]") + const maxLength = 16 // SanitizeName returns a name with only allowed characters and a reasonable length @@ -15,10 +16,3 @@ func SanitizeName(s string) string { s = s[:nameLength] return s } - -var reStripData = regexp.MustCompile("[^[:ascii:]]") - -// SanitizeData returns a string with only allowed characters for client-provided metadata inputs. -func SanitizeData(s string) string { - return reStripData.ReplaceAllString(s, "") -} diff --git a/chat/message/theme_test.go b/chat/message/theme_test.go index a62c20e..d3d4577 100644 --- a/chat/message/theme_test.go +++ b/chat/message/theme_test.go @@ -51,7 +51,7 @@ func TestTheme(t *testing.T) { t.Errorf("Got: %q; Expected: %q", actual, expected) } - u := NewUser(SimpleID("foo")) + u := NewUser("foo") u.colorIdx = 4 actual = colorTheme.ColorName(u) expected = "\033[38;05;5mfoo\033[0m" diff --git a/chat/message/user.go b/chat/message/user.go index 46bfb52..d10d02b 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -18,7 +18,6 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { - Identifier colorIdx int joined time.Time msg chan Message @@ -28,25 +27,26 @@ type User struct { closeOnce sync.Once mu sync.Mutex + name string config UserConfig replyTo *User // Set when user gets a /msg, for replying. } -func NewUser(identity Identifier) *User { +func NewUser(name string) *User { u := User{ - Identifier: identity, - config: DefaultUserConfig, - joined: time.Now(), - msg: make(chan Message, messageBuffer), - done: make(chan struct{}), + name: name, + config: DefaultUserConfig, + joined: time.Now(), + msg: make(chan Message, messageBuffer), + done: make(chan struct{}), } u.setColorIdx(rand.Int()) return &u } -func NewUserScreen(identity Identifier, screen io.WriteCloser) *User { - u := NewUser(identity) +func NewUserScreen(name string, screen io.WriteCloser) *User { + u := NewUser(name) u.screen = screen return u @@ -64,10 +64,28 @@ func (u *User) SetConfig(cfg UserConfig) { u.mu.Unlock() } +func (u *User) ID() string { + u.mu.Lock() + defer u.mu.Unlock() + return SanitizeName(u.name) +} + +func (u *User) Name() string { + u.mu.Lock() + defer u.mu.Unlock() + return u.name +} + +func (u *User) Joined() time.Time { + return u.joined +} + // Rename the user with a new Identifier. func (u *User) SetName(name string) { - u.Identifier.SetName(name) + u.mu.Lock() + u.name = name u.setColorIdx(rand.Int()) + u.mu.Unlock() } // ReplyTo returns the last user that messaged this user. diff --git a/chat/message/user_test.go b/chat/message/user_test.go index f70e674..871730d 100644 --- a/chat/message/user_test.go +++ b/chat/message/user_test.go @@ -9,7 +9,7 @@ func TestMakeUser(t *testing.T) { var actual, expected []byte s := &MockScreen{} - u := NewUserScreen(SimpleID("foo"), s) + u := NewUserScreen("foo", s) m := NewAnnounceMsg("hello") defer u.Close() diff --git a/chat/room.go b/chat/room.go index a7a70c0..1bf85db 100644 --- a/chat/room.go +++ b/chat/room.go @@ -154,7 +154,7 @@ func (r *Room) Join(m Member) (*roomMember, error) { } // Leave the room as a user, will announce. Mostly used during setup. -func (r *Room) Leave(u message.Identifier) error { +func (r *Room) Leave(u Member) error { err := r.Members.Remove(u.ID()) if err != nil { return err @@ -166,7 +166,7 @@ func (r *Room) Leave(u message.Identifier) error { } // Rename member with a new identity. This will not call rename on the member. -func (r *Room) Rename(oldID string, u message.Identifier) error { +func (r *Room) Rename(oldID string, u Member) error { if u.ID() == "" { return ErrInvalidName } @@ -182,7 +182,7 @@ func (r *Room) Rename(oldID string, u message.Identifier) error { // Member returns a corresponding Member object to a User if the Member is // present in this room. -func (r *Room) Member(u message.Identifier) (*roomMember, bool) { +func (r *Room) Member(u Member) (*roomMember, bool) { m, ok := r.MemberByID(u.ID()) if !ok { return nil, false @@ -203,7 +203,7 @@ func (r *Room) MemberByID(id string) (*roomMember, bool) { } // IsOp returns whether a user is an operator in this room. -func (r *Room) IsOp(u message.Identifier) bool { +func (r *Room) IsOp(u Member) bool { return r.Ops.In(u.ID()) } diff --git a/chat/room_test.go b/chat/room_test.go index 5f4b02f..d2ed523 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -58,7 +58,7 @@ func TestIgnore(t *testing.T) { users := make([]ScreenedUser, 3) for i := 0; i < 3; i++ { screen := &MockScreen{} - user := message.NewUserScreen(message.SimpleID(fmt.Sprintf("user%d", i)), screen) + user := message.NewUserScreen(fmt.Sprintf("user%d", i), screen) users[i] = ScreenedUser{ user: user, screen: screen, @@ -176,7 +176,7 @@ func TestRoomJoin(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -212,7 +212,7 @@ func TestRoomJoin(t *testing.T) { } func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -251,7 +251,7 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { } func TestRoomQuietToggleBroadcasts(t *testing.T) { - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -294,7 +294,7 @@ func TestQuietToggleDisplayState(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -335,7 +335,7 @@ func TestRoomNames(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen(message.SimpleID("foo"), s) + u := message.NewUserScreen("foo", s) ch := NewRoom() go ch.Serve() diff --git a/chat/set_test.go b/chat/set_test.go index 54955f6..61722a8 100644 --- a/chat/set_test.go +++ b/chat/set_test.go @@ -10,7 +10,7 @@ import ( func TestSet(t *testing.T) { var err error s := set.New() - u := message.NewUser(message.SimpleID("foo")) + u := message.NewUser("foo") if s.In(u.ID()) { t.Errorf("Set should be empty.") @@ -25,7 +25,7 @@ func TestSet(t *testing.T) { t.Errorf("Set should contain user.") } - u2 := message.NewUser(message.SimpleID("bar")) + u2 := message.NewUser("bar") err = s.Add(set.Itemize(u2.ID(), u2)) if err != nil { t.Error(err) diff --git a/client.go b/client.go new file mode 100644 index 0000000..5776cac --- /dev/null +++ b/client.go @@ -0,0 +1,45 @@ +package sshchat + +import ( + "net" + "time" + + humanize "github.com/dustin/go-humanize" + "github.com/shazow/ssh-chat/chat/message" + "github.com/shazow/ssh-chat/sshd" +) + +type Client struct { + sshd.Connection + message.User + + connected time.Time +} + +// Whois returns a whois description for non-admin users. +func (client Client) Whois() string { + conn, u := client.Connection, client.User + fingerprint := "(no public key)" + if conn.PublicKey() != nil { + fingerprint = sshd.Fingerprint(conn.PublicKey()) + } + return "name: " + u.Name() + message.Newline + + " > fingerprint: " + fingerprint + message.Newline + + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + + " > joined: " + humanize.Time(u.Joined()) +} + +// WhoisAdmin returns a whois description for admin users. +func (client Client) WhoisAdmin() string { + conn, u := client.Connection, client.User + ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) + fingerprint := "(no public key)" + if conn.PublicKey() != nil { + fingerprint = sshd.Fingerprint(conn.PublicKey()) + } + return "name: " + u.Name() + message.Newline + + " > ip: " + ip + message.Newline + + " > fingerprint: " + fingerprint + message.Newline + + " > client: " + SanitizeData(string(conn.ClientVersion())) + message.Newline + + " > joined: " + humanize.Time(u.Joined()) +} diff --git a/host.go b/host.go index ca3425d..55f7bdc 100644 --- a/host.go +++ b/host.go @@ -80,8 +80,8 @@ func (h *Host) isOp(conn sshd.Connection) bool { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { - ident := toIdentity(term.Conn) - user := message.NewUserScreen(ident, term) + requestedName := term.Conn.Name() + user := message.NewUserScreen(requestedName, term) cfg := user.Config() cfg.Theme = &h.theme user.SetConfig(cfg) @@ -105,7 +105,7 @@ func (h *Host) Connect(term *sshd.Terminal) { member, err := h.Join(user) if err != nil { // Try again... - ident.SetName(fmt.Sprintf("Guest%d", count)) + user.SetName(fmt.Sprintf("Guest%d", count)) member, err = h.Join(user) } if err != nil { @@ -331,34 +331,37 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - c.Add(chat.Command{ - Prefix: "/whois", - PrefixHelp: "USER", - Help: "Information about USER.", - Handler: func(room *chat.Room, msg message.CommandMsg) error { - args := msg.Args() - if len(args) == 0 { - return errors.New("must specify user") - } + // XXX: Temporarily disable whois + /* + c.Add(chat.Command{ + Prefix: "/whois", + PrefixHelp: "USER", + Help: "Information about 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") - } + target, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } - id := target.Identifier.(*identity) - var whois string - switch room.IsOp(msg.From()) { - case true: - whois = id.WhoisAdmin() - case false: - whois = id.Whois() - } - room.Send(message.NewSystemMsg(whois, msg.From())) + id := target.Identifier.(*identity) + var whois string + switch room.IsOp(msg.From()) { + case true: + whois = id.WhoisAdmin() + case false: + whois = id.Whois() + } + room.Send(message.NewSystemMsg(whois, msg.From())) - return nil - }, - }) + return nil + }, + }) + */ // Hidden commands c.Add(chat.Command{ @@ -378,7 +381,85 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - // Op commands + // XXX: Temporarily disable op and ban + /* + + c.Add(chat.Command{ + Op: true, + Prefix: "/op", + PrefixHelp: "USER [DURATION]", + Help: "Set USER as admin.", + Handler: func(room *chat.Room, msg message.CommandMsg) error { + if !room.IsOp(msg.From()) { + return errors.New("must be op") + } + + args := msg.Args() + if len(args) == 0 { + return errors.New("must specify user") + } + + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } + + user, ok := h.GetUser(args[0]) + if !ok { + return errors.New("user not found") + } + room.Ops.Add(set.Keyize(user.ID())) + + h.auth.Op(user.Identifier.(*identity).PublicKey(), until) + + body := fmt.Sprintf("Made op by %s.", msg.From().Name()) + room.Send(message.NewSystemMsg(body, user)) + + return nil + }, + }) + + // Op commands + c.Add(chat.Command{ + Op: true, + Prefix: "/ban", + PrefixHelp: "USER [DURATION]", + Help: "Ban USER from the server.", + Handler: func(room *chat.Room, msg message.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") + } + + var until time.Duration = 0 + if len(args) > 1 { + until, _ = time.ParseDuration(args[1]) + } + + id := target.Identifier.(*identity) + h.auth.Ban(id.PublicKey(), until) + h.auth.BanAddr(id.RemoteAddr(), until) + + body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) + room.Send(message.NewAnnounceMsg(body)) + target.Close() + + logger.Debugf("Banned: \n-> %s", id.Whois()) + + return nil + }, + }) + */ c.Add(chat.Command{ Op: true, Prefix: "/kick", @@ -406,46 +487,6 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - c.Add(chat.Command{ - Op: true, - Prefix: "/ban", - PrefixHelp: "USER [DURATION]", - Help: "Ban USER from the server.", - Handler: func(room *chat.Room, msg message.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") - } - - var until time.Duration = 0 - if len(args) > 1 { - until, _ = time.ParseDuration(args[1]) - } - - id := target.Identifier.(*identity) - h.auth.Ban(id.PublicKey(), until) - h.auth.BanAddr(id.RemoteAddr(), until) - - body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) - room.Send(message.NewAnnounceMsg(body)) - target.Close() - - logger.Debugf("Banned: \n-> %s", id.Whois()) - - return nil - }, - }) - c.Add(chat.Command{ Op: true, Prefix: "/motd", @@ -476,38 +517,4 @@ func (h *Host) InitCommands(c *chat.Commands) { }, }) - c.Add(chat.Command{ - Op: true, - Prefix: "/op", - PrefixHelp: "USER [DURATION]", - Help: "Set USER as admin.", - Handler: func(room *chat.Room, msg message.CommandMsg) error { - if !room.IsOp(msg.From()) { - return errors.New("must be op") - } - - args := msg.Args() - if len(args) == 0 { - return errors.New("must specify user") - } - - var until time.Duration = 0 - if len(args) > 1 { - until, _ = time.ParseDuration(args[1]) - } - - user, ok := h.GetUser(args[0]) - if !ok { - return errors.New("user not found") - } - room.Ops.Add(set.Keyize(user.ID())) - - h.auth.Op(user.Identifier.(*identity).PublicKey(), until) - - body := fmt.Sprintf("Made op by %s.", msg.From().Name()) - room.Send(message.NewSystemMsg(body, user)) - - return nil - }, - }) } diff --git a/host_test.go b/host_test.go index 405792e..784c521 100644 --- a/host_test.go +++ b/host_test.go @@ -27,7 +27,7 @@ func stripPrompt(s string) string { func TestHostGetPrompt(t *testing.T) { var expected, actual string - u := message.NewUser(&identity{id: "foo"}) + u := message.NewUser("foo") actual = u.Prompt() expected = "[foo] " diff --git a/identity.go b/identity.go deleted file mode 100644 index b33cb42..0000000 --- a/identity.go +++ /dev/null @@ -1,65 +0,0 @@ -package sshchat - -import ( - "net" - "time" - - "github.com/dustin/go-humanize" - "github.com/shazow/ssh-chat/chat" - "github.com/shazow/ssh-chat/chat/message" - "github.com/shazow/ssh-chat/sshd" -) - -// Identity is a container for everything that identifies a client. -type identity struct { - sshd.Connection - id string - created time.Time -} - -// Converts an sshd.Connection to an identity. -func toIdentity(conn sshd.Connection) *identity { - return &identity{ - Connection: conn, - id: chat.SanitizeName(conn.Name()), - created: time.Now(), - } -} - -func (i identity) ID() string { - return i.id -} - -func (i *identity) SetName(name string) { - i.id = chat.SanitizeName(name) -} - -func (i identity) Name() string { - return i.id -} - -// Whois returns a whois description for non-admin users. -func (i identity) Whois() string { - fingerprint := "(no public key)" - if i.PublicKey() != nil { - fingerprint = sshd.Fingerprint(i.PublicKey()) - } - return "name: " + i.Name() + message.Newline + - " > fingerprint: " + fingerprint + message.Newline + - " > client: " + chat.SanitizeData(string(i.ClientVersion())) + message.Newline + - " > joined: " + humanize.Time(i.created) -} - -// WhoisAdmin returns a whois description for admin users. -func (i identity) WhoisAdmin() string { - ip, _, _ := net.SplitHostPort(i.RemoteAddr().String()) - fingerprint := "(no public key)" - if i.PublicKey() != nil { - fingerprint = sshd.Fingerprint(i.PublicKey()) - } - return "name: " + i.Name() + message.Newline + - " > ip: " + ip + message.Newline + - " > fingerprint: " + fingerprint + message.Newline + - " > client: " + chat.SanitizeData(string(i.ClientVersion())) + message.Newline + - " > joined: " + humanize.Time(i.created) -} diff --git a/sanitize.go b/sanitize.go new file mode 100644 index 0000000..130caa3 --- /dev/null +++ b/sanitize.go @@ -0,0 +1,10 @@ +package sshchat + +import "regexp" + +var reStripData = regexp.MustCompile("[^[:ascii:]]") + +// SanitizeData returns a string with only allowed characters for client-provided metadata inputs. +func SanitizeData(s string) string { + return reStripData.ReplaceAllString(s, "") +}