github.com/minio/console@v1.4.1/web-app/src/websockets/objectBrowserWSMiddleware.ts (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2023 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  import get from "lodash/get";
    18  import { Middleware } from "@reduxjs/toolkit";
    19  import { AppState } from "../store";
    20  import { wsProtocol } from "../utils/wsUtils";
    21  import {
    22    errorInConnection,
    23    newMessage,
    24    resetMessages,
    25    setRecords,
    26    setReloadObjectsList,
    27    setRequestInProgress,
    28    setSearchObjects,
    29    setSelectedBucket,
    30    setSelectedObjects,
    31    setSimplePathHandler,
    32  } from "../screens/Console/ObjectBrowser/objectBrowserSlice";
    33  import {
    34    WebsocketRequest,
    35    WebsocketResponse,
    36  } from "../screens/Console/Buckets/ListBuckets/Objects/ListObjects/types";
    37  import { decodeURLString, encodeURLString } from "../common/utils";
    38  import { permissionItems } from "../screens/Console/Buckets/ListBuckets/Objects/utils";
    39  import { setErrorSnackMessage } from "../systemSlice";
    40  
    41  let wsInFlight: boolean = false;
    42  let currentRequestID: number = 0;
    43  
    44  export const objectBrowserWSMiddleware = (
    45    objectsWS: WebSocket,
    46  ): Middleware<{}, AppState> => {
    47    return (storeApi) => (next) => (action) => {
    48      const dispatch = storeApi.dispatch;
    49      const storeState = storeApi.getState();
    50  
    51      const allowResources = get(
    52        storeState,
    53        "console.session.allowResources",
    54        null,
    55      );
    56      const bucketName = get(storeState, "objectBrowser.selectedBucket", "");
    57  
    58      const { type } = action;
    59      switch (type) {
    60        case "socket/OBConnect":
    61          const sessionInitialized = get(storeState, "system.loggedIn", false);
    62  
    63          if (wsInFlight || !sessionInitialized) {
    64            return;
    65          }
    66  
    67          wsInFlight = true;
    68  
    69          const url = new URL(window.location.toString());
    70          const isDev = process.env.NODE_ENV === "development";
    71          const port = isDev ? "9090" : url.port;
    72  
    73          // check if we are using base path, if not this always is `/`
    74          const baseLocation = new URL(document.baseURI);
    75          const baseUrl = baseLocation.pathname;
    76  
    77          const wsProt = wsProtocol(url.protocol);
    78  
    79          objectsWS = new WebSocket(
    80            `${wsProt}://${url.hostname}:${port}${baseUrl}ws/objectManager`,
    81          );
    82  
    83          objectsWS.onopen = () => {
    84            wsInFlight = false;
    85          };
    86  
    87          objectsWS.onmessage = (message) => {
    88            const basicErrorMessage = {
    89              errorMessage: "An error occurred",
    90              detailedMessage:
    91                "An unknown error occurred. Please refer to Console logs to get more information.",
    92            };
    93  
    94            const response: WebsocketResponse = JSON.parse(
    95              message.data.toString(),
    96            );
    97            if (currentRequestID === response.request_id) {
    98              // If response is not from current request, we can omit
    99              if (response.request_id !== currentRequestID) {
   100                return;
   101              }
   102  
   103              if (response.error?.Code === 401) {
   104                // Session expired. We reload this page
   105                window.location.reload();
   106              } else if (response.error?.Code === 403) {
   107                const internalPathsPrefix = response.prefix;
   108                let pathPrefix = "";
   109  
   110                if (internalPathsPrefix) {
   111                  const decodedPath = decodeURLString(internalPathsPrefix);
   112  
   113                  pathPrefix = decodedPath.endsWith("/")
   114                    ? decodedPath
   115                    : decodedPath + "/";
   116                }
   117  
   118                const permitItems = permissionItems(
   119                  response.bucketName || bucketName,
   120                  pathPrefix,
   121                  allowResources || [],
   122                );
   123  
   124                if (!permitItems || permitItems.length === 0) {
   125                  const errorMsg = response.error.APIError;
   126  
   127                  dispatch(
   128                    setErrorSnackMessage({
   129                      errorMessage:
   130                        errorMsg.message || basicErrorMessage.errorMessage,
   131                      detailedError:
   132                        errorMsg.detailedMessage ||
   133                        basicErrorMessage.detailedMessage,
   134                    }),
   135                  );
   136                } else {
   137                  dispatch(setRequestInProgress(false));
   138                  dispatch(setRecords(permitItems));
   139                }
   140  
   141                return;
   142              } else if (response.error) {
   143                const errorMsg = response.error.APIError;
   144  
   145                dispatch(setRequestInProgress(false));
   146                dispatch(
   147                  setErrorSnackMessage({
   148                    errorMessage:
   149                      errorMsg.message || basicErrorMessage.errorMessage,
   150                    detailedError:
   151                      errorMsg.detailedMessage ||
   152                      basicErrorMessage.detailedMessage,
   153                  }),
   154                );
   155              }
   156  
   157              // This indicates final messages is received.
   158              if (response.request_end) {
   159                dispatch(setRequestInProgress(false));
   160                return;
   161              }
   162  
   163              if (response.data) {
   164                dispatch(setRequestInProgress(false));
   165                dispatch(newMessage(response.data));
   166              }
   167            }
   168          };
   169  
   170          objectsWS.onclose = () => {
   171            wsInFlight = false;
   172            console.warn("Websocket Disconnected. Attempting Reconnection...");
   173  
   174            // We reconnect after 3 seconds
   175            setTimeout(() => dispatch({ type: "socket/OBConnect" }), 3000);
   176          };
   177  
   178          objectsWS.onerror = () => {
   179            wsInFlight = false;
   180            console.error(
   181              "Error in websocket connection. Attempting reconnection...",
   182            );
   183            // Onclose will be triggered by specification, reconnect function will be executed there to avoid duplicated requests
   184          };
   185  
   186          break;
   187  
   188        case "socket/OBRequest":
   189          if (objectsWS && objectsWS.readyState === 1) {
   190            try {
   191              const newRequestID = currentRequestID + 1;
   192              const dataPayload = action.payload;
   193  
   194              dispatch(resetMessages());
   195              dispatch(errorInConnection(false));
   196              dispatch(setSimplePathHandler(dataPayload.path));
   197              dispatch(setSelectedBucket(dataPayload.bucketName));
   198              dispatch(setRequestInProgress(true));
   199              dispatch(setReloadObjectsList(false));
   200              dispatch(setSearchObjects(""));
   201              dispatch(setSelectedObjects([]));
   202  
   203              const request: WebsocketRequest = {
   204                bucket_name: dataPayload.bucketName,
   205                prefix: encodeURLString(dataPayload.path),
   206                mode: dataPayload.rewindMode ? "rewind" : "objects",
   207                date: dataPayload.date,
   208                request_id: newRequestID,
   209              };
   210  
   211              objectsWS.send(JSON.stringify(request));
   212  
   213              // We store the new ID for the requestID
   214              currentRequestID = newRequestID;
   215            } catch (e) {
   216              console.error(e);
   217            }
   218          } else {
   219            dispatch(setReloadObjectsList(false));
   220  
   221            if (!wsInFlight) {
   222              dispatch({ type: "socket/OBConnect" });
   223            }
   224            // Retry request after 1 second
   225            setTimeout(
   226              () =>
   227                dispatch({ type: "socket/OBRequest", payload: action.payload }),
   228              1000,
   229            );
   230          }
   231  
   232          break;
   233        case "socket/OBCancelLast":
   234          const request: WebsocketRequest = {
   235            mode: "cancel",
   236            request_id: currentRequestID,
   237          };
   238  
   239          if (objectsWS && objectsWS.readyState === 1) {
   240            objectsWS.send(JSON.stringify(request));
   241          }
   242          break;
   243        case "socket/OBDisconnect":
   244          objectsWS.close();
   245          break;
   246  
   247        default:
   248          break;
   249      }
   250      return next(action);
   251    };
   252  };