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 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 444 class 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 473 if __name__ == '__main__': 474 sys.exit(SyncServerRunner().main()) 475