4 from twisted.python import components
5 from twisted.web import server, resource
6 from twisted.internet import defer, task
7 from twisted.python import log
9 from zope.interface import Interface, implements
12 from twisted.words.xish import domish
14 from twisted.xish import domish
18 from session import make_session
20 from punjab.xmpp import ns
23 NS_BIND = 'http://jabber.org/protocol/httpbind'
24 NS_FEATURES = 'http://etherx.jabber.org/streams'
28 dummy element for a quicker parse
31 def __init__(self, *args, **kwargs):
37 class HttpbElementStream(domish.ExpatElementStream):
39 add rawXml to the elements
42 def _onStartElement(self, name, attrs):
43 # Generate a qname tuple from the provided name
48 qname = name.split(" ")
55 defaultUri = self.currElem.defaultUri
56 uri = self.currElem.uri
58 if not defaultUri and currentUri in self.defaultNsStack:
59 defaultUri = self.defaultNsStack[1]
61 if defaultUri and currentUri != defaultUri:
63 raw_xml = u"""<%s xmlns='%s'%s""" % (qname[1], qname[0], '%s')
66 raw_xml = u"""<%s%s""" % (qname[1], '%s')
70 for k, v in attrs.items():
73 attrs[(aqname[0], aqname[1])] = v
74 attr_str = attr_str + " xmlns:"+aqname[0] + "='" + aqname[1] + "'"
77 attr_str = attr_str + " " + k + "='" + v + "'"
79 raw_xml = raw_xml % (attr_str,)
81 # Construct the new element
82 e = domish.Element(qname, self.defaultNsStack[-1], attrs, self.localPrefixes)
83 self.localPrefixes = {}
85 # Document already started
86 if self.documentStarted == 1:
87 if self.currElem != None:
88 if len(self.currElem.children)==0 or isinstance(self.currElem.children[-1], domish.Element):
89 if self.currRawElem[-1] != ">":
90 self.currRawElem = self.currRawElem +">"
92 self.currElem.children.append(e)
93 e.parent = self.currElem
95 self.currRawElem = self.currRawElem + raw_xml
99 self.currRawElem = u''
100 self.documentStarted = 1
101 self.DocumentStartEvent(e)
103 def _onEndElement(self, _):
104 # Check for null current elem; end of doc
105 if self.currElem is None:
106 self.DocumentEndEvent()
108 # Check for parent that is None; that's
109 # the top of the stack
110 elif self.currElem.parent is None:
111 if len(self.currElem.children)>0:
112 self.currRawElem = self.currRawElem + "</"+ self.currElem.name+">"
114 self.currRawElem = self.currRawElem + "/>"
115 self.ElementEvent(self.currElem, self.currRawElem)
117 self.currRawElem = u''
118 # Anything else is just some element in the current
121 if len(self.currElem.children)==0:
122 self.currRawElem = self.currRawElem + "/>"
124 self.currRawElem = self.currRawElem + "</"+ self.currElem.name+">"
125 self.currElem = self.currElem.parent
127 def _onCdata(self, data):
128 if self.currElem != None:
129 if len(self.currElem.children)==0:
130 self.currRawElem = self.currRawElem + ">" + domish.escapeToXml(data)
131 #self.currRawElem = self.currRawElem + ">" + data
133 self.currRawElem = self.currRawElem + domish.escapeToXml(data)
134 #self.currRawElem = self.currRawElem + data
136 self.currElem.addContent(data)
138 def _onStartNamespace(self, prefix, uri):
139 # If this is the default namespace, put
142 self.defaultNsStack.append(uri)
144 self.localPrefixes[prefix] = uri
146 def _onEndNamespace(self, prefix):
147 # Remove last element on the stack
149 self.defaultNsStack.pop()
152 """ Preferred method to construct an ElementStream
154 Uses Expat-based stream if available, and falls back to Sux if necessary.
157 es = HttpbElementStream()
160 if domish.SuxElementStream is None:
161 raise Exception("No parsers available :(")
162 es = domish.SuxElementStream()
165 # make httpb body class, similar to xmlrpclib
169 An xml parser for parsing the body elements.
171 def __init__(self, use_t=False):
173 Call reset to initialize object
175 self.use_t = use_t # use domish element stream
179 def parse(self, buf):
181 Parse incoming xml and return the body and its children in a list
183 self.stream.parse(buf)
185 # return the doc element and its children in a list
186 return self.body, self.xmpp_elements
188 def serialize(self, obj):
190 Turn object into a string type
192 if isinstance(obj, domish.Element):
196 def onDocumentStart(self, rootelem):
198 The body document has started.
200 This should be a body.
202 if rootelem.name == 'body':
205 def onElement(self, element, raw_element = None):
207 A child element has been found.
209 if isinstance(element, domish.Element):
211 self.xmpp_elements.append(raw_element)
213 self.xmpp_elements.append(element)
222 self.stream = elementStream()
224 self.stream = domish.elementStream()
226 self.stream.DocumentStartEvent = self.onDocumentStart
227 self.stream.ElementEvent = self.onElement
228 self.stream.DocumentEndEvent = self.onDocumentEnd
230 self.xmpp_elements = []
233 def onDocumentEnd(self):
239 class IHttpbService(Interface):
241 Interface for http binding class
243 def __init__(self, verbose):
246 def startSession(self, body):
247 """ Start a punjab jabber session """
249 def endSession(self, session):
250 """ end a punjab jabber session """
252 def onExpire(self, session_id):
253 """ preform actions based on when the jabber connection expires """
255 def parseBody(self, body):
256 """ parse a body element """
259 def error(self, error):
260 """ send a body error element """
263 def inSession(self, body):
266 def getXmppElements(self, body, session):
271 class IHttpbFactory(Interface):
273 Factory class for generating binding sessions.
275 def startSession(self):
276 """ Start a punjab jabber session """
278 def endSession(self, session):
279 """ end a punjab jabber session """
281 def parseBody(self, body):
282 """ parse an body element """
284 def buildProtocol(self, addr):
285 """Return a protocol """
289 class Httpb(resource.Resource):
291 Http resource to handle BOSH requests.
294 def __init__(self, service, v = 0):
297 resource.Resource.__init__(self)
298 self.service = service
304 self.polling = self.service.polling or 15
306 def render_OPTIONS(self, request):
307 request.setHeader('Access-Control-Allow-Origin', '*')
308 request.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
309 request.setHeader('Access-Control-Allow-Headers', 'Content-Type')
310 request.setHeader('Access-Control-Max-Age', '86400')
313 def render_GET(self, request):
315 GET is not used, print docs.
317 request.setHeader('Access-Control-Allow-Origin', '*')
318 request.setHeader('Access-Control-Allow-Headers', 'Content-Type')
321 <a href='http://www.xmpp.org/extensions/xep-0124.html'>XEP-0124</a> - BOSH
325 def render_POST(self, request):
329 request.setHeader('Access-Control-Allow-Origin', '*')
330 request.setHeader('Access-Control-Allow-Headers', 'Content-Type')
331 request.content.seek(0, 0)
333 log.msg('HEADERS %s:' % (str(time.time()),))
334 log.msg(request.received_headers)
335 log.msg("HTTPB POST : ")
336 log.msg(str(request.content.read()))
337 request.content.seek(0, 0)
339 self.hp = HttpbParse()
341 body_tag, xmpp_elements = self.hp.parse(request.content.read())
344 if getattr(body_tag, 'name', '') != "body":
346 log.msg('Client sent bad POST data')
347 self.send_http_error(400, request)
348 return server.NOT_DONE_YET
349 except domish.ParserError:
350 log.msg('ERROR: Xml Parse Error')
353 self.send_http_error(400, request)
354 return server.NOT_DONE_YET
357 # reset parser, just in case
359 self.send_http_error(400, request)
360 return server.NOT_DONE_YET
362 if self.service.inSession(body_tag):
363 # sid is an existing session
364 if body_tag.getAttribute('rid'):
365 request.rid = body_tag['rid']
369 s, d = self.service.parseBody(body_tag, xmpp_elements)
370 d.addCallback(self.return_httpb, s, request)
371 elif body_tag.hasAttribute('sid'):
373 log.msg("no sid is found but the body element has a 'sid' attribute")
374 # This is an error, no sid is found but the body element has a 'sid' attribute
375 self.send_http_error(404, request)
376 return server.NOT_DONE_YET
379 s, d = self.service.startSession(body_tag, xmpp_elements)
380 d.addCallback(self.return_session, s, request)
382 # Add an error back for returned errors
383 d.addErrback(self.return_error, request)
384 return server.NOT_DONE_YET
387 def return_session(self, data, session, request):
389 if session.xmlstream is None:
390 self.send_http_error(200, request, 'remote-connection-failed',
392 return server.NOT_DONE_YET
394 b = domish.Element((NS_BIND, "body"))
395 # if we don't have an authid, we have to fail
396 if session.authid != 0:
397 b['authid'] = session.authid
399 self.send_http_error(500, request, 'internal-server-error',
401 return server.NOT_DONE_YET
403 b['sid'] = session.sid
404 b['wait'] = str(session.wait)
405 if session.secure == 0:
406 b['secure'] = 'false'
410 b['inactivity'] = str(session.inactivity)
411 ##b['polling'] = '15' # TODO: make this configurable
412 b['polling'] = str(self.polling)
413 b['requests'] = str(session.hold + 1)
414 b['window'] = str(session.window)
416 punjab.uriCheck(b, NS_BIND)
417 if session.attrs.has_key('content'):
418 b['content'] = session.attrs['content']
420 # We need to send features
423 if isinstance(felem, domish.Element):
428 self.return_body(request, b)
430 def return_httpb(self, data, session, request):
432 b = domish.Element((NS_BIND, "body"))
433 punjab.uriCheck(b, NS_BIND)
435 if getattr(session,'terminated', False):
436 b['type'] = 'terminate'
440 self.return_body(request, b, session.charset)
443 def return_error(self, e, request):
447 # TODO - clean this up and make errors better
448 if getattr(e.value,'stanza_error',None):
449 ec = getattr(e.value, 'children', None)
453 self.send_http_error(error.conditions[str(e.value.stanza_error)]['code'],
455 condition = str(e.value.stanza_error),
456 typ = error.conditions[str(e.value.stanza_error)]['type'],
459 return server.NOT_DONE_YET
461 self.send_http_error(error.conditions[str(e.value)]['code'],
464 error.conditions[str(e.value)]['type'])
465 return server.NOT_DONE_YET
467 self.send_http_error(500, request, 'internal-server-error', 'error', e)
473 def return_body(self, request, b, charset="utf-8"):
474 request.setResponseCode(200)
475 bxml = b.toXml(prefixes=ns.XMPP_PREFIXES.copy()).encode(charset,'replace')
477 request.setHeader('content-type', 'text/xml')
478 request.setHeader('content-length', len(bxml))
480 log.msg('\n\nRETURN HTTPB %s:' % (str(time.time()),))
482 if getattr(request, 'rid', None):
487 def send_http_error(self, code, request, condition = 'undefined-condition', typ = 'terminate', data = '', charset = 'utf-8', children=None):
488 request.setResponseCode(int(code))
490 b = domish.Element((NS_BIND, "body"))
492 b['condition'] = str(condition)
494 b['condition'] = 'undefined-condition'
499 b['type'] = 'terminate'
500 punjab.uriCheck(b, NS_BIND)
501 bxml = b.toXml().encode(charset, 'replace')
504 b.children += children
507 log.msg('HTTPB Error %d' %(int(code),))
509 if int(code) != 400 and int(code) != 404 and int(code) != 403:
511 if condition == 'see-other-uri':
512 b.addElement('uri', None, content = str(data))
514 t = b.addElement('text', content = str(data))
515 t['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-streams'
517 bxml = b.toXml().encode(charset, 'replace')
519 log.msg('HTTPB Return Error: ' + str(code) + ' -> ' + bxml)
520 request.setHeader("content-type", "text/xml")
521 request.setHeader("content-length", len(bxml))
524 request.setHeader("content-length", "0")
528 components.registerAdapter(Httpb, IHttpbService, resource.IResource)
531 class HttpbService(punjab.Service):
533 implements(IHttpbService)
538 verbose = 0, polling = 15,
539 use_raw = False, bindAddress=("0.0.0.0", 0),
540 session_creator = None):
541 if session_creator is not None:
542 self.make_session = session_creator
544 self.make_session = make_session
548 self.polling = polling
550 self.use_raw = use_raw
552 # run a looping call to do pollTimeouts on sessions
553 self.poll_timeouts = task.LoopingCall(self._doPollTimeOuts)
555 self.poll_timeouts.start(3) # run every 3 seconds
557 self.bindAddress=bindAddress
559 def _doPollTimeOuts(self):
561 Call poll time outs on sessions that have waited too long.
563 time_now = time.time() + 2.9 # need a number to offset the poll timeouts
564 for session in self.sessions.itervalues():
565 if len(session.waiting_requests)>0:
566 for wr in session.waiting_requests:
567 if time_now - wr.wait_start >= wr.timeout:
568 wr.delayedcall(wr.deferred)
571 def startSession(self, body, xmpp_elements):
572 """ Start a punjab jabber session """
575 if not body.hasAttribute('rid') or body['rid']=='':
577 log.msg('start session called but we had a rid')
578 return None, defer.fail(error.NotFound)
581 if not body.hasAttribute('to') or body['to']=='':
582 return None, defer.fail(error.BadRequest)
584 if self.white_list and body['to'] not in self.white_list:
585 return None, defer.fail(error.BadRequest)
588 if not body.hasAttribute('wait') or body['wait']=='':
593 if not body.hasAttribute("xml:lang") or body['xml:lang']=='':
594 for k in body.attributes:
595 if isinstance(k, tuple):
596 if str(k[1])=='lang' and body.getAttribute(k) !='':
597 lang = body.getAttribute(k)
600 if not body.hasAttribute('inactivity'):
601 body['inactivity'] = 60
603 return self.make_session(self, body.attributes)
606 def parseBody(self, body, xmpp_elements):
609 if body.hasAttribute('sid'):
610 sid = str(body['sid'])
613 log.msg('Session ID not found')
614 return None, defer.fail(error.NotFound)
616 if self.inSession(body):
617 s = self.sessions[sid]
618 s.touch() # any connection should be a renew on wait
621 log.msg('session does not exist?')
622 return None, defer.fail(error.NotFound)
623 ## XXX this seems to break xmpp:restart='true' --vargas
624 ## (cf. http://www.xmpp.org/extensions/xep-0206.html#preconditions-sasl [Example 10])
625 ## if body.hasAttribute('to') and body['to']!='':
626 ## return s, defer.fail(error.BadRequest)
629 # TODO - clean this up
632 if body.hasAttribute('newkey'):
633 newkey = body['newkey']
637 if body.hasAttribute('key') and not foundNewKey:
638 if s.key is not None:
639 nk = sha.new(body['key'])
641 next_key = body['key']
646 log.msg('Error in key')
647 return s, defer.fail(error.NotFound)
650 raise s, defer.fail(error.NotFound)
653 log.msg('HTTPB ERROR: ')
655 return s, defer.fail(error.NotFound)
657 # need to check if this is a valid rid (within tolerance)
658 if body.hasAttribute('rid') and body['rid']!='':
659 if s.cache_data.has_key(int(body['rid'])):
661 # implements issue 32 and returns the data returned on a dropped connection
662 return s, defer.succeed(s.cache_data[int(body['rid'])])
663 if abs(int(body['rid']) - int(s.rid)) > s.window:
665 log.msg('This rid is invalid %s %s ' % (str(body['rid']), str(s.rid),))
666 return s, defer.fail(error.NotFound)
669 log.msg('There is no rid on this request')
670 return s, defer.fail(error.NotFound)
672 return s, self._parse(s, body, xmpp_elements)
677 return s, defer.fail(error.InternalServerError)
680 def onExpire(self, session_id):
681 """ preform actions based on when the jabber connection expires """
683 log.msg('expire (%s)' % (str(session_id),))
684 log.msg(len(self.sessions.keys()))
686 def _parse(self, session, body_tag, xmpp_elements):
687 # increment the request counter
688 session.rid = session.rid + 1
692 if getattr(session, 'stream_error', None) != None:
693 # set up waiting request
695 d.errback(session.stream_error)
701 # send all the elements
702 for el in xmpp_elements:
703 if not isinstance(el, domish.Element):
704 session.sendRawXml(el)
707 # something is wrong here, need to figure out what
708 # the xmlns will be lost if this is not done
709 # punjab.uriCheck(el,NS_BIND)
710 # if el.uri and el.uri != NS_BIND:
711 # el['xmlns'] = el.uri
712 # TODO - get rid of this when we stop supporting old versions
714 if el.uri == NS_BIND:
716 if el.defaultUri == NS_BIND:
719 session.sendRawXml(el)
722 if body_tag.hasAttribute('type') and \
723 body_tag['type'] == 'terminate':
724 d = session.terminate()
727 d = session.poll(d, rid = int(body_tag['rid']))
731 def _returnIq(self, cur_session, d, iq):
733 A callback from auth iqs
735 return cur_session.poll(d)
737 def _cbIq(self, iq, cur_session, d):
739 A callback from auth iqs
742 # session.elems.append(iq)
743 return cur_session.poll(d)
746 def inSession(self, body):
748 if body.hasAttribute('sid'):
749 if self.sessions.has_key(body['sid']):
753 def getXmppElements(self, b, session):
755 Get waiting xmpp elements
757 for i, obj in enumerate(session.msgs):
758 m = session.msgs.pop(0)
760 for i, obj in enumerate(session.prs):
761 p = session.prs.pop(0)
763 for i, obj in enumerate(session.iqs):
764 iq = session.iqs.pop(0)
769 def endSession(self, cur_session):
770 """ end a punjab jabber session """
771 d = cur_session.terminate()