From 21463a5cadc4875f9e5a1716757392659820791d Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Fri, 2 Aug 2024 20:58:46 +0200 Subject: [PATCH] * session will now expire some time after last user activity and updated documentation. * downloads will now download again. Because of hx-boost the downloads where rendered in the browser. Now disabling hx-boost for the downloads section. * relative link for sessions page --- cmd/agent/agent.go | 54 +++++++++++----- pkg/agent/session/help.txt | 16 ++--- pkg/agent/session/session.go | 92 +++++++++++++++++----------- pkg/server/templates/downloads.templ | 2 +- pkg/server/templates/sessions.templ | 2 +- pkg/server/templates/usage.templ | 6 +- 6 files changed, 111 insertions(+), 61 deletions(-) diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 7355a82..fe64882 100755 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -60,21 +60,40 @@ func SftpHandler(sess ssh.Session) { } } +type UserActivityDetector struct { + session io.ReadWriter +} + +func (user UserActivityDetector) Read(p []byte) (int, error) { + n, err := user.session.Read(p) + if err == nil && n > 0 { + session.UserActivityDetected() + } + return n, err +} + +func (user UserActivityDetector) Write(p []byte) (int, error) { + return user.session.Write(p) +} + func sshServer(hostKeyFile string, shellCommand string, passwordHandler ssh.PasswordHandler, authorizedPublicKeys AuthorizedPublicKeys) *ssh.Server { - ssh.Handle(func(s ssh.Session) { + ssh.Handle(func(sshSession ssh.Session) { workingDirectory, _ := os.Getwd() env := append(os.Environ(), fmt.Sprintf("agentdir=%s", workingDirectory)) - process, err := terminal.PtySpawner.Start(s, env, shellCommand) + process, err := terminal.PtySpawner.Start(sshSession, env, shellCommand) if err != nil { panic(err) } sessionInfo := comms.NewSessionInfo( - s.LocalAddr().String(), "ssh", + sshSession.LocalAddr().String(), "ssh", ) - session.Login(sessionInfo, s) - iowrappers.SynchronizeStreams("shell -- ssh", process.Pipe(), s) + session.Login(sessionInfo, sshSession) + activityDetector := UserActivityDetector{ + session: sshSession, + } + iowrappers.SynchronizeStreams("shell -- ssh", process.Pipe(), activityDetector) session.LogOut(sessionInfo.ClientId) // will cause addition goroutines to remmain alive when the SSH // session is killed. For now acceptable since the agent is a short-lived @@ -171,8 +190,8 @@ func printHelp(msg string) { " password based access is possible. The password is configured on the converge server\n" + "--authorized-keys: SSH authorized keys file in openssh format. By default .authorized_keys in the\n" + " directory where the agent is started is used.\n" + - "--warning-time: advance warning time before sessio ends\n" + - "--expiry-time: expiry time of the session\n" + + "--warning-time: advance warning time before sessio ends (default '5m')\n" + + "--expiry-time: expiry time of the session (default '10m')\n" + "--check-interval: interval at which expiry is checked\n" + "--insecure: allow invalid certificates\n" + "--shells: comma-separated list of shells to add to the front of theshell search path\n" + @@ -191,12 +210,13 @@ func getArg(args []string) (value string, ret []string) { return args[1], args[1:] } -func parseDuration(args []string, val string) (time.Duration, []string) { - duration, err := time.ParseDuration(val) +func parseDuration(args []string) (time.Duration, []string) { + arg, args := getArg(args) + duration, err := time.ParseDuration(arg) if err != nil { printHelp(fmt.Sprintf("Error parsing duration: %v\n", err)) } - return duration, args[1:] + return duration, args } func main() { @@ -224,18 +244,17 @@ func main() { additionalShells := []string{} commaSeparated := "" for len(args) > 0 && strings.HasPrefix(args[0], "-") { - val := "" switch args[0] { case "--id": id, args = getArg(args) case "--authorized-keys": authorizedKeysFile, args = getArg(args) case "--warning-time": - advanceWarningTime, args = parseDuration(args, val) + advanceWarningTime, args = parseDuration(args) case "--expiry-time": - agentExpriryTime, args = parseDuration(args, val) + agentExpriryTime, args = parseDuration(args) case "--check-interval": - tickerInterval, args = parseDuration(args, val) + tickerInterval, args = parseDuration(args) case "--insecure": insecure = true case "--shells": @@ -247,6 +266,13 @@ func main() { args = args[1:] } + if 2*advanceWarningTime > agentExpriryTime { + printHelp("The warning time should be at most half the expiry time") + } + if 4*tickerInterval > agentExpriryTime { + printHelp("The check interval should be at most 1/4 of the agent interval") + } + shells = append(additionalShells, shells...) id = getId(id) diff --git a/pkg/agent/session/help.txt b/pkg/agent/session/help.txt index d970786..e40b601 100644 --- a/pkg/agent/session/help.txt +++ b/pkg/agent/session/help.txt @@ -1,5 +1,10 @@ -You can extend expiry of the session using +The session is automatically extended with %v every time the user is active. +If you get a warning that expiry of the session is near then simply pressing +any key on the keyboard will extend it. + +If the last users logs out, then by default the agent will exit with status 0. +To prevent this, create a .hold file in the agent directory as follows: {{ if eq .os "windows" -}} # cmd @@ -11,9 +16,6 @@ You can extend expiry of the session using {{- end }} The expiry time is equal to the modification time of the .hold -file with the expiry duration (%v) added. - -To prevent the agent from exiting after the last session is gone, -also use the above command in any shell. - - +file with the expiry duration added. By creating a .hold +file with a timestamp in the future, the session can be extended +beyond the default session timeout. diff --git a/pkg/agent/session/session.go b/pkg/agent/session/session.go index 124c47c..dbed5f6 100644 --- a/pkg/agent/session/session.go +++ b/pkg/agent/session/session.go @@ -33,13 +33,14 @@ type AgentState struct { startTime time.Time // Advance warning time to notify the user of something important happening - advanceWarningTime time.Duration + advanceWarningDuration time.Duration // session expiry time - agentExpriryTime time.Duration + agentExpiryDuration time.Duration // Last expiry time reported to the user. - lastExpiryTimmeReported time.Time + lastExpiryTimeReported time.Time + expiryIsNear bool // ticker tickerInterval time.Duration @@ -48,8 +49,8 @@ type AgentState struct { // map of unique session id to a session clients map[string]*AgentSession - lastUserLoginTime time.Time - agentUsed bool + lastUserActivityTime time.Time + agentUsed bool } type AgentSession struct { @@ -82,21 +83,20 @@ func ConfigureAgent(commChannel comms.CommChannel, } } state = AgentState{ - commChannel: commChannel, - startTime: time.Now(), - advanceWarningTime: advanceWarningTime, - agentExpriryTime: agentExpiryTime, - lastExpiryTimmeReported: time.Time{}, - tickerInterval: tickerInterval, - ticker: time.NewTicker(tickerInterval), - clients: make(map[string]*AgentSession), + commChannel: commChannel, + startTime: time.Now(), + advanceWarningDuration: advanceWarningTime, + agentExpiryDuration: agentExpiryTime, + lastExpiryTimeReported: time.Time{}, + tickerInterval: tickerInterval, + ticker: time.NewTicker(tickerInterval), + clients: make(map[string]*AgentSession), - lastUserLoginTime: time.Time{}, - agentUsed: false, + lastUserActivityTime: time.Time{}, + agentUsed: false, } - log.Printf("Agent expires at %s", - state.expiryTime(holdFilename).Format(time.DateTime)) + log.Printf("Agent expiry duration is %v", state.agentExpiryDuration) comms.Send(state.commChannel.SideChannel, comms.ConvergeMessage{ @@ -131,8 +131,23 @@ func LogOut(clientId string) { } } +func UserActivityDetected() { + events <- func() { + userActivityDetected() + } +} + // Internal interface synchronous +func userActivityDetected() { + state.lastUserActivityTime = time.Now() + if state.expiryIsNear { + messageUsers("User activity detected, session extended.") + sendExpiryTimeUpdateEvent() + state.expiryIsNear = false + } +} + func monitorHoldFile() { watcher, err := fsnotify.NewWatcher() if err != nil { @@ -200,7 +215,7 @@ func login(sessionInfo comms.SessionInfo, sshSession ssh.Session) { sshSession: sshSession, } state.clients[sessionInfo.ClientId] = &agentSession - state.lastUserLoginTime = time.Now() + state.lastUserActivityTime = time.Now() state.agentUsed = true err := comms.SendWithTimeout(state.commChannel.SideChannel, @@ -218,7 +233,7 @@ func login(sessionInfo comms.SessionInfo, sshSession ssh.Session) { func printHelpMessage(sshSession ssh.Session) { printMessage(sshSession, fmt.Sprintf(helpMessage, - state.agentExpriryTime)) + state.agentExpiryDuration)) } func formatHelpMessage() string { @@ -279,14 +294,14 @@ func fileExists(filename string) bool { func (state *AgentState) expiryTime(filename string) time.Time { if !state.agentUsed { - return state.startTime.Add(state.agentExpriryTime) + return state.startTime.Add(state.agentExpiryDuration) } expiryTime := time.Time{} stats, err := os.Stat(filename) if err == nil { - expiryTime = stats.ModTime().Add(state.agentExpriryTime) + expiryTime = stats.ModTime().Add(state.agentExpiryDuration) } - userLoginBaseExpiryTime := state.lastUserLoginTime.Add(state.agentExpriryTime) + userLoginBaseExpiryTime := state.lastUserActivityTime.Add(state.agentExpiryDuration) if userLoginBaseExpiryTime.After(expiryTime) { expiryTime = userLoginBaseExpiryTime } @@ -294,20 +309,24 @@ func (state *AgentState) expiryTime(filename string) time.Time { } func holdFileChange() { - newExpiryTIme := state.expiryTime(holdFilename) - if newExpiryTIme != state.lastExpiryTimmeReported { + newExpiryTime := state.expiryTime(holdFilename) + if newExpiryTime != state.lastExpiryTimeReported { message := fmt.Sprintf("Expiry time of session is now %s\n", - newExpiryTIme.Format(time.DateTime)) + newExpiryTime.Format(time.DateTime)) message += holdFileMessage() messageUsers(message) - state.lastExpiryTimmeReported = newExpiryTIme - comms.Send(state.commChannel.SideChannel, - comms.ConvergeMessage{ - Value: comms.NewExpiryTimeUpdate(state.expiryTime(holdFilename)), - }) + state.lastExpiryTimeReported = newExpiryTime + sendExpiryTimeUpdateEvent() } } +func sendExpiryTimeUpdateEvent() error { + return comms.Send(state.commChannel.SideChannel, + comms.ConvergeMessage{ + Value: comms.NewExpiryTimeUpdate(state.expiryTime(holdFilename)), + }) +} + // Behavior to implement // 1. there is a global timeout for all agent clients together: state.agentExpirtyTime // 2. The expiry time is relative to the modification time of the .hold file in the @@ -329,18 +348,21 @@ func check() { os.Exit(0) } - if expiryTime.Sub(now) < state.advanceWarningTime { + state.expiryIsNear = expiryTime.Sub(now) < state.advanceWarningDuration + if state.expiryIsNear { messageUsers( - fmt.Sprintf("Session will expire at %s", expiryTime.Format(time.DateTime))) - for _, session := range state.clients { - printHelpMessage(session.sshSession) - } + fmt.Sprintf("Session will expire at %s, press any key to extend it.", expiryTime.Format(time.DateTime))) + //for _, session := range state.clients { + // printHelpMessage(session.sshSession) + //} } if state.agentUsed && !fileExists(holdFilename) && len(state.clients) == 0 { log.Printf("All clients disconnected and no '%s' file found, exiting", holdFilename) os.Exit(0) } + + sendExpiryTimeUpdateEvent() } func messageUsers(message string) { diff --git a/pkg/server/templates/downloads.templ b/pkg/server/templates/downloads.templ index 99ea791..2899ab1 100644 --- a/pkg/server/templates/downloads.templ +++ b/pkg/server/templates/downloads.templ @@ -2,7 +2,7 @@ package templates templ Downloads() { -
+

downloads

diff --git a/pkg/server/templates/sessions.templ b/pkg/server/templates/sessions.templ index 8a1eb96..6886cc9 100644 --- a/pkg/server/templates/sessions.templ +++ b/pkg/server/templates/sessions.templ @@ -9,7 +9,7 @@ import ( templ Sessions(state *models.State, loc *time.Location) { -
+

sessions

diff --git a/pkg/server/templates/usage.templ b/pkg/server/templates/usage.templ index bf7a704..252f746 100644 --- a/pkg/server/templates/usage.templ +++ b/pkg/server/templates/usage.templ @@ -115,7 +115,7 @@ templ Usage(secure string, host string, username string) { # cd back to the agent directory cd $agentdir - # extend session lifetime + # prevent logout when last user exits touch $agentdir/.hold `} @@ -128,7 +128,7 @@ templ Usage(secure string, host string, username string) { # cd back to the agent directory cd %agentdir% - # extend session lifetime + # prevent logout when last user exits echo > %agentdir%\.hold `} @@ -138,7 +138,7 @@ templ Usage(secure string, host string, username string) { # cd back to the agent directory cd $env:agentdir - # extend session lifetime + # prevent logout when last user exits $null > $env:agentdir\.hold `}