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.
26 import testserver_base
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
,
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().
59 request
, client_address
= self
.get_request()
62 if self
.verify_request(request
, client_address
):
64 self
.process_request(request
, client_address
)
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.
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()
95 read_fds
= [ self
.fileno() ]
99 for fd
, xmpp_connection
in self
._xmpp
_socket
_map
.items():
100 is_r
= xmpp_connection
.readable()
101 is_w
= xmpp_connection
.writable()
107 exceptional_fds
.append(fd
)
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
:
119 if fd
== self
.fileno():
120 self
.HandleRequestNoBlock()
122 HandleXmppSocket(fd
, self
._xmpp
_socket
_map
,
123 asyncore
.dispatcher
.handle_read_event
)
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
):
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')
185 self
.wfile
.write('0123456789')
188 def ChromiumSyncCommandHandler(self
):
189 """Handle a chromiumsync command arriving via http.
191 This covers all sync protocol commands: authentication, getupdates, and
195 test_name
= "/chromiumsync/command"
196 if not self
._ShouldHandleRequest
(test_name
):
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')
204 raw_request
= gzip
.GzipFile(
205 fileobj
=StringIO
.StringIO(raw_request
)).read()
209 if not self
.server
.GetAuthenticated():
211 challenge
= 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
212 self
.server
.server_address
[0])
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
)
222 self
.wfile
.write(raw_reply
)
225 def ChromiumSyncMigrationOpHandler(self
):
226 test_name
= "/chromiumsync/migrate"
227 if not self
._ShouldHandleRequest
(test_name
):
230 http_response
, raw_reply
= self
.server
._sync
_handler
.HandleMigrate(
232 self
.send_response(http_response
)
233 self
.send_header('Content-Type', 'text/html')
234 self
.send_header('Content-Length', len(raw_reply
))
236 self
.wfile
.write(raw_reply
)
239 def ChromiumSyncCredHandler(self
):
240 test_name
= "/chromiumsync/cred"
241 if not self
._ShouldHandleRequest
(test_name
):
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)
249 self
.server
.SetAuthenticated(False)
251 self
.server
.SetAuthenticated(False)
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
))
259 self
.wfile
.write(raw_reply
)
262 def ChromiumSyncXmppCredHandler(self
):
263 test_name
= "/chromiumsync/xmppcred"
264 if not self
._ShouldHandleRequest
(test_name
):
266 xmpp_server
= self
.server
.GetXmppServer()
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)
273 xmpp_server
.SetAuthenticated(False)
275 xmpp_server
.SetAuthenticated(False)
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
))
283 self
.wfile
.write(raw_reply
)
286 def ChromiumSyncDisableNotificationsOpHandler(self
):
287 test_name
= "/chromiumsync/disablenotifications"
288 if not self
._ShouldHandleRequest
(test_name
):
290 self
.server
.GetXmppServer().DisableNotifications()
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
))
298 self
.wfile
.write(raw_reply
)
301 def ChromiumSyncEnableNotificationsOpHandler(self
):
302 test_name
= "/chromiumsync/enablenotifications"
303 if not self
._ShouldHandleRequest
(test_name
):
305 self
.server
.GetXmppServer().EnableNotifications()
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
))
313 self
.wfile
.write(raw_reply
)
316 def ChromiumSyncSendNotificationOpHandler(self
):
317 test_name
= "/chromiumsync/sendnotification"
318 if not self
._ShouldHandleRequest
(test_name
):
320 query
= urlparse
.urlparse(self
.path
)[4]
321 query_params
= urlparse
.parse_qs(query
)
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
)
330 raw_reply
= ('<html><title>Notification sent</title>'
331 '<H1>Notification sent with channel "%s" '
332 'and data "%s"</H1></html>'
334 self
.send_response(result
)
335 self
.send_header('Content-Type', 'text/html')
336 self
.send_header('Content-Length', len(raw_reply
))
338 self
.wfile
.write(raw_reply
)
341 def ChromiumSyncBirthdayErrorOpHandler(self
):
342 test_name
= "/chromiumsync/birthdayerror"
343 if not self
._ShouldHandleRequest
(test_name
):
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
))
350 self
.wfile
.write(raw_reply
)
353 def ChromiumSyncTransientErrorOpHandler(self
):
354 test_name
= "/chromiumsync/transienterror"
355 if not self
._ShouldHandleRequest
(test_name
):
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
))
362 self
.wfile
.write(raw_reply
)
365 def ChromiumSyncErrorOpHandler(self
):
366 test_name
= "/chromiumsync/error"
367 if not self
._ShouldHandleRequest
(test_name
):
369 result
, raw_reply
= self
.server
._sync
_handler
.HandleSetInducedError(
371 self
.send_response(result
)
372 self
.send_header('Content-Type', 'text/html')
373 self
.send_header('Content-Length', len(raw_reply
))
375 self
.wfile
.write(raw_reply
)
378 def ChromiumSyncSyncTabFaviconsOpHandler(self
):
379 test_name
= "/chromiumsync/synctabfavicons"
380 if not self
._ShouldHandleRequest
(test_name
):
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
))
387 self
.wfile
.write(raw_reply
)
390 def ChromiumSyncCreateSyncedBookmarksOpHandler(self
):
391 test_name
= "/chromiumsync/createsyncedbookmarks"
392 if not self
._ShouldHandleRequest
(test_name
):
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
))
399 self
.wfile
.write(raw_reply
)
402 def ChromiumSyncEnableKeystoreEncryptionOpHandler(self
):
403 test_name
= "/chromiumsync/enablekeystoreencryption"
404 if not self
._ShouldHandleRequest
(test_name
):
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
))
412 self
.wfile
.write(raw_reply
)
415 def ChromiumSyncRotateKeystoreKeysOpHandler(self
):
416 test_name
= "/chromiumsync/rotatekeystorekeys"
417 if not self
._ShouldHandleRequest
(test_name
):
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
))
425 self
.wfile
.write(raw_reply
)
428 def ChromiumSyncEnableManagedUserAcknowledgementHandler(self
):
429 test_name
= "/chromiumsync/enablemanageduseracknowledgement"
430 if not self
._ShouldHandleRequest
(test_name
):
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
))
438 self
.wfile
.write(raw_reply
)
441 def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self
):
442 test_name
= "/chromiumsync/enableprecommitgetupdateavoidance"
443 if not self
._ShouldHandleRequest
(test_name
):
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
))
451 self
.wfile
.write(raw_reply
)
454 def GaiaOAuth2TokenHandler(self
):
455 test_name
= "/o/oauth2/token"
456 if not self
._ShouldHandleRequest
(test_name
):
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
))
467 self
.wfile
.write(raw_reply
)
470 def GaiaSetOAuth2TokenResponseHandler(self
):
471 test_name
= "/setfakeoauth2token"
472 if not self
._ShouldHandleRequest
(test_name
):
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
)
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
))
504 self
.wfile
.write(raw_reply
)
507 def CustomizeClientCommandHandler(self
):
508 test_name
= "/customizeclientcommand"
509 if not self
._ShouldHandleRequest
(test_name
):
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]
518 command_string
= self
.server
._sync
_handler
.CustomizeClientCommand(
519 int(sessions_commit_delay
))
521 reply
= "The ClientCommand was customized:\n\n"
522 reply
+= "<code>{}</code>.".format(command_string
)
525 reply
= "sessions_commit_delay_seconds was not an int"
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
))
534 self
.wfile
.write(reply
)
537 class SyncServerRunner(testserver_base
.TestServerRunner
):
538 """TestServerRunner for the net test servers."""
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
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())