diff --git a/ssh/client.go b/ssh/client.go index a7e3263..6fd1994 100644 --- a/ssh/client.go +++ b/ssh/client.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net" + "os" "sync" "time" ) @@ -187,6 +188,10 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) { // net.Conn underlying the the SSH connection. type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error +// BannerCallback is the function type used for treat the banner sent by +// the server. A BannerCallback receives the message sent by the remote server. +type BannerCallback func(message string) error + // A ClientConfig structure is used to configure a Client. It must not be // modified after having been passed to an SSH function. type ClientConfig struct { @@ -209,6 +214,12 @@ type ClientConfig struct { // FixedHostKey can be used for simplistic host key checks. HostKeyCallback HostKeyCallback + // BannerCallback is called during the SSH dance to display a custom + // server's message. The client configuration can supply this callback to + // handle it as wished. The function BannerDisplayStderr can be used for + // simplistic display on Stderr. + BannerCallback BannerCallback + // ClientVersion contains the version identification string that will // be used for the connection. If empty, a reasonable default is used. ClientVersion string @@ -255,3 +266,13 @@ func FixedHostKey(key PublicKey) HostKeyCallback { hk := &fixedHostKey{key} return hk.check } + +// BannerDisplayStderr returns a function that can be used for +// ClientConfig.BannerCallback to display banners on os.Stderr. +func BannerDisplayStderr() BannerCallback { + return func(banner string) error { + _, err := os.Stderr.WriteString(banner) + + return err + } +} diff --git a/ssh/client_auth.go b/ssh/client_auth.go index 3acd8d4..a1252cb 100644 --- a/ssh/client_auth.go +++ b/ssh/client_auth.go @@ -283,7 +283,9 @@ func confirmKeyAck(key PublicKey, c packetConn) (bool, error) { } switch packet[0] { case msgUserAuthBanner: - // TODO(gpaul): add callback to present the banner to the user + if err := handleBannerResponse(c, packet); err != nil { + return false, err + } case msgUserAuthPubKeyOk: var msg userAuthPubKeyOkMsg if err := Unmarshal(packet, &msg); err != nil { @@ -325,7 +327,9 @@ func handleAuthResponse(c packetConn) (bool, []string, error) { switch packet[0] { case msgUserAuthBanner: - // TODO: add callback to present the banner to the user + if err := handleBannerResponse(c, packet); err != nil { + return false, nil, err + } case msgUserAuthFailure: var msg userAuthFailureMsg if err := Unmarshal(packet, &msg); err != nil { @@ -340,6 +344,24 @@ func handleAuthResponse(c packetConn) (bool, []string, error) { } } +func handleBannerResponse(c packetConn, packet []byte) error { + var msg userAuthBannerMsg + if err := Unmarshal(packet, &msg); err != nil { + return err + } + + transport, ok := c.(*handshakeTransport) + if !ok { + return nil + } + + if transport.bannerCallback != nil { + return transport.bannerCallback(msg.Message) + } + + return nil +} + // KeyboardInteractiveChallenge should print questions, optionally // disabling echoing (e.g. for passwords), and return all the answers. // Challenge may be called multiple times in a single session. After @@ -385,7 +407,9 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe // like handleAuthResponse, but with less options. switch packet[0] { case msgUserAuthBanner: - // TODO: Print banners during userauth. + if err := handleBannerResponse(c, packet); err != nil { + return false, nil, err + } continue case msgUserAuthInfoRequest: // OK diff --git a/ssh/client_test.go b/ssh/client_test.go index ccf5607..9dbe042 100644 --- a/ssh/client_test.go +++ b/ssh/client_test.go @@ -79,3 +79,41 @@ func TestHostKeyCheck(t *testing.T) { } } } + +func TestBannerCallback(t *testing.T) { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + + serverConf := &ServerConfig{ + NoClientAuth: true, + BannerCallback: func(conn ConnMetadata) string { + return "Hello World" + }, + } + serverConf.AddHostKey(testSigners["rsa"]) + go NewServerConn(c1, serverConf) + + var receivedBanner string + clientConf := ClientConfig{ + User: "user", + HostKeyCallback: InsecureIgnoreHostKey(), + BannerCallback: func(message string) error { + receivedBanner = message + return nil + }, + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + t.Fatal(err) + } + + expected := "Hello World" + if receivedBanner != expected { + t.Fatalf("got %s; want %s", receivedBanner, expected) + } +} diff --git a/ssh/handshake.go b/ssh/handshake.go index 932ce83..4f7912e 100644 --- a/ssh/handshake.go +++ b/ssh/handshake.go @@ -78,6 +78,11 @@ type handshakeTransport struct { dialAddress string remoteAddr net.Addr + // bannerCallback is non-empty if we are the client and it has been set in + // ClientConfig. In that case it is called during the user authentication + // dance to handle a custom server's message. + bannerCallback BannerCallback + // Algorithms agreed in the last key exchange. algorithms *algorithms @@ -120,6 +125,7 @@ func newClientTransport(conn keyingTransport, clientVersion, serverVersion []byt t.dialAddress = dialAddr t.remoteAddr = addr t.hostKeyCallback = config.HostKeyCallback + t.bannerCallback = config.BannerCallback if config.HostKeyAlgorithms != nil { t.hostKeyAlgorithms = config.HostKeyAlgorithms } else { diff --git a/ssh/messages.go b/ssh/messages.go index e6ecd3a..c96e1be 100644 --- a/ssh/messages.go +++ b/ssh/messages.go @@ -23,10 +23,6 @@ const ( msgUnimplemented = 3 msgDebug = 4 msgNewKeys = 21 - - // Standard authentication messages - msgUserAuthSuccess = 52 - msgUserAuthBanner = 53 ) // SSH messages: @@ -137,6 +133,18 @@ type userAuthFailureMsg struct { PartialSuccess bool } +// See RFC 4252, section 5.1 +const msgUserAuthSuccess = 52 + +// See RFC 4252, section 5.4 +const msgUserAuthBanner = 53 + +type userAuthBannerMsg struct { + Message string `sshtype:"53"` + // unused, but required to allow message parsing + Language string +} + // See RFC 4256, section 3.2 const msgUserAuthInfoRequest = 60 const msgUserAuthInfoResponse = 61 diff --git a/ssh/server.go b/ssh/server.go index 8a78b7c..148d2cb 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -95,6 +95,10 @@ type ServerConfig struct { // Note that RFC 4253 section 4.2 requires that this string start with // "SSH-2.0-". ServerVersion string + + // BannerCallback, if present, is called and the return string is sent to + // the client after key exchange completed but before authentication. + BannerCallback func(conn ConnMetadata) string } // AddHostKey adds a private key as a host key. If an existing host @@ -343,6 +347,19 @@ userAuthLoop: } s.user = userAuthReq.User + + if authFailures == 0 && config.BannerCallback != nil { + msg := config.BannerCallback(s) + if msg != "" { + bannerMsg := &userAuthBannerMsg{ + Message: msg, + } + if err := s.transport.writePacket(Marshal(bannerMsg)); err != nil { + return nil, err + } + } + } + perms = nil authErr := errors.New("no auth passed yet") diff --git a/ssh/test/banner_test.go b/ssh/test/banner_test.go new file mode 100644 index 0000000..f1ad469 --- /dev/null +++ b/ssh/test/banner_test.go @@ -0,0 +1,33 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin dragonfly freebsd linux netbsd openbsd + +package test + +import ( + "testing" + +) + +func TestBannerCallbackAgainstOpenSSH(t *testing.T) { + server := newServer(t) + defer server.Shutdown() + + clientConf := clientConfig() + + var receivedBanner string + clientConf.BannerCallback = func(message string) error { + receivedBanner = message + return nil + } + + conn := server.Dial(clientConf) + defer conn.Close() + + expected := "Server Banner" + if receivedBanner != expected { + t.Fatalf("got %v; want %v", receivedBanner, expected) + } +} diff --git a/ssh/test/test_unix_test.go b/ssh/test/test_unix_test.go index e673536..6b1e0ff 100644 --- a/ssh/test/test_unix_test.go +++ b/ssh/test/test_unix_test.go @@ -27,6 +27,7 @@ import ( const sshd_config = ` Protocol 2 +Banner {{.Dir}}/banner HostKey {{.Dir}}/id_rsa HostKey {{.Dir}}/id_dsa HostKey {{.Dir}}/id_ecdsa @@ -256,6 +257,8 @@ func newServer(t *testing.T) *server { } f.Close() + writeFile(filepath.Join(dir, "banner"), []byte("Server Banner")) + for k, v := range testdata.PEMBytes { filename := "id_" + k writeFile(filepath.Join(dir, filename), v)