Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / sync / tools / testserver / sync_testserver.py
blob66789ead097e9ac41d6b7d48d8c83a2c4c0554dd
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.
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.
11 """
13 import asyncore
14 import BaseHTTPServer
15 import errno
16 import os
17 import select
18 import socket
19 import sys
20 import urlparse
22 import chromiumsync
23 import echo_message
24 import testserver_base
25 import xmppserver
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,
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
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().
54 """
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)
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().
75 """
77 def HandleXmppSocket(fd, socket_map, handler):
78 """Runs the handler for the xmpp connection for fd.
80 Adapted from asyncore.read() et al.
81 """
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()
95 while True:
96 read_fds = [ self.fileno() ]
97 write_fds = []
98 exceptional_fds = []
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)
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
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)
126 for fd in write_fds:
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):
180 return False
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')
189 self.end_headers()
190 self.wfile.write('0123456789')
191 return True
193 def ChromiumSyncCommandHandler(self):
194 """Handle a chromiumsync command arriving via http.
196 This covers all sync protocol commands: authentication, getupdates, and
197 commit.
200 test_name = "/chromiumsync/command"
201 if not self._ShouldHandleRequest(test_name):
202 return False
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)
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
224 def ChromiumSyncMigrationOpHandler(self):
225 test_name = "/chromiumsync/migrate"
226 if not self._ShouldHandleRequest(test_name):
227 return False
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
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)
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
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)
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
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
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
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
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
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
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
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
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
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
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
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
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
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
469 def GaiaSetOAuth2TokenResponseHandler(self):
470 test_name = "/setfakeoauth2token"
471 if not self._ShouldHandleRequest(test_name):
472 return False
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)
479 response_code = 0
480 request_token = ''
481 access_token = ''
482 expires_in = 0
483 token_type = ''
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))
502 self.end_headers()
503 self.wfile.write(raw_reply)
504 return True
506 def TriggerSyncedNotificationHandler(self):
507 test_name = "/triggersyncednotification"
508 if not self._ShouldHandleRequest(test_name):
509 return False
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]
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
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
537 def TriggerSyncedNotificationAppInfoHandler(self):
538 test_name = "/triggersyncednotificationappinfo"
539 if not self._ShouldHandleRequest(test_name):
540 return False
542 query = urlparse.urlparse(self.path)[4]
543 query_params = urlparse.parse_qs(query)
545 app_info = ''
547 if 'synced_notification_app_info' in query_params:
548 app_info = query_params['synced_notification_app_info'][0]
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
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
568 def CustomizeClientCommandHandler(self):
569 test_name = "/customizeclientcommand"
570 if not self._ShouldHandleRequest(test_name):
571 return False
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]
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"
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
598 def SyncedNotificationsPageHandler(self):
599 test_name = "/syncednotifications"
600 if not self._ShouldHandleRequest(test_name):
601 return False
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))
608 self.end_headers()
609 self.wfile.write(html)
610 return True
612 def SyncedNotificationsAppInfoPageHandler(self):
613 test_name = "/syncednotificationsappinfo"
614 if not self._ShouldHandleRequest(test_name):
615 return False
617 html = \
618 open('sync/tools/testserver/synced_notification_app_info.html', 'r').\
619 read()
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
628 class SyncServerRunner(testserver_base.TestServerRunner):
629 """TestServerRunner for the net test servers."""
631 def __init__(self):
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
647 return server
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())