Compare commits

..

4 Commits

Author SHA1 Message Date
hyung-hwan 579876611c ssh broker code refactoring done 2026-06-06 12:32:45 +09:00
hyung-hwan 740222d99f code refactoring on logging code in ssh broker 2026-06-06 12:07:23 +09:00
hyung-hwan 0a5fc72dea some ssh broker code refactoring done 2026-06-06 11:53:32 +09:00
hyung-hwan e93941909d some code refactoring 2026-06-06 11:28:45 +09:00
4 changed files with 409 additions and 211 deletions
+28
View File
@@ -1186,6 +1186,34 @@ func (s *Store) CreateSSHSession(item models.SSHSession) (models.SSHSession, err
return item, nil return item, nil
} }
func (s *Store) ExpireStalePendingSSHSessions(cutoff int64, reason string) error {
var err error
_, err = s.Exec(`UPDATE ssh_sessions
SET status = 'closed', ended_at = ?, error = ?
WHERE status = 'pending' AND started_at < ?`,
cutoff,
strings.TrimSpace(reason),
cutoff,
)
return err
}
func (s *Store) CountPendingSSHSessionsForUser(userID string) (int, error) {
var row *sql.Row
var count int
var err error
row = s.QueryRow(`SELECT COUNT(*)
FROM ssh_sessions
WHERE user_public_id = ? AND status = 'pending'`, strings.TrimSpace(userID))
err = row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Store) GetSSHSession(id string) (models.SSHSession, error) { func (s *Store) GetSSHSession(id string) (models.SSHSession, error) {
var row *sql.Row var row *sql.Row
var item models.SSHSession var item models.SSHSession
@@ -323,7 +323,7 @@ func (api *API) openSSHSessionFileTransferClient(store *db.Store, user models.Us
var connectedHostKey ssh.PublicKey var connectedHostKey ssh.PublicKey
var err error var err error
prepared, prepareStage, err = api.prepareSSHSession(store, user, sessionItem, promptedPassword, otpCode) prepared, prepareStage, err = api.prepareSSHSession(store, &user, &sessionItem, promptedPassword, otpCode)
if err != nil { if err != nil {
if prepareStage == "load_profile" { if prepareStage == "load_profile" {
return nil, errors.New("ssh access profile not found") return nil, errors.New("ssh access profile not found")
+222 -68
View File
@@ -4,20 +4,37 @@ import "errors"
import "io" import "io"
import "strings" import "strings"
import "sync" import "sync"
import "time"
import "golang.org/x/crypto/ssh" import "golang.org/x/crypto/ssh"
import "golang.org/x/net/websocket" import "golang.org/x/net/websocket"
// TODO: do these need to tbe configurable?
const sshSessionPreparationTTL time.Duration = 60 * time.Second
const sshSessionPreparationMaxEntries int = 16384
const sshSessionPendingPerUserLimit int = 32
type sshActiveSession struct { type sshActiveSession struct {
mu sync.Mutex mu sync.Mutex
ws *websocket.Conn ws *websocket.Conn
client *ssh.Client client *ssh.Client
session *ssh.Session session *ssh.Session
stdin io.WriteCloser stdin io.WriteCloser
closeCode SSHSessionCloseCode
closeReason string closeReason string
closed bool closed bool
} }
type SSHSessionCloseCode string
const SSHSessionCloseNone SSHSessionCloseCode = ""
const SSHSessionCloseWebsocketClosed SSHSessionCloseCode = "websocket_closed"
const SSHSessionCloseWebsocketSendFailed SSHSessionCloseCode = "websocket_send_failed"
const SSHSessionCloseWebsocketInputQueueFull SSHSessionCloseCode = "websocket_input_queue_full"
const SSHSessionCloseDetached SSHSessionCloseCode = "detached"
const SSHSessionCloseUserDisconnect SSHSessionCloseCode = "user_disconnect"
const SSHSessionCloseUnknown SSHSessionCloseCode = "unknown"
type SSHSessionRegistry struct { type SSHSessionRegistry struct {
mu sync.Mutex mu sync.Mutex
items map[string]*sshActiveSession items map[string]*sshActiveSession
@@ -28,14 +45,30 @@ type SSHPromptedAuthInput struct {
OTPCode string OTPCode string
} }
type sshPromptedAuthStoreItem struct {
Input SSHPromptedAuthInput
CreatedAt time.Time
ExpiresAt time.Time
}
type sshPreparedSessionStoreItem struct {
Session sshSessionPrepared
CreatedAt time.Time
ExpiresAt time.Time
}
type SSHPromptedAuthStore struct { type SSHPromptedAuthStore struct {
mu sync.Mutex mu sync.Mutex
items map[string]SSHPromptedAuthInput items map[string]sshPromptedAuthStoreItem
ttl time.Duration
maxEntries int
} }
type SSHPreparedSessionStore struct { type SSHPreparedSessionStore struct {
mu sync.Mutex mu sync.Mutex
items map[string]sshSessionPrepared items map[string]sshPreparedSessionStoreItem
ttl time.Duration
maxEntries int
} }
func NewSSHSessionRegistry() *SSHSessionRegistry { func NewSSHSessionRegistry() *SSHSessionRegistry {
@@ -51,7 +84,9 @@ func NewSSHPromptedAuthStore() *SSHPromptedAuthStore {
var store *SSHPromptedAuthStore var store *SSHPromptedAuthStore
store = &SSHPromptedAuthStore{ store = &SSHPromptedAuthStore{
items: map[string]SSHPromptedAuthInput{}, items: map[string]sshPromptedAuthStoreItem{},
ttl: sshSessionPreparationTTL,
maxEntries: sshSessionPreparationMaxEntries,
} }
return store return store
} }
@@ -60,32 +95,91 @@ func NewSSHPreparedSessionStore() *SSHPreparedSessionStore {
var store *SSHPreparedSessionStore var store *SSHPreparedSessionStore
store = &SSHPreparedSessionStore{ store = &SSHPreparedSessionStore{
items: map[string]sshSessionPrepared{}, items: map[string]sshPreparedSessionStoreItem{},
ttl: sshSessionPreparationTTL,
maxEntries: sshSessionPreparationMaxEntries,
} }
return store return store
} }
func sshPreparationEntryExpired(now time.Time, expiresAt time.Time) bool {
return !expiresAt.IsZero() && !now.Before(expiresAt)
}
func (s *SSHPromptedAuthStore) purgeExpiredLocked(now time.Time) {
var id string
var item sshPromptedAuthStoreItem
for id, item = range s.items {
if sshPreparationEntryExpired(now, item.ExpiresAt) {
delete(s.items, id)
}
}
}
func (s *SSHPromptedAuthStore) enforceMaxEntriesLocked() {
var id string
var oldestID string
var item sshPromptedAuthStoreItem
var oldestAt time.Time
if s.maxEntries <= 0 {
return
}
for len(s.items) > s.maxEntries {
oldestID = ""
oldestAt = time.Time{}
for id, item = range s.items {
if oldestID == "" || item.CreatedAt.Before(oldestAt) {
oldestID = id
oldestAt = item.CreatedAt
}
}
if oldestID == "" {
return
}
delete(s.items, oldestID)
}
}
func (s *SSHPromptedAuthStore) Put(id string, input SSHPromptedAuthInput) { func (s *SSHPromptedAuthStore) Put(id string, input SSHPromptedAuthInput) {
var trimmedID string var trimmedID string
var now time.Time
if s == nil { return } if s == nil { return }
trimmedID = strings.TrimSpace(id) trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return } if trimmedID == "" { return }
if input.Password == "" && input.OTPCode == "" { return }
now = time.Now().UTC()
s.mu.Lock() s.mu.Lock()
s.items[trimmedID] = input s.purgeExpiredLocked(now)
s.items[trimmedID] = sshPromptedAuthStoreItem{
Input: input,
CreatedAt: now,
ExpiresAt: now.Add(s.ttl),
}
s.enforceMaxEntriesLocked()
s.mu.Unlock() s.mu.Unlock()
} }
func (s *SSHPromptedAuthStore) Take(id string) SSHPromptedAuthInput { func (s *SSHPromptedAuthStore) Take(id string) SSHPromptedAuthInput {
var trimmedID string var trimmedID string
var input SSHPromptedAuthInput var input SSHPromptedAuthInput
var item sshPromptedAuthStoreItem
var ok bool
var now time.Time
if s == nil { return input } if s == nil { return input }
trimmedID = strings.TrimSpace(id) trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return input } if trimmedID == "" { return input }
now = time.Now().UTC()
s.mu.Lock() s.mu.Lock()
input = s.items[trimmedID] s.purgeExpiredLocked(now)
delete(s.items, trimmedID) item, ok = s.items[trimmedID]
if ok {
input = item.Input
delete(s.items, trimmedID)
}
s.mu.Unlock() s.mu.Unlock()
return input return input
} }
@@ -93,7 +187,7 @@ func (s *SSHPromptedAuthStore) Take(id string) SSHPromptedAuthInput {
func (s *SSHPromptedAuthStore) Delete(id string) { func (s *SSHPromptedAuthStore) Delete(id string) {
var trimmedID string var trimmedID string
if s == nil { return} if s == nil { return }
trimmedID = strings.TrimSpace(id) trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return } if trimmedID == "" { return }
s.mu.Lock() s.mu.Lock()
@@ -101,8 +195,45 @@ func (s *SSHPromptedAuthStore) Delete(id string) {
s.mu.Unlock() s.mu.Unlock()
} }
func (s *SSHPreparedSessionStore) purgeExpiredLocked(now time.Time) {
var id string
var item sshPreparedSessionStoreItem
for id, item = range s.items {
if sshPreparationEntryExpired(now, item.ExpiresAt) {
delete(s.items, id)
}
}
}
func (s *SSHPreparedSessionStore) enforceMaxEntriesLocked() {
var id string
var oldestID string
var item sshPreparedSessionStoreItem
var oldestAt time.Time
if s.maxEntries <= 0 {
return
}
for len(s.items) > s.maxEntries {
oldestID = ""
oldestAt = time.Time{}
for id, item = range s.items {
if oldestID == "" || item.CreatedAt.Before(oldestAt) {
oldestID = id
oldestAt = item.CreatedAt
}
}
if oldestID == "" {
return
}
delete(s.items, oldestID)
}
}
func (s *SSHPreparedSessionStore) Put(id string, item sshSessionPrepared) { func (s *SSHPreparedSessionStore) Put(id string, item sshSessionPrepared) {
var trimmedID string var trimmedID string
var now time.Time
if s == nil { if s == nil {
return return
@@ -111,15 +242,24 @@ func (s *SSHPreparedSessionStore) Put(id string, item sshSessionPrepared) {
if trimmedID == "" { if trimmedID == "" {
return return
} }
now = time.Now().UTC()
s.mu.Lock() s.mu.Lock()
s.items[trimmedID] = item s.purgeExpiredLocked(now)
s.items[trimmedID] = sshPreparedSessionStoreItem{
Session: item,
CreatedAt: now,
ExpiresAt: now.Add(s.ttl),
}
s.enforceMaxEntriesLocked()
s.mu.Unlock() s.mu.Unlock()
} }
func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) { func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
var trimmedID string var trimmedID string
var item sshSessionPrepared var item sshSessionPrepared
var storeItem sshPreparedSessionStoreItem
var ok bool var ok bool
var now time.Time
if s == nil { if s == nil {
return item, false return item, false
@@ -128,8 +268,13 @@ func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
if trimmedID == "" { if trimmedID == "" {
return item, false return item, false
} }
now = time.Now().UTC()
s.mu.Lock() s.mu.Lock()
item, ok = s.items[trimmedID] s.purgeExpiredLocked(now)
storeItem, ok = s.items[trimmedID]
if ok {
item = storeItem.Session
}
s.mu.Unlock() s.mu.Unlock()
return item, ok return item, ok
} }
@@ -137,7 +282,9 @@ func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) { func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) {
var trimmedID string var trimmedID string
var item sshSessionPrepared var item sshSessionPrepared
var storeItem sshPreparedSessionStoreItem
var ok bool var ok bool
var now time.Time
if s == nil { if s == nil {
return item, false return item, false
@@ -146,9 +293,12 @@ func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) {
if trimmedID == "" { if trimmedID == "" {
return item, false return item, false
} }
now = time.Now().UTC()
s.mu.Lock() s.mu.Lock()
item, ok = s.items[trimmedID] s.purgeExpiredLocked(now)
storeItem, ok = s.items[trimmedID]
if ok { if ok {
item = storeItem.Session
delete(s.items, trimmedID) delete(s.items, trimmedID)
} }
s.mu.Unlock() s.mu.Unlock()
@@ -194,19 +344,28 @@ func (r *SSHSessionRegistry) Unregister(id string, item *sshActiveSession) {
} }
} }
func (r *SSHSessionRegistry) RequestClose(id string, reason string) bool { func sshSessionCloseMessage(code SSHSessionCloseCode) string {
if code == SSHSessionCloseWebsocketClosed { return "websocket closed" }
if code == SSHSessionCloseWebsocketSendFailed { return "websocket send failed" }
if code == SSHSessionCloseWebsocketInputQueueFull { return "websocket input queue full" }
if code == SSHSessionCloseDetached { return "detached" }
if code == SSHSessionCloseUserDisconnect { return "disconnected by user" }
return strings.TrimSpace(string(code))
}
func (r *SSHSessionRegistry) RequestClose(id string, code SSHSessionCloseCode) bool {
return r.RequestCloseWithReason(id, code, sshSessionCloseMessage(code))
}
func (r *SSHSessionRegistry) RequestCloseWithReason(id string, code SSHSessionCloseCode, reason string) bool {
var item *sshActiveSession var item *sshActiveSession
if r == nil { if r == nil { return false }
return false
}
r.mu.Lock() r.mu.Lock()
item = r.items[id] item = r.items[id]
r.mu.Unlock() r.mu.Unlock()
if item == nil { if item == nil { return false }
return false item.RequestCloseWithReason(code, reason)
}
item.RequestClose(reason)
return true return true
} }
@@ -216,68 +375,70 @@ func (s *sshActiveSession) SetResources(ws *websocket.Conn, client *ssh.Client,
var closeSession *ssh.Session var closeSession *ssh.Session
var closeStdin io.WriteCloser var closeStdin io.WriteCloser
if s == nil { if s == nil { return }
return
}
s.mu.Lock() s.mu.Lock()
if s.closed { if s.closed {
closeWS = ws closeWS = ws
closeClient = client closeClient = client
closeSession = session closeSession = session
closeStdin = stdin closeStdin = stdin
s.mu.Unlock() s.mu.Unlock()
if closeStdin != nil { if closeStdin != nil { _ = closeStdin.Close() }
_ = closeStdin.Close() if closeSession != nil { _ = closeSession.Close() }
} if closeClient != nil { _ = closeClient.Close() }
if closeSession != nil { if closeWS != nil { _ = closeWS.Close() }
_ = closeSession.Close()
}
if closeClient != nil {
_ = closeClient.Close()
}
if closeWS != nil {
_ = closeWS.Close()
}
return return
} }
if ws != nil {
s.ws = ws if ws != nil { s.ws = ws }
} if client != nil { s.client = client }
if client != nil { if session != nil { s.session = session }
s.client = client if stdin != nil { s.stdin = stdin }
}
if session != nil {
s.session = session
}
if stdin != nil {
s.stdin = stdin
}
s.mu.Unlock() s.mu.Unlock()
} }
func (s *sshActiveSession) CloseReason() string { func (s *sshActiveSession) CloseReason() string {
var reason string var reason string
if s == nil { if s == nil { return "" }
return ""
}
s.mu.Lock() s.mu.Lock()
reason = s.closeReason reason = s.closeReason
s.mu.Unlock() s.mu.Unlock()
return reason return reason
} }
func (s *sshActiveSession) RequestClose(reason string) { func (s *sshActiveSession) CloseCode() SSHSessionCloseCode {
var code SSHSessionCloseCode
if s == nil { return SSHSessionCloseNone }
s.mu.Lock()
code = s.closeCode
s.mu.Unlock()
return code
}
func (s *sshActiveSession) RequestCloseWithReason(code SSHSessionCloseCode, reason string) {
var ws *websocket.Conn var ws *websocket.Conn
var client *ssh.Client var client *ssh.Client
var session *ssh.Session var session *ssh.Session
var stdin io.WriteCloser var stdin io.WriteCloser
if s == nil { if s == nil { return }
return
}
s.mu.Lock() s.mu.Lock()
if code == SSHSessionCloseNone {
code = SSHSessionCloseUnknown
}
if strings.TrimSpace(reason) == "" {
reason = sshSessionCloseMessage(code)
}
if s.closeReason == "" { if s.closeReason == "" {
s.closeCode = code
s.closeReason = reason s.closeReason = reason
} }
if s.closed { if s.closed {
@@ -290,16 +451,9 @@ func (s *sshActiveSession) RequestClose(reason string) {
session = s.session session = s.session
stdin = s.stdin stdin = s.stdin
s.mu.Unlock() s.mu.Unlock()
if stdin != nil {
_ = stdin.Close() if stdin != nil { _ = stdin.Close() }
} if session != nil { _ = session.Close() }
if session != nil { if client != nil { _ = client.Close() }
_ = session.Close() if ws != nil { _ = ws.Close() }
}
if client != nil {
_ = client.Close()
}
if ws != nil {
_ = ws.Close()
}
} }
+158 -142
View File
@@ -152,7 +152,7 @@ const sshAuthMethodStoredPrivateKey string = "stored_private_key"
const sshSecondFactorNone string = "none" const sshSecondFactorNone string = "none"
const sshSecondFactorPromptedTOTP string = "prompted_totp" const sshSecondFactorPromptedTOTP string = "prompted_totp"
func (api *API) logSSHSessionConnectStart(sessionItem models.SSHSession, profile models.SSHAccessProfile, user models.User) { func (api *API) logSSHSessionConnectStart(sessionItem *models.SSHSession, profile *models.SSHAccessProfile, user *models.User) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
"connect start session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s", "connect start session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s",
sessionItem.ID, sessionItem.ID,
@@ -168,11 +168,11 @@ func (api *API) logSSHSessionConnectStart(sessionItem models.SSHSession, profile
profile.AuthMethod) profile.AuthMethod)
} }
func (api *API) logSSHSessionPrepare(sessionID string, remoteAddr string, user models.User, profile models.SSHAccessProfile) { func (api *API) logSSHSessionPrepare(sessionItem *models.SSHSession, user *models.User, profile *models.SSHAccessProfile) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
"connect prepare build session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s", "connect prepare build session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s",
sessionID, sessionItem.ID,
remoteAddr, sessionItem.RemoteAddr,
user.Username, user.Username,
profile.ID, profile.ID,
profile.Name, profile.Name,
@@ -184,11 +184,11 @@ func (api *API) logSSHSessionPrepare(sessionID string, remoteAddr string, user m
profile.AuthMethod) profile.AuthMethod)
} }
func (api *API) logSSHSessionPreparedUse(sessionID string, remoteAddr string, user models.User, profile models.SSHAccessProfile) { func (api *API) logSSHSessionPreparedUse(sessionItem *models.SSHSession, user *models.User, profile *models.SSHAccessProfile) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
"connect prepare use session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s", "connect prepare use session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s",
sessionID, sessionItem.ID,
remoteAddr, sessionItem.RemoteAddr,
user.Username, user.Username,
profile.ID, profile.ID,
profile.Name, profile.Name,
@@ -200,7 +200,7 @@ func (api *API) logSSHSessionPreparedUse(sessionID string, remoteAddr string, us
profile.AuthMethod) profile.AuthMethod)
} }
func (api *API) logSSHSessionConnectSuccess(sessionItem models.SSHSession, profile models.SSHAccessProfile, user models.User, hostKeyFingerprint string) { func (api *API) logSSHSessionConnectSuccess(sessionItem *models.SSHSession, profile *models.SSHAccessProfile, user *models.User, hostKeyFingerprint string) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_INFO,
"connect success session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s host_key=%s", "connect success session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s host_key=%s",
sessionItem.ID, sessionItem.ID,
@@ -217,7 +217,7 @@ func (api *API) logSSHSessionConnectSuccess(sessionItem models.SSHSession, profi
hostKeyFingerprint) hostKeyFingerprint)
} }
func (api *API) logSSHSessionConnectFailure(sessionItem models.SSHSession, profile models.SSHAccessProfile, user models.User, stage string, err error) { func (api *API) logSSHSessionConnectFailure(sessionItem *models.SSHSession, profile *models.SSHAccessProfile, user *models.User, stage string, err error) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"connect failed session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s stage=%s err=%v", "connect failed session=%s requester=%s user=%s profile=%s profile_name=%q server=%s server_name=%q target=%s:%d remote_user=%s auth=%s stage=%s err=%v",
sessionItem.ID, sessionItem.ID,
@@ -235,18 +235,18 @@ func (api *API) logSSHSessionConnectFailure(sessionItem models.SSHSession, profi
err) err)
} }
func (api *API) logSSHSessionPrepareFailure(sessionID string, remoteAddr string, username string, profileID string, stage string, err error) { func (api *API) logSSHSessionPrepareFailure(sessionItem *models.SSHSession, user *models.User, stage string, err error) {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"connect failed session=%s requester=%s user=%s profile=%s stage=%s err=%v", "connect failed session=%s requester=%s user=%s profile=%s stage=%s err=%v",
sessionID, sessionItem.ID,
remoteAddr, sessionItem.RemoteAddr,
username, user.Username,
profileID, sessionItem.ProfileID,
stage, stage,
err) err)
} }
func (api *API) logSSHSessionClosed(sessionItem models.SSHSession, profile models.SSHAccessProfile, user models.User, status string, reason string, connectedAt int64, endedAt int64) { func (api *API) logSSHSessionClosed(sessionItem *models.SSHSession, profile *models.SSHAccessProfile, user *models.User, status string, reason string, connectedAt int64, endedAt int64) {
var durationSeconds int64 var durationSeconds int64
durationSeconds = 0 durationSeconds = 0
@@ -1581,6 +1581,8 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
var prepareStage string var prepareStage string
var hostKeyFingerprint string var hostKeyFingerprint string
var belongs bool var belongs bool
var pendingCount int
var pendingCutoff int64
var err error var err error
user, ok = middleware.UserFromContext(r.Context()) user, ok = middleware.UserFromContext(r.Context())
@@ -1593,6 +1595,21 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"}) WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return return
} }
pendingCutoff = time.Now().UTC().Add(-sshSessionPreparationTTL).Unix()
err = api.store(r).ExpireStalePendingSSHSessions(pendingCutoff, "session preparation expired")
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
pendingCount, err = api.store(r).CountPendingSSHSessionsForUser(user.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if pendingCount >= sshSessionPendingPerUserLimit {
WriteJSON(w, http.StatusTooManyRequests, map[string]string{"error": "too many pending ssh sessions; attach or wait for pending sessions to expire"})
return
}
profile, err = api.store(r).GetSSHAccessProfileForUser(user.ID, params["id"]) profile, err = api.store(r).GetSSHAccessProfileForUser(user.ID, params["id"])
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@@ -1665,7 +1682,7 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
return return
} }
prepared, prepareStage, err = api.prepareSSHSession(api.store(r), user, item, req.Password, req.OTPCode) prepared, prepareStage, err = api.prepareSSHSession(api.store(r), &user, &item, req.Password, req.OTPCode)
if err != nil { if err != nil {
if prepareStage == "host_key_missing" || prepareStage == "build_auth" { if prepareStage == "host_key_missing" || prepareStage == "build_auth" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
@@ -1851,7 +1868,7 @@ func (api *API) DisconnectSSHSessionForSelf(w http.ResponseWriter, r *http.Reque
api.SSHPreparedSessionStore.Delete(item.ID) api.SSHPreparedSessionStore.Delete(item.ID)
} }
if api.SSHSessionRegistry != nil { if api.SSHSessionRegistry != nil {
closed = api.SSHSessionRegistry.RequestClose(item.ID, "disconnected by user") closed = api.SSHSessionRegistry.RequestClose(item.ID, SSHSessionCloseUserDisconnect)
} }
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
if !closed { if !closed {
@@ -2328,9 +2345,7 @@ func discoverSSHServerHostKey(host string, port int, defaultUser string) (models
addr = net.JoinHostPort(strings.TrimSpace(host), fmt.Sprintf("%d", port)) addr = net.JoinHostPort(strings.TrimSpace(host), fmt.Sprintf("%d", port))
defaultUser = strings.TrimSpace(defaultUser) defaultUser = strings.TrimSpace(defaultUser)
if defaultUser == "" {
defaultUser = "codit"
}
config = &ssh.ClientConfig{ config = &ssh.ClientConfig{
User: defaultUser, User: defaultUser,
Auth: []ssh.AuthMethod{}, Auth: []ssh.AuthMethod{},
@@ -2359,21 +2374,17 @@ func discoverSSHServerHostKey(host string, port int, defaultUser string) (models
item.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(discovered))) item.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(discovered)))
item.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered)) item.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered))
if err != nil { if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unable to authenticate") { if strings.Contains(strings.ToLower(err.Error()), "unable to authenticate") { return item, nil }
return item, nil if strings.Contains(strings.ToLower(err.Error()), "no auth passed yet") { return item, nil }
}
if strings.Contains(strings.ToLower(err.Error()), "no auth passed yet") {
return item, nil
}
} }
return item, nil return item, nil
} }
func (api *API) prepareSSHSession(store *db.Store, user models.User, sessionItem models.SSHSession, promptedPassword string, otpCode string) (sshSessionPrepared, string, error) { func (api *API) prepareSSHSession(store *db.Store, user *models.User, sessionItem *models.SSHSession, promptedPassword string, otpCode string) (sshSessionPrepared, string, error) {
var prepared sshSessionPrepared var prepared sshSessionPrepared
var err error var err error
prepared.Session = sessionItem prepared.Session = *sessionItem
prepared.Profile, err = store.GetSSHAccessProfileForUser(user.ID, sessionItem.ProfileID) prepared.Profile, err = store.GetSSHAccessProfileForUser(user.ID, sessionItem.ProfileID)
if err != nil { if err != nil {
return prepared, "load_profile", err return prepared, "load_profile", err
@@ -2386,7 +2397,7 @@ func (api *API) prepareSSHSession(store *db.Store, user models.User, sessionItem
prepared.Profile.ServerID = prepared.Profile.Server.ID prepared.Profile.ServerID = prepared.Profile.Server.ID
} }
api.logSSHSessionPrepare(sessionItem.ID, sessionItem.RemoteAddr, user, prepared.Profile) api.logSSHSessionPrepare(sessionItem, user, &prepared.Profile)
prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID) prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
if err != nil { if err != nil {
return prepared, "load_host_keys", err return prepared, "load_host_keys", err
@@ -2405,26 +2416,26 @@ func (api *API) prepareSSHSession(store *db.Store, user models.User, sessionItem
return prepared, "", nil return prepared, "", nil
} }
func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem models.SSHSession, prepared sshSessionPrepared) (sshSessionPrepared, error) { func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem *models.SSHSession, prepared *sshSessionPrepared) error {
var err error var err error
var i int var i int
var reloadServer bool var reloadServer bool
var reloadHostKeys bool var reloadHostKeys bool
if strings.TrimSpace(prepared.Session.ID) == "" { if strings.TrimSpace(prepared.Session.ID) == "" {
return prepared, errors.New("prepared ssh session is missing session id") return errors.New("prepared ssh session is missing session id")
} }
if prepared.Session.ID != sessionItem.ID { if prepared.Session.ID != sessionItem.ID {
return prepared, fmt.Errorf("prepared ssh session id mismatch: prepared=%s session=%s", prepared.Session.ID, sessionItem.ID) return fmt.Errorf("prepared ssh session id mismatch: prepared=%s session=%s", prepared.Session.ID, sessionItem.ID)
} }
if strings.TrimSpace(prepared.Session.ServerID) != "" && prepared.Session.ServerID != sessionItem.ServerID { if strings.TrimSpace(prepared.Session.ServerID) != "" && prepared.Session.ServerID != sessionItem.ServerID {
return prepared, fmt.Errorf("prepared ssh session server mismatch: prepared=%s session=%s", prepared.Session.ServerID, sessionItem.ServerID) return fmt.Errorf("prepared ssh session server mismatch: prepared=%s session=%s", prepared.Session.ServerID, sessionItem.ServerID)
} }
if strings.TrimSpace(prepared.Profile.ID) != "" && prepared.Profile.ID != sessionItem.ProfileID { if strings.TrimSpace(prepared.Profile.ID) != "" && prepared.Profile.ID != sessionItem.ProfileID {
return prepared, fmt.Errorf("prepared ssh profile mismatch: prepared=%s session=%s", prepared.Profile.ID, sessionItem.ProfileID) return fmt.Errorf("prepared ssh profile mismatch: prepared=%s session=%s", prepared.Profile.ID, sessionItem.ProfileID)
} }
if strings.TrimSpace(sessionItem.ServerID) == "" { if strings.TrimSpace(sessionItem.ServerID) == "" {
return prepared, errors.New("ssh session server_id is empty") return errors.New("ssh session server_id is empty")
} }
reloadServer = prepared.Profile.ServerID != sessionItem.ServerID || prepared.Profile.Server.ID != sessionItem.ServerID reloadServer = prepared.Profile.ServerID != sessionItem.ServerID || prepared.Profile.Server.ID != sessionItem.ServerID
@@ -2444,7 +2455,7 @@ func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem m
sessionItem.Port) sessionItem.Port)
prepared.Profile.Server, err = store.GetSSHServer(sessionItem.ServerID) prepared.Profile.Server, err = store.GetSSHServer(sessionItem.ServerID)
if err != nil { if err != nil {
return prepared, err return err
} }
prepared.Profile.ServerID = prepared.Profile.Server.ID prepared.Profile.ServerID = prepared.Profile.Server.ID
if strings.TrimSpace(sessionItem.Host) != "" { if strings.TrimSpace(sessionItem.Host) != "" {
@@ -2465,22 +2476,37 @@ func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem m
if reloadHostKeys { if reloadHostKeys {
prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID) prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
if err != nil { if err != nil {
return prepared, err return err
} }
prepared.PinHostKeyOnFirstUse = false prepared.PinHostKeyOnFirstUse = false
if len(prepared.HostKeys) == 0 { if len(prepared.HostKeys) == 0 {
if normalizeSSHServerHostKeyPolicy(prepared.Profile.Server.HostKeyPolicy) != "trust_on_first_use" { if normalizeSSHServerHostKeyPolicy(prepared.Profile.Server.HostKeyPolicy) != "trust_on_first_use" {
return prepared, errors.New("no pinned host key for " + prepared.Profile.Server.Name) return errors.New("no pinned host key for " + prepared.Profile.Server.Name)
} }
prepared.PinHostKeyOnFirstUse = true prepared.PinHostKeyOnFirstUse = true
} }
} }
prepared.Session = sessionItem prepared.Session = *sessionItem
return prepared, nil return nil
}
func (api *API) requestSSHSessionCloseOnWebsocketError(inputErrCh chan<- error, readerErrCh <-chan error, sessionId string) {
var err error
var code SSHSessionCloseCode
err = <-readerErrCh
code = sshSessionWebsocketCloseCode(err)
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionId, code)
}
inputErrCh <- err
} }
func (api *API) serveSSHSessionStream(ws *websocket.Conn, store *db.Store, user models.User, sessionItem models.SSHSession) { func (api *API) serveSSHSessionStream(ws *websocket.Conn, store *db.Store, user models.User, sessionItem models.SSHSession) {
// TODO: remove this single stream handler if the frontend doesn't really need it..
// it's hard to maintain multiple functions for similar features. the frontend
// should use the workspace stream api instead.
var sendMu sync.Mutex var sendMu sync.Mutex
var inputCh chan sshSessionStreamMessage var inputCh chan sshSessionStreamMessage
var inputErrCh chan error var inputErrCh chan error
@@ -2490,45 +2516,35 @@ func (api *API) serveSSHSessionStream(ws *websocket.Conn, store *db.Store, user
var preparedPtr *sshSessionPrepared var preparedPtr *sshSessionPrepared
defer ws.Close() defer ws.Close()
inputCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize) inputCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize)
inputErrCh = make(chan error, 1) inputErrCh = make(chan error, 1)
readerErrCh = make(chan error, 1) readerErrCh = make(chan error, 1)
go api.readSSHSessionWebsocket(ws, inputCh, readerErrCh) // read websocket and write to inputCh
go func() {
var err error
var reason string
err = <-readerErrCh // read websocket and write to inputCh and readerErrCh
reason = sshSessionWebsocketCloseReason(err) go api.readSSHSessionWebsocket(ws, inputCh, readerErrCh)
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionItem.ID, reason) // waits for websocket reader error, converts it to a reason, requests session close, then relays the error to inputErrCh
} go api.requestSSHSessionCloseOnWebsocketError(inputErrCh, readerErrCh, sessionItem.ID)
inputErrCh <- err
}()
prepared, preparedOK = api.SSHPreparedSessionStore.Take(sessionItem.ID) prepared, preparedOK = api.SSHPreparedSessionStore.Take(sessionItem.ID)
if !preparedOK { if preparedOK { preparedPtr = &prepared }
prepared = sshSessionPrepared{} // if preparedOK is not true, preparedPtr is nil. api.RunSSHSessionStream build one inside itself.
} else {
preparedPtr = &prepared
}
// this reads the inputCh // this reads the inputCh
api.runSSHSessionStream(store, user, sessionItem, ws, inputCh, inputErrCh, api.runSSHSessionStream(store, &user, &sessionItem, ws, inputCh, inputErrCh, preparedPtr,
preparedPtr, func(msg sshSessionStreamMessage) { // stream message sender
func(msg sshSessionStreamMessage) {
var err error var err error
err = api.sendSSHSessionWS(&sendMu, ws, msg) err = api.sendSSHSessionWS(&sendMu, ws, msg)
if err != nil && api.SSHSessionRegistry != nil { if err != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionItem.ID, "websocket send failed") api.SSHSessionRegistry.RequestClose(sessionItem.ID, SSHSessionCloseWebsocketSendFailed)
} }
}, },
func(data []byte) { func(data []byte) { // binary data sender
var err error var err error
err = api.sendSSHSessionBinary(&sendMu, ws, data) err = api.sendSSHSessionBinary(&sendMu, ws, data)
if err != nil && api.SSHSessionRegistry != nil { if err != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionItem.ID, "websocket send failed") api.SSHSessionRegistry.RequestClose(sessionItem.ID, SSHSessionCloseWebsocketSendFailed)
} }
}) })
} }
@@ -2548,7 +2564,7 @@ func (api *API) serveSSHWorkspaceStream(ws *websocket.Conn, store *db.Store, use
var attachedID string var attachedID string
var attachmentDoneCh chan sshWorkspaceAttachmentDone var attachmentDoneCh chan sshWorkspaceAttachmentDone
var attachmentDone sshWorkspaceAttachmentDone var attachmentDone sshWorkspaceAttachmentDone
var websocketCloseReason string var websocketCloseCode SSHSessionCloseCode
defer ws.Close() defer ws.Close()
controlCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize) controlCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize)
@@ -2561,7 +2577,7 @@ event_loop:
for { for {
select { select {
case err = <-controlErrCh: case err = <-controlErrCh:
websocketCloseReason = sshSessionWebsocketCloseReason(err) websocketCloseCode = sshSessionWebsocketCloseCode(err)
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"workspace stream closed requester=%s user=%s err=%v", "workspace stream closed requester=%s user=%s err=%v",
remoteAddr, remoteAddr,
@@ -2570,7 +2586,7 @@ event_loop:
for attachedID, attachment = range attachments { for attachedID, attachment = range attachments {
if api.SSHSessionRegistry != nil { if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(attachedID, websocketCloseReason) api.SSHSessionRegistry.RequestClose(attachedID, websocketCloseCode)
} }
select { select {
case attachment.InputErrCh <- err: case attachment.InputErrCh <- err:
@@ -2624,9 +2640,7 @@ event_loop:
if !preparedOK { if !preparedOK {
attachedStatus = "closed" attachedStatus = "closed"
attachedMessage = item.Error attachedMessage = item.Error
if item.Status == "error" { if item.Status == "error" { attachedStatus = "error" }
attachedStatus = "error"
}
if attachedMessage == "" { if attachedMessage == "" {
if item.Status == "closed" { if item.Status == "closed" {
attachedMessage = "SSH session is closed" attachedMessage = "SSH session is closed"
@@ -2668,33 +2682,32 @@ event_loop:
user.Username) user.Username)
attachedItem = item attachedItem = item
attachedSessionID = attachedItem.ID attachedSessionID = attachedItem.ID
go func(runItem models.SSHSession, runPrepared sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) { go func(runUser models.User, runItem models.SSHSession, runPrepared sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) {
var runPreparedPtr *sshSessionPrepared // accept runUser, runItem, runPrepared by value because the values can mutate
// in the main event loop if we reference them directly(closure or pointer)
runPreparedPtr = &runPrepared
defer func() { defer func() {
select { select {
case attachmentDoneCh <- sshWorkspaceAttachmentDone{SessionID: runSessionID, Attachment: runAttachment}: case attachmentDoneCh <- sshWorkspaceAttachmentDone{SessionID: runSessionID, Attachment: runAttachment}:
default: default:
} }
}() }()
api.runSSHSessionStream(store, user, runItem, nil, runAttachment.InputCh, runAttachment.InputErrCh, runPreparedPtr, api.runSSHSessionStream(store, &runUser, &runItem, nil, runAttachment.InputCh, runAttachment.InputErrCh, &runPrepared,
func(status sshSessionStreamMessage) { func(status sshSessionStreamMessage) {
var sendErr error var sendErr error
status.SessionID = runSessionID status.SessionID = runSessionID
sendErr = api.sendSSHSessionWS(&sendMu, ws, status) sendErr = api.sendSSHSessionWS(&sendMu, ws, status)
if sendErr != nil && api.SSHSessionRegistry != nil { if sendErr != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(runSessionID, "websocket send failed") api.SSHSessionRegistry.RequestClose(runSessionID, SSHSessionCloseWebsocketSendFailed)
} }
}, },
func(data []byte) { func(data []byte) {
var sendErr error var sendErr error
sendErr = api.sendSSHWorkspaceOutput(&sendMu, ws, runSessionID, data) sendErr = api.sendSSHWorkspaceOutput(&sendMu, ws, runSessionID, data)
if sendErr != nil && api.SSHSessionRegistry != nil { if sendErr != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(runSessionID, "websocket send failed") api.SSHSessionRegistry.RequestClose(runSessionID, SSHSessionCloseWebsocketSendFailed)
} }
}) })
}(attachedItem, attachedPrepared, attachedSessionID, attachment) }(user, attachedItem, attachedPrepared, attachedSessionID, attachment)
continue continue
} }
attachment, ok = attachments[msg.SessionID] attachment, ok = attachments[msg.SessionID]
@@ -2710,7 +2723,7 @@ event_loop:
} }
if msg.Type == "detach" { if msg.Type == "detach" {
if api.SSHSessionRegistry != nil { if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(msg.SessionID, "detached") api.SSHSessionRegistry.RequestClose(msg.SessionID, SSHSessionCloseDetached)
} }
select { select {
case attachment.InputErrCh <- io.EOF: case attachment.InputErrCh <- io.EOF:
@@ -2731,7 +2744,7 @@ event_loop:
user.Username, user.Username,
msg.Type) msg.Type)
if api.SSHSessionRegistry != nil { if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(msg.SessionID, "websocket input queue full") api.SSHSessionRegistry.RequestClose(msg.SessionID, SSHSessionCloseWebsocketInputQueueFull)
} }
select { select {
case attachment.InputErrCh <- errors.New("websocket input queue full"): case attachment.InputErrCh <- errors.New("websocket input queue full"):
@@ -2742,7 +2755,7 @@ event_loop:
} }
} }
func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionItem models.SSHSession, transportWS *websocket.Conn, inputCh <-chan sshSessionStreamMessage, inputErrCh <-chan error, prepared *sshSessionPrepared, sendStatus func(sshSessionStreamMessage), sendBinary func([]byte)) { func (api *API) runSSHSessionStream(store *db.Store, user *models.User, sessionItem *models.SSHSession, transportWS *websocket.Conn, inputCh <-chan sshSessionStreamMessage, inputErrCh <-chan error, prepared *sshSessionPrepared, sendStatus func(sshSessionStreamMessage), sendBinary func([]byte)) {
var profile models.SSHAccessProfile var profile models.SSHAccessProfile
var authMethods []ssh.AuthMethod var authMethods []ssh.AuthMethod
var hostKeys []models.SSHServerHostKey var hostKeys []models.SSHServerHostKey
@@ -2754,6 +2767,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
var stderr io.Reader var stderr io.Reader
var runtimeSession *sshActiveSession var runtimeSession *sshActiveSession
var closeReason string var closeReason string
var closeCode SSHSessionCloseCode
var err error var err error
var connectedFingerprint string var connectedFingerprint string
var connectedHostKey ssh.PublicKey var connectedHostKey ssh.PublicKey
@@ -2807,7 +2821,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
promptedOTPCode = promptedAuthInput.OTPCode promptedOTPCode = promptedAuthInput.OTPCode
} }
if prepared != nil { if prepared != nil {
*prepared, err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, *prepared) err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, prepared)
if err != nil { if err != nil {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"prepared session rejected session=%s requester=%s user=%s err=%v", "prepared session rejected session=%s requester=%s user=%s err=%v",
@@ -2825,17 +2839,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
profile = prepared.Profile profile = prepared.Profile
hostKeys = prepared.HostKeys hostKeys = prepared.HostKeys
authMethods = prepared.AuthMethods authMethods = prepared.AuthMethods
api.logSSHSessionPreparedUse(sessionItem.ID, sessionItem.RemoteAddr, user, profile) api.logSSHSessionPreparedUse(sessionItem, user, &profile)
} else { } else {
prepared = &sshSessionPrepared{} prepared = &sshSessionPrepared{}
*prepared, prepareStage, err = api.prepareSSHSession(store, user, sessionItem, promptedPassword, promptedOTPCode) *prepared, prepareStage, err = api.prepareSSHSession(store, user, sessionItem, promptedPassword, promptedOTPCode)
if err != nil { if err != nil {
if prepareStage == "load_profile" { if prepareStage == "load_profile" {
api.logSSHSessionPrepareFailure(sessionItem.ID, sessionItem.RemoteAddr, user.Username, sessionItem.ProfileID, prepareStage, err) api.logSSHSessionPrepareFailure(sessionItem, user, prepareStage, err)
} else if prepared.Profile.ID != "" { } else if prepared.Profile.ID != "" {
api.logSSHSessionConnectFailure(sessionItem, prepared.Profile, user, prepareStage, err) api.logSSHSessionConnectFailure(sessionItem, &prepared.Profile, user, prepareStage, err)
} else { } else {
api.logSSHSessionPrepareFailure(sessionItem.ID, sessionItem.RemoteAddr, user.Username, sessionItem.ProfileID, prepareStage, err) api.logSSHSessionPrepareFailure(sessionItem, user, prepareStage, err)
} }
if sendStatus != nil { if sendStatus != nil {
if prepareStage == "load_profile" { if prepareStage == "load_profile" {
@@ -2857,16 +2871,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
authMethods = prepared.AuthMethods authMethods = prepared.AuthMethods
} }
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", "", 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", "", 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectStart(sessionItem, profile, user) api.logSSHSessionConnectStart(sessionItem, &profile, user)
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "connecting", "", 0, 0, "") _ = store.UpdateSSHSessionStatus(sessionItem.ID, "connecting", "", 0, 0, "")
if sendStatus != nil { if sendStatus != nil {
@@ -2880,7 +2895,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
} }
sshClient, err = ssh.Dial("tcp", net.JoinHostPort(profile.Server.Host, fmt.Sprintf("%d", profile.Server.Port)), sshConfig) sshClient, err = ssh.Dial("tcp", net.JoinHostPort(profile.Server.Host, fmt.Sprintf("%d", profile.Server.Port)), sshConfig)
if err != nil { if err != nil {
api.logSSHSessionConnectFailure(sessionItem, profile, user, "dial", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "dial", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -2892,7 +2907,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
if prepared.PinHostKeyOnFirstUse { if prepared.PinHostKeyOnFirstUse {
if connectedHostKey == nil { if connectedHostKey == nil {
err = errors.New("failed to capture ssh host key") err = errors.New("failed to capture ssh host key")
api.logSSHSessionConnectFailure(sessionItem, profile, user, "pin_host_key", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "pin_host_key", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -2919,7 +2934,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
} }
} }
if pinnedHostKey.ID == "" { if pinnedHostKey.ID == "" {
api.logSSHSessionConnectFailure(sessionItem, profile, user, "pin_host_key", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "pin_host_key", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: "failed to pin host key: " + err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: "failed to pin host key: " + err.Error()})
} }
@@ -2938,11 +2953,12 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
} }
runtimeSession.SetResources(transportWS, sshClient, nil, nil) runtimeSession.SetResources(transportWS, sshClient, nil, nil)
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2950,16 +2966,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
sshSession, err = sshClient.NewSession() sshSession, err = sshClient.NewSession()
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "new_session", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "new_session", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -2970,11 +2987,12 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
defer sshSession.Close() defer sshSession.Close()
runtimeSession.SetResources(transportWS, sshClient, sshSession, nil) runtimeSession.SetResources(transportWS, sshClient, sshSession, nil)
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -2982,16 +3000,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
stdin, err = sshSession.StdinPipe() stdin, err = sshSession.StdinPipe()
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stdin_pipe", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stdin_pipe", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -3002,16 +3021,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
stdout, err = sshSession.StdoutPipe() stdout, err = sshSession.StdoutPipe()
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stdout_pipe", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stdout_pipe", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -3022,16 +3042,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
stderr, err = sshSession.StderrPipe() stderr, err = sshSession.StderrPipe()
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stderr_pipe", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stderr_pipe", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -3041,10 +3062,11 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
} }
runtimeSession.SetResources(transportWS, sshClient, sshSession, stdin) runtimeSession.SetResources(transportWS, sshClient, sshSession, stdin)
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -3056,16 +3078,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
}) })
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "request_pty", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "request_pty", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -3074,11 +3097,12 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
return return
} }
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -3086,16 +3110,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
err = sshSession.Shell() err = sshSession.Shell()
if err != nil { if err != nil {
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
api.logSSHSessionConnectFailure(sessionItem, profile, user, "shell", err) api.logSSHSessionConnectFailure(sessionItem, &profile, user, "shell", err)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "error", Message: err.Error()})
} }
@@ -3104,18 +3129,19 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
return return
} }
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
} }
connectedAt = time.Now().UTC().Unix() connectedAt = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "connected", connectedFingerprint, connectedAt, 0, "") _ = store.UpdateSSHSessionStatus(sessionItem.ID, "connected", connectedFingerprint, connectedAt, 0, "")
api.logSSHSessionConnectSuccess(sessionItem, profile, user, connectedFingerprint) api.logSSHSessionConnectSuccess(sessionItem, &profile, user, connectedFingerprint)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "connected", Message: connectedFingerprint}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "connected", Message: connectedFingerprint})
} }
@@ -3186,10 +3212,11 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
_ = stdin.Close() _ = stdin.Close()
now = time.Now().UTC().Unix() now = time.Now().UTC().Unix()
closeReason = runtimeSession.CloseReason() closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" { if closeReason != "" {
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, closeReason) _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, connectedAt, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, connectedAt, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) { if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
} }
return return
@@ -3227,7 +3254,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
waitErr) waitErr)
} }
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, "") _ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, "")
api.logSSHSessionClosed(sessionItem, profile, user, "closed", "", connectedAt, now) api.logSSHSessionClosed(sessionItem, &profile, user, "closed", "", connectedAt, now)
if sendStatus != nil { if sendStatus != nil {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: ""}) sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: ""})
} }
@@ -3467,27 +3494,16 @@ func (api *API) readSSHSessionWebsocket(ws *websocket.Conn, inputCh chan<- sshSe
} }
} }
func sshSessionWebsocketCloseReason(err error) string { func sshSessionWebsocketCloseCode(err error) SSHSessionCloseCode {
if err != nil && strings.Contains(err.Error(), "input queue full") { if err != nil && strings.Contains(err.Error(), "input queue full") { return SSHSessionCloseWebsocketInputQueueFull }
return err.Error() return SSHSessionCloseWebsocketClosed
}
return "websocket closed"
} }
func isSSHSessionTransportCloseReason(reason string) bool { func isSSHSessionTransportCloseCode(code SSHSessionCloseCode) bool {
reason = strings.TrimSpace(reason) if code == SSHSessionCloseWebsocketClosed { return true }
if reason == "websocket closed" { if code == SSHSessionCloseWebsocketSendFailed { return true }
return true if code == SSHSessionCloseWebsocketInputQueueFull { return true }
} if code == SSHSessionCloseDetached { return true }
if reason == "websocket send failed" {
return true
}
if reason == "websocket input queue full" {
return true
}
if reason == "detached" {
return true
}
return false return false
} }