• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
9import dbus
10import gobject
11import logging
12import subprocess
13import threading
14import time
15
16from autotest_lib.client.bin import test, utils
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.cros import cros_ui
19from autotest_lib.client.cros.networking import shill_proxy
20
21from dbus.mainloop.glib import DBusGMainLoop
22from SocketServer import ThreadingTCPServer, StreamRequestHandler
23
24class 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
46class 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
73class 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
106class 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
199class 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 network property changes
225       self._listener.listen_for_signal('PropertyChanged',
226                                        'org.chromium.flimflam.Service',
227                                        '/')
228       # Listen on the proxy port.
229       self._proxy_server = ProxyListener(('', 3128))
230
231    # Set the proxy with Shill. This only works for shared connections
232    # (like Eth).
233    def set_proxy(self, service_name, proxy_config):
234        """Changes the ProxyConfig property on the specified shill service.
235
236        @param service_name: the name, as a str, of the shill service
237        @param proxy_config: the ProxyConfig property value string
238
239        @raises TestFail if the service is not found.
240        """
241        shill = self._shill
242        service = shill.find_object('Service', { 'Name' : service_name })
243        if not service:
244            raise error.TestFail('Service ' + service_name +
245                                 ' not found to test proxy with.')
246        props = service.GetProperties()
247        old_proxy = ''
248        if props.has_key('ProxyConfig'):
249          old_proxy = props['ProxyConfig']
250        if self.service_settings.has_key(service_name) == False:
251          logging.info('Preexisting ProxyConfig: ' + service_name +
252                       ' -> ' + old_proxy)
253          self.service_settings[service_name] = old_proxy
254        logging.info('Setting proxy to ' + proxy_config)
255        service.SetProperties({'ProxyConfig': proxy_config})
256
257
258    def reset_services(self):
259        """Walks the dict of service->ProxyConfig values and sets the
260           proxy back to the originally observed value.
261        """
262        if len(self.service_settings) == 0:
263          return
264        for k,v in self.service_settings.items():
265          logging.info('Resetting ProxyConfig: ' + k + ' -> ' + v)
266          self.set_proxy(k, v)
267
268
269    def check_chrome(self, proxy_type, proxy_config, timeout):
270        """Check that Chrome has acknowledged the supplied proxy config
271           by asking for resolution over DBus.
272
273        @param proxy_type: PAC-style string type (e.g., 'PROXY', 'SOCKS')
274        @param proxy_config: PAC-style config string (e.g., 127.0.0.1:1234)
275        @param timeout: time in seconds to wait for Chrome to issue a signal.
276
277        @return True if a matching response is seen and False otherwise
278        """
279        bus = dbus.SystemBus()
280        dbus_proxy = bus.get_object('org.chromium.NetworkProxyService',
281                                    '/org/chromium/NetworkProxyService')
282        service = dbus.Interface(dbus_proxy,
283                                 'org.chromium.NetworkProxyServiceInterface')
284
285        attempts = timeout
286        while attempts > 0:
287          result, _ = service.ResolveProxy('https://clients3.google.com')
288          if str(result) == proxy_type + ' ' + proxy_config:
289            return True
290          attempts -= 1
291          time.sleep(1)
292        logging.error('Last response seen before giving up: ' + str(result))
293        return False
294
295    def check_tlsdated(self, timeout):
296        """Check that tlsdated uses the set proxy.
297        @param timeout: time in seconds to wait for tlsdate to restart and query
298        @return True if tlsdated hits the proxy server and False otherwise
299        """
300        # Restart tlsdated to force a network resync
301        # (The other option is to force it to think there is no network sync.)
302        try:
303            self._proxy_server.run()
304        except Exception as e:
305            logging.error("Proxy error =>" + str(e))
306            return False
307        logging.info("proxy started!")
308        status = subprocess.call(['initctl', 'restart', 'tlsdated'])
309        if status != 0:
310          logging.info("failed to restart tlsdated")
311          return False
312        attempts = timeout
313        logging.info("waiting for hits on the proxy server")
314        while attempts > 0:
315          if self._proxy_server.hits() > 0:
316            self._proxy_server.reset_hits()
317            return True
318          time.sleep(1)
319          attempts -= 1
320        logging.info("no hits")
321        return False
322
323
324    def cleanup(self):
325        """Reset all the service data and teardown the proxy."""
326        self.reset_services()
327        logging.info("tearing down the proxy server")
328        self._proxy_server.stop()
329        logging.info("proxy server down")
330        super(network_ProxyResolver, self).cleanup()
331
332
333    def test_same_ip_proxy_at_signin_chrome_system_tlsdated(
334                                                        self,
335                                                        service_name,
336                                                        test_timeout=TIMEOUT):
337        """ Set the user policy, waits for condition, then logs out.
338
339        @param service_name: shill service name to test on
340        @param test_timeout: the total time in seconds split among all timeouts.
341        """
342        proxy_type = 'http'
343        proxy_port = '3128'
344        proxy_host = '127.0.0.1'
345        proxy_url = proxy_type + '://' + proxy_host + ':' + proxy_port
346        # TODO(wad) Only do the below if it was a single protocol proxy.
347        # proxy_config = proxy_type + '=' + proxy_host + ':' + proxy_port
348        proxy_config = proxy_host + ':' + proxy_port
349        self.set_proxy(service_name, '{"mode":"fixed_servers","server":"' +
350                                     proxy_config + '"}')
351
352        logging.info("checking chrome")
353        if self.check_chrome('PROXY', proxy_config, test_timeout/3) == False:
354          raise error.TestFail('Chrome failed to resolve the proxy')
355
356        # Restart tlsdate to force a network fix
357        logging.info("checking tlsdated")
358        if self.check_tlsdated(test_timeout/3) == False:
359          raise error.TestFail('tlsdated never tried the proxy')
360        logging.info("done!")
361
362    def run_once(self, test_type, **params):
363        logging.info('client: Running client test %s', test_type)
364        getattr(self, test_type)(**params)
365