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())