github.com/minio/console@v1.4.1/api/ws_handle.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"log"
    24  	"net"
    25  	"net/http"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/minio/madmin-go/v3"
    31  
    32  	"github.com/minio/console/pkg/utils"
    33  
    34  	errorsApi "github.com/go-openapi/errors"
    35  	"github.com/minio/console/models"
    36  	"github.com/minio/console/pkg/auth"
    37  	"github.com/minio/websocket"
    38  )
    39  
    40  var upgrader = websocket.Upgrader{
    41  	ReadBufferSize:  0,
    42  	WriteBufferSize: 1024,
    43  }
    44  
    45  const (
    46  	// websocket base path
    47  	wsBasePath = "/ws"
    48  )
    49  
    50  // ConsoleWebsocketAdmin interface of a Websocket Client
    51  type ConsoleWebsocketAdmin interface {
    52  	trace()
    53  	console()
    54  }
    55  
    56  type wsAdminClient struct {
    57  	// websocket connection.
    58  	conn wsConn
    59  	// MinIO admin Client
    60  	client MinioAdmin
    61  }
    62  
    63  // ConsoleWebsocket interface of a Websocket Client
    64  type ConsoleWebsocket interface {
    65  	watch(options watchOptions)
    66  }
    67  
    68  type wsS3Client struct {
    69  	// websocket connection.
    70  	conn wsConn
    71  	// mcClient
    72  	client MCClient
    73  }
    74  
    75  // ConsoleWebSocketMClient interface of a Websocket Client
    76  type ConsoleWebsocketMClient interface {
    77  	objectManager(options objectsListOpts)
    78  }
    79  
    80  type wsMinioClient struct {
    81  	// websocket connection.
    82  	conn wsConn
    83  	// MinIO admin Client
    84  	client minioClient
    85  }
    86  
    87  // WSConn interface with all functions to be implemented
    88  // by mock when testing, it should include all websocket.Conn
    89  // respective api calls that are used within this project.
    90  type WSConn interface {
    91  	writeMessage(messageType int, data []byte) error
    92  	close() error
    93  	readMessage() (messageType int, p []byte, err error)
    94  	remoteAddress() string
    95  }
    96  
    97  // Interface implementation
    98  //
    99  // Define the structure of a websocket Connection
   100  type wsConn struct {
   101  	conn *websocket.Conn
   102  }
   103  
   104  // Types for trace request. this adds support for calls, threshold, status and extra filters
   105  type TraceRequest struct {
   106  	s3         bool
   107  	internal   bool
   108  	storage    bool
   109  	os         bool
   110  	threshold  int64
   111  	onlyErrors bool
   112  	statusCode int64
   113  	method     string
   114  	funcName   string
   115  	path       string
   116  }
   117  
   118  // Type for log requests. This allows for filtering by node and kind
   119  type LogRequest struct {
   120  	node    string
   121  	logType string
   122  }
   123  
   124  func (c wsConn) writeMessage(messageType int, data []byte) error {
   125  	return c.conn.WriteMessage(messageType, data)
   126  }
   127  
   128  func (c wsConn) close() error {
   129  	return c.conn.Close()
   130  }
   131  
   132  func (c wsConn) readMessage() (messageType int, p []byte, err error) {
   133  	return c.conn.ReadMessage()
   134  }
   135  
   136  func (c wsConn) remoteAddress() string {
   137  	clientIP, _, err := net.SplitHostPort(c.conn.RemoteAddr().String())
   138  	if err != nil {
   139  		// In case there's an error, return an empty string
   140  		log.Printf("Invalid ws.clientIP = %s\n", err)
   141  		return ""
   142  	}
   143  	return clientIP
   144  }
   145  
   146  // serveWS validates the incoming request and
   147  // upgrades the request to a Websocket protocol.
   148  // Websocket communication will be done depending
   149  // on the path.
   150  // Request should come like ws://<host>:<port>/ws/<api>
   151  func serveWS(w http.ResponseWriter, req *http.Request) {
   152  	ctx := req.Context()
   153  	wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
   154  	// Perform authentication before upgrading to a Websocket Connection
   155  	// authenticate WS connection with Console
   156  	session, err := auth.GetClaimsFromTokenInRequest(req)
   157  	if err != nil && (errors.Is(err, auth.ErrReadingToken) && !strings.HasPrefix(wsPath, `/objectManager`)) {
   158  		ErrorWithContext(ctx, err)
   159  		errorsApi.ServeError(w, req, errorsApi.New(http.StatusUnauthorized, err.Error()))
   160  		return
   161  	}
   162  
   163  	// If we are using a subpath we are most likely behind a reverse proxy so we most likely
   164  	// can't validate the proper Origin since we don't know the source domain, so we are going
   165  	// to allow the connection to be upgraded in this case.
   166  	if getSubPath() != "/" || getConsoleDevMode() {
   167  		upgrader.CheckOrigin = func(_ *http.Request) bool {
   168  			return true
   169  		}
   170  	}
   171  
   172  	// upgrades the HTTP server connection to the WebSocket protocol.
   173  	conn, err := upgrader.Upgrade(w, req, nil)
   174  	if err != nil {
   175  		ErrorWithContext(ctx, err)
   176  		errorsApi.ServeError(w, req, err)
   177  		return
   178  	}
   179  
   180  	switch {
   181  	case strings.HasPrefix(wsPath, `/trace`):
   182  		wsAdminClient, err := newWebSocketAdminClient(conn, session)
   183  		if err != nil {
   184  			ErrorWithContext(ctx, err)
   185  			closeWsConn(conn)
   186  			return
   187  		}
   188  
   189  		calls := req.URL.Query().Get("calls")
   190  		threshold, _ := strconv.ParseInt(req.URL.Query().Get("threshold"), 10, 64)
   191  		onlyErrors := req.URL.Query().Get("onlyErrors")
   192  		stCode, errorStCode := strconv.ParseInt(req.URL.Query().Get("statusCode"), 10, 64)
   193  		method := req.URL.Query().Get("method")
   194  		funcName := req.URL.Query().Get("funcname")
   195  		path := req.URL.Query().Get("path")
   196  
   197  		statusCode := int64(0)
   198  
   199  		if errorStCode == nil {
   200  			statusCode = stCode
   201  		}
   202  
   203  		traceRequestItem := TraceRequest{
   204  			s3:         strings.Contains(calls, "s3") || strings.Contains(calls, "all"),
   205  			internal:   strings.Contains(calls, "internal") || strings.Contains(calls, "all"),
   206  			storage:    strings.Contains(calls, "storage") || strings.Contains(calls, "all"),
   207  			os:         strings.Contains(calls, "os") || strings.Contains(calls, "all"),
   208  			onlyErrors: onlyErrors == "yes",
   209  			threshold:  threshold,
   210  			statusCode: statusCode,
   211  			method:     method,
   212  			funcName:   funcName,
   213  			path:       path,
   214  		}
   215  
   216  		go wsAdminClient.trace(ctx, traceRequestItem)
   217  	case strings.HasPrefix(wsPath, `/console`):
   218  
   219  		wsAdminClient, err := newWebSocketAdminClient(conn, session)
   220  		if err != nil {
   221  			ErrorWithContext(ctx, err)
   222  			closeWsConn(conn)
   223  			return
   224  		}
   225  		node := req.URL.Query().Get("node")
   226  		logType := req.URL.Query().Get("logType")
   227  
   228  		logRequestItem := LogRequest{
   229  			node:    node,
   230  			logType: logType,
   231  		}
   232  		go wsAdminClient.console(ctx, logRequestItem)
   233  	case strings.HasPrefix(wsPath, `/health-info`):
   234  		deadline, err := getHealthInfoOptionsFromReq(req)
   235  		if err != nil {
   236  			ErrorWithContext(ctx, fmt.Errorf("error getting health info options: %v", err))
   237  			closeWsConn(conn)
   238  			return
   239  		}
   240  		wsAdminClient, err := newWebSocketAdminClient(conn, session)
   241  		if err != nil {
   242  			ErrorWithContext(ctx, err)
   243  			closeWsConn(conn)
   244  			return
   245  		}
   246  		go wsAdminClient.healthInfo(ctx, deadline)
   247  	case strings.HasPrefix(wsPath, `/watch`):
   248  		wOptions, err := getWatchOptionsFromReq(req)
   249  		if err != nil {
   250  			ErrorWithContext(ctx, fmt.Errorf("error getting watch options: %v", err))
   251  			closeWsConn(conn)
   252  			return
   253  		}
   254  		wsS3Client, err := newWebSocketS3Client(conn, session, wOptions.BucketName, "")
   255  		if err != nil {
   256  			ErrorWithContext(ctx, err)
   257  			closeWsConn(conn)
   258  			return
   259  		}
   260  		go wsS3Client.watch(ctx, wOptions)
   261  	case strings.HasPrefix(wsPath, `/speedtest`):
   262  		speedtestOpts, err := getSpeedtestOptionsFromReq(req)
   263  		if err != nil {
   264  			ErrorWithContext(ctx, fmt.Errorf("error getting speedtest options: %v", err))
   265  			closeWsConn(conn)
   266  			return
   267  		}
   268  		wsAdminClient, err := newWebSocketAdminClient(conn, session)
   269  		if err != nil {
   270  			ErrorWithContext(ctx, err)
   271  			closeWsConn(conn)
   272  			return
   273  		}
   274  		go wsAdminClient.speedtest(ctx, speedtestOpts)
   275  	case strings.HasPrefix(wsPath, `/profile`):
   276  		pOptions, err := getProfileOptionsFromReq(req)
   277  		if err != nil {
   278  			ErrorWithContext(ctx, fmt.Errorf("error getting profile options: %v", err))
   279  			closeWsConn(conn)
   280  			return
   281  		}
   282  		wsAdminClient, err := newWebSocketAdminClient(conn, session)
   283  		if err != nil {
   284  			ErrorWithContext(ctx, err)
   285  			closeWsConn(conn)
   286  			return
   287  		}
   288  		go wsAdminClient.profile(ctx, pOptions)
   289  
   290  	case strings.HasPrefix(wsPath, `/objectManager`):
   291  		wsMinioClient, err := newWebSocketMinioClient(conn, session)
   292  		if err != nil {
   293  			ErrorWithContext(ctx, err)
   294  			closeWsConn(conn)
   295  			return
   296  		}
   297  
   298  		go wsMinioClient.objectManager(session)
   299  	default:
   300  		// path not found
   301  		closeWsConn(conn)
   302  	}
   303  }
   304  
   305  // newWebSocketAdminClient returns a wsAdminClient authenticated as an admin user
   306  func newWebSocketAdminClient(conn *websocket.Conn, autClaims *models.Principal) (*wsAdminClient, error) {
   307  	// create a websocket connection interface implementation
   308  	// defining the connection to be used
   309  	wsConnection := wsConn{conn: conn}
   310  
   311  	clientIP := wsConnection.remoteAddress()
   312  	// Only start Websocket Interaction after user has been
   313  	// authenticated with MinIO
   314  	mAdmin, err := newAdminFromClaims(autClaims, clientIP)
   315  	if err != nil {
   316  		LogError("error creating madmin client: %v", err)
   317  		return nil, err
   318  	}
   319  
   320  	// create a minioClient interface implementation
   321  	// defining the client to be used
   322  	adminClient := AdminClient{Client: mAdmin}
   323  	// create websocket client and handle request
   324  	wsAdminClient := &wsAdminClient{conn: wsConnection, client: adminClient}
   325  	return wsAdminClient, nil
   326  }
   327  
   328  // newWebSocketS3Client returns a wsAdminClient authenticated as Console admin
   329  func newWebSocketS3Client(conn *websocket.Conn, claims *models.Principal, bucketName, prefix string) (*wsS3Client, error) {
   330  	// Only start Websocket Interaction after user has been
   331  	// authenticated with MinIO
   332  	clientIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
   333  	if err != nil {
   334  		// In case there's an error, return an empty string
   335  		log.Printf("Invalid ws.clientIP = %s\n", err)
   336  	}
   337  
   338  	s3Client, err := newS3BucketClient(claims, bucketName, prefix, clientIP)
   339  	if err != nil {
   340  		LogError("error creating S3Client:", err)
   341  		return nil, err
   342  	}
   343  	// create a websocket connection interface implementation
   344  	// defining the connection to be used
   345  	wsConnection := wsConn{conn: conn}
   346  	// create a s3Client interface implementation
   347  	// defining the client to be used
   348  	mcS3C := mcClient{client: s3Client}
   349  	// create websocket client and handle request
   350  	wsS3Client := &wsS3Client{conn: wsConnection, client: mcS3C}
   351  	return wsS3Client, nil
   352  }
   353  
   354  func newWebSocketMinioClient(conn *websocket.Conn, claims *models.Principal) (*wsMinioClient, error) {
   355  	// Only start Websocket Interaction after user has been
   356  	// authenticated with MinIO
   357  	clientIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
   358  	if err != nil {
   359  		// In case there's an error, return an empty string
   360  		log.Printf("Invalid ws.clientIP = %s\n", err)
   361  	}
   362  	mClient, err := newMinioClient(claims, clientIP)
   363  	if err != nil {
   364  		LogError("error creating MinioClient:", err)
   365  		return nil, err
   366  	}
   367  
   368  	// create a websocket connection interface implementation
   369  	// defining the connection to be used
   370  	wsConnection := wsConn{conn: conn}
   371  	// create a minioClient interface implementation
   372  	// defining the client to be used
   373  	minioClient := minioClient{client: mClient}
   374  
   375  	// create websocket client and handle request
   376  	wsMinioClient := &wsMinioClient{conn: wsConnection, client: minioClient}
   377  	return wsMinioClient, nil
   378  }
   379  
   380  // wsReadClientCtx reads the messages that come from the client
   381  // if the client sends a Close Message the context will be
   382  // canceled. If the connection is closed the goroutine inside
   383  // will return.
   384  func wsReadClientCtx(parentContext context.Context, conn WSConn) context.Context {
   385  	// a cancel context is needed to end all goroutines used
   386  	ctx, cancel := context.WithCancel(context.Background())
   387  
   388  	var requestID string
   389  	var SessionID string
   390  	var UserAgent string
   391  	var Host string
   392  	var RemoteHost string
   393  
   394  	if val, o := parentContext.Value(utils.ContextRequestID).(string); o {
   395  		requestID = val
   396  	}
   397  	if val, o := parentContext.Value(utils.ContextRequestUserID).(string); o {
   398  		SessionID = val
   399  	}
   400  	if val, o := parentContext.Value(utils.ContextRequestUserAgent).(string); o {
   401  		UserAgent = val
   402  	}
   403  	if val, o := parentContext.Value(utils.ContextRequestHost).(string); o {
   404  		Host = val
   405  	}
   406  	if val, o := parentContext.Value(utils.ContextRequestRemoteAddr).(string); o {
   407  		RemoteHost = val
   408  	}
   409  
   410  	ctx = context.WithValue(ctx, utils.ContextRequestID, requestID)
   411  	ctx = context.WithValue(ctx, utils.ContextRequestUserID, SessionID)
   412  	ctx = context.WithValue(ctx, utils.ContextRequestUserAgent, UserAgent)
   413  	ctx = context.WithValue(ctx, utils.ContextRequestHost, Host)
   414  	ctx = context.WithValue(ctx, utils.ContextRequestRemoteAddr, RemoteHost)
   415  
   416  	go func() {
   417  		defer cancel()
   418  		for {
   419  			_, _, err := conn.readMessage()
   420  			if err != nil {
   421  				// if errors of type websocket.CloseError and is Unexpected
   422  				if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
   423  					ErrorWithContext(ctx, fmt.Errorf("error unexpected CloseError on ReadMessage: %v", err))
   424  					return
   425  				}
   426  				// Not all errors are of type websocket.CloseError.
   427  				if _, ok := err.(*websocket.CloseError); !ok {
   428  					ErrorWithContext(ctx, fmt.Errorf("error on ReadMessage: %v", err))
   429  					return
   430  				}
   431  				// else is an expected Close Error
   432  				return
   433  			}
   434  		}
   435  	}()
   436  	return ctx
   437  }
   438  
   439  // closeWsConn sends Close Message and closes the websocket connection
   440  func closeWsConn(conn *websocket.Conn) {
   441  	conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
   442  	conn.Close()
   443  }
   444  
   445  // trace serves madmin.ServiceTraceInfo
   446  // on a Websocket connection.
   447  func (wsc *wsAdminClient) trace(ctx context.Context, traceRequestItem TraceRequest) {
   448  	defer func() {
   449  		LogInfo("trace stopped")
   450  		// close connection after return
   451  		wsc.conn.close()
   452  	}()
   453  	LogInfo("trace started")
   454  
   455  	ctx = wsReadClientCtx(ctx, wsc.conn)
   456  
   457  	err := startTraceInfo(ctx, wsc.conn, wsc.client, traceRequestItem)
   458  
   459  	sendWsCloseMessage(wsc.conn, err)
   460  }
   461  
   462  // console serves madmin.GetLogs
   463  // on a Websocket connection.
   464  func (wsc *wsAdminClient) console(ctx context.Context, logRequestItem LogRequest) {
   465  	defer func() {
   466  		LogInfo("console logs stopped")
   467  		// close connection after return
   468  		wsc.conn.close()
   469  	}()
   470  	LogInfo("console logs started")
   471  
   472  	ctx = wsReadClientCtx(ctx, wsc.conn)
   473  
   474  	err := startConsoleLog(ctx, wsc.conn, wsc.client, logRequestItem)
   475  
   476  	sendWsCloseMessage(wsc.conn, err)
   477  }
   478  
   479  func (wsc *wsS3Client) watch(ctx context.Context, params *watchOptions) {
   480  	defer func() {
   481  		LogInfo("watch stopped")
   482  		// close connection after return
   483  		wsc.conn.close()
   484  	}()
   485  	LogInfo("watch started")
   486  
   487  	ctx = wsReadClientCtx(ctx, wsc.conn)
   488  
   489  	err := startWatch(ctx, wsc.conn, wsc.client, params)
   490  
   491  	sendWsCloseMessage(wsc.conn, err)
   492  }
   493  
   494  func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duration) {
   495  	defer func() {
   496  		LogInfo("health info stopped")
   497  		// close connection after return
   498  		wsc.conn.close()
   499  	}()
   500  	LogInfo("health info started")
   501  
   502  	ctx = wsReadClientCtx(ctx, wsc.conn)
   503  	err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline)
   504  
   505  	sendWsCloseMessage(wsc.conn, err)
   506  }
   507  
   508  func (wsc *wsAdminClient) speedtest(ctx context.Context, opts *madmin.SpeedtestOpts) {
   509  	defer func() {
   510  		LogInfo("speedtest stopped")
   511  		// close connection after return
   512  		wsc.conn.close()
   513  	}()
   514  	LogInfo("speedtest started")
   515  
   516  	ctx = wsReadClientCtx(ctx, wsc.conn)
   517  
   518  	err := startSpeedtest(ctx, wsc.conn, wsc.client, opts)
   519  
   520  	sendWsCloseMessage(wsc.conn, err)
   521  }
   522  
   523  func (wsc *wsAdminClient) profile(ctx context.Context, opts *profileOptions) {
   524  	defer func() {
   525  		LogInfo("profile stopped")
   526  		// close connection after return
   527  		wsc.conn.close()
   528  	}()
   529  	LogInfo("profile started")
   530  
   531  	ctx = wsReadClientCtx(ctx, wsc.conn)
   532  
   533  	err := startProfiling(ctx, wsc.conn, wsc.client, opts)
   534  
   535  	sendWsCloseMessage(wsc.conn, err)
   536  }
   537  
   538  // sendWsCloseMessage sends Websocket Connection Close Message indicating the Status Code
   539  // see https://tools.ietf.org/html/rfc6455#page-45
   540  func sendWsCloseMessage(conn WSConn, err error) {
   541  	if err != nil {
   542  		LogError("original ws error: %v", err)
   543  		// If connection exceeded read deadline send Close
   544  		// Message Policy Violation code since we don't want
   545  		// to let the receiver figure out the read deadline.
   546  		// This is a generic code designed if there is a
   547  		// need to hide specific details about the policy.
   548  		if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
   549  			conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
   550  			return
   551  		}
   552  		// else, internal server error
   553  		conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
   554  		return
   555  	}
   556  	// normal closure
   557  	conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
   558  }