progress/broken: multiuser support hardcoded, by multiplexing terminals

/whois breaks, empty message breaks, prompt replication is fragile
This commit is contained in:
Andrey Petrov 2017-04-30 16:38:25 -04:00
parent 7b3818acc1
commit 76849c0d3d
5 changed files with 187 additions and 37 deletions

View File

@ -25,7 +25,7 @@ $(KEY):
ssh-keygen -f $(KEY) -P '' ssh-keygen -f $(KEY) -P ''
run: $(BINARY) $(KEY) run: $(BINARY) $(KEY)
./$(BINARY) -i $(KEY) --bind ":$(PORT)" -vv ./$(BINARY) -i $(KEY) --bind "127.0.0.1:$(PORT)" -vv
debug: $(BINARY) $(KEY) debug: $(BINARY) $(KEY)
./$(BINARY) --pprof 6060 -i $(KEY) --bind ":$(PORT)" -vv ./$(BINARY) --pprof 6060 -i $(KEY) --bind ":$(PORT)" -vv

View File

@ -1,6 +1,7 @@
package sshchat package sshchat
import ( import (
"io"
"sync" "sync"
"time" "time"
@ -9,22 +10,17 @@ import (
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
type multiTerm interface {
Connections() []sshd.Connection
Add(*sshd.Terminal)
ReadLine() (string, error)
io.WriteCloser
}
type client struct { type client struct {
Member Member
sync.Mutex sync.Mutex
conns []sshd.Connection multiTerm
}
func (cl *client) Connections() []sshd.Connection {
return cl.conns
}
func (cl *client) Close() error {
// TODO: Stack errors?
for _, conn := range cl.conns {
conn.Close()
}
return nil
} }
type Member interface { type Member interface {

57
host.go
View File

@ -32,10 +32,9 @@ type Host struct {
// Default theme // Default theme
theme message.Theme theme message.Theme
mu sync.Mutex mu sync.Mutex
motd string motd string
count int count int
clients map[chat.Member][]client
} }
// NewHost creates a Host on top of an existing listener. // NewHost creates a Host on top of an existing listener.
@ -46,7 +45,6 @@ func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
listener: listener, listener: listener,
commands: chat.Commands{}, commands: chat.Commands{},
auth: auth, auth: auth,
clients: map[chat.Member][]client{},
} }
// Make our own commands registry instance. // Make our own commands registry instance.
@ -72,15 +70,30 @@ func (h *Host) SetMotd(motd string) {
h.mu.Unlock() h.mu.Unlock()
} }
var globalUser *client
// 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(t *sshd.Terminal) {
requestedName := term.Conn.Name() // XXX: Hack to test multiple users per key
screen := message.BufferedScreen(requestedName, term) if globalUser != nil {
user := &client{ globalUser.Add(t)
Member: screen, return
conns: []sshd.Connection{term.Conn},
} }
conn := t.Conn
remoteAddr := conn.RemoteAddr()
requestedName := conn.Name()
term := sshd.MultiTerm(t)
screen := message.BufferedScreen(requestedName, term)
user := &client{
Member: screen,
multiTerm: term,
}
defer user.Close()
// XXX: Hack to test multiple users per key
globalUser = user
h.mu.Lock() h.mu.Lock()
motd := h.motd motd := h.motd
count := h.count count := h.count
@ -91,10 +104,6 @@ func (h *Host) Connect(term *sshd.Terminal) {
cfg.Theme = &h.theme cfg.Theme = &h.theme
user.SetConfig(cfg) user.SetConfig(cfg)
// Close term once user is closed.
defer screen.Close()
defer term.Close()
go screen.Consume() go screen.Consume()
// Send MOTD // Send MOTD
@ -109,7 +118,7 @@ func (h *Host) Connect(term *sshd.Terminal) {
member, err = h.Join(user) member, err = h.Join(user)
} }
if err != nil { if err != nil {
logger.Errorf("[%s] Failed to join: %s", term.Conn.RemoteAddr(), err) logger.Errorf("[%s] Failed to join: %s", conn.RemoteAddr(), err)
return return
} }
@ -118,27 +127,29 @@ func (h *Host) Connect(term *sshd.Terminal) {
term.AutoCompleteCallback = h.AutoCompleteFunction(user) term.AutoCompleteCallback = h.AutoCompleteFunction(user)
user.SetHighlight(user.Name()) user.SetHighlight(user.Name())
// XXX: Mark multiterm as ready?
// Should the user be op'd on join? // Should the user be op'd on join?
if key := term.Conn.PublicKey(); key != nil { if key := conn.PublicKey(); key != nil {
authItem, err := h.auth.ops.Get(newAuthKey(key)) authItem, err := h.auth.ops.Get(newAuthKey(key))
if err == nil { if err == nil {
err = h.Room.Ops.Add(set.Rename(authItem, member.ID())) err = h.Room.Ops.Add(set.Rename(authItem, member.ID()))
} }
} }
if err != nil { if err != nil {
logger.Warningf("[%s] Failed to op: %s", term.Conn.RemoteAddr(), err) logger.Warningf("[%s] Failed to op: %s", remoteAddr, err)
} }
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", remoteAddr, user.Name())
for { for {
line, err := term.ReadLine() line, err := user.ReadLine()
if err == io.EOF { if err == io.EOF {
// Closed // Closed
break break
} else if err != nil { } else if err != nil {
logger.Errorf("[%s] Terminal reading error: %s", term.Conn.RemoteAddr(), err) logger.Errorf("[%s] Terminal reading error: %s", remoteAddr, err)
break break
} }
@ -175,10 +186,10 @@ func (h *Host) Connect(term *sshd.Terminal) {
err = h.Leave(user) err = h.Leave(user)
if err != nil { if err != nil {
logger.Errorf("[%s] Failed to leave: %s", term.Conn.RemoteAddr(), err) logger.Errorf("[%s] Failed to leave: %s", remoteAddr, err)
return return
} }
logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) logger.Debugf("[%s] Leaving: %s", remoteAddr, user.Name())
} }
// Serve our chat room onto the listener // Serve our chat room onto the listener

131
sshd/multiterm.go Normal file
View File

@ -0,0 +1,131 @@
package sshd
import (
"fmt"
"io"
"sync"
)
type termLine struct {
Term *Terminal
Line string
Err error
}
func MultiTerm(terms ...*Terminal) *multiTerm {
mt := &multiTerm{
lines: make(chan termLine),
}
for _, t := range terms {
mt.Add(t)
}
return mt
}
type multiTerm struct {
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
mu sync.Mutex
terms []*Terminal
add chan *Terminal
lines chan termLine
prompt string
}
func (mt *multiTerm) SetPrompt(prompt string) {
mt.mu.Lock()
mt.prompt = prompt
mt.mu.Unlock()
for _, t := range mt.Terminals() {
t.SetPrompt(prompt)
}
}
func (mt *multiTerm) Connections() []Connection {
terms := mt.Terminals()
conns := make([]Connection, len(terms))
for _, term := range terms {
conns = append(conns, term.Conn)
}
return conns
}
func (mt *multiTerm) Terminals() []*Terminal {
mt.mu.Lock()
terms := mt.terms
mt.mu.Unlock()
return terms
}
func (mt *multiTerm) Add(t *Terminal) {
mt.mu.Lock()
mt.terms = append(mt.terms, t)
prompt := mt.prompt
mt.mu.Unlock()
t.AutoCompleteCallback = mt.AutoCompleteCallback
t.SetPrompt(prompt)
go func() {
var line termLine
for {
line.Line, line.Err = t.ReadLine()
line.Term = t
mt.lines <- line
if line.Err != nil {
// FIXME: Should we not abort on all errors?
break
}
}
}()
}
func (mt *multiTerm) ReadLine() (string, error) {
line := <-mt.lines
mt.mu.Lock()
prompt := mt.prompt
mt.mu.Unlock()
if line.Err == nil {
// Write the line to all the other terminals
for _, w := range mt.Terminals() {
if w == line.Term {
continue
}
// XXX: This is super hacky and frankly wrong.
w.Write([]byte(prompt + line.Line + "\n\r"))
// TODO: Remove terminal if it fails to write?
}
}
return line.Line, line.Err
}
func (mt *multiTerm) Write(p []byte) (n int, err error) {
for _, w := range mt.Terminals() {
n, err = w.Write(p)
if err != nil {
return
}
if n != len(p) {
err = io.ErrShortWrite
return
}
}
return len(p), nil
}
func (mt *multiTerm) Close() error {
mt.mu.Lock()
var errs []error
for _, t := range mt.terms {
if err := t.Close(); err != nil {
errs = append(errs, err)
}
}
mt.terms = nil
mt.mu.Unlock()
if len(errs) == 0 {
return nil
}
return fmt.Errorf("%d errors: %q", len(errs), errs)
}

12
util.go Normal file
View File

@ -0,0 +1,12 @@
package sshchat
import "fmt"
type multiError []error
func (err multiError) Error() string {
if len(err) == 0 {
return ""
}
return fmt.Sprintf("%d errors: %q", len(err), err)
}