* 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 committed by Erik Brakkee
parent c45b6ed090
commit 830594740b
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,
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)

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" -}}
# 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.

View File

@ -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) {

View File

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

View File

@ -9,7 +9,7 @@ import (
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>
<div id="status">

View File

@ -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
`}</pre>
@ -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
`}</pre>
@ -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
`}</pre>