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 };