xref: /openbmc/bmcweb/include/ibm/management_console_rest.hpp (revision 504af5a0568171b72caf13234cc81380b261fa21)
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 "ibm/utils.hpp"
9 #include "logging.hpp"
10 #include "str_utility.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 
handleFilePut(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & fileID)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 
handleConfigFileList(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)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 
deleteConfigFiles(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)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 
handleFileGet(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & fileID)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 
handleFileDelete(const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & fileID)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 
handleBroadcastService(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)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 
handleFileUrl(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & fileID)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 
isValidConfigFileName(const std::string & fileName,crow::Response & res)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 
requestRoutes(App & app)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