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
8By default, it listens on an ephemeral port and xmpp_port and sends the port
9numbers back to the originating process over a pipe. The originating process can
10specify an explicit port and xmpp_port if necessary.
11"""
12
13import asyncore
14import BaseHTTPServer
15import errno
16import os
17import select
18import socket
19import sys
20import urlparse
21
22import chromiumsync
23import echo_message
24import testserver_base
25import xmppserver
26
27
28class 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
135class 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
444class 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
473if __name__ == '__main__':
474  sys.exit(SyncServerRunner().main())
475