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.
6 """This is a python sync server used for testing Chrome Sync.
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.
24 import testserver_base
28 class SyncHTTPServer(testserver_base.ClientRestrictingServerMixIn,
29 testserver_base.BrokenPipeHandlerMixIn,
30 testserver_base.StoppableHTTPServer):
31 """An HTTP server that handles sync commands."""
33 def __init__(self, server_address, xmpp_port, request_handler_class):
34 testserver_base.StoppableHTTPServer.__init__(self,
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
44 def GetXmppServer(self):
45 return self._xmpp_server
47 def HandleCommand(self, query, raw_request):
48 return self._sync_handler.HandleCommand(query, raw_request)
50 def HandleRequestNoBlock(self):
51 """Handles a single request.
53 Copied from SocketServer._handle_request_noblock().
57 request, client_address = self.get_request()
60 if self.verify_request(request, client_address):
62 self.process_request(request, client_address)
64 self.handle_error(request, client_address)
65 self.close_request(request)
67 def SetAuthenticated(self, auth_valid):
68 self.authenticated = auth_valid
70 def GetAuthenticated(self):
71 return self.authenticated
73 def serve_forever(self):
74 """This is a merge of asyncore.loop() and SocketServer.serve_forever().
77 def HandleXmppSocket(fd, socket_map, handler):
78 """Runs the handler for the xmpp connection for fd.
80 Adapted from asyncore.read() et al.
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:
89 handler(xmpp_connection)
90 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
93 xmpp_connection.handle_error()
96 read_fds = [ self.fileno() ]
100 for fd, xmpp_connection in self._xmpp_socket_map.items():
101 is_r = xmpp_connection.readable()
102 is_w = xmpp_connection.writable()
108 exceptional_fds.append(fd)
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:
120 if fd == self.fileno():
121 self.HandleRequestNoBlock()
123 HandleXmppSocket(fd, self._xmpp_socket_map,
124 asyncore.dispatcher.handle_read_event)
127 HandleXmppSocket(fd, self._xmpp_socket_map,
128 asyncore.dispatcher.handle_write_event)
130 for fd in exceptional_fds:
131 HandleXmppSocket(fd, self._xmpp_socket_map,
132 asyncore.dispatcher.handle_expt_event)
135 class SyncPageHandler(testserver_base.BasePageHandler):
136 """Handler for the main HTTP sync server."""
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]
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, [])
172 def ChromiumSyncTimeHandler(self):
173 """Handle Chromium sync .../time requests.
175 The syncer sometimes checks server reachability by examining /time.
178 test_name = "/chromiumsync/time"
179 if not self._ShouldHandleRequest(test_name):
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)
187 self.send_response(200)
188 self.send_header('Content-Type', 'text/plain')
190 self.wfile.write('0123456789')
193 def ChromiumSyncCommandHandler(self):
194 """Handle a chromiumsync command arriving via http.
196 This covers all sync protocol commands: authentication, getupdates, and
200 test_name = "/chromiumsync/command"
201 if not self._ShouldHandleRequest(test_name):
204 length = int(self.headers.getheader('content-length'))
205 raw_request = self.rfile.read(length)
208 if not self.server.GetAuthenticated():
210 challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
211 self.server.server_address[0])
213 http_response, raw_reply = self.server.HandleCommand(
214 self.path, raw_request)
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)
221 self.wfile.write(raw_reply)
224 def ChromiumSyncMigrationOpHandler(self):
225 test_name = "/chromiumsync/migrate"
226 if not self._ShouldHandleRequest(test_name):
229 http_response, raw_reply = self.server._sync_handler.HandleMigrate(
231 self.send_response(http_response)
232 self.send_header('Content-Type', 'text/html')
233 self.send_header('Content-Length', len(raw_reply))
235 self.wfile.write(raw_reply)
238 def ChromiumSyncCredHandler(self):
239 test_name = "/chromiumsync/cred"
240 if not self._ShouldHandleRequest(test_name):
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)
248 self.server.SetAuthenticated(False)
250 self.server.SetAuthenticated(False)
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))
258 self.wfile.write(raw_reply)
261 def ChromiumSyncXmppCredHandler(self):
262 test_name = "/chromiumsync/xmppcred"
263 if not self._ShouldHandleRequest(test_name):
265 xmpp_server = self.server.GetXmppServer()
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)
272 xmpp_server.SetAuthenticated(False)
274 xmpp_server.SetAuthenticated(False)
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))
282 self.wfile.write(raw_reply)
285 def ChromiumSyncDisableNotificationsOpHandler(self):
286 test_name = "/chromiumsync/disablenotifications"
287 if not self._ShouldHandleRequest(test_name):
289 self.server.GetXmppServer().DisableNotifications()
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))
297 self.wfile.write(raw_reply)
300 def ChromiumSyncEnableNotificationsOpHandler(self):
301 test_name = "/chromiumsync/enablenotifications"
302 if not self._ShouldHandleRequest(test_name):
304 self.server.GetXmppServer().EnableNotifications()
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))
312 self.wfile.write(raw_reply)
315 def ChromiumSyncSendNotificationOpHandler(self):
316 test_name = "/chromiumsync/sendnotification"
317 if not self._ShouldHandleRequest(test_name):
319 query = urlparse.urlparse(self.path)[4]
320 query_params = urlparse.parse_qs(query)
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)
329 raw_reply = ('<html><title>Notification sent</title>'
330 '<H1>Notification sent with channel "%s" '
331 'and data "%s"</H1></html>'
333 self.send_response(result)
334 self.send_header('Content-Type', 'text/html')
335 self.send_header('Content-Length', len(raw_reply))
337 self.wfile.write(raw_reply)
340 def ChromiumSyncBirthdayErrorOpHandler(self):
341 test_name = "/chromiumsync/birthdayerror"
342 if not self._ShouldHandleRequest(test_name):
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))
349 self.wfile.write(raw_reply)
352 def ChromiumSyncTransientErrorOpHandler(self):
353 test_name = "/chromiumsync/transienterror"
354 if not self._ShouldHandleRequest(test_name):
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))
361 self.wfile.write(raw_reply)
364 def ChromiumSyncErrorOpHandler(self):
365 test_name = "/chromiumsync/error"
366 if not self._ShouldHandleRequest(test_name):
368 result, raw_reply = self.server._sync_handler.HandleSetInducedError(
370 self.send_response(result)
371 self.send_header('Content-Type', 'text/html')
372 self.send_header('Content-Length', len(raw_reply))
374 self.wfile.write(raw_reply)
377 def ChromiumSyncSyncTabFaviconsOpHandler(self):
378 test_name = "/chromiumsync/synctabfavicons"
379 if not self._ShouldHandleRequest(test_name):
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))
386 self.wfile.write(raw_reply)
389 def ChromiumSyncCreateSyncedBookmarksOpHandler(self):
390 test_name = "/chromiumsync/createsyncedbookmarks"
391 if not self._ShouldHandleRequest(test_name):
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))
398 self.wfile.write(raw_reply)
401 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self):
402 test_name = "/chromiumsync/enablekeystoreencryption"
403 if not self._ShouldHandleRequest(test_name):
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))
411 self.wfile.write(raw_reply)
414 def ChromiumSyncRotateKeystoreKeysOpHandler(self):
415 test_name = "/chromiumsync/rotatekeystorekeys"
416 if not self._ShouldHandleRequest(test_name):
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))
424 self.wfile.write(raw_reply)
427 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self):
428 test_name = "/chromiumsync/enablemanageduseracknowledgement"
429 if not self._ShouldHandleRequest(test_name):
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))
437 self.wfile.write(raw_reply)
440 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self):
441 test_name = "/chromiumsync/enableprecommitgetupdateavoidance"
442 if not self._ShouldHandleRequest(test_name):
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))
450 self.wfile.write(raw_reply)
453 def GaiaOAuth2TokenHandler(self):
454 test_name = "/o/oauth2/token"
455 if not self._ShouldHandleRequest(test_name):
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))
466 self.wfile.write(raw_reply)
469 def GaiaSetOAuth2TokenResponseHandler(self):
470 test_name = "/setfakeoauth2token"
471 if not self._ShouldHandleRequest(test_name):
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)
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]
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))
503 self.wfile.write(raw_reply)
506 def TriggerSyncedNotificationHandler(self):
507 test_name = "/triggersyncednotification"
508 if not self._ShouldHandleRequest(test_name):
511 query = urlparse.urlparse(self.path)[4]
512 query_params = urlparse.parse_qs(query)
514 serialized_notification = ''
516 if 'serialized_notification' in query_params:
517 serialized_notification = query_params['serialized_notification'][0]
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)
525 except chromiumsync.ClientNotConnectedError:
526 reply = ('The client is not connected to the server, so the notification'
527 ' could not be created.')
530 self.send_response(response_code)
531 self.send_header('Content-Type', 'text/html')
532 self.send_header('Content-Length', len(reply))
534 self.wfile.write(reply)
537 def TriggerSyncedNotificationAppInfoHandler(self):
538 test_name = "/triggersyncednotificationappinfo"
539 if not self._ShouldHandleRequest(test_name):
542 query = urlparse.urlparse(self.path)[4]
543 query_params = urlparse.parse_qs(query)
547 if 'synced_notification_app_info' in query_params:
548 app_info = query_params['synced_notification_app_info'][0]
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)
556 except chromiumsync.ClientNotConnectedError:
557 reply = ('The client is not connected to the server, so the app info'
558 ' could not be created.')
561 self.send_response(response_code)
562 self.send_header('Content-Type', 'text/html')
563 self.send_header('Content-Length', len(reply))
565 self.wfile.write(reply)
568 def CustomizeClientCommandHandler(self):
569 test_name = "/customizeclientcommand"
570 if not self._ShouldHandleRequest(test_name):
573 query = urlparse.urlparse(self.path)[4]
574 query_params = urlparse.parse_qs(query)
576 if 'sessions_commit_delay_seconds' in query_params:
577 sessions_commit_delay = query_params['sessions_commit_delay_seconds'][0]
579 command_string = self.server._sync_handler.CustomizeClientCommand(
580 int(sessions_commit_delay))
582 reply = "The ClientCommand was customized:\n\n"
583 reply += "<code>{}</code>.".format(command_string)
586 reply = "sessions_commit_delay_seconds was not an int"
589 reply = "sessions_commit_delay_seconds is required"
591 self.send_response(response_code)
592 self.send_header('Content-Type', 'text/html')
593 self.send_header('Content-Length', len(reply))
595 self.wfile.write(reply)
598 def SyncedNotificationsPageHandler(self):
599 test_name = "/syncednotifications"
600 if not self._ShouldHandleRequest(test_name):
603 html = open('sync/tools/testserver/synced_notifications.html', 'r').read()
605 self.send_response(200)
606 self.send_header('Content-Type', 'text/html')
607 self.send_header('Content-Length', len(html))
609 self.wfile.write(html)
612 def SyncedNotificationsAppInfoPageHandler(self):
613 test_name = "/syncednotificationsappinfo"
614 if not self._ShouldHandleRequest(test_name):
618 open('sync/tools/testserver/synced_notification_app_info.html', 'r').\
621 self.send_response(200)
622 self.send_header('Content-Type', 'text/html')
623 self.send_header('Content-Length', len(html))
625 self.wfile.write(html)
628 class SyncServerRunner(testserver_base.TestServerRunner):
629 """TestServerRunner for the net test servers."""
632 super(SyncServerRunner, self).__init__()
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
649 def run_server(self):
650 testserver_base.TestServerRunner.run_server(self)
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')
661 if __name__ == '__main__':
662 sys.exit(SyncServerRunner().main())