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