diff --git a/constants.go b/constants.go index a2c9506ab98..83f65972b0a 100644 --- a/constants.go +++ b/constants.go @@ -211,9 +211,15 @@ const ( // LinuxAdminGID is the ID of the standard adm group on linux LinuxAdminGID = 4 - // LinuxOS is the name of the linux OS + // LinuxOS is the GOOS constant used for Linux. LinuxOS = "linux" + // WindowsOS is the GOOS constant used for Microsoft Windows. + WindowsOS = "windows" + + // DarwinOS is the GOOS constant for Apple macOS/darwin. + DarwinOS = "darwin" + // DirMaskSharedGroup is the mask for a directory accessible // by the owner and group DirMaskSharedGroup = 0770 @@ -377,6 +383,9 @@ const ( // EnvHome is home environment variable EnvHome = "HOME" + // EnvUserProfile is the home directory environment variable on Windows. + EnvUserProfile = "USERPROFILE" + // KubeServiceAddr is an address for kubernetes endpoint service KubeServiceAddr = "kubernetes.default.svc.cluster.local:443" @@ -411,3 +420,14 @@ const ( // go standard lib are using that is the only way to identify some errors UseOfClosedNetworkConnection = "use of closed network connection" ) + +const ( + // OpenBrowserLinux is the command used to open a web browser on Linux. + OpenBrowserLinux = "sensible-browser" + + // OpenBrowserDarwin is the command used to open a web browser on macOS/Darwin. + OpenBrowserDarwin = "open" + + // OpenBrowserWindows is the command used to open a web browser on Windows. + OpenBrowserWindows = "rundll32.exe" +) diff --git a/lib/client/api.go b/lib/client/api.go index 63e8cce346e..64b145e54dd 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -33,6 +33,7 @@ import ( "os/user" "path" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -665,7 +666,10 @@ func NewClient(c *Config) (tc *TeleportClient, err error) { // accessPoint returns access point based on the cache policy func (tc *TeleportClient) accessPoint(clt auth.AccessPoint, proxyHostPort string, clusterName string) (auth.AccessPoint, error) { - if tc.CachePolicy == nil { + // If no caching policy was set or on Windows (where Teleport does not + // support file locking at the moment), return direct access to the access + // point. + if tc.CachePolicy == nil || runtime.GOOS == teleport.WindowsOS { log.Debugf("not using caching access point") return clt, nil } @@ -1681,15 +1685,10 @@ func loopbackPool(proxyAddr string) *x509.CertPool { return certPool } -// connects to a local SSH agent +// connectToSSHAgent connects to the local SSH agent and returns a agent.Agent. func connectToSSHAgent() agent.Agent { socketPath := os.Getenv(teleport.SSHAuthSock) - if socketPath == "" { - log.Infof("[KEY AGENT] %v is not set. Try running eval `ssh-agent` and trying again.", teleport.SSHAuthSock) - return nil - } - - conn, err := agentconn.DialAgent(socketPath) + conn, err := agentconn.Dial(socketPath) if err != nil { log.Errorf("[KEY AGENT] Unable to connect to SSH agent on socket: %q.", socketPath) return nil diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index 25e082add8a..b57827d51a2 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -19,11 +19,14 @@ package client import ( "bytes" "fmt" + "runtime" "time" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/trace" + "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" ) @@ -46,16 +49,6 @@ type Key struct { // AsAgentKeys converts client.Key struct to a []*agent.AddedKey. All elements // of the []*agent.AddedKey slice need to be loaded into the agent! -// -// This is done because OpenSSH clients older than OpenSSH 7.3/7.3p1 -// (2016-08-01) have a bug in how they use certificates that have been loaded -// in an agent. Specifically when you add a certificate to an agent, you can't -// just embed the private key within the certificate, you have to add the -// certificate and private key to the agent separately. Teleport works around -// this behavior to ensure OpenSSH interoperability. -// -// For more details see the following: https://bugzilla.mindrot.org/show_bug.cgi?id=2550 -// WARNING: callers expect the returned slice to be __exactly as it is__ func (k *Key) AsAgentKeys() ([]*agent.AddedKey, error) { // unmarshal certificate bytes into a ssh.PublicKey publicKey, _, _, _, err := ssh.ParseAuthorizedKey(k.Cert) @@ -72,7 +65,31 @@ func (k *Key) AsAgentKeys() ([]*agent.AddedKey, error) { // put a teleport identifier along with the teleport user into the comment field comment := fmt.Sprintf("teleport:%v", publicKey.(*ssh.Certificate).KeyId) - // return a certificate (with embedded private key) as well as a private key + // On Windows, return the certificate with the private key embedded. + if runtime.GOOS == teleport.WindowsOS { + return []*agent.AddedKey{ + &agent.AddedKey{ + PrivateKey: privateKey, + Certificate: publicKey.(*ssh.Certificate), + Comment: comment, + LifetimeSecs: 0, + ConfirmBeforeUse: false, + }, + }, nil + } + + // On Unix, return the certificate (with embedded private key) as well as + // a private key. + // + // This is done because OpenSSH clients older than OpenSSH 7.3/7.3p1 + // (2016-08-01) have a bug in how they use certificates that have been loaded + // in an agent. Specifically when you add a certificate to an agent, you can't + // just embed the private key within the certificate, you have to add the + // certificate and private key to the agent separately. Teleport works around + // this behavior to ensure OpenSSH interoperability. + // + // For more details see the following: https://bugzilla.mindrot.org/show_bug.cgi?id=2550 + // WARNING: callers expect the returned slice to be __exactly as it is__ return []*agent.AddedKey{ &agent.AddedKey{ PrivateKey: privateKey, diff --git a/lib/client/session.go b/lib/client/session.go index f9a846589a9..5657ebf9c2b 100644 --- a/lib/client/session.go +++ b/lib/client/session.go @@ -1,3 +1,5 @@ +// +build !windows + /* Copyright 2016 Gravitational, Inc. @@ -22,13 +24,18 @@ import ( "io" "net" "os" + "os/signal" "strings" + "syscall" + "time" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" "github.com/docker/docker/pkg/term" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" @@ -277,6 +284,109 @@ func (ns *NodeSession) allocateTerminal(termType string, s *ssh.Session) (io.Rea ), nil } +func (ns *NodeSession) updateTerminalSize(s *ssh.Session) { + // SIGWINCH is sent to the process when the window size of the terminal has + // changed. + sigwinchCh := make(chan os.Signal, 1) + signal.Notify(sigwinchCh, syscall.SIGWINCH) + + lastSize, err := term.GetWinsize(0) + if err != nil { + log.Errorf("Unable to get window size: %v", err) + return + } + + // Sync the local terminal with size received from the remote server every + // two seconds. If we try and do it live, synchronization jitters occur. + tickerCh := time.NewTicker(defaults.TerminalResizePeriod) + defer tickerCh.Stop() + + for { + select { + // The client updated the size of the local PTY. This change needs to occur + // on the server side PTY as well. + case sigwinch := <-sigwinchCh: + if sigwinch == nil { + return + } + + currSize, err := term.GetWinsize(0) + if err != nil { + log.Warnf("Unable to get window size: %v.", err) + continue + } + + // Terminal size has not changed, don't do anything. + if currSize.Height == lastSize.Height && currSize.Width == lastSize.Width { + continue + } + + // Send the "window-change" request over the channel. + _, err = s.SendRequest( + sshutils.WindowChangeRequest, + false, + ssh.Marshal(sshutils.WinChangeReqParams{ + W: uint32(currSize.Width), + H: uint32(currSize.Height), + })) + if err != nil { + log.Warnf("Unable to send %v reqest: %v.", sshutils.WindowChangeRequest, err) + continue + } + + log.Debugf("Updated window size from %v to %v due to SIGWINCH.", lastSize, currSize) + + lastSize = currSize + + // Extract "resize" events in the stream and store the last window size. + case event := <-ns.nodeClient.TC.EventsChannel(): + // Only "resize" events are important to tsh, all others can be ignored. + if event.GetType() != events.ResizeEvent { + continue + } + + terminalParams, err := session.UnmarshalTerminalParams(event.GetString(events.TerminalSize)) + if err != nil { + log.Warnf("Unable to unmarshal terminal parameters: %v.", err) + continue + } + + lastSize = terminalParams.Winsize() + log.Debugf("Recevied window size %v from node in session.\n", lastSize, event.GetString(events.SessionEventID)) + + // Update size of local terminal with the last size received from remote server. + case <-tickerCh.C: + // Get the current size of the terminal and the last size report that was + // received. + currSize, err := term.GetWinsize(0) + if err != nil { + log.Warnf("Unable to get current terminal size: %v.", err) + continue + } + + // Terminal size has not changed, don't do anything. + if currSize.Width == lastSize.Width && currSize.Height == lastSize.Height { + continue + } + + // This changes the size of the local PTY. This will re-draw what's within + // the window. + err = term.SetWinsize(0, lastSize) + if err != nil { + log.Warnf("Unable to update terminal size: %v.\n", err) + continue + } + + // This is what we use to resize the physical terminal window itself. + os.Stdout.Write([]byte(fmt.Sprintf("\x1b[8;%d;%dt", lastSize.Height, lastSize.Width))) + + log.Debugf("Updated window size from to %v due to remote window change.", currSize, lastSize) + case <-ns.closer.C: + return + } + } +} + // isTerminalAttached returns true when this session is be controlled by // a real terminal. // It will return False for sessions initiated by the Web client or @@ -375,6 +485,42 @@ func (ns *NodeSession) runCommand(ctx context.Context, cmd []string, callback Sh }) } +// watchSignals register UNIX signal handlers and properly terminates a remote shell session +// must be called as a goroutine right after a remote shell is created +func (ns *NodeSession) watchSignals(shell io.Writer) { + exitSignals := make(chan os.Signal, 1) + // catch SIGTERM + signal.Notify(exitSignals, syscall.SIGTERM) + go func() { + defer ns.closer.Close() + <-exitSignals + }() + // Catch Ctrl-C signal + ctrlCSignal := make(chan os.Signal, 1) + signal.Notify(ctrlCSignal, syscall.SIGINT) + go func() { + for { + <-ctrlCSignal + _, err := shell.Write([]byte{3}) + if err != nil { + log.Errorf(err.Error()) + } + } + }() + // Catch Ctrl-Z signal + ctrlZSignal := make(chan os.Signal, 1) + signal.Notify(ctrlZSignal, syscall.SIGTSTP) + go func() { + for { + <-ctrlZSignal + _, err := shell.Write([]byte{26}) + if err != nil { + log.Errorf(err.Error()) + } + } + }() +} + // pipeInOut launches two goroutines: one to pipe the local input into the remote shell, // and another to pipe the output of the remote shell into the local output func (ns *NodeSession) pipeInOut(shell io.ReadWriteCloser) { diff --git a/lib/client/session_unix.go b/lib/client/session_unix.go deleted file mode 100644 index ed3e19532f9..00000000000 --- a/lib/client/session_unix.go +++ /dev/null @@ -1,161 +0,0 @@ -// +build !windows - -package client - -import ( - "fmt" - "io" - "os" - "os/signal" - "syscall" - "time" - - "golang.org/x/crypto/ssh" - - "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/events" - "github.com/gravitational/teleport/lib/session" - "github.com/gravitational/teleport/lib/sshutils" - "github.com/moby/moby/pkg/term" - - log "github.com/sirupsen/logrus" -) - -func (ns *NodeSession) updateTerminalSize(s *ssh.Session) { - // SIGWINCH is sent to the process when the window size of the terminal has - // changed. - sigwinchCh := make(chan os.Signal, 1) - signal.Notify(sigwinchCh, syscall.SIGWINCH) - - lastSize, err := term.GetWinsize(0) - if err != nil { - log.Errorf("Unable to get window size: %v", err) - return - } - - // Sync the local terminal with size received from the remote server every - // two seconds. If we try and do it live, synchronization jitters occur. - tickerCh := time.NewTicker(defaults.TerminalResizePeriod) - defer tickerCh.Stop() - - for { - select { - // The client updated the size of the local PTY. This change needs to occur - // on the server side PTY as well. - case sigwinch := <-sigwinchCh: - if sigwinch == nil { - return - } - - currSize, err := term.GetWinsize(0) - if err != nil { - log.Warnf("Unable to get window size: %v.", err) - continue - } - - // Terminal size has not changed, don't do anything. - if currSize.Height == lastSize.Height && currSize.Width == lastSize.Width { - continue - } - - // Send the "window-change" request over the channel. - _, err = s.SendRequest( - sshutils.WindowChangeRequest, - false, - ssh.Marshal(sshutils.WinChangeReqParams{ - W: uint32(currSize.Width), - H: uint32(currSize.Height), - })) - if err != nil { - log.Warnf("Unable to send %v reqest: %v.", sshutils.WindowChangeRequest, err) - continue - } - - log.Debugf("Updated window size from %v to %v due to SIGWINCH.", lastSize, currSize) - - lastSize = currSize - - // Extract "resize" events in the stream and store the last window size. - case event := <-ns.nodeClient.TC.EventsChannel(): - // Only "resize" events are important to tsh, all others can be ignored. - if event.GetType() != events.ResizeEvent { - continue - } - - terminalParams, err := session.UnmarshalTerminalParams(event.GetString(events.TerminalSize)) - if err != nil { - log.Warnf("Unable to unmarshal terminal parameters: %v.", err) - continue - } - - lastSize = terminalParams.Winsize() - log.Debugf("Recevied window size %v from node in session.\n", lastSize, event.GetString(events.SessionEventID)) - - // Update size of local terminal with the last size received from remote server. - case <-tickerCh.C: - // Get the current size of the terminal and the last size report that was - // received. - currSize, err := term.GetWinsize(0) - if err != nil { - log.Warnf("Unable to get current terminal size: %v.", err) - continue - } - - // Terminal size has not changed, don't do anything. - if currSize.Width == lastSize.Width && currSize.Height == lastSize.Height { - continue - } - - // This changes the size of the local PTY. This will re-draw what's within - // the window. - err = term.SetWinsize(0, lastSize) - if err != nil { - log.Warnf("Unable to update terminal size: %v.\n", err) - continue - } - - // This is what we use to resize the physical terminal window itself. - os.Stdout.Write([]byte(fmt.Sprintf("\x1b[8;%d;%dt", lastSize.Height, lastSize.Width))) - - log.Debugf("Updated window size from to %v due to remote window change.", currSize, lastSize) - case <-ns.closer.C: - return - } - } -} - -// watchSignals register UNIX signal handlers and properly terminates a remote shell session -// must be called as a goroutine right after a remote shell is created -func (ns *NodeSession) watchSignals(shell io.Writer) { - exitSignals := make(chan os.Signal, 1) - // catch SIGTERM - signal.Notify(exitSignals, syscall.SIGTERM) - go func() { - defer ns.closer.Close() - <-exitSignals - }() - // Catch Ctrl-C signal - ctrlCSignal := make(chan os.Signal, 1) - signal.Notify(ctrlCSignal, syscall.SIGINT) - go func() { - for { - <-ctrlCSignal - _, err := shell.Write([]byte{3}) - if err != nil { - log.Errorf(err.Error()) - } - } - }() - // Catch Ctrl-Z signal - ctrlZSignal := make(chan os.Signal, 1) - signal.Notify(ctrlZSignal, syscall.SIGTSTP) - go func() { - for { - <-ctrlZSignal - _, err := shell.Write([]byte{26}) - if err != nil { - log.Errorf(err.Error()) - } - } - }() -} diff --git a/lib/client/session_windows.go b/lib/client/session_windows.go index e01b076505b..7e9c28f0085 100644 --- a/lib/client/session_windows.go +++ b/lib/client/session_windows.go @@ -1,40 +1,62 @@ +// +build windows + +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package client import ( - "io" - "os" - "os/signal" - "syscall" + "context" + "io" - "golang.org/x/crypto/ssh" + "github.com/gravitational/teleport/lib/session" - log "github.com/sirupsen/logrus" + "github.com/gravitational/trace" ) -func (ns *NodeSession) updateTerminalSize(s *ssh.Session) { - return +// NodeSession is a bare minimum implementation to get Windows to compile. +// This sits behind a build flag because github.com/docker/docker/pkg/term +// on Windows does not support "SetWinsize". Because tsh on Windows does not +// support "tsh ssh" this code will never be called. +type NodeSession struct { + ExitMsg string } -// watchSignals register UNIX signal handlers and properly terminates a remote shell session -// must be called as a goroutine right after a remote shell is created -func (ns *NodeSession) watchSignals(shell io.Writer) { - exitSignals := make(chan os.Signal, 1) - // catch SIGTERM - signal.Notify(exitSignals, syscall.SIGTERM) - go func() { - defer ns.closer.Close() - <-exitSignals - }() - // Catch Ctrl-C signal - ctrlCSignal := make(chan os.Signal, 1) - signal.Notify(ctrlCSignal, syscall.SIGINT) - go func() { - for { - <-ctrlCSignal - _, err := shell.Write([]byte{3}) - if err != nil { - log.Errorf(err.Error()) - } - } - }() +func newSession(client *NodeClient, + joinSession *session.Session, + env map[string]string, + stdin io.Reader, + stdout io.Writer, + stderr io.Writer) (*NodeSession, error) { + + return nil, trace.BadParameter("sessions not supported on Windows") +} + +func (ns *NodeSession) runCommand(ctx context.Context, cmd []string, callback ShellCreatedCallback, interactive bool) error { + return trace.BadParameter("sessions not supported on Windows") +} + +func (ns *NodeSession) runShell(callback ShellCreatedCallback) error { + return trace.BadParameter("sessions not supported on Windows") +} + +func (ns *NodeSession) NodeClient() *NodeClient { + return nil +} + +func (ns *NodeSession) Close() error { + return trace.BadParameter("sessions not supported on Windows") } diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 2ec71253ff1..27a4b874d68 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -12,7 +12,6 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - */ package client @@ -27,20 +26,19 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" "os/exec" - "path/filepath" "runtime" "strings" "time" + "github.com/gravitational/teleport" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/roundtrip" "github.com/gravitational/trace" + "github.com/mailgun/lemma/secret" log "github.com/sirupsen/logrus" - "github.com/tstranex/u2f" ) @@ -156,20 +154,8 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey proxyURL.Path = "/web/msg/info/login_success" redirectSuccessURL := proxyURL.String() - var ssoLoginURL string - makeHandler := func(fn func(http.ResponseWriter, *http.Request) (*auth.SSHLoginResponse, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/login" { - if ssoLoginURL == "" { - http.Error(w, "SSO Login URL is undefined", http.StatusInternalServerError) - } else { - http.Redirect(w, r, ssoLoginURL, http.StatusFound) - } - - return - } - response, err := fn(w, r) if err != nil { if trace.IsNotFound(err) { @@ -239,32 +225,37 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey return nil, trace.Wrap(err) } - ssoLoginURL = re.RedirectURL - + // If a command was found to launch the browser, create and start it. var execCmd *exec.Cmd - if runtime.GOOS == "windows" { - loginRedirectURL := server.URL + "/login" - fmt.Printf("If browser window does not open automatically, open it by clicking on the link:\n %v\n", loginRedirectURL) - execCmd = exec.Command(filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe"), "url.dll,FileProtocolHandler", loginRedirectURL) - } else { - fmt.Printf("If browser window does not open automatically, open it by clicking on the link:\n %v\n", re.RedirectURL) - - var command = "sensible-browser" - if runtime.GOOS == "darwin" { - command = "open" + switch runtime.GOOS { + // macOS. + case teleport.DarwinOS: + path, err := exec.LookPath(teleport.OpenBrowserDarwin) + if err == nil { + execCmd = exec.Command(path, re.RedirectURL) } - - path, err := exec.LookPath(command) + // Windows. + case teleport.WindowsOS: + path, err := exec.LookPath(teleport.OpenBrowserWindows) + if err == nil { + execCmd = exec.Command(path, "url.dll,FileProtocolHandler", re.RedirectURL) + } + // Linux or any other operating sytem. + default: + path, err := exec.LookPath(teleport.OpenBrowserLinux) if err == nil { execCmd = exec.Command(path, re.RedirectURL) } } - if execCmd != nil { execCmd.Start() } - log.Infof("waiting for response on %v", server.URL) + // Print to screen in-case the command that launches the browser did not run. + fmt.Printf("If browser window does not open automatically, open it by ") + fmt.Printf("clicking on the link:\n %v\n", re.RedirectURL) + + log.Infof("Waiting for response at: %v.", server.URL) select { case err := <-errorC: diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go index bf1ac933fca..c2835b0f584 100644 --- a/lib/defaults/defaults.go +++ b/lib/defaults/defaults.go @@ -439,3 +439,7 @@ const ( HMACSHA1 = "hmac-sha1" HMACSHA196 = "hmac-sha1-96" ) + +// WindowsOpenSSHNamedPipe is the address of the named pipe that the +// OpenSSH agent is on. +const WindowsOpenSSHNamedPipe = `\\.\pipe\openssh-ssh-agent` diff --git a/lib/events/api_test.go b/lib/events/api_test.go index 388748d7aad..4a1b5eac62f 100644 --- a/lib/events/api_test.go +++ b/lib/events/api_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package events import "gopkg.in/check.v1" diff --git a/lib/events/discard.go b/lib/events/discard.go index 1d6e42dc627..44549d2be5a 100644 --- a/lib/events/discard.go +++ b/lib/events/discard.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package events import ( diff --git a/lib/events/forward.go b/lib/events/forward.go index c7c4dc5a785..3b3ccafea8e 100644 --- a/lib/events/forward.go +++ b/lib/events/forward.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package events import ( diff --git a/lib/events/sessionlog.go b/lib/events/sessionlog.go index f17e0065089..2db9c58640b 100644 --- a/lib/events/sessionlog.go +++ b/lib/events/sessionlog.go @@ -17,6 +17,7 @@ limitations under the License. package events import ( + "archive/tar" "compress/gzip" "encoding/json" "fmt" @@ -136,6 +137,25 @@ func (sl *DiskSessionLogger) Close() error { return nil } +func openFileForTar(filename string) (*tar.Header, io.ReadCloser, error) { + fi, err := os.Stat(filename) + if err != nil { + return nil, nil, trace.ConvertSystemError(err) + } + + header, err := tar.FileInfoHeader(fi, "") + if err != nil { + return nil, nil, trace.ConvertSystemError(err) + } + + f, err := os.Open(filename) + if err != nil { + return nil, nil, trace.ConvertSystemError(err) + } + + return header, f, nil +} + // Finalize is called by the session when it's closing. This is where we're // releasing audit resources associated with the session func (sl *DiskSessionLogger) Finalize() error { diff --git a/lib/events/sessionlog_unix.go b/lib/events/sessionlog_unix.go deleted file mode 100644 index 3dc96c882e3..00000000000 --- a/lib/events/sessionlog_unix.go +++ /dev/null @@ -1,36 +0,0 @@ -// +build !windows - -package events - -import ( - "archive/tar" - "io" - "os" - "path/filepath" - "syscall" - - "github.com/gravitational/trace" -) - -func openFileForTar(filename string) (*tar.Header, io.ReadCloser, error) { - fi, err := os.Stat(filename) - if err != nil { - return nil, nil, trace.ConvertSystemError(err) - } - header := tar.Header{ - Name: filepath.Base(filename), - Size: fi.Size(), - Mode: int64(fi.Mode()), - ModTime: fi.ModTime(), - } - sys, ok := fi.Sys().(*syscall.Stat_t) - if ok { - header.Uid = int(sys.Uid) - header.Gid = int(sys.Gid) - } - f, err := os.Open(filename) - if err != nil { - return nil, nil, trace.ConvertSystemError(err) - } - return &header, f, nil -} diff --git a/lib/events/sessionlog_windows.go b/lib/events/sessionlog_windows.go deleted file mode 100644 index 1d80c230bb3..00000000000 --- a/lib/events/sessionlog_windows.go +++ /dev/null @@ -1,28 +0,0 @@ -package events - -import ( - "archive/tar" - "io" - "os" - "path/filepath" - - "github.com/gravitational/trace" -) - -func openFileForTar(filename string) (*tar.Header, io.ReadCloser, error) { - fi, err := os.Stat(filename) - if err != nil { - return nil, nil, trace.ConvertSystemError(err) - } - header := tar.Header{ - Name: filepath.Base(filename), - Size: fi.Size(), - Mode: int64(fi.Mode()), - ModTime: fi.ModTime(), - } - f, err := os.Open(filename) - if err != nil { - return nil, nil, trace.ConvertSystemError(err) - } - return &header, f, nil -} diff --git a/lib/shell/shell.go b/lib/shell/shell.go index 9ec287bf732..05c1bcecb95 100644 --- a/lib/shell/shell.go +++ b/lib/shell/shell.go @@ -16,72 +16,29 @@ limitations under the License. package shell -/* -#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS -#include -#include -#include -#include - -static int mygetpwnam_r(const char *name, struct passwd *pwd, - char *buf, size_t buflen, struct passwd **result) { - return getpwnam_r(name, pwd, buf, buflen, result); -} -*/ -import "C" - import ( - "os/user" - "strings" - "syscall" - "unsafe" - "github.com/gravitational/trace" - log "github.com/sirupsen/logrus" + + "github.com/sirupsen/logrus" ) const ( DefaultShell = "/bin/sh" ) -// GetLoginShell determines the login shell for a given username +// GetLoginShell determines the login shell for a given username. func GetLoginShell(username string) (string, error) { - // see if the username is valid - _, err := user.Lookup(username) + var err error + var shellcmd string + + shellcmd, err = getLoginShell(username) if err != nil { + if !trace.IsNotFound(err) { + logrus.Warnf("No shell specified for %v, using default %v.", username, DefaultShell) + return DefaultShell, nil + } return "", trace.Wrap(err) } - // based on stdlib user/lookup_unix.go packages which does not return user shell - // https://golang.org/src/os/user/lookup_unix.go - var pwd C.struct_passwd - var result *C.struct_passwd - - bufSize := C.sysconf(C._SC_GETPW_R_SIZE_MAX) - if bufSize == -1 { - bufSize = 1024 - } - if bufSize <= 0 || bufSize > 1<<20 { - return "", trace.Errorf("lookupPosixShell: unreasonable _SC_GETPW_R_SIZE_MAX of %d", bufSize) - } - buf := C.malloc(C.size_t(bufSize)) - defer C.free(buf) - var rv C.int - nameC := C.CString(username) - defer C.free(unsafe.Pointer(nameC)) - rv = C.mygetpwnam_r(nameC, - &pwd, - (*C.char)(buf), - C.size_t(bufSize), - &result) - if rv != 0 || result == nil { - log.Errorf("lookupPosixShell: lookup username %s: %s", username, syscall.Errno(rv)) - return "", trace.Errorf("cannot determine shell for %s", username) - } - shellCmd := strings.TrimSpace(C.GoString(pwd.pw_shell)) - if len(shellCmd) == 0 { - log.Warnf("no shell specified for %s. using default=%s", username, DefaultShell) - shellCmd = DefaultShell - } - return shellCmd, nil + return shellcmd, nil } diff --git a/lib/shell/shell_native.go b/lib/shell/shell_native.go deleted file mode 100644 index 3456b8483c4..00000000000 --- a/lib/shell/shell_native.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !cgo - -package shell - -const ( - DefaultShell = "/bin/sh" -) - -func GetLoginShell(username string) (string, error) { - return DefaultShell, nil -} diff --git a/lib/shell/shell_unix.go b/lib/shell/shell_unix.go new file mode 100644 index 00000000000..8dfadd8e36d --- /dev/null +++ b/lib/shell/shell_unix.go @@ -0,0 +1,88 @@ +// +build !windows + +/* +Copyright 2017 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package shell + +/* +#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS +#include +#include +#include +#include + +static int mygetpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result) { + return getpwnam_r(name, pwd, buf, buflen, result); +} +*/ +import "C" + +import ( + "os/user" + "strings" + "syscall" + "unsafe" + + "github.com/gravitational/trace" + log "github.com/sirupsen/logrus" +) + +// getLoginShell determines the login shell for a given username +func getLoginShell(username string) (string, error) { + // See if the username is valid. + _, err := user.Lookup(username) + if err != nil { + return "", trace.Wrap(err) + } + + // Based on stdlib user/lookup_unix.go packages which does not return + // user shell: https://golang.org/src/os/user/lookup_unix.go + var pwd C.struct_passwd + var result *C.struct_passwd + + bufSize := C.sysconf(C._SC_GETPW_R_SIZE_MAX) + if bufSize == -1 { + bufSize = 1024 + } + if bufSize <= 0 || bufSize > 1<<20 { + return "", trace.BadParameter("lookupPosixShell: unreasonable _SC_GETPW_R_SIZE_MAX of %d", bufSize) + } + buf := C.malloc(C.size_t(bufSize)) + defer C.free(buf) + var rv C.int + nameC := C.CString(username) + defer C.free(unsafe.Pointer(nameC)) + rv = C.mygetpwnam_r(nameC, + &pwd, + (*C.char)(buf), + C.size_t(bufSize), + &result) + if rv != 0 || result == nil { + log.Errorf("lookupPosixShell: lookup username %s: %s", username, syscall.Errno(rv)) + return "", trace.BadParameter("cannot determine shell for %s", username) + } + + // If no shell was found, return trace.NotFound to allow the caller to set + // the default shell. + shellCmd := strings.TrimSpace(C.GoString(pwd.pw_shell)) + if len(shellCmd) == 0 { + return "", trace.NotFound("no shell specified for %v", username) + } + + return shellCmd, nil +} diff --git a/lib/shell/shell_windows.go b/lib/shell/shell_windows.go new file mode 100644 index 00000000000..7179a3f43bd --- /dev/null +++ b/lib/shell/shell_windows.go @@ -0,0 +1,29 @@ +// +build windows + +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package shell + +import ( + "github.com/gravitational/trace" +) + +// getLoginShell always return an error on Windows. This code his behind a +// build flag to allow cross compilation (Unix version uses CGO). +func getLoginShell(username string) (string, error) { + return "", trace.BadParameter("login shell on Windows is not supported") +} diff --git a/lib/utils/agentconn/agent_unix.go b/lib/utils/agentconn/agent_unix.go new file mode 100644 index 00000000000..ce5912f33a7 --- /dev/null +++ b/lib/utils/agentconn/agent_unix.go @@ -0,0 +1,35 @@ +// +build !windows + +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package agentconn + +import ( + "net" + + "github.com/gravitational/trace" +) + +// Dial creates net.Conn to a SSH agent listening on a Unix socket. +func Dial(socket string) (net.Conn, error) { + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, trace.Wrap(err) + } + + return conn, nil +} diff --git a/lib/utils/agentconn/agent_windows.go b/lib/utils/agentconn/agent_windows.go new file mode 100644 index 00000000000..2e0b7ff9995 --- /dev/null +++ b/lib/utils/agentconn/agent_windows.go @@ -0,0 +1,41 @@ +// +build windows + +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package agentconn + +import ( + "net" + + "github.com/gravitational/teleport/lib/defaults" + + "github.com/gravitational/trace" + + "github.com/Microsoft/go-winio" +) + +// Dial creates net.Conn to a SSH agent listening on a Windows named pipe. +// This is behind a build flag because winio.DialPipe is only available on +// Windows. +func Dial(socket string) (net.Conn, error) { + conn, err := winio.DialPipe(defaults.WindowsOpenSSHNamedPipe, nil) + if err != nil { + return nil, trace.Wrap(err) + } + + return conn, nil +} diff --git a/lib/utils/agentconn/agentconn.go b/lib/utils/agentconn/agentconn.go deleted file mode 100644 index 8f096d29ab7..00000000000 --- a/lib/utils/agentconn/agentconn.go +++ /dev/null @@ -1,23 +0,0 @@ -package agentconn - -import ( - "fmt" - "net" - "os" -) - -const SocketEnvironmentVariableName = "SSH_AUTH_SOCK" - -func DialDefaultAgent() (net.Conn, error) { - socket := os.Getenv(SocketEnvironmentVariableName) - - if socket == "" { - return nil, fmt.Errorf("%s is not set") - } - - return dialAgent(socket) -} - -func DialAgent(socket string) (net.Conn, error) { - return dialAgent(socket) -} diff --git a/lib/utils/agentconn/agentconn_unix.go b/lib/utils/agentconn/agentconn_unix.go deleted file mode 100644 index 590dc809609..00000000000 --- a/lib/utils/agentconn/agentconn_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !windows - -package agentconn - -import "net" - -func dialAgent(socket string) (net.Conn, error) { - return net.Dial("unix", socket) -} diff --git a/lib/utils/agentconn/agentconn_windows.go b/lib/utils/agentconn/agentconn_windows.go deleted file mode 100644 index 8514a8c1a3a..00000000000 --- a/lib/utils/agentconn/agentconn_windows.go +++ /dev/null @@ -1,85 +0,0 @@ -package agentconn - -import ( - "encoding/binary" - "fmt" - "io/ioutil" - "net" - "os" - "regexp" - - "github.com/Microsoft/go-winio" -) - -// The Cygwin support below was originally based on this implementation: -// https://github.com/abourget/secrets-bridge/blob/master/pkg/agentfwd/agentconn_windows.go - -const microsoftOpenSSHAgentPipe = "\\\\.\\pipe\\openssh-ssh-agent" - -var windowsFakeSocket = regexp.MustCompile(`!(\d+) ([A-Fa-f0-9-]+)`) - -func dialAgent(socket string) (net.Conn, error) { - // If the socket path isn't set, or is set to the named pipe associated with - // Microsoft's native OpenSSH implementation, try connecting with a native - // Windows named pipe. - if socket == "" || socket == microsoftOpenSSHAgentPipe { - return winio.DialPipe(microsoftOpenSSHAgentPipe, nil) - } - - // If a socket path was specified, fall back to Cygwin's Unix socket emulation - socketFileData, err := ioutil.ReadFile(socket) - if err != nil { - return nil, err - } - - matches := windowsFakeSocket.FindStringSubmatch(string(socketFileData)) - if matches == nil { - return nil, fmt.Errorf("couldn't parse SSH_AUTH_SOCK file %s", socket) - } - - tcpPort := matches[1] - key := matches[2] - - conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", tcpPort)) - if err != nil { - return conn, err - } - - b := make([]byte, 16) - fmt.Sscanf(key, - "%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x-%02x%02x%02x%02x", - &b[3], &b[2], &b[1], &b[0], - &b[7], &b[6], &b[5], &b[4], - &b[11], &b[10], &b[9], &b[8], - &b[15], &b[14], &b[13], &b[12], - ) - - if _, err = conn.Write(b); err != nil { - return nil, fmt.Errorf("write b: %v", err) - } - - b2 := make([]byte, 16) - if _, err = conn.Read(b2); err != nil { - return nil, fmt.Errorf("read b2: %v", err) - } - - pidsUids := make([]byte, 12) - pid := os.Getpid() - uid := 0 - gid := pid // for cygwin's AF_UNIX -> AF_INET, pid = gid - - binary.LittleEndian.PutUint32(pidsUids, uint32(pid)) - binary.LittleEndian.PutUint32(pidsUids[4:], uint32(uid)) - binary.LittleEndian.PutUint32(pidsUids[8:], uint32(gid)) - - if _, err = conn.Write(pidsUids); err != nil { - return nil, fmt.Errorf("write pid,uid,gid: %v", err) - } - - b3 := make([]byte, 12) - if _, err = conn.Read(b3); err != nil { - return nil, fmt.Errorf("read pid,uid,gid: %v", err) - } - - return conn, nil -} diff --git a/lib/utils/cli.go b/lib/utils/cli.go index a2d18e73db0..c32384c4556 100644 --- a/lib/utils/cli.go +++ b/lib/utils/cli.go @@ -49,7 +49,13 @@ func InitLogger(purpose LoggingPurpose, level log.Level) { switch purpose { case LoggingForCLI: - SwitchLoggingtoSyslog() + // If debug logging was asked for on the CLI, then write logs to stderr. + // Otherwise discard all logs. + if level == log.DebugLevel { + log.SetOutput(os.Stderr) + } else { + log.SetOutput(ioutil.Discard) + } case LoggingForDaemon: log.SetOutput(os.Stderr) case LoggingForTests: diff --git a/lib/utils/cli_nosyslog.go b/lib/utils/cli_nosyslog.go deleted file mode 100644 index 2b1269f36f1..00000000000 --- a/lib/utils/cli_nosyslog.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build windows - -package utils - -import ( - "os" - - log "github.com/sirupsen/logrus" -) - -func SwitchLoggingtoSyslog() { - // syslog is not available on windows - log.SetOutput(os.Stderr) -} diff --git a/lib/utils/cli_syslog.go b/lib/utils/cli_syslog.go deleted file mode 100644 index ccf3a87761f..00000000000 --- a/lib/utils/cli_syslog.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build !windows,!nacl,!plan9 - -package utils - -import ( - "log/syslog" - "os" - - log "github.com/sirupsen/logrus" - logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" -) - -// SwitchLoggingtoSyslog tells the logger to send the output to syslog -func SwitchLoggingtoSyslog() { - log.StandardLogger().SetHooks(make(log.LevelHooks)) - hook, err := logrusSyslog.NewSyslogHook("", "", syslog.LOG_WARNING, "") - if err != nil { - // syslog not available - log.SetOutput(os.Stderr) - } else { - // ... and disable stderr: - log.AddHook(hook) - log.SetOutput(ioutil.Discard) - } -} diff --git a/lib/utils/fs.go b/lib/utils/fs.go index a4c8d1b83bb..758c87d6db3 100644 --- a/lib/utils/fs.go +++ b/lib/utils/fs.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "runtime" "github.com/gravitational/teleport" "github.com/gravitational/trace" @@ -34,7 +35,7 @@ import ( // It also makes sure that base dir exists func EnsureLocalPath(customPath string, defaultLocalDir, defaultLocalPath string) (string, error) { if customPath == "" { - homeDir := os.Getenv(teleport.EnvHome) + homeDir := getHomeDir() if homeDir == "" { return "", trace.BadParameter("no path provided and environment variable %v is not not set", teleport.EnvHome) } @@ -156,3 +157,16 @@ func StatDir(path string) (os.FileInfo, error) { } return fi, nil } + +// getHomeDir returns the home directory based off the OS. +func getHomeDir() string { + switch runtime.GOOS { + case teleport.LinuxOS: + return os.Getenv(teleport.EnvHome) + case teleport.DarwinOS: + return os.Getenv(teleport.EnvHome) + case teleport.WindowsOS: + return os.Getenv(teleport.EnvUserProfile) + } + return "" +} diff --git a/lib/utils/fs_unix.go b/lib/utils/fs_unix.go index 34def9289b7..7b9ae2d0476 100644 --- a/lib/utils/fs_unix.go +++ b/lib/utils/fs_unix.go @@ -1,12 +1,28 @@ // +build !windows +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package utils import ( - "os" - "syscall" + "os" + "syscall" - "github.com/gravitational/trace" + "github.com/gravitational/trace" ) // FSWriteLock grabs Flock-style filesystem lock on an open file diff --git a/lib/utils/fs_windows.go b/lib/utils/fs_windows.go index 845f9348eaf..bc75b51749a 100644 --- a/lib/utils/fs_windows.go +++ b/lib/utils/fs_windows.go @@ -1,21 +1,45 @@ +// +build windows + package utils -import "os" +/* +Copyright 2018 Gravitational, Inc. -// For the moment, these functions are no-ops that should be replaced with a -// native implementation in the future. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import ( + "os" + + "github.com/gravitational/trace" +) + +// FSWriteLock not supported on Windows. func FSWriteLock(f *os.File) error { - return nil + return trace.BadParameter("file locking not supported on Windows") } +// FSTryWriteLock not supported on Windows. func FSTryWriteLock(f *os.File) error { - return nil + return trace.BadParameter("file locking not supported on Windows") } +// FSReadLock not supported on Windows. func FSReadLock(f *os.File) error { - return nil + return trace.BadParameter("file locking not supported on Windows") } +// FSUnlock not supported on Windows. func FSUnlock(f *os.File) error { - return nil + return trace.BadParameter("file locking not supported on Windows") } diff --git a/lib/utils/syslog.go b/lib/utils/syslog.go new file mode 100644 index 00000000000..acb9871d571 --- /dev/null +++ b/lib/utils/syslog.go @@ -0,0 +1,43 @@ +// +build !windows + +/* +Copyright 2018 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "io/ioutil" + "log/syslog" + "os" + + log "github.com/sirupsen/logrus" + logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" +) + +// SwitchLoggingtoSyslog tells the logger to send the output to syslog. This +// code is behind a build flag because Windows does not support syslog. +func SwitchLoggingtoSyslog() { + log.StandardLogger().SetHooks(make(log.LevelHooks)) + hook, err := logrusSyslog.NewSyslogHook("", "", syslog.LOG_WARNING, "") + if err != nil { + // syslog not available + log.SetOutput(os.Stderr) + } else { + // ... and disable stderr: + log.AddHook(hook) + log.SetOutput(ioutil.Discard) + } +} diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index a6274028c96..f7644cc8d57 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -27,6 +27,7 @@ import ( "os" "os/signal" "path" + "runtime" "strings" "syscall" "time" @@ -245,6 +246,16 @@ func Run(args []string, underTest bool) { // about the certificate. status := app.Command("status", "Display the list of proxy servers and retrieved certificates") + // On Windows, hide the "ssh", "join", "play", "scp", and "bench" commands + // because they all use a terminal. + if runtime.GOOS == teleport.WindowsOS { + ssh.Hidden() + join.Hidden() + play.Hidden() + scp.Hidden() + bench.Hidden() + } + // parse CLI commands+flags: command, err := app.Parse(args) if err != nil {