Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / sync / tools / testserver / sync_testserver.py
blobd65be40ffbbffdc3dbe95889d9902e9d7057f76d
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 handle_request(self):
74 """Adaptation of asyncore.loop"""
75 def HandleXmppSocket(fd, socket_map, handler):
76 """Runs the handler for the xmpp connection for fd.
78 Adapted from asyncore.read() et al.
79 """
81 xmpp_connection = socket_map.get(fd)
82 # This could happen if a previous handler call caused fd to get
83 # removed from socket_map.
84 if xmpp_connection is None:
85 return
86 try:
87 handler(xmpp_connection)
88 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
89 raise
90 except:
91 xmpp_connection.handle_error()
93 read_fds = [ self.fileno() ]
94 write_fds = []
95 exceptional_fds = []
97 for fd, xmpp_connection in self._xmpp_socket_map.items():
98 is_r = xmpp_connection.readable()
99 is_w = xmpp_connection.writable()
100 if is_r:
101 read_fds.append(fd)
102 if is_w:
103 write_fds.append(fd)
104 if is_r or is_w:
105 exceptional_fds.append(fd)
107 try:
108 read_fds, write_fds, exceptional_fds = (
109 select.select(read_fds, write_fds, exceptional_fds))
110 except select.error, err:
111 if err.args[0] != errno.EINTR:
112 raise
113 else:
114 return
116 for fd in read_fds:
117 if fd == self.fileno():
118 self.HandleRequestNoBlock()
119 return
120 HandleXmppSocket(fd, self._xmpp_socket_map,
121 asyncore.dispatcher.handle_read_event)
123 for fd in write_fds:
124 HandleXmppSocket(fd, self._xmpp_socket_map,
125 asyncore.dispatcher.handle_write_event)
127 for fd in exceptional_fds:
128 HandleXmppSocket(fd, self._xmpp_socket_map,
129 asyncore.dispatcher.handle_expt_event)
132 class SyncPageHandler(testserver_base.BasePageHandler):
133 """Handler for the main HTTP sync server."""
135 def __init__(self, request, client_address, sync_http_server):
136 get_handlers = [self.ChromiumSyncTimeHandler,
137 self.ChromiumSyncMigrationOpHandler,
138 self.ChromiumSyncCredHandler,
139 self.ChromiumSyncXmppCredHandler,
140 self.ChromiumSyncDisableNotificationsOpHandler,
141 self.ChromiumSyncEnableNotificationsOpHandler,
142 self.ChromiumSyncSendNotificationOpHandler,
143 self.ChromiumSyncBirthdayErrorOpHandler,
144 self.ChromiumSyncTransientErrorOpHandler,
145 self.ChromiumSyncErrorOpHandler,
146 self.ChromiumSyncSyncTabFaviconsOpHandler,
147 self.ChromiumSyncCreateSyncedBookmarksOpHandler,
148 self.ChromiumSyncEnableKeystoreEncryptionOpHandler,
149 self.ChromiumSyncRotateKeystoreKeysOpHandler,
150 self.ChromiumSyncEnableManagedUserAcknowledgementHandler,
151 self.ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler,
152 self.GaiaOAuth2TokenHandler,
153 self.GaiaSetOAuth2TokenResponseHandler,
154 self.CustomizeClientCommandHandler]
156 post_handlers = [self.ChromiumSyncCommandHandler,
157 self.ChromiumSyncTimeHandler,
158 self.GaiaOAuth2TokenHandler,
159 self.GaiaSetOAuth2TokenResponseHandler]
160 testserver_base.BasePageHandler.__init__(self, request, client_address,
161 sync_http_server, [], get_handlers,
162 [], post_handlers, [])
165 def ChromiumSyncTimeHandler(self):
166 """Handle Chromium sync .../time requests.
168 The syncer sometimes checks server reachability by examining /time.
171 test_name = "/chromiumsync/time"
172 if not self._ShouldHandleRequest(test_name):
173 return False
175 # Chrome hates it if we send a response before reading the request.
176 if self.headers.getheader('content-length'):
177 length = int(self.headers.getheader('content-length'))
178 _raw_request = self.rfile.read(length)
180 self.send_response(200)
181 self.send_header('Content-Type', 'text/plain')
182 self.end_headers()
183 self.wfile.write('0123456789')
184 return True
186 def ChromiumSyncCommandHandler(self):
187 """Handle a chromiumsync command arriving via http.
189 This covers all sync protocol commands: authentication, getupdates, and
190 commit.
193 test_name = "/chromiumsync/command"
194 if not self._ShouldHandleRequest(test_name):
195 return False
197 length = int(self.headers.getheader('content-length'))
198 raw_request = self.rfile.read(length)
199 http_response = 200
200 raw_reply = None
201 if not self.server.GetAuthenticated():
202 http_response = 401
203 challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
204 self.server.server_address[0])
205 else:
206 http_response, raw_reply = self.server.HandleCommand(
207 self.path, raw_request)
209 ### Now send the response to the client. ###
210 self.send_response(http_response)
211 if http_response == 401:
212 self.send_header('www-Authenticate', challenge)
213 self.end_headers()
214 self.wfile.write(raw_reply)
215 return True
217 def ChromiumSyncMigrationOpHandler(self):
218 test_name = "/chromiumsync/migrate"
219 if not self._ShouldHandleRequest(test_name):
220 return False
222 http_response, raw_reply = self.server._sync_handler.HandleMigrate(
223 self.path)
224 self.send_response(http_response)
225 self.send_header('Content-Type', 'text/html')
226 self.send_header('Content-Length', len(raw_reply))
227 self.end_headers()
228 self.wfile.write(raw_reply)
229 return True
231 def ChromiumSyncCredHandler(self):
232 test_name = "/chromiumsync/cred"
233 if not self._ShouldHandleRequest(test_name):
234 return False
235 try:
236 query = urlparse.urlparse(self.path)[4]
237 cred_valid = urlparse.parse_qs(query)['valid']
238 if cred_valid[0] == 'True':
239 self.server.SetAuthenticated(True)
240 else:
241 self.server.SetAuthenticated(False)
242 except Exception:
243 self.server.SetAuthenticated(False)
245 http_response = 200
246 raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated()
247 self.send_response(http_response)
248 self.send_header('Content-Type', 'text/html')
249 self.send_header('Content-Length', len(raw_reply))
250 self.end_headers()
251 self.wfile.write(raw_reply)
252 return True
254 def ChromiumSyncXmppCredHandler(self):
255 test_name = "/chromiumsync/xmppcred"
256 if not self._ShouldHandleRequest(test_name):
257 return False
258 xmpp_server = self.server.GetXmppServer()
259 try:
260 query = urlparse.urlparse(self.path)[4]
261 cred_valid = urlparse.parse_qs(query)['valid']
262 if cred_valid[0] == 'True':
263 xmpp_server.SetAuthenticated(True)
264 else:
265 xmpp_server.SetAuthenticated(False)
266 except:
267 xmpp_server.SetAuthenticated(False)
269 http_response = 200
270 raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated()
271 self.send_response(http_response)
272 self.send_header('Content-Type', 'text/html')
273 self.send_header('Content-Length', len(raw_reply))
274 self.end_headers()
275 self.wfile.write(raw_reply)
276 return True
278 def ChromiumSyncDisableNotificationsOpHandler(self):
279 test_name = "/chromiumsync/disablenotifications"
280 if not self._ShouldHandleRequest(test_name):
281 return False
282 self.server.GetXmppServer().DisableNotifications()
283 result = 200
284 raw_reply = ('<html><title>Notifications disabled</title>'
285 '<H1>Notifications disabled</H1></html>')
286 self.send_response(result)
287 self.send_header('Content-Type', 'text/html')
288 self.send_header('Content-Length', len(raw_reply))
289 self.end_headers()
290 self.wfile.write(raw_reply)
291 return True
293 def ChromiumSyncEnableNotificationsOpHandler(self):
294 test_name = "/chromiumsync/enablenotifications"
295 if not self._ShouldHandleRequest(test_name):
296 return False
297 self.server.GetXmppServer().EnableNotifications()
298 result = 200
299 raw_reply = ('<html><title>Notifications enabled</title>'
300 '<H1>Notifications enabled</H1></html>')
301 self.send_response(result)
302 self.send_header('Content-Type', 'text/html')
303 self.send_header('Content-Length', len(raw_reply))
304 self.end_headers()
305 self.wfile.write(raw_reply)
306 return True
308 def ChromiumSyncSendNotificationOpHandler(self):
309 test_name = "/chromiumsync/sendnotification"
310 if not self._ShouldHandleRequest(test_name):
311 return False
312 query = urlparse.urlparse(self.path)[4]
313 query_params = urlparse.parse_qs(query)
314 channel = ''
315 data = ''
316 if 'channel' in query_params:
317 channel = query_params['channel'][0]
318 if 'data' in query_params:
319 data = query_params['data'][0]
320 self.server.GetXmppServer().SendNotification(channel, data)
321 result = 200
322 raw_reply = ('<html><title>Notification sent</title>'
323 '<H1>Notification sent with channel "%s" '
324 'and data "%s"</H1></html>'
325 % (channel, data))
326 self.send_response(result)
327 self.send_header('Content-Type', 'text/html')
328 self.send_header('Content-Length', len(raw_reply))
329 self.end_headers()
330 self.wfile.write(raw_reply)
331 return True
333 def ChromiumSyncBirthdayErrorOpHandler(self):
334 test_name = "/chromiumsync/birthdayerror"
335 if not self._ShouldHandleRequest(test_name):
336 return False
337 result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError()
338 self.send_response(result)
339 self.send_header('Content-Type', 'text/html')
340 self.send_header('Content-Length', len(raw_reply))
341 self.end_headers()
342 self.wfile.write(raw_reply)
343 return True
345 def ChromiumSyncTransientErrorOpHandler(self):
346 test_name = "/chromiumsync/transienterror"
347 if not self._ShouldHandleRequest(test_name):
348 return False
349 result, raw_reply = self.server._sync_handler.HandleSetTransientError()
350 self.send_response(result)
351 self.send_header('Content-Type', 'text/html')
352 self.send_header('Content-Length', len(raw_reply))
353 self.end_headers()
354 self.wfile.write(raw_reply)
355 return True
357 def ChromiumSyncErrorOpHandler(self):
358 test_name = "/chromiumsync/error"
359 if not self._ShouldHandleRequest(test_name):
360 return False
361 result, raw_reply = self.server._sync_handler.HandleSetInducedError(
362 self.path)
363 self.send_response(result)
364 self.send_header('Content-Type', 'text/html')
365 self.send_header('Content-Length', len(raw_reply))
366 self.end_headers()
367 self.wfile.write(raw_reply)
368 return True
370 def ChromiumSyncSyncTabFaviconsOpHandler(self):
371 test_name = "/chromiumsync/synctabfavicons"
372 if not self._ShouldHandleRequest(test_name):
373 return False
374 result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons()
375 self.send_response(result)
376 self.send_header('Content-Type', 'text/html')
377 self.send_header('Content-Length', len(raw_reply))
378 self.end_headers()
379 self.wfile.write(raw_reply)
380 return True
382 def ChromiumSyncCreateSyncedBookmarksOpHandler(self):
383 test_name = "/chromiumsync/createsyncedbookmarks"
384 if not self._ShouldHandleRequest(test_name):
385 return False
386 result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks()
387 self.send_response(result)
388 self.send_header('Content-Type', 'text/html')
389 self.send_header('Content-Length', len(raw_reply))
390 self.end_headers()
391 self.wfile.write(raw_reply)
392 return True
394 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self):
395 test_name = "/chromiumsync/enablekeystoreencryption"
396 if not self._ShouldHandleRequest(test_name):
397 return False
398 result, raw_reply = (
399 self.server._sync_handler.HandleEnableKeystoreEncryption())
400 self.send_response(result)
401 self.send_header('Content-Type', 'text/html')
402 self.send_header('Content-Length', len(raw_reply))
403 self.end_headers()
404 self.wfile.write(raw_reply)
405 return True
407 def ChromiumSyncRotateKeystoreKeysOpHandler(self):
408 test_name = "/chromiumsync/rotatekeystorekeys"
409 if not self._ShouldHandleRequest(test_name):
410 return False
411 result, raw_reply = (
412 self.server._sync_handler.HandleRotateKeystoreKeys())
413 self.send_response(result)
414 self.send_header('Content-Type', 'text/html')
415 self.send_header('Content-Length', len(raw_reply))
416 self.end_headers()
417 self.wfile.write(raw_reply)
418 return True
420 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self):
421 test_name = "/chromiumsync/enablemanageduseracknowledgement"
422 if not self._ShouldHandleRequest(test_name):
423 return False
424 result, raw_reply = (
425 self.server._sync_handler.HandleEnableManagedUserAcknowledgement())
426 self.send_response(result)
427 self.send_header('Content-Type', 'text/html')
428 self.send_header('Content-Length', len(raw_reply))
429 self.end_headers()
430 self.wfile.write(raw_reply)
431 return True
433 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self):
434 test_name = "/chromiumsync/enableprecommitgetupdateavoidance"
435 if not self._ShouldHandleRequest(test_name):
436 return False
437 result, raw_reply = (
438 self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance())
439 self.send_response(result)
440 self.send_header('Content-Type', 'text/html')
441 self.send_header('Content-Length', len(raw_reply))
442 self.end_headers()
443 self.wfile.write(raw_reply)
444 return True
446 def GaiaOAuth2TokenHandler(self):
447 test_name = "/o/oauth2/token"
448 if not self._ShouldHandleRequest(test_name):
449 return False
450 if self.headers.getheader('content-length'):
451 length = int(self.headers.getheader('content-length'))
452 _raw_request = self.rfile.read(length)
453 result, raw_reply = (
454 self.server._sync_handler.HandleGetOauth2Token())
455 self.send_response(result)
456 self.send_header('Content-Type', 'application/json')
457 self.send_header('Content-Length', len(raw_reply))
458 self.end_headers()
459 self.wfile.write(raw_reply)
460 return True
462 def GaiaSetOAuth2TokenResponseHandler(self):
463 test_name = "/setfakeoauth2token"
464 if not self._ShouldHandleRequest(test_name):
465 return False
467 # The index of 'query' is 4.
468 # See http://docs.python.org/2/library/urlparse.html
469 query = urlparse.urlparse(self.path)[4]
470 query_params = urlparse.parse_qs(query)
472 response_code = 0
473 request_token = ''
474 access_token = ''
475 expires_in = 0
476 token_type = ''
478 if 'response_code' in query_params:
479 response_code = query_params['response_code'][0]
480 if 'request_token' in query_params:
481 request_token = query_params['request_token'][0]
482 if 'access_token' in query_params:
483 access_token = query_params['access_token'][0]
484 if 'expires_in' in query_params:
485 expires_in = query_params['expires_in'][0]
486 if 'token_type' in query_params:
487 token_type = query_params['token_type'][0]
489 result, raw_reply = (
490 self.server._sync_handler.HandleSetOauth2Token(
491 response_code, request_token, access_token, expires_in, token_type))
492 self.send_response(result)
493 self.send_header('Content-Type', 'text/html')
494 self.send_header('Content-Length', len(raw_reply))
495 self.end_headers()
496 self.wfile.write(raw_reply)
497 return True
499 def CustomizeClientCommandHandler(self):
500 test_name = "/customizeclientcommand"
501 if not self._ShouldHandleRequest(test_name):
502 return False
504 query = urlparse.urlparse(self.path)[4]
505 query_params = urlparse.parse_qs(query)
507 if 'sessions_commit_delay_seconds' in query_params:
508 sessions_commit_delay = query_params['sessions_commit_delay_seconds'][0]
509 try:
510 command_string = self.server._sync_handler.CustomizeClientCommand(
511 int(sessions_commit_delay))
512 response_code = 200
513 reply = "The ClientCommand was customized:\n\n"
514 reply += "<code>{}</code>.".format(command_string)
515 except ValueError:
516 response_code = 400
517 reply = "sessions_commit_delay_seconds was not an int"
518 else:
519 response_code = 400
520 reply = "sessions_commit_delay_seconds is required"
522 self.send_response(response_code)
523 self.send_header('Content-Type', 'text/html')
524 self.send_header('Content-Length', len(reply))
525 self.end_headers()
526 self.wfile.write(reply)
527 return True
529 class SyncServerRunner(testserver_base.TestServerRunner):
530 """TestServerRunner for the net test servers."""
532 def __init__(self):
533 super(SyncServerRunner, self).__init__()
535 def create_server(self, server_data):
536 port = self.options.port
537 host = self.options.host
538 xmpp_port = self.options.xmpp_port
539 server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler)
540 print ('Sync HTTP server started at %s:%d/chromiumsync...' %
541 (host, server.server_port))
542 print ('Fake OAuth2 Token server started at %s:%d/o/oauth2/token...' %
543 (host, server.server_port))
544 print ('Sync XMPP server started at %s:%d...' %
545 (host, server.xmpp_port))
546 server_data['port'] = server.server_port
547 server_data['xmpp_port'] = server.xmpp_port
548 return server
550 def run_server(self):
551 testserver_base.TestServerRunner.run_server(self)
553 def add_options(self):
554 testserver_base.TestServerRunner.add_options(self)
555 self.option_parser.add_option('--xmpp-port', default='0', type='int',
556 help='Port used by the XMPP server. If '
557 'unspecified, the XMPP server will listen on '
558 'an ephemeral port.')
559 # Override the default logfile name used in testserver.py.
560 self.option_parser.set_defaults(log_file='sync_testserver.log')
562 if __name__ == '__main__':
563 sys.exit(SyncServerRunner().main())