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