Compare commits

...

2 Commits

4 changed files with 44 additions and 197 deletions
+26 -148
View File
@@ -102,7 +102,6 @@ type sshSessionConnectRequest struct {
type sshSessionConnectResponse struct { type sshSessionConnectResponse struct {
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
Status string `json:"status"` Status string `json:"status"`
WebSocketPath string `json:"websocket_path"`
ServerName string `json:"server_name"` ServerName string `json:"server_name"`
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
@@ -1708,7 +1707,6 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
response = sshSessionConnectResponse{ response = sshSessionConnectResponse{
SessionID: item.ID, SessionID: item.ID,
Status: item.Status, Status: item.Status,
WebSocketPath: "/api/ssh/sessions/" + item.ID + "/stream",
ServerName: profile.Server.Name, ServerName: profile.Server.Name,
Host: profile.Server.Host, Host: profile.Server.Host,
Port: profile.Server.Port, Port: profile.Server.Port,
@@ -1881,38 +1879,6 @@ func (api *API) DisconnectSSHSessionForSelf(w http.ResponseWriter, r *http.Reque
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }
func (api *API) StreamSSHSessionForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var item models.SSHSession
var store *db.Store
var err error
// one websocket deals with one ssh session
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
store = api.store(r)
item, err = store.GetSSHSession(params["id"])
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusInternalServerError)
return
}
if item.UserID != user.ID {
w.WriteHeader(http.StatusForbidden)
return
}
websocket.Handler(func(ws *websocket.Conn) {
api.serveSSHSessionStream(ws, store, user, item)
}).ServeHTTP(w, r)
}
func (api *API) StreamSSHWorkspaceForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) { func (api *API) StreamSSHWorkspaceForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User var user models.User
var ok bool var ok bool
@@ -2416,11 +2382,8 @@ func (api *API) prepareSSHSession(store *db.Store, user *models.User, sessionIte
return prepared, "", nil return prepared, "", nil
} }
func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem *models.SSHSession, prepared *sshSessionPrepared) error { func (api *API) validatePreparedSSHSessionForUse(sessionItem *models.SSHSession, prepared *sshSessionPrepared) error {
var err error
var i int var i int
var reloadServer bool
var reloadHostKeys bool
if strings.TrimSpace(prepared.Session.ID) == "" { if strings.TrimSpace(prepared.Session.ID) == "" {
return errors.New("prepared ssh session is missing session id") return errors.New("prepared ssh session is missing session id")
@@ -2428,127 +2391,41 @@ func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem *
if prepared.Session.ID != sessionItem.ID { if prepared.Session.ID != sessionItem.ID {
return 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 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 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 errors.New("ssh session server_id is empty") return errors.New("ssh session server_id is empty")
} }
if strings.TrimSpace(prepared.Session.ServerID) == "" {
reloadServer = prepared.Profile.ServerID != sessionItem.ServerID || prepared.Profile.Server.ID != sessionItem.ServerID return errors.New("prepared ssh session server_id is empty")
if !reloadServer && strings.TrimSpace(sessionItem.Host) != "" {
reloadServer = prepared.Profile.Server.Host != sessionItem.Host || prepared.Profile.Server.Port != sessionItem.Port
} }
if reloadServer { if prepared.Session.ServerID != sessionItem.ServerID {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, return fmt.Errorf("prepared ssh session server mismatch: prepared=%s session=%s", prepared.Session.ServerID, sessionItem.ServerID)
"prepared session target mismatch session=%s profile=%s prepared_server=%s prepared_target=%s:%d session_server=%s session_target=%s:%d action=reload_selected_server",
sessionItem.ID,
sessionItem.ProfileID,
prepared.Profile.ServerID,
prepared.Profile.Server.Host,
prepared.Profile.Server.Port,
sessionItem.ServerID,
sessionItem.Host,
sessionItem.Port)
prepared.Profile.Server, err = store.GetSSHServer(sessionItem.ServerID)
if err != nil {
return err
} }
prepared.Profile.ServerID = prepared.Profile.Server.ID if strings.TrimSpace(prepared.Profile.ID) == "" {
if strings.TrimSpace(sessionItem.Host) != "" { return errors.New("prepared ssh profile is missing profile id")
prepared.Profile.Server.Host = sessionItem.Host
} }
if sessionItem.Port > 0 { if prepared.Profile.ID != sessionItem.ProfileID {
prepared.Profile.Server.Port = sessionItem.Port return fmt.Errorf("prepared ssh profile mismatch: prepared=%s session=%s", prepared.Profile.ID, sessionItem.ProfileID)
} }
reloadHostKeys = true if prepared.Profile.ServerID != sessionItem.ServerID {
return fmt.Errorf("prepared ssh profile server mismatch: prepared=%s session=%s", prepared.Profile.ServerID, sessionItem.ServerID)
}
if prepared.Profile.Server.ID != sessionItem.ServerID {
return fmt.Errorf("prepared ssh server mismatch: prepared=%s session=%s", prepared.Profile.Server.ID, sessionItem.ServerID)
}
if strings.TrimSpace(sessionItem.Host) != "" && prepared.Profile.Server.Host != sessionItem.Host {
return fmt.Errorf("prepared ssh target host mismatch: prepared=%s session=%s", prepared.Profile.Server.Host, sessionItem.Host)
}
if sessionItem.Port > 0 && prepared.Profile.Server.Port != sessionItem.Port {
return fmt.Errorf("prepared ssh target port mismatch: prepared=%d session=%d", prepared.Profile.Server.Port, sessionItem.Port)
} }
for i = 0; i < len(prepared.HostKeys); i++ { for i = 0; i < len(prepared.HostKeys); i++ {
if prepared.HostKeys[i].ServerID != sessionItem.ServerID { if prepared.HostKeys[i].ServerID != sessionItem.ServerID {
reloadHostKeys = true return fmt.Errorf("prepared ssh host key server mismatch: prepared=%s session=%s", prepared.HostKeys[i].ServerID, sessionItem.ServerID)
break
} }
} }
if reloadHostKeys {
prepared.HostKeys, err = store.ListSSHServerHostKeys(sessionItem.ServerID)
if err != nil {
return err
}
prepared.PinHostKeyOnFirstUse = false
if len(prepared.HostKeys) == 0 {
if normalizeSSHServerHostKeyPolicy(prepared.Profile.Server.HostKeyPolicy) != "trust_on_first_use" {
return errors.New("no pinned host key for " + prepared.Profile.Server.Name)
}
prepared.PinHostKeyOnFirstUse = true
}
}
prepared.Session = *sessionItem
return 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) {
// 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
var readerErrCh chan error
var prepared sshSessionPrepared
var preparedOK bool
var preparedPtr *sshSessionPrepared
defer ws.Close()
inputCh = make(chan sshSessionStreamMessage, sshSessionInputBufferSize)
inputErrCh = make(chan error, 1)
readerErrCh = make(chan error, 1)
// 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 { 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) { // stream message sender
var err error
err = api.sendSSHSessionWS(&sendMu, ws, msg)
if err != nil && api.SSHSessionRegistry != nil {
api.SSHSessionRegistry.RequestClose(sessionItem.ID, SSHSessionCloseWebsocketSendFailed)
}
},
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, SSHSessionCloseWebsocketSendFailed)
}
})
}
func (api *API) serveSSHWorkspaceStream(ws *websocket.Conn, store *db.Store, user models.User, remoteAddr string) { func (api *API) serveSSHWorkspaceStream(ws *websocket.Conn, store *db.Store, user models.User, remoteAddr string) {
var sendMu sync.Mutex var sendMu sync.Mutex
var controlCh chan sshSessionStreamMessage var controlCh chan sshSessionStreamMessage
@@ -2685,10 +2562,11 @@ event_loop:
go func(runUser models.User, runItem models.SSHSession, runPrepared sshSessionPrepared, runSessionID string, runAttachment *sshWorkspaceAttachment) { 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 // accept runUser, runItem, runPrepared by value because the values can mutate
// in the main event loop if we reference them directly(closure or pointer) // in the main event loop if we reference them directly(closure or pointer)
defer func() { defer func() {
select { select {
case attachmentDoneCh <- sshWorkspaceAttachmentDone{SessionID: runSessionID, Attachment: runAttachment}: case attachmentDoneCh <- sshWorkspaceAttachmentDone{SessionID: runSessionID, Attachment: runAttachment}:
default: default: // non-block
} }
}() }()
api.runSSHSessionStream(store, &runUser, &runItem, nil, runAttachment.InputCh, runAttachment.InputErrCh, &runPrepared, api.runSSHSessionStream(store, &runUser, &runItem, nil, runAttachment.InputCh, runAttachment.InputErrCh, &runPrepared,
@@ -2727,7 +2605,7 @@ event_loop:
} }
select { select {
case attachment.InputErrCh <- io.EOF: case attachment.InputErrCh <- io.EOF:
default: default: // non-block
} }
delete(attachments, msg.SessionID) delete(attachments, msg.SessionID)
continue continue
@@ -2748,7 +2626,7 @@ event_loop:
} }
select { select {
case attachment.InputErrCh <- errors.New("websocket input queue full"): case attachment.InputErrCh <- errors.New("websocket input queue full"):
default: default: // non-block
} }
} }
} }
@@ -2821,7 +2699,7 @@ func (api *API) runSSHSessionStream(store *db.Store, user *models.User, sessionI
promptedOTPCode = promptedAuthInput.OTPCode promptedOTPCode = promptedAuthInput.OTPCode
} }
if prepared != nil { if prepared != nil {
err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, prepared) err = api.validatePreparedSSHSessionForUse(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",
-1
View File
@@ -793,7 +793,6 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf) router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf) router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf) router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf)
router.Handle("GET", "/api/ssh/sessions/:id/stream", api.StreamSSHSessionForSelf)
router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate) router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate)
router.Handle("POST", "/api/ssh/user-cas/:id/sign", api.SignSSHUserKeyForSelf) router.Handle("POST", "/api/ssh/user-cas/:id/sign", api.SignSSHUserKeyForSelf)
router.Handle("GET", "/api/ssh/issuances", api.ListSSHUserCAIssuancesForSelf) router.Handle("GET", "/api/ssh/issuances", api.ListSSHUserCAIssuancesForSelf)
-29
View File
@@ -5970,33 +5970,6 @@ paths:
$ref: '#/components/responses/Unauthorized' $ref: '#/components/responses/Unauthorized'
'404': '404':
$ref: '#/components/responses/NotFound' $ref: '#/components/responses/NotFound'
/api/ssh/sessions/{id}/stream:
get:
tags:
- SSH Self Service
summary: Stream SSHSession For Self
operationId: StreamSSHSessionForSelf
x-codit-handler: StreamSSHSessionForSelf
description: WebSocket stream endpoint. The HTTP request upgrades to a WebSocket connection.
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Successful response.
content:
text/plain:
schema:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
/api/ssh/cert/inspect: /api/ssh/cert/inspect:
post: post:
tags: tags:
@@ -9641,8 +9614,6 @@ components:
type: string type: string
status: status:
type: string type: string
websocket_path:
type: string
server_name: server_name:
type: string type: string
host: host:
-1
View File
@@ -1140,7 +1140,6 @@ export interface SSHSessionListResponse {
export interface SSHSessionConnectResponse { export interface SSHSessionConnectResponse {
session_id: string session_id: string
status: string status: string
websocket_path: string
server_name: string server_name: string
host: string host: string
port: number port: number