1package sshchat
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"io"
8	"strings"
9	"sync"
10	"time"
11
12	"github.com/shazow/rateio"
13	"github.com/shazow/ssh-chat/chat"
14	"github.com/shazow/ssh-chat/chat/message"
15	"github.com/shazow/ssh-chat/internal/humantime"
16	"github.com/shazow/ssh-chat/internal/sanitize"
17	"github.com/shazow/ssh-chat/sshd"
18)
19
20const maxInputLength int = 1024
21
22// GetPrompt will render the terminal prompt string based on the user.
23func GetPrompt(user *message.User) string {
24	name := user.Name()
25	cfg := user.Config()
26	if cfg.Theme != nil {
27		name = cfg.Theme.ColorName(user)
28	}
29	return fmt.Sprintf("[%s] ", name)
30}
31
32// Host is the bridge between sshd and chat modules
33// TODO: Should be easy to add support for multiple rooms, if we want.
34type Host struct {
35	*chat.Room
36	listener *sshd.SSHListener
37	commands chat.Commands
38	auth     *Auth
39
40	// Version string to print on /version
41	Version string
42
43	// Default theme
44	theme message.Theme
45
46	mu    sync.Mutex
47	motd  string
48	count int
49
50	// GetMOTD is used to reload the motd from an external source
51	GetMOTD func() (string, error)
52}
53
54// NewHost creates a Host on top of an existing listener.
55func NewHost(listener *sshd.SSHListener, auth *Auth) *Host {
56	room := chat.NewRoom()
57	h := Host{
58		Room:     room,
59		listener: listener,
60		commands: chat.Commands{},
61		auth:     auth,
62	}
63
64	// Make our own commands registry instance.
65	chat.InitCommands(&h.commands)
66	h.InitCommands(&h.commands)
67	room.SetCommands(h.commands)
68
69	go room.Serve()
70	return &h
71}
72
73// SetTheme sets the default theme for the host.
74func (h *Host) SetTheme(theme message.Theme) {
75	h.mu.Lock()
76	h.theme = theme
77	h.mu.Unlock()
78}
79
80// SetMotd sets the host's message of the day.
81// TODO: Change to SetMOTD
82func (h *Host) SetMotd(motd string) {
83	h.mu.Lock()
84	h.motd = motd
85	h.mu.Unlock()
86}
87
88func (h *Host) isOp(conn sshd.Connection) bool {
89	key := conn.PublicKey()
90	if key == nil {
91		return false
92	}
93	return h.auth.IsOp(key)
94}
95
96// Connect a specific Terminal to this host and its room.
97func (h *Host) Connect(term *sshd.Terminal) {
98	id := NewIdentity(term.Conn)
99	user := message.NewUserScreen(id, term)
100	user.OnChange = func() {
101		term.SetPrompt(GetPrompt(user))
102		user.SetHighlight(user.ID())
103	}
104	cfg := user.Config()
105
106	apiMode := strings.ToLower(term.Term()) == "bot"
107
108	if apiMode {
109		cfg.Theme = message.MonoTheme
110		cfg.Echo = false
111	} else {
112		term.SetEnterClear(true) // We provide our own echo rendering
113		cfg.Theme = &h.theme
114	}
115
116	user.SetConfig(cfg)
117
118	// Load user config overrides from ENV
119	// TODO: Would be nice to skip the command parsing pipeline just to load
120	// config values. Would need to factor out some command handler logic into
121	// accessible helpers.
122	env := term.Env()
123	for _, e := range env {
124		switch e.Key {
125		case "SSHCHAT_TIMESTAMP":
126			if e.Value != "" && e.Value != "0" {
127				cmd := "/timestamp"
128				if e.Value != "1" {
129					cmd += " " + e.Value
130				}
131				if msg, ok := message.NewPublicMsg(cmd, user).ParseCommand(); ok {
132					h.Room.HandleMsg(msg)
133				}
134			}
135		case "SSHCHAT_THEME":
136			cmd := "/theme " + e.Value
137			if msg, ok := message.NewPublicMsg(cmd, user).ParseCommand(); ok {
138				h.Room.HandleMsg(msg)
139			}
140		}
141	}
142
143	go user.Consume()
144
145	// Close term once user is closed.
146	defer user.Close()
147	defer term.Close()
148
149	h.mu.Lock()
150	motd := h.motd
151	count := h.count
152	h.count++
153	h.mu.Unlock()
154
155	// Send MOTD
156	if motd != "" {
157		user.Send(message.NewAnnounceMsg(motd))
158	}
159
160	member, err := h.Join(user)
161	if err != nil {
162		// Try again...
163		id.SetName(fmt.Sprintf("Guest%d", count))
164		member, err = h.Join(user)
165	}
166	if err != nil {
167		logger.Errorf("[%s] Failed to join: %s", term.Conn.RemoteAddr(), err)
168		return
169	}
170
171	// Successfully joined.
172	if !apiMode {
173		term.SetPrompt(GetPrompt(user))
174		term.AutoCompleteCallback = h.AutoCompleteFunction(user)
175		user.SetHighlight(user.Name())
176	}
177
178	// Should the user be op'd on join?
179	if h.isOp(term.Conn) {
180		member.IsOp = true
181	}
182	ratelimit := rateio.NewSimpleLimiter(3, time.Second*3)
183
184	logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name())
185
186	for {
187		line, err := term.ReadLine()
188		if err == io.EOF {
189			// Closed
190			break
191		} else if err != nil {
192			logger.Errorf("[%s] Terminal reading error: %s", term.Conn.RemoteAddr(), err)
193			break
194		}
195
196		err = ratelimit.Count(1)
197		if err != nil {
198			user.Send(message.NewSystemMsg("Message rejected: Rate limiting is in effect.", user))
199			continue
200		}
201		if len(line) > maxInputLength {
202			user.Send(message.NewSystemMsg("Message rejected: Input too long.", user))
203			continue
204		}
205		if line == "" {
206			// Silently ignore empty lines.
207			term.Write([]byte{})
208			continue
209		}
210
211		m := message.ParseInput(line, user)
212
213		if !apiMode {
214			if m, ok := m.(*message.CommandMsg); ok {
215				// Other messages render themselves by the room, commands we'll
216				// have to re-echo ourselves manually.
217				user.HandleMsg(m)
218			}
219		}
220
221		// FIXME: Any reason to use h.room.Send(m) instead?
222		h.HandleMsg(m)
223
224		if apiMode {
225			// Skip the remaining rendering workarounds
226			continue
227		}
228	}
229
230	err = h.Leave(user)
231	if err != nil {
232		logger.Errorf("[%s] Failed to leave: %s", term.Conn.RemoteAddr(), err)
233		return
234	}
235	logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name())
236}
237
238// Serve our chat room onto the listener
239func (h *Host) Serve() {
240	h.listener.HandlerFunc = h.Connect
241	h.listener.Serve()
242}
243
244func (h *Host) completeName(partial string, skipName string) string {
245	names := h.NamesPrefix(partial)
246	if len(names) == 0 {
247		// Didn't find anything
248		return ""
249	} else if name := names[0]; name != skipName {
250		// First name is not the skipName, great
251		return name
252	} else if len(names) > 1 {
253		// Next candidate
254		return names[1]
255	}
256	return ""
257}
258
259func (h *Host) completeCommand(partial string) string {
260	for cmd := range h.commands {
261		if strings.HasPrefix(cmd, partial) {
262			return cmd
263		}
264	}
265	return ""
266}
267
268// AutoCompleteFunction returns a callback for terminal autocompletion
269func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
270	return func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
271		if key != 9 {
272			return
273		}
274
275		if line == "" || strings.HasSuffix(line[:pos], " ") {
276			// Don't autocomplete spaces.
277			return
278		}
279
280		fields := strings.Fields(line[:pos])
281		isFirst := len(fields) < 2
282		partial := ""
283		if len(fields) > 0 {
284			partial = fields[len(fields)-1]
285		}
286		posPartial := pos - len(partial)
287
288		var completed string
289		if isFirst && strings.HasPrefix(line, "/") {
290			// Command
291			completed = h.completeCommand(partial)
292			if completed == "/reply" {
293				replyTo := u.ReplyTo()
294				if replyTo != nil {
295					name := replyTo.ID()
296					_, found := h.GetUser(name)
297					if found {
298						completed = "/msg " + name
299					} else {
300						u.SetReplyTo(nil)
301					}
302				}
303			}
304		} else {
305			// Name
306			completed = h.completeName(partial, u.Name())
307			if completed == "" {
308				return
309			}
310			if isFirst {
311				completed += ":"
312			}
313		}
314		completed += " "
315
316		// Reposition the cursor
317		newLine = strings.Replace(line[posPartial:], partial, completed, 1)
318		newLine = line[:posPartial] + newLine
319		newPos = pos + (len(completed) - len(partial))
320		ok = true
321		return
322	}
323}
324
325// GetUser returns a message.User based on a name.
326func (h *Host) GetUser(name string) (*message.User, bool) {
327	m, ok := h.MemberByID(name)
328	if !ok {
329		return nil, false
330	}
331	return m.User, true
332}
333
334// InitCommands adds host-specific commands to a Commands container. These will
335// override any existing commands.
336func (h *Host) InitCommands(c *chat.Commands) {
337	c.Add(chat.Command{
338		Prefix:     "/msg",
339		PrefixHelp: "USER MESSAGE",
340		Help:       "Send MESSAGE to USER.",
341		Handler: func(room *chat.Room, msg message.CommandMsg) error {
342			args := msg.Args()
343			switch len(args) {
344			case 0:
345				return errors.New("must specify user")
346			case 1:
347				return errors.New("must specify message")
348			}
349
350			target, ok := h.GetUser(args[0])
351			if !ok {
352				return errors.New("user not found")
353			}
354
355			m := message.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target)
356			room.Send(&m)
357
358			txt := fmt.Sprintf("[Sent PM to %s]", target.Name())
359			ms := message.NewSystemMsg(txt, msg.From())
360			room.Send(ms)
361			target.SetReplyTo(msg.From())
362			return nil
363		},
364	})
365
366	c.Add(chat.Command{
367		Prefix:     "/reply",
368		PrefixHelp: "MESSAGE",
369		Help:       "Reply with MESSAGE to the previous private message.",
370		Handler: func(room *chat.Room, msg message.CommandMsg) error {
371			args := msg.Args()
372			switch len(args) {
373			case 0:
374				return errors.New("must specify message")
375			}
376
377			target := msg.From().ReplyTo()
378			if target == nil {
379				return errors.New("no message to reply to")
380			}
381
382			name := target.Name()
383			_, found := h.GetUser(name)
384			if !found {
385				return errors.New("user not found")
386			}
387
388			m := message.NewPrivateMsg(strings.Join(args, " "), msg.From(), target)
389			room.Send(&m)
390
391			txt := fmt.Sprintf("[Sent PM to %s]", name)
392			ms := message.NewSystemMsg(txt, msg.From())
393			room.Send(ms)
394			target.SetReplyTo(msg.From())
395			return nil
396		},
397	})
398
399	c.Add(chat.Command{
400		Prefix:     "/whois",
401		PrefixHelp: "USER",
402		Help:       "Information about USER.",
403		Handler: func(room *chat.Room, msg message.CommandMsg) error {
404			args := msg.Args()
405			if len(args) == 0 {
406				return errors.New("must specify user")
407			}
408
409			target, ok := h.GetUser(args[0])
410			if !ok {
411				return errors.New("user not found")
412			}
413			id := target.Identifier.(*Identity)
414			var whois string
415			switch room.IsOp(msg.From()) {
416			case true:
417				whois = id.WhoisAdmin(room)
418			case false:
419				whois = id.Whois()
420			}
421			room.Send(message.NewSystemMsg(whois, msg.From()))
422
423			return nil
424		},
425	})
426
427	// Hidden commands
428	c.Add(chat.Command{
429		Prefix: "/version",
430		Handler: func(room *chat.Room, msg message.CommandMsg) error {
431			room.Send(message.NewSystemMsg(h.Version, msg.From()))
432			return nil
433		},
434	})
435
436	timeStarted := time.Now()
437	c.Add(chat.Command{
438		Prefix: "/uptime",
439		Handler: func(room *chat.Room, msg message.CommandMsg) error {
440			room.Send(message.NewSystemMsg(humantime.Since(timeStarted), msg.From()))
441			return nil
442		},
443	})
444
445	// Op commands
446	c.Add(chat.Command{
447		Op:         true,
448		Prefix:     "/kick",
449		PrefixHelp: "USER",
450		Help:       "Kick USER from the server.",
451		Handler: func(room *chat.Room, msg message.CommandMsg) error {
452			if !room.IsOp(msg.From()) {
453				return errors.New("must be op")
454			}
455
456			args := msg.Args()
457			if len(args) == 0 {
458				return errors.New("must specify user")
459			}
460
461			target, ok := h.GetUser(args[0])
462			if !ok {
463				return errors.New("user not found")
464			}
465
466			body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name())
467			room.Send(message.NewAnnounceMsg(body))
468			target.Close()
469			return nil
470		},
471	})
472
473	c.Add(chat.Command{
474		Op:         true,
475		Prefix:     "/ban",
476		PrefixHelp: "QUERY [DURATION]",
477		Help:       "Ban from the server. QUERY can be a username to ban the fingerprint and ip, or quoted \"key=value\" pairs with keys like ip, fingerprint, client.",
478		Handler: func(room *chat.Room, msg message.CommandMsg) error {
479			// TODO: Would be nice to specify what to ban. Key? Ip? etc.
480			if !room.IsOp(msg.From()) {
481				return errors.New("must be op")
482			}
483
484			args := msg.Args()
485			if len(args) == 0 {
486				return errors.New("must specify user")
487			}
488
489			query := args[0]
490			target, ok := h.GetUser(query)
491			if !ok {
492				query = strings.Join(args, " ")
493				if strings.Contains(query, "=") {
494					return h.auth.BanQuery(query)
495				}
496				return errors.New("user not found")
497			}
498
499			var until time.Duration
500			if len(args) > 1 {
501				until, _ = time.ParseDuration(args[1])
502			}
503
504			id := target.Identifier.(*Identity)
505			h.auth.Ban(id.PublicKey(), until)
506			h.auth.BanAddr(id.RemoteAddr(), until)
507
508			body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name())
509			room.Send(message.NewAnnounceMsg(body))
510			target.Close()
511
512			logger.Debugf("Banned: \n-> %s", id.Whois())
513
514			return nil
515		},
516	})
517
518	c.Add(chat.Command{
519		Op:     true,
520		Prefix: "/banned",
521		Help:   "List the current ban conditions.",
522		Handler: func(room *chat.Room, msg message.CommandMsg) error {
523			if !room.IsOp(msg.From()) {
524				return errors.New("must be op")
525			}
526
527			bannedIPs, bannedFingerprints, bannedClients := h.auth.Banned()
528
529			buf := bytes.Buffer{}
530			fmt.Fprintf(&buf, "Banned:")
531			for _, key := range bannedIPs {
532				fmt.Fprintf(&buf, "\n   \"ip=%s\"", key)
533			}
534			for _, key := range bannedFingerprints {
535				fmt.Fprintf(&buf, "\n   \"fingerprint=%s\"", key)
536			}
537			for _, key := range bannedClients {
538				fmt.Fprintf(&buf, "\n   \"client=%s\"", key)
539			}
540
541			room.Send(message.NewSystemMsg(buf.String(), msg.From()))
542
543			return nil
544		},
545	})
546
547	c.Add(chat.Command{
548		Op:         true,
549		Prefix:     "/motd",
550		PrefixHelp: "[MESSAGE]",
551		Help:       "Set a new MESSAGE of the day, or print the motd if no MESSAGE.",
552		Handler: func(room *chat.Room, msg message.CommandMsg) error {
553			args := msg.Args()
554			user := msg.From()
555
556			h.mu.Lock()
557			motd := h.motd
558			h.mu.Unlock()
559
560			if len(args) == 0 {
561				room.Send(message.NewSystemMsg(motd, user))
562				return nil
563			}
564			if !room.IsOp(user) {
565				return errors.New("must be OP to modify the MOTD")
566			}
567
568			var err error
569			var s string = strings.Join(args, " ")
570
571			if s == "@" {
572				if h.GetMOTD == nil {
573					return errors.New("motd reload not set")
574				}
575				if s, err = h.GetMOTD(); err != nil {
576					return err
577				}
578			}
579
580			h.SetMotd(s)
581			fromMsg := fmt.Sprintf("New message of the day set by %s:", msg.From().Name())
582			room.Send(message.NewAnnounceMsg(fromMsg + message.Newline + "-> " + s))
583
584			return nil
585		},
586	})
587
588	c.Add(chat.Command{
589		Op:         true,
590		Prefix:     "/op",
591		PrefixHelp: "USER [DURATION|remove]",
592		Help:       "Set USER as admin. Duration only applies to pubkey reconnects.",
593		Handler: func(room *chat.Room, msg message.CommandMsg) error {
594			if !room.IsOp(msg.From()) {
595				return errors.New("must be op")
596			}
597
598			args := msg.Args()
599			if len(args) == 0 {
600				return errors.New("must specify user")
601			}
602
603			opValue := true
604			var until time.Duration
605			if len(args) > 1 {
606				if args[1] == "remove" {
607					// Expire instantly
608					until = time.Duration(1)
609					opValue = false
610				} else {
611					until, _ = time.ParseDuration(args[1])
612				}
613			}
614
615			member, ok := room.MemberByID(args[0])
616			if !ok {
617				return errors.New("user not found")
618			}
619			member.IsOp = opValue
620
621			id := member.Identifier.(*Identity)
622			h.auth.Op(id.PublicKey(), until)
623
624			var body string
625			if opValue {
626				body = fmt.Sprintf("Made op by %s.", msg.From().Name())
627			} else {
628				body = fmt.Sprintf("Removed op by %s.", msg.From().Name())
629			}
630			room.Send(message.NewSystemMsg(body, member.User))
631
632			return nil
633		},
634	})
635
636	c.Add(chat.Command{
637		Op:         true,
638		Prefix:     "/rename",
639		PrefixHelp: "USER NEW_NAME [SYMBOL]",
640		Help:       "Rename USER to NEW_NAME, add optional SYMBOL prefix",
641		Handler: func(room *chat.Room, msg message.CommandMsg) error {
642			if !room.IsOp(msg.From()) {
643				return errors.New("must be op")
644			}
645
646			args := msg.Args()
647			if len(args) < 2 {
648				return errors.New("must specify user and new name")
649			}
650
651			member, ok := room.MemberByID(args[0])
652			if !ok {
653				return errors.New("user not found")
654			}
655
656			symbolSet := false
657			if len(args) == 3 {
658				s := args[2]
659				if id, ok := member.Identifier.(*Identity); ok {
660					id.SetSymbol(s)
661				} else {
662					return errors.New("user does not support setting symbol")
663				}
664
665				body := fmt.Sprintf("Assigned symbol %q by %s.", s, msg.From().Name())
666				room.Send(message.NewSystemMsg(body, member.User))
667				symbolSet = true
668			}
669
670			oldID := member.ID()
671			newID := sanitize.Name(args[1])
672			if newID == oldID && !symbolSet {
673				return errors.New("new name is the same as the original")
674			} else if (newID == "" || newID == oldID) && symbolSet {
675				if member.User.OnChange != nil {
676					member.User.OnChange()
677				}
678				return nil
679			}
680
681			member.SetID(newID)
682			err := room.Rename(oldID, member)
683			if err != nil {
684				member.SetID(oldID)
685				return err
686			}
687
688			body := fmt.Sprintf("%s was renamed by %s.", oldID, msg.From().Name())
689			room.Send(message.NewAnnounceMsg(body))
690
691			return nil
692		},
693	})
694}
695