| // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| |
| #include <base/files/file.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/files/scoped_temp_dir.h> |
| #include <base/json/json_reader.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/stringprintf.h> |
| #include <build/build_config.h> |
| #include <build/buildflag.h> |
| |
| #include "featured/service.h" |
| |
| namespace featured { |
| |
| namespace { |
| constexpr char kPlatformFeaturesPath[] = "/etc/init/platform-features.json"; |
| |
| // Allow featured do write operations to path with these prefixes only. |
| std::vector<std::string> allowedPrefixes = {"/proc", "/sys"}; |
| |
| bool CheckPathPrefix(const base::FilePath& path) { |
| bool valid = false; |
| |
| for (std::string& prefix_str : allowedPrefixes) { |
| if (base::FilePath(prefix_str).IsParent(path)) { |
| valid = true; |
| break; |
| } |
| } |
| |
| return valid; |
| } |
| |
| // JSON Helper to retrieve a string value given a string key |
| bool GetStringFromKey(const base::Value& obj, |
| const std::string& key, |
| std::string* value) { |
| const std::string* val = obj.FindStringKey(key); |
| if (!val || val->empty()) { |
| return false; |
| } |
| |
| *value = *val; |
| return true; |
| } |
| } // namespace |
| |
| WriteFileCommand::WriteFileCommand(const std::string& file_name, |
| const std::string& value) |
| : FeatureCommand("WriteFile") { |
| file_name_ = file_name; |
| value_ = value; |
| } |
| |
| bool WriteFileCommand::Execute() { |
| base::FilePath fpath = base::FilePath(file_name_); |
| if (!CheckPathPrefix(fpath)) { |
| PLOG(ERROR) << "Unable to write to path, prefix check fail: " << file_name_; |
| PLOG(ERROR) << "Make sure to update the prefix list in sources and the " |
| "selinux config."; |
| return false; |
| } |
| |
| if (!base::WriteFile(fpath, value_)) { |
| PLOG(ERROR) << "Unable to write to " << file_name_; |
| return false; |
| } |
| return true; |
| } |
| |
| MkdirCommand::MkdirCommand(const std::string& path) : FeatureCommand("Mkdir") { |
| path_ = base::FilePath(path); |
| } |
| |
| bool MkdirCommand::Execute() { |
| if (!CheckPathPrefix(path_)) { |
| PLOG(ERROR) << "Unable to create directory, prefix check fail: " << path_; |
| return false; |
| } |
| |
| if (base::PathExists(path_)) { |
| PLOG(ERROR) << "Path already exists, cannot create directory: " << path_; |
| return false; |
| } |
| |
| if (!base::CreateDirectory(path_)) { |
| PLOG(ERROR) << "Unable to create directory: " << path_; |
| return false; |
| } |
| return true; |
| } |
| |
| FileExistsCommand::FileExistsCommand(const std::string& file_name) |
| : FeatureCommand("FileExists") { |
| file_name_ = file_name; |
| } |
| |
| bool FileExistsCommand::Execute() { |
| return base::PathExists(base::FilePath(file_name_)); |
| } |
| |
| FileNotExistsCommand::FileNotExistsCommand(const std::string& file_name) |
| : FeatureCommand("FileNotExists") { |
| file_name_ = file_name; |
| } |
| |
| bool FileNotExistsCommand::Execute() { |
| return !base::PathExists(base::FilePath(file_name_)); |
| } |
| |
| bool PlatformFeature::Execute() const { |
| for (auto& cmd : exec_cmds_) { |
| if (!cmd->Execute()) { |
| LOG(ERROR) << "Failed to execute command: " << cmd->name(); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool PlatformFeature::IsSupported() const { |
| for (auto& cmd : support_check_cmds_) { |
| if (!cmd->Execute()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool JsonFeatureParser::ParseFileContents(const std::string& file_contents) { |
| if (features_parsed_) |
| return true; |
| |
| VLOG(1) << "JSON file contents: " << file_contents; |
| |
| base::JSONReader::ValueWithError root = |
| base::JSONReader::ReadAndReturnValueWithError(file_contents); |
| if (!root.value) { |
| LOG(ERROR) << "Failed to parse conf file: " << kPlatformFeaturesPath; |
| return false; |
| } |
| |
| if (!root.value->is_list() || root.value->GetList().size() == 0) { |
| LOG(ERROR) << "features list should be non-zero size!"; |
| return false; |
| } |
| |
| for (const auto& item : root.value->GetList()) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "features conf not list of dicts!"; |
| return false; |
| } |
| |
| auto feature_obj_optional = MakeFeatureObject(item); |
| if (!feature_obj_optional) { |
| return false; |
| } |
| PlatformFeature feature_obj(std::move(*feature_obj_optional)); |
| |
| auto got = feature_map_.find(feature_obj.name()); |
| if (got != feature_map_.end()) { |
| LOG(ERROR) << "Duplicate feature name found: " << feature_obj.name(); |
| return false; |
| } |
| |
| feature_map_.insert( |
| std::make_pair(feature_obj.name(), std::move(feature_obj))); |
| } |
| |
| features_parsed_ = true; |
| return true; |
| } |
| |
| // PlatformFeature implementation (collect and execute commands). |
| std::optional<PlatformFeature> JsonFeatureParser::MakeFeatureObject( |
| const base::Value& feature_obj) { |
| std::string feat_name; |
| if (!GetStringFromKey(feature_obj, "name", &feat_name)) { |
| LOG(ERROR) << "features conf contains empty names"; |
| return std::nullopt; |
| } |
| |
| // Commands for querying if device is supported |
| const base::Value* support_cmd_list_obj = |
| feature_obj.FindListKey("support_check_commands"); |
| |
| std::vector<std::unique_ptr<FeatureCommand>> query_cmds; |
| if (!support_cmd_list_obj) { |
| // Feature is assumed to be always supported, such as a kernel parameter |
| // that is on all device kernels. |
| query_cmds.push_back(std::make_unique<AlwaysSupportedCommand>()); |
| } else { |
| // A support check command was provided, add it to the feature object. |
| if (!support_cmd_list_obj->is_list() || |
| support_cmd_list_obj->GetList().size() == 0) { |
| LOG(ERROR) << "Invalid format for support_check_commands commands"; |
| return std::nullopt; |
| } |
| |
| for (const auto& item : support_cmd_list_obj->GetList()) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "support_check_commands is not list of dicts."; |
| return std::nullopt; |
| } |
| |
| std::string cmd_name; |
| |
| if (!GetStringFromKey(item, "name", &cmd_name)) { |
| LOG(ERROR) << "Invalid/Empty command name in features config."; |
| return std::nullopt; |
| } |
| |
| if (cmd_name == "FileExists" || cmd_name == "FileNotExists") { |
| std::string file_name; |
| |
| VLOG(1) << "featured: command is " << cmd_name; |
| if (!GetStringFromKey(item, "path", &file_name)) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| std::unique_ptr<FeatureCommand> cmd; |
| if (cmd_name == "FileExists") { |
| cmd = std::make_unique<FileExistsCommand>(file_name); |
| } else { |
| cmd = std::make_unique<FileNotExistsCommand>(file_name); |
| } |
| |
| query_cmds.push_back(std::move(cmd)); |
| } else { |
| LOG(ERROR) << "Invalid support command name in features config: " |
| << cmd_name; |
| return std::nullopt; |
| } |
| } |
| } |
| |
| // Commands to execute to enable feature |
| const base::Value* cmd_list_obj = feature_obj.FindListKey("commands"); |
| if (!cmd_list_obj || !cmd_list_obj->is_list() || |
| cmd_list_obj->GetList().size() == 0) { |
| LOG(ERROR) << "Failed to get commands list in feature."; |
| return std::nullopt; |
| } |
| |
| std::vector<std::unique_ptr<FeatureCommand>> feature_cmds; |
| for (const auto& item : cmd_list_obj->GetList()) { |
| if (!item.is_dict()) { |
| LOG(ERROR) << "Invalid command in features config."; |
| return std::nullopt; |
| } |
| std::string cmd_name; |
| |
| if (!GetStringFromKey(item, "name", &cmd_name)) { |
| LOG(ERROR) << "Invalid command in features config."; |
| return std::nullopt; |
| } |
| |
| if (cmd_name == "WriteFile") { |
| std::string file_name, value; |
| |
| VLOG(1) << "featured: command is WriteFile"; |
| if (!GetStringFromKey(item, "path", &file_name)) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| if (!GetStringFromKey(item, "value", &value)) { |
| LOG(ERROR) << "JSON contains invalid command value!"; |
| return std::nullopt; |
| } |
| feature_cmds.push_back( |
| std::make_unique<WriteFileCommand>(file_name, value)); |
| } else if (cmd_name == "Mkdir") { |
| std::string file_name, value; |
| |
| VLOG(1) << "featured: command is Mkdir"; |
| if (!GetStringFromKey(item, "path", &file_name)) { |
| LOG(ERROR) << "JSON contains invalid path!"; |
| return std::nullopt; |
| } |
| |
| feature_cmds.push_back(std::make_unique<MkdirCommand>(file_name)); |
| } else { |
| LOG(ERROR) << "Invalid command name in features config: " << cmd_name; |
| return std::nullopt; |
| } |
| } |
| |
| return PlatformFeature(feat_name, std::move(query_cmds), |
| std::move(feature_cmds)); |
| } |
| |
| bool DbusFeaturedService::ParseFeatureList() { |
| if (parser_->AreFeaturesParsed()) |
| return true; |
| |
| std::string file_contents; |
| if (!ReadFileToString(base::FilePath(kPlatformFeaturesPath), |
| &file_contents)) { |
| LOG(ERROR) << "Failed to read conf file: " << kPlatformFeaturesPath; |
| return false; |
| } |
| |
| return parser_->ParseFileContents(file_contents); |
| } |
| |
| bool DbusFeaturedService::EnableFeatures() { |
| if (!ParseFeatureList()) { |
| return false; |
| } |
| for (const auto& it : *(parser_->GetFeatureMap())) { |
| if (it.second.IsSupported() && |
| library_->IsEnabledBlocking(*it.second.feature())) { |
| it.second.Execute(); |
| } |
| } |
| return true; |
| } |
| |
| bool DbusFeaturedService::IsPlatformFeatureEnabled(const std::string& name) { |
| if (!ParseFeatureList()) { |
| return false; |
| } |
| |
| auto feature = parser_->GetFeatureMap()->find(name); |
| if (feature == parser_->GetFeatureMap()->end()) { |
| LOG(ERROR) << "Feature not found in features config!"; |
| return false; |
| } |
| |
| const PlatformFeature& feature_obj = feature->second; |
| if (!feature_obj.IsSupported()) { |
| VLOG(1) << "device does not support feature " << name; |
| return false; |
| } |
| |
| bool ret = library_->IsEnabledBlocking(*feature_obj.feature()); |
| |
| VLOG(1) << "featured: IsPlatformFeatureEnabled: Feature " << name |
| << " enabled? " << ret; |
| return ret; |
| } |
| |
| void DbusFeaturedService::IsPlatformFeatureEnabledWrap( |
| dbus::MethodCall* method_call, |
| dbus::ExportedObject::ResponseSender sender) { |
| bool ret; |
| |
| dbus::MessageReader reader(method_call); |
| std::string name; |
| if (!reader.PopString(&name)) { |
| LOG(ERROR) << "missing string argument to IsPlatformFeatureEnabled"; |
| ret = false; |
| } else { |
| ret = IsPlatformFeatureEnabled(name); |
| } |
| |
| std::unique_ptr<dbus::Response> response = |
| dbus::Response::FromMethodCall(method_call); |
| dbus::MessageWriter writer(response.get()); |
| |
| writer.AppendBool(ret); |
| |
| std::move(sender).Run(std::move(response)); |
| } |
| |
| bool DbusFeaturedService::Start(dbus::Bus* bus, |
| std::shared_ptr<DbusFeaturedService> ptr) { |
| if (!bus || !bus->Connect()) { |
| LOG(ERROR) << "Failed to connect to DBus"; |
| return false; |
| } |
| |
| library_ = feature::PlatformFeatures::New(bus); |
| |
| dbus::ObjectPath path(featured::kFeaturedServicePath); |
| dbus::ExportedObject* object = bus->GetExportedObject(path); |
| if (!object) { |
| LOG(ERROR) << "Failed to get exported object at " << path.value(); |
| return false; |
| } |
| |
| if (!EnableFeatures()) { |
| LOG(ERROR) << "Failed to enable features"; |
| return false; |
| } |
| |
| if (!object->ExportMethodAndBlock( |
| featured::kFeaturedServiceName, featured::kIsPlatformFeatureEnabled, |
| base::BindRepeating( |
| &DbusFeaturedService::IsPlatformFeatureEnabledWrap, ptr))) { |
| bus->UnregisterExportedObject(path); |
| LOG(ERROR) << "Failed to export method " |
| << featured::kIsPlatformFeatureEnabled; |
| return false; |
| } |
| |
| if (!bus->RequestOwnershipAndBlock(featured::kFeaturedServiceName, |
| dbus::Bus::REQUIRE_PRIMARY)) { |
| bus->UnregisterExportedObject(path); |
| LOG(ERROR) << "Failed to get ownership of " |
| << featured::kFeaturedServiceName; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace featured |