diff --git a/.gitignore b/.gitignore index 1bec925..6207059 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ host_key host_key.pub ssh-chat +*.log diff --git a/client.go b/client.go index f3d886b..2c1e0d8 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package main import ( "fmt" "strings" + "sync" "time" "golang.org/x/crypto/ssh" @@ -14,10 +15,10 @@ const ( MsgBuffer int = 50 // MaxMsgLength is the maximum length of a message - MaxMsgLength int = 512 + MaxMsgLength int = 1024 // HelpText is the text returned by /help - HelpText string = systemMessageFormat + `-> Available commands: + HelpText string = `Available commands: /about - About this chat. /exit - Exit the chat. /help - Show this help text. @@ -28,28 +29,28 @@ const ( /whois $NAME - Display information about another connected user. /msg $NAME $MESSAGE - Sends a private message to a user. /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 string = systemMessageFormat + `-> Available operator commands: - /ban $NAME - Banish a user from the chat - /kick $NAME - Kick em' out. - /op $NAME - Promote a user to server operator. - /silence $NAME - Revoke a user's ability to speak. - /shutdown $MESSAGE - Broadcast message and shutdown server. - /motd $MESSAGE - Set message shown whenever somebody joins. - /whitelist $FINGERPRINT - Add fingerprint to whitelist, prevent anyone else from joining.` + Reset + OpHelpText string = `Available operator commands: + /ban $NAME - Banish a user from the chat + /kick $NAME - Kick em' out. + /op $NAME - Promote a user to server operator. + /silence $NAME - Revoke a user's ability to speak. + /shutdown $MESSAGE - Broadcast message and shutdown server. + /motd $MESSAGE - Set message shown whenever somebody joins. + /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 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 instead of a shell. Source: https://github.com/shazow/ssh-chat - For more, visit shazow.net or follow at twitter.com/shazow -` + Reset + For more, visit shazow.net or follow at twitter.com/shazow` // RequiredWait is the time a client is required to wait between messages RequiredWait time.Duration = time.Second / 2 @@ -71,6 +72,8 @@ type Client struct { lastTX time.Time beepMe bool colorMe bool + closed bool + sync.RWMutex } // 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 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 @@ -114,7 +117,7 @@ func (c *Client) WriteLines(msg []string) { // Send sends the given message func (c *Client) Send(msg string) { - if len(msg) > MaxMsgLength { + if len(msg) > MaxMsgLength || c.closed { return } select { @@ -170,6 +173,9 @@ func (c *Client) Rename(name string) { // Fingerprint returns the fingerprint func (c *Client) Fingerprint() string { + if c.Conn.Permissions == nil { + return "" + } return c.Conn.Permissions.Extensions["fingerprint"] } @@ -190,8 +196,9 @@ func (c *Client) handleShell(channel ssh.Channel) { go func() { // Block until done, then remove. c.Conn.Wait() - close(c.Msg) + c.closed = true c.Server.Remove(c) + close(c.Msg) }() go func() { @@ -218,14 +225,14 @@ func (c *Client) handleShell(channel ssh.Channel) { case "/exit": channel.Close() case "/help": - c.WriteLines(strings.Split(HelpText, "\n")) + c.SysMsg(strings.Replace(HelpText, "\n", "\r\n", -1)) if c.Server.IsOp(c) { - c.WriteLines(strings.Split(OpHelpText, "\n")) + c.SysMsg(strings.Replace(OpHelpText, "\n", "\r\n", -1)) } case "/about": - c.WriteLines(strings.Split(AboutText, "\n")) + c.SysMsg(strings.Replace(AboutText, "\n", "\r\n", -1)) case "/uptime": - c.Write(c.Server.Uptime()) + c.SysMsg(c.Server.Uptime()) case "/beep": c.beepMe = !c.beepMe if c.beepMe { @@ -255,7 +262,7 @@ func (c *Client) handleShell(channel ssh.Channel) { c.SysMsg("Missing $NAME from: /nick $NAME") } case "/whois": - if len(parts) == 2 { + if len(parts) >= 2 { client := c.Server.Who(parts[1]) if client != nil { 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") } else { fingerprint := parts[1] - c.Server.Whitelist(fingerprint) - c.SysMsg("Added %s to the whitelist", fingerprint) + go func() { + 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: diff --git a/motd.txt b/motd.txt index ac8395e..da0f972 100644 --- a/motd.txt +++ b/motd.txt @@ -1 +1 @@ -Welcome to chat.shazow.net, enter /help for more.  \ No newline at end of file +Welcome to chat.shazow.net, enter /help for more.  diff --git a/server.go b/server.go index ca57dee..181d2f7 100644 --- a/server.go +++ b/server.go @@ -1,9 +1,12 @@ package main import ( + "bufio" "crypto/md5" + "encoding/base64" "fmt" "net" + "net/http" "regexp" "strings" "sync" @@ -41,7 +44,7 @@ type Server struct { admins map[string]struct{} // fingerprint lookup bannedPK map[string]*time.Time // fingerprint lookup started time.Time - sync.Mutex + sync.RWMutex } // NewServer constructs a new server @@ -77,6 +80,15 @@ func NewServer(privateKey []byte) (*Server, error) { perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": fingerprint}} 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) @@ -100,6 +112,9 @@ func (s *Server) Broadcast(msg string, except *Client) { logger.Debugf("Broadcast to %d: %s", s.Len(), msg) s.history.Add(msg) + s.RLock() + defer s.RUnlock() + for _, client := range s.clients { if except != nil && client == except { continue @@ -133,9 +148,7 @@ func (s *Server) Privmsg(nick, message string, sender *Client) error { // SetMotd sets the Message of the Day (MOTD) func (s *Server) SetMotd(motd string) { - s.Lock() s.motd = motd - s.Unlock() } // 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) func (s *Server) Rename(client *Client, newName string) { - s.Lock() var oldName string if strings.ToLower(newName) == strings.ToLower(client.Name) { oldName = client.Name client.Rename(newName) } else { + s.Lock() newName, err := s.proposeName(newName) if err != nil { client.SysMsg("%s", err) @@ -236,6 +249,9 @@ func (s *Server) Rename(client *Client, newName string) { func (s *Server) List(prefix *string) []string { r := []string{} + s.RLock() + defer s.RUnlock() + for name, client := range s.clients { if prefix != nil && !strings.HasPrefix(name, strings.ToLower(*prefix)) { continue @@ -260,11 +276,84 @@ func (s *Server) Op(fingerprint string) { } // 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) s.Lock() s.whitelist[fingerprint] = struct{}{} 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 @@ -409,9 +498,11 @@ func (s *Server) AutoCompleteFunction(line string, pos int, key rune) (newLine s // Stop stops the server func (s *Server) Stop() { + s.Lock() for _, client := range s.clients { client.Conn.Close() } + s.Unlock() close(s.done) }