xref: /openbmc/bmcweb/include/ibm/management_console_rest.hpp (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
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