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