mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-17 01:12:23 +03:00
I have heard this should be fairly well supported, so I'm hoping these colors will all show up nicely for everyone.
301 lines
7.1 KiB
Go
301 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const MSG_BUFFER int = 10
|
|
|
|
const HELP_TEXT string = SYSTEM_MESSAGE_FORMAT + `
|
|
-> Available commands:
|
|
/about
|
|
/exit
|
|
/help
|
|
/list
|
|
/nick $NAME
|
|
/whois $NAME
|
|
` + RESET
|
|
|
|
const ABOUT_TEXT string = SYSTEM_MESSAGE_FORMAT + `
|
|
-> ssh-chat is made by @shazow.
|
|
|
|
It is a custom ssh server built in Go to serve a chat experience
|
|
instead of a shell.
|
|
|
|
Source: https://github.com/shazow/ssh-chat
|
|
|
|
For more, visit shazow.net or follow at twitter.com/shazow
|
|
` + RESET
|
|
|
|
type Client struct {
|
|
Server *Server
|
|
Conn *ssh.ServerConn
|
|
Msg chan string
|
|
Name string
|
|
Color string
|
|
Op bool
|
|
ready chan struct{}
|
|
term *terminal.Terminal
|
|
termWidth int
|
|
termHeight int
|
|
silencedUntil time.Time
|
|
}
|
|
|
|
func NewClient(server *Server, conn *ssh.ServerConn) *Client {
|
|
return &Client{
|
|
Server: server,
|
|
Conn: conn,
|
|
Name: conn.User(),
|
|
Color: RandomColor256(),
|
|
Msg: make(chan string, MSG_BUFFER),
|
|
ready: make(chan struct{}, 1),
|
|
}
|
|
}
|
|
|
|
func (c *Client) ColoredName() string {
|
|
return ColorString(c.Color, c.Name)
|
|
}
|
|
|
|
func (c *Client) SysMsg(msg string, args ...interface{}) {
|
|
c.Msg <- ContinuousFormat(SYSTEM_MESSAGE_FORMAT, "-> " + fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
func (c *Client) SysMsg2(msg string, args ...interface{}) {
|
|
c.Write(ContinuousFormat(SYSTEM_MESSAGE_FORMAT, "-> " + fmt.Sprintf(msg, args...)))
|
|
}
|
|
|
|
func (c *Client) Write(msg string) {
|
|
c.term.Write([]byte(msg + "\r\n"))
|
|
}
|
|
|
|
func (c *Client) WriteLines(msg []string) {
|
|
for _, line := range msg {
|
|
c.Write(line)
|
|
}
|
|
}
|
|
|
|
func (c *Client) IsSilenced() bool {
|
|
return c.silencedUntil.After(time.Now())
|
|
}
|
|
|
|
func (c *Client) Silence(d time.Duration) {
|
|
c.silencedUntil = time.Now().Add(d)
|
|
}
|
|
|
|
func (c *Client) Resize(width int, height int) error {
|
|
err := c.term.SetSize(width, height)
|
|
if err != nil {
|
|
logger.Errorf("Resize failed: %dx%d", width, height)
|
|
return err
|
|
}
|
|
c.termWidth, c.termHeight = width, height
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Rename(name string) {
|
|
c.Name = name
|
|
c.term.SetPrompt(fmt.Sprintf("[%s] ", c.ColoredName()))
|
|
}
|
|
|
|
func (c *Client) Fingerprint() string {
|
|
return c.Conn.Permissions.Extensions["fingerprint"]
|
|
}
|
|
|
|
func (c *Client) handleShell(channel ssh.Channel) {
|
|
defer channel.Close()
|
|
|
|
// FIXME: This shouldn't live here, need to restructure the call chaining.
|
|
c.Server.Add(c)
|
|
go func() {
|
|
// Block until done, then remove.
|
|
c.Conn.Wait()
|
|
c.Server.Remove(c)
|
|
}()
|
|
|
|
go func() {
|
|
for msg := range c.Msg {
|
|
c.Write(msg)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
line, err := c.term.ReadLine()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
parts := strings.SplitN(line, " ", 3)
|
|
isCmd := strings.HasPrefix(parts[0], "/")
|
|
|
|
if isCmd {
|
|
// TODO: Factor this out.
|
|
switch parts[0] {
|
|
case "/test-colors": // Shh, this command is a secret!
|
|
c.Write(ColorString("32", "Lorem ipsum dolor sit amet,"))
|
|
c.Write("consectetur " + ColorString("31;1", "adipiscing") + " elit.")
|
|
case "/exit":
|
|
channel.Close()
|
|
case "/help":
|
|
c.WriteLines(strings.Split(HELP_TEXT, "\n"))
|
|
case "/about":
|
|
c.WriteLines(strings.Split(ABOUT_TEXT, "\n"))
|
|
case "/me":
|
|
me := strings.TrimLeft(line, "/me")
|
|
if me == "" {
|
|
me = " is at a loss for words."
|
|
}
|
|
msg := fmt.Sprintf("** %s%s", c.ColoredName(), me)
|
|
if c.IsSilenced() || len(msg) > 1000 {
|
|
c.SysMsg("Message rejected.")
|
|
} else {
|
|
c.Server.Broadcast(msg, nil)
|
|
}
|
|
case "/nick":
|
|
if len(parts) == 2 {
|
|
c.Server.Rename(c, parts[1])
|
|
} else {
|
|
c.SysMsg("Missing $NAME from: /nick $NAME")
|
|
}
|
|
case "/whois":
|
|
if len(parts) == 2 {
|
|
client := c.Server.Who(parts[1])
|
|
if client != nil {
|
|
version := RE_STRIP_TEXT.ReplaceAllString(string(client.Conn.ClientVersion()), "")
|
|
if len(version) > 100 {
|
|
version = "Evil Jerk with a superlong string"
|
|
}
|
|
c.SysMsg("%s is %s via %s", client.ColoredName(), client.Fingerprint(), version)
|
|
} else {
|
|
c.SysMsg("No such name: %s", parts[1])
|
|
}
|
|
} else {
|
|
c.SysMsg("Missing $NAME from: /whois $NAME")
|
|
}
|
|
case "/list":
|
|
names := c.Server.List(nil)
|
|
c.SysMsg("%d connected: %s", len(names), strings.Join(names, ", "))
|
|
case "/ban":
|
|
if !c.Server.IsOp(c) {
|
|
c.SysMsg("You're not an admin.")
|
|
} else if len(parts) != 2 {
|
|
c.SysMsg("Missing $NAME from: /ban $NAME")
|
|
} else {
|
|
client := c.Server.Who(parts[1])
|
|
if client == nil {
|
|
c.SysMsg("No such name: %s", parts[1])
|
|
} else {
|
|
fingerprint := client.Fingerprint()
|
|
client.SysMsg2("Banned by %s.", c.ColoredName())
|
|
c.Server.Ban(fingerprint, nil)
|
|
client.Conn.Close()
|
|
c.Server.Broadcast(fmt.Sprintf("* %s was banned by %s", parts[1], c.ColoredName()), nil)
|
|
}
|
|
}
|
|
case "/op":
|
|
if !c.Server.IsOp(c) {
|
|
c.SysMsg("You're not an admin.")
|
|
} else if len(parts) != 2 {
|
|
c.SysMsg("Missing $NAME from: /op $NAME")
|
|
} else {
|
|
client := c.Server.Who(parts[1])
|
|
if client == nil {
|
|
c.SysMsg("No such name: %s", parts[1])
|
|
} else {
|
|
fingerprint := client.Fingerprint()
|
|
client.SysMsg2("Made op by %s.", c.ColoredName())
|
|
c.Server.Op(fingerprint)
|
|
}
|
|
}
|
|
case "/silence":
|
|
if !c.Server.IsOp(c) {
|
|
c.SysMsg("You're not an admin.")
|
|
} else if len(parts) < 2 {
|
|
c.SysMsg("Missing $NAME from: /silence $NAME")
|
|
} else {
|
|
duration := time.Duration(5) * time.Minute
|
|
if len(parts) >= 3 {
|
|
parsedDuration, err := time.ParseDuration(parts[2])
|
|
if err == nil {
|
|
duration = parsedDuration
|
|
}
|
|
}
|
|
client := c.Server.Who(parts[1])
|
|
if client == nil {
|
|
c.SysMsg("No such name: %s", parts[1])
|
|
} else {
|
|
client.Silence(duration)
|
|
client.SysMsg2("Silenced for %s by %s.", duration, c.ColoredName())
|
|
}
|
|
}
|
|
default:
|
|
c.SysMsg("Invalid command: %s", line)
|
|
}
|
|
continue
|
|
}
|
|
|
|
msg := fmt.Sprintf("%s: %s", c.ColoredName(), line)
|
|
if c.IsSilenced() || len(msg) > 1000 {
|
|
c.SysMsg("Message rejected.")
|
|
continue
|
|
}
|
|
c.Server.Broadcast(msg, c)
|
|
}
|
|
|
|
}
|
|
|
|
func (c *Client) handleChannels(channels <-chan ssh.NewChannel) {
|
|
prompt := fmt.Sprintf("[%s] ", c.ColoredName())
|
|
|
|
hasShell := false
|
|
|
|
for ch := range channels {
|
|
if t := ch.ChannelType(); t != "session" {
|
|
ch.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t))
|
|
continue
|
|
}
|
|
|
|
channel, requests, err := ch.Accept()
|
|
if err != nil {
|
|
logger.Errorf("Could not accept channel: %v", err)
|
|
continue
|
|
}
|
|
defer channel.Close()
|
|
|
|
c.term = terminal.NewTerminal(channel, prompt)
|
|
for req := range requests {
|
|
var width, height int
|
|
var ok bool
|
|
|
|
switch req.Type {
|
|
case "shell":
|
|
if c.term != nil && !hasShell {
|
|
go c.handleShell(channel)
|
|
ok = true
|
|
hasShell = true
|
|
}
|
|
case "pty-req":
|
|
width, height, ok = parsePtyRequest(req.Payload)
|
|
if ok {
|
|
err := c.Resize(width, height)
|
|
ok = err == nil
|
|
}
|
|
case "window-change":
|
|
width, height, ok = parseWinchRequest(req.Payload)
|
|
if ok {
|
|
err := c.Resize(width, height)
|
|
ok = err == nil
|
|
}
|
|
}
|
|
|
|
if req.WantReply {
|
|
req.Reply(ok, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|