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