Merge branch 'master' into name-autocomplete
Conflicts: server.go
This commit is contained in:
commit
15a5052c33
|
@ -1,3 +1,4 @@
|
||||||
host_key
|
host_key
|
||||||
host_key.pub
|
host_key.pub
|
||||||
ssh-chat
|
ssh-chat
|
||||||
|
*.log
|
||||||
|
|
61
client.go
61
client.go
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -14,10 +15,10 @@ const (
|
||||||
MsgBuffer int = 50
|
MsgBuffer int = 50
|
||||||
|
|
||||||
// MaxMsgLength is the maximum length of a message
|
// MaxMsgLength is the maximum length of a message
|
||||||
MaxMsgLength int = 512
|
MaxMsgLength int = 1024
|
||||||
|
|
||||||
// HelpText is the text returned by /help
|
// HelpText is the text returned by /help
|
||||||
HelpText string = systemMessageFormat + `-> Available commands:
|
HelpText string = `Available commands:
|
||||||
/about - About this chat.
|
/about - About this chat.
|
||||||
/exit - Exit the chat.
|
/exit - Exit the chat.
|
||||||
/help - Show this help text.
|
/help - Show this help text.
|
||||||
|
@ -28,28 +29,28 @@ const (
|
||||||
/whois $NAME - Display information about another connected user.
|
/whois $NAME - Display information about another connected user.
|
||||||
/msg $NAME $MESSAGE - Sends a private message to a user.
|
/msg $NAME $MESSAGE - Sends a private message to a user.
|
||||||
/motd - Prints the Message of the Day.
|
/motd - Prints the Message of the Day.
|
||||||
/theme [color|mono] - Set client theme.` + Reset
|
/theme [color|mono] - Set client theme.`
|
||||||
|
|
||||||
// OpHelpText is the additional text returned by /help if the client is an Op
|
// OpHelpText is the additional text returned by /help if the client is an Op
|
||||||
OpHelpText string = systemMessageFormat + `-> Available operator commands:
|
OpHelpText string = `Available operator commands:
|
||||||
/ban $NAME - Banish a user from the chat
|
/ban $NAME - Banish a user from the chat
|
||||||
/kick $NAME - Kick em' out.
|
/kick $NAME - Kick em' out.
|
||||||
/op $NAME - Promote a user to server operator.
|
/op $NAME - Promote a user to server operator.
|
||||||
/silence $NAME - Revoke a user's ability to speak.
|
/silence $NAME - Revoke a user's ability to speak.
|
||||||
/shutdown $MESSAGE - Broadcast message and shutdown server.
|
/shutdown $MESSAGE - Broadcast message and shutdown server.
|
||||||
/motd $MESSAGE - Set message shown whenever somebody joins.
|
/motd $MESSAGE - Set message shown whenever somebody joins.
|
||||||
/whitelist $FINGERPRINT - Add fingerprint to whitelist, prevent anyone else from joining.` + Reset
|
/whitelist $FINGERPRINT - Add fingerprint to whitelist, prevent anyone else from joining.
|
||||||
|
/whitelist github.com/$USER - Add github user's pubkeys to whitelist.`
|
||||||
|
|
||||||
// AboutText is the text returned by /about
|
// AboutText is the text returned by /about
|
||||||
AboutText string = systemMessageFormat + `-> ssh-chat is made by @shazow.
|
AboutText string = `ssh-chat is made by @shazow.
|
||||||
|
|
||||||
It is a custom ssh server built in Go to serve a chat experience
|
It is a custom ssh server built in Go to serve a chat experience
|
||||||
instead of a shell.
|
instead of a shell.
|
||||||
|
|
||||||
Source: https://github.com/shazow/ssh-chat
|
Source: https://github.com/shazow/ssh-chat
|
||||||
|
|
||||||
For more, visit shazow.net or follow at twitter.com/shazow
|
For more, visit shazow.net or follow at twitter.com/shazow`
|
||||||
` + Reset
|
|
||||||
|
|
||||||
// RequiredWait is the time a client is required to wait between messages
|
// RequiredWait is the time a client is required to wait between messages
|
||||||
RequiredWait time.Duration = time.Second / 2
|
RequiredWait time.Duration = time.Second / 2
|
||||||
|
@ -71,6 +72,8 @@ type Client struct {
|
||||||
lastTX time.Time
|
lastTX time.Time
|
||||||
beepMe bool
|
beepMe bool
|
||||||
colorMe bool
|
colorMe bool
|
||||||
|
closed bool
|
||||||
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient constructs a new client
|
// NewClient constructs a new client
|
||||||
|
@ -94,7 +97,7 @@ func (c *Client) ColoredName() string {
|
||||||
|
|
||||||
// SysMsg sends a message in continuous format over the message channel
|
// SysMsg sends a message in continuous format over the message channel
|
||||||
func (c *Client) SysMsg(msg string, args ...interface{}) {
|
func (c *Client) SysMsg(msg string, args ...interface{}) {
|
||||||
c.Msg <- ContinuousFormat(systemMessageFormat, "-> "+fmt.Sprintf(msg, args...))
|
c.Send(ContinuousFormat(systemMessageFormat, "-> "+fmt.Sprintf(msg, args...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes the given message
|
// Write writes the given message
|
||||||
|
@ -114,7 +117,7 @@ func (c *Client) WriteLines(msg []string) {
|
||||||
|
|
||||||
// Send sends the given message
|
// Send sends the given message
|
||||||
func (c *Client) Send(msg string) {
|
func (c *Client) Send(msg string) {
|
||||||
if len(msg) > MaxMsgLength {
|
if len(msg) > MaxMsgLength || c.closed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
@ -170,6 +173,9 @@ func (c *Client) Rename(name string) {
|
||||||
|
|
||||||
// Fingerprint returns the fingerprint
|
// Fingerprint returns the fingerprint
|
||||||
func (c *Client) Fingerprint() string {
|
func (c *Client) Fingerprint() string {
|
||||||
|
if c.Conn.Permissions == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return c.Conn.Permissions.Extensions["fingerprint"]
|
return c.Conn.Permissions.Extensions["fingerprint"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,8 +196,9 @@ func (c *Client) handleShell(channel ssh.Channel) {
|
||||||
go func() {
|
go func() {
|
||||||
// Block until done, then remove.
|
// Block until done, then remove.
|
||||||
c.Conn.Wait()
|
c.Conn.Wait()
|
||||||
close(c.Msg)
|
c.closed = true
|
||||||
c.Server.Remove(c)
|
c.Server.Remove(c)
|
||||||
|
close(c.Msg)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -218,14 +225,14 @@ func (c *Client) handleShell(channel ssh.Channel) {
|
||||||
case "/exit":
|
case "/exit":
|
||||||
channel.Close()
|
channel.Close()
|
||||||
case "/help":
|
case "/help":
|
||||||
c.WriteLines(strings.Split(HelpText, "\n"))
|
c.SysMsg(strings.Replace(HelpText, "\n", "\r\n", -1))
|
||||||
if c.Server.IsOp(c) {
|
if c.Server.IsOp(c) {
|
||||||
c.WriteLines(strings.Split(OpHelpText, "\n"))
|
c.SysMsg(strings.Replace(OpHelpText, "\n", "\r\n", -1))
|
||||||
}
|
}
|
||||||
case "/about":
|
case "/about":
|
||||||
c.WriteLines(strings.Split(AboutText, "\n"))
|
c.SysMsg(strings.Replace(AboutText, "\n", "\r\n", -1))
|
||||||
case "/uptime":
|
case "/uptime":
|
||||||
c.Write(c.Server.Uptime())
|
c.SysMsg(c.Server.Uptime())
|
||||||
case "/beep":
|
case "/beep":
|
||||||
c.beepMe = !c.beepMe
|
c.beepMe = !c.beepMe
|
||||||
if c.beepMe {
|
if c.beepMe {
|
||||||
|
@ -255,7 +262,7 @@ func (c *Client) handleShell(channel ssh.Channel) {
|
||||||
c.SysMsg("Missing $NAME from: /nick $NAME")
|
c.SysMsg("Missing $NAME from: /nick $NAME")
|
||||||
}
|
}
|
||||||
case "/whois":
|
case "/whois":
|
||||||
if len(parts) == 2 {
|
if len(parts) >= 2 {
|
||||||
client := c.Server.Who(parts[1])
|
client := c.Server.Who(parts[1])
|
||||||
if client != nil {
|
if client != nil {
|
||||||
version := reStripText.ReplaceAllString(string(client.Conn.ClientVersion()), "")
|
version := reStripText.ReplaceAllString(string(client.Conn.ClientVersion()), "")
|
||||||
|
@ -415,8 +422,14 @@ func (c *Client) handleShell(channel ssh.Channel) {
|
||||||
c.SysMsg("Missing $FINGERPRINT from: /whitelist $FINGERPRINT")
|
c.SysMsg("Missing $FINGERPRINT from: /whitelist $FINGERPRINT")
|
||||||
} else {
|
} else {
|
||||||
fingerprint := parts[1]
|
fingerprint := parts[1]
|
||||||
c.Server.Whitelist(fingerprint)
|
go func() {
|
||||||
c.SysMsg("Added %s to the whitelist", fingerprint)
|
err = c.Server.Whitelist(fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
c.SysMsg("Error adding to whitelist: %s", err)
|
||||||
|
} else {
|
||||||
|
c.SysMsg("Added %s to the whitelist", fingerprint)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
2
motd.txt
2
motd.txt
|
@ -1 +1 @@
|
||||||
[39;05;91mWelcome to chat.shazow.net, enter /help for more. [0m
|
[39;91mWelcome to chat.shazow.net, enter /help for more. [0m
|
||||||
|
|
101
server.go
101
server.go
|
@ -1,9 +1,12 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -41,7 +44,7 @@ type Server struct {
|
||||||
admins map[string]struct{} // fingerprint lookup
|
admins map[string]struct{} // fingerprint lookup
|
||||||
bannedPK map[string]*time.Time // fingerprint lookup
|
bannedPK map[string]*time.Time // fingerprint lookup
|
||||||
started time.Time
|
started time.Time
|
||||||
sync.Mutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer constructs a new server
|
// NewServer constructs a new server
|
||||||
|
@ -77,6 +80,15 @@ func NewServer(privateKey []byte) (*Server, error) {
|
||||||
perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": fingerprint}}
|
perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": fingerprint}}
|
||||||
return perm, nil
|
return perm, nil
|
||||||
},
|
},
|
||||||
|
KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||||
|
if server.IsBanned("") {
|
||||||
|
return nil, fmt.Errorf("Interactive login disabled.")
|
||||||
|
}
|
||||||
|
if !server.IsWhitelisted("") {
|
||||||
|
return nil, fmt.Errorf("Not Whitelisted.")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
config.AddHostKey(signer)
|
config.AddHostKey(signer)
|
||||||
|
|
||||||
|
@ -100,6 +112,9 @@ func (s *Server) Broadcast(msg string, except *Client) {
|
||||||
logger.Debugf("Broadcast to %d: %s", s.Len(), msg)
|
logger.Debugf("Broadcast to %d: %s", s.Len(), msg)
|
||||||
s.history.Add(msg)
|
s.history.Add(msg)
|
||||||
|
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
for _, client := range s.clients {
|
for _, client := range s.clients {
|
||||||
if except != nil && client == except {
|
if except != nil && client == except {
|
||||||
continue
|
continue
|
||||||
|
@ -133,9 +148,7 @@ func (s *Server) Privmsg(nick, message string, sender *Client) error {
|
||||||
|
|
||||||
// SetMotd sets the Message of the Day (MOTD)
|
// SetMotd sets the Message of the Day (MOTD)
|
||||||
func (s *Server) SetMotd(motd string) {
|
func (s *Server) SetMotd(motd string) {
|
||||||
s.Lock()
|
|
||||||
s.motd = motd
|
s.motd = motd
|
||||||
s.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MotdUnicast sends the MOTD as a SysMsg
|
// MotdUnicast sends the MOTD as a SysMsg
|
||||||
|
@ -209,12 +222,12 @@ func (s *Server) proposeName(name string) (string, error) {
|
||||||
|
|
||||||
// Rename renames the given client (user)
|
// Rename renames the given client (user)
|
||||||
func (s *Server) Rename(client *Client, newName string) {
|
func (s *Server) Rename(client *Client, newName string) {
|
||||||
s.Lock()
|
|
||||||
var oldName string
|
var oldName string
|
||||||
if strings.ToLower(newName) == strings.ToLower(client.Name) {
|
if strings.ToLower(newName) == strings.ToLower(client.Name) {
|
||||||
oldName = client.Name
|
oldName = client.Name
|
||||||
client.Rename(newName)
|
client.Rename(newName)
|
||||||
} else {
|
} else {
|
||||||
|
s.Lock()
|
||||||
newName, err := s.proposeName(newName)
|
newName, err := s.proposeName(newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.SysMsg("%s", err)
|
client.SysMsg("%s", err)
|
||||||
|
@ -236,6 +249,9 @@ func (s *Server) Rename(client *Client, newName string) {
|
||||||
func (s *Server) List(prefix *string) []string {
|
func (s *Server) List(prefix *string) []string {
|
||||||
r := []string{}
|
r := []string{}
|
||||||
|
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
for name, client := range s.clients {
|
for name, client := range s.clients {
|
||||||
if prefix != nil && !strings.HasPrefix(name, strings.ToLower(*prefix)) {
|
if prefix != nil && !strings.HasPrefix(name, strings.ToLower(*prefix)) {
|
||||||
continue
|
continue
|
||||||
|
@ -260,11 +276,84 @@ func (s *Server) Op(fingerprint string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whitelist adds the given fingerprint to the whitelist
|
// Whitelist adds the given fingerprint to the whitelist
|
||||||
func (s *Server) Whitelist(fingerprint string) {
|
func (s *Server) Whitelist(fingerprint string) error {
|
||||||
|
if fingerprint == "" {
|
||||||
|
return fmt.Errorf("Invalid fingerprint.")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(fingerprint, "github.com/") {
|
||||||
|
return s.whitelistIdentityURL(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.whitelistFingerprint(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) whitelistIdentityURL(user string) error {
|
||||||
|
logger.Infof("Adding github account %s to whitelist", user)
|
||||||
|
|
||||||
|
user = strings.Replace(user, "github.com/", "", -1)
|
||||||
|
keys, err := getGithubPubKeys(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return fmt.Errorf("No keys for github user %s", user)
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
fingerprint := Fingerprint(key)
|
||||||
|
s.whitelistFingerprint(fingerprint)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) whitelistFingerprint(fingerprint string) error {
|
||||||
logger.Infof("Adding whitelist: %s", fingerprint)
|
logger.Infof("Adding whitelist: %s", fingerprint)
|
||||||
s.Lock()
|
s.Lock()
|
||||||
s.whitelist[fingerprint] = struct{}{}
|
s.whitelist[fingerprint] = struct{}{}
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client for getting github pub keys
|
||||||
|
var client = http.Client{
|
||||||
|
Timeout: time.Duration(10 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an array of public keys for the given github user URL
|
||||||
|
func getGithubPubKeys(user string) ([]ssh.PublicKey, error) {
|
||||||
|
resp, err := client.Get("http://github.com/" + user + ".keys")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
pubs := []ssh.PublicKey{}
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
if text == "Not Found" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
splitKey := strings.SplitN(text, " ", -1)
|
||||||
|
|
||||||
|
// In case of malformated key
|
||||||
|
if len(splitKey) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyDecoded, err := base64.StdEncoding.DecodeString(splitKey[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := ssh.ParsePublicKey(bodyDecoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubs = append(pubs, pub)
|
||||||
|
}
|
||||||
|
return pubs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uptime returns the time since the server was started
|
// Uptime returns the time since the server was started
|
||||||
|
@ -409,9 +498,11 @@ func (s *Server) AutoCompleteFunction(line string, pos int, key rune) (newLine s
|
||||||
|
|
||||||
// Stop stops the server
|
// Stop stops the server
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
|
s.Lock()
|
||||||
for _, client := range s.clients {
|
for _, client := range s.clients {
|
||||||
client.Conn.Close()
|
client.Conn.Close()
|
||||||
}
|
}
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue