Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(133)

Side by Side Diff: net/tools/testserver/xmppserver.py

Issue 11971025: [sync] Divorce python sync test server chromiumsync.py from testserver.py (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase Created 7 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « net/tools/testserver/testserver_base.py ('k') | net/tools/testserver/xmppserver_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """A bare-bones and non-compliant XMPP server.
6
7 Just enough of the protocol is implemented to get it to work with
8 Chrome's sync notification system.
9 """
10
11 import asynchat
12 import asyncore
13 import base64
14 import re
15 import socket
16 from xml.dom import minidom
17
18 # pychecker complains about the use of fileno(), which is implemented
19 # by asyncore by forwarding to an internal object via __getattr__.
20 __pychecker__ = 'no-classattr'
21
22
23 class Error(Exception):
24 """Error class for this module."""
25 pass
26
27
28 class UnexpectedXml(Error):
29 """Raised when an unexpected XML element has been encountered."""
30
31 def __init__(self, xml_element):
32 xml_text = xml_element.toxml()
33 Error.__init__(self, 'Unexpected XML element', xml_text)
34
35
36 def ParseXml(xml_string):
37 """Parses the given string as XML and returns a minidom element
38 object.
39 """
40 dom = minidom.parseString(xml_string)
41
42 # minidom handles xmlns specially, but there's a bug where it sets
43 # the attribute value to None, which causes toxml() or toprettyxml()
44 # to break.
45 def FixMinidomXmlnsBug(xml_element):
46 if xml_element.getAttribute('xmlns') is None:
47 xml_element.setAttribute('xmlns', '')
48
49 def ApplyToAllDescendantElements(xml_element, fn):
50 fn(xml_element)
51 for node in xml_element.childNodes:
52 if node.nodeType == node.ELEMENT_NODE:
53 ApplyToAllDescendantElements(node, fn)
54
55 root = dom.documentElement
56 ApplyToAllDescendantElements(root, FixMinidomXmlnsBug)
57 return root
58
59
60 def CloneXml(xml):
61 """Returns a deep copy of the given XML element.
62
63 Args:
64 xml: The XML element, which should be something returned from
65 ParseXml() (i.e., a root element).
66 """
67 return xml.ownerDocument.cloneNode(True).documentElement
68
69
70 class StanzaParser(object):
71 """A hacky incremental XML parser.
72
73 StanzaParser consumes data incrementally via FeedString() and feeds
74 its delegate complete parsed stanzas (i.e., XML documents) via
75 FeedStanza(). Any stanzas passed to FeedStanza() are unlinked after
76 the callback is done.
77
78 Use like so:
79
80 class MyClass(object):
81 ...
82 def __init__(self, ...):
83 ...
84 self._parser = StanzaParser(self)
85 ...
86
87 def SomeFunction(self, ...):
88 ...
89 self._parser.FeedString(some_data)
90 ...
91
92 def FeedStanza(self, stanza):
93 ...
94 print stanza.toprettyxml()
95 ...
96 """
97
98 # NOTE(akalin): The following regexps are naive, but necessary since
99 # none of the existing Python 2.4/2.5 XML libraries support
100 # incremental parsing. This works well enough for our purposes.
101 #
102 # The regexps below assume that any present XML element starts at
103 # the beginning of the string, but there may be trailing whitespace.
104
105 # Matches an opening stream tag (e.g., '<stream:stream foo="bar">')
106 # (assumes that the stream XML namespace is defined in the tag).
107 _stream_re = re.compile(r'^(<stream:stream [^>]*>)\s*')
108
109 # Matches an empty element tag (e.g., '<foo bar="baz"/>').
110 _empty_element_re = re.compile(r'^(<[^>]*/>)\s*')
111
112 # Matches a non-empty element (e.g., '<foo bar="baz">quux</foo>').
113 # Does *not* handle nested elements.
114 _non_empty_element_re = re.compile(r'^(<([^ >]*)[^>]*>.*?</\2>)\s*')
115
116 # The closing tag for a stream tag. We have to insert this
117 # ourselves since all XML stanzas are children of the stream tag,
118 # which is never closed until the connection is closed.
119 _stream_suffix = '</stream:stream>'
120
121 def __init__(self, delegate):
122 self._buffer = ''
123 self._delegate = delegate
124
125 def FeedString(self, data):
126 """Consumes the given string data, possibly feeding one or more
127 stanzas to the delegate.
128 """
129 self._buffer += data
130 while (self._ProcessBuffer(self._stream_re, self._stream_suffix) or
131 self._ProcessBuffer(self._empty_element_re) or
132 self._ProcessBuffer(self._non_empty_element_re)):
133 pass
134
135 def _ProcessBuffer(self, regexp, xml_suffix=''):
136 """If the buffer matches the given regexp, removes the match from
137 the buffer, appends the given suffix, parses it, and feeds it to
138 the delegate.
139
140 Returns:
141 Whether or not the buffer matched the given regexp.
142 """
143 results = regexp.match(self._buffer)
144 if not results:
145 return False
146 xml_text = self._buffer[:results.end()] + xml_suffix
147 self._buffer = self._buffer[results.end():]
148 stanza = ParseXml(xml_text)
149 self._delegate.FeedStanza(stanza)
150 # Needed because stanza may have cycles.
151 stanza.unlink()
152 return True
153
154
155 class Jid(object):
156 """Simple struct for an XMPP jid (essentially an e-mail address with
157 an optional resource string).
158 """
159
160 def __init__(self, username, domain, resource=''):
161 self.username = username
162 self.domain = domain
163 self.resource = resource
164
165 def __str__(self):
166 jid_str = "%s@%s" % (self.username, self.domain)
167 if self.resource:
168 jid_str += '/' + self.resource
169 return jid_str
170
171 def GetBareJid(self):
172 return Jid(self.username, self.domain)
173
174
175 class IdGenerator(object):
176 """Simple class to generate unique IDs for XMPP messages."""
177
178 def __init__(self, prefix):
179 self._prefix = prefix
180 self._id = 0
181
182 def GetNextId(self):
183 next_id = "%s.%s" % (self._prefix, self._id)
184 self._id += 1
185 return next_id
186
187
188 class HandshakeTask(object):
189 """Class to handle the initial handshake with a connected XMPP
190 client.
191 """
192
193 # The handshake states in order.
194 (_INITIAL_STREAM_NEEDED,
195 _AUTH_NEEDED,
196 _AUTH_STREAM_NEEDED,
197 _BIND_NEEDED,
198 _SESSION_NEEDED,
199 _FINISHED) = range(6)
200
201 # Used when in the _INITIAL_STREAM_NEEDED and _AUTH_STREAM_NEEDED
202 # states. Not an XML object as it's only the opening tag.
203 #
204 # The from and id attributes are filled in later.
205 _STREAM_DATA = (
206 '<stream:stream from="%s" id="%s" '
207 'version="1.0" xmlns:stream="http://etherx.jabber.org/streams" '
208 'xmlns="jabber:client">')
209
210 # Used when in the _INITIAL_STREAM_NEEDED state.
211 _AUTH_STANZA = ParseXml(
212 '<stream:features xmlns:stream="http://etherx.jabber.org/streams">'
213 ' <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">'
214 ' <mechanism>PLAIN</mechanism>'
215 ' <mechanism>X-GOOGLE-TOKEN</mechanism>'
216 ' </mechanisms>'
217 '</stream:features>')
218
219 # Used when in the _AUTH_NEEDED state.
220 _AUTH_SUCCESS_STANZA = ParseXml(
221 '<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>')
222
223 # Used when in the _AUTH_NEEDED state.
224 _AUTH_FAILURE_STANZA = ParseXml(
225 '<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>')
226
227 # Used when in the _AUTH_STREAM_NEEDED state.
228 _BIND_STANZA = ParseXml(
229 '<stream:features xmlns:stream="http://etherx.jabber.org/streams">'
230 ' <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/>'
231 ' <session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>'
232 '</stream:features>')
233
234 # Used when in the _BIND_NEEDED state.
235 #
236 # The id and jid attributes are filled in later.
237 _BIND_RESULT_STANZA = ParseXml(
238 '<iq id="" type="result">'
239 ' <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">'
240 ' <jid/>'
241 ' </bind>'
242 '</iq>')
243
244 # Used when in the _SESSION_NEEDED state.
245 #
246 # The id attribute is filled in later.
247 _IQ_RESPONSE_STANZA = ParseXml('<iq id="" type="result"/>')
248
249 def __init__(self, connection, resource_prefix, authenticated):
250 self._connection = connection
251 self._id_generator = IdGenerator(resource_prefix)
252 self._username = ''
253 self._domain = ''
254 self._jid = None
255 self._authenticated = authenticated
256 self._resource_prefix = resource_prefix
257 self._state = self._INITIAL_STREAM_NEEDED
258
259 def FeedStanza(self, stanza):
260 """Inspects the given stanza and changes the handshake state if needed.
261
262 Called when a stanza is received from the client. Inspects the
263 stanza to make sure it has the expected attributes given the
264 current state, advances the state if needed, and sends a reply to
265 the client if needed.
266 """
267 def ExpectStanza(stanza, name):
268 if stanza.tagName != name:
269 raise UnexpectedXml(stanza)
270
271 def ExpectIq(stanza, type, name):
272 ExpectStanza(stanza, 'iq')
273 if (stanza.getAttribute('type') != type or
274 stanza.firstChild.tagName != name):
275 raise UnexpectedXml(stanza)
276
277 def GetStanzaId(stanza):
278 return stanza.getAttribute('id')
279
280 def HandleStream(stanza):
281 ExpectStanza(stanza, 'stream:stream')
282 domain = stanza.getAttribute('to')
283 if domain:
284 self._domain = domain
285 SendStreamData()
286
287 def SendStreamData():
288 next_id = self._id_generator.GetNextId()
289 stream_data = self._STREAM_DATA % (self._domain, next_id)
290 self._connection.SendData(stream_data)
291
292 def GetUserDomain(stanza):
293 encoded_username_password = stanza.firstChild.data
294 username_password = base64.b64decode(encoded_username_password)
295 (_, username_domain, _) = username_password.split('\0')
296 # The domain may be omitted.
297 #
298 # If we were using python 2.5, we'd be able to do:
299 #
300 # username, _, domain = username_domain.partition('@')
301 # if not domain:
302 # domain = self._domain
303 at_pos = username_domain.find('@')
304 if at_pos != -1:
305 username = username_domain[:at_pos]
306 domain = username_domain[at_pos+1:]
307 else:
308 username = username_domain
309 domain = self._domain
310 return (username, domain)
311
312 def Finish():
313 self._state = self._FINISHED
314 self._connection.HandshakeDone(self._jid)
315
316 if self._state == self._INITIAL_STREAM_NEEDED:
317 HandleStream(stanza)
318 self._connection.SendStanza(self._AUTH_STANZA, False)
319 self._state = self._AUTH_NEEDED
320
321 elif self._state == self._AUTH_NEEDED:
322 ExpectStanza(stanza, 'auth')
323 (self._username, self._domain) = GetUserDomain(stanza)
324 if self._authenticated:
325 self._connection.SendStanza(self._AUTH_SUCCESS_STANZA, False)
326 self._state = self._AUTH_STREAM_NEEDED
327 else:
328 self._connection.SendStanza(self._AUTH_FAILURE_STANZA, False)
329 Finish()
330
331 elif self._state == self._AUTH_STREAM_NEEDED:
332 HandleStream(stanza)
333 self._connection.SendStanza(self._BIND_STANZA, False)
334 self._state = self._BIND_NEEDED
335
336 elif self._state == self._BIND_NEEDED:
337 ExpectIq(stanza, 'set', 'bind')
338 stanza_id = GetStanzaId(stanza)
339 resource_element = stanza.getElementsByTagName('resource')[0]
340 resource = resource_element.firstChild.data
341 full_resource = '%s.%s' % (self._resource_prefix, resource)
342 response = CloneXml(self._BIND_RESULT_STANZA)
343 response.setAttribute('id', stanza_id)
344 self._jid = Jid(self._username, self._domain, full_resource)
345 jid_text = response.parentNode.createTextNode(str(self._jid))
346 response.getElementsByTagName('jid')[0].appendChild(jid_text)
347 self._connection.SendStanza(response)
348 self._state = self._SESSION_NEEDED
349
350 elif self._state == self._SESSION_NEEDED:
351 ExpectIq(stanza, 'set', 'session')
352 stanza_id = GetStanzaId(stanza)
353 xml = CloneXml(self._IQ_RESPONSE_STANZA)
354 xml.setAttribute('id', stanza_id)
355 self._connection.SendStanza(xml)
356 Finish()
357
358
359 def AddrString(addr):
360 return '%s:%d' % addr
361
362
363 class XmppConnection(asynchat.async_chat):
364 """A single XMPP client connection.
365
366 This class handles the connection to a single XMPP client (via a
367 socket). It does the XMPP handshake and also implements the (old)
368 Google notification protocol.
369 """
370
371 # Used for acknowledgements to the client.
372 #
373 # The from and id attributes are filled in later.
374 _IQ_RESPONSE_STANZA = ParseXml('<iq from="" id="" type="result"/>')
375
376 def __init__(self, sock, socket_map, delegate, addr, authenticated):
377 """Starts up the xmpp connection.
378
379 Args:
380 sock: The socket to the client.
381 socket_map: A map from sockets to their owning objects.
382 delegate: The delegate, which is notified when the XMPP
383 handshake is successful, when the connection is closed, and
384 when a notification has to be broadcast.
385 addr: The host/port of the client.
386 """
387 # We do this because in versions of python < 2.6,
388 # async_chat.__init__ doesn't take a map argument nor pass it to
389 # dispatcher.__init__. We rely on the fact that
390 # async_chat.__init__ calls dispatcher.__init__ as the last thing
391 # it does, and that calling dispatcher.__init__ with socket=None
392 # and map=None is essentially a no-op.
393 asynchat.async_chat.__init__(self)
394 asyncore.dispatcher.__init__(self, sock, socket_map)
395
396 self.set_terminator(None)
397
398 self._delegate = delegate
399 self._parser = StanzaParser(self)
400 self._jid = None
401
402 self._addr = addr
403 addr_str = AddrString(self._addr)
404 self._handshake_task = HandshakeTask(self, addr_str, authenticated)
405 print 'Starting connection to %s' % self
406
407 def __str__(self):
408 if self._jid:
409 return str(self._jid)
410 else:
411 return AddrString(self._addr)
412
413 # async_chat implementation.
414
415 def collect_incoming_data(self, data):
416 self._parser.FeedString(data)
417
418 # This is only here to make pychecker happy.
419 def found_terminator(self):
420 asynchat.async_chat.found_terminator(self)
421
422 def close(self):
423 print "Closing connection to %s" % self
424 self._delegate.OnXmppConnectionClosed(self)
425 asynchat.async_chat.close(self)
426
427 # Called by self._parser.FeedString().
428 def FeedStanza(self, stanza):
429 if self._handshake_task:
430 self._handshake_task.FeedStanza(stanza)
431 elif stanza.tagName == 'iq' and stanza.getAttribute('type') == 'result':
432 # Ignore all client acks.
433 pass
434 elif (stanza.firstChild and
435 stanza.firstChild.namespaceURI == 'google:push'):
436 self._HandlePushCommand(stanza)
437 else:
438 raise UnexpectedXml(stanza)
439
440 # Called by self._handshake_task.
441 def HandshakeDone(self, jid):
442 if jid:
443 self._jid = jid
444 self._handshake_task = None
445 self._delegate.OnXmppHandshakeDone(self)
446 print "Handshake done for %s" % self
447 else:
448 print "Handshake failed for %s" % self
449 self.close()
450
451 def _HandlePushCommand(self, stanza):
452 if stanza.tagName == 'iq' and stanza.firstChild.tagName == 'subscribe':
453 # Subscription request.
454 self._SendIqResponseStanza(stanza)
455 elif stanza.tagName == 'message' and stanza.firstChild.tagName == 'push':
456 # Send notification request.
457 self._delegate.ForwardNotification(self, stanza)
458 else:
459 raise UnexpectedXml(command_xml)
460
461 def _SendIqResponseStanza(self, iq):
462 stanza = CloneXml(self._IQ_RESPONSE_STANZA)
463 stanza.setAttribute('from', str(self._jid.GetBareJid()))
464 stanza.setAttribute('id', iq.getAttribute('id'))
465 self.SendStanza(stanza)
466
467 def SendStanza(self, stanza, unlink=True):
468 """Sends a stanza to the client.
469
470 Args:
471 stanza: The stanza to send.
472 unlink: Whether to unlink stanza after sending it. (Pass in
473 False if stanza is a constant.)
474 """
475 self.SendData(stanza.toxml())
476 if unlink:
477 stanza.unlink()
478
479 def SendData(self, data):
480 """Sends raw data to the client.
481 """
482 # We explicitly encode to ascii as that is what the client expects
483 # (some minidom library functions return unicode strings).
484 self.push(data.encode('ascii'))
485
486 def ForwardNotification(self, notification_stanza):
487 """Forwards a notification to the client."""
488 notification_stanza.setAttribute('from', str(self._jid.GetBareJid()))
489 notification_stanza.setAttribute('to', str(self._jid))
490 self.SendStanza(notification_stanza, False)
491
492
493 class XmppServer(asyncore.dispatcher):
494 """The main XMPP server class.
495
496 The XMPP server starts accepting connections on the given address
497 and spawns off XmppConnection objects for each one.
498
499 Use like so:
500
501 socket_map = {}
502 xmpp_server = xmppserver.XmppServer(socket_map, ('127.0.0.1', 5222))
503 asyncore.loop(30.0, False, socket_map)
504 """
505
506 # Used when sending a notification.
507 _NOTIFICATION_STANZA = ParseXml(
508 '<message>'
509 ' <push xmlns="google:push">'
510 ' <data/>'
511 ' </push>'
512 '</message>')
513
514 def __init__(self, socket_map, addr):
515 asyncore.dispatcher.__init__(self, None, socket_map)
516 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
517 self.set_reuse_addr()
518 self.bind(addr)
519 self.listen(5)
520 self._socket_map = socket_map
521 self._connections = set()
522 self._handshake_done_connections = set()
523 self._notifications_enabled = True
524 self._authenticated = True
525
526 def handle_accept(self):
527 (sock, addr) = self.accept()
528 xmpp_connection = XmppConnection(
529 sock, self._socket_map, self, addr, self._authenticated)
530 self._connections.add(xmpp_connection)
531 # Return the new XmppConnection for testing.
532 return xmpp_connection
533
534 def close(self):
535 # A copy is necessary since calling close on each connection
536 # removes it from self._connections.
537 for connection in self._connections.copy():
538 connection.close()
539 asyncore.dispatcher.close(self)
540
541 def EnableNotifications(self):
542 self._notifications_enabled = True
543
544 def DisableNotifications(self):
545 self._notifications_enabled = False
546
547 def MakeNotification(self, channel, data):
548 """Makes a notification from the given channel and encoded data.
549
550 Args:
551 channel: The channel on which to send the notification.
552 data: The notification payload.
553 """
554 notification_stanza = CloneXml(self._NOTIFICATION_STANZA)
555 push_element = notification_stanza.getElementsByTagName('push')[0]
556 push_element.setAttribute('channel', channel)
557 data_element = push_element.getElementsByTagName('data')[0]
558 encoded_data = base64.b64encode(data)
559 data_text = notification_stanza.parentNode.createTextNode(encoded_data)
560 data_element.appendChild(data_text)
561 return notification_stanza
562
563 def SendNotification(self, channel, data):
564 """Sends a notification to all connections.
565
566 Args:
567 channel: The channel on which to send the notification.
568 data: The notification payload.
569 """
570 notification_stanza = self.MakeNotification(channel, data)
571 self.ForwardNotification(None, notification_stanza)
572 notification_stanza.unlink()
573
574 def SetAuthenticated(self, auth_valid):
575 self._authenticated = auth_valid
576
577 def GetAuthenticated(self):
578 return self._authenticated
579
580 # XmppConnection delegate methods.
581 def OnXmppHandshakeDone(self, xmpp_connection):
582 self._handshake_done_connections.add(xmpp_connection)
583
584 def OnXmppConnectionClosed(self, xmpp_connection):
585 self._connections.discard(xmpp_connection)
586 self._handshake_done_connections.discard(xmpp_connection)
587
588 def ForwardNotification(self, unused_xmpp_connection, notification_stanza):
589 if self._notifications_enabled:
590 for connection in self._handshake_done_connections:
591 print 'Sending notification to %s' % connection
592 connection.ForwardNotification(notification_stanza)
593 else:
594 print 'Notifications disabled; dropping notification'
OLDNEW
« no previous file with comments | « net/tools/testserver/testserver_base.py ('k') | net/tools/testserver/xmppserver_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698