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(
235                     "/ibm/v1/Host/ConfigFiles/" + 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"]["target"] =
247         "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll";
248 }
249 
250 inline void
251     deleteConfigFiles(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
252 {
253     std::error_code ec;
254     std::filesystem::path loc(
255         "/var/lib/bmcweb/ibm-management-console/configfiles");
256     if (std::filesystem::exists(loc) && std::filesystem::is_directory(loc))
257     {
258         std::filesystem::remove_all(loc, ec);
259         if (ec)
260         {
261             asyncResp->res.result(
262                 boost::beast::http::status::internal_server_error);
263             asyncResp->res.jsonValue["Description"] = internalServerError;
264             BMCWEB_LOG_DEBUG("deleteConfigFiles: Failed to delete the "
265                              "config files directory. ec : {}",
266                              ec.message());
267         }
268     }
269 }
270 
271 inline void handleFileGet(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
272                           const std::string& fileID)
273 {
274     BMCWEB_LOG_DEBUG("HandleGet on SaveArea files on path: {}", fileID);
275     std::filesystem::path loc(
276         "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID);
277     if (!std::filesystem::exists(loc) || !std::filesystem::is_regular_file(loc))
278     {
279         BMCWEB_LOG_WARNING("{} Not found", loc.string());
280         asyncResp->res.result(boost::beast::http::status::not_found);
281         asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
282         return;
283     }
284 
285     std::ifstream readfile(loc.string());
286     if (!readfile)
287     {
288         BMCWEB_LOG_WARNING("{} Not found", loc.string());
289         asyncResp->res.result(boost::beast::http::status::not_found);
290         asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
291         return;
292     }
293 
294     std::string contentDispositionParam =
295         "attachment; filename=\"" + fileID + "\"";
296     asyncResp->res.addHeader(boost::beast::http::field::content_disposition,
297                              contentDispositionParam);
298     std::string fileData;
299     fileData = {std::istreambuf_iterator<char>(readfile),
300                 std::istreambuf_iterator<char>()};
301     asyncResp->res.jsonValue["Data"] = fileData;
302 }
303 
304 inline void
305     handleFileDelete(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
306                      const std::string& fileID)
307 {
308     std::string filePath(
309         "/var/lib/bmcweb/ibm-management-console/configfiles/" + fileID);
310     BMCWEB_LOG_DEBUG("Removing the file : {}", filePath);
311     std::ifstream fileOpen(filePath.c_str());
312     if (static_cast<bool>(fileOpen))
313     {
314         if (remove(filePath.c_str()) == 0)
315         {
316             BMCWEB_LOG_DEBUG("File removed!");
317             asyncResp->res.jsonValue["Description"] = "File Deleted";
318         }
319         else
320         {
321             BMCWEB_LOG_ERROR("File not removed!");
322             asyncResp->res.result(
323                 boost::beast::http::status::internal_server_error);
324             asyncResp->res.jsonValue["Description"] = internalServerError;
325         }
326     }
327     else
328     {
329         BMCWEB_LOG_WARNING("File not found!");
330         asyncResp->res.result(boost::beast::http::status::not_found);
331         asyncResp->res.jsonValue["Description"] = resourceNotFoundMsg;
332     }
333 }
334 
335 inline void
336     handleBroadcastService(const crow::Request& req,
337                            const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
338 {
339     std::string broadcastMsg;
340 
341     if (!redfish::json_util::readJsonPatch(req, asyncResp->res, "Message",
342                                            broadcastMsg))
343     {
344         BMCWEB_LOG_DEBUG("Not a Valid JSON");
345         asyncResp->res.result(boost::beast::http::status::bad_request);
346         return;
347     }
348     if (broadcastMsg.size() > maxBroadcastMsgSize)
349     {
350         BMCWEB_LOG_ERROR("Message size exceeds maximum allowed size[1KB]");
351         asyncResp->res.result(boost::beast::http::status::bad_request);
352         return;
353     }
354 }
355 
356 inline void handleFileUrl(const crow::Request& req,
357                           const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
358                           const std::string& fileID)
359 {
360     if (req.method() == boost::beast::http::verb::put)
361     {
362         handleFilePut(req, asyncResp, fileID);
363         return;
364     }
365     if (req.method() == boost::beast::http::verb::get)
366     {
367         handleFileGet(asyncResp, fileID);
368         return;
369     }
370     if (req.method() == boost::beast::http::verb::delete_)
371     {
372         handleFileDelete(asyncResp, fileID);
373         return;
374     }
375 }
376 
377 inline bool isValidConfigFileName(const std::string& fileName,
378                                   crow::Response& res)
379 {
380     if (fileName.empty())
381     {
382         BMCWEB_LOG_ERROR("Empty filename");
383         res.jsonValue["Description"] = "Empty file path in the url";
384         return false;
385     }
386 
387     // ConfigFile name is allowed to take upper and lowercase letters,
388     // numbers and hyphen
389     std::size_t found = fileName.find_first_not_of(
390         "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-");
391     if (found != std::string::npos)
392     {
393         BMCWEB_LOG_ERROR("Unsupported character in filename: {}", fileName);
394         res.jsonValue["Description"] = "Unsupported character in filename";
395         return false;
396     }
397 
398     // Check the filename length
399     if (fileName.length() > 20)
400     {
401         BMCWEB_LOG_ERROR("Name must be maximum 20 characters. "
402                          "Input filename length is: {}",
403                          fileName.length());
404         res.jsonValue["Description"] = "Filename must be maximum 20 characters";
405         return false;
406     }
407 
408     return true;
409 }
410 
411 inline void requestRoutes(App& app)
412 {
413     // allowed only for admin
414     BMCWEB_ROUTE(app, "/ibm/v1/")
415         .privileges({{"ConfigureComponents", "ConfigureManager"}})
416         .methods(boost::beast::http::verb::get)(
417             [](const crow::Request&,
418                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
419                 asyncResp->res.jsonValue["@odata.type"] =
420                     "#ibmServiceRoot.v1_0_0.ibmServiceRoot";
421                 asyncResp->res.jsonValue["@odata.id"] = "/ibm/v1/";
422                 asyncResp->res.jsonValue["Id"] = "IBM Rest RootService";
423                 asyncResp->res.jsonValue["Name"] = "IBM Service Root";
424                 asyncResp->res.jsonValue["ConfigFiles"]["@odata.id"] =
425                     "/ibm/v1/Host/ConfigFiles";
426                 asyncResp->res.jsonValue["BroadcastService"]["@odata.id"] =
427                     "/ibm/v1/HMC/BroadcastService";
428             });
429 
430     BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles")
431         .privileges({{"ConfigureComponents", "ConfigureManager"}})
432         .methods(boost::beast::http::verb::get)(
433             [](const crow::Request&,
434                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
435                 handleConfigFileList(asyncResp);
436             });
437 
438     BMCWEB_ROUTE(app,
439                  "/ibm/v1/Host/ConfigFiles/Actions/IBMConfigFiles.DeleteAll")
440         .privileges({{"ConfigureComponents", "ConfigureManager"}})
441         .methods(boost::beast::http::verb::post)(
442             [](const crow::Request&,
443                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
444                 deleteConfigFiles(asyncResp);
445             });
446 
447     BMCWEB_ROUTE(app, "/ibm/v1/Host/ConfigFiles/<str>")
448         .privileges({{"ConfigureComponents", "ConfigureManager"}})
449         .methods(boost::beast::http::verb::put, boost::beast::http::verb::get,
450                  boost::beast::http::verb::delete_)(
451             [](const crow::Request& req,
452                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
453                const std::string& fileName) {
454                 BMCWEB_LOG_DEBUG("ConfigFile : {}", fileName);
455                 // Validate the incoming fileName
456                 if (!isValidConfigFileName(fileName, asyncResp->res))
457                 {
458                     asyncResp->res.result(
459                         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