github.com/argoproj/argo-cd/v3@v3.2.1/server/application/websocket.go (about) 1 package application 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "sync" 9 "time" 10 11 "github.com/argoproj/argo-cd/v3/common" 12 httputil "github.com/argoproj/argo-cd/v3/util/http" 13 "github.com/argoproj/argo-cd/v3/util/rbac" 14 util_session "github.com/argoproj/argo-cd/v3/util/session" 15 16 "github.com/gorilla/websocket" 17 log "github.com/sirupsen/logrus" 18 "k8s.io/client-go/tools/remotecommand" 19 ) 20 21 const ( 22 ReconnectCode = 1 23 ReconnectMessage = "\nReconnect because the token was refreshed...\n" 24 ) 25 26 var upgrader = func() websocket.Upgrader { 27 upgrader := websocket.Upgrader{} 28 upgrader.HandshakeTimeout = time.Second * 2 29 upgrader.CheckOrigin = func(_ *http.Request) bool { 30 return true 31 } 32 return upgrader 33 }() 34 35 // terminalSession implements PtyHandler 36 type terminalSession struct { 37 ctx context.Context 38 wsConn *websocket.Conn 39 sizeChan chan remotecommand.TerminalSize 40 doneChan chan struct{} 41 readLock sync.Mutex 42 writeLock sync.Mutex 43 sessionManager *util_session.SessionManager 44 token *string 45 appRBACName string 46 terminalOpts *TerminalOptions 47 } 48 49 // getToken get auth token from web socket request 50 func getToken(r *http.Request) (string, error) { 51 cookies := r.Cookies() 52 return httputil.JoinCookies(common.AuthCookieName, cookies) 53 } 54 55 // newTerminalSession create terminalSession 56 func newTerminalSession(ctx context.Context, w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager, appRBACName string, terminalOpts *TerminalOptions) (*terminalSession, error) { 57 token, err := getToken(r) 58 if err != nil { 59 return nil, err 60 } 61 62 conn, err := upgrader.Upgrade(w, r, responseHeader) 63 if err != nil { 64 return nil, err 65 } 66 session := &terminalSession{ 67 ctx: ctx, 68 wsConn: conn, 69 sizeChan: make(chan remotecommand.TerminalSize), 70 doneChan: make(chan struct{}), 71 sessionManager: sessionManager, 72 token: &token, 73 appRBACName: appRBACName, 74 terminalOpts: terminalOpts, 75 } 76 return session, nil 77 } 78 79 // Done close the done channel. 80 func (t *terminalSession) Done() { 81 close(t.doneChan) 82 } 83 84 func (t *terminalSession) StartKeepalives(dur time.Duration) { 85 ticker := time.NewTicker(dur) 86 defer ticker.Stop() 87 for { 88 select { 89 case <-ticker.C: 90 err := t.Ping() 91 if err != nil { 92 log.Errorf("ping error: %v", err) 93 return 94 } 95 case <-t.doneChan: 96 return 97 } 98 } 99 } 100 101 // Next called in a loop from remotecommand as long as the process is running 102 func (t *terminalSession) Next() *remotecommand.TerminalSize { 103 select { 104 case size := <-t.sizeChan: 105 return &size 106 case <-t.doneChan: 107 return nil 108 } 109 } 110 111 // reconnect send reconnect code to client and ask them init new ws session 112 func (t *terminalSession) reconnect() (int, error) { 113 reconnectCommand, _ := json.Marshal(TerminalCommand{ 114 Code: ReconnectCode, 115 }) 116 reconnectMessage, _ := json.Marshal(TerminalMessage{ 117 Operation: "stdout", 118 Data: ReconnectMessage, 119 }) 120 t.writeLock.Lock() 121 err := t.wsConn.WriteMessage(websocket.TextMessage, reconnectMessage) 122 if err != nil { 123 log.Errorf("write message err: %v", err) 124 return 0, err 125 } 126 err = t.wsConn.WriteMessage(websocket.TextMessage, reconnectCommand) 127 if err != nil { 128 log.Errorf("write message err: %v", err) 129 return 0, err 130 } 131 t.writeLock.Unlock() 132 return 0, nil 133 } 134 135 func (t *terminalSession) validatePermissions(p []byte) (int, error) { 136 permissionDeniedMessage, _ := json.Marshal(TerminalMessage{ 137 Operation: "stdout", 138 Data: "Permission denied", 139 }) 140 if err := t.terminalOpts.Enf.EnforceErr(t.ctx.Value("claims"), rbac.ResourceApplications, rbac.ActionGet, t.appRBACName); err != nil { 141 err = t.wsConn.WriteMessage(websocket.TextMessage, permissionDeniedMessage) 142 if err != nil { 143 log.Errorf("permission denied message err: %v", err) 144 } 145 return copy(p, EndOfTransmission), common.PermissionDeniedAPIError 146 } 147 148 if err := t.terminalOpts.Enf.EnforceErr(t.ctx.Value("claims"), rbac.ResourceExec, rbac.ActionCreate, t.appRBACName); err != nil { 149 err = t.wsConn.WriteMessage(websocket.TextMessage, permissionDeniedMessage) 150 if err != nil { 151 log.Errorf("permission denied message err: %v", err) 152 } 153 return copy(p, EndOfTransmission), common.PermissionDeniedAPIError 154 } 155 return 0, nil 156 } 157 158 func (t *terminalSession) performValidationsAndReconnect(p []byte) (int, error) { 159 // In disable auth mode, no point verifying the token or validating permissions 160 if t.terminalOpts.DisableAuth { 161 return 0, nil 162 } 163 164 // check if token still valid 165 _, newToken, err := t.sessionManager.VerifyToken(*t.token) 166 // err in case if token is revoked, newToken in case if refresh happened 167 if err != nil || newToken != "" { 168 // need to send reconnect code in case if token was refreshed 169 return t.reconnect() 170 } 171 code, err := t.validatePermissions(p) 172 if err != nil { 173 return code, err 174 } 175 176 return 0, nil 177 } 178 179 // Read called in a loop from remote command as long as the process is running 180 func (t *terminalSession) Read(p []byte) (int, error) { 181 code, err := t.performValidationsAndReconnect(p) 182 if err != nil { 183 return code, err 184 } 185 186 t.readLock.Lock() 187 _, message, err := t.wsConn.ReadMessage() 188 t.readLock.Unlock() 189 if err != nil { 190 if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 191 log.Errorf("unexpected closer error: %v", err) 192 return copy(p, EndOfTransmission), err 193 } 194 log.Errorf("read message error: %v", err) 195 return copy(p, EndOfTransmission), err 196 } 197 var msg TerminalMessage 198 if err := json.Unmarshal(message, &msg); err != nil { 199 log.Errorf("read parse message err: %v", err) 200 return copy(p, EndOfTransmission), err 201 } 202 switch msg.Operation { 203 case "stdin": 204 return copy(p, msg.Data), nil 205 case "resize": 206 t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows} 207 return 0, nil 208 default: 209 return copy(p, EndOfTransmission), fmt.Errorf("unknown message type %s", msg.Operation) 210 } 211 } 212 213 // Ping called periodically to ensure connection stays alive through load balancers 214 func (t *terminalSession) Ping() error { 215 t.writeLock.Lock() 216 err := t.wsConn.WriteMessage(websocket.PingMessage, []byte("ping")) 217 t.writeLock.Unlock() 218 if err != nil { 219 log.Errorf("ping message err: %v", err) 220 } 221 return err 222 } 223 224 // Write called from remote command whenever there is any output 225 func (t *terminalSession) Write(p []byte) (int, error) { 226 msg, err := json.Marshal(TerminalMessage{ 227 Operation: "stdout", 228 Data: string(p), 229 }) 230 if err != nil { 231 log.Errorf("write parse message err: %v", err) 232 return 0, err 233 } 234 t.writeLock.Lock() 235 err = t.wsConn.WriteMessage(websocket.TextMessage, msg) 236 t.writeLock.Unlock() 237 if err != nil { 238 log.Errorf("write message err: %v", err) 239 return 0, err 240 } 241 return len(p), nil 242 } 243 244 // Close closes websocket connection 245 func (t *terminalSession) Close() error { 246 return t.wsConn.Close() 247 }