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 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.
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:
87 handler(xmpp_connection
)
88 except (asyncore
.ExitNow
, KeyboardInterrupt, SystemExit):
91 xmpp_connection
.handle_error()
93 read_fds
= [ self
.fileno() ]
97 for fd
, xmpp_connection
in self
._xmpp
_socket
_map
.items():
98 is_r
= xmpp_connection
.readable()
99 is_w
= xmpp_connection
.writable()
105 exceptional_fds
.append(fd
)
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
:
117 if fd
== self
.fileno():
118 self
.HandleRequestNoBlock()
120 HandleXmppSocket(fd
, self
._xmpp
_socket
_map
,
121 asyncore
.dispatcher
.handle_read_event
)
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
):
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')
183 self
.wfile
.write('0123456789')
186 def ChromiumSyncCommandHandler(self
):
187 """Handle a chromiumsync command arriving via http.
189 This covers all sync protocol commands: authentication, getupdates, and
193 test_name
= "/chromiumsync/command"
194 if not self
._ShouldHandleRequest
(test_name
):
197 length
= int(self
.headers
.getheader('content-length'))
198 raw_request
= self
.rfile
.read(length
)
201 if not self
.server
.GetAuthenticated():
203 challenge
= 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
204 self
.server
.server_address
[0])
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
)
214 self
.wfile
.write(raw_reply
)
217 def ChromiumSyncMigrationOpHandler(self
):
218 test_name
= "/chromiumsync/migrate"
219 if not self
._ShouldHandleRequest
(test_name
):
222 http_response
, raw_reply
= self
.server
._sync
_handler
.HandleMigrate(
224 self
.send_response(http_response
)
225 self
.send_header('Content-Type', 'text/html')
226 self
.send_header('Content-Length', len(raw_reply
))
228 self
.wfile
.write(raw_reply
)
231 def ChromiumSyncCredHandler(self
):
232 test_name
= "/chromiumsync/cred"
233 if not self
._ShouldHandleRequest
(test_name
):
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)
241 self
.server
.SetAuthenticated(False)
243 self
.server
.SetAuthenticated(False)
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
))
251 self
.wfile
.write(raw_reply
)
254 def ChromiumSyncXmppCredHandler(self
):
255 test_name
= "/chromiumsync/xmppcred"
256 if not self
._ShouldHandleRequest
(test_name
):
258 xmpp_server
= self
.server
.GetXmppServer()
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)
265 xmpp_server
.SetAuthenticated(False)
267 xmpp_server
.SetAuthenticated(False)
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
))
275 self
.wfile
.write(raw_reply
)
278 def ChromiumSyncDisableNotificationsOpHandler(self
):
279 test_name
= "/chromiumsync/disablenotifications"
280 if not self
._ShouldHandleRequest
(test_name
):
282 self
.server
.GetXmppServer().DisableNotifications()
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
))
290 self
.wfile
.write(raw_reply
)
293 def ChromiumSyncEnableNotificationsOpHandler(self
):
294 test_name
= "/chromiumsync/enablenotifications"
295 if not self
._ShouldHandleRequest
(test_name
):
297 self
.server
.GetXmppServer().EnableNotifications()
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
))
305 self
.wfile
.write(raw_reply
)
308 def ChromiumSyncSendNotificationOpHandler(self
):
309 test_name
= "/chromiumsync/sendnotification"
310 if not self
._ShouldHandleRequest
(test_name
):
312 query
= urlparse
.urlparse(self
.path
)[4]
313 query_params
= urlparse
.parse_qs(query
)
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
)
322 raw_reply
= ('<html><title>Notification sent</title>'
323 '<H1>Notification sent with channel "%s" '
324 'and data "%s"</H1></html>'
326 self
.send_response(result
)
327 self
.send_header('Content-Type', 'text/html')
328 self
.send_header('Content-Length', len(raw_reply
))
330 self
.wfile
.write(raw_reply
)
333 def ChromiumSyncBirthdayErrorOpHandler(self
):
334 test_name
= "/chromiumsync/birthdayerror"
335 if not self
._ShouldHandleRequest
(test_name
):
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
))
342 self
.wfile
.write(raw_reply
)
345 def ChromiumSyncTransientErrorOpHandler(self
):
346 test_name
= "/chromiumsync/transienterror"
347 if not self
._ShouldHandleRequest
(test_name
):
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
))
354 self
.wfile
.write(raw_reply
)
357 def ChromiumSyncErrorOpHandler(self
):
358 test_name
= "/chromiumsync/error"
359 if not self
._ShouldHandleRequest
(test_name
):
361 result
, raw_reply
= self
.server
._sync
_handler
.HandleSetInducedError(
363 self
.send_response(result
)
364 self
.send_header('Content-Type', 'text/html')
365 self
.send_header('Content-Length', len(raw_reply
))
367 self
.wfile
.write(raw_reply
)
370 def ChromiumSyncSyncTabFaviconsOpHandler(self
):
371 test_name
= "/chromiumsync/synctabfavicons"
372 if not self
._ShouldHandleRequest
(test_name
):
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
))
379 self
.wfile
.write(raw_reply
)
382 def ChromiumSyncCreateSyncedBookmarksOpHandler(self
):
383 test_name
= "/chromiumsync/createsyncedbookmarks"
384 if not self
._ShouldHandleRequest
(test_name
):
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
))
391 self
.wfile
.write(raw_reply
)
394 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self
):
395 test_name
= "/chromiumsync/enablekeystoreencryption"
396 if not self
._ShouldHandleRequest
(test_name
):
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
))
404 self
.wfile
.write(raw_reply
)
407 def ChromiumSyncRotateKeystoreKeysOpHandler(self
):
408 test_name
= "/chromiumsync/rotatekeystorekeys"
409 if not self
._ShouldHandleRequest
(test_name
):
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
))
417 self
.wfile
.write(raw_reply
)
420 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self
):
421 test_name
= "/chromiumsync/enablemanageduseracknowledgement"
422 if not self
._ShouldHandleRequest
(test_name
):
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
))
430 self
.wfile
.write(raw_reply
)
433 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self
):
434 test_name
= "/chromiumsync/enableprecommitgetupdateavoidance"
435 if not self
._ShouldHandleRequest
(test_name
):
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
))
443 self
.wfile
.write(raw_reply
)
446 def GaiaOAuth2TokenHandler(self
):
447 test_name
= "/o/oauth2/token"
448 if not self
._ShouldHandleRequest
(test_name
):
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
))
459 self
.wfile
.write(raw_reply
)
462 def GaiaSetOAuth2TokenResponseHandler(self
):
463 test_name
= "/setfakeoauth2token"
464 if not self
._ShouldHandleRequest
(test_name
):
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
)
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
))
496 self
.wfile
.write(raw_reply
)
499 def CustomizeClientCommandHandler(self
):
500 test_name
= "/customizeclientcommand"
501 if not self
._ShouldHandleRequest
(test_name
):
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]
510 command_string
= self
.server
._sync
_handler
.CustomizeClientCommand(
511 int(sessions_commit_delay
))
513 reply
= "The ClientCommand was customized:\n\n"
514 reply
+= "<code>{}</code>.".format(command_string
)
517 reply
= "sessions_commit_delay_seconds was not an int"
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
))
526 self
.wfile
.write(reply
)
529 class SyncServerRunner(testserver_base
.TestServerRunner
):
530 """TestServerRunner for the net test servers."""
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
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())