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 {
SessionID string `json:"session_id"`
Status string `json:"status"`
WebSocketPath string `json:"websocket_path"`
ServerName string `json:"server_name"`
Host string `json:"host"`
Port int `json:"port"`
@@ -1708,7 +1707,6 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
response = sshSessionConnectResponse{
SessionID: item.ID,
Status: item.Status,
WebSocketPath: "/api/ssh/sessions/" + item.ID + "/stream",
ServerName: profile.Server.Name,
Host: profile.Server.Host,
Port: profile.Server.Port,
@@ -1881,38 +1879,6 @@ func (api *API) DisconnectSSHSessionForSelf(w http.ResponseWriter, r *http.Reque
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) {
var user models.User
var ok bool
@@ -2416,11 +2382,8 @@ func (api *API) prepareSSHSession(store *db.Store, user *models.User, sessionIte
return prepared, "", nil
}
func (api *API) reconcileSSHPreparedSessionForUse(store *db.Store, sessionItem *models.SSHSession, prepared *sshSessionPrepared) error {
var err error
func (api *API) validatePreparedSSHSessionForUse(sessionItem *models.SSHSession, prepared *sshSessionPrepared) error {
var i int
var reloadServer bool
var reloadHostKeys bool
if strings.TrimSpace(prepared.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 {
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) == "" {
return errors.New("ssh session server_id is empty")
}
reloadServer = prepared.Profile.ServerID != sessionItem.ServerID || prepared.Profile.Server.ID != sessionItem.ServerID
if !reloadServer && strings.TrimSpace(sessionItem.Host) != "" {
reloadServer = prepared.Profile.Server.Host != sessionItem.Host || prepared.Profile.Server.Port != sessionItem.Port
if strings.TrimSpace(prepared.Session.ServerID) == "" {
return errors.New("prepared ssh session server_id is empty")
}
if reloadServer {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"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
if prepared.Session.ServerID != sessionItem.ServerID {
return fmt.Errorf("prepared ssh session server mismatch: prepared=%s session=%s", prepared.Session.ServerID, sessionItem.ServerID)
}
prepared.Profile.ServerID = prepared.Profile.Server.ID
if strings.TrimSpace(sessionItem.Host) != "" {
prepared.Profile.Server.Host = sessionItem.Host
if strings.TrimSpace(prepared.Profile.ID) == "" {
return errors.New("prepared ssh profile is missing profile id")
}
if sessionItem.Port > 0 {
prepared.Profile.Server.Port = sessionItem.Port
if prepared.Profile.ID != sessionItem.ProfileID {
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++ {
if prepared.HostKeys[i].ServerID != sessionItem.ServerID {
reloadHostKeys = true
break
return fmt.Errorf("prepared ssh host key server mismatch: prepared=%s session=%s", prepared.HostKeys[i].ServerID, sessionItem.ServerID)
}
}
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
}
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) {
var sendMu sync.Mutex
var controlCh chan sshSessionStreamMessage
@@ -2685,10 +2562,11 @@ event_loop:
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:
default: // non-block
}
}()
api.runSSHSessionStream(store, &runUser, &runItem, nil, runAttachment.InputCh, runAttachment.InputErrCh, &runPrepared,
@@ -2727,7 +2605,7 @@ event_loop:
}
select {
case attachment.InputErrCh <- io.EOF:
default:
default: // non-block
}
delete(attachments, msg.SessionID)
continue
@@ -2748,7 +2626,7 @@ event_loop:
}
select {
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
}
if prepared != nil {
err = api.reconcileSSHPreparedSessionForUse(store, sessionItem, prepared)
err = api.validatePreparedSSHSessionForUse(sessionItem, prepared)
if err != nil {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN,
"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/files/upload", api.UploadSSHSessionFilesForSelf)
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/user-cas/:id/sign", api.SignSSHUserKeyForSelf)
router.Handle("GET", "/api/ssh/issuances", api.ListSSHUserCAIssuancesForSelf)
-29
View File
@@ -5970,33 +5970,6 @@ paths:
$ref: '#/components/responses/Unauthorized'
'404':
$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:
post:
tags:
@@ -9641,8 +9614,6 @@ components:
type: string
status:
type: string
websocket_path:
type: string
server_name:
type: string
host:
-1
View File
@@ -1140,7 +1140,6 @@ export interface SSHSessionListResponse {
export interface SSHSessionConnectResponse {
session_id: string
status: string
websocket_path: string
server_name: string
host: string
port: number