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)
}
u := NewUser(SimpleID("foo"))
u := NewUser("foo")
expected = "foo: hello"
actual = NewPublicMsg("hello", u).String()
if actual != expected {

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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.
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
},
})
}

View File

@ -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] "

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