1#!/usr/bin/env python 2# Copyright 2013 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""This is a python sync server used for testing Chrome Sync. 7 8By default, it listens on an ephemeral port and xmpp_port and sends the port 9numbers back to the originating process over a pipe. The originating process can 10specify an explicit port and xmpp_port if necessary. 11""" 12 13import asyncore 14import BaseHTTPServer 15import errno 16import os 17import select 18import socket 19import sys 20import urlparse 21 22import chromiumsync 23import echo_message 24import testserver_base 25import xmppserver 26 27 28class SyncHTTPServer(testserver_base.ClientRestrictingServerMixIn, 29 testserver_base.BrokenPipeHandlerMixIn, 30 testserver_base.StoppableHTTPServer): 31 """An HTTP server that handles sync commands.""" 32 33 def __init__(self, server_address, xmpp_port, request_handler_class): 34 testserver_base.StoppableHTTPServer.__init__(self, 35 server_address, 36 request_handler_class) 37 self._sync_handler = chromiumsync.TestServer() 38 self._xmpp_socket_map = {} 39 self._xmpp_server = xmppserver.XmppServer( 40 self._xmpp_socket_map, ('localhost', xmpp_port)) 41 self.xmpp_port = self._xmpp_server.getsockname()[1] 42 self.authenticated = True 43 44 def GetXmppServer(self): 45 return self._xmpp_server 46 47 def HandleCommand(self, query, raw_request): 48 return self._sync_handler.HandleCommand(query, raw_request) 49 50 def HandleRequestNoBlock(self): 51 """Handles a single request. 52 53 Copied from SocketServer._handle_request_noblock(). 54 """ 55 56 try: 57 request, client_address = self.get_request() 58 except socket.error: 59 return 60 if self.verify_request(request, client_address): 61 try: 62 self.process_request(request, client_address) 63 except Exception: 64 self.handle_error(request, client_address) 65 self.close_request(request) 66 67 def SetAuthenticated(self, auth_valid): 68 self.authenticated = auth_valid 69 70 def GetAuthenticated(self): 71 return self.authenticated 72 73 def serve_forever(self): 74 """This is a merge of asyncore.loop() and SocketServer.serve_forever(). 75 """ 76 77 def HandleXmppSocket(fd, socket_map, handler): 78 """Runs the handler for the xmpp connection for fd. 79 80 Adapted from asyncore.read() et al. 81 """ 82 83 xmpp_connection = socket_map.get(fd) 84 # This could happen if a previous handler call caused fd to get 85 # removed from socket_map. 86 if xmpp_connection is None: 87 return 88 try: 89 handler(xmpp_connection) 90 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit): 91 raise 92 except: 93 xmpp_connection.handle_error() 94 95 while True: 96 read_fds = [ self.fileno() ] 97 write_fds = [] 98 exceptional_fds = [] 99 100 for fd, xmpp_connection in self._xmpp_socket_map.items(): 101 is_r = xmpp_connection.readable() 102 is_w = xmpp_connection.writable() 103 if is_r: 104 read_fds.append(fd) 105 if is_w: 106 write_fds.append(fd) 107 if is_r or is_w: 108 exceptional_fds.append(fd) 109 110 try: 111 read_fds, write_fds, exceptional_fds = ( 112 select.select(read_fds, write_fds, exceptional_fds)) 113 except select.error, err: 114 if err.args[0] != errno.EINTR: 115 raise 116 else: 117 continue 118 119 for fd in read_fds: 120 if fd == self.fileno(): 121 self.HandleRequestNoBlock() 122 continue 123 HandleXmppSocket(fd, self._xmpp_socket_map, 124 asyncore.dispatcher.handle_read_event) 125 126 for fd in write_fds: 127 HandleXmppSocket(fd, self._xmpp_socket_map, 128 asyncore.dispatcher.handle_write_event) 129 130 for fd in exceptional_fds: 131 HandleXmppSocket(fd, self._xmpp_socket_map, 132 asyncore.dispatcher.handle_expt_event) 133 134 135class SyncPageHandler(testserver_base.BasePageHandler): 136 """Handler for the main HTTP sync server.""" 137 138 def __init__(self, request, client_address, sync_http_server): 139 get_handlers = [self.ChromiumSyncTimeHandler, 140 self.ChromiumSyncMigrationOpHandler, 141 self.ChromiumSyncCredHandler, 142 self.ChromiumSyncXmppCredHandler, 143 self.ChromiumSyncDisableNotificationsOpHandler, 144 self.ChromiumSyncEnableNotificationsOpHandler, 145 self.ChromiumSyncSendNotificationOpHandler, 146 self.ChromiumSyncBirthdayErrorOpHandler, 147 self.ChromiumSyncTransientErrorOpHandler, 148 self.ChromiumSyncErrorOpHandler, 149 self.ChromiumSyncSyncTabFaviconsOpHandler, 150 self.ChromiumSyncCreateSyncedBookmarksOpHandler, 151 self.ChromiumSyncEnableKeystoreEncryptionOpHandler, 152 self.ChromiumSyncRotateKeystoreKeysOpHandler, 153 self.ChromiumSyncEnableManagedUserAcknowledgementHandler, 154 self.ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler] 155 156 post_handlers = [self.ChromiumSyncCommandHandler, 157 self.ChromiumSyncTimeHandler] 158 testserver_base.BasePageHandler.__init__(self, request, client_address, 159 sync_http_server, [], get_handlers, 160 [], post_handlers, []) 161 162 163 def ChromiumSyncTimeHandler(self): 164 """Handle Chromium sync .../time requests. 165 166 The syncer sometimes checks server reachability by examining /time. 167 """ 168 169 test_name = "/chromiumsync/time" 170 if not self._ShouldHandleRequest(test_name): 171 return False 172 173 # Chrome hates it if we send a response before reading the request. 174 if self.headers.getheader('content-length'): 175 length = int(self.headers.getheader('content-length')) 176 _raw_request = self.rfile.read(length) 177 178 self.send_response(200) 179 self.send_header('Content-Type', 'text/plain') 180 self.end_headers() 181 self.wfile.write('0123456789') 182 return True 183 184 def ChromiumSyncCommandHandler(self): 185 """Handle a chromiumsync command arriving via http. 186 187 This covers all sync protocol commands: authentication, getupdates, and 188 commit. 189 """ 190 191 test_name = "/chromiumsync/command" 192 if not self._ShouldHandleRequest(test_name): 193 return False 194 195 length = int(self.headers.getheader('content-length')) 196 raw_request = self.rfile.read(length) 197 http_response = 200 198 raw_reply = None 199 if not self.server.GetAuthenticated(): 200 http_response = 401 201 challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % ( 202 self.server.server_address[0]) 203 else: 204 http_response, raw_reply = self.server.HandleCommand( 205 self.path, raw_request) 206 207 ### Now send the response to the client. ### 208 self.send_response(http_response) 209 if http_response == 401: 210 self.send_header('www-Authenticate', challenge) 211 self.end_headers() 212 self.wfile.write(raw_reply) 213 return True 214 215 def ChromiumSyncMigrationOpHandler(self): 216 test_name = "/chromiumsync/migrate" 217 if not self._ShouldHandleRequest(test_name): 218 return False 219 220 http_response, raw_reply = self.server._sync_handler.HandleMigrate( 221 self.path) 222 self.send_response(http_response) 223 self.send_header('Content-Type', 'text/html') 224 self.send_header('Content-Length', len(raw_reply)) 225 self.end_headers() 226 self.wfile.write(raw_reply) 227 return True 228 229 def ChromiumSyncCredHandler(self): 230 test_name = "/chromiumsync/cred" 231 if not self._ShouldHandleRequest(test_name): 232 return False 233 try: 234 query = urlparse.urlparse(self.path)[4] 235 cred_valid = urlparse.parse_qs(query)['valid'] 236 if cred_valid[0] == 'True': 237 self.server.SetAuthenticated(True) 238 else: 239 self.server.SetAuthenticated(False) 240 except Exception: 241 self.server.SetAuthenticated(False) 242 243 http_response = 200 244 raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated() 245 self.send_response(http_response) 246 self.send_header('Content-Type', 'text/html') 247 self.send_header('Content-Length', len(raw_reply)) 248 self.end_headers() 249 self.wfile.write(raw_reply) 250 return True 251 252 def ChromiumSyncXmppCredHandler(self): 253 test_name = "/chromiumsync/xmppcred" 254 if not self._ShouldHandleRequest(test_name): 255 return False 256 xmpp_server = self.server.GetXmppServer() 257 try: 258 query = urlparse.urlparse(self.path)[4] 259 cred_valid = urlparse.parse_qs(query)['valid'] 260 if cred_valid[0] == 'True': 261 xmpp_server.SetAuthenticated(True) 262 else: 263 xmpp_server.SetAuthenticated(False) 264 except: 265 xmpp_server.SetAuthenticated(False) 266 267 http_response = 200 268 raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated() 269 self.send_response(http_response) 270 self.send_header('Content-Type', 'text/html') 271 self.send_header('Content-Length', len(raw_reply)) 272 self.end_headers() 273 self.wfile.write(raw_reply) 274 return True 275 276 def ChromiumSyncDisableNotificationsOpHandler(self): 277 test_name = "/chromiumsync/disablenotifications" 278 if not self._ShouldHandleRequest(test_name): 279 return False 280 self.server.GetXmppServer().DisableNotifications() 281 result = 200 282 raw_reply = ('<html><title>Notifications disabled</title>' 283 '<H1>Notifications disabled</H1></html>') 284 self.send_response(result) 285 self.send_header('Content-Type', 'text/html') 286 self.send_header('Content-Length', len(raw_reply)) 287 self.end_headers() 288 self.wfile.write(raw_reply) 289 return True 290 291 def ChromiumSyncEnableNotificationsOpHandler(self): 292 test_name = "/chromiumsync/enablenotifications" 293 if not self._ShouldHandleRequest(test_name): 294 return False 295 self.server.GetXmppServer().EnableNotifications() 296 result = 200 297 raw_reply = ('<html><title>Notifications enabled</title>' 298 '<H1>Notifications enabled</H1></html>') 299 self.send_response(result) 300 self.send_header('Content-Type', 'text/html') 301 self.send_header('Content-Length', len(raw_reply)) 302 self.end_headers() 303 self.wfile.write(raw_reply) 304 return True 305 306 def ChromiumSyncSendNotificationOpHandler(self): 307 test_name = "/chromiumsync/sendnotification" 308 if not self._ShouldHandleRequest(test_name): 309 return False 310 query = urlparse.urlparse(self.path)[4] 311 query_params = urlparse.parse_qs(query) 312 channel = '' 313 data = '' 314 if 'channel' in query_params: 315 channel = query_params['channel'][0] 316 if 'data' in query_params: 317 data = query_params['data'][0] 318 self.server.GetXmppServer().SendNotification(channel, data) 319 result = 200 320 raw_reply = ('<html><title>Notification sent</title>' 321 '<H1>Notification sent with channel "%s" ' 322 'and data "%s"</H1></html>' 323 % (channel, data)) 324 self.send_response(result) 325 self.send_header('Content-Type', 'text/html') 326 self.send_header('Content-Length', len(raw_reply)) 327 self.end_headers() 328 self.wfile.write(raw_reply) 329 return True 330 331 def ChromiumSyncBirthdayErrorOpHandler(self): 332 test_name = "/chromiumsync/birthdayerror" 333 if not self._ShouldHandleRequest(test_name): 334 return False 335 result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError() 336 self.send_response(result) 337 self.send_header('Content-Type', 'text/html') 338 self.send_header('Content-Length', len(raw_reply)) 339 self.end_headers() 340 self.wfile.write(raw_reply) 341 return True 342 343 def ChromiumSyncTransientErrorOpHandler(self): 344 test_name = "/chromiumsync/transienterror" 345 if not self._ShouldHandleRequest(test_name): 346 return False 347 result, raw_reply = self.server._sync_handler.HandleSetTransientError() 348 self.send_response(result) 349 self.send_header('Content-Type', 'text/html') 350 self.send_header('Content-Length', len(raw_reply)) 351 self.end_headers() 352 self.wfile.write(raw_reply) 353 return True 354 355 def ChromiumSyncErrorOpHandler(self): 356 test_name = "/chromiumsync/error" 357 if not self._ShouldHandleRequest(test_name): 358 return False 359 result, raw_reply = self.server._sync_handler.HandleSetInducedError( 360 self.path) 361 self.send_response(result) 362 self.send_header('Content-Type', 'text/html') 363 self.send_header('Content-Length', len(raw_reply)) 364 self.end_headers() 365 self.wfile.write(raw_reply) 366 return True 367 368 def ChromiumSyncSyncTabFaviconsOpHandler(self): 369 test_name = "/chromiumsync/synctabfavicons" 370 if not self._ShouldHandleRequest(test_name): 371 return False 372 result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons() 373 self.send_response(result) 374 self.send_header('Content-Type', 'text/html') 375 self.send_header('Content-Length', len(raw_reply)) 376 self.end_headers() 377 self.wfile.write(raw_reply) 378 return True 379 380 def ChromiumSyncCreateSyncedBookmarksOpHandler(self): 381 test_name = "/chromiumsync/createsyncedbookmarks" 382 if not self._ShouldHandleRequest(test_name): 383 return False 384 result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks() 385 self.send_response(result) 386 self.send_header('Content-Type', 'text/html') 387 self.send_header('Content-Length', len(raw_reply)) 388 self.end_headers() 389 self.wfile.write(raw_reply) 390 return True 391 392 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self): 393 test_name = "/chromiumsync/enablekeystoreencryption" 394 if not self._ShouldHandleRequest(test_name): 395 return False 396 result, raw_reply = ( 397 self.server._sync_handler.HandleEnableKeystoreEncryption()) 398 self.send_response(result) 399 self.send_header('Content-Type', 'text/html') 400 self.send_header('Content-Length', len(raw_reply)) 401 self.end_headers() 402 self.wfile.write(raw_reply) 403 return True 404 405 def ChromiumSyncRotateKeystoreKeysOpHandler(self): 406 test_name = "/chromiumsync/rotatekeystorekeys" 407 if not self._ShouldHandleRequest(test_name): 408 return False 409 result, raw_reply = ( 410 self.server._sync_handler.HandleRotateKeystoreKeys()) 411 self.send_response(result) 412 self.send_header('Content-Type', 'text/html') 413 self.send_header('Content-Length', len(raw_reply)) 414 self.end_headers() 415 self.wfile.write(raw_reply) 416 return True 417 418 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self): 419 test_name = "/chromiumsync/enablemanageduseracknowledgement" 420 if not self._ShouldHandleRequest(test_name): 421 return False 422 result, raw_reply = ( 423 self.server._sync_handler.HandleEnableManagedUserAcknowledgement()) 424 self.send_response(result) 425 self.send_header('Content-Type', 'text/html') 426 self.send_header('Content-Length', len(raw_reply)) 427 self.end_headers() 428 self.wfile.write(raw_reply) 429 return True 430 431 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self): 432 test_name = "/chromiumsync/enableprecommitgetupdateavoidance" 433 if not self._ShouldHandleRequest(test_name): 434 return False 435 result, raw_reply = ( 436 self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance()) 437 self.send_response(result) 438 self.send_header('Content-Type', 'text/html') 439 self.send_header('Content-Length', len(raw_reply)) 440 self.end_headers() 441 self.wfile.write(raw_reply) 442 return True 443 444class SyncServerRunner(testserver_base.TestServerRunner): 445 """TestServerRunner for the net test servers.""" 446 447 def __init__(self): 448 super(SyncServerRunner, self).__init__() 449 450 def create_server(self, server_data): 451 port = self.options.port 452 host = self.options.host 453 xmpp_port = self.options.xmpp_port 454 server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler) 455 print 'Sync HTTP server started on port %d...' % server.server_port 456 print 'Sync XMPP server started on port %d...' % server.xmpp_port 457 server_data['port'] = server.server_port 458 server_data['xmpp_port'] = server.xmpp_port 459 return server 460 461 def run_server(self): 462 testserver_base.TestServerRunner.run_server(self) 463 464 def add_options(self): 465 testserver_base.TestServerRunner.add_options(self) 466 self.option_parser.add_option('--xmpp-port', default='0', type='int', 467 help='Port used by the XMPP server. If ' 468 'unspecified, the XMPP server will listen on ' 469 'an ephemeral port.') 470 # Override the default logfile name used in testserver.py. 471 self.option_parser.set_defaults(log_file='sync_testserver.log') 472 473if __name__ == '__main__': 474 sys.exit(SyncServerRunner().main()) 475