* 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:
parent
5a91d86b39
commit
21463a5cad
@ -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)
|
||||||
|
@ -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.
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,8 +49,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentSession struct {
|
type AgentSession struct {
|
||||||
@ -82,21 +83,20 @@ 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,20 +309,24 @@ 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()
|
||||||
comms.ConvergeMessage{
|
|
||||||
Value: comms.NewExpiryTimeUpdate(state.expiryTime(holdFilename)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendExpiryTimeUpdateEvent() error {
|
||||||
|
return comms.Send(state.commChannel.SideChannel,
|
||||||
|
comms.ConvergeMessage{
|
||||||
|
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
|
||||||
// 2. The expiry time is relative to the modification time of the .hold file in the
|
// 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)
|
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) {
|
||||||
|
@ -2,7 +2,7 @@ package templates
|
|||||||
|
|
||||||
|
|
||||||
templ Downloads() {
|
templ Downloads() {
|
||||||
<div>
|
<div hx-boost="false">
|
||||||
|
|
||||||
<h1>downloads</h1>
|
<h1>downloads</h1>
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user