diff --git a/Makefile b/Makefile index a7c70d7..99150e4 100644 --- a/Makefile +++ b/Makefile @@ -43,4 +43,4 @@ release: deploy: build/ssh-chat-linux_amd64.tgz ssh -p 2022 ssh.chat tar xvz < build/ssh-chat-linux_amd64.tgz @echo " --- Ready to deploy ---" - @echo "Run: ssh -p 2022 ssh.chat sudo systemctl restart ssh-chat" + @echo "Run: ssh -t -p 2022 ssh.chat sudo systemctl restart ssh-chat" diff --git a/README.md b/README.md index cc04b84..86d7e52 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ $ ssh ssh.chat Please abide by our [project's Code of Conduct](https://github.com/shazow/ssh-chat/blob/master/CODE_OF_CONDUCT.md) while participating in chat. -The server's RSA key fingerprint is `MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db` or `SHA256:HQDLlZsXL3t0lV5CHM0OXeZ5O6PcfHuzkS8cRbbTLBI`. If you see something different, you might be [MITM](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)'d. +The host's public key is `ssh.chat ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKPrQofxXqoz2y9A7NFkkENt6iW8/mvpfes3RY/41Oyt` and the fingerprint is `SHA256:yoqMXkCysMTBsvhu2yRoMUl+EmZKlvkN+ZKmL3115xU` (as of 2021-10-13). + +If you see something different, you might be [MITM](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)'d. (Apologies if the server is down, try again shortly.) diff --git a/chat/command.go b/chat/command.go index 6420324..6a8fcd2 100644 --- a/chat/command.go +++ b/chat/command.go @@ -469,12 +469,13 @@ func InitCommands(c *Commands) { msg.From().SetAway(awayMsg) if awayMsg != "" { room.Send(message.NewEmoteMsg("has gone away: "+awayMsg, msg.From())) - } else if !isAway { - room.Send(message.NewSystemMsg("Not away. Append a reason message to set away.", msg.From())) - } else { - room.Send(message.NewEmoteMsg("is back.", msg.From())) + return nil } - return nil + if isAway { + room.Send(message.NewEmoteMsg("is back.", msg.From())) + return nil + } + return errors.New("not away. Append a reason message to set away") }, }) @@ -486,8 +487,9 @@ func InitCommands(c *Commands) { if isAway { msg.From().SetAway("") room.Send(message.NewEmoteMsg("is back.", msg.From())) + return nil } - return nil + return errors.New("must be away to be back") }, }) diff --git a/chat/command_test.go b/chat/command_test.go index 839d34c..37f5cb8 100644 --- a/chat/command_test.go +++ b/chat/command_test.go @@ -25,10 +25,17 @@ func TestAwayCommands(t *testing.T) { // expected output IsUserAway bool AwayMessage string + + // expected state change + ExpectsError func(awayBefore bool) bool } - awayStep := step{"/away snorkling", true, "snorkling"} - notAwayStep := step{"/away", false, ""} - backStep := step{"/back", false, ""} + neverError := func(_ bool) bool { return false } + // if the user was away before, then the error is expected + errorIfAwayBefore := func(awayBefore bool) bool { return awayBefore } + + awayStep := step{"/away snorkling", true, "snorkling", neverError} + notAwayStep := step{"/away", false, "", errorIfAwayBefore} + backStep := step{"/back", false, "", errorIfAwayBefore} steps := []step{awayStep, notAwayStep, backStep} cases := [][]int{ @@ -42,7 +49,12 @@ func TestAwayCommands(t *testing.T) { for _, s := range []step{steps[c[0]], steps[c[1]], steps[c[2]]} { msg, _ := message.NewPublicMsg(s.Msg, u).ParseCommand() - cmds.Run(room, *msg) + awayBeforeCommand, _, _ := u.GetAway() + + err := cmds.Run(room, *msg) + if err != nil && s.ExpectsError(awayBeforeCommand) { + t.Fatalf("unexpected error running the command: %+v", err) + } isAway, _, awayMsg := u.GetAway() if isAway != s.IsUserAway { diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 8730bf6..b967132 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -26,16 +26,16 @@ var Version string = "dev" // Options contains the flag options type Options struct { - Admin string `long:"admin" description:"File of public keys who are admins."` - Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` - Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` - Log string `long:"log" description:"Write chat log to this file."` - Motd string `long:"motd" description:"Optional Message of the Day file."` - Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` - Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` - Version bool `long:"version" description:"Print version and exit."` - Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` - Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."` + Admin string `long:"admin" description:"File of public keys who are admins."` + Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"` + Identity []string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` + Log string `long:"log" description:"Write chat log to this file."` + Motd string `long:"motd" description:"Optional Message of the Day file."` + Pprof int `long:"pprof" description:"Enable pprof http server for profiling."` + Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` + Version bool `long:"version" description:"Print version and exit."` + Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."` + Passphrase string `long:"unsafe-passphrase" description:"Require an interactive passphrase to connect. Whitelist feature is more secure."` } const extraHelp = `There are hidden options and easter eggs in ssh-chat. The source code is a good @@ -100,25 +100,28 @@ func main() { message.SetLogger(os.Stderr) } - privateKeyPath := options.Identity - if strings.HasPrefix(privateKeyPath, "~/") { - user, err := user.Current() - if err == nil { - privateKeyPath = strings.Replace(privateKeyPath, "~", user.HomeDir, 1) - } - } - - signer, err := ReadPrivateKey(privateKeyPath) - if err != nil { - fail(3, "Failed to read identity private key: %v\n", err) - } - auth := sshchat.NewAuth() config := sshd.MakeAuth(auth) - config.AddHostKey(signer) config.ServerVersion = "SSH-2.0-Go ssh-chat" // FIXME: Should we be using config.NoClientAuth = true by default? + for _, privateKeyPath := range options.Identity { + if strings.HasPrefix(privateKeyPath, "~/") { + user, err := user.Current() + if err == nil { + privateKeyPath = strings.Replace(privateKeyPath, "~", user.HomeDir, 1) + } + } + + signer, err := ReadPrivateKey(privateKeyPath) + if err != nil { + fail(3, "Failed to read identity private key: %v\n", err) + } + + config.AddHostKey(signer) + fmt.Printf("Added server identity: %s\n", sshd.Fingerprint(signer.PublicKey())) + } + s, err := sshd.ListenSSH(options.Bind, config) if err != nil { fail(4, "Failed to listen on socket: %v\n", err) diff --git a/go.mod b/go.mod index 459aafa..b8991bf 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ require ( github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 github.com/jessevdk/go-flags v1.5.0 github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + golang.org/x/crypto v0.0.0-20211202192323-5770296d904e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210324051608-47abb6519492 + golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 - golang.org/x/text v0.3.5 + golang.org/x/text v0.3.6 ) go 1.13 diff --git a/go.sum b/go.sum index 6af5af8..0750189 100644 --- a/go.sum +++ b/go.sum @@ -4,19 +4,20 @@ github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LF github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 h1:zwQ1HBo5FYwn1ksMd19qBCKO8JAWE9wmHivEpkw/DvE= github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs= golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/sshd/terminal/terminal.go b/sshd/terminal/terminal.go index ec04e8f..998c79d 100644 --- a/sshd/terminal/terminal.go +++ b/sshd/terminal/terminal.go @@ -264,6 +264,10 @@ func (t *Terminal) moveCursorToPos(pos int) { return } + if pos > len(t.line) { + pos = len(t.line) + } + x := visualLength(t.prompt) + visualLength(t.line[:pos]) y := x / t.termWidth x = x % t.termWidth