|
|
@@ -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",
|
|
|
|