1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "app.hpp" 6 #include "async_resp.hpp" 7 #include "error_messages.hpp" 8 #include "event_service_manager.hpp" 9 #include "ibm/utils.hpp" 10 #include "resource_messages.hpp" 11 #include "str_utility.hpp" 12 #include "utils/json_utils.hpp" 13 14 #include <boost/container/flat_set.hpp> 15 #include <nlohmann/json.hpp> 16 #include <sdbusplus/message/types.hpp> 17 18 #include <filesystem> 19 #include <fstream> 20 21 namespace crow 22 { 23 namespace ibm_mc 24 { 25 constexpr const char* methodNotAllowedMsg = "Method Not Allowed"; 26 constexpr const char* resourceNotFoundMsg = "Resource Not Found"; 27 constexpr const char* contentNotAcceptableMsg = "Content Not Acceptable"; 28 constexpr const char* internalServerError = "Internal Server Error"; 29 30 constexpr size_t maxSaveareaDirSize = 31 25000000; // Allow save area dir size to be max 25MB 32 constexpr size_t minSaveareaFileSize = 33 100; // Allow save area file size of minimum 100B 34 constexpr size_t maxSaveareaFileSize = 35 500000; // Allow save area file size upto 500KB 36 constexpr size_t maxBroadcastMsgSize = 37 1000; // Allow Broadcast message size upto 1KB 38 39 inline void handleFilePut(const crow::Request& req, 40 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 41 const std::string& fileID) 42 { 43 std::error_code ec; 44 // Check the content-type of the request 45 boost::beast::string_view contentType = req.getHeaderValue("content-type"); 46 if (!bmcweb::asciiIEquals(contentType, "application/octet-stream")) 47 { 48 asyncResp->res.result(boost::beast::http::status::not_acceptable); 49 asyncResp->res.jsonValue["Description"] = contentNotAcceptableMsg; 50 return; 51 } 52 BMCWEB_LOG_DEBUG( 53 "File upload in application/octet-stream format. Continue.."); 54 55 BMCWEB_LOG_DEBUG( 56 "handleIbmPut: Request to create/update the save-area file"); 57 std::string_view path = 58 "/var/lib/bmcweb/ibm-management-console/configfiles"; 59 if (!crow::ibm_utils::createDirectory(path)) 60 { 61 asyncResp->res.result(boost::beast::http::status::not_found); 62 asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg; 63 return; 64 } 65 66 std::ofstream file; 67 std::filesystem::path loc( 68 "/var/lib/bmcweb/ibm-management-console/configfiles"); 69 70 // Get the current size of the savearea directory 71 std::filesystem::recursive_directory_iterator iter(loc, ec); 72 if (ec) 73 { 74 asyncResp->res.result( 75 boost::beast::http::status::internal_server_error); 76 asyncResp->res.jsonValue["Description"] = internalServerError; 77 BMCWEB_LOG_DEBUG("handleIbmPut: Failed to prepare save-area " 78 "directory iterator. ec : {}", 79 ec.message()); 80 return; 81 } 82 std::uintmax_t saveAreaDirSize = 0; 83 for (const auto& it : iter) 84 { 85 if (!std::filesystem::is_directory(it, ec)) 86 { 87 if (ec) 88 { 89 asyncResp->res.result( 90 boost::beast::http::status::internal_server_error); 91 asyncResp->res.jsonValue["Description"] = internalServerError; 92 BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find save-area " 93 "directory . ec : {}", 94 ec.message()); 95 return; 96 } 97 std::uintmax_t fileSize = std::filesystem::file_size(it, ec); 98 if (ec) 99 { 100 asyncResp->res.result( 101 boost::beast::http::status::internal_server_error); 102 asyncResp->res.jsonValue["Description"] = internalServerError; 103 BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find save-area " 104 "file size inside the directory . ec : {}", 105 ec.message()); 106 return; 107 } 108 saveAreaDirSize += fileSize; 109 } 110 } 111 BMCWEB_LOG_DEBUG("saveAreaDirSize: {}", saveAreaDirSize); 112 113 // Get the file size getting uploaded 114 const std::string& data = req.body(); 115 BMCWEB_LOG_DEBUG("data length: {}", data.length()); 116 117 if (data.length() < minSaveareaFileSize) 118 { 119 asyncResp->res.result(boost::beast::http::status::bad_request); 120 asyncResp->res.jsonValue["Description"] = 121 "File size is less than minimum allowed size[100B]"; 122 return; 123 } 124 if (data.length() > maxSaveareaFileSize) 125 { 126 asyncResp->res.result(boost::beast::http::status::bad_request); 127 asyncResp->res.jsonValue["Description"] = 128 "File size exceeds maximum allowed size[500KB]"; 129 return; 130 } 131 132 // Form the file path 133 loc /= fileID; 134 BMCWEB_LOG_DEBUG("Writing to the file: {}", loc.string()); 135 136 // Check if the same file exists in the directory 137 bool fileExists = std::filesystem::exists(loc, ec); 138 if (ec) 139 { 140 asyncResp->res.result( 141 boost::beast::http::status::internal_server_error); 142 asyncResp->res.jsonValue["Description"] = internalServerError; 143 BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find if file exists. ec : {}", 144 ec.message()); 145 return; 146 } 147 148 std::uintmax_t newSizeToWrite = 0; 149 if (fileExists) 150 { 151 // File exists. Get the current file size 152 std::uintmax_t currentFileSize = std::filesystem::file_size(loc, ec); 153 if (ec) 154 { 155 asyncResp->res.result( 156 boost::beast::http::status::internal_server_error); 157 asyncResp->res.jsonValue["Description"] = internalServerError; 158 BMCWEB_LOG_DEBUG("handleIbmPut: Failed to find file size. ec : {}", 159 ec.message()); 160 return; 161 } 162 // Calculate the difference in the file size. 163 // If the data.length is greater than the existing file size, then 164 // calculate the difference. Else consider the delta size as zero - 165 // because there is no increase in the total directory size. 166 // We need to add the diff only if the incoming data is larger than the 167 // existing filesize 168 if (data.length() > currentFileSize) 169 { 170 newSizeToWrite = data.length() - currentFileSize; 171 } 172 BMCWEB_LOG_DEBUG("newSizeToWrite: {}", newSizeToWrite); 173 } 174 else 175 { 176 // This is a new file upload 177 newSizeToWrite = data.length(); 178 } 179 180 // Calculate the total dir size before writing the new file 181 BMCWEB_LOG_DEBUG("total new size: {}", saveAreaDirSize + newSizeToWrite); 182 183 if ((saveAreaDirSize + newSizeToWrite) > maxSaveareaDirSize) 184 { 185 asyncResp->res.result(boost::beast::http::status::bad_request); 186 asyncResp->res.jsonValue["Description"] = 187 "File size does not fit in the savearea " 188 "directory maximum allowed size[25MB]"; 189 return; 190 } 191 192 file.open(loc, std::ofstream::out); 193 194 // set the permission of the file to 600 195 std::filesystem::perms permission = std::filesystem::perms::owner_write | 196 std::filesystem::perms::owner_read; 197 std::filesystem::permissions(loc, permission); 198 199 if (file.fail()) 200 { 201 BMCWEB_LOG_DEBUG("Error while opening the file for writing"); 202 asyncResp->res.result( 203 boost::beast::http::status::internal_server_error); 204 asyncResp->res.jsonValue["Description"] = 205 "Error while creating the file"; 206 return; 207 } 208 file << data; 209 210 // Push an event 211 if (fileExists) 212 { 213 BMCWEB_LOG_DEBUG("config file is updated"); 214 asyncResp->res.jsonValue["Description"] = "File Updated"; 215 } 216 else 217 { 218 BMCWEB_LOG_DEBUG("config file is created"); 219 asyncResp->res.jsonValue["Description"] = "File Created"; 220 } 221 } 222 223 inline void 224 handleConfigFileList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 225 { 226 std::vector<std::string> pathObjList; 227 std::filesystem::path loc( 228 "/var/lib/bmcweb/ibm-management-console/configfiles"); 229 if (std::filesystem::exists(loc) && std::filesystem::is_directory(loc)) 230 { 231 for (const auto& file : std::filesystem::directory_iterator(loc)) 232 { 233 const std::filesystem::path& pathObj = file.path(); 234 if (std::filesystem::is_regular_file(pathObj)) 235 { 236 pathObjList.emplace_back( 237 "/ibm/v1/Host/ConfigFiles/" + pathObj.filename().string()); 238 } 239 } 240 } 241 asyncResp->res.jsonValue["@odata.type"] = 242 "#IBMConfigFile.v1_0_0.IBMConfigFile"; 243 asyncResp->res.jsonValue["@odata.id"] = "/ibm/v1/Host/ConfigFiles/"; 244 asyncResp->res.jsonValue["Id"] = "ConfigFiles"; 245 asyncResp->res.jsonValue["Name"] = "ConfigFiles"; 246 247 asyncResp->res.jsonValue["Members"] = std::move(pathObjList); 248 asyncResp->res.jsonValue["Actions"]["#IBMConfigFiles.DeleteAll"]["target"] = 249 "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll"; 250 } 251 252 inline void 253 deleteConfigFiles(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 254 { 255 std::error_code ec; 256 std::filesystem::path loc( 257 "/var/lib/bmcweb/ibm-management-console/configfiles"); 258 if (std::filesystem::exists(loc) && std::filesystem::is_directory(loc)) 259 { 260 std::filesystem::remove_all(loc, ec); 261 if (ec) 262 { 263 asyncResp->res.result( 264 boost::beast::http::status::internal_server_error); 265 asyncResp->res.jsonValue["Description"] = internalServerError; 266 BMCWEB_LOG_DEBUG("deleteConfigFiles: Failed to delete the " 267 "config files directory. ec : {}", 268 ec.message()); 269 } 270 } 271 } 272 273 inline void handleFileGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 274 const std::string& fileID) 275 { 276 BMCWEB_LOG_DEBUG("HandleGet on SaveArea files on path: {}", fileID); 277 std::filesystem::path loc( 278 "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID); 279 if (!std::filesystem::exists(loc) || !std::filesystem::is_regular_file(loc)) 280 { 281 BMCWEB_LOG_WARNING("{} Not found", loc.string()); 282 asyncResp->res.result(boost::beast::http::status::not_found); 283 asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg; 284 return; 285 } 286 287 std::ifstream readfile(loc.string()); 288 if (!readfile) 289 { 290 BMCWEB_LOG_WARNING("{} Not found", loc.string()); 291 asyncResp->res.result(boost::beast::http::status::not_found); 292 asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg; 293 return; 294 } 295 296 std::string contentDispositionParam = 297 "attachment; filename=\"" + fileID + "\""; 298 asyncResp->res.addHeader(boost::beast::http::field::content_disposition, 299 contentDispositionParam); 300 std::string fileData; 301 fileData = {std::istreambuf_iterator<char>(readfile), 302 std::istreambuf_iterator<char>()}; 303 asyncResp->res.jsonValue["Data"] = fileData; 304 } 305 306 inline void 307 handleFileDelete(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 308 const std::string& fileID) 309 { 310 std::string filePath( 311 "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID); 312 BMCWEB_LOG_DEBUG("Removing the file : {}", filePath); 313 std::ifstream fileOpen(filePath.c_str()); 314 if (static_cast<bool>(fileOpen)) 315 { 316 if (remove(filePath.c_str()) == 0) 317 { 318 BMCWEB_LOG_DEBUG("File removed!"); 319 asyncResp->res.jsonValue["Description"] = "File Deleted"; 320 } 321 else 322 { 323 BMCWEB_LOG_ERROR("File not removed!"); 324 asyncResp->res.result( 325 boost::beast::http::status::internal_server_error); 326 asyncResp->res.jsonValue["Description"] = internalServerError; 327 } 328 } 329 else 330 { 331 BMCWEB_LOG_WARNING("File not found!"); 332 asyncResp->res.result(boost::beast::http::status::not_found); 333 asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg; 334 } 335 } 336 337 inline void 338 handleBroadcastService(const crow::Request& req, 339 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 340 { 341 std::string broadcastMsg; 342 343 if (!redfish::json_util::readJsonPatch(req, asyncResp->res, "Message", 344 broadcastMsg)) 345 { 346 BMCWEB_LOG_DEBUG("Not a Valid JSON"); 347 asyncResp->res.result(boost::beast::http::status::bad_request); 348 return; 349 } 350 if (broadcastMsg.size() > maxBroadcastMsgSize) 351 { 352 BMCWEB_LOG_ERROR("Message size exceeds maximum allowed size[1KB]"); 353 asyncResp->res.result(boost::beast::http::status::bad_request); 354 return; 355 } 356 } 357 358 inline void handleFileUrl(const crow::Request& req, 359 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 360 const std::string& fileID) 361 { 362 if (req.method() == boost::beast::http::verb::put) 363 { 364 handleFilePut(req, asyncResp, fileID); 365 return; 366 } 367 if (req.method() == boost::beast::http::verb::get) 368 { 369 handleFileGet(asyncResp, fileID); 370 return; 371 } 372 if (req.method() == boost::beast::http::verb::delete_) 373 { 374 handleFileDelete(asyncResp, fileID); 375 return; 376 } 377 } 378 379 inline bool isValidConfigFileName(const std::string& fileName, 380 crow::Response& res) 381 { 382 if (fileName.empty()) 383 { 384 BMCWEB_LOG_ERROR("Empty filename"); 385 res.jsonValue["Description"] = "Empty file path in the url"; 386 return false; 387 } 388 389 // ConfigFile name is allowed to take upper and lowercase letters, 390 // numbers and hyphen 391 std::size_t found = fileName.find_first_not_of( 392 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"); 393 if (found != std::string::npos) 394 { 395 BMCWEB_LOG_ERROR("Unsupported character in filename: {}", fileName); 396 res.jsonValue["Description"] = "Unsupported character in filename"; 397 return false; 398 } 399 400 // Check the filename length 401 if (fileName.length() > 20) 402 { 403 BMCWEB_LOG_ERROR("Name must be maximum 20 characters. " 404 "Input filename length is: {}", 405 fileName.length()); 406 res.jsonValue["Description"] = "Filename must be maximum 20 characters"; 407 return false; 408 } 409 410 return true; 411 } 412 413 inline void requestRoutes(App& app) 414 { 415 // allowed only for admin 416 BMCWEB_ROUTE(app, "/ibm/v1/") 417 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 418 .methods(boost::beast::http::verb::get)( 419 [](const crow::Request&, 420 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 421 asyncResp->res.jsonValue["@odata.type"] = 422 "#ibmServiceRoot.v1_0_0.ibmServiceRoot"; 423 asyncResp->res.jsonValue["@odata.id"] = "/ibm/v1/"; 424 asyncResp->res.jsonValue["Id"] = "IBM Rest RootService"; 425 asyncResp->res.jsonValue["Name"] = "IBM Service Root"; 426 asyncResp->res.jsonValue["ConfigFiles"]["@odata.id"] = 427 "/ibm/v1/Host/ConfigFiles"; 428 asyncResp->res.jsonValue["BroadcastService"]["@odata.id"] = 429 "/ibm/v1/HMC/BroadcastService"; 430 }); 431 432 BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles") 433 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 434 .methods(boost::beast::http::verb::get)( 435 [](const crow::Request&, 436 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 437 handleConfigFileList(asyncResp); 438 }); 439 440 BMCWEB_ROUTE(app, 441 "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll") 442 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 443 .methods(boost::beast::http::verb::post)( 444 [](const crow::Request&, 445 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 446 deleteConfigFiles(asyncResp); 447 }); 448 449 BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles/<str>") 450 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 451 .methods(boost::beast::http::verb::put, boost::beast::http::verb::get, 452 boost::beast::http::verb::delete_)( 453 [](const crow::Request& req, 454 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 455 const std::string& fileName) { 456 BMCWEB_LOG_DEBUG("ConfigFile : {}", fileName); 457 // Validate the incoming fileName 458 if (!isValidConfigFileName(fileName, asyncResp->res)) 459 { 460 asyncResp->res.result( 461 boost::beast::http::status::bad_request); 462 return; 463 } 464 handleFileUrl(req, asyncResp, fileName); 465 }); 466 467 BMCWEB_ROUTE(app, "/ibm/v1/HMC/BroadcastService") 468 .privileges({{"ConfigureComponents", "ConfigureManager"}}) 469 .methods(boost::beast::http::verb::post)( 470 [](const crow::Request& req, 471 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 472 handleBroadcastService(req, asyncResp); 473 }); 474 } 475 476 } // namespace ibm_mc 477 } // namespace crow 478