mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-06-07 02:43:05 +03:00
Merge branch 'refactor' of github.com:shazow/ssh-chat into refactor
This commit is contained in:
commit
7a39898b36
@ -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
|
||||||
|
9
auth.go
9
auth.go
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
56
cmd.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user