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  }