Merge branch 'refactor' of github.com:shazow/ssh-chat into refactor

This commit is contained in:
Andrey Petrov 2015-01-06 21:43:08 -08:00
commit 7a39898b36
8 changed files with 227 additions and 11 deletions

View File

@ -26,7 +26,7 @@ Usage:
Application Options: Application Options:
-v, --verbose Show verbose logging. -v, --verbose Show verbose logging.
-i, --identity= Private key to identify server with. (~/.ssh/id_rsa) -i, --identity= Private key to identify server with. (~/.ssh/id_rsa)
--bind= Host and port to listen on. (0.0.0.0:22) --bind= Host and port to listen on. (0.0.0.0:2022)
--admin= Fingerprint of pubkey to mark as admin. --admin= Fingerprint of pubkey to mark as admin.
--whitelist= Optional file of pubkey fingerprints that are allowed to connect --whitelist= Optional file of pubkey fingerprints that are allowed to connect
--motd= Message of the Day file (optional) --motd= Message of the Day file (optional)
@ -40,7 +40,7 @@ After doing `go get github.com/shazow/ssh-chat` on this repo, you should be able
to run a command like: to run a command like:
``` ```
$ ssh-chat --verbose --bind ":2022" --identity ~/.ssh/id_dsa $ ssh-chat --verbose --bind ":22" --identity ~/.ssh/id_dsa
``` ```
To bind on port 22, you'll need to make sure it's free (move any other ssh To bind on port 22, you'll need to make sure it's free (move any other ssh

View File

@ -17,6 +17,15 @@ type Auth struct {
sync.RWMutex sync.RWMutex
} }
// NewAuth creates a new default Auth.
func NewAuth() Auth {
return Auth{
whitelist: make(map[string]struct{}),
banned: make(map[string]struct{}),
ops: make(map[string]struct{}),
}
}
// AllowAnonymous determines if anonymous users are permitted. // AllowAnonymous determines if anonymous users are permitted.
func (a Auth) AllowAnonymous() bool { func (a Auth) AllowAnonymous() bool {
a.RLock() a.RLock()

View File

@ -85,6 +85,12 @@ func (ch *Channel) HandleMsg(m Message) {
// Skip // Skip
return return
} }
if _, ok := m.(*AnnounceMsg); ok {
if user.Config.Quiet {
// Skip
return
}
}
err := user.Send(m) err := user.Send(m)
if err != nil { if err != nil {
ch.Leave(user) ch.Leave(user)
@ -168,7 +174,7 @@ func (ch *Channel) NamesPrefix(prefix string) []string {
members := ch.members.ListPrefix(prefix) members := ch.members.ListPrefix(prefix)
names := make([]string, len(members)) names := make([]string, len(members))
for i, u := range members { for i, u := range members {
names[i] = u.(*User).Name() names[i] = u.(*Member).User.Name()
} }
return names return names
} }

View File

@ -56,3 +56,137 @@ func TestChannelJoin(t *testing.T) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
} }
} }
func TestChannelDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) {
u := NewUser("foo")
u.Config = UserConfig{
Quiet: true,
}
ch := NewChannel()
defer ch.Close()
err := ch.Join(u)
if err != nil {
t.Fatal(err)
}
// Drain the initial Join message
<-ch.broadcast
go func() {
for msg := range u.msg {
if _, ok := msg.(*AnnounceMsg); ok {
t.Errorf("Got unexpected `%T`", msg)
}
}
}()
// Call with an AnnounceMsg and all the other types
// and assert we received only non-announce messages
ch.HandleMsg(NewAnnounceMsg("Ignored"))
// Assert we still get all other types of messages
ch.HandleMsg(NewEmoteMsg("hello", u))
ch.HandleMsg(NewSystemMsg("hello", u))
ch.HandleMsg(NewPrivateMsg("hello", u, u))
ch.HandleMsg(NewPublicMsg("hello", u))
}
func TestChannelQuietToggleBroadcasts(t *testing.T) {
u := NewUser("foo")
u.Config = UserConfig{
Quiet: true,
}
ch := NewChannel()
defer ch.Close()
err := ch.Join(u)
if err != nil {
t.Fatal(err)
}
// Drain the initial Join message
<-ch.broadcast
u.ToggleQuietMode()
expectedMsg := NewAnnounceMsg("Ignored")
ch.HandleMsg(expectedMsg)
msg := <-u.msg
if _, ok := msg.(*AnnounceMsg); !ok {
t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg)
}
u.ToggleQuietMode()
ch.HandleMsg(NewAnnounceMsg("Ignored"))
ch.HandleMsg(NewSystemMsg("hello", u))
msg = <-u.msg
if _, ok := msg.(*AnnounceMsg); ok {
t.Errorf("Got unexpected `%T`", msg)
}
}
func TestQuietToggleDisplayState(t *testing.T) {
var expected, actual []byte
s := &MockScreen{}
u := NewUser("foo")
ch := NewChannel()
go ch.Serve()
defer ch.Close()
err := ch.Join(u)
if err != nil {
t.Fatal(err)
}
// Drain the initial Join message
<-ch.broadcast
ch.Send(ParseInput("/quiet", u))
u.ConsumeOne(s)
expected = []byte("-> Quiet mode is toggled ON" + Newline)
s.Read(&actual)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
ch.Send(ParseInput("/quiet", u))
u.ConsumeOne(s)
expected = []byte("-> Quiet mode is toggled OFF" + Newline)
s.Read(&actual)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
}
func TestChannelNames(t *testing.T) {
var expected, actual []byte
s := &MockScreen{}
u := NewUser("foo")
ch := NewChannel()
go ch.Serve()
defer ch.Close()
err := ch.Join(u)
if err != nil {
t.Fatal(err)
}
// Drain the initial Join message
<-ch.broadcast
ch.Send(ParseInput("/names", u))
u.ConsumeOne(s)
expected = []byte("-> 1 connected: foo" + Newline)
s.Read(&actual)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Got: `%s`; Expected: `%s`", actual, expected)
}
}

View File

@ -196,6 +196,24 @@ func InitCommands(c *Commands) {
}, },
}) })
c.Add(Command{
Prefix: "/quiet",
Help: "Silence announcement-type messages (join, part, rename, etc).",
Handler: func(channel *Channel, msg CommandMsg) error {
u := msg.From()
u.ToggleQuietMode()
var body string
if u.Config.Quiet {
body = "Quiet mode is toggled ON"
} else {
body = "Quiet mode is toggled OFF"
}
channel.Send(NewSystemMsg(body, u))
return nil
},
})
c.Add(Command{ c.Add(Command{
Op: true, Op: true,
Prefix: "/op", Prefix: "/op",

View File

@ -59,6 +59,11 @@ func (u *User) SetName(name string) {
u.SetColorIdx(rand.Int()) u.SetColorIdx(rand.Int())
} }
// ToggleQuietMode will toggle whether or not quiet mode is enabled
func (u *User) ToggleQuietMode() {
u.Config.Quiet = !u.Config.Quiet
}
// SetColorIdx will set the colorIdx to a specific value, primarily used for // SetColorIdx will set the colorIdx to a specific value, primarily used for
// testing. // testing.
func (u *User) SetColorIdx(idx int) { func (u *User) SetColorIdx(idx int) {
@ -122,6 +127,7 @@ func (u *User) Send(m Message) error {
type UserConfig struct { type UserConfig struct {
Highlight bool Highlight bool
Bell bool Bell bool
Quiet bool
Theme *Theme Theme *Theme
} }
@ -132,6 +138,7 @@ func init() {
DefaultUserConfig = &UserConfig{ DefaultUserConfig = &UserConfig{
Highlight: true, Highlight: true,
Bell: false, Bell: false,
Quiet: false,
} }
// TODO: Seed random? // TODO: Seed random?

56
cmd.go
View File

@ -2,6 +2,8 @@ package main
import ( import (
"bufio" "bufio"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -14,6 +16,7 @@ import (
"github.com/alexcesaro/log/golog" "github.com/alexcesaro/log/golog"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
@ -24,7 +27,7 @@ import _ "net/http/pprof"
type Options struct { type Options struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."`
Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:22"` Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
Admin []string `long:"admin" description:"Fingerprint of pubkey to mark as admin."` Admin []string `long:"admin" description:"Fingerprint of pubkey to mark as admin."`
Whitelist string `long:"whitelist" description:"Optional file of pubkey fingerprints who are allowed to connect."` Whitelist string `long:"whitelist" description:"Optional file of pubkey fingerprints who are allowed to connect."`
Motd string `long:"motd" description:"Optional Message of the Day file."` Motd string `long:"motd" description:"Optional Message of the Day file."`
@ -80,21 +83,19 @@ func main() {
} }
} }
privateKey, err := ioutil.ReadFile(privateKeyPath) privateKey, err := readPrivateKey(privateKeyPath)
if err != nil { if err != nil {
logger.Errorf("Failed to load identity: %v", err) logger.Errorf("Couldn't read private key: %v", err)
os.Exit(2) os.Exit(2)
return
} }
signer, err := ssh.ParsePrivateKey(privateKey) signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil { if err != nil {
logger.Errorf("Failed to parse key: %v", err) logger.Errorf("Failed to parse key: %v", err)
os.Exit(3) os.Exit(3)
return
} }
auth := Auth{} auth := NewAuth()
config := sshd.MakeAuth(auth) config := sshd.MakeAuth(auth)
config.AddHostKey(signer) config.AddHostKey(signer)
@ -102,10 +103,11 @@ func main() {
if err != nil { if err != nil {
logger.Errorf("Failed to listen on socket: %v", err) logger.Errorf("Failed to listen on socket: %v", err)
os.Exit(4) os.Exit(4)
return
} }
defer s.Close() defer s.Close()
fmt.Printf("Listening for connections on %v\n", s.Addr().String())
host := NewHost(s) host := NewHost(s)
host.auth = &auth host.auth = &auth
host.theme = &chat.Themes[0] host.theme = &chat.Themes[0]
@ -151,3 +153,43 @@ func main() {
logger.Warningf("Interrupt signal detected, shutting down.") logger.Warningf("Interrupt signal detected, shutting down.")
os.Exit(0) os.Exit(0)
} }
// readPrivateKey attempts to read your private key and possibly decrypt it if it
// requires a passphrase.
// This function will prompt for a passphrase on STDIN if the environment variable (`IDENTITY_PASSPHRASE`),
// is not set.
func readPrivateKey(privateKeyPath string) ([]byte, error) {
privateKey, err := ioutil.ReadFile(privateKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to load identity: %v", err)
}
block, rest := pem.Decode(privateKey)
if len(rest) > 0 {
return nil, fmt.Errorf("extra data when decoding private key")
}
if !x509.IsEncryptedPEMBlock(block) {
return privateKey, nil
}
passphrase := []byte(os.Getenv("IDENTITY_PASSPHRASE"))
if len(passphrase) == 0 {
fmt.Printf("Enter passphrase: ")
passphrase, err = terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, fmt.Errorf("couldn't read passphrase: %v", err)
}
fmt.Println()
}
der, err := x509.DecryptPEMBlock(block, passphrase)
if err != nil {
return nil, fmt.Errorf("decrypt failed: %v", err)
}
privateKey = pem.EncodeToMemory(&pem.Block{
Type: block.Type,
Bytes: der,
})
return privateKey, nil
}

View File

@ -7,7 +7,7 @@ package sshd
config := MakeNoAuth() config := MakeNoAuth()
config.AddHostKey(signer) config.AddHostKey(signer)
s, err := ListenSSH("0.0.0.0:22", config) s, err := ListenSSH("0.0.0.0:2022", config)
if err != nil { if err != nil {
// Handle opening socket error // Handle opening socket error
} }