1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "event_service_store.hpp" 6 #include "logging.hpp" 7 #include "ossl_random.hpp" 8 #include "sessions.hpp" 9 // NOLINTNEXTLINE(misc-include-cleaner) 10 #include "utility.hpp" 11 12 #include <boost/beast/core/file_base.hpp> 13 #include <boost/beast/core/file_posix.hpp> 14 #include <boost/beast/http/fields.hpp> 15 #include <nlohmann/json.hpp> 16 17 #include <chrono> 18 #include <cstdint> 19 #include <filesystem> 20 #include <fstream> 21 #include <memory> 22 #include <optional> 23 #include <string> 24 #include <system_error> 25 #include <utility> 26 27 namespace persistent_data 28 { 29 30 class ConfigFile 31 { 32 uint64_t jsonRevision = 1; 33 34 public: 35 static std::string getStateFile() 36 { 37 // NOLINTNEXTLINE(concurrency-mt-unsafe) 38 const char* stateDir = std::getenv("STATE_DIRECTORY"); 39 if (stateDir == nullptr) 40 { 41 stateDir = "."; 42 } 43 return std::string(stateDir) + "/bmcweb_persistent_data.json"; 44 } 45 46 static const std::string& filename() 47 { 48 const static std::string fname = getStateFile(); 49 return fname; 50 } 51 52 ConfigFile() 53 { 54 readData(); 55 } 56 57 ~ConfigFile() 58 { 59 // Make sure we aren't writing stale sessions 60 persistent_data::SessionStore::getInstance().applySessionTimeouts(); 61 if (persistent_data::SessionStore::getInstance().needsWrite()) 62 { 63 writeData(); 64 } 65 } 66 67 ConfigFile(const ConfigFile&) = delete; 68 ConfigFile(ConfigFile&&) = delete; 69 ConfigFile& operator=(const ConfigFile&) = delete; 70 ConfigFile& operator=(ConfigFile&&) = delete; 71 72 // TODO(ed) this should really use protobuf, or some other serialization 73 // library, but adding another dependency is somewhat outside the scope of 74 // this application for the moment 75 void readData() 76 { 77 std::ifstream persistentFile(filename()); 78 uint64_t fileRevision = 0; 79 if (persistentFile.is_open()) 80 { 81 // call with exceptions disabled 82 auto data = nlohmann::json::parse(persistentFile, nullptr, false); 83 if (data.is_discarded()) 84 { 85 BMCWEB_LOG_ERROR("Error parsing persistent data in json file."); 86 } 87 else 88 { 89 const nlohmann::json::object_t* obj = 90 data.get_ptr<nlohmann::json::object_t*>(); 91 if (obj == nullptr) 92 { 93 return; 94 } 95 for (const auto& item : *obj) 96 { 97 if (item.first == "revision") 98 { 99 fileRevision = 0; 100 101 const uint64_t* uintPtr = 102 item.second.get_ptr<const uint64_t*>(); 103 if (uintPtr == nullptr) 104 { 105 BMCWEB_LOG_ERROR("Failed to read revision flag"); 106 } 107 else 108 { 109 fileRevision = *uintPtr; 110 } 111 } 112 else if (item.first == "system_uuid") 113 { 114 const std::string* jSystemUuid = 115 item.second.get_ptr<const std::string*>(); 116 if (jSystemUuid != nullptr) 117 { 118 systemUuid = *jSystemUuid; 119 } 120 } 121 else if (item.first == "service_identification") 122 { 123 const std::string* jServiceIdentification = 124 item.second.get_ptr<const std::string*>(); 125 if (jServiceIdentification != nullptr) 126 { 127 serviceIdentification = *jServiceIdentification; 128 } 129 } 130 else if (item.first == "auth_config") 131 { 132 const nlohmann::json::object_t* jObj = 133 item.second 134 .get_ptr<const nlohmann::json::object_t*>(); 135 if (jObj == nullptr) 136 { 137 continue; 138 } 139 SessionStore::getInstance() 140 .getAuthMethodsConfig() 141 .fromJson(*jObj); 142 } 143 else if (item.first == "sessions") 144 { 145 for (const auto& elem : item.second) 146 { 147 const nlohmann::json::object_t* jObj = 148 elem.get_ptr<const nlohmann::json::object_t*>(); 149 if (jObj == nullptr) 150 { 151 continue; 152 } 153 std::shared_ptr<UserSession> newSession = 154 UserSession::fromJson(*jObj); 155 156 if (newSession == nullptr) 157 { 158 BMCWEB_LOG_ERROR("Problem reading session " 159 "from persistent store"); 160 continue; 161 } 162 163 BMCWEB_LOG_DEBUG("Restored session: {} {} {}", 164 newSession->csrfToken, 165 newSession->uniqueId, 166 newSession->sessionToken); 167 SessionStore::getInstance().authTokens.emplace( 168 newSession->sessionToken, newSession); 169 } 170 } 171 else if (item.first == "timeout") 172 { 173 const int64_t* jTimeout = 174 item.second.get_ptr<const int64_t*>(); 175 if (jTimeout == nullptr) 176 { 177 BMCWEB_LOG_DEBUG( 178 "Problem reading session timeout value"); 179 continue; 180 } 181 std::chrono::seconds sessionTimeoutInseconds(*jTimeout); 182 BMCWEB_LOG_DEBUG("Restored Session Timeout: {}", 183 sessionTimeoutInseconds.count()); 184 SessionStore::getInstance().updateSessionTimeout( 185 sessionTimeoutInseconds); 186 } 187 else if (item.first == "eventservice_config") 188 { 189 const nlohmann::json::object_t* esobj = 190 item.second 191 .get_ptr<const nlohmann::json::object_t*>(); 192 if (esobj == nullptr) 193 { 194 BMCWEB_LOG_DEBUG( 195 "Problem reading EventService value"); 196 continue; 197 } 198 199 EventServiceStore::getInstance() 200 .getEventServiceConfig() 201 .fromJson(*esobj); 202 } 203 else if (item.first == "subscriptions") 204 { 205 for (const auto& elem : item.second) 206 { 207 const nlohmann::json::object_t* subobj = 208 elem.get_ptr<const nlohmann::json::object_t*>(); 209 if (subobj == nullptr) 210 { 211 continue; 212 } 213 214 std::optional<UserSubscription> newSub = 215 UserSubscription::fromJson(*subobj); 216 217 if (!newSub) 218 { 219 BMCWEB_LOG_ERROR( 220 "Problem reading subscription from persistent store"); 221 continue; 222 } 223 224 std::string id = newSub->id; 225 BMCWEB_LOG_DEBUG("Restored subscription: {} {}", id, 226 newSub->customText); 227 228 EventServiceStore::getInstance() 229 .subscriptionsConfigMap.emplace( 230 id, std::make_shared<UserSubscription>( 231 std::move(*newSub))); 232 } 233 } 234 else 235 { 236 // Do nothing in the case of extra fields. We may have 237 // cases where fields are added in the future, and we 238 // want to at least attempt to gracefully support 239 // downgrades in that case, even if we don't officially 240 // support it 241 } 242 } 243 } 244 } 245 bool needWrite = false; 246 247 if (systemUuid.empty()) 248 { 249 systemUuid = bmcweb::getRandomUUID(); 250 needWrite = true; 251 } 252 if (fileRevision < jsonRevision) 253 { 254 needWrite = true; 255 } 256 // write revision changes or system uuid changes immediately 257 if (needWrite) 258 { 259 writeData(); 260 } 261 } 262 263 void writeData() 264 { 265 const std::string& fname = filename(); 266 std::filesystem::path path(fname); 267 path = path.parent_path(); 268 if (!path.empty()) 269 { 270 std::error_code ecDir; 271 std::filesystem::create_directories(path, ecDir); 272 if (ecDir) 273 { 274 BMCWEB_LOG_CRITICAL("Can't create persistent folders {}", 275 ecDir.message()); 276 return; 277 } 278 } 279 boost::beast::file_posix persistentFile; 280 boost::system::error_code ec; 281 persistentFile.open(fname.c_str(), boost::beast::file_mode::write, ec); 282 if (ec) 283 { 284 BMCWEB_LOG_CRITICAL("Unable to store persistent data to file {}", 285 ec.message()); 286 return; 287 } 288 289 // set the permission of the file to 640 290 std::filesystem::perms permission = 291 std::filesystem::perms::owner_read | 292 std::filesystem::perms::owner_write | 293 std::filesystem::perms::group_read; 294 std::filesystem::permissions(fname, permission, ec); 295 if (ec) 296 { 297 BMCWEB_LOG_CRITICAL("Failed to set filesystem permissions {}", 298 ec.message()); 299 return; 300 } 301 const AuthConfigMethods& c = 302 SessionStore::getInstance().getAuthMethodsConfig(); 303 const auto& eventServiceConfig = 304 EventServiceStore::getInstance().getEventServiceConfig(); 305 nlohmann::json::object_t data; 306 nlohmann::json& authConfig = data["auth_config"]; 307 308 authConfig["XToken"] = c.xtoken; 309 authConfig["Cookie"] = c.cookie; 310 authConfig["SessionToken"] = c.sessionToken; 311 authConfig["BasicAuth"] = c.basic; 312 authConfig["TLS"] = c.tls; 313 authConfig["TLSStrict"] = c.tlsStrict; 314 authConfig["MTLSCommonNameParseMode"] = 315 static_cast<int>(c.mTLSCommonNameParsingMode); 316 317 nlohmann::json& eventserviceConfig = data["eventservice_config"]; 318 eventserviceConfig["ServiceEnabled"] = eventServiceConfig.enabled; 319 eventserviceConfig["DeliveryRetryAttempts"] = 320 eventServiceConfig.retryAttempts; 321 eventserviceConfig["DeliveryRetryIntervalSeconds"] = 322 eventServiceConfig.retryTimeoutInterval; 323 324 data["system_uuid"] = systemUuid; 325 data["service_identification"] = serviceIdentification; 326 data["revision"] = jsonRevision; 327 data["timeout"] = SessionStore::getInstance().getTimeoutInSeconds(); 328 329 nlohmann::json& sessions = data["sessions"]; 330 sessions = nlohmann::json::array(); 331 for (const auto& p : SessionStore::getInstance().authTokens) 332 { 333 if (p.second->sessionType != persistent_data::SessionType::Basic && 334 p.second->sessionType != 335 persistent_data::SessionType::MutualTLS) 336 { 337 nlohmann::json::object_t session; 338 session["unique_id"] = p.second->uniqueId; 339 session["session_token"] = p.second->sessionToken; 340 session["username"] = p.second->username; 341 session["csrf_token"] = p.second->csrfToken; 342 session["client_ip"] = p.second->clientIp; 343 const std::optional<std::string>& clientId = p.second->clientId; 344 if (clientId) 345 { 346 session["client_id"] = *clientId; 347 } 348 sessions.emplace_back(std::move(session)); 349 } 350 } 351 nlohmann::json& subscriptions = data["subscriptions"]; 352 subscriptions = nlohmann::json::array(); 353 for (const auto& it : 354 EventServiceStore::getInstance().subscriptionsConfigMap) 355 { 356 if (it.second == nullptr) 357 { 358 continue; 359 } 360 const UserSubscription& subValue = *it.second; 361 if (subValue.subscriptionType == "SSE") 362 { 363 BMCWEB_LOG_DEBUG("The subscription type is SSE, so skipping."); 364 continue; 365 } 366 nlohmann::json::object_t headers; 367 for (const boost::beast::http::fields::value_type& header : 368 subValue.httpHeaders) 369 { 370 // Note, these are technically copies because nlohmann doesn't 371 // support key lookup by std::string_view. At least the 372 // following code can use move 373 // https://github.com/nlohmann/json/issues/1529 374 std::string name(header.name_string()); 375 headers[std::move(name)] = header.value(); 376 } 377 378 nlohmann::json::object_t subscription; 379 380 subscription["Id"] = subValue.id; 381 subscription["Context"] = subValue.customText; 382 subscription["DeliveryRetryPolicy"] = subValue.retryPolicy; 383 subscription["SendHeartbeat"] = subValue.sendHeartbeat; 384 subscription["HeartbeatIntervalMinutes"] = 385 subValue.hbIntervalMinutes; 386 subscription["Destination"] = subValue.destinationUrl; 387 subscription["EventFormatType"] = subValue.eventFormatType; 388 subscription["HttpHeaders"] = std::move(headers); 389 subscription["MessageIds"] = subValue.registryMsgIds; 390 subscription["Protocol"] = subValue.protocol; 391 subscription["RegistryPrefixes"] = subValue.registryPrefixes; 392 subscription["OriginResources"] = subValue.originResources; 393 subscription["ResourceTypes"] = subValue.resourceTypes; 394 subscription["SubscriptionType"] = subValue.subscriptionType; 395 subscription["MetricReportDefinitions"] = 396 subValue.metricReportDefinitions; 397 subscription["VerifyCertificate"] = subValue.verifyCertificate; 398 399 subscriptions.emplace_back(std::move(subscription)); 400 } 401 std::string out = nlohmann::json(data).dump( 402 -1, ' ', true, nlohmann::json::error_handler_t::replace); 403 persistentFile.write(out.data(), out.size(), ec); 404 if (ec) 405 { 406 BMCWEB_LOG_ERROR("Failed to write file {}", ec.message()); 407 } 408 } 409 410 std::string systemUuid; 411 std::string serviceIdentification; 412 }; 413 414 inline ConfigFile& getConfig() 415 { 416 static ConfigFile f; 417 return f; 418 } 419 420 } // namespace persistent_data 421