* 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
This commit is contained in:
Erik Brakkee 2024-08-02 20:58:46 +02:00
parent 5a91d86b39
commit 21463a5cad
6 changed files with 111 additions and 61 deletions

View File

@ -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, func sshServer(hostKeyFile string, shellCommand string,
passwordHandler ssh.PasswordHandler, passwordHandler ssh.PasswordHandler,
authorizedPublicKeys AuthorizedPublicKeys) *ssh.Server { authorizedPublicKeys AuthorizedPublicKeys) *ssh.Server {
ssh.Handle(func(s ssh.Session) { ssh.Handle(func(sshSession ssh.Session) {
workingDirectory, _ := os.Getwd() workingDirectory, _ := os.Getwd()
env := append(os.Environ(), fmt.Sprintf("agentdir=%s", workingDirectory)) 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 { if err != nil {
panic(err) panic(err)
} }
sessionInfo := comms.NewSessionInfo( sessionInfo := comms.NewSessionInfo(
s.LocalAddr().String(), "ssh", sshSession.LocalAddr().String(), "ssh",
) )
session.Login(sessionInfo, s) session.Login(sessionInfo, sshSession)
iowrappers.SynchronizeStreams("shell -- ssh", process.Pipe(), s) activityDetector := UserActivityDetector{
session: sshSession,
}
iowrappers.SynchronizeStreams("shell -- ssh", process.Pipe(), activityDetector)
session.LogOut(sessionInfo.ClientId) session.LogOut(sessionInfo.ClientId)
// will cause addition goroutines to remmain alive when the SSH // will cause addition goroutines to remmain alive when the SSH
// session is killed. For now acceptable since the agent is a short-lived // 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" + " 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" + "--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" + " directory where the agent is started is used.\n" +
"--warning-time: advance warning time before sessio ends\n" + "--warning-time: advance warning time before sessio ends (default '5m')\n" +
"--expiry-time: expiry time of the session\n" + "--expiry-time: expiry time of the session (default '10m')\n" +
"--check-interval: interval at which expiry is checked\n" + "--check-interval: interval at which expiry is checked\n" +
"--insecure: allow invalid certificates\n" + "--insecure: allow invalid certificates\n" +
"--shells: comma-separated list of shells to add to the front of theshell search path\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:] return args[1], args[1:]
} }
func parseDuration(args []string, val string) (time.Duration, []string) { func parseDuration(args []string) (time.Duration, []string) {
duration, err := time.ParseDuration(val) arg, args := getArg(args)
duration, err := time.ParseDuration(arg)
if err != nil { if err != nil {
printHelp(fmt.Sprintf("Error parsing duration: %v\n", err)) printHelp(fmt.Sprintf("Error parsing duration: %v\n", err))
} }
return duration, args[1:] return duration, args
} }
func main() { func main() {
@ -224,18 +244,17 @@ func main() {
additionalShells := []string{} additionalShells := []string{}
commaSeparated := "" commaSeparated := ""
for len(args) > 0 && strings.HasPrefix(args[0], "-") { for len(args) > 0 && strings.HasPrefix(args[0], "-") {
val := ""
switch args[0] { switch args[0] {
case "--id": case "--id":
id, args = getArg(args) id, args = getArg(args)
case "--authorized-keys": case "--authorized-keys":
authorizedKeysFile, args = getArg(args) authorizedKeysFile, args = getArg(args)
case "--warning-time": case "--warning-time":
advanceWarningTime, args = parseDuration(args, val) advanceWarningTime, args = parseDuration(args)
case "--expiry-time": case "--expiry-time":
agentExpriryTime, args = parseDuration(args, val) agentExpriryTime, args = parseDuration(args)
case "--check-interval": case "--check-interval":
tickerInterval, args = parseDuration(args, val) tickerInterval, args = parseDuration(args)
case "--insecure": case "--insecure":
insecure = true insecure = true
case "--shells": case "--shells":
@ -247,6 +266,13 @@ func main() {
args = args[1:] 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...) shells = append(additionalShells, shells...)
id = getId(id) id = getId(id)

View File

@ -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" -}} {{ if eq .os "windows" -}}
# cmd # cmd
@ -11,9 +16,6 @@ You can extend expiry of the session using
{{- end }} {{- end }}
The expiry time is equal to the modification time of the .hold The expiry time is equal to the modification time of the .hold
file with the expiry duration (%v) added. file with the expiry duration added. By creating a .hold
file with a timestamp in the future, the session can be extended
To prevent the agent from exiting after the last session is gone, beyond the default session timeout.
also use the above command in any shell.

View File

@ -33,13 +33,14 @@ type AgentState struct {
startTime time.Time startTime time.Time
// Advance warning time to notify the user of something important happening // Advance warning time to notify the user of something important happening
advanceWarningTime time.Duration advanceWarningDuration time.Duration
// session expiry time // session expiry time
agentExpriryTime time.Duration agentExpiryDuration time.Duration
// Last expiry time reported to the user. // Last expiry time reported to the user.
lastExpiryTimmeReported time.Time lastExpiryTimeReported time.Time
expiryIsNear bool
// ticker // ticker
tickerInterval time.Duration tickerInterval time.Duration
@ -48,7 +49,7 @@ type AgentState struct {
// map of unique session id to a session // map of unique session id to a session
clients map[string]*AgentSession clients map[string]*AgentSession
lastUserLoginTime time.Time lastUserActivityTime time.Time
agentUsed bool agentUsed bool
} }
@ -84,19 +85,18 @@ func ConfigureAgent(commChannel comms.CommChannel,
state = AgentState{ state = AgentState{
commChannel: commChannel, commChannel: commChannel,
startTime: time.Now(), startTime: time.Now(),
advanceWarningTime: advanceWarningTime, advanceWarningDuration: advanceWarningTime,
agentExpriryTime: agentExpiryTime, agentExpiryDuration: agentExpiryTime,
lastExpiryTimmeReported: time.Time{}, lastExpiryTimeReported: time.Time{},
tickerInterval: tickerInterval, tickerInterval: tickerInterval,
ticker: time.NewTicker(tickerInterval), ticker: time.NewTicker(tickerInterval),
clients: make(map[string]*AgentSession), clients: make(map[string]*AgentSession),
lastUserLoginTime: time.Time{}, lastUserActivityTime: time.Time{},
agentUsed: false, agentUsed: false,
} }
log.Printf("Agent expires at %s", log.Printf("Agent expiry duration is %v", state.agentExpiryDuration)
state.expiryTime(holdFilename).Format(time.DateTime))
comms.Send(state.commChannel.SideChannel, comms.Send(state.commChannel.SideChannel,
comms.ConvergeMessage{ comms.ConvergeMessage{
@ -131,8 +131,23 @@ func LogOut(clientId string) {
} }
} }
func UserActivityDetected() {
events <- func() {
userActivityDetected()
}
}
// Internal interface synchronous // Internal interface synchronous
func userActivityDetected() {
state.lastUserActivityTime = time.Now()
if state.expiryIsNear {
messageUsers("User activity detected, session extended.")
sendExpiryTimeUpdateEvent()
state.expiryIsNear = false
}
}
func monitorHoldFile() { func monitorHoldFile() {
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@ -200,7 +215,7 @@ func login(sessionInfo comms.SessionInfo, sshSession ssh.Session) {
sshSession: sshSession, sshSession: sshSession,
} }
state.clients[sessionInfo.ClientId] = &agentSession state.clients[sessionInfo.ClientId] = &agentSession
state.lastUserLoginTime = time.Now() state.lastUserActivityTime = time.Now()
state.agentUsed = true state.agentUsed = true
err := comms.SendWithTimeout(state.commChannel.SideChannel, err := comms.SendWithTimeout(state.commChannel.SideChannel,
@ -218,7 +233,7 @@ func login(sessionInfo comms.SessionInfo, sshSession ssh.Session) {
func printHelpMessage(sshSession ssh.Session) { func printHelpMessage(sshSession ssh.Session) {
printMessage(sshSession, fmt.Sprintf(helpMessage, printMessage(sshSession, fmt.Sprintf(helpMessage,
state.agentExpriryTime)) state.agentExpiryDuration))
} }
func formatHelpMessage() string { func formatHelpMessage() string {
@ -279,14 +294,14 @@ func fileExists(filename string) bool {
func (state *AgentState) expiryTime(filename string) time.Time { func (state *AgentState) expiryTime(filename string) time.Time {
if !state.agentUsed { if !state.agentUsed {
return state.startTime.Add(state.agentExpriryTime) return state.startTime.Add(state.agentExpiryDuration)
} }
expiryTime := time.Time{} expiryTime := time.Time{}
stats, err := os.Stat(filename) stats, err := os.Stat(filename)
if err == nil { 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) { if userLoginBaseExpiryTime.After(expiryTime) {
expiryTime = userLoginBaseExpiryTime expiryTime = userLoginBaseExpiryTime
} }
@ -294,19 +309,23 @@ func (state *AgentState) expiryTime(filename string) time.Time {
} }
func holdFileChange() { func holdFileChange() {
newExpiryTIme := state.expiryTime(holdFilename) newExpiryTime := state.expiryTime(holdFilename)
if newExpiryTIme != state.lastExpiryTimmeReported { if newExpiryTime != state.lastExpiryTimeReported {
message := fmt.Sprintf("Expiry time of session is now %s\n", message := fmt.Sprintf("Expiry time of session is now %s\n",
newExpiryTIme.Format(time.DateTime)) newExpiryTime.Format(time.DateTime))
message += holdFileMessage() message += holdFileMessage()
messageUsers(message) messageUsers(message)
state.lastExpiryTimmeReported = newExpiryTIme state.lastExpiryTimeReported = newExpiryTime
comms.Send(state.commChannel.SideChannel, sendExpiryTimeUpdateEvent()
}
}
func sendExpiryTimeUpdateEvent() error {
return comms.Send(state.commChannel.SideChannel,
comms.ConvergeMessage{ comms.ConvergeMessage{
Value: comms.NewExpiryTimeUpdate(state.expiryTime(holdFilename)), Value: comms.NewExpiryTimeUpdate(state.expiryTime(holdFilename)),
}) })
} }
}
// Behavior to implement // Behavior to implement
// 1. there is a global timeout for all agent clients together: state.agentExpirtyTime // 1. there is a global timeout for all agent clients together: state.agentExpirtyTime
@ -329,18 +348,21 @@ func check() {
os.Exit(0) os.Exit(0)
} }
if expiryTime.Sub(now) < state.advanceWarningTime { state.expiryIsNear = expiryTime.Sub(now) < state.advanceWarningDuration
if state.expiryIsNear {
messageUsers( messageUsers(
fmt.Sprintf("Session will expire at %s", expiryTime.Format(time.DateTime))) fmt.Sprintf("Session will expire at %s, press any key to extend it.", expiryTime.Format(time.DateTime)))
for _, session := range state.clients { //for _, session := range state.clients {
printHelpMessage(session.sshSession) // printHelpMessage(session.sshSession)
} //}
} }
if state.agentUsed && !fileExists(holdFilename) && len(state.clients) == 0 { if state.agentUsed && !fileExists(holdFilename) && len(state.clients) == 0 {
log.Printf("All clients disconnected and no '%s' file found, exiting", holdFilename) log.Printf("All clients disconnected and no '%s' file found, exiting", holdFilename)
os.Exit(0) os.Exit(0)
} }
sendExpiryTimeUpdateEvent()
} }
func messageUsers(message string) { func messageUsers(message string) {

View File

@ -2,7 +2,7 @@ package templates
templ Downloads() { templ Downloads() {
<div> <div hx-boost="false">
<h1>downloads</h1> <h1>downloads</h1>

View File

@ -9,7 +9,7 @@ import (
templ Sessions(state *models.State, loc *time.Location) { templ Sessions(state *models.State, loc *time.Location) {
<div hx-ext="ws" ws-connect="/ws/sessions"> <div hx-ext="ws" ws-connect="../ws/sessions">
<h1>sessions</h1> <h1>sessions</h1>
<div id="status"> <div id="status">

View File

@ -115,7 +115,7 @@ templ Usage(secure string, host string, username string) {
# cd back to the agent directory # cd back to the agent directory
cd $agentdir cd $agentdir
# extend session lifetime # prevent logout when last user exits
touch $agentdir/.hold touch $agentdir/.hold
`}</pre> `}</pre>
@ -128,7 +128,7 @@ templ Usage(secure string, host string, username string) {
# cd back to the agent directory # cd back to the agent directory
cd %agentdir% cd %agentdir%
# extend session lifetime # prevent logout when last user exits
echo > %agentdir%\.hold echo > %agentdir%\.hold
`}</pre> `}</pre>
@ -138,7 +138,7 @@ templ Usage(secure string, host string, username string) {
# cd back to the agent directory # cd back to the agent directory
cd $env:agentdir cd $env:agentdir
# extend session lifetime # prevent logout when last user exits
$null > $env:agentdir\.hold $null > $env:agentdir\.hold
`}</pre> `}</pre>