-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: update sessionhandler and command line for logs support
- Loading branch information
Showing
4 changed files
with
178 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
// Package main implements the ssh-portal executable. | ||
package main | ||
|
||
import ( | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
package sshserver | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/gliderlabs/ssh" | ||
"github.com/prometheus/client_golang/prometheus" | ||
|
@@ -44,20 +46,19 @@ func getSSHIntent(sftp bool, cmd []string) []string { | |
// handler is that the command is set to sftp-server. This implies that the | ||
// target container must have a sftp-server binary installed for sftp to work. | ||
// There is no support for a built-in sftp server. | ||
func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { | ||
func sessionHandler(log *zap.Logger, c *k8s.Client, | ||
sftp, logAccessEnabled bool) ssh.Handler { | ||
return func(s ssh.Session) { | ||
sessionTotal.Inc() | ||
ctx := s.Context() | ||
sid := ctx.SessionID() | ||
// start the command | ||
log.Debug("starting command exec", | ||
log.Debug("starting session", | ||
zap.String("sessionID", sid), | ||
zap.Strings("rawCommand", s.Command()), | ||
zap.String("subsystem", s.Subsystem()), | ||
) | ||
// parse the command line arguments to extract any service or container args | ||
service, container, rawCmd := parseConnectionParams(s.Command()) | ||
cmd := getSSHIntent(sftp, rawCmd) | ||
service, container, logs, rawCmd := parseConnectionParams(s.Command()) | ||
// validate the service and container | ||
if err := k8s.ValidateLabelValue(service); err != nil { | ||
log.Debug("invalid service name", | ||
|
@@ -103,8 +104,6 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { | |
} | ||
return | ||
} | ||
// check if a pty was requested, and get the window size channel | ||
_, winch, pty := s.Pty() | ||
// extract info passed through the context by the authhandler | ||
eid, ok := ctx.Value(environmentIDKey).(int) | ||
if !ok { | ||
|
@@ -126,6 +125,71 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { | |
if !ok { | ||
log.Warn("couldn't extract SSH key fingerprint from session context") | ||
} | ||
if len(logs) != 0 { | ||
if !logAccessEnabled { | ||
log.Debug("logs access is not enabled", | ||
zap.String("logsArgument", logs), | ||
zap.String("sessionID", sid)) | ||
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", | ||
sid) | ||
if err != nil { | ||
log.Warn("couldn't send error to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
// Send a non-zero exit code to the client on internal logs error. | ||
// OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to | ||
// differentiate this error. | ||
if err = s.Exit(253); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
return | ||
} | ||
follow, tailLines, err := parseLogsArg(service, logs, rawCmd) | ||
if err != nil { | ||
log.Debug("couldn't parse logs argument", | ||
zap.String("logsArgument", logs), | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", | ||
sid) | ||
if err != nil { | ||
log.Warn("couldn't send error to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
// Send a non-zero exit code to the client on internal logs error. | ||
// OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to | ||
// differentiate this error. | ||
if err = s.Exit(253); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
return | ||
} | ||
log.Info("sending logs to SSH client", | ||
zap.Int("environmentID", eid), | ||
zap.Int("projectID", pid), | ||
zap.String("SSHFingerprint", fingerprint), | ||
zap.String("container", container), | ||
zap.String("deployment", deployment), | ||
zap.String("environmentName", ename), | ||
zap.String("namespace", s.User()), | ||
zap.String("projectName", pname), | ||
zap.String("sessionID", sid), | ||
zap.Bool("follow", follow), | ||
zap.Int64("tailLines", tailLines), | ||
) | ||
doLogs(ctx, log, s, deployment, container, follow, tailLines, c, sid) | ||
return | ||
} | ||
// handle sftp and sh fallback | ||
cmd := getSSHIntent(sftp, rawCmd) | ||
// check if a pty was requested, and get the window size channel | ||
_, winch, pty := s.Pty() | ||
log.Info("executing SSH command", | ||
zap.Bool("pty", pty), | ||
zap.Int("environmentID", eid), | ||
|
@@ -139,39 +203,108 @@ func sessionHandler(log *zap.Logger, c *k8s.Client, sftp bool) ssh.Handler { | |
zap.String("sessionID", sid), | ||
zap.Strings("command", cmd), | ||
) | ||
err = c.Exec(ctx, s.User(), deployment, container, cmd, s, | ||
s.Stderr(), pty, winch) | ||
doExec(ctx, log, s, deployment, container, cmd, c, pty, winch, sid) | ||
} | ||
} | ||
|
||
// startClientKeepalive sends a keepalive request to the client via the channel | ||
// embedded in ssh.Session at a regular interval. If the client fails to | ||
// respond, the channel is closed, and cancel is called. | ||
func startClientKeepalive(ctx context.Context, cancel context.CancelFunc, | ||
log *zap.Logger, s ssh.Session) { | ||
ticker := time.NewTicker(2 * time.Second) | ||
defer ticker.Stop() | ||
for { | ||
select { | ||
case <-ticker.C: | ||
// https://github.com/openssh/openssh-portable/blob/ | ||
// edc2ef4e418e514c99701451fae4428ec04ce538/serverloop.c#L127-L158 | ||
_, err := s.SendRequest("[email protected]", true, nil) | ||
if err != nil { | ||
log.Debug("client closed connection", zap.Error(err)) | ||
_ = s.Close() | ||
cancel() | ||
return | ||
} | ||
case <-ctx.Done(): | ||
return | ||
} | ||
} | ||
} | ||
|
||
func doLogs(ctx ssh.Context, log *zap.Logger, s ssh.Session, deployment, | ||
container string, follow bool, tailLines int64, c *k8s.Client, sid string) { | ||
// Wrap the ssh.Context so we can cancel goroutines started from this | ||
// function without affecting the SSH session. | ||
childCtx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
// In a multiplexed connection (multiple SSH channels to the single TCP | ||
// connection), if the client disconnects from the channel the session | ||
// context will not be cancelled (because the TCP connection is still up), | ||
// and k8s.Logs() will hang. | ||
// | ||
// To work around this problem, start a goroutine to send a regular keepalive | ||
// ping to the client. If the keepalive fails, close the channel and cancel | ||
// the childCtx. | ||
go startClientKeepalive(childCtx, cancel, log, s) | ||
err := c.Logs(childCtx, s.User(), deployment, container, follow, tailLines, s) | ||
if err != nil { | ||
log.Warn("couldn't send logs", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", | ||
sid) | ||
if err != nil { | ||
if exitErr, ok := err.(exec.ExitError); ok { | ||
log.Debug("couldn't execute command", | ||
log.Warn("couldn't send error to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
// Send a non-zero exit code to the client on internal logs error. | ||
// OpenSSH uses 255 for this, 254 is an exec failure, so use 253 to | ||
// differentiate this error. | ||
if err = s.Exit(253); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
} | ||
log.Debug("finished command logs", zap.String("sessionID", sid)) | ||
} | ||
|
||
func doExec(ctx ssh.Context, log *zap.Logger, s ssh.Session, deployment, | ||
container string, cmd []string, c *k8s.Client, pty bool, | ||
winch <-chan ssh.Window, sid string) { | ||
err := c.Exec(ctx, s.User(), deployment, container, cmd, s, | ||
s.Stderr(), pty, winch) | ||
if err != nil { | ||
if exitErr, ok := err.(exec.ExitError); ok { | ||
log.Debug("couldn't execute command", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
if err = s.Exit(exitErr.ExitStatus()); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
if err = s.Exit(exitErr.ExitStatus()); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
} else { | ||
log.Warn("couldn't execute command", | ||
} | ||
} else { | ||
log.Warn("couldn't execute command", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", | ||
sid) | ||
if err != nil { | ||
log.Warn("couldn't send error to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
// Send a non-zero exit code to the client on internal exec error. | ||
// OpenSSH uses 255 for this, so use 254 to differentiate the error. | ||
if err = s.Exit(254); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
_, err = fmt.Fprintf(s.Stderr(), "error executing command. SID: %s\r\n", | ||
sid) | ||
if err != nil { | ||
log.Warn("couldn't send error to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
// Send a non-zero exit code to the client on internal exec error. | ||
// OpenSSH uses 255 for this, so use 254 to differentiate the error. | ||
if err = s.Exit(254); err != nil { | ||
log.Warn("couldn't send exit code to client", | ||
zap.String("sessionID", sid), | ||
zap.Error(err)) | ||
} | ||
} | ||
} | ||
log.Debug("finished command exec", | ||
zap.String("sessionID", sid)) | ||
} | ||
log.Debug("finished command exec", zap.String("sessionID", sid)) | ||
} |