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