Home | History | Annotate | Download | only in network_ProxyResolver
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.;
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 #
      5 # This is an integration test which ensures that a proxy set on a
      6 # shared network connection is exposed via LibCrosSevice and used
      7 # by tlsdated during time synchronization.
      8 
      9 import dbus
     10 import gobject
     11 import logging
     12 import subprocess
     13 import threading
     14 import time
     15 
     16 from autotest_lib.client.bin import test, utils
     17 from autotest_lib.client.common_lib import error
     18 from autotest_lib.client.cros import cros_ui
     19 from autotest_lib.client.cros.networking import shill_proxy
     20 
     21 from dbus.mainloop.glib import DBusGMainLoop
     22 from SocketServer import ThreadingTCPServer, StreamRequestHandler
     23 
     24 class ProxyHandler(StreamRequestHandler):
     25     """Matching request handler for the ThreadedHitServer
     26        that notes when an expected request is seen.
     27     """
     28     wbufsize = -1
     29     def handle(self):
     30         """Reads the first line, up to 40 characters, looking
     31            for the CONNECT string that tlsdated sends. If it
     32            is found, the server's hit() method is called.
     33 
     34            All requests receive a HTTP 504 error.
     35         """
     36         # Read up to 40 characters
     37         data = self.rfile.readline(40).strip()
     38         logging.info('ProxyHandler::handle(): <%s>', data)
     39         # TODO(wad) Add User-agent check when it lands in tlsdate.
     40         # Also, abstract the time server and move this code into cros/.
     41         if data.__contains__('CONNECT clients3.google.com:443 HTTP/1.1'):
     42           self.server.hit()
     43         self.wfile.write("HTTP/1.1 504 Gateway Timeout\r\n" +
     44                          "Connection: close\r\n\r\n")
     45 
     46 class ThreadedHitServer(ThreadingTCPServer):
     47     """A threaded TCP server which services requests
     48        and allows the handler to track "hits".
     49     """
     50     def __init__(self, server_address, HandlerClass):
     51         """Constructor
     52 
     53         @param server_address: tuple of server IP and port to listen on.
     54         @param HandlerClass: the RequestHandler class to instantiate per req.
     55         """
     56         self._hits = 0
     57         ThreadingTCPServer.__init__(self, server_address, HandlerClass)
     58 
     59     def hit(self):
     60         """Increment the hit count. Usually called by the HandlerClass"""
     61         self._hits += 1
     62 
     63     def reset_hits(self):
     64         """Set the hit count to 0"""
     65         self._hits = 0
     66 
     67     def hits(self):
     68         """Get the number of matched requests
     69         @return the count of matched requests
     70         """
     71         return self._hits
     72 
     73 class ProxyListener(object):
     74     """A fake listener for tracking if an expected CONNECT request is
     75        seen at the provided server address. Any hits are exposed to be
     76        consumed by the caller.
     77     """
     78     def __init__(self, server_address):
     79         """Constructor
     80 
     81         @param server_address: tuple of server IP and port to listen on.
     82         """
     83         self._server = ThreadedHitServer(server_address, ProxyHandler)
     84         self._thread = threading.Thread(target=self._server.serve_forever)
     85 
     86     def run(self):
     87         """Run the server on a thread"""
     88         self._thread.start()
     89 
     90     def stop(self):
     91         """Stop the server and its threads"""
     92         self._server.shutdown()
     93         self._server.socket.close()
     94         self._thread.join()
     95 
     96     def reset_hits(self):
     97         """Reset the number of matched requests to 0"""
     98         return self._server.reset_hits()
     99 
    100     def hits(self):
    101         """Get the number of matched requests
    102         @return the count of matched requests
    103         """
    104         return self._server.hits()
    105 
    106 class SignalListener(object):
    107     """A class to listen for a DBus signal
    108     """
    109     DEFAULT_TIMEOUT = 60
    110     _main_loop = None
    111     _signals = { }
    112 
    113     def __init__(self, g_main_loop):
    114         """Constructor
    115 
    116         @param g_mail_loop: glib main loop object.
    117         """
    118         self._main_loop = g_main_loop
    119 
    120 
    121     def listen_for_signal(self, signal, interface, path):
    122         """Listen with a default handler
    123         @param signal: signal name to listen for
    124         @param interface: DBus interface to expect it from
    125         @param path: DBus path associated with the signal
    126         """
    127         self.__listen_to_signal(self.__handle_signal, signal, interface, path)
    128 
    129 
    130     def wait_for_signals(self, desc,
    131                          timeout=DEFAULT_TIMEOUT):
    132         """Block for |timeout| seconds waiting for the signals to come in.
    133 
    134         @param desc: string describing the high-level reason you're waiting
    135                      for the signals.
    136         @param timeout: maximum seconds to wait for the signals.
    137 
    138         @raises TimeoutError if the timeout is hit.
    139         """
    140         utils.poll_for_condition(
    141             condition=lambda: self.__received_signals(),
    142             desc=desc,
    143             timeout=self.DEFAULT_TIMEOUT)
    144         all_signals = self._signals.copy()
    145         self.__reset_signal_state()
    146         return all_signals
    147 
    148 
    149     def __received_signals(self):
    150         """Run main loop until all pending events are done, checks for signals.
    151 
    152         Runs self._main_loop until it says it has no more events pending,
    153         then returns the state of the internal variables tracking whether
    154         desired signals have been received.
    155 
    156         @return True if both signals have been handled, False otherwise.
    157         """
    158         context = self._main_loop.get_context()
    159         while context.iteration(False):
    160             pass
    161         return len(self._signals) > 0
    162 
    163 
    164     def __reset_signal_state(self):
    165         """Resets internal signal tracking state."""
    166         self._signals = { }
    167 
    168 
    169     def __listen_to_signal(self, callback, signal, interface, path):
    170         """Connect a callback to a given session_manager dbus signal.
    171 
    172         Sets up a signal receiver for signal, and calls the provided callback
    173         when it comes in.
    174 
    175         @param callback: a callable to call when signal is received.
    176         @param signal: the signal to listen for.
    177         """
    178         bus = dbus.SystemBus(mainloop=self._main_loop)
    179         bus.add_signal_receiver(
    180             handler_function=callback,
    181             signal_name=signal,
    182             dbus_interface=interface,
    183             bus_name=None,
    184             path=path,
    185             member_keyword='signal_name')
    186 
    187 
    188     def __handle_signal(self, *args, **kwargs):
    189         """Callback to be used when a new key signal is received."""
    190         signal_name = kwargs.pop('signal_name', '')
    191         #signal_data = str(args[0])
    192         logging.info("SIGNAL: " + signal_name + ", " + str(args));
    193         if self._signals.has_key(signal_name):
    194           self._signals[signal_name].append(args)
    195         else:
    196           self._signals[signal_name] = [args]
    197 
    198 
    199 class network_ProxyResolver(test.test):
    200     """A test fixture for validating the integration of
    201        shill, Chrome, and tlsdated's proxy resolution.
    202     """
    203     version = 1
    204     auto_login = False
    205     service_settings = { }
    206 
    207     TIMEOUT = 360
    208 
    209     def initialize(self):
    210        """Constructor
    211           Sets up the test such that all DBus signals can be
    212           received and a fake proxy server can be instantiated.
    213           Additionally, the UI is restarted to ensure consistent
    214           shared network use.
    215        """
    216        super(network_ProxyResolver, self).initialize()
    217        cros_ui.stop()
    218        cros_ui.start()
    219        DBusGMainLoop(set_as_default=True)
    220        self._listener = SignalListener(gobject.MainLoop())
    221        self._shill = shill_proxy.ShillProxy.get_proxy()
    222        if self._shill is None:
    223          raise error.TestFail('Could not connect to shill')
    224        # Listen for ProxyResolve responses
    225        self._listener.listen_for_signal('ProxyChange',
    226                                         'org.chromium.AutotestProxyInterface',
    227                                         '/org/chromium/LibCrosService')
    228        # Listen for network property changes
    229        self._listener.listen_for_signal('PropertyChanged',
    230                                         'org.chromium.flimflam.Service',
    231                                         '/')
    232        # Listen on the proxy port.
    233        self._proxy_server = ProxyListener(('', 3128))
    234 
    235     # Set the proxy with Shill. This only works for shared connections
    236     # (like Eth).
    237     def set_proxy(self, service_name, proxy_config):
    238         """Changes the ProxyConfig property on the specified shill service.
    239 
    240         @param service_name: the name, as a str, of the shill service
    241         @param proxy_config: the ProxyConfig property value string
    242 
    243         @raises TestFail if the service is not found.
    244         """
    245         shill = self._shill
    246         service = shill.find_object('Service', { 'Name' : service_name })
    247         if not service:
    248             raise error.TestFail('Service ' + service_name +
    249                                  ' not found to test proxy with.')
    250         props = service.GetProperties()
    251         old_proxy = ''
    252         if props.has_key('ProxyConfig'):
    253           old_proxy = props['ProxyConfig']
    254         if self.service_settings.has_key(service_name) == False:
    255           logging.info('Preexisting ProxyConfig: ' + service_name +
    256                        ' -> ' + old_proxy)
    257           self.service_settings[service_name] = old_proxy
    258         logging.info('Setting proxy to ' + proxy_config)
    259         service.SetProperties({'ProxyConfig': proxy_config})
    260 
    261 
    262     def reset_services(self):
    263         """Walks the dict of service->ProxyConfig values and sets the
    264            proxy back to the originally observed value.
    265         """
    266         if len(self.service_settings) == 0:
    267           return
    268         for k,v in self.service_settings.items():
    269           logging.info('Resetting ProxyConfig: ' + k + ' -> ' + v)
    270           self.set_proxy(k, v)
    271 
    272 
    273     def check_chrome(self, proxy_type, proxy_config, timeout):
    274         """Check that Chrome has acknowledged the supplied proxy config
    275            by asking for resolution over DBus.
    276 
    277         @param proxy_type: PAC-style string type (e.g., 'PROXY', 'SOCKS')
    278         @param proxy_config: PAC-style config string (e.g., 127.0.0.1:1234)
    279         @param timeout: time in seconds to wait for Chrome to issue a signal.
    280 
    281         @return True if a matching response is seen and False otherwise
    282         """
    283         bus = dbus.SystemBus()
    284         dbus_proxy = bus.get_object('org.chromium.LibCrosService',
    285                                     '/org/chromium/LibCrosService')
    286         cros_service = dbus.Interface(dbus_proxy,
    287                                       'org.chromium.LibCrosServiceInterface')
    288         attempts = timeout
    289         while attempts > 0:
    290           cros_service.ResolveNetworkProxy(
    291                                        'https://clients3.google.com',
    292                                        'org.chromium.AutotestProxyInterface',
    293                                        'ProxyChange')
    294           signals = self._listener.wait_for_signals(
    295                         'waiting for proxy resolution from Chrome')
    296           if signals['ProxyChange'][0][1] == proxy_type + ' ' + proxy_config:
    297             return True
    298           attempts -= 1
    299           time.sleep(1)
    300         logging.error('Last DBus signal seen before giving up: ' + str(signals))
    301         return False
    302 
    303     def check_tlsdated(self, timeout):
    304         """Check that tlsdated uses the set proxy.
    305         @param timeout: time in seconds to wait for tlsdate to restart and query
    306         @return True if tlsdated hits the proxy server and False otherwise
    307         """
    308         # Restart tlsdated to force a network resync
    309         # (The other option is to force it to think there is no network sync.)
    310         try:
    311             self._proxy_server.run()
    312         except Exception as e:
    313             logging.error("Proxy error =>" + str(e))
    314             return False
    315         logging.info("proxy started!")
    316         status = subprocess.call(['initctl', 'restart', 'tlsdated'])
    317         if status != 0:
    318           logging.info("failed to restart tlsdated")
    319           return False
    320         attempts = timeout
    321         logging.info("waiting for hits on the proxy server")
    322         while attempts > 0:
    323           if self._proxy_server.hits() > 0:
    324             self._proxy_server.reset_hits()
    325             return True
    326           time.sleep(1)
    327           attempts -= 1
    328         logging.info("no hits")
    329         return False
    330 
    331 
    332     def cleanup(self):
    333         """Reset all the service data and teardown the proxy."""
    334         self.reset_services()
    335         logging.info("tearing down the proxy server")
    336         self._proxy_server.stop()
    337         logging.info("proxy server down")
    338         super(network_ProxyResolver, self).cleanup()
    339 
    340 
    341     def test_same_ip_proxy_at_signin_chrome_system_tlsdated(
    342                                                         self,
    343                                                         service_name,
    344                                                         test_timeout=TIMEOUT):
    345         """ Set the user policy, waits for condition, then logs out.
    346 
    347         @param service_name: shill service name to test on
    348         @param test_timeout: the total time in seconds split among all timeouts.
    349         """
    350         proxy_type = 'http'
    351         proxy_port = '3128'
    352         proxy_host = '127.0.0.1'
    353         proxy_url = proxy_type + '://' + proxy_host + ':' + proxy_port
    354         # TODO(wad) Only do the below if it was a single protocol proxy.
    355         # proxy_config = proxy_type + '=' + proxy_host + ':' + proxy_port
    356         proxy_config = proxy_host + ':' + proxy_port
    357         self.set_proxy(service_name, '{"mode":"fixed_servers","server":"' +
    358                                      proxy_config + '"}')
    359 
    360         logging.info("checking chrome")
    361         if self.check_chrome('PROXY', proxy_config, test_timeout/3) == False:
    362           raise error.TestFail('Chrome failed to resolve the proxy')
    363 
    364         # Restart tlsdate to force a network fix
    365         logging.info("checking tlsdated")
    366         if self.check_tlsdated(test_timeout/3) == False:
    367           raise error.TestFail('tlsdated never tried the proxy')
    368         logging.info("done!")
    369 
    370     def run_once(self, test_type, **params):
    371         logging.info('client: Running client test %s', test_type)
    372         getattr(self, test_type)(**params)
    373