Root sshchat package, main moved into cmd/ssh-chat/.

This commit is contained in:
Andrey Petrov 2015-01-21 11:47:59 -08:00
parent 76902aea1c
commit 8188deef30
12 changed files with 76 additions and 61 deletions

View File

@ -2,13 +2,12 @@ BINARY = ssh-chat
KEY = host_key KEY = host_key
PORT = 2022 PORT = 2022
SRCS = %.go
all: $(BINARY) all: $(BINARY)
**/*.go: $(BINARY): **/**/*.go **/*.go *.go
go build ./... go build -ldflags "-X main.buildCommit `git describe --long --tags --dirty --always`" ./cmd/ssh-chat
$(BINARY): **/*.go *.go
go build -ldflags "-X main.buildCommit `git describe --long --tags --dirty --always`" .
deps: deps:
go get . go get .

29
auth.go
View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"errors" "errors"
@ -16,8 +16,8 @@ var ErrNotWhitelisted = errors.New("not whitelisted")
// The error returned a key is checked that is banned. // The error returned a key is checked that is banned.
var ErrBanned = errors.New("banned") var ErrBanned = errors.New("banned")
// NewAuthKey returns string from an ssh.PublicKey. // newAuthKey returns string from an ssh.PublicKey used to index the key in our lookup.
func NewAuthKey(key ssh.PublicKey) string { func newAuthKey(key ssh.PublicKey) string {
if key == nil { if key == nil {
return "" return ""
} }
@ -25,8 +25,8 @@ func NewAuthKey(key ssh.PublicKey) string {
return sshd.Fingerprint(key) return sshd.Fingerprint(key)
} }
// NewAuthAddr returns a string from a net.Addr // newAuthAddr returns a string from a net.Addr used to index the address the key in our lookup.
func NewAuthAddr(addr net.Addr) string { func newAuthAddr(addr net.Addr) string {
if addr == nil { if addr == nil {
return "" return ""
} }
@ -34,8 +34,7 @@ func NewAuthAddr(addr net.Addr) string {
return host return host
} }
// Auth stores fingerprint lookups // Auth stores lookups for bans, whitelists, and ops. It implements the sshd.Auth interface.
// TODO: Add timed auth by using a time.Time instead of struct{} for values.
type Auth struct { type Auth struct {
sync.RWMutex sync.RWMutex
bannedAddr *Set bannedAddr *Set
@ -44,7 +43,7 @@ type Auth struct {
ops *Set ops *Set
} }
// NewAuth creates a new default Auth. // NewAuth creates a new empty Auth.
func NewAuth() *Auth { func NewAuth() *Auth {
return &Auth{ return &Auth{
bannedAddr: NewSet(), bannedAddr: NewSet(),
@ -61,7 +60,7 @@ func (a Auth) AllowAnonymous() bool {
// Check determines if a pubkey fingerprint is permitted. // Check determines if a pubkey fingerprint is permitted.
func (a *Auth) Check(addr net.Addr, key ssh.PublicKey) (bool, error) { func (a *Auth) Check(addr net.Addr, key ssh.PublicKey) (bool, error) {
authkey := NewAuthKey(key) authkey := newAuthKey(key)
if a.whitelist.Len() != 0 { if a.whitelist.Len() != 0 {
// Only check whitelist if there is something in it, otherwise it's disabled. // 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) banned := a.banned.In(authkey)
if !banned { if !banned {
banned = a.bannedAddr.In(NewAuthAddr(addr)) banned = a.bannedAddr.In(newAuthAddr(addr))
} }
if banned { if banned {
return false, ErrBanned return false, ErrBanned
@ -88,7 +87,7 @@ func (a *Auth) Op(key ssh.PublicKey, d time.Duration) {
if key == nil { if key == nil {
return return
} }
authkey := NewAuthKey(key) authkey := newAuthKey(key)
if d != 0 { if d != 0 {
a.ops.AddExpiring(authkey, d) a.ops.AddExpiring(authkey, d)
} else { } else {
@ -102,7 +101,7 @@ func (a *Auth) IsOp(key ssh.PublicKey) bool {
if key == nil { if key == nil {
return false return false
} }
authkey := NewAuthKey(key) authkey := newAuthKey(key)
return a.ops.In(authkey) return a.ops.In(authkey)
} }
@ -111,7 +110,7 @@ func (a *Auth) Whitelist(key ssh.PublicKey, d time.Duration) {
if key == nil { if key == nil {
return return
} }
authkey := NewAuthKey(key) authkey := newAuthKey(key)
if d != 0 { if d != 0 {
a.whitelist.AddExpiring(authkey, d) a.whitelist.AddExpiring(authkey, d)
} else { } else {
@ -125,7 +124,7 @@ func (a *Auth) Ban(key ssh.PublicKey, d time.Duration) {
if key == nil { if key == nil {
return return
} }
a.BanFingerprint(NewAuthKey(key), d) a.BanFingerprint(newAuthKey(key), d)
} }
// BanFingerprint will set a public key fingerprint as banned. // 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. // Ban will set an IP address as banned.
func (a *Auth) BanAddr(addr net.Addr, d time.Duration) { func (a *Auth) BanAddr(addr net.Addr, d time.Duration) {
key := NewAuthAddr(addr) key := newAuthAddr(addr)
if d != 0 { if d != 0 {
a.bannedAddr.AddExpiring(key, d) a.bannedAddr.AddExpiring(key, d)
} else { } else {

View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"crypto/rand" "crypto/rand"

View File

@ -15,6 +15,7 @@ import (
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/shazow/ssh-chat"
"github.com/shazow/ssh-chat/chat" "github.com/shazow/ssh-chat/chat"
"github.com/shazow/ssh-chat/chat/message" "github.com/shazow/ssh-chat/chat/message"
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
@ -39,7 +40,10 @@ var logLevels = []log.Level{
log.Debug, log.Debug,
} }
var buildCommit string func fail(code int, format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(code)
}
func main() { func main() {
options := Options{} options := Options{}
@ -66,7 +70,7 @@ func main() {
} }
logLevel := logLevels[numVerbose] logLevel := logLevels[numVerbose]
logger = golog.New(os.Stderr, logLevel) sshchat.SetLogger(golog.New(os.Stderr, logLevel))
if logLevel == log.Debug { if logLevel == log.Debug {
// Enable logging from submodules // Enable logging from submodules
@ -84,33 +88,29 @@ func main() {
privateKey, err := ReadPrivateKey(privateKeyPath) privateKey, err := ReadPrivateKey(privateKeyPath)
if err != nil { if err != nil {
logger.Errorf("Couldn't read private key: %v", err) fail(2, "Couldn't read private key: %v\n", err)
os.Exit(2)
} }
signer, err := ssh.ParsePrivateKey(privateKey) signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil { if err != nil {
logger.Errorf("Failed to parse key: %v", err) fail(3, "Failed to parse key: %v\n", err)
os.Exit(3)
} }
auth := NewAuth() auth := sshchat.NewAuth()
config := sshd.MakeAuth(auth) config := sshd.MakeAuth(auth)
config.AddHostKey(signer) config.AddHostKey(signer)
s, err := sshd.ListenSSH(options.Bind, config) s, err := sshd.ListenSSH(options.Bind, config)
if err != nil { if err != nil {
logger.Errorf("Failed to listen on socket: %v", err) fail(4, "Failed to listen on socket: %v\n", err)
os.Exit(4)
} }
defer s.Close() defer s.Close()
s.RateLimit = true s.RateLimit = true
fmt.Printf("Listening for connections on %v\n", s.Addr().String()) fmt.Printf("Listening for connections on %v\n", s.Addr().String())
host := NewHost(s) host := sshchat.NewHost(s, auth)
host.auth = auth host.SetTheme(message.Themes[0])
host.theme = &message.Themes[0]
err = fromFile(options.Admin, func(line []byte) error { err = fromFile(options.Admin, func(line []byte) error {
key, _, _, _, err := ssh.ParseAuthorizedKey(line) key, _, _, _, err := ssh.ParseAuthorizedKey(line)
@ -121,8 +121,7 @@ func main() {
return nil return nil
}) })
if err != nil { if err != nil {
logger.Errorf("Failed to load admins: %v", err) fail(5, "Failed to load admins: %v\n", err)
os.Exit(5)
} }
err = fromFile(options.Whitelist, func(line []byte) error { err = fromFile(options.Whitelist, func(line []byte) error {
@ -131,19 +130,16 @@ func main() {
return err return err
} }
auth.Whitelist(key, 0) auth.Whitelist(key, 0)
logger.Debugf("Whitelisted: %s", line)
return nil return nil
}) })
if err != nil { if err != nil {
logger.Errorf("Failed to load whitelist: %v", err) fail(6, "Failed to load whitelist: %v\n", err)
os.Exit(5)
} }
if options.Motd != "" { if options.Motd != "" {
motd, err := ioutil.ReadFile(options.Motd) motd, err := ioutil.ReadFile(options.Motd)
if err != nil { if err != nil {
logger.Errorf("Failed to load MOTD file: %v", err) fail(7, "Failed to load MOTD file: %v\n", err)
return
} }
motdString := strings.TrimSpace(string(motd)) motdString := strings.TrimSpace(string(motd))
// hack to normalize line endings into \r\n // hack to normalize line endings into \r\n
@ -157,8 +153,7 @@ func main() {
} else if options.Log != "" { } else if options.Log != "" {
fp, err := os.OpenFile(options.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) fp, err := os.OpenFile(options.Log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil { if err != nil {
logger.Errorf("Failed to open log file for writing: %v", err) fail(8, "Failed to open log file for writing: %v", err)
return
} }
host.SetLogging(fp) host.SetLogging(fp)
} }
@ -170,7 +165,7 @@ func main() {
signal.Notify(sig, os.Interrupt) signal.Notify(sig, os.Interrupt)
<-sig // Wait for ^C signal <-sig // Wait for ^C signal
logger.Warningf("Interrupt signal detected, shutting down.") fmt.Fprintln(os.Stderr, "Interrupt signal detected, shutting down.")
os.Exit(0) os.Exit(0)
} }

11
godoc.go Normal file
View File

@ -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

16
host.go
View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"errors" "errors"
@ -13,6 +13,8 @@ import (
"github.com/shazow/ssh-chat/sshd" "github.com/shazow/ssh-chat/sshd"
) )
var buildCommit string
const maxInputLength int = 1024 const maxInputLength int = 1024
// GetPrompt will render the terminal prompt string based on the user. // GetPrompt will render the terminal prompt string based on the user.
@ -36,16 +38,17 @@ type Host struct {
count int count int
// Default theme // Default theme
theme *message.Theme theme message.Theme
} }
// NewHost creates a Host on top of an existing listener. // 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() room := chat.NewRoom()
h := Host{ h := Host{
Room: room, Room: room,
listener: listener, listener: listener,
commands: chat.Commands{}, commands: chat.Commands{},
auth: auth,
} }
// Make our own commands registry instance. // Make our own commands registry instance.
@ -57,6 +60,11 @@ func NewHost(listener *sshd.SSHListener) *Host {
return &h 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. // SetMotd sets the host's message of the day.
func (h *Host) SetMotd(motd string) { func (h *Host) SetMotd(motd string) {
h.motd = motd h.motd = motd
@ -74,7 +82,7 @@ func (h Host) isOp(conn sshd.Connection) bool {
func (h *Host) Connect(term *sshd.Terminal) { func (h *Host) Connect(term *sshd.Terminal) {
id := NewIdentity(term.Conn) id := NewIdentity(term.Conn)
user := message.NewUserScreen(id, term) user := message.NewUserScreen(id, term)
user.Config.Theme = h.theme user.Config.Theme = &h.theme
go func() { go func() {
// Close term once user is closed. // Close term once user is closed.
user.Wait() user.Wait()

View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"bufio" "bufio"
@ -56,7 +56,7 @@ func TestHostNameCollision(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer s.Close() defer s.Close()
host := NewHost(s) host := NewHost(s, nil)
go host.Serve() go host.Serve()
done := make(chan struct{}, 1) done := make(chan struct{}, 1)
@ -70,7 +70,7 @@ func TestHostNameCollision(t *testing.T) {
scanner.Scan() scanner.Scan()
actual := scanner.Text() actual := scanner.Text()
if !strings.HasPrefix(actual, "[foo] ") { 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) actual = stripPrompt(actual)
@ -133,8 +133,7 @@ func TestHostWhitelist(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
defer s.Close() defer s.Close()
host := NewHost(s) host := NewHost(s, auth)
host.auth = auth
go host.Serve() go host.Serve()
target := s.Addr().String() target := s.Addr().String()
@ -174,7 +173,7 @@ func TestHostKick(t *testing.T) {
} }
defer s.Close() defer s.Close()
addr := s.Addr().String() addr := s.Addr().String()
host := NewHost(s) host := NewHost(s, nil)
go host.Serve() go host.Serve()
connected := make(chan struct{}) connected := make(chan struct{})

View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"bytes" "bytes"
@ -9,8 +9,12 @@ import (
var logger *golog.Logger var logger *golog.Logger
func SetLogger(l *golog.Logger) {
logger = l
}
func init() { func init() {
// Set a default null logger // Set a default null logger
var b bytes.Buffer var b bytes.Buffer
logger = golog.New(&b, log.Debug) SetLogger(golog.New(&b, log.Debug))
} }

8
set.go
View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"sync" "sync"
@ -19,20 +19,20 @@ func (v value) Bool() bool {
return true return true
} }
type SetValue interface { type setValue interface {
Bool() bool Bool() bool
} }
// Set with expire-able keys // Set with expire-able keys
type Set struct { type Set struct {
lookup map[string]SetValue lookup map[string]setValue
sync.Mutex sync.Mutex
} }
// NewSet creates a new set. // NewSet creates a new set.
func NewSet() *Set { func NewSet() *Set {
return &Set{ return &Set{
lookup: map[string]SetValue{}, lookup: map[string]setValue{},
} }
} }

View File

@ -1,4 +1,4 @@
package main package sshchat
import ( import (
"testing" "testing"