progress: refactor: Remove Identifier interface, reverse ownership of sshchat identity and message.User

This commit is contained in:
Andrey Petrov 2016-09-04 16:40:51 -04:00
parent f64b2a3607
commit 91718a511b
14 changed files with 211 additions and 228 deletions

View File

@ -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
}

View File

@ -11,7 +11,7 @@ func TestMessage(t *testing.T) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
} }
u := NewUser(SimpleID("foo")) u := NewUser("foo")
expected = "foo: hello" expected = "foo: hello"
actual = NewPublicMsg("hello", u).String() actual = NewPublicMsg("hello", u).String()
if actual != expected { if actual != expected {

View File

@ -1,8 +1,9 @@
package chat package message
import "regexp" import "regexp"
var reStripName = regexp.MustCompile("[^\\w.-]") var reStripName = regexp.MustCompile("[^\\w.-]")
const maxLength = 16 const maxLength = 16
// SanitizeName returns a name with only allowed characters and a reasonable length // SanitizeName returns a name with only allowed characters and a reasonable length
@ -15,10 +16,3 @@ func SanitizeName(s string) string {
s = s[:nameLength] s = s[:nameLength]
return s 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, "")
}

View File

@ -51,7 +51,7 @@ func TestTheme(t *testing.T) {
t.Errorf("Got: %q; Expected: %q", actual, expected) t.Errorf("Got: %q; Expected: %q", actual, expected)
} }
u := NewUser(SimpleID("foo")) u := NewUser("foo")
u.colorIdx = 4 u.colorIdx = 4
actual = colorTheme.ColorName(u) actual = colorTheme.ColorName(u)
expected = "\033[38;05;5mfoo\033[0m" expected = "\033[38;05;5mfoo\033[0m"

View File

@ -18,7 +18,6 @@ var ErrUserClosed = errors.New("user closed")
// 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
colorIdx int colorIdx int
joined time.Time joined time.Time
msg chan Message msg chan Message
@ -28,25 +27,26 @@ type User struct {
closeOnce sync.Once closeOnce sync.Once
mu sync.Mutex mu sync.Mutex
name string
config UserConfig config UserConfig
replyTo *User // Set when user gets a /msg, for replying. replyTo *User // Set when user gets a /msg, for replying.
} }
func NewUser(identity Identifier) *User { func NewUser(name string) *User {
u := User{ u := User{
Identifier: identity, name: name,
config: DefaultUserConfig, config: DefaultUserConfig,
joined: time.Now(), joined: time.Now(),
msg: make(chan Message, messageBuffer), msg: make(chan Message, messageBuffer),
done: make(chan struct{}), done: make(chan struct{}),
} }
u.setColorIdx(rand.Int()) u.setColorIdx(rand.Int())
return &u return &u
} }
func NewUserScreen(identity Identifier, screen io.WriteCloser) *User { func NewUserScreen(name string, screen io.WriteCloser) *User {
u := NewUser(identity) u := NewUser(name)
u.screen = screen u.screen = screen
return u return u
@ -64,10 +64,28 @@ func (u *User) SetConfig(cfg UserConfig) {
u.mu.Unlock() 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. // Rename the user with a new Identifier.
func (u *User) SetName(name string) { func (u *User) SetName(name string) {
u.Identifier.SetName(name) u.mu.Lock()
u.name = name
u.setColorIdx(rand.Int()) u.setColorIdx(rand.Int())
u.mu.Unlock()
} }
// ReplyTo returns the last user that messaged this user. // ReplyTo returns the last user that messaged this user.

View File

@ -9,7 +9,7 @@ func TestMakeUser(t *testing.T) {
var actual, expected []byte var actual, expected []byte
s := &MockScreen{} s := &MockScreen{}
u := NewUserScreen(SimpleID("foo"), s) u := NewUserScreen("foo", s)
m := NewAnnounceMsg("hello") m := NewAnnounceMsg("hello")
defer u.Close() defer u.Close()

View File

@ -154,7 +154,7 @@ func (r *Room) Join(m Member) (*roomMember, error) {
} }
// Leave the room as a user, will announce. Mostly used during setup. // 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()) err := r.Members.Remove(u.ID())
if err != nil { if err != nil {
return err 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. // 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() == "" { if u.ID() == "" {
return ErrInvalidName 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 // Member returns a corresponding Member object to a User if the Member is
// present in this room. // 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()) m, ok := r.MemberByID(u.ID())
if !ok { if !ok {
return nil, false 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. // 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()) return r.Ops.In(u.ID())
} }

View File

@ -58,7 +58,7 @@ func TestIgnore(t *testing.T) {
users := make([]ScreenedUser, 3) users := make([]ScreenedUser, 3)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
screen := &MockScreen{} 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{ users[i] = ScreenedUser{
user: user, user: user,
screen: screen, screen: screen,
@ -176,7 +176,7 @@ func TestRoomJoin(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := message.NewUserScreen(message.SimpleID("foo"), s) u := message.NewUserScreen("foo", s)
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()
@ -212,7 +212,7 @@ func TestRoomJoin(t *testing.T) {
} }
func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
u := message.NewUser(message.SimpleID("foo")) u := message.NewUser("foo")
u.SetConfig(message.UserConfig{ u.SetConfig(message.UserConfig{
Quiet: true, Quiet: true,
}) })
@ -251,7 +251,7 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
} }
func TestRoomQuietToggleBroadcasts(t *testing.T) { func TestRoomQuietToggleBroadcasts(t *testing.T) {
u := message.NewUser(message.SimpleID("foo")) u := message.NewUser("foo")
u.SetConfig(message.UserConfig{ u.SetConfig(message.UserConfig{
Quiet: true, Quiet: true,
}) })
@ -294,7 +294,7 @@ func TestQuietToggleDisplayState(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := message.NewUserScreen(message.SimpleID("foo"), s) u := message.NewUserScreen("foo", s)
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()
@ -335,7 +335,7 @@ func TestRoomNames(t *testing.T) {
var expected, actual []byte var expected, actual []byte
s := &MockScreen{} s := &MockScreen{}
u := message.NewUserScreen(message.SimpleID("foo"), s) u := message.NewUserScreen("foo", s)
ch := NewRoom() ch := NewRoom()
go ch.Serve() go ch.Serve()

View File

@ -10,7 +10,7 @@ import (
func TestSet(t *testing.T) { func TestSet(t *testing.T) {
var err error var err error
s := set.New() s := set.New()
u := message.NewUser(message.SimpleID("foo")) u := message.NewUser("foo")
if s.In(u.ID()) { if s.In(u.ID()) {
t.Errorf("Set should be empty.") t.Errorf("Set should be empty.")
@ -25,7 +25,7 @@ func TestSet(t *testing.T) {
t.Errorf("Set should contain user.") t.Errorf("Set should contain user.")
} }
u2 := message.NewUser(message.SimpleID("bar")) u2 := message.NewUser("bar")
err = s.Add(set.Itemize(u2.ID(), u2)) err = s.Add(set.Itemize(u2.ID(), u2))
if err != nil { if err != nil {
t.Error(err) t.Error(err)

45
client.go Normal file
View File

@ -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())
}

213
host.go
View File

@ -80,8 +80,8 @@ func (h *Host) isOp(conn sshd.Connection) bool {
// 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) {
ident := toIdentity(term.Conn) requestedName := term.Conn.Name()
user := message.NewUserScreen(ident, term) user := message.NewUserScreen(requestedName, term)
cfg := user.Config() cfg := user.Config()
cfg.Theme = &h.theme cfg.Theme = &h.theme
user.SetConfig(cfg) user.SetConfig(cfg)
@ -105,7 +105,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
member, err := h.Join(user) member, err := h.Join(user)
if err != nil { if err != nil {
// Try again... // Try again...
ident.SetName(fmt.Sprintf("Guest%d", count)) user.SetName(fmt.Sprintf("Guest%d", count))
member, err = h.Join(user) member, err = h.Join(user)
} }
if err != nil { if err != nil {
@ -331,34 +331,37 @@ func (h *Host) InitCommands(c *chat.Commands) {
}, },
}) })
c.Add(chat.Command{ // XXX: Temporarily disable whois
Prefix: "/whois", /*
PrefixHelp: "USER", c.Add(chat.Command{
Help: "Information about USER.", Prefix: "/whois",
Handler: func(room *chat.Room, msg message.CommandMsg) error { PrefixHelp: "USER",
args := msg.Args() Help: "Information about USER.",
if len(args) == 0 { Handler: func(room *chat.Room, msg message.CommandMsg) error {
return errors.New("must specify user") args := msg.Args()
} if len(args) == 0 {
return errors.New("must specify user")
}
target, ok := h.GetUser(args[0]) target, ok := h.GetUser(args[0])
if !ok { if !ok {
return errors.New("user not found") return errors.New("user not found")
} }
id := target.Identifier.(*identity) id := target.Identifier.(*identity)
var whois string var whois string
switch room.IsOp(msg.From()) { switch room.IsOp(msg.From()) {
case true: case true:
whois = id.WhoisAdmin() whois = id.WhoisAdmin()
case false: case false:
whois = id.Whois() whois = id.Whois()
} }
room.Send(message.NewSystemMsg(whois, msg.From())) room.Send(message.NewSystemMsg(whois, msg.From()))
return nil return nil
}, },
}) })
*/
// Hidden commands // Hidden commands
c.Add(chat.Command{ 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{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/kick", 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{ c.Add(chat.Command{
Op: true, Op: true,
Prefix: "/motd", 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
},
})
} }

View File

@ -27,7 +27,7 @@ func stripPrompt(s string) string {
func TestHostGetPrompt(t *testing.T) { func TestHostGetPrompt(t *testing.T) {
var expected, actual string var expected, actual string
u := message.NewUser(&identity{id: "foo"}) u := message.NewUser("foo")
actual = u.Prompt() actual = u.Prompt()
expected = "[foo] " expected = "[foo] "

View File

@ -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)
}

10
sanitize.go Normal file
View File

@ -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, "")
}