diff --git a/Makefile b/Makefile index a2b03db..4ac1400 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,12 @@ BINARY = ssh-chat KEY = host_key PORT = 2022 +SRCS = %.go + all: $(BINARY) -**/*.go: - go build ./... - -$(BINARY): **/*.go *.go - go build -ldflags "-X main.buildCommit `git describe --long --tags --dirty --always`" . +$(BINARY): **/**/*.go **/*.go *.go + go build -ldflags "-X main.buildCommit `git describe --long --tags --dirty --always`" ./cmd/ssh-chat deps: go get . diff --git a/auth.go b/auth.go index 8c86b26..e8f9192 100644 --- a/auth.go +++ b/auth.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "errors" @@ -16,8 +16,8 @@ var ErrNotWhitelisted = errors.New("not whitelisted") // The error returned a key is checked that is banned. var ErrBanned = errors.New("banned") -// NewAuthKey returns string from an ssh.PublicKey. -func NewAuthKey(key ssh.PublicKey) string { +// newAuthKey returns string from an ssh.PublicKey used to index the key in our lookup. +func newAuthKey(key ssh.PublicKey) string { if key == nil { return "" } @@ -25,8 +25,8 @@ func NewAuthKey(key ssh.PublicKey) string { return sshd.Fingerprint(key) } -// NewAuthAddr returns a string from a net.Addr -func NewAuthAddr(addr net.Addr) string { +// newAuthAddr returns a string from a net.Addr used to index the address the key in our lookup. +func newAuthAddr(addr net.Addr) string { if addr == nil { return "" } @@ -34,8 +34,7 @@ func NewAuthAddr(addr net.Addr) string { return host } -// Auth stores fingerprint lookups -// TODO: Add timed auth by using a time.Time instead of struct{} for values. +// Auth stores lookups for bans, whitelists, and ops. It implements the sshd.Auth interface. type Auth struct { sync.RWMutex bannedAddr *Set @@ -44,7 +43,7 @@ type Auth struct { ops *Set } -// NewAuth creates a new default Auth. +// NewAuth creates a new empty Auth. func NewAuth() *Auth { return &Auth{ bannedAddr: NewSet(), @@ -61,7 +60,7 @@ func (a Auth) AllowAnonymous() bool { // Check determines if a pubkey fingerprint is permitted. func (a *Auth) Check(addr net.Addr, key ssh.PublicKey) (bool, error) { - authkey := NewAuthKey(key) + authkey := newAuthKey(key) if a.whitelist.Len() != 0 { // Only check whitelist if there is something in it, otherwise it's disabled. @@ -74,7 +73,7 @@ func (a *Auth) Check(addr net.Addr, key ssh.PublicKey) (bool, error) { banned := a.banned.In(authkey) if !banned { - banned = a.bannedAddr.In(NewAuthAddr(addr)) + banned = a.bannedAddr.In(newAuthAddr(addr)) } if banned { return false, ErrBanned @@ -88,7 +87,7 @@ func (a *Auth) Op(key ssh.PublicKey, d time.Duration) { if key == nil { return } - authkey := NewAuthKey(key) + authkey := newAuthKey(key) if d != 0 { a.ops.AddExpiring(authkey, d) } else { @@ -102,7 +101,7 @@ func (a *Auth) IsOp(key ssh.PublicKey) bool { if key == nil { return false } - authkey := NewAuthKey(key) + authkey := newAuthKey(key) return a.ops.In(authkey) } @@ -111,7 +110,7 @@ func (a *Auth) Whitelist(key ssh.PublicKey, d time.Duration) { if key == nil { return } - authkey := NewAuthKey(key) + authkey := newAuthKey(key) if d != 0 { a.whitelist.AddExpiring(authkey, d) } else { @@ -125,7 +124,7 @@ func (a *Auth) Ban(key ssh.PublicKey, d time.Duration) { if key == nil { return } - a.BanFingerprint(NewAuthKey(key), d) + a.BanFingerprint(newAuthKey(key), d) } // BanFingerprint will set a public key fingerprint as banned. @@ -140,7 +139,7 @@ func (a *Auth) BanFingerprint(authkey string, d time.Duration) { // Ban will set an IP address as banned. func (a *Auth) BanAddr(addr net.Addr, d time.Duration) { - key := NewAuthAddr(addr) + key := newAuthAddr(addr) if d != 0 { a.bannedAddr.AddExpiring(key, d) } else { diff --git a/auth_test.go b/auth_test.go index 981a1d6..4692557 100644 --- a/auth_test.go +++ b/auth_test.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "crypto/rand" diff --git a/cmd.go b/cmd/ssh-chat/cmd.go similarity index 84% rename from cmd.go rename to cmd/ssh-chat/cmd.go index 55646fc..0c0fb99 100644 --- a/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -15,6 +15,7 @@ import ( "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh" + "github.com/shazow/ssh-chat" "github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/sshd" @@ -39,7 +40,10 @@ var logLevels = []log.Level{ log.Debug, } -var buildCommit string +func fail(code int, format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(code) +} func main() { options := Options{} @@ -66,7 +70,7 @@ func main() { } logLevel := logLevels[numVerbose] - logger = golog.New(os.Stderr, logLevel) + sshchat.SetLogger(golog.New(os.Stderr, logLevel)) if logLevel == log.Debug { // Enable logging from submodules @@ -84,33 +88,29 @@ func main() { privateKey, err := ReadPrivateKey(privateKeyPath) if err != nil { - logger.Errorf("Couldn't read private key: %v", err) - os.Exit(2) + fail(2, "Couldn't read private key: %v\n", err) } signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { - logger.Errorf("Failed to parse key: %v", err) - os.Exit(3) + fail(3, "Failed to parse key: %v\n", err) } - auth := NewAuth() + auth := sshchat.NewAuth() config := sshd.MakeAuth(auth) config.AddHostKey(signer) s, err := sshd.ListenSSH(options.Bind, config) if err != nil { - logger.Errorf("Failed to listen on socket: %v", err) - os.Exit(4) + fail(4, "Failed to listen on socket: %v\n", err) } defer s.Close() s.RateLimit = true fmt.Printf("Listening for connections on %v\n", s.Addr().String()) - host := NewHost(s) - host.auth = auth - host.theme = &message.Themes[0] + host := sshchat.NewHost(s, auth) + host.SetTheme(message.Themes[0]) err = fromFile(options.Admin, func(line []byte) error { key, _, _, _, err := ssh.ParseAuthorizedKey(line) @@ -121,8 +121,7 @@ func main() { return nil }) if err != nil { - logger.Errorf("Failed to load admins: %v", err) - os.Exit(5) + fail(5, "Failed to load admins: %v\n", err) } err = fromFile(options.Whitelist, func(line []byte) error { @@ -131,19 +130,16 @@ func main() { return err } auth.Whitelist(key, 0) - logger.Debugf("Whitelisted: %s", line) return nil }) if err != nil { - logger.Errorf("Failed to load whitelist: %v", err) - os.Exit(5) + fail(6, "Failed to load whitelist: %v\n", err) } if options.Motd != "" { motd, err := ioutil.ReadFile(options.Motd) if err != nil { - logger.Errorf("Failed to load MOTD file: %v", err) - return + fail(7, "Failed to load MOTD file: %v\n", err) } motdString := strings.TrimSpace(string(motd)) // hack to normalize line endings into \r\n @@ -157,8 +153,7 @@ func main() { } else if options.Log != "" { fp, err := os.OpenFile(options.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { - logger.Errorf("Failed to open log file for writing: %v", err) - return + fail(8, "Failed to open log file for writing: %v", err) } host.SetLogging(fp) } @@ -170,7 +165,7 @@ func main() { signal.Notify(sig, os.Interrupt) <-sig // Wait for ^C signal - logger.Warningf("Interrupt signal detected, shutting down.") + fmt.Fprintln(os.Stderr, "Interrupt signal detected, shutting down.") os.Exit(0) } diff --git a/key.go b/cmd/ssh-chat/key.go similarity index 100% rename from key.go rename to cmd/ssh-chat/key.go diff --git a/godoc.go b/godoc.go new file mode 100644 index 0000000..8b4842b --- /dev/null +++ b/godoc.go @@ -0,0 +1,11 @@ +/* +sshchat package is an implementation of an ssh server which serves a chat room +instead of a shell. + +sshd subdirectory contains the ssh-related pieces which know nothing about chat. + +chat subdirectory contains the chat-related pieces which know nothing about ssh. + +The Host type is the glue between the sshd and chat pieces. +*/ +package sshchat diff --git a/host.go b/host.go index aab7843..ddaa260 100644 --- a/host.go +++ b/host.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "errors" @@ -13,6 +13,8 @@ import ( "github.com/shazow/ssh-chat/sshd" ) +var buildCommit string + const maxInputLength int = 1024 // GetPrompt will render the terminal prompt string based on the user. @@ -36,16 +38,17 @@ type Host struct { count int // Default theme - theme *message.Theme + theme message.Theme } // NewHost creates a Host on top of an existing listener. -func NewHost(listener *sshd.SSHListener) *Host { +func NewHost(listener *sshd.SSHListener, auth *Auth) *Host { room := chat.NewRoom() h := Host{ Room: room, listener: listener, commands: chat.Commands{}, + auth: auth, } // Make our own commands registry instance. @@ -57,6 +60,11 @@ func NewHost(listener *sshd.SSHListener) *Host { return &h } +// SetTheme sets the default theme for the host. +func (h *Host) SetTheme(theme message.Theme) { + h.theme = theme +} + // SetMotd sets the host's message of the day. func (h *Host) SetMotd(motd string) { h.motd = motd @@ -74,7 +82,7 @@ func (h Host) isOp(conn sshd.Connection) bool { func (h *Host) Connect(term *sshd.Terminal) { id := NewIdentity(term.Conn) user := message.NewUserScreen(id, term) - user.Config.Theme = h.theme + user.Config.Theme = &h.theme go func() { // Close term once user is closed. user.Wait() diff --git a/host_test.go b/host_test.go index 0288ab2..f804f28 100644 --- a/host_test.go +++ b/host_test.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "bufio" @@ -56,7 +56,7 @@ func TestHostNameCollision(t *testing.T) { t.Fatal(err) } defer s.Close() - host := NewHost(s) + host := NewHost(s, nil) go host.Serve() done := make(chan struct{}, 1) @@ -70,7 +70,7 @@ func TestHostNameCollision(t *testing.T) { scanner.Scan() actual := scanner.Text() if !strings.HasPrefix(actual, "[foo] ") { - t.Errorf("First client failed to get 'foo' name.") + t.Errorf("First client failed to get 'foo' name: %q", actual) } actual = stripPrompt(actual) @@ -133,8 +133,7 @@ func TestHostWhitelist(t *testing.T) { t.Fatal(err) } defer s.Close() - host := NewHost(s) - host.auth = auth + host := NewHost(s, auth) go host.Serve() target := s.Addr().String() @@ -174,7 +173,7 @@ func TestHostKick(t *testing.T) { } defer s.Close() addr := s.Addr().String() - host := NewHost(s) + host := NewHost(s, nil) go host.Serve() connected := make(chan struct{}) diff --git a/identity.go b/identity.go index 4403d8b..21988e3 100644 --- a/identity.go +++ b/identity.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "fmt" diff --git a/logger.go b/logger.go index 4fabd05..d1c64ac 100644 --- a/logger.go +++ b/logger.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "bytes" @@ -9,8 +9,12 @@ import ( var logger *golog.Logger +func SetLogger(l *golog.Logger) { + logger = l +} + func init() { // Set a default null logger var b bytes.Buffer - logger = golog.New(&b, log.Debug) + SetLogger(golog.New(&b, log.Debug)) } diff --git a/set.go b/set.go index 86afe13..f0d607c 100644 --- a/set.go +++ b/set.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "sync" @@ -19,20 +19,20 @@ func (v value) Bool() bool { return true } -type SetValue interface { +type setValue interface { Bool() bool } // Set with expire-able keys type Set struct { - lookup map[string]SetValue + lookup map[string]setValue sync.Mutex } // NewSet creates a new set. func NewSet() *Set { return &Set{ - lookup: map[string]SetValue{}, + lookup: map[string]setValue{}, } } diff --git a/set_test.go b/set_test.go index 0a4b9ea..1d7fbef 100644 --- a/set_test.go +++ b/set_test.go @@ -1,4 +1,4 @@ -package main +package sshchat import ( "testing"