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
}
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) {
var row *sql.Row
var item models.SSHSession
@@ -323,7 +323,7 @@ func (api *API) openSSHSessionFileTransferClient(store *db.Store, user models.Us
var connectedHostKey ssh.PublicKey
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 prepareStage == "load_profile" {
return nil, errors.New("ssh access profile not found")
+218 -64
View File
@@ -4,20 +4,37 @@ import "errors"
import "io"
import "strings"
import "sync"
import "time"
import "golang.org/x/crypto/ssh"
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 {
mu sync.Mutex
ws *websocket.Conn
client *ssh.Client
session *ssh.Session
stdin io.WriteCloser
closeCode SSHSessionCloseCode
closeReason string
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 {
mu sync.Mutex
items map[string]*sshActiveSession
@@ -28,14 +45,30 @@ type SSHPromptedAuthInput struct {
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 {
mu sync.Mutex
items map[string]SSHPromptedAuthInput
items map[string]sshPromptedAuthStoreItem
ttl time.Duration
maxEntries int
}
type SSHPreparedSessionStore struct {
mu sync.Mutex
items map[string]sshSessionPrepared
items map[string]sshPreparedSessionStoreItem
ttl time.Duration
maxEntries int
}
func NewSSHSessionRegistry() *SSHSessionRegistry {
@@ -51,7 +84,9 @@ func NewSSHPromptedAuthStore() *SSHPromptedAuthStore {
var store *SSHPromptedAuthStore
store = &SSHPromptedAuthStore{
items: map[string]SSHPromptedAuthInput{},
items: map[string]sshPromptedAuthStoreItem{},
ttl: sshSessionPreparationTTL,
maxEntries: sshSessionPreparationMaxEntries,
}
return store
}
@@ -60,32 +95,91 @@ func NewSSHPreparedSessionStore() *SSHPreparedSessionStore {
var store *SSHPreparedSessionStore
store = &SSHPreparedSessionStore{
items: map[string]sshSessionPrepared{},
items: map[string]sshPreparedSessionStoreItem{},
ttl: sshSessionPreparationTTL,
maxEntries: sshSessionPreparationMaxEntries,
}
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) {
var trimmedID string
var now time.Time
if s == nil { return }
trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return }
if input.Password == "" && input.OTPCode == "" { return }
now = time.Now().UTC()
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()
}
func (s *SSHPromptedAuthStore) Take(id string) SSHPromptedAuthInput {
var trimmedID string
var input SSHPromptedAuthInput
var item sshPromptedAuthStoreItem
var ok bool
var now time.Time
if s == nil { return input }
trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return input }
now = time.Now().UTC()
s.mu.Lock()
input = s.items[trimmedID]
s.purgeExpiredLocked(now)
item, ok = s.items[trimmedID]
if ok {
input = item.Input
delete(s.items, trimmedID)
}
s.mu.Unlock()
return input
}
@@ -93,7 +187,7 @@ func (s *SSHPromptedAuthStore) Take(id string) SSHPromptedAuthInput {
func (s *SSHPromptedAuthStore) Delete(id string) {
var trimmedID string
if s == nil { return}
if s == nil { return }
trimmedID = strings.TrimSpace(id)
if trimmedID == "" { return }
s.mu.Lock()
@@ -101,8 +195,45 @@ func (s *SSHPromptedAuthStore) Delete(id string) {
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) {
var trimmedID string
var now time.Time
if s == nil {
return
@@ -111,15 +242,24 @@ func (s *SSHPreparedSessionStore) Put(id string, item sshSessionPrepared) {
if trimmedID == "" {
return
}
now = time.Now().UTC()
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()
}
func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
var trimmedID string
var item sshSessionPrepared
var storeItem sshPreparedSessionStoreItem
var ok bool
var now time.Time
if s == nil {
return item, false
@@ -128,8 +268,13 @@ func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
if trimmedID == "" {
return item, false
}
now = time.Now().UTC()
s.mu.Lock()
item, ok = s.items[trimmedID]
s.purgeExpiredLocked(now)
storeItem, ok = s.items[trimmedID]
if ok {
item = storeItem.Session
}
s.mu.Unlock()
return item, ok
}
@@ -137,7 +282,9 @@ func (s *SSHPreparedSessionStore) Get(id string) (sshSessionPrepared, bool) {
func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) {
var trimmedID string
var item sshSessionPrepared
var storeItem sshPreparedSessionStoreItem
var ok bool
var now time.Time
if s == nil {
return item, false
@@ -146,9 +293,12 @@ func (s *SSHPreparedSessionStore) Take(id string) (sshSessionPrepared, bool) {
if trimmedID == "" {
return item, false
}
now = time.Now().UTC()
s.mu.Lock()
item, ok = s.items[trimmedID]
s.purgeExpiredLocked(now)
storeItem, ok = s.items[trimmedID]
if ok {
item = storeItem.Session
delete(s.items, trimmedID)
}
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
if r == nil {
return false
}
if r == nil { return false }
r.mu.Lock()
item = r.items[id]
r.mu.Unlock()
if item == nil {
return false
}
item.RequestClose(reason)
if item == nil { return false }
item.RequestCloseWithReason(code, reason)
return true
}
@@ -216,68 +375,70 @@ func (s *sshActiveSession) SetResources(ws *websocket.Conn, client *ssh.Client,
var closeSession *ssh.Session
var closeStdin io.WriteCloser
if s == nil {
return
}
if s == nil { return }
s.mu.Lock()
if s.closed {
closeWS = ws
closeClient = client
closeSession = session
closeStdin = stdin
s.mu.Unlock()
if closeStdin != nil {
_ = closeStdin.Close()
}
if closeSession != nil {
_ = closeSession.Close()
}
if closeClient != nil {
_ = closeClient.Close()
}
if closeWS != nil {
_ = closeWS.Close()
}
if closeStdin != nil { _ = closeStdin.Close() }
if closeSession != nil { _ = closeSession.Close() }
if closeClient != nil { _ = closeClient.Close() }
if closeWS != nil { _ = closeWS.Close() }
return
}
if ws != nil {
s.ws = ws
}
if client != nil {
s.client = client
}
if session != nil {
s.session = session
}
if stdin != nil {
s.stdin = stdin
}
if ws != nil { s.ws = ws }
if client != nil { s.client = client }
if session != nil { s.session = session }
if stdin != nil { s.stdin = stdin }
s.mu.Unlock()
}
func (s *sshActiveSession) CloseReason() string {
var reason string
if s == nil {
return ""
}
if s == nil { return "" }
s.mu.Lock()
reason = s.closeReason
s.mu.Unlock()
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 client *ssh.Client
var session *ssh.Session
var stdin io.WriteCloser
if s == nil {
return
}
if s == nil { return }
s.mu.Lock()
if code == SSHSessionCloseNone {
code = SSHSessionCloseUnknown
}
if strings.TrimSpace(reason) == "" {
reason = sshSessionCloseMessage(code)
}
if s.closeReason == "" {
s.closeCode = code
s.closeReason = reason
}
if s.closed {
@@ -290,16 +451,9 @@ func (s *sshActiveSession) RequestClose(reason string) {
session = s.session
stdin = s.stdin
s.mu.Unlock()
if stdin != nil {
_ = stdin.Close()
}
if session != nil {
_ = session.Close()
}
if client != nil {
_ = client.Close()
}
if ws != nil {
_ = ws.Close()
}
if stdin != nil { _ = stdin.Close() }
if session != nil { _ = session.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 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,
"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,
@@ -168,11 +168,11 @@ func (api *API) logSSHSessionConnectStart(sessionItem models.SSHSession, profile
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,
"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,
remoteAddr,
sessionItem.ID,
sessionItem.RemoteAddr,
user.Username,
profile.ID,
profile.Name,
@@ -184,11 +184,11 @@ func (api *API) logSSHSessionPrepare(sessionID string, remoteAddr string, user m
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,
"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,
remoteAddr,
sessionItem.ID,
sessionItem.RemoteAddr,
user.Username,
profile.ID,
profile.Name,
@@ -200,7 +200,7 @@ func (api *API) logSSHSessionPreparedUse(sessionID string, remoteAddr string, us
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,
"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,
@@ -217,7 +217,7 @@ func (api *API) logSSHSessionConnectSuccess(sessionItem models.SSHSession, profi
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,
"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,
@@ -235,18 +235,18 @@ func (api *API) logSSHSessionConnectFailure(sessionItem models.SSHSession, profi
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,
"connect failed session=%s requester=%s user=%s profile=%s stage=%s err=%v",
sessionID,
remoteAddr,
username,
profileID,
sessionItem.ID,
sessionItem.RemoteAddr,
user.Username,
sessionItem.ProfileID,
stage,
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
durationSeconds = 0
@@ -1581,6 +1581,8 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
var prepareStage string
var hostKeyFingerprint string
var belongs bool
var pendingCount int
var pendingCutoff int64
var err error
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"})
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"])
if err != nil {
if err == sql.ErrNoRows {
@@ -1665,7 +1682,7 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
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 prepareStage == "host_key_missing" || prepareStage == "build_auth" {
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)
}
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()
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))
defaultUser = strings.TrimSpace(defaultUser)
if defaultUser == "" {
defaultUser = "codit"
}
config = &ssh.ClientConfig{
User: defaultUser,
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.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered))
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "unable to authenticate") {
return item, nil
}
if strings.Contains(strings.ToLower(err.Error()), "no auth passed yet") {
return item, nil
}
if strings.Contains(strings.ToLower(err.Error()), "unable to authenticate") { return item, nil }
if strings.Contains(strings.ToLower(err.Error()), "no auth passed yet") { 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 err error
prepared.Session = sessionItem
prepared.Session = *sessionItem
prepared.Profile, err = store.GetSSHAccessProfileForUser(user.ID, sessionItem.ProfileID)
if err != nil {
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
}
api.logSSHSessionPrepare(sessionItem.ID, sessionItem.RemoteAddr, user, prepared.Profile)
api.logSSHSessionPrepare(sessionItem, user, &prepared.Profile)
prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
if err != nil {
return prepared, "load_host_keys", err
@@ -2405,26 +2416,26 @@ func (api *API) prepareSSHSession(store *db.Store, user models.User, sessionItem
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 i int
var reloadServer bool
var reloadHostKeys bool
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 {
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 {
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 {
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) == "" {
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
@@ -2444,7 +2455,7 @@ func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem m
sessionItem.Port)
prepared.Profile.Server, err = store.GetSSHServer(sessionItem.ServerID)
if err != nil {
return prepared, err
return err
}
prepared.Profile.ServerID = prepared.Profile.Server.ID
if strings.TrimSpace(sessionItem.Host) != "" {
@@ -2465,22 +2476,37 @@ func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem m
if reloadHostKeys {
prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
if err != nil {
return prepared, err
return err
}
prepared.PinHostKeyOnFirstUse = false
if len(prepared.HostKeys) == 0 {
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.Session = sessionItem
return prepared, nil
prepared.Session = *sessionItem
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) {
// 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 inputCh chan sshSessionStreamMessage
var inputErrCh chan error
@@ -2490,45 +2516,35 @@ func (api *API) serveSSHSessionStream(ws *websocket.Conn, store *db.Store, user
var preparedPtr *sshSessionPrepared
defer ws.Close()
inputCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize)
inputErrCh = 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
reason = sshSessionWebsocketCloseReason(err)
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionItem.ID, reason)
}
inputErrCh <- err
}()
// read websocket and write to inputCh and readerErrCh
go api.readSSHSessionWebsocket(ws, inputCh, readerErrCh)
// 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)
prepared, preparedOK = api.SSHPreparedSessionStore.Take(sessionItem.ID)
if !preparedOK {
prepared = sshSessionPrepared{}
} else {
preparedPtr = &prepared
}
if preparedOK { preparedPtr = &prepared }
// if preparedOK is not true, preparedPtr is nil. api.RunSSHSessionStream build one inside itself.
// this reads the inputCh
api.runSSHSessionStream(store, user, sessionItem, ws, inputCh, inputErrCh,
preparedPtr,
func(msg sshSessionStreamMessage) {
api.runSSHSessionStream(store, &user, &sessionItem, ws, inputCh, inputErrCh, preparedPtr,
func(msg sshSessionStreamMessage) { // stream message sender
var err error
err = api.sendSSHSessionWS(&sendMu, ws, msg)
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
err = api.sendSSHSessionBinary(&sendMu, ws, data)
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 attachmentDoneCh chan sshWorkspaceAttachmentDone
var attachmentDone sshWorkspaceAttachmentDone
var websocketCloseReason string
var websocketCloseCode SSHSessionCloseCode
defer ws.Close()
controlCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize)
@@ -2561,7 +2577,7 @@ event_loop:
for {
select {
case err = <-controlErrCh:
websocketCloseReason = sshSessionWebsocketCloseReason(err)
websocketCloseCode = sshSessionWebsocketCloseCode(err)
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"workspace stream closed requester=%s user=%s err=%v",
remoteAddr,
@@ -2570,7 +2586,7 @@ event_loop:
for attachedID, attachment = range attachments {
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(attachedID, websocketCloseReason)
api.SSHSessionRegistry.RequestClose(attachedID, websocketCloseCode)
}
select {
case attachment.InputErrCh <- err:
@@ -2624,9 +2640,7 @@ event_loop:
if !preparedOK {
attachedStatus = "closed"
attachedMessage = item.Error
if item.Status == "error" {
attachedStatus = "error"
}
if item.Status == "error" { attachedStatus = "error" }
if attachedMessage == "" {
if item.Status == "closed" {
attachedMessage = "SSH session is closed"
@@ -2668,33 +2682,32 @@ event_loop:
user.Username)
attachedItem = item
attachedSessionID = attachedItem.ID
go func(runItem models.SSHSession, runPrepared sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) {
var runPreparedPtr *sshSessionPrepared
runPreparedPtr = &runPrepared
go func(runUser models.User, runItem models.SSHSession, runPrepared sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) {
// accept runUser, runItem, runPrepared by value because the values can mutate
// in the main event loop if we reference them directly(closure or pointer)
defer func() {
select {
case attachmentDoneCh <- sshWorkspaceAttachmentDone{SessionID: runSessionID, Attachment: runAttachment}:
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) {
var sendErr error
status.SessionID = runSessionID
sendErr = api.sendSSHSessionWS(&sendMu, ws, status)
if sendErr != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(runSessionID, "websocket send failed")
api.SSHSessionRegistry.RequestClose(runSessionID, SSHSessionCloseWebsocketSendFailed)
}
},
func(data []byte) {
var sendErr error
sendErr = api.sendSSHWorkspaceOutput(&sendMu, ws, runSessionID, data)
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
}
attachment, ok = attachments[msg.SessionID]
@@ -2710,7 +2723,7 @@ event_loop:
}
if msg.Type == "detach" {
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(msg.SessionID, "detached")
api.SSHSessionRegistry.RequestClose(msg.SessionID, SSHSessionCloseDetached)
}
select {
case attachment.InputErrCh <- io.EOF:
@@ -2731,7 +2744,7 @@ event_loop:
user.Username,
msg.Type)
if api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(msg.SessionID, "websocket input queue full")
api.SSHSessionRegistry.RequestClose(msg.SessionID, SSHSessionCloseWebsocketInputQueueFull)
}
select {
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 authMethods []ssh.AuthMethod
var hostKeys []models.SSHServerHostKey
@@ -2754,6 +2767,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
var stderr io.Reader
var runtimeSession *sshActiveSession
var closeReason string
var closeCode SSHSessionCloseCode
var err error
var connectedFingerprint string
var connectedHostKey ssh.PublicKey
@@ -2807,7 +2821,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
promptedOTPCode = promptedAuthInput.OTPCode
}
if prepared != nil {
*prepared, err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, *prepared)
err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, prepared)
if err != nil {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"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
hostKeys = prepared.HostKeys
authMethods = prepared.AuthMethods
api.logSSHSessionPreparedUse(sessionItem.ID, sessionItem.RemoteAddr, user, profile)
api.logSSHSessionPreparedUse(sessionItem, user, &profile)
} else {
prepared = &sshSessionPrepared{}
*prepared, prepareStage, err = api.prepareSSHSession(store, user, sessionItem, promptedPassword, promptedOTPCode)
if err != nil {
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 != "" {
api.logSSHSessionConnectFailure(sessionItem, prepared.Profile, user, prepareStage, err)
api.logSSHSessionConnectFailure(sessionItem, &prepared.Profile, user, prepareStage, err)
} else {
api.logSSHSessionPrepareFailure(sessionItem.ID, sessionItem.RemoteAddr, user.Username, sessionItem.ProfileID, prepareStage, err)
api.logSSHSessionPrepareFailure(sessionItem, user, prepareStage, err)
}
if sendStatus != nil {
if prepareStage == "load_profile" {
@@ -2857,16 +2871,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
authMethods = prepared.AuthMethods
}
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", "", 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectStart(sessionItem, profile, user)
api.logSSHSessionConnectStart(sessionItem, &profile, user)
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "connecting", "", 0, 0, "")
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)
if err != nil {
api.logSSHSessionConnectFailure(sessionItem, profile, user, "dial", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "dial", err)
if sendStatus != nil {
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 connectedHostKey == nil {
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 {
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 == "" {
api.logSSHSessionConnectFailure(sessionItem, profile, user, "pin_host_key", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "pin_host_key", err)
if sendStatus != nil {
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)
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
@@ -2950,16 +2966,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
sshSession, err = sshClient.NewSession()
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "new_session", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "new_session", err)
if sendStatus != nil {
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()
runtimeSession.SetResources(transportWS, sshClient, sshSession, nil)
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
@@ -2982,16 +3000,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
stdin, err = sshSession.StdinPipe()
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stdin_pipe", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stdin_pipe", err)
if sendStatus != nil {
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()
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stdout_pipe", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stdout_pipe", err)
if sendStatus != nil {
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()
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "stderr_pipe", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "stderr_pipe", err)
if sendStatus != nil {
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)
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = 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})
}
return
@@ -3056,16 +3078,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
})
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "request_pty", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "request_pty", err)
if sendStatus != nil {
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
}
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
@@ -3086,16 +3110,17 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
err = sshSession.Shell()
if err != nil {
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
api.logSSHSessionConnectFailure(sessionItem, profile, user, "shell", err)
api.logSSHSessionConnectFailure(sessionItem, &profile, user, "shell", err)
if sendStatus != nil {
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
}
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
now = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, 0, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, 0, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
}
connectedAt = time.Now().UTC().Unix()
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "connected", connectedFingerprint, connectedAt, 0, "")
api.logSSHSessionConnectSuccess(sessionItem, profile, user, connectedFingerprint)
api.logSSHSessionConnectSuccess(sessionItem, &profile, user, connectedFingerprint)
if sendStatus != nil {
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()
now = time.Now().UTC().Unix()
closeReason = runtimeSession.CloseReason()
closeCode = runtimeSession.CloseCode()
if closeReason != "" {
_ = store.UpdateSSHSessionStatus(sessionItem.ID, "closed", connectedFingerprint, connectedAt, now, closeReason)
api.logSSHSessionClosed(sessionItem, profile, user, "closed", closeReason, connectedAt, now)
if sendStatus != nil && !isSSHSessionTransportCloseReason(closeReason) {
api.logSSHSessionClosed(sessionItem, &profile, user, "closed", closeReason, connectedAt, now)
if sendStatus != nil && !isSSHSessionTransportCloseCode(closeCode) {
sendStatus(sshSessionStreamMessage{Type: "status", Status: "closed", Message: closeReason})
}
return
@@ -3227,7 +3254,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user models.User, sessionIt
waitErr)
}
_ = 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 {
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 {
if err != nil && strings.Contains(err.Error(), "input queue full") {
return err.Error()
}
return "websocket closed"
func sshSessionWebsocketCloseCode(err error) SSHSessionCloseCode {
if err != nil && strings.Contains(err.Error(), "input queue full") { return SSHSessionCloseWebsocketInputQueueFull }
return SSHSessionCloseWebsocketClosed
}
func isSSHSessionTransportCloseReason(reason string) bool {
reason = strings.TrimSpace(reason)
if reason == "websocket closed" {
return true
}
if reason == "websocket send failed" {
return true
}
if reason == "websocket input queue full" {
return true
}
if reason == "detached" {
return true
}
func isSSHSessionTransportCloseCode(code SSHSessionCloseCode) bool {
if code == SSHSessionCloseWebsocketClosed { return true }
if code == SSHSessionCloseWebsocketSendFailed { return true }
if code == SSHSessionCloseWebsocketInputQueueFull { return true }
if code == SSHSessionCloseDetached { return true }
return false
}