Extend local auth server to support OAuth login flow

The local auth server currently supports legacy ClientLogin only. This CL
adds support for the new OAuth-based Service login. This will allow the
local auth server to be used in PyAuto tests that require OAuth so that
policy can be fetched.

BUG=chromium-os:32036
TEST=Login against local auth server without --skip-oauth-login works
TEST=remote_tests.sh suite:smoke succeeds

Change-Id: I89f1154be7f7c52a941726fed8cd010a0ef399c9
Reviewed-on: https://gerrit.chromium.org/gerrit/24501
Reviewed-by: Bartosz Fabianowski <bartfab@chromium.org>
Tested-by: Bartosz Fabianowski <bartfab@chromium.org>
Commit-Ready: Bartosz Fabianowski <bartfab@chromium.org>
diff --git a/client/cros/dns_server.py b/client/cros/dns_server.py
index 90c2f9a..2ee1036 100644
--- a/client/cros/dns_server.py
+++ b/client/cros/dns_server.py
@@ -2,15 +2,28 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import logging, threading, time
+import dbus, logging, os, socket, stat, sys, threading, time
+from dbus.mainloop.glib import DBusGMainLoop
 
-import common
+import common, constants, flimflam_test_path
 from autotest_lib.client.bin import utils
+import flimflam  # Requires flimflam_test_path to be imported first.
 
 class LocalDns(object):
-    """a wrapper around miniFakeDns that handles managing running the server
-    in a separate thread.
+    """A wrapper around miniFakeDns that runs the server in a separate thread
+    and redirects all DNS queries to it.
     """
+    # This is a symlink.  We look up the real path at runtime by following it.
+    _resolv_bak_file = 'resolv.conf.bak'
+
+    def __connect_to_flimflam(self):
+        """Connect to the network manager via DBus.
+
+        Stores dbus connection in self._flim upon success, throws on failure.
+        """
+        self._bus_loop = DBusGMainLoop(set_as_default=True)
+        self._system_bus = dbus.SystemBus(mainloop=self._bus_loop)
+        self._flim = flimflam.FlimFlam(self._system_bus)
 
     def __init__(self, fake_ip="127.0.0.1", local_port=53):
         import miniFakeDns  # So we don't need to install it in the chroot.
@@ -18,12 +31,106 @@
         self._stopper = threading.Event()
         self._thread = threading.Thread(target=self._dns.run,
                                         args=(self._stopper,))
+        self.__connect_to_flimflam()
 
+    def __get_host_by_name(self, hostname):
+        """Resolve the dotted-quad IPv4 address of |hostname|
+
+        This used to use suave python code, like this:
+            hosts = socket.getaddrinfo(hostname, 80, socket.AF_INET)
+            (fam, socktype, proto, canonname, (host, port)) = hosts[0]
+            return host
+
+        But that hangs sometimes, and we don't understand why.  So, use
+        a subprocess with a timeout.
+        """
+        try:
+            host = utils.system_output('%s -c "import socket; '
+                                       'print socket.gethostbyname(\'%s\')"' % (
+                                       sys.executable, hostname),
+                                       ignore_status=True, timeout=2)
+        except Exception as e:
+            logging.warning(e)
+            return None
+        return host or None
+
+    def __attempt_resolve(self, hostname, ip, expected=True):
+        logging.debug('Attempting to resolve %s to %s' % (hostname, ip))
+        host = self.__get_host_by_name(hostname)
+        logging.debug('Resolve attempt for %s got %s' % (hostname, host))
+        return host and (host == ip) == expected
 
     def run(self):
+        """Start the mock DNS server and redirect all queries to it."""
         self._thread.start()
+        # Turn off captive portal checking, until we fix
+        # http://code.google.com/p/chromium-os/issues/detail?id=19640
+        self.check_portal_list = self._flim.GetCheckPortalList()
+        self._flim.SetCheckPortalList('')
+        # Redirect all DNS queries to the mock DNS server.
+        try:
+            # Follow resolv.conf symlink.
+            resolv = os.path.realpath(constants.RESOLV_CONF_FILE)
+            # Grab path to the real file, do following work in that directory.
+            resolv_dir = os.path.dirname(resolv)
+            resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file)
+            resolv_contents = 'nameserver 127.0.0.1'
+            # Test to make sure the current resolv.conf isn't already our
+            # specially modified version.  If this is the case, we have
+            # probably been interrupted while in the middle of this test
+            # in a previous run.  The last thing we want to do at this point
+            # is to overwrite a legitimate backup.
+            if (utils.read_one_line(resolv) == resolv_contents and
+                os.path.exists(resolv_bak)):
+                logging.error('Current resolv.conf is setup for our local '
+                              'server, and a backup already exists!  '
+                              'Skipping the backup step.')
+            else:
+                # Back up the current resolv.conf.
+                os.rename(resolv, resolv_bak)
+            # To stop flimflam from editing resolv.conf while we're working
+            # with it, we want to make the directory -r-xr-xr-x.  Open an
+            # fd to the file first, so that we'll retain the ability to
+            # alter it.
+            resolv_fd = open(resolv, 'w')
+            self._resolv_dir_mode = os.stat(resolv_dir).st_mode
+            os.chmod(resolv_dir, (stat.S_IRUSR | stat.S_IXUSR |
+                                  stat.S_IRGRP | stat.S_IXGRP |
+                                  stat.S_IROTH | stat.S_IXOTH))
+            resolv_fd.write(resolv_contents)
+            resolv_fd.close()
+            assert utils.read_one_line(resolv) == resolv_contents
+        except Exception as e:
+            logging.error(str(e))
+            raise e
 
+        utils.poll_for_condition(
+            lambda: self.__attempt_resolve('www.google.com.', '127.0.0.1'),
+            utils.TimeoutError('Timed out waiting for DNS changes.'),
+            timeout=10)
 
     def stop(self):
-        self._stopper.set()
-        self._thread.join()
+        """Restore the backed-up DNS settings and stop the mock DNS server."""
+        try:
+            # Follow resolv.conf symlink.
+            resolv = os.path.realpath(constants.RESOLV_CONF_FILE)
+            # Grab path to the real file, do following work in that directory.
+            resolv_dir = os.path.dirname(resolv)
+            resolv_bak = os.path.join(resolv_dir, self._resolv_bak_file)
+            os.chmod(resolv_dir, self._resolv_dir_mode)
+            os.rename(resolv_bak, resolv)
+
+            utils.poll_for_condition(
+                lambda: self.__attempt_resolve('www.google.com.',
+                                               '127.0.0.1',
+                                               expected=False),
+                utils.TimeoutError('Timed out waiting to revert DNS.  '
+                                   'resolv.conf contents are: ' +
+                                   utils.read_one_line(resolv)),
+                timeout=10)
+        finally:
+            # Set captive portal checking to whatever it was at the start.
+            self._flim.SetCheckPortalList(self.check_portal_list)
+            # Stop the DNS server.
+            self._stopper.set()
+            self._thread.join()