From e27ddc80f9b4d08b2f88a6dc1404f574e00358be Mon Sep 17 00:00:00 2001 From: Slava Aseev Date: Wed, 25 May 2022 07:09:09 +0000 Subject: Handle kioslaverc config located in XDG_CONFIG_DIRS This commit introduces the override behaviour for kioslaverc config like in KDE5's kreadconfig5. This feature can be used in some distros to provide system-level proxy configurations. In KDE5 the kioslaverc can be located in any of XDG_CONFIG_DIRS and in the $HOME/.config (this is already supprorted). Also it is possible to have multiple configs in different locations. In this case each value from kioslaverc config from location with higher priority overrides value with same key from previously read configs. For example, if XDG_CONFIG_DIRS environment is set to: /home/test/.config/kdedefaults:/etc/kf5/xdg:/etc/xdg the override order would be: /etc/xdg/ /etc/kf5/xdg/ /home/test/.config/kdedefaults/ /home/test/.config/ <- has the highest priority in any case For example, reading this two configs: /etc/xdg/kioslaverc contained: [Proxy Settings] ProxyType=2 httpProxy=www.google.com 80 NoProxyFor=.google.com,.kde.org /home/test/.config/kioslaverc contained: [Proxy Settings] ProxyType=1 ReversedException=true would result in: [Proxy Settings] ProxyType=1 httpProxy=www.google.com 80 NoProxyFor=.google.com,.kde.org ReversedException=true Change-Id: I70d0d7d36d377a95610e4757a5855528ccc1b4e4 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3632300 Commit-Queue: Adam Rice Reviewed-by: Adam Rice Cr-Commit-Position: refs/heads/main@{#1007261} --- .../proxy_config_service_linux.cc | 180 +++++++++++------- .../proxy_config_service_linux_unittest.cc | 100 ++++++++++ 2 files changed, 207 insertions(+), 73 deletions(-) diff --git a/net/proxy_resolution/proxy_config_service_linux.cc b/net/proxy_resolution/proxy_config_service_linux.cc index 16f3ca64891..1a3b7d46020 100644 --- a/net/proxy_resolution/proxy_config_service_linux.cc +++ b/net/proxy_resolution/proxy_config_service_linux.cc @@ -23,6 +23,7 @@ #include "base/nix/xdg_util.h" #include "base/observer_list.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" #include "base/strings/string_tokenizer.h" #include "base/strings/string_util.h" #include "base/task/sequenced_task_runner.h" @@ -532,11 +533,11 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { // This has to be called on the UI thread (http://crbug.com/69057). base::ThreadRestrictions::ScopedAllowIO allow_io; - // Derive the location of the kde config dir from the environment. + // Derive the location(s) of the kde config dir from the environment. std::string home; if (env_var_getter->GetVar("KDEHOME", &home) && !home.empty()) { // $KDEHOME is set. Use it unconditionally. - kde_config_dir_ = KDEHomeToConfigPath(base::FilePath(home)); + kde_config_dirs_.emplace_back(KDEHomeToConfigPath(base::FilePath(home))); } else { // $KDEHOME is unset. Try to figure out what to use. This seems to be // the common case on most distributions. @@ -547,7 +548,7 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { base::nix::DESKTOP_ENVIRONMENT_KDE3) { // KDE3 always uses .kde for its configuration. base::FilePath kde_path = base::FilePath(home).Append(".kde"); - kde_config_dir_ = KDEHomeToConfigPath(kde_path); + kde_config_dirs_.emplace_back(KDEHomeToConfigPath(kde_path)); } else if (base::nix::GetDesktopEnvironment(env_var_getter) == base::nix::DESKTOP_ENVIRONMENT_KDE4) { // Some distributions patch KDE4 to use .kde4 instead of .kde, so that @@ -577,13 +578,27 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { } } if (use_kde4) { - kde_config_dir_ = KDEHomeToConfigPath(kde4_path); + kde_config_dirs_.emplace_back(KDEHomeToConfigPath(kde4_path)); } else { - kde_config_dir_ = KDEHomeToConfigPath(kde3_path); + kde_config_dirs_.emplace_back(KDEHomeToConfigPath(kde3_path)); } } else { // KDE 5 migrated to ~/.config for storing kioslaverc. - kde_config_dir_ = base::FilePath(home).Append(".config"); + kde_config_dirs_.emplace_back(base::FilePath(home).Append(".config")); + + // kioslaverc also can be stored in any of XDG_CONFIG_DIRS + std::string config_dirs; + if (env_var_getter_->GetVar("XDG_CONFIG_DIRS", &config_dirs)) { + auto dirs = base::SplitString(config_dirs, ":", base::KEEP_WHITESPACE, + base::SPLIT_WANT_NONEMPTY); + for (const auto& dir : dirs) { + kde_config_dirs_.emplace_back(dir); + } + } + + // Reverses the order of paths to store them in ascending order of + // priority + std::reverse(kde_config_dirs_.begin(), kde_config_dirs_.end()); } } } @@ -652,8 +667,15 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { // the first change, and it will never change again). So, we watch the // directory instead. We then act only on changes to the kioslaverc entry. // TODO(eroman): What if the file is deleted? (handle with IN_DELETE). - if (inotify_add_watch(inotify_fd_, kde_config_dir_.value().c_str(), - IN_MODIFY | IN_MOVED_TO) < 0) { + size_t failed_dirs = 0; + for (const auto& kde_config_dir : kde_config_dirs_) { + if (inotify_add_watch(inotify_fd_, kde_config_dir.value().c_str(), + IN_MODIFY | IN_MOVED_TO) < 0) { + ++failed_dirs; + } + } + // Fail if inotify_add_watch failed with every directory + if (failed_dirs == kde_config_dirs_.size()) { return false; } notify_delegate_ = delegate; @@ -840,81 +862,93 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { } } - // Reads kioslaverc one line at a time and calls AddKDESetting() to add - // each relevant name-value pair to the appropriate value table. + // Reads kioslaverc from all paths one line at a time and calls + // AddKDESetting() to add each relevant name-value pair to the appropriate + // value table. Each value can be overwritten by values from configs from + // the following paths. void UpdateCachedSettings() { - base::FilePath kioslaverc = kde_config_dir_.Append("kioslaverc"); - base::ScopedFILE input(base::OpenFile(kioslaverc, "r")); - if (!input.get()) - return; - ResetCachedSettings(); - bool in_proxy_settings = false; - bool line_too_long = false; - char line[BUFFER_SIZE]; - // fgets() will return NULL on EOF or error. - while (fgets(line, sizeof(line), input.get())) { - // fgets() guarantees the line will be properly terminated. - size_t length = strlen(line); - if (!length) - continue; - // This should be true even with CRLF endings. - if (line[length - 1] != '\n') { - line_too_long = true; - continue; - } - if (line_too_long) { - // The previous line had no line ending, but this done does. This is - // the end of the line that was too long, so warn here and skip it. - LOG(WARNING) << "skipped very long line in " << kioslaverc.value(); - line_too_long = false; + bool at_least_one_kioslaverc_opened = false; + for (const auto& kde_config_dir : kde_config_dirs_) { + base::FilePath kioslaverc = kde_config_dir.Append("kioslaverc"); + base::ScopedFILE input(base::OpenFile(kioslaverc, "r")); + if (!input.get()) continue; + + // Reset cached settings once only if some config was successfully opened + if (!at_least_one_kioslaverc_opened) { + ResetCachedSettings(); } - // Remove the LF at the end, and the CR if there is one. - line[--length] = '\0'; - if (length && line[length - 1] == '\r') - line[--length] = '\0'; - // Now parse the line. - if (line[0] == '[') { - // Switching sections. All we care about is whether this is - // the (a?) proxy settings section, for both KDE3 and KDE4. - in_proxy_settings = !strncmp(line, "[Proxy Settings]", 16); - } else if (in_proxy_settings) { - // A regular line, in the (a?) proxy settings section. - char* split = strchr(line, '='); - // Skip this line if it does not contain an = sign. - if (!split) + at_least_one_kioslaverc_opened = true; + bool in_proxy_settings = false; + bool line_too_long = false; + char line[BUFFER_SIZE]; + // fgets() will return NULL on EOF or error. + while (fgets(line, sizeof(line), input.get())) { + // fgets() guarantees the line will be properly terminated. + size_t length = strlen(line); + if (!length) continue; - // Split the line on the = and advance |split|. - *(split++) = 0; - std::string key = line; - std::string value = split; - base::TrimWhitespaceASCII(key, base::TRIM_ALL, &key); - base::TrimWhitespaceASCII(value, base::TRIM_ALL, &value); - // Skip this line if the key name is empty. - if (key.empty()) + // This should be true even with CRLF endings. + if (line[length - 1] != '\n') { + line_too_long = true; + continue; + } + if (line_too_long) { + // The previous line had no line ending, but this one does. This is + // the end of the line that was too long, so warn here and skip it. + LOG(WARNING) << "skipped very long line in " << kioslaverc.value(); + line_too_long = false; continue; - // Is the value name localized? - if (key[key.length() - 1] == ']') { - // Find the matching bracket. - length = key.rfind('['); - // Skip this line if the localization indicator is malformed. - if (length == std::string::npos) + } + // Remove the LF at the end, and the CR if there is one. + line[--length] = '\0'; + if (length && line[length - 1] == '\r') + line[--length] = '\0'; + // Now parse the line. + if (line[0] == '[') { + // Switching sections. All we care about is whether this is + // the (a?) proxy settings section, for both KDE3 and KDE4. + in_proxy_settings = !strncmp(line, "[Proxy Settings]", 16); + } else if (in_proxy_settings) { + // A regular line, in the (a?) proxy settings section. + char* split = strchr(line, '='); + // Skip this line if it does not contain an = sign. + if (!split) continue; - // Trim the localization indicator off. - key.resize(length); - // Remove any resulting trailing whitespace. - base::TrimWhitespaceASCII(key, base::TRIM_TRAILING, &key); - // Skip this line if the key name is now empty. + // Split the line on the = and advance |split|. + *(split++) = 0; + std::string key = line; + std::string value = split; + base::TrimWhitespaceASCII(key, base::TRIM_ALL, &key); + base::TrimWhitespaceASCII(value, base::TRIM_ALL, &value); + // Skip this line if the key name is empty. if (key.empty()) continue; + // Is the value name localized? + if (key[key.length() - 1] == ']') { + // Find the matching bracket. + length = key.rfind('['); + // Skip this line if the localization indicator is malformed. + if (length == std::string::npos) + continue; + // Trim the localization indicator off. + key.resize(length); + // Remove any resulting trailing whitespace. + base::TrimWhitespaceASCII(key, base::TRIM_TRAILING, &key); + // Skip this line if the key name is now empty. + if (key.empty()) + continue; + } + // Now fill in the tables. + AddKDESetting(key, value); } - // Now fill in the tables. - AddKDESetting(key, value); } + if (ferror(input.get())) + LOG(ERROR) << "error reading " << kioslaverc.value(); + } + if (at_least_one_kioslaverc_opened) { + ResolveModeEffects(); } - if (ferror(input.get())) - LOG(ERROR) << "error reading " << kioslaverc.value(); - ResolveModeEffects(); } // This is the callback from the debounce timer. @@ -990,7 +1024,7 @@ class SettingGetterImplKDE : public ProxyConfigServiceLinux::SettingGetter { std::unique_ptr inotify_watcher_; ProxyConfigServiceLinux::Delegate* notify_delegate_; std::unique_ptr debounce_timer_; - base::FilePath kde_config_dir_; + std::vector kde_config_dirs_; bool indirect_manual_; bool auto_no_pac_; bool reversed_bypass_list_; diff --git a/net/proxy_resolution/proxy_config_service_linux_unittest.cc b/net/proxy_resolution/proxy_config_service_linux_unittest.cc index 386028ed959..597a943c6f4 100644 --- a/net/proxy_resolution/proxy_config_service_linux_unittest.cc +++ b/net/proxy_resolution/proxy_config_service_linux_unittest.cc @@ -56,6 +56,7 @@ struct EnvVarValues { const char* SOCKS_SERVER; const char* SOCKS_VERSION; const char* no_proxy; + const char* XDG_CONFIG_DIRS; }; // Undo macro pollution from GDK includes (from message_loop.h). @@ -123,6 +124,7 @@ class MockEnvironment : public base::Environment { ENTRY(no_proxy); ENTRY(SOCKS_SERVER); ENTRY(SOCKS_VERSION); + ENTRY(XDG_CONFIG_DIRS); #undef ENTRY Reset(); } @@ -429,6 +431,12 @@ class ProxyConfigServiceLinuxTest : public PlatformTest, kioslaverc4_ = kde4_config_.Append(FILE_PATH_LITERAL("kioslaverc")); // Set up paths for KDE 5 kioslaverc5_ = config_home_.Append(FILE_PATH_LITERAL("kioslaverc")); + config_xdg_home_ = user_home_.Append(FILE_PATH_LITERAL("xdg")); + config_kdedefaults_home_ = + config_home_.Append(FILE_PATH_LITERAL("kdedefaults")); + kioslaverc5_xdg_ = config_xdg_home_.Append(FILE_PATH_LITERAL("kioslaverc")); + kioslaverc5_kdedefaults_ = + config_kdedefaults_home_.Append(FILE_PATH_LITERAL("kioslaverc")); } void TearDown() override { @@ -439,6 +447,8 @@ class ProxyConfigServiceLinuxTest : public PlatformTest, base::FilePath user_home_; base::FilePath config_home_; + base::FilePath config_xdg_home_; + base::FilePath config_kdedefaults_home_; // KDE3 paths. base::FilePath kde_home_; base::FilePath kioslaverc_; @@ -448,6 +458,8 @@ class ProxyConfigServiceLinuxTest : public PlatformTest, base::FilePath kioslaverc4_; // KDE5 paths. base::FilePath kioslaverc5_; + base::FilePath kioslaverc5_xdg_; + base::FilePath kioslaverc5_kdedefaults_; }; // Builds an identifier for each test in an array. @@ -1932,6 +1944,94 @@ TEST_F(ProxyConfigServiceLinuxTest, KDEFileChanged) { // doesn't trigger any notifications, but it probably should. } +TEST_F(ProxyConfigServiceLinuxTest, KDEMultipleKioslaverc) { + std::string xdg_config_dirs = config_kdedefaults_home_.value(); + xdg_config_dirs += ':'; + xdg_config_dirs += config_xdg_home_.value(); + + const struct { + // Short description to identify the test + std::string description; + + // Input. + std::string kioslaverc; + base::FilePath kioslaverc_path; + bool auto_detect; + GURL pac_url; + ProxyRulesExpectation proxy_rules; + } tests[] = { + { + TEST_DESC("Use xdg/kioslaverc"), + + // Input. + "[Proxy Settings]\nProxyType=3\n" + "Proxy Config Script=http://wpad/wpad.dat\n" + "httpsProxy=www.foo.com\n", + kioslaverc5_xdg_, // kioslaverc path + true, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::Empty(), + }, + { + TEST_DESC(".config/kdedefaults/kioslaverc overrides xdg/kioslaverc"), + + // Input. + "[Proxy Settings]\nProxyType=2\n" + "NoProxyFor=.google.com,.kde.org\n", + kioslaverc5_kdedefaults_, // kioslaverc path + false, // auto_detect + GURL("http://wpad/wpad.dat"), // pac_url + ProxyRulesExpectation::Empty(), + }, + { + TEST_DESC(".config/kioslaverc overrides others"), + + // Input. + "[Proxy Settings]\nProxyType=1\nhttpProxy=www.google.com 80\n" + "ReversedException=true\n", + kioslaverc5_, // kioslaverc path + false, // auto_detect + GURL(), // pac_url + ProxyRulesExpectation::PerSchemeWithBypassReversed( + "www.google.com:80", // http + "www.foo.com:80", // https + "", // ftp + "*.google.com,*.kde.org"), // bypass rules, + }, + }; + + // Create directories for all configs + base::CreateDirectory(config_home_); + base::CreateDirectory(config_xdg_home_); + base::CreateDirectory(config_kdedefaults_home_); + + for (size_t i = 0; i < std::size(tests); ++i) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "] %s", i, + tests[i].description.c_str())); + std::unique_ptr env(new MockEnvironment); + env->values.XDG_CURRENT_DESKTOP = "KDE"; + env->values.KDE_SESSION_VERSION = "5"; + env->values.HOME = user_home_.value().c_str(); + env->values.XDG_CONFIG_DIRS = xdg_config_dirs.c_str(); + SyncConfigGetter sync_config_getter(new ProxyConfigServiceLinux( + std::move(env), TRAFFIC_ANNOTATION_FOR_TESTS)); + ProxyConfigWithAnnotation config; + // Write the kioslaverc file to specified location. + base::WriteFile(tests[i].kioslaverc_path, tests[i].kioslaverc); + CHECK(base::PathExists(tests[i].kioslaverc_path)); + sync_config_getter.SetupAndInitialFetch(); + ProxyConfigService::ConfigAvailability availability = + sync_config_getter.SyncGetLatestProxyConfig(&config); + EXPECT_EQ(availability, ProxyConfigService::CONFIG_VALID); + + if (availability == ProxyConfigService::CONFIG_VALID) { + EXPECT_EQ(tests[i].auto_detect, config.value().auto_detect()); + EXPECT_EQ(tests[i].pac_url, config.value().pac_url()); + EXPECT_TRUE(tests[i].proxy_rules.Matches(config.value().proxy_rules())); + } + } +} + } // namespace } // namespace net -- 2.25.4