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 }