allow limiting servers with a white list
[punjab-krb5-preauth] / punjab / httpb.py
1 """
2  http binding interface
3 """
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
8
9 from zope.interface import Interface, implements
10
11 try:
12     from twisted.words.xish import domish
13 except ImportError:
14     from twisted.xish import domish
15
16 import sha, time
17 import error
18 from session import make_session
19 import punjab
20 from punjab.xmpp import ns
21
22
23 NS_BIND = 'http://jabber.org/protocol/httpbind'
24 NS_FEATURES = 'http://etherx.jabber.org/streams'
25
26 class DummyElement:
27     """
28     dummy element for a quicker parse
29     """
30     # currently not used
31     def __init__(self, *args, **kwargs):
32
33         self.children = []
34
35
36
37 class HttpbElementStream(domish.ExpatElementStream):
38     """
39     add rawXml to the elements
40     """
41
42     def _onStartElement(self, name, attrs):
43         # Generate a qname tuple from the provided name
44         attr_str   = ''
45         prefix_str = ''
46         defaultUri = None
47         uri        = None
48         qname = name.split(" ")
49         if len(qname) == 1:
50             qname = ('', name)
51             currentUri = None
52         else:
53             currentUri = qname[0]
54         if self.currElem:
55             defaultUri = self.currElem.defaultUri
56             uri = self.currElem.uri
57             
58         if not defaultUri and currentUri in self.defaultNsStack:
59             defaultUri = self.defaultNsStack[1]
60         
61         if defaultUri and currentUri != defaultUri:
62
63             raw_xml = u"""<%s xmlns='%s'%s""" % (qname[1], qname[0], '%s')
64         
65         else:
66             raw_xml = u"""<%s%s""" % (qname[1], '%s')
67
68
69         # Process attributes
70         for k, v in attrs.items():
71             if k.find(" ") != -1:
72                 aqname = k.split(" ")
73                 attrs[(aqname[0], aqname[1])] = v
74                 attr_str = attr_str + " xmlns:"+aqname[0] + "='" + aqname[1] + "'" 
75                 del attrs[k]
76
77             attr_str = attr_str + " " + k + "='" + v + "'" 
78
79         raw_xml = raw_xml % (attr_str,)
80         
81         # Construct the new element
82         e = domish.Element(qname, self.defaultNsStack[-1], attrs, self.localPrefixes)
83         self.localPrefixes = {}
84         
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 +">"
91
92                 self.currElem.children.append(e)
93                 e.parent = self.currElem
94             
95             self.currRawElem = self.currRawElem + raw_xml
96             self.currElem = e
97         # New document
98         else:
99             self.currRawElem = u''
100             self.documentStarted = 1
101             self.DocumentStartEvent(e)
102
103     def _onEndElement(self, _):
104         # Check for null current elem; end of doc
105         if self.currElem is None:
106             self.DocumentEndEvent()
107             
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+">"
113             else:
114                 self.currRawElem = self.currRawElem + "/>"
115             self.ElementEvent(self.currElem, self.currRawElem)
116             self.currElem = None
117             self.currRawElem = u''
118         # Anything else is just some element in the current
119         # packet wrapping up
120         else:
121             if len(self.currElem.children)==0:
122                 self.currRawElem = self.currRawElem + "/>"
123             else:
124                 self.currRawElem = self.currRawElem + "</"+ self.currElem.name+">"
125             self.currElem = self.currElem.parent
126
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
132             else:
133                 self.currRawElem = self.currRawElem  + domish.escapeToXml(data)
134                 #self.currRawElem = self.currRawElem  + data
135             
136             self.currElem.addContent(data)
137
138     def _onStartNamespace(self, prefix, uri):
139         # If this is the default namespace, put
140         # it on the stack
141         if prefix is None:
142             self.defaultNsStack.append(uri)
143         else:
144             self.localPrefixes[prefix] = uri
145
146     def _onEndNamespace(self, prefix):
147         # Remove last element on the stack
148         if prefix is None:
149             self.defaultNsStack.pop()
150
151 def elementStream():
152     """ Preferred method to construct an ElementStream
153
154     Uses Expat-based stream if available, and falls back to Sux if necessary.
155     """
156     try:
157         es = HttpbElementStream()
158         return es
159     except ImportError:
160         if domish.SuxElementStream is None:
161             raise Exception("No parsers available :(")
162         es = domish.SuxElementStream()
163         return es
164
165 # make httpb body class, similar to xmlrpclib
166
167 class HttpbParse:
168     """
169     An xml parser for parsing the body elements.
170     """
171     def __init__(self, use_t=False):
172         """
173         Call reset to initialize object
174         """
175         self.use_t = use_t # use domish element stream
176         self._reset()
177
178
179     def parse(self, buf):
180         """
181         Parse incoming xml and return the body and its children in a list
182         """
183         self.stream.parse(buf)
184         
185         # return the doc element and its children in a list
186         return self.body, self.xmpp_elements 
187     
188     def serialize(self, obj):
189         """
190         Turn object into a string type
191         """
192         if isinstance(obj, domish.Element):
193             obj = obj.toXml()
194         return obj
195
196     def onDocumentStart(self, rootelem):
197         """
198         The body document has started.
199         
200         This should be a body.
201         """
202         if rootelem.name == 'body':
203             self.body = rootelem
204
205     def onElement(self, element, raw_element = None):
206         """
207         A child element has been found.
208         """
209         if isinstance(element, domish.Element):
210             if raw_element:
211                 self.xmpp_elements.append(raw_element)
212             else:
213                 self.xmpp_elements.append(element)
214         else:
215             pass
216
217     def _reset(self):
218         """
219         Setup the parser
220         """
221         if not self.use_t:
222             self.stream = elementStream()
223         else:
224             self.stream = domish.elementStream()
225
226         self.stream.DocumentStartEvent = self.onDocumentStart
227         self.stream.ElementEvent = self.onElement
228         self.stream.DocumentEndEvent = self.onDocumentEnd
229         self.body = ""
230         self.xmpp_elements = []
231         
232
233     def onDocumentEnd(self):
234         """
235         Body End
236         """
237         pass
238
239 class IHttpbService(Interface):
240     """
241     Interface for http binding class
242     """
243     def __init__(self, verbose):
244         """ """
245
246     def startSession(self, body):
247         """ Start a punjab jabber session """
248
249     def endSession(self, session):
250         """ end a punjab jabber session """
251
252     def onExpire(self, session_id):
253         """ preform actions based on when the jabber connection expires """
254
255     def parseBody(self, body):
256         """ parse a body element """
257
258
259     def error(self, error):
260         """ send a body error element """
261
262
263     def inSession(self, body):
264         """ """
265
266     def getXmppElements(self, body, session):
267         """ """
268         
269
270         
271 class IHttpbFactory(Interface):
272     """
273     Factory class for generating binding sessions.
274     """
275     def startSession(self):
276         """ Start a punjab jabber session """
277
278     def endSession(self, session):
279         """ end a punjab jabber session """
280
281     def parseBody(self, body):
282         """ parse an body element """
283
284     def buildProtocol(self, addr):
285         """Return a protocol """
286
287         
288     
289 class Httpb(resource.Resource):
290     """
291     Http resource to handle BOSH requests.
292     """
293     isLeaf = True
294     def __init__(self, service, v = 0):
295         """Initialize.
296         """
297         resource.Resource.__init__(self)
298         self.service  = service
299         self.hp       = None
300         self.children = {}
301         self.client   = 0
302         self.verbose  = v
303
304         self.polling = self.service.polling or 15
305
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')
311         return ""
312                 
313     def render_GET(self, request):
314         """
315         GET is not used, print docs.
316         """
317         request.setHeader('Access-Control-Allow-Origin', '*')
318         request.setHeader('Access-Control-Allow-Headers', 'Content-Type')
319         return """<html>
320                  <body>
321                  <a href='http://www.xmpp.org/extensions/xep-0124.html'>XEP-0124</a> - BOSH
322                  </body>
323                </html>"""
324
325     def render_POST(self, request):
326         """
327         Parse received xml
328         """
329         request.setHeader('Access-Control-Allow-Origin', '*')
330         request.setHeader('Access-Control-Allow-Headers', 'Content-Type')
331         request.content.seek(0, 0)
332         if self.service.v:
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)       
338
339         self.hp       = HttpbParse()
340         try:
341             body_tag, xmpp_elements = self.hp.parse(request.content.read()) 
342             self.hp._reset()
343
344             if getattr(body_tag, 'name', '') != "body":
345                 if self.service.v:
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')
351             log.err()
352             self.hp._reset()
353             self.send_http_error(400, request) 
354             return server.NOT_DONE_YET
355         except:
356             log.err()
357             # reset parser, just in case
358             self.hp._reset()
359             self.send_http_error(400, request) 
360             return server.NOT_DONE_YET
361         else:
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']
366                     if self.service.v:
367                         log.msg(request.rid)
368                 
369                 s, d = self.service.parseBody(body_tag, xmpp_elements)
370                 d.addCallback(self.return_httpb, s, request)
371             elif body_tag.hasAttribute('sid'):
372                 if self.service.v:
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
377             else:
378                 # start session
379                 s, d = self.service.startSession(body_tag, xmpp_elements)
380                 d.addCallback(self.return_session, s, request)
381                 
382             # Add an error back for returned errors
383             d.addErrback(self.return_error, request)
384         return server.NOT_DONE_YET
385         
386
387     def return_session(self, data, session, request):
388         # create body
389         if session.xmlstream is None:
390             self.send_http_error(200, request, 'remote-connection-failed',
391                                  'terminate')
392             return server.NOT_DONE_YET
393         
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
398         else:
399             self.send_http_error(500, request, 'internal-server-error',
400                                  'terminate')
401             return server.NOT_DONE_YET
402         
403         b['sid']  = session.sid
404         b['wait'] = str(session.wait)
405         if session.secure == 0:
406             b['secure'] = 'false'
407         else:
408             b['secure'] = 'true'
409             
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)
415         
416         punjab.uriCheck(b, NS_BIND)
417         if session.attrs.has_key('content'):
418             b['content'] = session.attrs['content']
419
420         # We need to send features
421         while len(data) > 0:
422             felem = data.pop(0)
423             if isinstance(felem, domish.Element):
424                 b.addChild(felem)
425             else:
426                 b.addRawXml(felem)
427
428         self.return_body(request, b)
429
430     def return_httpb(self, data, session, request):
431         # create body                
432         b = domish.Element((NS_BIND, "body"))
433         punjab.uriCheck(b, NS_BIND)
434         session.touch()
435         if getattr(session,'terminated', False):
436             b['type']      = 'terminate'
437         if data:
438             b.children += data
439         
440         self.return_body(request, b, session.charset)        
441
442     
443     def return_error(self, e, request):
444         echildren = []
445         
446         try:
447             # TODO - clean this up and make errors better
448             if getattr(e.value,'stanza_error',None):
449                 ec = getattr(e.value, 'children', None)
450                 if ec:
451                     echildren = ec
452                     
453                 self.send_http_error(error.conditions[str(e.value.stanza_error)]['code'],
454                                      request,
455                                      condition = str(e.value.stanza_error),
456                                      typ = error.conditions[str(e.value.stanza_error)]['type'],
457                                      children=echildren)
458                 
459                 return  server.NOT_DONE_YET
460             elif e.value:
461                 self.send_http_error(error.conditions[str(e.value)]['code'],
462                                      request,
463                                      str(e.value),
464                                      error.conditions[str(e.value)]['type'])
465                 return  server.NOT_DONE_YET
466             else:
467                 self.send_http_error(500, request, 'internal-server-error', 'error', e)
468         except:
469             log.err()
470             pass
471
472     
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')
476         
477         request.setHeader('content-type', 'text/xml')
478         request.setHeader('content-length', len(bxml))
479         if self.service.v:
480             log.msg('\n\nRETURN HTTPB %s:' % (str(time.time()),))
481             log.msg(bxml)
482             if getattr(request, 'rid', None):
483                 log.msg(request.rid)
484         request.write(bxml)
485         request.finish()
486             
487     def send_http_error(self, code, request, condition = 'undefined-condition', typ = 'terminate', data = '', charset = 'utf-8', children=None):
488         request.setResponseCode(int(code))
489         
490         b = domish.Element((NS_BIND, "body"))
491         if condition:
492             b['condition'] = str(condition)
493         else:
494             b['condition'] = 'undefined-condition'
495
496         if typ:
497             b['type']      = str(typ)
498         else:
499             b['type']      = 'terminate'
500         punjab.uriCheck(b, NS_BIND)
501         bxml           = b.toXml().encode(charset, 'replace')
502
503         if children:
504             b.children += children
505
506         if self.service.v:
507             log.msg('HTTPB Error %d' %(int(code),))
508         
509         if int(code) != 400 and int(code) != 404 and int(code) != 403:
510             if data != '':
511                 if condition == 'see-other-uri':
512                     b.addElement('uri', None, content = str(data))
513                 else:
514                     t = b.addElement('text', content = str(data))
515                     t['xmlns'] = 'urn:ietf:params:xml:ns:xmpp-streams'
516                     
517             bxml           = b.toXml().encode(charset, 'replace')
518             if self.service.v:
519                 log.msg('HTTPB Return Error: ' + str(code) + ' -> ' + bxml)
520             request.setHeader("content-type", "text/xml")
521             request.setHeader("content-length", len(bxml))    
522             request.write(bxml)
523         else:
524             request.setHeader("content-length", "0")
525         request.finish()
526
527
528 components.registerAdapter(Httpb, IHttpbService, resource.IResource)
529
530
531 class HttpbService(punjab.Service):
532
533     implements(IHttpbService)
534
535     white_list = []
536
537     def __init__(self, 
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
543         else:
544             self.make_session = make_session
545         self.v  = verbose
546         self.sessions = {}
547         self.counter  = 0
548         self.polling = polling
549         # self.expired  = {}
550         self.use_raw  = use_raw
551
552         # run a looping call to do pollTimeouts on sessions
553         self.poll_timeouts = task.LoopingCall(self._doPollTimeOuts)
554
555         self.poll_timeouts.start(3) # run every 3 seconds
556
557         self.bindAddress=bindAddress
558
559     def _doPollTimeOuts(self):
560         """
561         Call poll time outs on sessions that have waited too long.
562         """
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)
569
570             
571     def startSession(self, body, xmpp_elements):
572         """ Start a punjab jabber session """
573     
574         # look for rid
575         if not body.hasAttribute('rid') or body['rid']=='':
576             if self.v:
577                 log.msg('start session called but we had a rid')
578             return None, defer.fail(error.NotFound)
579                 
580         # look for to
581         if not body.hasAttribute('to') or body['to']=='':
582             return None, defer.fail(error.BadRequest)
583         
584         if self.white_list and body['to'] not in self.white_list:
585             return None, defer.fail(error.BadRequest)
586
587         # look for wait
588         if not body.hasAttribute('wait') or body['wait']=='':
589             body['wait'] = 3
590                 
591         # look for lang
592         lang = None
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)
598         if lang:
599             body['lang'] = lang
600         if not body.hasAttribute('inactivity'):
601             body['inactivity'] = 60 
602         
603         return self.make_session(self, body.attributes)
604                 
605
606     def parseBody(self, body, xmpp_elements):
607         try:
608             # grab session                    
609             if body.hasAttribute('sid'):
610                 sid = str(body['sid'])
611             else:
612                 if self.v:
613                     log.msg('Session ID not found')
614                 return None, defer.fail(error.NotFound)
615
616             if self.inSession(body):
617                 s = self.sessions[sid]
618                 s.touch() # any connection should be a renew on wait
619             else:
620                 if self.v:
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)
627             
628             # check for keys
629             # TODO - clean this up
630             foundNewKey = False
631             
632             if body.hasAttribute('newkey'):
633                 newkey = body['newkey']
634                 s.key = newkey
635                 foundNewKey = True
636             try:
637                 if body.hasAttribute('key') and not foundNewKey:
638                     if s.key is not None:
639                         nk = sha.new(body['key'])
640                         key = nk.hexdigest()
641                         next_key = body['key']
642                         if key == s.key:
643                             s.key = next_key
644                         else:
645                             if self.v:
646                                 log.msg('Error in key')
647                             return s, defer.fail(error.NotFound)                        
648                     else:
649                         log.err()
650                         raise s, defer.fail(error.NotFound)
651                         
652             except:
653                 log.msg('HTTPB ERROR: ')
654                 log.err()
655                 return s, defer.fail(error.NotFound)
656                     
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'])):
660                     s.touch()
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:
664                     if self.v:
665                         log.msg('This rid is invalid %s %s ' % (str(body['rid']), str(s.rid),))
666                     return  s, defer.fail(error.NotFound)
667             else:
668                 if self.v:
669                     log.msg('There is no rid on this request')
670                 return  s, defer.fail(error.NotFound)
671             
672             return s, self._parse(s, body, xmpp_elements)
673             
674             
675         except:
676             log.err()
677             return  s, defer.fail(error.InternalServerError)
678
679             
680     def onExpire(self, session_id):
681         """ preform actions based on when the jabber connection expires """
682         if self.v:
683             log.msg('expire (%s)' % (str(session_id),))
684             log.msg(len(self.sessions.keys()))
685         
686     def _parse(self, session, body_tag, xmpp_elements):
687         # increment the request counter
688         session.rid  = session.rid + 1
689         dont_poll = False
690         d = None
691         
692         if getattr(session, 'stream_error', None) != None:
693             # set up waiting request
694             d = defer.Deferred()            
695             d.errback(session.stream_error)
696             session.elems = []
697             session.terminate()
698
699             dont_poll = True
700         else:
701             # send all the elements
702             for el in xmpp_elements:
703                 if not isinstance(el, domish.Element):
704                     session.sendRawXml(el)
705                     continue
706             
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
713                 #        of twisted.words
714                 if el.uri == NS_BIND:
715                     el.uri = None
716                 if el.defaultUri == NS_BIND:
717                     el.defaultUri = None
718                     
719                 session.sendRawXml(el)
720
721
722         if body_tag.hasAttribute('type') and \
723            body_tag['type'] == 'terminate':
724             d = session.terminate()
725         elif not dont_poll:
726             # normal request
727             d = session.poll(d, rid = int(body_tag['rid']))
728             
729         return d
730         
731     def _returnIq(self, cur_session, d, iq):
732         """
733         A callback from auth iqs
734         """
735         return cur_session.poll(d)
736
737     def _cbIq(self, iq, cur_session, d):
738         """
739         A callback from auth iqs
740         """
741         
742         # session.elems.append(iq)
743         return cur_session.poll(d)
744         
745         
746     def inSession(self, body):
747         """ """
748         if body.hasAttribute('sid'):
749             if self.sessions.has_key(body['sid']):        
750                 return True
751         return False
752
753     def getXmppElements(self, b, session):
754         """
755         Get waiting xmpp elements
756         """
757         for i, obj in enumerate(session.msgs):
758             m = session.msgs.pop(0)
759             b.addChild(m)            
760         for i, obj in enumerate(session.prs):
761             p = session.prs.pop(0)
762             b.addChild(p)            
763         for i, obj in enumerate(session.iqs):
764             iq = session.iqs.pop(0)
765             b.addChild(iq)
766         
767         return b
768
769     def endSession(self, cur_session):
770         """ end a punjab jabber session """
771         d = cur_session.terminate()
772         return d
773