Linux: get GNOME or KDE proxy settings.
BUG=17363, 20407
TEST=none

Review URL: http://codereview.chromium.org/174327

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@24831 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/proxy/proxy_config_service_linux.cc b/net/proxy/proxy_config_service_linux.cc
index 2998a0c..a3a9993 100644
--- a/net/proxy/proxy_config_service_linux.cc
+++ b/net/proxy/proxy_config_service_linux.cc
@@ -4,13 +4,23 @@
 
 #include "net/proxy/proxy_config_service_linux.h"
 
+#include <errno.h>
+#include <fcntl.h>
 #include <gconf/gconf-client.h>
+#include <limits.h>
+#include <stdio.h>
 #include <stdlib.h>
+#include <sys/inotify.h>
+#include <unistd.h>
 
+#include "base/file_path.h"
+#include "base/file_util.h"
 #include "base/logging.h"
+#include "base/message_loop.h"
 #include "base/string_tokenizer.h"
 #include "base/string_util.h"
 #include "base/task.h"
+#include "base/timer.h"
 #include "googleurl/src/url_canon.h"
 #include "net/base/net_errors.h"
 #include "net/http/http_util.h"
@@ -55,6 +65,10 @@
     host = "socks4://" + host;
   else if (scheme == ProxyServer::SCHEME_SOCKS5)
     host = "socks5://" + host;
+  // If there is a trailing slash, remove it so |host| will parse correctly
+  // even if it includes a port number (since the slash is not numeric).
+  if (host.length() && host[host.length() - 1] == '/')
+    host.resize(host.length() - 1);
   return host;
 }
 
@@ -155,30 +169,16 @@
 
 namespace {
 
-// static
-// gconf notification callback, dispatched from the default
-// glib main loop.
-void OnGConfChangeNotification(
-    GConfClient* client, guint cnxn_id,
-    GConfEntry* entry, gpointer user_data) {
-  // It would be nice to debounce multiple callbacks in quick
-  // succession, since I guess we'll get one for each changed key. As
-  // it is we will read settings from gconf once for each callback.
-  LOG(INFO) << "gconf change notification for key "
-            << gconf_entry_get_key(entry);
-  // We don't track which key has changed, just that something did change.
-  // Forward to a method on the proxy config service delegate object.
-  ProxyConfigServiceLinux::Delegate* config_service_delegate =
-      reinterpret_cast<ProxyConfigServiceLinux::Delegate*>(user_data);
-  config_service_delegate->OnCheckProxyConfigSettings();
-}
+const int kDebounceTimeoutMilliseconds = 250;
 
-class GConfSettingGetterImpl
+// This is the "real" gconf version that actually uses gconf.
+class GConfSettingGetterImplGConf
     : public ProxyConfigServiceLinux::GConfSettingGetter {
  public:
-  GConfSettingGetterImpl() : client_(NULL), loop_(NULL) {}
+  GConfSettingGetterImplGConf()
+      : client_(NULL), notify_delegate_(NULL), loop_(NULL) {}
 
-  virtual ~GConfSettingGetterImpl() {
+  virtual ~GConfSettingGetterImplGConf() {
     // client_ should have been released before now, from
     // Delegate::OnDestroy(), while running on the UI thread. However
     // on exiting the process, it may happen that
@@ -191,20 +191,22 @@
         // We are on the UI thread so we can clean it safely. This is
         // the case at least for ui_tests running under Valgrind in
         // bug 16076.
-        LOG(INFO) << "~GConfSettingGetterImpl: releasing gconf client";
-        Release();
+        LOG(INFO) << "~GConfSettingGetterImplGConf: releasing gconf client";
+        Shutdown();
       } else {
-        LOG(WARNING) << "~GConfSettingGetterImpl: leaking gconf client";
+        LOG(WARNING) << "~GConfSettingGetterImplGConf: leaking gconf client";
         client_ = NULL;
       }
     }
     DCHECK(!client_);
   }
 
-  virtual bool Init() {
+  virtual bool Init(MessageLoop* glib_default_loop,
+                    MessageLoopForIO* file_loop) {
+    DCHECK(MessageLoop::current() == glib_default_loop);
     DCHECK(!client_);
     DCHECK(!loop_);
-    loop_ = MessageLoopForUI::current();
+    loop_ = glib_default_loop;
     client_ = gconf_client_get_default();
     if (!client_) {
       // It's not clear whether/when this can return NULL.
@@ -224,13 +226,13 @@
     if (error != NULL) {
       LOG(ERROR) << "Error requesting gconf directory: " << error->message;
       g_error_free(error);
-      Release();
+      Shutdown();
       return false;
     }
     return true;
   }
 
-  void Release() {
+  void Shutdown() {
     if (client_) {
       DCHECK(MessageLoop::current() == loop_);
       // This also disables gconf notifications.
@@ -240,29 +242,38 @@
     }
   }
 
-  bool SetupNotification(void* callback_user_data) {
+  bool SetupNotification(ProxyConfigServiceLinux::Delegate* delegate) {
     DCHECK(client_);
     DCHECK(MessageLoop::current() == loop_);
     GError* error = NULL;
+    notify_delegate_ = delegate;
     gconf_client_notify_add(
         client_, "/system/proxy",
-        OnGConfChangeNotification, callback_user_data,
+        OnGConfChangeNotification, this,
         NULL, &error);
     if (error == NULL) {
       gconf_client_notify_add(
           client_, "/system/http_proxy",
-          OnGConfChangeNotification, callback_user_data,
+          OnGConfChangeNotification, this,
           NULL, &error);
     }
     if (error != NULL) {
       LOG(ERROR) << "Error requesting gconf notifications: " << error->message;
       g_error_free(error);
-      Release();
+      Shutdown();
       return false;
     }
     return true;
   }
 
+  MessageLoop* GetNotificationLoop() {
+    return loop_;
+  }
+
+  virtual const char* GetDataSource() {
+    return "gconf";
+  }
+
   virtual bool GetString(const char* key, std::string* result) {
     DCHECK(client_);
     DCHECK(MessageLoop::current() == loop_);
@@ -345,14 +356,451 @@
     return false;
   }
 
+  // This is the callback from the debounce timer.
+  void OnDebouncedNotification() {
+    DCHECK(MessageLoop::current() == loop_);
+    DCHECK(notify_delegate_);
+    // Forward to a method on the proxy config service delegate object.
+    notify_delegate_->OnCheckProxyConfigSettings();
+  }
+
+  void OnChangeNotification() {
+    // We don't use Reset() because the timer may not yet be running.
+    // (In that case Stop() is a no-op.)
+    debounce_timer_.Stop();
+    debounce_timer_.Start(base::TimeDelta::FromMilliseconds(
+        kDebounceTimeoutMilliseconds), this,
+        &GConfSettingGetterImplGConf::OnDebouncedNotification);
+  }
+
+  // gconf notification callback, dispatched from the default glib main loop.
+  static void OnGConfChangeNotification(
+      GConfClient* client, guint cnxn_id,
+      GConfEntry* entry, gpointer user_data) {
+    LOG(INFO) << "gconf change notification for key "
+              << gconf_entry_get_key(entry);
+    // We don't track which key has changed, just that something did change.
+    GConfSettingGetterImplGConf* setting_getter =
+        reinterpret_cast<GConfSettingGetterImplGConf*>(user_data);
+    setting_getter->OnChangeNotification();
+  }
+
   GConfClient* client_;
+  ProxyConfigServiceLinux::Delegate* notify_delegate_;
+  base::OneShotTimer<GConfSettingGetterImplGConf> debounce_timer_;
 
   // Message loop of the thread that we make gconf calls on. It should
   // be the UI thread and all our methods should be called on this
   // thread. Only for assertions.
   MessageLoop* loop_;
 
-  DISALLOW_COPY_AND_ASSIGN(GConfSettingGetterImpl);
+  DISALLOW_COPY_AND_ASSIGN(GConfSettingGetterImplGConf);
+};
+
+// This is the KDE version that reads kioslaverc and simulates gconf.
+// Doing this allows the main Delegate code, as well as the unit tests
+// for it, to stay the same - and the settings map fairly well besides.
+class GConfSettingGetterImplKDE
+    : public ProxyConfigServiceLinux::GConfSettingGetter,
+      public base::MessagePumpLibevent::Watcher {
+ public:
+  explicit GConfSettingGetterImplKDE(
+      base::EnvironmentVariableGetter* env_var_getter)
+      : inotify_fd_(-1), notify_delegate_(NULL), indirect_manual_(false),
+        auto_no_pac_(false), reversed_exception_(false), file_loop_(NULL) {
+    // We don't save the env var getter for later use since we don't own it.
+    // Instead we use it here and save the result we actually care about.
+    std::string kde_home;
+    if (!env_var_getter->Getenv("KDE_HOME", &kde_home)) {
+      if (!env_var_getter->Getenv("HOME", &kde_home))
+        // User has no $HOME? Give up. Later we'll report the failure.
+        return;
+      kde_home = FilePath(kde_home).Append(FILE_PATH_LITERAL(".kde")).value();
+    }
+    kde_config_dir_ = FilePath(kde_home).Append(
+        FILE_PATH_LITERAL("share")).Append(FILE_PATH_LITERAL("config"));
+  }
+
+  virtual ~GConfSettingGetterImplKDE() {
+    // inotify_fd_ should have been closed before now, from
+    // Delegate::OnDestroy(), while running on the file thread. However
+    // on exiting the process, it may happen that Delegate::OnDestroy()
+    // task is left pending on the file loop after the loop was quit,
+    // and pending tasks may then be deleted without being run.
+    // Here in the KDE version, we can safely close the file descriptor
+    // anyway. (Not that it really matters; the process is exiting.)
+    if (inotify_fd_ >= 0)
+      Shutdown();
+    DCHECK(inotify_fd_ < 0);
+  }
+
+  virtual bool Init(MessageLoop* glib_default_loop,
+                    MessageLoopForIO* file_loop) {
+    DCHECK(inotify_fd_ < 0);
+    inotify_fd_ = inotify_init();
+    if (inotify_fd_ < 0) {
+      LOG(ERROR) << "inotify_init failed: " << strerror(errno);
+      return false;
+    }
+    int flags = fcntl(inotify_fd_, F_GETFL);
+    if (fcntl(inotify_fd_, F_SETFL, flags | O_NONBLOCK) < 0) {
+      LOG(ERROR) << "fcntl failed: " << strerror(errno);
+      close(inotify_fd_);
+      inotify_fd_ = -1;
+      return false;
+    }
+    file_loop_ = file_loop;
+    // The initial read is done on the current thread, not |file_loop_|,
+    // since we will need to have it for SetupAndFetchInitialConfig().
+    UpdateCachedSettings();
+    return true;
+  }
+
+  void Shutdown() {
+    if (inotify_fd_ >= 0) {
+      ResetCachedSettings();
+      inotify_watcher_.StopWatchingFileDescriptor();
+      close(inotify_fd_);
+      inotify_fd_ = -1;
+    }
+  }
+
+  bool SetupNotification(ProxyConfigServiceLinux::Delegate* delegate) {
+    DCHECK(inotify_fd_ >= 0);
+    DCHECK(file_loop_);
+    // We can't just watch the kioslaverc file directly, since KDE will write
+    // a new copy of it and then rename it whenever settings are changed and
+    // inotify watches inodes (so we'll be watching the old deleted file after
+    // 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.
+    if (inotify_add_watch(inotify_fd_, kde_config_dir_.value().c_str(),
+                          IN_MODIFY | IN_MOVED_TO) < 0)
+      return false;
+    notify_delegate_ = delegate;
+    return file_loop_->WatchFileDescriptor(inotify_fd_, true,
+        MessageLoopForIO::WATCH_READ, &inotify_watcher_, this);
+  }
+
+  MessageLoop* GetNotificationLoop() {
+    return file_loop_;
+  }
+
+  // Implement base::MessagePumpLibevent::Delegate.
+  void OnFileCanReadWithoutBlocking(int fd) {
+    DCHECK(fd == inotify_fd_);
+    DCHECK(MessageLoop::current() == file_loop_);
+    OnChangeNotification();
+  }
+  void OnFileCanWriteWithoutBlocking(int fd) {
+    NOTREACHED();
+  }
+
+  virtual const char* GetDataSource() {
+    return "KDE";
+  }
+
+  virtual bool GetString(const char* key, std::string* result) {
+    string_map_type::iterator it = string_table_.find(key);
+    if (it == string_table_.end())
+      return false;
+    *result = it->second;
+    return true;
+  }
+  virtual bool GetBoolean(const char* key, bool* result) {
+    // We don't ever have any booleans.
+    return false;
+  }
+  virtual bool GetInt(const char* key, int* result) {
+    // We don't ever have any integers. (See AddProxy() below about ports.)
+    return false;
+  }
+  virtual bool GetStringList(const char* key,
+                             std::vector<std::string>* result) {
+    strings_map_type::iterator it = strings_table_.find(key);
+    if (it == strings_table_.end())
+      return false;
+    *result = it->second;
+    return true;
+  }
+
+ private:
+  void ResetCachedSettings() {
+    string_table_.clear();
+    strings_table_.clear();
+    indirect_manual_ = false;
+    auto_no_pac_ = false;
+    reversed_exception_ = false;
+  }
+
+  void AddProxy(std::string prefix, std::string value) {
+    if (value.empty() || value.substr(0, 3) == "//:")
+      // No proxy.
+      return;
+    // We don't need to parse the port number out; GetProxyFromGConf()
+    // would only append it right back again. So we just leave the port
+    // number right in the host string.
+    string_table_[prefix + "host"] = value;
+  }
+
+  void AddKDESetting(std::string key, const char* value) {
+    // The astute reader may notice that there is no mention of SOCKS
+    // here. That's because KDE handles socks is a strange way, and we
+    // don't support it. Rather than just a setting for the SOCKS server,
+    // it has a setting for a library to LD_PRELOAD in all your programs
+    // that will transparently SOCKSify them. Such libraries each have
+    // their own configuration, and thus, we can't get it from KDE.
+    if (key == "ProxyType") {
+      const char* mode = "none";
+      indirect_manual_ = false;
+      auto_no_pac_ = false;
+      switch (StringToInt(value)) {
+        case 0:  // No proxy, or maybe kioslaverc syntax error.
+          break;
+        case 1:  // Manual configuration.
+          mode = "manual";
+          break;
+        case 2:  // PAC URL.
+          mode = "auto";
+          break;
+        case 3:  // WPAD.
+          mode = "auto";
+          auto_no_pac_ = true;
+          break;
+        case 4:  // Indirect manual via environment variables.
+          mode = "manual";
+          indirect_manual_ = true;
+          break;
+      }
+      string_table_["/system/proxy/mode"] = mode;
+    } else if (key == "Proxy Config Script") {
+      string_table_["/system/proxy/autoconfig_url"] = value;
+    } else if (key == "httpProxy") {
+      AddProxy("/system/http_proxy/", value);
+    } else if (key == "httpsProxy") {
+      AddProxy("/system/proxy/secure_", value);
+    } else if (key == "ftpProxy") {
+      AddProxy("/system/proxy/ftp_", value);
+    } else if (key == "ReversedException") {
+      // We count "true" or any nonzero number as true, otherwise false.
+      // Note that if the value is not actually numeric StringToInt()
+      // will return 0, which we count as false.
+      reversed_exception_ = !strcmp(value, "true") || StringToInt(value);
+    } else if (key == "NoProxyFor") {
+      std::vector<std::string> exceptions;
+      StringTokenizer tk(value, ",");
+      while (tk.GetNext()) {
+        std::string token = tk.token();
+        if (!token.empty())
+          exceptions.push_back(token);
+      }
+      strings_table_["/system/http_proxy/ignore_hosts"] = exceptions;
+    } else if (key == "AuthMode") {
+      // Check for authentication, just so we can warn.
+      int mode = StringToInt(value);
+      if (mode) {
+        // ProxyConfig does not support authentication parameters, but
+        // Chrome will prompt for the password later. So we ignore this.
+        LOG(WARNING) <<
+            "Proxy authentication parameters ignored, see bug 16709";
+      }
+    }
+  }
+
+  void ResolveIndirect(std::string key) {
+    // We can't save the environment variable getter that was passed
+    // when this object was constructed, but this setting is likely
+    // to be pretty unusual and the actual values it would return can
+    // be tested without using it. So we just use getenv() here.
+    string_map_type::iterator it = string_table_.find(key);
+    if (it != string_table_.end()) {
+      char* value = getenv(it->second.c_str());
+      if (value)
+        it->second = value;
+    }
+  }
+
+  // The settings in kioslaverc could occur in any order, but some affect
+  // others. Rather than read the whole file in and then query them in an
+  // order that allows us to handle that, we read the settings in whatever
+  // order they occur and do any necessary tweaking after we finish.
+  void ResolveModeEffects() {
+    if (indirect_manual_) {
+      ResolveIndirect("/system/http_proxy/host");
+      ResolveIndirect("/system/proxy/secure_host");
+      ResolveIndirect("/system/proxy/ftp_host");
+    }
+    if (auto_no_pac_) {
+      // Remove the PAC URL; we're not supposed to use it.
+      string_table_.erase("/system/proxy/autoconfig_url");
+    }
+    if (reversed_exception_) {
+      // We don't actually support this setting. (It means to use the proxy
+      // *only* for the exception list, rather than everything but them.)
+      // Nevertheless we can do better than *exactly the opposite* of the
+      // desired behavior by clearing the exception list and warning.
+      strings_table_.erase("/system/http_proxy/ignore_hosts");
+      LOG(WARNING) << "KDE reversed proxy exception list not supported";
+    }
+  }
+
+  // Reads kioslaverc one line at a time and calls AddKDESetting() to add
+  // each relevant name-value pair to the appropriate value table.
+  void UpdateCachedSettings() {
+    FilePath kioslaverc = kde_config_dir_.Append(
+        FILE_PATH_LITERAL("kioslaverc"));
+    file_util::ScopedFILE input(file_util::OpenFile(kioslaverc, "r"));
+    if (!input.get())
+      return;
+    ResetCachedSettings();
+    bool in_proxy_settings = false;
+    bool line_too_long = false;
+    char line[512];
+    // feof() and ferror() only tell you if you have hit EOF or an error
+    // after you try to read some data that encounters that condition. So,
+    // the initialize statement of this loop is the same as the update
+    // statement. We need to start out each iteration with fgets().
+    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;
+        continue;
+      }
+      // 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* value = strchr(line, '=');
+        if (!value)
+          continue;
+        // The length of the value name.
+        length = value - line;
+        if (!length)
+          continue;
+        // Is the value name localized?
+        if (line[length - 1] == ']') {
+          // Find the matching '['.
+          for (; length && line[length - 1] != '['; --length);
+          if (!length)
+            continue;
+          // Trim the localization indicator off.
+          line[length - 1] = '\0';
+        }
+        // Split the string on the = sign, and advance |value| to the value.
+        *(value++) = '\0';
+        // Now fill in the tables.
+        AddKDESetting(line, value);
+      }
+    }
+    if (ferror(input.get()))
+      LOG(ERROR) << "error reading " << kioslaverc.value();
+    ResolveModeEffects();
+  }
+
+  // This is the callback from the debounce timer.
+  void OnDebouncedNotification() {
+    DCHECK(MessageLoop::current() == file_loop_);
+    LOG(INFO) << "inotify change notification for kioslaverc";
+    UpdateCachedSettings();
+    DCHECK(notify_delegate_);
+    // Forward to a method on the proxy config service delegate object.
+    notify_delegate_->OnCheckProxyConfigSettings();
+  }
+
+  // Called by OnFileCanReadWithoutBlocking() on the file thread. Reads
+  // from the inotify file descriptor and starts up a debounce timer if
+  // an event for kioslaverc is seen.
+  void OnChangeNotification() {
+    DCHECK(inotify_fd_ >= 0);
+    DCHECK(MessageLoop::current() == file_loop_);
+    char event_buf[(sizeof(inotify_event) + NAME_MAX + 1) * 4];
+    bool kioslaverc_touched = false;
+    ssize_t r;
+    while ((r = read(inotify_fd_, event_buf, sizeof(event_buf))) > 0) {
+      // inotify returns variable-length structures, which is why we have
+      // this strange-looking loop instead of iterating through an array.
+      char* event_ptr = event_buf;
+      while (event_ptr < event_buf + r) {
+        inotify_event* event = reinterpret_cast<inotify_event*>(event_ptr);
+        // The kernel always feeds us whole events.
+        CHECK(event_ptr + sizeof(inotify_event) <= event_buf + r);
+        CHECK(event->name + event->len <= event_buf + r);
+        if (!strcmp(event->name, "kioslaverc"))
+          kioslaverc_touched = true;
+        // Advance the pointer just past the end of the filename.
+        event_ptr = event->name + event->len;
+      }
+      // We keep reading even if |kioslaverc_touched| is true to drain the
+      // inotify event queue.
+    }
+    if (!r)
+      // Instead of returning -1 and setting errno to EINVAL if there is not
+      // enough buffer space, older kernels (< 2.6.21) return 0. Simulate the
+      // new behavior (EINVAL) so we can reuse the code below.
+      errno = EINVAL;
+    if (errno != EAGAIN) {
+      LOG(WARNING) << "error reading inotify file descriptor: "
+                   << strerror(errno);
+      if (errno == EINVAL) {
+        // Our buffer is not large enough to read the next event. This should
+        // not happen (because its size is calculated to always be sufficiently
+        // large), but if it does we'd warn continuously since |inotify_fd_|
+        // would be forever ready to read. Close it and stop watching instead.
+        LOG(ERROR) << "inotify failure; no longer watching kioslaverc!";
+        inotify_watcher_.StopWatchingFileDescriptor();
+        close(inotify_fd_);
+        inotify_fd_ = -1;
+      }
+    }
+    if (kioslaverc_touched) {
+      // We don't use Reset() because the timer may not yet be running.
+      // (In that case Stop() is a no-op.)
+      debounce_timer_.Stop();
+      debounce_timer_.Start(base::TimeDelta::FromMilliseconds(
+          kDebounceTimeoutMilliseconds), this,
+          &GConfSettingGetterImplKDE::OnDebouncedNotification);
+    }
+  }
+
+  typedef std::map<std::string, std::string> string_map_type;
+  typedef std::map<std::string, std::vector<std::string> > strings_map_type;
+
+  int inotify_fd_;
+  base::MessagePumpLibevent::FileDescriptorWatcher inotify_watcher_;
+  ProxyConfigServiceLinux::Delegate* notify_delegate_;
+  base::OneShotTimer<GConfSettingGetterImplKDE> debounce_timer_;
+  FilePath kde_config_dir_;
+  bool indirect_manual_;
+  bool auto_no_pac_;
+  bool reversed_exception_;
+
+  // We cache these settings whenever we re-read the kioslaverc file.
+  string_map_type string_table_;
+  strings_map_type strings_table_;
+
+  // Message loop of the file thread, for reading kioslaverc. If NULL,
+  // just read it directly (for testing). We also handle inotify events
+  // on this thread.
+  MessageLoopForIO* file_loop_;
+
+  DISALLOW_COPY_AND_ASSIGN(GConfSettingGetterImplKDE);
 };
 
 }  // namespace
@@ -367,7 +815,7 @@
     return false;
   }
   // Check for an optional port.
-  int port;
+  int port = 0;
   gconf_getter_->GetInt((key + "port").c_str(), &port);
   if (port != 0) {
     // If a port is set and non-zero:
@@ -478,7 +926,7 @@
   }
 
   // Check for authentication, just so we can warn.
-  bool use_auth;
+  bool use_auth = false;
   gconf_getter_->GetBoolean("/system/http_proxy/use_authentication",
                             &use_auth);
   if (use_auth) {
@@ -498,6 +946,24 @@
 }
 
 ProxyConfigServiceLinux::Delegate::Delegate(
+    base::EnvironmentVariableGetter* env_var_getter)
+    : env_var_getter_(env_var_getter),
+      glib_default_loop_(NULL), io_loop_(NULL) {
+  // Figure out which GConfSettingGetterImpl to use, if any.
+  switch (base::GetDesktopEnvironment(env_var_getter)) {
+    case base::DESKTOP_ENVIRONMENT_GNOME:
+      gconf_getter_.reset(new GConfSettingGetterImplGConf());
+      break;
+    case base::DESKTOP_ENVIRONMENT_KDE3:
+    case base::DESKTOP_ENVIRONMENT_KDE4:
+      gconf_getter_.reset(new GConfSettingGetterImplKDE(env_var_getter));
+      break;
+    case base::DESKTOP_ENVIRONMENT_OTHER:
+      break;
+  }
+}
+
+ProxyConfigServiceLinux::Delegate::Delegate(
     base::EnvironmentVariableGetter* env_var_getter,
     GConfSettingGetter* gconf_getter)
     : env_var_getter_(env_var_getter), gconf_getter_(gconf_getter),
@@ -505,18 +971,19 @@
 }
 
 void ProxyConfigServiceLinux::Delegate::SetupAndFetchInitialConfig(
-    MessageLoop* glib_default_loop, MessageLoop* io_loop) {
+    MessageLoop* glib_default_loop, MessageLoop* io_loop,
+    MessageLoopForIO* file_loop) {
   // We should be running on the default glib main loop thread right
   // now. gconf can only be accessed from this thread.
   DCHECK(MessageLoop::current() == glib_default_loop);
   glib_default_loop_ = glib_default_loop;
   io_loop_ = io_loop;
 
-  // If we are passed a NULL io_loop, then we don't set up gconf
-  // notifications. This should not be the usual case but is intended
-  // to simplify test setups.
-  if (!io_loop_)
-    LOG(INFO) << "Monitoring of gconf setting changes is disabled";
+  // If we are passed a NULL io_loop or file_loop, then we don't set up
+  // proxy setting change notifications. This should not be the usual
+  // case but is intended to simplify test setups.
+  if (!io_loop_ || !file_loop)
+    LOG(INFO) << "Monitoring of proxy setting changes is disabled";
 
   // Fetch and cache the current proxy config. The config is left in
   // cached_config_, where GetProxyConfig() running on the IO thread
@@ -531,36 +998,27 @@
   // mislead us.
 
   bool got_config = false;
-  switch (base::GetDesktopEnvironment(env_var_getter_.get())) {
-    case base::DESKTOP_ENVIRONMENT_GNOME:
-      if (gconf_getter_->Init() &&
-          (!io_loop || gconf_getter_->SetupNotification(this))) {
-        if (GetConfigFromGConf(&cached_config_)) {
-          cached_config_.set_id(1);  // mark it as valid
-          got_config = true;
-          LOG(INFO) << "Obtained proxy setting from gconf";
-          // If gconf proxy mode is "none", meaning direct, then we take
-          // that to be a valid config and will not check environment
-          // variables.  The alternative would have been to look for a proxy
-          // where ever we can find one.
-          //
-          // Keep a copy of the config for use from this thread for
-          // comparison with updated settings when we get notifications.
-          reference_config_ = cached_config_;
-          reference_config_.set_id(1);  // mark it as valid
-        } else {
-          gconf_getter_->Release();  // Stop notifications
-        }
+  if (gconf_getter_.get()) {
+    if (gconf_getter_->Init(glib_default_loop, file_loop) &&
+        (!io_loop || !file_loop || gconf_getter_->SetupNotification(this))) {
+      if (GetConfigFromGConf(&cached_config_)) {
+        cached_config_.set_id(1);  // mark it as valid
+        got_config = true;
+        LOG(INFO) << "Obtained proxy settings from " <<
+            gconf_getter_->GetDataSource();
+        // If gconf proxy mode is "none", meaning direct, then we take
+        // that to be a valid config and will not check environment
+        // variables. The alternative would have been to look for a proxy
+        // whereever we can find one.
+        //
+        // Keep a copy of the config for use from this thread for
+        // comparison with updated settings when we get notifications.
+        reference_config_ = cached_config_;
+        reference_config_.set_id(1);  // mark it as valid
+      } else {
+        gconf_getter_->Shutdown();  // Stop notifications
       }
-      break;
-
-    case base::DESKTOP_ENVIRONMENT_KDE3:
-    case base::DESKTOP_ENVIRONMENT_KDE4:
-      NOTIMPLEMENTED() << "Bug 17363: obey KDE proxy settings.";
-      break;
-
-    case base::DESKTOP_ENVIRONMENT_OTHER:
-      break;
+    }
   }
 
   if (!got_config) {
@@ -571,14 +1029,14 @@
     // work.
     if (GetConfigFromEnv(&cached_config_)) {
       cached_config_.set_id(1);  // mark it as valid
-      LOG(INFO) << "Obtained proxy setting from environment variables";
+      LOG(INFO) << "Obtained proxy settings from environment variables";
     }
   }
 }
 
 void ProxyConfigServiceLinux::Delegate::Reset() {
   DCHECK(!glib_default_loop_ || MessageLoop::current() == glib_default_loop_);
-  gconf_getter_->Release();
+  gconf_getter_->Shutdown();
   cached_config_ = ProxyConfig();
 }
 
@@ -592,11 +1050,11 @@
   return cached_config_.is_valid() ? OK : ERR_FAILED;
 }
 
+// Depending on the GConfSettingGetter in use, this method will be called
+// on either the UI thread (GConf) or the file thread (KDE).
 void ProxyConfigServiceLinux::Delegate::OnCheckProxyConfigSettings() {
-  // This should be dispatched from the thread with the default glib
-  // main loop, which allows us to access gconf.
-  DCHECK(MessageLoop::current() == glib_default_loop_);
-
+  MessageLoop* required_loop = gconf_getter_->GetNotificationLoop();
+  DCHECK(!required_loop || MessageLoop::current() == required_loop);
   ProxyConfig new_config;
   bool valid = GetConfigFromGConf(&new_config);
   if (valid)
@@ -626,15 +1084,17 @@
 }
 
 void ProxyConfigServiceLinux::Delegate::PostDestroyTask() {
-  if (MessageLoop::current() == glib_default_loop_) {
+  if (!gconf_getter_.get())
+    return;
+  MessageLoop* shutdown_loop = gconf_getter_->GetNotificationLoop();
+  if (!shutdown_loop || MessageLoop::current() == shutdown_loop) {
     // Already on the right thread, call directly.
     // This is the case for the unittests.
     OnDestroy();
   } else {
-    // Post to UI thread. Note that on browser shutdown, we may quit
-    // the UI MessageLoop and exit the program before ever running
-    // this.
-    glib_default_loop_->PostTask(
+    // Post to shutdown thread. Note that on browser shutdown, we may quit
+    // this MessageLoop and exit the program before ever running this.
+    shutdown_loop->PostTask(
         FROM_HERE,
         NewRunnableMethod(
             this,
@@ -642,13 +1102,13 @@
   }
 }
 void ProxyConfigServiceLinux::Delegate::OnDestroy() {
-  DCHECK(!glib_default_loop_ || MessageLoop::current() == glib_default_loop_);
-  gconf_getter_->Release();
+  MessageLoop* shutdown_loop = gconf_getter_->GetNotificationLoop();
+  DCHECK(!shutdown_loop || MessageLoop::current() == shutdown_loop);
+  gconf_getter_->Shutdown();
 }
 
 ProxyConfigServiceLinux::ProxyConfigServiceLinux()
-    : delegate_(new Delegate(base::EnvironmentVariableGetter::Create(),
-                             new GConfSettingGetterImpl())) {
+    : delegate_(new Delegate(base::EnvironmentVariableGetter::Create())) {
 }
 
 ProxyConfigServiceLinux::ProxyConfigServiceLinux(