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 8 By default, it listens on an ephemeral port and xmpp_port and sends the port 9 numbers back to the originating process over a pipe. The originating process can 10 specify an explicit port and xmpp_port if necessary. 11 """ 12 13 import asyncore 14 import BaseHTTPServer 15 import errno 16 import os 17 import select 18 import socket 19 import sys 20 import urlparse 21 22 import chromiumsync 23 import echo_message 24 import testserver_base 25 import xmppserver 26 27 28 class 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 135 class 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 self.GaiaOAuth2TokenHandler, 156 self.GaiaSetOAuth2TokenResponseHandler, 157 self.TriggerSyncedNotificationHandler, 158 self.SyncedNotificationsPageHandler, 159 self.TriggerSyncedNotificationAppInfoHandler, 160 self.SyncedNotificationsAppInfoPageHandler, 161 self.CustomizeClientCommandHandler] 162 163 post_handlers = [self.ChromiumSyncCommandHandler, 164 self.ChromiumSyncTimeHandler, 165 self.GaiaOAuth2TokenHandler, 166 self.GaiaSetOAuth2TokenResponseHandler] 167 testserver_base.BasePageHandler.__init__(self, request, client_address, 168 sync_http_server, [], get_handlers, 169 [], post_handlers, []) 170 171 172 def ChromiumSyncTimeHandler(self): 173 """Handle Chromium sync .../time requests. 174 175 The syncer sometimes checks server reachability by examining /time. 176 """ 177 178 test_name = "/chromiumsync/time" 179 if not self._ShouldHandleRequest(test_name): 180 return False 181 182 # Chrome hates it if we send a response before reading the request. 183 if self.headers.getheader('content-length'): 184 length = int(self.headers.getheader('content-length')) 185 _raw_request = self.rfile.read(length) 186 187 self.send_response(200) 188 self.send_header('Content-Type', 'text/plain') 189 self.end_headers() 190 self.wfile.write('0123456789') 191 return True 192 193 def ChromiumSyncCommandHandler(self): 194 """Handle a chromiumsync command arriving via http. 195 196 This covers all sync protocol commands: authentication, getupdates, and 197 commit. 198 """ 199 200 test_name = "/chromiumsync/command" 201 if not self._ShouldHandleRequest(test_name): 202 return False 203 204 length = int(self.headers.getheader('content-length')) 205 raw_request = self.rfile.read(length) 206 http_response = 200 207 raw_reply = None 208 if not self.server.GetAuthenticated(): 209 http_response = 401 210 challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % ( 211 self.server.server_address[0]) 212 else: 213 http_response, raw_reply = self.server.HandleCommand( 214 self.path, raw_request) 215 216 ### Now send the response to the client. ### 217 self.send_response(http_response) 218 if http_response == 401: 219 self.send_header('www-Authenticate', challenge) 220 self.end_headers() 221 self.wfile.write(raw_reply) 222 return True 223 224 def ChromiumSyncMigrationOpHandler(self): 225 test_name = "/chromiumsync/migrate" 226 if not self._ShouldHandleRequest(test_name): 227 return False 228 229 http_response, raw_reply = self.server._sync_handler.HandleMigrate( 230 self.path) 231 self.send_response(http_response) 232 self.send_header('Content-Type', 'text/html') 233 self.send_header('Content-Length', len(raw_reply)) 234 self.end_headers() 235 self.wfile.write(raw_reply) 236 return True 237 238 def ChromiumSyncCredHandler(self): 239 test_name = "/chromiumsync/cred" 240 if not self._ShouldHandleRequest(test_name): 241 return False 242 try: 243 query = urlparse.urlparse(self.path)[4] 244 cred_valid = urlparse.parse_qs(query)['valid'] 245 if cred_valid[0] == 'True': 246 self.server.SetAuthenticated(True) 247 else: 248 self.server.SetAuthenticated(False) 249 except Exception: 250 self.server.SetAuthenticated(False) 251 252 http_response = 200 253 raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated() 254 self.send_response(http_response) 255 self.send_header('Content-Type', 'text/html') 256 self.send_header('Content-Length', len(raw_reply)) 257 self.end_headers() 258 self.wfile.write(raw_reply) 259 return True 260 261 def ChromiumSyncXmppCredHandler(self): 262 test_name = "/chromiumsync/xmppcred" 263 if not self._ShouldHandleRequest(test_name): 264 return False 265 xmpp_server = self.server.GetXmppServer() 266 try: 267 query = urlparse.urlparse(self.path)[4] 268 cred_valid = urlparse.parse_qs(query)['valid'] 269 if cred_valid[0] == 'True': 270 xmpp_server.SetAuthenticated(True) 271 else: 272 xmpp_server.SetAuthenticated(False) 273 except: 274 xmpp_server.SetAuthenticated(False) 275 276 http_response = 200 277 raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated() 278 self.send_response(http_response) 279 self.send_header('Content-Type', 'text/html') 280 self.send_header('Content-Length', len(raw_reply)) 281 self.end_headers() 282 self.wfile.write(raw_reply) 283 return True 284 285 def ChromiumSyncDisableNotificationsOpHandler(self): 286 test_name = "/chromiumsync/disablenotifications" 287 if not self._ShouldHandleRequest(test_name): 288 return False 289 self.server.GetXmppServer().DisableNotifications() 290 result = 200 291 raw_reply = ('<html><title>Notifications disabled</title>' 292 '<H1>Notifications disabled</H1></html>') 293 self.send_response(result) 294 self.send_header('Content-Type', 'text/html') 295 self.send_header('Content-Length', len(raw_reply)) 296 self.end_headers() 297 self.wfile.write(raw_reply) 298 return True 299 300 def ChromiumSyncEnableNotificationsOpHandler(self): 301 test_name = "/chromiumsync/enablenotifications" 302 if not self._ShouldHandleRequest(test_name): 303 return False 304 self.server.GetXmppServer().EnableNotifications() 305 result = 200 306 raw_reply = ('<html><title>Notifications enabled</title>' 307 '<H1>Notifications enabled</H1></html>') 308 self.send_response(result) 309 self.send_header('Content-Type', 'text/html') 310 self.send_header('Content-Length', len(raw_reply)) 311 self.end_headers() 312 self.wfile.write(raw_reply) 313 return True 314 315 def ChromiumSyncSendNotificationOpHandler(self): 316 test_name = "/chromiumsync/sendnotification" 317 if not self._ShouldHandleRequest(test_name): 318 return False 319 query = urlparse.urlparse(self.path)[4] 320 query_params = urlparse.parse_qs(query) 321 channel = '' 322 data = '' 323 if 'channel' in query_params: 324 channel = query_params['channel'][0] 325 if 'data' in query_params: 326 data = query_params['data'][0] 327 self.server.GetXmppServer().SendNotification(channel, data) 328 result = 200 329 raw_reply = ('<html><title>Notification sent</title>' 330 '<H1>Notification sent with channel "%s" ' 331 'and data "%s"</H1></html>' 332 % (channel, data)) 333 self.send_response(result) 334 self.send_header('Content-Type', 'text/html') 335 self.send_header('Content-Length', len(raw_reply)) 336 self.end_headers() 337 self.wfile.write(raw_reply) 338 return True 339 340 def ChromiumSyncBirthdayErrorOpHandler(self): 341 test_name = "/chromiumsync/birthdayerror" 342 if not self._ShouldHandleRequest(test_name): 343 return False 344 result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError() 345 self.send_response(result) 346 self.send_header('Content-Type', 'text/html') 347 self.send_header('Content-Length', len(raw_reply)) 348 self.end_headers() 349 self.wfile.write(raw_reply) 350 return True 351 352 def ChromiumSyncTransientErrorOpHandler(self): 353 test_name = "/chromiumsync/transienterror" 354 if not self._ShouldHandleRequest(test_name): 355 return False 356 result, raw_reply = self.server._sync_handler.HandleSetTransientError() 357 self.send_response(result) 358 self.send_header('Content-Type', 'text/html') 359 self.send_header('Content-Length', len(raw_reply)) 360 self.end_headers() 361 self.wfile.write(raw_reply) 362 return True 363 364 def ChromiumSyncErrorOpHandler(self): 365 test_name = "/chromiumsync/error" 366 if not self._ShouldHandleRequest(test_name): 367 return False 368 result, raw_reply = self.server._sync_handler.HandleSetInducedError( 369 self.path) 370 self.send_response(result) 371 self.send_header('Content-Type', 'text/html') 372 self.send_header('Content-Length', len(raw_reply)) 373 self.end_headers() 374 self.wfile.write(raw_reply) 375 return True 376 377 def ChromiumSyncSyncTabFaviconsOpHandler(self): 378 test_name = "/chromiumsync/synctabfavicons" 379 if not self._ShouldHandleRequest(test_name): 380 return False 381 result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons() 382 self.send_response(result) 383 self.send_header('Content-Type', 'text/html') 384 self.send_header('Content-Length', len(raw_reply)) 385 self.end_headers() 386 self.wfile.write(raw_reply) 387 return True 388 389 def ChromiumSyncCreateSyncedBookmarksOpHandler(self): 390 test_name = "/chromiumsync/createsyncedbookmarks" 391 if not self._ShouldHandleRequest(test_name): 392 return False 393 result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks() 394 self.send_response(result) 395 self.send_header('Content-Type', 'text/html') 396 self.send_header('Content-Length', len(raw_reply)) 397 self.end_headers() 398 self.wfile.write(raw_reply) 399 return True 400 401 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self): 402 test_name = "/chromiumsync/enablekeystoreencryption" 403 if not self._ShouldHandleRequest(test_name): 404 return False 405 result, raw_reply = ( 406 self.server._sync_handler.HandleEnableKeystoreEncryption()) 407 self.send_response(result) 408 self.send_header('Content-Type', 'text/html') 409 self.send_header('Content-Length', len(raw_reply)) 410 self.end_headers() 411 self.wfile.write(raw_reply) 412 return True 413 414 def ChromiumSyncRotateKeystoreKeysOpHandler(self): 415 test_name = "/chromiumsync/rotatekeystorekeys" 416 if not self._ShouldHandleRequest(test_name): 417 return False 418 result, raw_reply = ( 419 self.server._sync_handler.HandleRotateKeystoreKeys()) 420 self.send_response(result) 421 self.send_header('Content-Type', 'text/html') 422 self.send_header('Content-Length', len(raw_reply)) 423 self.end_headers() 424 self.wfile.write(raw_reply) 425 return True 426 427 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self): 428 test_name = "/chromiumsync/enablemanageduseracknowledgement" 429 if not self._ShouldHandleRequest(test_name): 430 return False 431 result, raw_reply = ( 432 self.server._sync_handler.HandleEnableManagedUserAcknowledgement()) 433 self.send_response(result) 434 self.send_header('Content-Type', 'text/html') 435 self.send_header('Content-Length', len(raw_reply)) 436 self.end_headers() 437 self.wfile.write(raw_reply) 438 return True 439 440 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self): 441 test_name = "/chromiumsync/enableprecommitgetupdateavoidance" 442 if not self._ShouldHandleRequest(test_name): 443 return False 444 result, raw_reply = ( 445 self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance()) 446 self.send_response(result) 447 self.send_header('Content-Type', 'text/html') 448 self.send_header('Content-Length', len(raw_reply)) 449 self.end_headers() 450 self.wfile.write(raw_reply) 451 return True 452 453 def GaiaOAuth2TokenHandler(self): 454 test_name = "/o/oauth2/token" 455 if not self._ShouldHandleRequest(test_name): 456 return False 457 if self.headers.getheader('content-length'): 458 length = int(self.headers.getheader('content-length')) 459 _raw_request = self.rfile.read(length) 460 result, raw_reply = ( 461 self.server._sync_handler.HandleGetOauth2Token()) 462 self.send_response(result) 463 self.send_header('Content-Type', 'application/json') 464 self.send_header('Content-Length', len(raw_reply)) 465 self.end_headers() 466 self.wfile.write(raw_reply) 467 return True 468 469 def GaiaSetOAuth2TokenResponseHandler(self): 470 test_name = "/setfakeoauth2token" 471 if not self._ShouldHandleRequest(test_name): 472 return False 473 474 # The index of 'query' is 4. 475 # See http://docs.python.org/2/library/urlparse.html 476 query = urlparse.urlparse(self.path)[4] 477 query_params = urlparse.parse_qs(query) 478 479 response_code = 0 480 request_token = '' 481 access_token = '' 482 expires_in = 0 483 token_type = '' 484 485 if 'response_code' in query_params: 486 response_code = query_params['response_code'][0] 487 if 'request_token' in query_params: 488 request_token = query_params['request_token'][0] 489 if 'access_token' in query_params: 490 access_token = query_params['access_token'][0] 491 if 'expires_in' in query_params: 492 expires_in = query_params['expires_in'][0] 493 if 'token_type' in query_params: 494 token_type = query_params['token_type'][0] 495 496 result, raw_reply = ( 497 self.server._sync_handler.HandleSetOauth2Token( 498 response_code, request_token, access_token, expires_in, token_type)) 499 self.send_response(result) 500 self.send_header('Content-Type', 'text/html') 501 self.send_header('Content-Length', len(raw_reply)) 502 self.end_headers() 503 self.wfile.write(raw_reply) 504 return True 505 506 def TriggerSyncedNotificationHandler(self): 507 test_name = "/triggersyncednotification" 508 if not self._ShouldHandleRequest(test_name): 509 return False 510 511 query = urlparse.urlparse(self.path)[4] 512 query_params = urlparse.parse_qs(query) 513 514 serialized_notification = '' 515 516 if 'serialized_notification' in query_params: 517 serialized_notification = query_params['serialized_notification'][0] 518 519 try: 520 notification_string = self.server._sync_handler.account \ 521 .AddSyncedNotification(serialized_notification) 522 reply = "A synced notification was triggered:\n\n" 523 reply += "<code>{}</code>.".format(notification_string) 524 response_code = 200 525 except chromiumsync.ClientNotConnectedError: 526 reply = ('The client is not connected to the server, so the notification' 527 ' could not be created.') 528 response_code = 400 529 530 self.send_response(response_code) 531 self.send_header('Content-Type', 'text/html') 532 self.send_header('Content-Length', len(reply)) 533 self.end_headers() 534 self.wfile.write(reply) 535 return True 536 537 def TriggerSyncedNotificationAppInfoHandler(self): 538 test_name = "/triggersyncednotificationappinfo" 539 if not self._ShouldHandleRequest(test_name): 540 return False 541 542 query = urlparse.urlparse(self.path)[4] 543 query_params = urlparse.parse_qs(query) 544 545 app_info = '' 546 547 if 'synced_notification_app_info' in query_params: 548 app_info = query_params['synced_notification_app_info'][0] 549 550 try: 551 app_info_string = self.server._sync_handler.account \ 552 .AddSyncedNotificationAppInfo(app_info) 553 reply = "A synced notification app info was sent:\n\n" 554 reply += "<code>{}</code>.".format(app_info_string) 555 response_code = 200 556 except chromiumsync.ClientNotConnectedError: 557 reply = ('The client is not connected to the server, so the app info' 558 ' could not be created.') 559 response_code = 400 560 561 self.send_response(response_code) 562 self.send_header('Content-Type', 'text/html') 563 self.send_header('Content-Length', len(reply)) 564 self.end_headers() 565 self.wfile.write(reply) 566 return True 567 568 def CustomizeClientCommandHandler(self): 569 test_name = "/customizeclientcommand" 570 if not self._ShouldHandleRequest(test_name): 571 return False 572 573 query = urlparse.urlparse(self.path)[4] 574 query_params = urlparse.parse_qs(query) 575 576 if 'sessions_commit_delay_seconds' in query_params: 577 sessions_commit_delay = query_params['sessions_commit_delay_seconds'][0] 578 try: 579 command_string = self.server._sync_handler.CustomizeClientCommand( 580 int(sessions_commit_delay)) 581 response_code = 200 582 reply = "The ClientCommand was customized:\n\n" 583 reply += "<code>{}</code>.".format(command_string) 584 except ValueError: 585 response_code = 400 586 reply = "sessions_commit_delay_seconds was not an int" 587 else: 588 response_code = 400 589 reply = "sessions_commit_delay_seconds is required" 590 591 self.send_response(response_code) 592 self.send_header('Content-Type', 'text/html') 593 self.send_header('Content-Length', len(reply)) 594 self.end_headers() 595 self.wfile.write(reply) 596 return True 597 598 def SyncedNotificationsPageHandler(self): 599 test_name = "/syncednotifications" 600 if not self._ShouldHandleRequest(test_name): 601 return False 602 603 html = open('sync/tools/testserver/synced_notifications.html', 'r').read() 604 605 self.send_response(200) 606 self.send_header('Content-Type', 'text/html') 607 self.send_header('Content-Length', len(html)) 608 self.end_headers() 609 self.wfile.write(html) 610 return True 611 612 def SyncedNotificationsAppInfoPageHandler(self): 613 test_name = "/syncednotificationsappinfo" 614 if not self._ShouldHandleRequest(test_name): 615 return False 616 617 html = \ 618 open('sync/tools/testserver/synced_notification_app_info.html', 'r').\ 619 read() 620 621 self.send_response(200) 622 self.send_header('Content-Type', 'text/html') 623 self.send_header('Content-Length', len(html)) 624 self.end_headers() 625 self.wfile.write(html) 626 return True 627 628 class SyncServerRunner(testserver_base.TestServerRunner): 629 """TestServerRunner for the net test servers.""" 630 631 def __init__(self): 632 super(SyncServerRunner, self).__init__() 633 634 def create_server(self, server_data): 635 port = self.options.port 636 host = self.options.host 637 xmpp_port = self.options.xmpp_port 638 server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler) 639 print ('Sync HTTP server started at %s:%d/chromiumsync...' % 640 (host, server.server_port)) 641 print ('Fake OAuth2 Token server started at %s:%d/o/oauth2/token...' % 642 (host, server.server_port)) 643 print ('Sync XMPP server started at %s:%d...' % 644 (host, server.xmpp_port)) 645 server_data['port'] = server.server_port 646 server_data['xmpp_port'] = server.xmpp_port 647 return server 648 649 def run_server(self): 650 testserver_base.TestServerRunner.run_server(self) 651 652 def add_options(self): 653 testserver_base.TestServerRunner.add_options(self) 654 self.option_parser.add_option('--xmpp-port', default='0', type='int', 655 help='Port used by the XMPP server. If ' 656 'unspecified, the XMPP server will listen on ' 657 'an ephemeral port.') 658 # Override the default logfile name used in testserver.py. 659 self.option_parser.set_defaults(log_file='sync_testserver.log') 660 661 if __name__ == '__main__': 662 sys.exit(SyncServerRunner().main()) 663