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 
    156     post_handlers = [self.ChromiumSyncCommandHandler,
    157                      self.ChromiumSyncTimeHandler]
    158     testserver_base.BasePageHandler.__init__(self, request, client_address,
    159                                              sync_http_server, [], get_handlers,
    160                                              [], post_handlers, [])
    161 
    162 
    163   def ChromiumSyncTimeHandler(self):
    164     """Handle Chromium sync .../time requests.
    165 
    166     The syncer sometimes checks server reachability by examining /time.
    167     """
    168 
    169     test_name = "/chromiumsync/time"
    170     if not self._ShouldHandleRequest(test_name):
    171       return False
    172 
    173     # Chrome hates it if we send a response before reading the request.
    174     if self.headers.getheader('content-length'):
    175       length = int(self.headers.getheader('content-length'))
    176       _raw_request = self.rfile.read(length)
    177 
    178     self.send_response(200)
    179     self.send_header('Content-Type', 'text/plain')
    180     self.end_headers()
    181     self.wfile.write('0123456789')
    182     return True
    183 
    184   def ChromiumSyncCommandHandler(self):
    185     """Handle a chromiumsync command arriving via http.
    186 
    187     This covers all sync protocol commands: authentication, getupdates, and
    188     commit.
    189     """
    190 
    191     test_name = "/chromiumsync/command"
    192     if not self._ShouldHandleRequest(test_name):
    193       return False
    194 
    195     length = int(self.headers.getheader('content-length'))
    196     raw_request = self.rfile.read(length)
    197     http_response = 200
    198     raw_reply = None
    199     if not self.server.GetAuthenticated():
    200       http_response = 401
    201       challenge = 'GoogleLogin realm="http://%s", service="chromiumsync"' % (
    202         self.server.server_address[0])
    203     else:
    204       http_response, raw_reply = self.server.HandleCommand(
    205           self.path, raw_request)
    206 
    207     ### Now send the response to the client. ###
    208     self.send_response(http_response)
    209     if http_response == 401:
    210       self.send_header('www-Authenticate', challenge)
    211     self.end_headers()
    212     self.wfile.write(raw_reply)
    213     return True
    214 
    215   def ChromiumSyncMigrationOpHandler(self):
    216     test_name = "/chromiumsync/migrate"
    217     if not self._ShouldHandleRequest(test_name):
    218       return False
    219 
    220     http_response, raw_reply = self.server._sync_handler.HandleMigrate(
    221         self.path)
    222     self.send_response(http_response)
    223     self.send_header('Content-Type', 'text/html')
    224     self.send_header('Content-Length', len(raw_reply))
    225     self.end_headers()
    226     self.wfile.write(raw_reply)
    227     return True
    228 
    229   def ChromiumSyncCredHandler(self):
    230     test_name = "/chromiumsync/cred"
    231     if not self._ShouldHandleRequest(test_name):
    232       return False
    233     try:
    234       query = urlparse.urlparse(self.path)[4]
    235       cred_valid = urlparse.parse_qs(query)['valid']
    236       if cred_valid[0] == 'True':
    237         self.server.SetAuthenticated(True)
    238       else:
    239         self.server.SetAuthenticated(False)
    240     except Exception:
    241       self.server.SetAuthenticated(False)
    242 
    243     http_response = 200
    244     raw_reply = 'Authenticated: %s ' % self.server.GetAuthenticated()
    245     self.send_response(http_response)
    246     self.send_header('Content-Type', 'text/html')
    247     self.send_header('Content-Length', len(raw_reply))
    248     self.end_headers()
    249     self.wfile.write(raw_reply)
    250     return True
    251 
    252   def ChromiumSyncXmppCredHandler(self):
    253     test_name = "/chromiumsync/xmppcred"
    254     if not self._ShouldHandleRequest(test_name):
    255       return False
    256     xmpp_server = self.server.GetXmppServer()
    257     try:
    258       query = urlparse.urlparse(self.path)[4]
    259       cred_valid = urlparse.parse_qs(query)['valid']
    260       if cred_valid[0] == 'True':
    261         xmpp_server.SetAuthenticated(True)
    262       else:
    263         xmpp_server.SetAuthenticated(False)
    264     except:
    265       xmpp_server.SetAuthenticated(False)
    266 
    267     http_response = 200
    268     raw_reply = 'XMPP Authenticated: %s ' % xmpp_server.GetAuthenticated()
    269     self.send_response(http_response)
    270     self.send_header('Content-Type', 'text/html')
    271     self.send_header('Content-Length', len(raw_reply))
    272     self.end_headers()
    273     self.wfile.write(raw_reply)
    274     return True
    275 
    276   def ChromiumSyncDisableNotificationsOpHandler(self):
    277     test_name = "/chromiumsync/disablenotifications"
    278     if not self._ShouldHandleRequest(test_name):
    279       return False
    280     self.server.GetXmppServer().DisableNotifications()
    281     result = 200
    282     raw_reply = ('<html><title>Notifications disabled</title>'
    283                  '<H1>Notifications disabled</H1></html>')
    284     self.send_response(result)
    285     self.send_header('Content-Type', 'text/html')
    286     self.send_header('Content-Length', len(raw_reply))
    287     self.end_headers()
    288     self.wfile.write(raw_reply)
    289     return True
    290 
    291   def ChromiumSyncEnableNotificationsOpHandler(self):
    292     test_name = "/chromiumsync/enablenotifications"
    293     if not self._ShouldHandleRequest(test_name):
    294       return False
    295     self.server.GetXmppServer().EnableNotifications()
    296     result = 200
    297     raw_reply = ('<html><title>Notifications enabled</title>'
    298                  '<H1>Notifications enabled</H1></html>')
    299     self.send_response(result)
    300     self.send_header('Content-Type', 'text/html')
    301     self.send_header('Content-Length', len(raw_reply))
    302     self.end_headers()
    303     self.wfile.write(raw_reply)
    304     return True
    305 
    306   def ChromiumSyncSendNotificationOpHandler(self):
    307     test_name = "/chromiumsync/sendnotification"
    308     if not self._ShouldHandleRequest(test_name):
    309       return False
    310     query = urlparse.urlparse(self.path)[4]
    311     query_params = urlparse.parse_qs(query)
    312     channel = ''
    313     data = ''
    314     if 'channel' in query_params:
    315       channel = query_params['channel'][0]
    316     if 'data' in query_params:
    317       data = query_params['data'][0]
    318     self.server.GetXmppServer().SendNotification(channel, data)
    319     result = 200
    320     raw_reply = ('<html><title>Notification sent</title>'
    321                  '<H1>Notification sent with channel "%s" '
    322                  'and data "%s"</H1></html>'
    323                  % (channel, data))
    324     self.send_response(result)
    325     self.send_header('Content-Type', 'text/html')
    326     self.send_header('Content-Length', len(raw_reply))
    327     self.end_headers()
    328     self.wfile.write(raw_reply)
    329     return True
    330 
    331   def ChromiumSyncBirthdayErrorOpHandler(self):
    332     test_name = "/chromiumsync/birthdayerror"
    333     if not self._ShouldHandleRequest(test_name):
    334       return False
    335     result, raw_reply = self.server._sync_handler.HandleCreateBirthdayError()
    336     self.send_response(result)
    337     self.send_header('Content-Type', 'text/html')
    338     self.send_header('Content-Length', len(raw_reply))
    339     self.end_headers()
    340     self.wfile.write(raw_reply)
    341     return True
    342 
    343   def ChromiumSyncTransientErrorOpHandler(self):
    344     test_name = "/chromiumsync/transienterror"
    345     if not self._ShouldHandleRequest(test_name):
    346       return False
    347     result, raw_reply = self.server._sync_handler.HandleSetTransientError()
    348     self.send_response(result)
    349     self.send_header('Content-Type', 'text/html')
    350     self.send_header('Content-Length', len(raw_reply))
    351     self.end_headers()
    352     self.wfile.write(raw_reply)
    353     return True
    354 
    355   def ChromiumSyncErrorOpHandler(self):
    356     test_name = "/chromiumsync/error"
    357     if not self._ShouldHandleRequest(test_name):
    358       return False
    359     result, raw_reply = self.server._sync_handler.HandleSetInducedError(
    360         self.path)
    361     self.send_response(result)
    362     self.send_header('Content-Type', 'text/html')
    363     self.send_header('Content-Length', len(raw_reply))
    364     self.end_headers()
    365     self.wfile.write(raw_reply)
    366     return True
    367 
    368   def ChromiumSyncSyncTabFaviconsOpHandler(self):
    369     test_name = "/chromiumsync/synctabfavicons"
    370     if not self._ShouldHandleRequest(test_name):
    371       return False
    372     result, raw_reply = self.server._sync_handler.HandleSetSyncTabFavicons()
    373     self.send_response(result)
    374     self.send_header('Content-Type', 'text/html')
    375     self.send_header('Content-Length', len(raw_reply))
    376     self.end_headers()
    377     self.wfile.write(raw_reply)
    378     return True
    379 
    380   def ChromiumSyncCreateSyncedBookmarksOpHandler(self):
    381     test_name = "/chromiumsync/createsyncedbookmarks"
    382     if not self._ShouldHandleRequest(test_name):
    383       return False
    384     result, raw_reply = self.server._sync_handler.HandleCreateSyncedBookmarks()
    385     self.send_response(result)
    386     self.send_header('Content-Type', 'text/html')
    387     self.send_header('Content-Length', len(raw_reply))
    388     self.end_headers()
    389     self.wfile.write(raw_reply)
    390     return True
    391 
    392   def ChromiumSyncEnableKeystoreEncryptionOpHandler(self):
    393     test_name = "/chromiumsync/enablekeystoreencryption"
    394     if not self._ShouldHandleRequest(test_name):
    395       return False
    396     result, raw_reply = (
    397         self.server._sync_handler.HandleEnableKeystoreEncryption())
    398     self.send_response(result)
    399     self.send_header('Content-Type', 'text/html')
    400     self.send_header('Content-Length', len(raw_reply))
    401     self.end_headers()
    402     self.wfile.write(raw_reply)
    403     return True
    404 
    405   def ChromiumSyncRotateKeystoreKeysOpHandler(self):
    406     test_name = "/chromiumsync/rotatekeystorekeys"
    407     if not self._ShouldHandleRequest(test_name):
    408       return False
    409     result, raw_reply = (
    410         self.server._sync_handler.HandleRotateKeystoreKeys())
    411     self.send_response(result)
    412     self.send_header('Content-Type', 'text/html')
    413     self.send_header('Content-Length', len(raw_reply))
    414     self.end_headers()
    415     self.wfile.write(raw_reply)
    416     return True
    417 
    418   def ChromiumSyncEnableManagedUserAcknowledgementHandler(self):
    419     test_name = "/chromiumsync/enablemanageduseracknowledgement"
    420     if not self._ShouldHandleRequest(test_name):
    421       return False
    422     result, raw_reply = (
    423         self.server._sync_handler.HandleEnableManagedUserAcknowledgement())
    424     self.send_response(result)
    425     self.send_header('Content-Type', 'text/html')
    426     self.send_header('Content-Length', len(raw_reply))
    427     self.end_headers()
    428     self.wfile.write(raw_reply)
    429     return True
    430 
    431   def ChromiumSyncEnablePreCommitGetUpdateAvoidanceHandler(self):
    432     test_name = "/chromiumsync/enableprecommitgetupdateavoidance"
    433     if not self._ShouldHandleRequest(test_name):
    434       return False
    435     result, raw_reply = (
    436         self.server._sync_handler.HandleEnablePreCommitGetUpdateAvoidance())
    437     self.send_response(result)
    438     self.send_header('Content-Type', 'text/html')
    439     self.send_header('Content-Length', len(raw_reply))
    440     self.end_headers()
    441     self.wfile.write(raw_reply)
    442     return True
    443 
    444 class SyncServerRunner(testserver_base.TestServerRunner):
    445   """TestServerRunner for the net test servers."""
    446 
    447   def __init__(self):
    448     super(SyncServerRunner, self).__init__()
    449 
    450   def create_server(self, server_data):
    451     port = self.options.port
    452     host = self.options.host
    453     xmpp_port = self.options.xmpp_port
    454     server = SyncHTTPServer((host, port), xmpp_port, SyncPageHandler)
    455     print 'Sync HTTP server started on port %d...' % server.server_port
    456     print 'Sync XMPP server started on port %d...' % server.xmpp_port
    457     server_data['port'] = server.server_port
    458     server_data['xmpp_port'] = server.xmpp_port
    459     return server
    460 
    461   def run_server(self):
    462     testserver_base.TestServerRunner.run_server(self)
    463 
    464   def add_options(self):
    465     testserver_base.TestServerRunner.add_options(self)
    466     self.option_parser.add_option('--xmpp-port', default='0', type='int',
    467                                   help='Port used by the XMPP server. If '
    468                                   'unspecified, the XMPP server will listen on '
    469                                   'an ephemeral port.')
    470     # Override the default logfile name used in testserver.py.
    471     self.option_parser.set_defaults(log_file='sync_testserver.log')
    472 
    473 if __name__ == '__main__':
    474   sys.exit(SyncServerRunner().main())
    475