Add 'did_proceed' and 'repeat_visit' to ClientMalwareReportRequest to track CTR.
[chromium-blink-merge.git] / sync / tools / testserver / sync_testserver.py
blob32c746ea615f7e4b6f923bb4751ae07b71b78d9b
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 gzip
17 import os
18 import select
19 import StringIO
20 import socket
21 import sys
22 import urlparse
24 import chromiumsync
25 import echo_message
26 import testserver_base
27 import xmppserver
30 class SyncHTTPServer(testserver_base.ClientRestrictingServerMixIn,
31 testserver_base.BrokenPipeHandlerMixIn,
32 testserver_base.StoppableHTTPServer):
33 """An HTTP server that handles sync commands."""
35 def __init__(self, server_address, xmpp_port, request_handler_class):
36 testserver_base.StoppableHTTPServer.__init__(self,
37 server_address,
38 request_handler_class)
39 self._sync_handler = chromiumsync.TestServer()
40 self._xmpp_socket_map = {}
41 self._xmpp_server = xmppserver.XmppServer(
42 self._xmpp_socket_map, ('localhost', xmpp_port))
43 self.xmpp_port = self._xmpp_server.getsockname()[1]
44 self.authenticated = True
46 def GetXmppServer(self):
47 return self._xmpp_server
49 def HandleCommand(self, query, raw_request):
50 return self._sync_handler.HandleCommand(query, raw_request)
52 def HandleRequestNoBlock(self):
53 """Handles a single request.
55 Copied from SocketServer._handle_request_noblock().
56 """
58 try:
59 request, client_address = self.get_request()
60 except socket.error:
61 return
62 if self.verify_request(request, client_address):
63 try:
64 self.process_request(request, client_address)
65 except Exception:
66 self.handle_error(request, client_address)
67 self.close_request(request)
69 def SetAuthenticated(self, auth_valid):
70 self.authenticated = auth_valid
72 def GetAuthenticated(self):
73 return self.authenticated
75 def handle_request(self):
76 """Adaptation of asyncore.loop"""
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 read_fds = [ self.fileno() ]
96 write_fds = []
97 exceptional_fds = []
99 for fd, xmpp_connection in self._xmpp_socket_map.items():
100 is_r = xmpp_connection.readable()
101 is_w = xmpp_connection.writable()
102 if is_r:
103 read_fds.append(fd)
104 if is_w:
105 write_fds.append(fd)
106 if is_r or is_w:
107 exceptional_fds.append(fd)
109 try:
110 read_fds, write_fds, exceptional_fds = (
111 select.select(read_fds, write_fds, exceptional_fds))
112 except select.error, err:
113 if err.args[0] != errno.EINTR:
114 raise
115 else:
116 return
118 for fd in read_fds:
119 if fd == self.fileno():
120 self.HandleRequestNoBlock()
121 return
122 HandleXmppSocket(fd, self._xmpp_socket_map,
123 asyncore.dispatcher.handle_read_event)
125 for fd in write_fds:
126 HandleXmppSocket(fd, self._xmpp_socket_map,
127 asyncore.dispatcher.handle_write_event)
129 for fd in exceptional_fds:
130 HandleXmppSocket(fd, self._xmpp_socket_map,
131 asyncore.dispatcher.handle_expt_event)
134 class SyncPageHandler(testserver_base.BasePageHandler):
135 """Handler for the main HTTP sync server."""
137 def __init__(self, request, client_address, sync_http_server):
138 get_handlers = [self.ChromiumSyncTimeHandler,
139 self.ChromiumSyncMigrationOpHandler,
140 self.ChromiumSyncCredHandler,
141 self.ChromiumSyncXmppCredHandler,
142 self.ChromiumSyncDisableNotificationsOpHandler,
143 self.ChromiumSyncEnableNotificationsOpHandler,
144 self.ChromiumSyncSendNotificationOpHandler,
145 self.ChromiumSyncBirthdayErrorOpHandler,
146 self.ChromiumSyncTransientErrorOpHandler,
147 self.ChromiumSyncErrorOpHandler,
148 self.ChromiumSyncSyncTabFaviconsOpHandler,
149 self.ChromiumSyncCreateSyncedBookmarksOpHandler,
150 self.ChromiumSyncEnableKeystoreEncryptionOpHandler,
151 self.ChromiumSyncRotateKeystoreKeysOpHandler,
152 self.ChromiumSyncEnableManagedUserAcknowledgementHandler,
153 self.ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler,
154 self.GaiaOAuth2TokenHandler,
155 self.GaiaSetOAuth2TokenResponseHandler,
156 self.CustomizeClientCommandHandler]
158 post_handlers = [self.ChromiumSyncCommandHandler,
159 self.ChromiumSyncTimeHandler,
160 self.GaiaOAuth2TokenHandler,
161 self.GaiaSetOAuth2TokenResponseHandler]
162 testserver_base.BasePageHandler.__init__(self, request, client_address,
163 sync_http_server, [], get_handlers,
164 [], post_handlers, [])
167 def ChromiumSyncTimeHandler(self):
168 """Handle Chromium sync .../time requests.
170 The syncer sometimes checks server reachability by examining /time.
173 test_name = "/chromiumsync/time"
174 if not self._ShouldHandleRequest(test_name):
175 return False
177 # Chrome hates it if we send a response before reading the request.
178 if self.headers.getheader('content-length'):
179 length = int(self.headers.getheader('content-length'))
180 _raw_request = self.rfile.read(length)
182 self.send_response(200)
183 self.send_header('Content-Type', 'text/plain')
184 self.end_headers()
185 self.wfile.write('0123456789')
186 return True
188 def ChromiumSyncCommandHandler(self):
189 """Handle a chromiumsync command arriving via http.
191 This covers all sync protocol commands: authentication, getupdates, and
192 commit.
195 test_name = "/chromiumsync/command"
196 if not self._ShouldHandleRequest(test_name):
197 return False
199 length = int(self.headers.getheader('content-length'))
200 raw_request = self.rfile.read(length)
201 if self.headers.getheader('Content-Encoding'):
202 encode = self.headers.getheader('Content-Encoding')
203 if encode == "gzip":
204 raw_request = gzip.GzipFile(
205 fileobj=StringIO.StringIO(raw_request)).read()
207 http_response = 200
208 raw_reply = None
209 if not self.server.GetAuthenticated():
210 http_response = 401
211 challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
212 self.server.server_address[0])
213 else:
214 http_response, raw_reply = self.server.HandleCommand(
215 self.path, raw_request)
217 ### Now send the response to the client. ###
218 self.send_response(http_response)
219 if http_response == 401:
220 self.send_header('www-Authenticate', challenge)
221 self.end_headers()
222 self.wfile.write(raw_reply)
223 return True
225 def ChromiumSyncMigrationOpHandler(self):
226 test_name = "/chromiumsync/migrate"
227 if not self._ShouldHandleRequest(test_name):
228 return False
230 http_response, raw_reply = self.server._sync_handler.HandleMigrate(
231 self.path)
232 self.send_response(http_response)
233 self.send_header('Content-Type', 'text/html')
234 self.send_header('Content-Length', len(raw_reply))
235 self.end_headers()
236 self.wfile.write(raw_reply)
237 return True
239 def ChromiumSyncCredHandler(self):
240 test_name = "/chromiumsync/cred"
241 if not self._ShouldHandleRequest(test_name):
242 return False
243 try:
244 query = urlparse.urlparse(self.path)[4]
245 cred_valid = urlparse.parse_qs(query)['valid']
246 if cred_valid[0] == 'True':
247 self.server.SetAuthenticated(True)
248 else:
249 self.server.SetAuthenticated(False)
250 except Exception:
251 self.server.SetAuthenticated(False)
253 http_response = 200
254 raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated()
255 self.send_response(http_response)
256 self.send_header('Content-Type', 'text/html')
257 self.send_header('Content-Length', len(raw_reply))
258 self.end_headers()
259 self.wfile.write(raw_reply)
260 return True
262 def ChromiumSyncXmppCredHandler(self):
263 test_name = "/chromiumsync/xmppcred"
264 if not self._ShouldHandleRequest(test_name):
265 return False
266 xmpp_server = self.server.GetXmppServer()
267 try:
268 query = urlparse.urlparse(self.path)[4]
269 cred_valid = urlparse.parse_qs(query)['valid']
270 if cred_valid[0] == 'True':
271 xmpp_server.SetAuthenticated(True)
272 else:
273 xmpp_server.SetAuthenticated(False)
274 except:
275 xmpp_server.SetAuthenticated(False)
277 http_response = 200
278 raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated()
279 self.send_response(http_response)
280 self.send_header('Content-Type', 'text/html')
281 self.send_header('Content-Length', len(raw_reply))
282 self.end_headers()
283 self.wfile.write(raw_reply)
284 return True
286 def ChromiumSyncDisableNotificationsOpHandler(self):
287 test_name = "/chromiumsync/disablenotifications"
288 if not self._ShouldHandleRequest(test_name):
289 return False
290 self.server.GetXmppServer().DisableNotifications()
291 result = 200
292 raw_reply = ('<html><title>Notifications disabled</title>'
293 '<H1>Notifications disabled</H1></html>')
294 self.send_response(result)
295 self.send_header('Content-Type', 'text/html')
296 self.send_header('Content-Length', len(raw_reply))
297 self.end_headers()
298 self.wfile.write(raw_reply)
299 return True
301 def ChromiumSyncEnableNotificationsOpHandler(self):
302 test_name = "/chromiumsync/enablenotifications"
303 if not self._ShouldHandleRequest(test_name):
304 return False
305 self.server.GetXmppServer().EnableNotifications()
306 result = 200
307 raw_reply = ('<html><title>Notifications enabled</title>'
308 '<H1>Notifications enabled</H1></html>')
309 self.send_response(result)
310 self.send_header('Content-Type', 'text/html')
311 self.send_header('Content-Length', len(raw_reply))
312 self.end_headers()
313 self.wfile.write(raw_reply)
314 return True
316 def ChromiumSyncSendNotificationOpHandler(self):
317 test_name = "/chromiumsync/sendnotification"
318 if not self._ShouldHandleRequest(test_name):
319 return False
320 query = urlparse.urlparse(self.path)[4]
321 query_params = urlparse.parse_qs(query)
322 channel = ''
323 data = ''
324 if 'channel' in query_params:
325 channel = query_params['channel'][0]
326 if 'data' in query_params:
327 data = query_params['data'][0]
328 self.server.GetXmppServer().SendNotification(channel, data)
329 result = 200
330 raw_reply = ('<html><title>Notification sent</title>'
331 '<H1>Notification sent with channel "%s" '
332 'and data "%s"</H1></html>'
333 % (channel, data))
334 self.send_response(result)
335 self.send_header('Content-Type', 'text/html')
336 self.send_header('Content-Length', len(raw_reply))
337 self.end_headers()
338 self.wfile.write(raw_reply)
339 return True
341 def ChromiumSyncBirthdayErrorOpHandler(self):
342 test_name = "/chromiumsync/birthdayerror"
343 if not self._ShouldHandleRequest(test_name):
344 return False
345 result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError()
346 self.send_response(result)
347 self.send_header('Content-Type', 'text/html')
348 self.send_header('Content-Length', len(raw_reply))
349 self.end_headers()
350 self.wfile.write(raw_reply)
351 return True
353 def ChromiumSyncTransientErrorOpHandler(self):
354 test_name = "/chromiumsync/transienterror"
355 if not self._ShouldHandleRequest(test_name):
356 return False
357 result, raw_reply = self.server._sync_handler.HandleSetTransientError()
358 self.send_response(result)
359 self.send_header('Content-Type', 'text/html')
360 self.send_header('Content-Length', len(raw_reply))
361 self.end_headers()
362 self.wfile.write(raw_reply)
363 return True
365 def ChromiumSyncErrorOpHandler(self):
366 test_name = "/chromiumsync/error"
367 if not self._ShouldHandleRequest(test_name):
368 return False
369 result, raw_reply = self.server._sync_handler.HandleSetInducedError(
370 self.path)
371 self.send_response(result)
372 self.send_header('Content-Type', 'text/html')
373 self.send_header('Content-Length', len(raw_reply))
374 self.end_headers()
375 self.wfile.write(raw_reply)
376 return True
378 def ChromiumSyncSyncTabFaviconsOpHandler(self):
379 test_name = "/chromiumsync/synctabfavicons"
380 if not self._ShouldHandleRequest(test_name):
381 return False
382 result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons()
383 self.send_response(result)
384 self.send_header('Content-Type', 'text/html')
385 self.send_header('Content-Length', len(raw_reply))
386 self.end_headers()
387 self.wfile.write(raw_reply)
388 return True
390 def ChromiumSyncCreateSyncedBookmarksOpHandler(self):
391 test_name = "/chromiumsync/createsyncedbookmarks"
392 if not self._ShouldHandleRequest(test_name):
393 return False
394 result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks()
395 self.send_response(result)
396 self.send_header('Content-Type', 'text/html')
397 self.send_header('Content-Length', len(raw_reply))
398 self.end_headers()
399 self.wfile.write(raw_reply)
400 return True
402 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self):
403 test_name = "/chromiumsync/enablekeystoreencryption"
404 if not self._ShouldHandleRequest(test_name):
405 return False
406 result, raw_reply = (
407 self.server._sync_handler.HandleEnableKeystoreEncryption())
408 self.send_response(result)
409 self.send_header('Content-Type', 'text/html')
410 self.send_header('Content-Length', len(raw_reply))
411 self.end_headers()
412 self.wfile.write(raw_reply)
413 return True
415 def ChromiumSyncRotateKeystoreKeysOpHandler(self):
416 test_name = "/chromiumsync/rotatekeystorekeys"
417 if not self._ShouldHandleRequest(test_name):
418 return False
419 result, raw_reply = (
420 self.server._sync_handler.HandleRotateKeystoreKeys())
421 self.send_response(result)
422 self.send_header('Content-Type', 'text/html')
423 self.send_header('Content-Length', len(raw_reply))
424 self.end_headers()
425 self.wfile.write(raw_reply)
426 return True
428 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self):
429 test_name = "/chromiumsync/enablemanageduseracknowledgement"
430 if not self._ShouldHandleRequest(test_name):
431 return False
432 result, raw_reply = (
433 self.server._sync_handler.HandleEnableManagedUserAcknowledgement())
434 self.send_response(result)
435 self.send_header('Content-Type', 'text/html')
436 self.send_header('Content-Length', len(raw_reply))
437 self.end_headers()
438 self.wfile.write(raw_reply)
439 return True
441 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self):
442 test_name = "/chromiumsync/enableprecommitgetupdateavoidance"
443 if not self._ShouldHandleRequest(test_name):
444 return False
445 result, raw_reply = (
446 self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance())
447 self.send_response(result)
448 self.send_header('Content-Type', 'text/html')
449 self.send_header('Content-Length', len(raw_reply))
450 self.end_headers()
451 self.wfile.write(raw_reply)
452 return True
454 def GaiaOAuth2TokenHandler(self):
455 test_name = "/o/oauth2/token"
456 if not self._ShouldHandleRequest(test_name):
457 return False
458 if self.headers.getheader('content-length'):
459 length = int(self.headers.getheader('content-length'))
460 _raw_request = self.rfile.read(length)
461 result, raw_reply = (
462 self.server._sync_handler.HandleGetOauth2Token())
463 self.send_response(result)
464 self.send_header('Content-Type', 'application/json')
465 self.send_header('Content-Length', len(raw_reply))
466 self.end_headers()
467 self.wfile.write(raw_reply)
468 return True
470 def GaiaSetOAuth2TokenResponseHandler(self):
471 test_name = "/setfakeoauth2token"
472 if not self._ShouldHandleRequest(test_name):
473 return False
475 # The index of 'query' is 4.
476 # See http://docs.python.org/2/library/urlparse.html
477 query = urlparse.urlparse(self.path)[4]
478 query_params = urlparse.parse_qs(query)
480 response_code = 0
481 request_token = ''
482 access_token = ''
483 expires_in = 0
484 token_type = ''
486 if 'response_code' in query_params:
487 response_code = query_params['response_code'][0]
488 if 'request_token' in query_params:
489 request_token = query_params['request_token'][0]
490 if 'access_token' in query_params:
491 access_token = query_params['access_token'][0]
492 if 'expires_in' in query_params:
493 expires_in = query_params['expires_in'][0]
494 if 'token_type' in query_params:
495 token_type = query_params['token_type'][0]
497 result, raw_reply = (
498 self.server._sync_handler.HandleSetOauth2Token(
499 response_code, request_token, access_token, expires_in, token_type))
500 self.send_response(result)
501 self.send_header('Content-Type', 'text/html')
502 self.send_header('Content-Length', len(raw_reply))
503 self.end_headers()
504 self.wfile.write(raw_reply)
505 return True
507 def CustomizeClientCommandHandler(self):
508 test_name = "/customizeclientcommand"
509 if not self._ShouldHandleRequest(test_name):
510 return False
512 query = urlparse.urlparse(self.path)[4]
513 query_params = urlparse.parse_qs(query)
515 if 'sessions_commit_delay_seconds' in query_params:
516 sessions_commit_delay = query_params['sessions_commit_delay_seconds'][0]
517 try:
518 command_string = self.server._sync_handler.CustomizeClientCommand(
519 int(sessions_commit_delay))
520 response_code = 200
521 reply = "The ClientCommand was customized:\n\n"
522 reply += "<code>{}</code>.".format(command_string)
523 except ValueError:
524 response_code = 400
525 reply = "sessions_commit_delay_seconds was not an int"
526 else:
527 response_code = 400
528 reply = "sessions_commit_delay_seconds is required"
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 class SyncServerRunner(testserver_base.TestServerRunner):
538 """TestServerRunner for the net test servers."""
540 def __init__(self):
541 super(SyncServerRunner, self).__init__()
543 def create_server(self, server_data):
544 port = self.options.port
545 host = self.options.host
546 xmpp_port = self.options.xmpp_port
547 server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler)
548 print ('Sync HTTP server started at %s:%d/chromiumsync...' %
549 (host, server.server_port))
550 print ('Fake OAuth2 Token server started at %s:%d/o/oauth2/token...' %
551 (host, server.server_port))
552 print ('Sync XMPP server started at %s:%d...' %
553 (host, server.xmpp_port))
554 server_data['port'] = server.server_port
555 server_data['xmpp_port'] = server.xmpp_port
556 return server
558 def run_server(self):
559 testserver_base.TestServerRunner.run_server(self)
561 def add_options(self):
562 testserver_base.TestServerRunner.add_options(self)
563 self.option_parser.add_option('--xmpp-port', default='0', type='int',
564 help='Port used by the XMPP server. If '
565 'unspecified, the XMPP server will listen on '
566 'an ephemeral port.')
567 # Override the default logfile name used in testserver.py.
568 self.option_parser.set_defaults(log_file='sync_testserver.log')
570 if __name__ == '__main__':
571 sys.exit(SyncServerRunner().main())