Home | History | Annotate | Download | only in testserver
      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.
      5 
      6 """This is a python sync server used for testing Chrome Sync.
      7 
      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 """
     12 
     13 import asyncore
     14 import BaseHTTPServer
     15 import errno
     16 import os
     17 import select
     18 import socket
     19 import sys
     20 import urlparse
     21 
     22 import chromiumsync
     23 import echo_message
     24 import testserver_base
     25 import xmppserver
     26 
     27 
     28 class SyncHTTPServer(testserver_base.ClientRestrictingServerMixIn,
     29                      testserver_base.BrokenPipeHandlerMixIn,
     30                      testserver_base.StoppableHTTPServer):
     31   """An HTTP server that handles sync commands."""
     32 
     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
     43 
     44   def GetXmppServer(self):
     45     return self._xmpp_server
     46 
     47   def HandleCommand(self, query, raw_request):
     48     return self._sync_handler.HandleCommand(query, raw_request)
     49 
     50   def HandleRequestNoBlock(self):
     51     """Handles a single request.
     52 
     53     Copied from SocketServer._handle_request_noblock().
     54     """
     55 
     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)
     66 
     67   def SetAuthenticated(self, auth_valid):
     68     self.authenticated = auth_valid
     69 
     70   def GetAuthenticated(self):
     71     return self.authenticated
     72 
     73   def serve_forever(self):
     74     """This is a merge of asyncore.loop() and SocketServer.serve_forever().
     75     """
     76 
     77     def HandleXmppSocket(fd, socket_map, handler):
     78       """Runs the handler for the xmpp connection for fd.
     79 
     80       Adapted from asyncore.read() et al.
     81       """
     82 
     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()
     94 
     95     while True:
     96       read_fds = [ self.fileno() ]
     97       write_fds = []
     98       exceptional_fds = []
     99 
    100       for fd, xmpp_connection in self._xmpp_socket_map.items():
    101         is_r = xmpp_connection.readable()
    102         is_w = xmpp_connection.writable()
    103         if is_r:
    104           read_fds.append(fd)
    105         if is_w:
    106           write_fds.append(fd)
    107         if is_r or is_w:
    108           exceptional_fds.append(fd)
    109 
    110       try:
    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:
    115           raise
    116         else:
    117           continue
    118 
    119       for fd in read_fds:
    120         if fd == self.fileno():
    121           self.HandleRequestNoBlock()
    122           continue
    123         HandleXmppSocket(fd, self._xmpp_socket_map,
    124                          asyncore.dispatcher.handle_read_event)
    125 
    126       for fd in write_fds:
    127         HandleXmppSocket(fd, self._xmpp_socket_map,
    128                          asyncore.dispatcher.handle_write_event)
    129 
    130       for fd in exceptional_fds:
    131         HandleXmppSocket(fd, self._xmpp_socket_map,
    132                          asyncore.dispatcher.handle_expt_event)
    133 
    134 
    135 class SyncPageHandler(testserver_base.BasePageHandler):
    136   """Handler for the main HTTP sync server."""
    137 
    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.CustomizeClientCommandHandler]
    160 
    161     post_handlers = [self.ChromiumSyncCommandHandler,
    162                      self.ChromiumSyncTimeHandler,
    163                      self.GaiaOAuth2TokenHandler,
    164                      self.GaiaSetOAuth2TokenResponseHandler]
    165     testserver_base.BasePageHandler.__init__(self, request, client_address,
    166                                              sync_http_server, [], get_handlers,
    167                                              [], post_handlers, [])
    168 
    169 
    170   def ChromiumSyncTimeHandler(self):
    171     """Handle Chromium sync .../time requests.
    172 
    173     The syncer sometimes checks server reachability by examining /time.
    174     """
    175 
    176     test_name = "/chromiumsync/time"
    177     if not self._ShouldHandleRequest(test_name):
    178       return False
    179 
    180     # Chrome hates it if we send a response before reading the request.
    181     if self.headers.getheader('content-length'):
    182       length = int(self.headers.getheader('content-length'))
    183       _raw_request = self.rfile.read(length)
    184 
    185     self.send_response(200)
    186     self.send_header('Content-Type', 'text/plain')
    187     self.end_headers()
    188     self.wfile.write('0123456789')
    189     return True
    190 
    191   def ChromiumSyncCommandHandler(self):
    192     """Handle a chromiumsync command arriving via http.
    193 
    194     This covers all sync protocol commands: authentication, getupdates, and
    195     commit.
    196     """
    197 
    198     test_name = "/chromiumsync/command"
    199     if not self._ShouldHandleRequest(test_name):
    200       return False
    201 
    202     length = int(self.headers.getheader('content-length'))
    203     raw_request = self.rfile.read(length)
    204     http_response = 200
    205     raw_reply = None
    206     if not self.server.GetAuthenticated():
    207       http_response = 401
    208       challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
    209         self.server.server_address[0])
    210     else:
    211       http_response, raw_reply = self.server.HandleCommand(
    212           self.path, raw_request)
    213 
    214     ### Now send the response to the client. ###
    215     self.send_response(http_response)
    216     if http_response == 401:
    217       self.send_header('www-Authenticate', challenge)
    218     self.end_headers()
    219     self.wfile.write(raw_reply)
    220     return True
    221 
    222   def ChromiumSyncMigrationOpHandler(self):
    223     test_name = "/chromiumsync/migrate"
    224     if not self._ShouldHandleRequest(test_name):
    225       return False
    226 
    227     http_response, raw_reply = self.server._sync_handler.HandleMigrate(
    228         self.path)
    229     self.send_response(http_response)
    230     self.send_header('Content-Type', 'text/html')
    231     self.send_header('Content-Length', len(raw_reply))
    232     self.end_headers()
    233     self.wfile.write(raw_reply)
    234     return True
    235 
    236   def ChromiumSyncCredHandler(self):
    237     test_name = "/chromiumsync/cred"
    238     if not self._ShouldHandleRequest(test_name):
    239       return False
    240     try:
    241       query = urlparse.urlparse(self.path)[4]
    242       cred_valid = urlparse.parse_qs(query)['valid']
    243       if cred_valid[0] == 'True':
    244         self.server.SetAuthenticated(True)
    245       else:
    246         self.server.SetAuthenticated(False)
    247     except Exception:
    248       self.server.SetAuthenticated(False)
    249 
    250     http_response = 200
    251     raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated()
    252     self.send_response(http_response)
    253     self.send_header('Content-Type', 'text/html')
    254     self.send_header('Content-Length', len(raw_reply))
    255     self.end_headers()
    256     self.wfile.write(raw_reply)
    257     return True
    258 
    259   def ChromiumSyncXmppCredHandler(self):
    260     test_name = "/chromiumsync/xmppcred"
    261     if not self._ShouldHandleRequest(test_name):
    262       return False
    263     xmpp_server = self.server.GetXmppServer()
    264     try:
    265       query = urlparse.urlparse(self.path)[4]
    266       cred_valid = urlparse.parse_qs(query)['valid']
    267       if cred_valid[0] == 'True':
    268         xmpp_server.SetAuthenticated(True)
    269       else:
    270         xmpp_server.SetAuthenticated(False)
    271     except:
    272       xmpp_server.SetAuthenticated(False)
    273 
    274     http_response = 200
    275     raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated()
    276     self.send_response(http_response)
    277     self.send_header('Content-Type', 'text/html')
    278     self.send_header('Content-Length', len(raw_reply))
    279     self.end_headers()
    280     self.wfile.write(raw_reply)
    281     return True
    282 
    283   def ChromiumSyncDisableNotificationsOpHandler(self):
    284     test_name = "/chromiumsync/disablenotifications"
    285     if not self._ShouldHandleRequest(test_name):
    286       return False
    287     self.server.GetXmppServer().DisableNotifications()
    288     result = 200
    289     raw_reply = ('<html><title>Notifications disabled</title>'
    290                  '<H1>Notifications disabled</H1></html>')
    291     self.send_response(result)
    292     self.send_header('Content-Type', 'text/html')
    293     self.send_header('Content-Length', len(raw_reply))
    294     self.end_headers()
    295     self.wfile.write(raw_reply)
    296     return True
    297 
    298   def ChromiumSyncEnableNotificationsOpHandler(self):
    299     test_name = "/chromiumsync/enablenotifications"
    300     if not self._ShouldHandleRequest(test_name):
    301       return False
    302     self.server.GetXmppServer().EnableNotifications()
    303     result = 200
    304     raw_reply = ('<html><title>Notifications enabled</title>'
    305                  '<H1>Notifications enabled</H1></html>')
    306     self.send_response(result)
    307     self.send_header('Content-Type', 'text/html')
    308     self.send_header('Content-Length', len(raw_reply))
    309     self.end_headers()
    310     self.wfile.write(raw_reply)
    311     return True
    312 
    313   def ChromiumSyncSendNotificationOpHandler(self):
    314     test_name = "/chromiumsync/sendnotification"
    315     if not self._ShouldHandleRequest(test_name):
    316       return False
    317     query = urlparse.urlparse(self.path)[4]
    318     query_params = urlparse.parse_qs(query)
    319     channel = ''
    320     data = ''
    321     if 'channel' in query_params:
    322       channel = query_params['channel'][0]
    323     if 'data' in query_params:
    324       data = query_params['data'][0]
    325     self.server.GetXmppServer().SendNotification(channel, data)
    326     result = 200
    327     raw_reply = ('<html><title>Notification sent</title>'
    328                  '<H1>Notification sent with channel "%s" '
    329                  'and data "%s"</H1></html>'
    330                  % (channel, data))
    331     self.send_response(result)
    332     self.send_header('Content-Type', 'text/html')
    333     self.send_header('Content-Length', len(raw_reply))
    334     self.end_headers()
    335     self.wfile.write(raw_reply)
    336     return True
    337 
    338   def ChromiumSyncBirthdayErrorOpHandler(self):
    339     test_name = "/chromiumsync/birthdayerror"
    340     if not self._ShouldHandleRequest(test_name):
    341       return False
    342     result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError()
    343     self.send_response(result)
    344     self.send_header('Content-Type', 'text/html')
    345     self.send_header('Content-Length', len(raw_reply))
    346     self.end_headers()
    347     self.wfile.write(raw_reply)
    348     return True
    349 
    350   def ChromiumSyncTransientErrorOpHandler(self):
    351     test_name = "/chromiumsync/transienterror"
    352     if not self._ShouldHandleRequest(test_name):
    353       return False
    354     result, raw_reply = self.server._sync_handler.HandleSetTransientError()
    355     self.send_response(result)
    356     self.send_header('Content-Type', 'text/html')
    357     self.send_header('Content-Length', len(raw_reply))
    358     self.end_headers()
    359     self.wfile.write(raw_reply)
    360     return True
    361 
    362   def ChromiumSyncErrorOpHandler(self):
    363     test_name = "/chromiumsync/error"
    364     if not self._ShouldHandleRequest(test_name):
    365       return False
    366     result, raw_reply = self.server._sync_handler.HandleSetInducedError(
    367         self.path)
    368     self.send_response(result)
    369     self.send_header('Content-Type', 'text/html')
    370     self.send_header('Content-Length', len(raw_reply))
    371     self.end_headers()
    372     self.wfile.write(raw_reply)
    373     return True
    374 
    375   def ChromiumSyncSyncTabFaviconsOpHandler(self):
    376     test_name = "/chromiumsync/synctabfavicons"
    377     if not self._ShouldHandleRequest(test_name):
    378       return False
    379     result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons()
    380     self.send_response(result)
    381     self.send_header('Content-Type', 'text/html')
    382     self.send_header('Content-Length', len(raw_reply))
    383     self.end_headers()
    384     self.wfile.write(raw_reply)
    385     return True
    386 
    387   def ChromiumSyncCreateSyncedBookmarksOpHandler(self):
    388     test_name = "/chromiumsync/createsyncedbookmarks"
    389     if not self._ShouldHandleRequest(test_name):
    390       return False
    391     result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks()
    392     self.send_response(result)
    393     self.send_header('Content-Type', 'text/html')
    394     self.send_header('Content-Length', len(raw_reply))
    395     self.end_headers()
    396     self.wfile.write(raw_reply)
    397     return True
    398 
    399   def ChromiumSyncEnableKeystoreEncryptionOpHandler(self):
    400     test_name = "/chromiumsync/enablekeystoreencryption"
    401     if not self._ShouldHandleRequest(test_name):
    402       return False
    403     result, raw_reply = (
    404         self.server._sync_handler.HandleEnableKeystoreEncryption())
    405     self.send_response(result)
    406     self.send_header('Content-Type', 'text/html')
    407     self.send_header('Content-Length', len(raw_reply))
    408     self.end_headers()
    409     self.wfile.write(raw_reply)
    410     return True
    411 
    412   def ChromiumSyncRotateKeystoreKeysOpHandler(self):
    413     test_name = "/chromiumsync/rotatekeystorekeys"
    414     if not self._ShouldHandleRequest(test_name):
    415       return False
    416     result, raw_reply = (
    417         self.server._sync_handler.HandleRotateKeystoreKeys())
    418     self.send_response(result)
    419     self.send_header('Content-Type', 'text/html')
    420     self.send_header('Content-Length', len(raw_reply))
    421     self.end_headers()
    422     self.wfile.write(raw_reply)
    423     return True
    424 
    425   def ChromiumSyncEnableManagedUserAcknowledgementHandler(self):
    426     test_name = "/chromiumsync/enablemanageduseracknowledgement"
    427     if not self._ShouldHandleRequest(test_name):
    428       return False
    429     result, raw_reply = (
    430         self.server._sync_handler.HandleEnableManagedUserAcknowledgement())
    431     self.send_response(result)
    432     self.send_header('Content-Type', 'text/html')
    433     self.send_header('Content-Length', len(raw_reply))
    434     self.end_headers()
    435     self.wfile.write(raw_reply)
    436     return True
    437 
    438   def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self):
    439     test_name = "/chromiumsync/enableprecommitgetupdateavoidance"
    440     if not self._ShouldHandleRequest(test_name):
    441       return False
    442     result, raw_reply = (
    443         self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance())
    444     self.send_response(result)
    445     self.send_header('Content-Type', 'text/html')
    446     self.send_header('Content-Length', len(raw_reply))
    447     self.end_headers()
    448     self.wfile.write(raw_reply)
    449     return True
    450 
    451   def GaiaOAuth2TokenHandler(self):
    452     test_name = "/o/oauth2/token"
    453     if not self._ShouldHandleRequest(test_name):
    454       return False
    455     if self.headers.getheader('content-length'):
    456       length = int(self.headers.getheader('content-length'))
    457       _raw_request = self.rfile.read(length)
    458     result, raw_reply = (
    459         self.server._sync_handler.HandleGetOauth2Token())
    460     self.send_response(result)
    461     self.send_header('Content-Type', 'application/json')
    462     self.send_header('Content-Length', len(raw_reply))
    463     self.end_headers()
    464     self.wfile.write(raw_reply)
    465     return True
    466 
    467   def GaiaSetOAuth2TokenResponseHandler(self):
    468     test_name = "/setfakeoauth2token"
    469     if not self._ShouldHandleRequest(test_name):
    470       return False
    471 
    472     # The index of 'query' is 4.
    473     # See http://docs.python.org/2/library/urlparse.html
    474     query = urlparse.urlparse(self.path)[4]
    475     query_params = urlparse.parse_qs(query)
    476 
    477     response_code = 0
    478     request_token = ''
    479     access_token = ''
    480     expires_in = 0
    481     token_type = ''
    482 
    483     if 'response_code' in query_params:
    484       response_code = query_params['response_code'][0]
    485     if 'request_token' in query_params:
    486       request_token = query_params['request_token'][0]
    487     if 'access_token' in query_params:
    488       access_token = query_params['access_token'][0]
    489     if 'expires_in' in query_params:
    490       expires_in = query_params['expires_in'][0]
    491     if 'token_type' in query_params:
    492       token_type = query_params['token_type'][0]
    493 
    494     result, raw_reply = (
    495         self.server._sync_handler.HandleSetOauth2Token(
    496             response_code, request_token, access_token, expires_in, token_type))
    497     self.send_response(result)
    498     self.send_header('Content-Type', 'text/html')
    499     self.send_header('Content-Length', len(raw_reply))
    500     self.end_headers()
    501     self.wfile.write(raw_reply)
    502     return True
    503 
    504   def TriggerSyncedNotificationHandler(self):
    505     test_name = "/triggersyncednotification"
    506     if not self._ShouldHandleRequest(test_name):
    507       return False
    508 
    509     query = urlparse.urlparse(self.path)[4]
    510     query_params = urlparse.parse_qs(query)
    511 
    512     serialized_notification = ''
    513 
    514     if 'serialized_notification' in query_params:
    515       serialized_notification = query_params['serialized_notification'][0]
    516 
    517     try:
    518       notification_string = self.server._sync_handler.account \
    519           .AddSyncedNotification(serialized_notification)
    520       reply = "A synced notification was triggered:\n\n"
    521       reply += "<code>{}</code>.".format(notification_string)
    522       response_code = 200
    523     except chromiumsync.ClientNotConnectedError:
    524       reply = ('The client is not connected to the server, so the notification'
    525                ' could not be created.')
    526       response_code = 400
    527 
    528     self.send_response(response_code)
    529     self.send_header('Content-Type', 'text/html')
    530     self.send_header('Content-Length', len(reply))
    531     self.end_headers()
    532     self.wfile.write(reply)
    533     return True
    534 
    535   def CustomizeClientCommandHandler(self):
    536     test_name = "/customizeclientcommand"
    537     if not self._ShouldHandleRequest(test_name):
    538       return False
    539 
    540     query = urlparse.urlparse(self.path)[4]
    541     query_params = urlparse.parse_qs(query)
    542 
    543     if 'sessions_commit_delay_seconds' in query_params:
    544       sessions_commit_delay = query_params['sessions_commit_delay_seconds'][0]
    545       try:
    546         command_string = self.server._sync_handler.CustomizeClientCommand(
    547             int(sessions_commit_delay))
    548         response_code = 200
    549         reply = "The ClientCommand was customized:\n\n"
    550         reply += "<code>{}</code>.".format(command_string)
    551       except ValueError:
    552         response_code = 400
    553         reply = "sessions_commit_delay_seconds was not an int"
    554     else:
    555       response_code = 400
    556       reply = "sessions_commit_delay_seconds is required"
    557 
    558     self.send_response(response_code)
    559     self.send_header('Content-Type', 'text/html')
    560     self.send_header('Content-Length', len(reply))
    561     self.end_headers()
    562     self.wfile.write(reply)
    563     return True
    564 
    565   def SyncedNotificationsPageHandler(self):
    566     test_name = "/syncednotifications"
    567     if not self._ShouldHandleRequest(test_name):
    568       return False
    569 
    570     html = open('sync/tools/testserver/synced_notifications.html', 'r').read()
    571 
    572     self.send_response(200)
    573     self.send_header('Content-Type', 'text/html')
    574     self.send_header('Content-Length', len(html))
    575     self.end_headers()
    576     self.wfile.write(html)
    577     return True
    578 
    579 
    580 class SyncServerRunner(testserver_base.TestServerRunner):
    581   """TestServerRunner for the net test servers."""
    582 
    583   def __init__(self):
    584     super(SyncServerRunner, self).__init__()
    585 
    586   def create_server(self, server_data):
    587     port = self.options.port
    588     host = self.options.host
    589     xmpp_port = self.options.xmpp_port
    590     server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler)
    591     print ('Sync HTTP server started at %s:%d/chromiumsync...' %
    592            (host, server.server_port))
    593     print ('Fake OAuth2 Token server started at %s:%d/o/oauth2/token...' %
    594            (host, server.server_port))
    595     print ('Sync XMPP server started at %s:%d...' %
    596            (host, server.xmpp_port))
    597     server_data['port'] = server.server_port
    598     server_data['xmpp_port'] = server.xmpp_port
    599     return server
    600 
    601   def run_server(self):
    602     testserver_base.TestServerRunner.run_server(self)
    603 
    604   def add_options(self):
    605     testserver_base.TestServerRunner.add_options(self)
    606     self.option_parser.add_option('--xmpp-port', default='0', type='int',
    607                                   help='Port used by the XMPP server. If '
    608                                   'unspecified, the XMPP server will listen on '
    609                                   'an ephemeral port.')
    610     # Override the default logfile name used in testserver.py.
    611     self.option_parser.set_defaults(log_file='sync_testserver.log')
    612 
    613 if __name__ == '__main__':
    614   sys.exit(SyncServerRunner().main())
    615