github.com/secure-build/gitlab-runner@v12.5.0+incompatible/session/session.go (about)

     1  package session
     2  
     3  import (
     4  	"net/http"
     5  	"reflect"
     6  	"sync"
     7  
     8  	"github.com/gorilla/mux"
     9  	"github.com/gorilla/websocket"
    10  	"github.com/sirupsen/logrus"
    11  
    12  	"gitlab.com/gitlab-org/gitlab-runner/helpers"
    13  	"gitlab.com/gitlab-org/gitlab-runner/session/proxy"
    14  	"gitlab.com/gitlab-org/gitlab-runner/session/terminal"
    15  )
    16  
    17  type connectionInUseError struct{}
    18  
    19  func (connectionInUseError) Error() string {
    20  	return "Connection already in use"
    21  }
    22  
    23  type Session struct {
    24  	Endpoint string
    25  	Token    string
    26  
    27  	mux *mux.Router
    28  
    29  	interactiveTerminal terminal.InteractiveTerminal
    30  	terminalConn        terminal.Conn
    31  
    32  	proxyPool proxy.Pool
    33  
    34  	// Signal when client disconnects from terminal.
    35  	DisconnectCh chan error
    36  	// Signal when terminal session timeout.
    37  	TimeoutCh chan error
    38  
    39  	log *logrus.Entry
    40  	sync.Mutex
    41  }
    42  
    43  func NewSession(logger *logrus.Entry) (*Session, error) {
    44  	endpoint, token, err := generateEndpoint()
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	if logger == nil {
    50  		logger = logrus.NewEntry(logrus.StandardLogger())
    51  	}
    52  
    53  	logger = logger.WithField("uri", endpoint)
    54  
    55  	sess := &Session{
    56  		Endpoint:     endpoint,
    57  		Token:        token,
    58  		DisconnectCh: make(chan error),
    59  		TimeoutCh:    make(chan error),
    60  
    61  		log: logger,
    62  	}
    63  
    64  	sess.setMux()
    65  
    66  	return sess, nil
    67  }
    68  
    69  func generateEndpoint() (string, string, error) {
    70  	sessionUUID, err := helpers.GenerateRandomUUID(32)
    71  	if err != nil {
    72  		return "", "", err
    73  	}
    74  
    75  	token, err := generateToken()
    76  	if err != nil {
    77  		return "", "", err
    78  	}
    79  
    80  	return "/session/" + sessionUUID, token, nil
    81  }
    82  
    83  func generateToken() (string, error) {
    84  	token, err := helpers.GenerateRandomUUID(32)
    85  	if err != nil {
    86  		return "", err
    87  	}
    88  
    89  	return token, nil
    90  }
    91  
    92  func (s *Session) withAuthorization(next http.Handler) http.Handler {
    93  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    94  		logger := s.log.WithField("uri", r.RequestURI)
    95  		logger.Debug("Endpoint session request")
    96  
    97  		if s.Token != r.Header.Get("Authorization") {
    98  			logger.Error("Authorization header is not valid")
    99  			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
   100  			return
   101  		}
   102  
   103  		next.ServeHTTP(w, r)
   104  	})
   105  }
   106  
   107  func (s *Session) setMux() {
   108  	s.Lock()
   109  	defer s.Unlock()
   110  
   111  	s.mux = mux.NewRouter()
   112  	s.mux.Handle(s.Endpoint+"/proxy/{resource}/{port}/{requestedUri:.*}", s.withAuthorization(http.HandlerFunc(s.proxyHandler)))
   113  	s.mux.Handle(s.Endpoint+"/exec", s.withAuthorization(http.HandlerFunc(s.execHandler)))
   114  }
   115  
   116  func (s *Session) proxyHandler(w http.ResponseWriter, r *http.Request) {
   117  	logger := s.log.WithField("uri", r.RequestURI)
   118  	logger.Debug("Proxy session request")
   119  
   120  	params := mux.Vars(r)
   121  	serviceName := params["resource"]
   122  
   123  	serviceProxy := s.proxyPool[serviceName]
   124  	if serviceProxy == nil {
   125  		logger.Warn("Proxy not found")
   126  		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   127  		return
   128  	}
   129  
   130  	if serviceProxy.ConnectionHandler == nil {
   131  		logger.Warn("Proxy connection handler is not defined")
   132  		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
   133  		return
   134  	}
   135  
   136  	serviceProxy.ConnectionHandler.ProxyRequest(w, r, params["requestedUri"], params["port"], serviceProxy.Settings)
   137  }
   138  
   139  func (s *Session) execHandler(w http.ResponseWriter, r *http.Request) {
   140  	logger := s.log.WithField("uri", r.RequestURI)
   141  	logger.Debug("Exec terminal session request")
   142  
   143  	if !s.terminalAvailable() {
   144  		logger.Error("Interactive terminal not set")
   145  		http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
   146  		return
   147  	}
   148  
   149  	if !websocket.IsWebSocketUpgrade(r) {
   150  		logger.Error("Request is not a web socket connection")
   151  		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
   152  		return
   153  	}
   154  
   155  	terminalConn, err := s.newTerminalConn()
   156  	if _, ok := err.(connectionInUseError); ok {
   157  		logger.Warn("Terminal already connected, revoking connection")
   158  		http.Error(w, http.StatusText(http.StatusLocked), http.StatusLocked)
   159  		return
   160  	}
   161  
   162  	if err != nil {
   163  		logger.WithError(err).Error("Failed to connect to terminal")
   164  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   165  		return
   166  	}
   167  
   168  	defer s.closeTerminalConn(terminalConn)
   169  	logger.Debugln("Starting terminal session")
   170  	terminalConn.Start(w, r, s.TimeoutCh, s.DisconnectCh)
   171  }
   172  
   173  func (s *Session) terminalAvailable() bool {
   174  	s.Lock()
   175  	defer s.Unlock()
   176  
   177  	return s.interactiveTerminal != nil
   178  }
   179  
   180  func (s *Session) newTerminalConn() (terminal.Conn, error) {
   181  	s.Lock()
   182  	defer s.Unlock()
   183  
   184  	if s.terminalConn != nil {
   185  		return nil, connectionInUseError{}
   186  	}
   187  
   188  	conn, err := s.interactiveTerminal.Connect()
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	s.terminalConn = conn
   194  
   195  	return conn, nil
   196  }
   197  
   198  func (s *Session) closeTerminalConn(conn terminal.Conn) {
   199  	s.Lock()
   200  	defer s.Unlock()
   201  
   202  	err := conn.Close()
   203  	if err != nil {
   204  		s.log.WithError(err).Warn("Failed to close terminal connection")
   205  	}
   206  
   207  	if reflect.ValueOf(s.terminalConn) == reflect.ValueOf(conn) {
   208  		s.log.Warningln("Closed active terminal connection")
   209  		s.terminalConn = nil
   210  	}
   211  }
   212  
   213  func (s *Session) SetInteractiveTerminal(interactiveTerminal terminal.InteractiveTerminal) {
   214  	s.Lock()
   215  	defer s.Unlock()
   216  	s.interactiveTerminal = interactiveTerminal
   217  }
   218  
   219  func (s *Session) SetProxyPool(pooler proxy.Pooler) {
   220  	s.Lock()
   221  	defer s.Unlock()
   222  	s.proxyPool = pooler.Pool()
   223  }
   224  
   225  func (s *Session) Mux() *mux.Router {
   226  	return s.mux
   227  }
   228  
   229  func (s *Session) Connected() bool {
   230  	s.Lock()
   231  	defer s.Unlock()
   232  
   233  	return s.terminalConn != nil
   234  }
   235  
   236  func (s *Session) Kill() error {
   237  	s.Lock()
   238  	defer s.Unlock()
   239  
   240  	if s.terminalConn == nil {
   241  		return nil
   242  	}
   243  
   244  	err := s.terminalConn.Close()
   245  	s.terminalConn = nil
   246  
   247  	return err
   248  }