Merge pull request #345 from sangeeths/xep_0332

XEP-0332: HTTP over XMPP transport
This commit is contained in:
Mike Taylor 2015-04-28 22:44:27 -04:00
commit 192b7e0349
11 changed files with 435 additions and 1 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ sleekxmpp.egg-info/
*~ *~
.baboon/ .baboon/
.DS_STORE .DS_STORE
.idea/

View File

@ -161,7 +161,7 @@ item itself, and the JID and node that will own the item.
In this case, the owning JID and node are provided with the In this case, the owning JID and node are provided with the
parameters ``ijid`` and ``node``. parameters ``ijid`` and ``node``.
Peforming Disco Queries Performing Disco Queries
----------------------- -----------------------
The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs
and their nodes for disco information. Since these methods are wrappers for and their nodes for disco information. Since these methods are wrappers for

101
examples/http_over_xmpp.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp import ClientXMPP
from optparse import OptionParser
import logging
import getpass
class HTTPOverXMPPClient(ClientXMPP):
def __init__(self, jid, password):
ClientXMPP.__init__(self, jid, password)
self.register_plugin('xep_0332') # HTTP over XMPP Transport
self.add_event_handler(
'session_start', self.session_start, threaded=True
)
self.add_event_handler('http_request', self.http_request_received)
self.add_event_handler('http_response', self.http_response_received)
def http_request_received(self, iq):
pass
def http_response_received(self, iq):
print 'HTTP Response Received : ', iq
print 'From : ', iq['from']
print 'To : ', iq['to']
print 'Type : ', iq['type']
print 'Headers : ', iq['resp']['headers']
print 'Code : ', iq['resp']['code']
print 'Message : ', iq['resp']['message']
print 'Data : ', iq['resp']['data']
def session_start(self, event):
# TODO: Fill in the blanks
self['xep_0332'].send_request(
to='?', method='?', resource='?', headers={}
)
self.disconnect()
if __name__ == '__main__':
#
# NOTE: To run this example, fill up the blanks in session_start() and
# use the following command.
#
# ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v]
#
parser = OptionParser()
# Output verbosity options.
parser.add_option(
'-v', '--verbose', help='set logging to DEBUG', action='store_const',
dest='loglevel', const=logging.DEBUG, default=logging.ERROR
)
# JID and password options.
parser.add_option('-J', '--jid', dest='jid', help='JID')
parser.add_option('-P', '--password', dest='password', help='Password')
# XMPP server ip and port options.
parser.add_option(
'-i', '--ipaddr', dest='ipaddr',
help='IP Address of the XMPP server', default=None
)
parser.add_option(
'-p', '--port', dest='port',
help='Port of the XMPP server', default=None
)
opts, args = parser.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input('Username: ')
if opts.password is None:
opts.password = getpass.getpass('Password: ')
xmpp = HTTPOverXMPPClient(opts.jid, opts.password)
if xmpp.connect((opts.ipaddr, int(opts.port))):
print 'Connected!'
xmpp.process(block=True)
else:
print 'Not connected!'
print 'Goodbye....'

View File

@ -123,6 +123,8 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0323/stanza', 'sleekxmpp/plugins/xep_0323/stanza',
'sleekxmpp/plugins/xep_0325', 'sleekxmpp/plugins/xep_0325',
'sleekxmpp/plugins/xep_0325/stanza', 'sleekxmpp/plugins/xep_0325/stanza',
'sleekxmpp/plugins/xep_0332',
'sleekxmpp/plugins/xep_0332/stanza',
'sleekxmpp/plugins/google', 'sleekxmpp/plugins/google',
'sleekxmpp/plugins/google/gmail', 'sleekxmpp/plugins/google/gmail',
'sleekxmpp/plugins/google/auth', 'sleekxmpp/plugins/google/auth',

View File

@ -83,4 +83,5 @@ __all__ = [
'xep_0319', # Last User Interaction in Presence 'xep_0319', # Last User Interaction in Presence
'xep_0323', # IoT Systems Sensor Data 'xep_0323', # IoT Systems Sensor Data
'xep_0325', # IoT Systems Control 'xep_0325', # IoT Systems Control
'xep_0332', # HTTP Over XMPP Transport
] ]

View File

@ -0,0 +1,17 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0332 import stanza
from sleekxmpp.plugins.xep_0332.http import XEP_0332
register_plugin(XEP_0332)

View File

@ -0,0 +1,143 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.base import BasePlugin
from sleekxmpp.plugins.xep_0332.stanza import Request, Response, Data
from sleekxmpp.plugins.xep_0131.stanza import Headers
log = logging.getLogger(__name__)
class XEP_0332(BasePlugin):
"""
XEP-0332: HTTP over XMPP transport
"""
name = 'xep_0332'
description = 'XEP-0332: HTTP over XMPP transport'
#: xep_0047 not included.
#: xep_0001, 0137 and 0166 are missing
dependencies = set(['xep_0030', 'xep_0131'])
#: TODO: Do we really need to mention the supported_headers?!
default_config = {
'supported_headers': set([
'Content-Length', 'Transfer-Encoding', 'DateTime',
'Accept-Charset', 'Location', 'Content-ID', 'Description',
'Content-Language', 'Content-Transfer-Encoding', 'Timestamp',
'Expires', 'User-Agent', 'Host', 'Proxy-Authorization', 'Date',
'WWW-Authenticate', 'Accept-Encoding', 'Server', 'Error-Info',
'Identifier', 'Content-Location', 'Content-Encoding', 'Distribute',
'Accept', 'Proxy-Authenticate', 'ETag', 'Expect', 'Content-Type'
])
}
def plugin_init(self):
self.xmpp.register_handler(Callback(
'HTTP Request', StanzaPath('iq/req'), self._handle_request
))
self.xmpp.register_handler(Callback(
'HTTP Response', StanzaPath('iq/resp'), self._handle_response
))
register_stanza_plugin(Iq, Request, iterable=True)
register_stanza_plugin(Iq, Response, iterable=True)
register_stanza_plugin(Request, Headers, iterable=True)
register_stanza_plugin(Request, Data, iterable=True)
register_stanza_plugin(Response, Headers, iterable=True)
register_stanza_plugin(Response, Data, iterable=True)
# TODO: Should we register any api's here? self.api.register()
def plugin_end(self):
self.xmpp.remove_handler('HTTP Request')
self.xmpp.remove_handler('HTTP Response')
self.xmpp['xep_0030'].del_feature('urn:xmpp:http')
for header in self.supported_headers:
self.xmpp['xep_0030'].del_feature(
feature='%s#%s' % (Headers.namespace, header)
)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('urn:xmpp:http')
for header in self.supported_headers:
self.xmpp['xep_0030'].add_feature(
'%s#%s' % (Headers.namespace, header)
)
# TODO: Do we need to add the supported headers to xep_0131?
# self.xmpp['xep_0131'].supported_headers.add(header)
def _handle_request(self, iq):
self.xmpp.event('http_request', iq)
def _handle_response(self, iq):
self.xmpp.event('http_response', iq)
def send_request(self, to=None, method=None, resource=None, headers=None,
data=None, **kwargs):
iq = self.xmpp.Iq()
iq['from'] = self.xmpp.boundjid
iq['to'] = to
iq['type'] = 'set'
iq['req']['headers'] = headers
iq['req']['method'] = method
iq['req']['resource'] = resource
iq['req']['version'] = '1.1' # TODO: set this implicitly
if data is not None:
iq['req']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
def send_response(self, to=None, code=None, message=None, headers=None,
data=None, **kwargs):
iq = self.xmpp.Iq()
iq['from'] = self.xmpp.boundjid
iq['to'] = to
iq['type'] = 'result'
iq['resp']['headers'] = headers
iq['resp']['code'] = code
iq['resp']['message'] = message
iq['resp']['version'] = '1.1' # TODO: set this implicitly
if data is not None:
iq['resp']['data'] = data
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)
def send_error(self, to=None, ecode=500, etype='wait',
econd='internal-server-error', **kwargs):
iq = self.xmpp.Iq()
iq['type'] = 'error'
iq['from'] = self.xmpp.boundjid
iq['to'] = to
iq['error']['code'] = ecode
iq['error']['type'] = etype
iq['error']['condition'] = econd
return iq.send(
timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)
)

View File

@ -0,0 +1,13 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.xep_0332.stanza.request import Request
from sleekxmpp.plugins.xep_0332.stanza.response import Response
from sleekxmpp.plugins.xep_0332.stanza.data import Data

View File

@ -0,0 +1,30 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class Data(ElementBase):
"""
The data element.
"""
name = 'data'
namespace = 'urn:xmpp:http'
interfaces = set(['data'])
plugin_attrib = 'data'
is_extension = True
def get_data(self, encoding='text'):
data = self._get_sub_text(encoding, None)
return str(data) if data is not None else data
def set_data(self, data, encoding='text'):
self._set_sub_text(encoding, text=data)

View File

@ -0,0 +1,65 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class Request(ElementBase):
"""
All HTTP communication is done using the `Request`/`Response` paradigm.
Each HTTP Request is made sending an `iq` stanza containing a `req` element
to the server. Each `iq` stanza sent is of type `set`.
Examples:
<iq type='set' from='a@b.com/browser' to='x@y.com' id='1'>
<req xmlns='urn:xmpp:http' method='GET' resource='/api/users' version='1.1'>
<headers xmlns='http://jabber.org/protocol/shim'>
<header name='Host'>b.com</header>
</headers>
</req>
</iq>
<iq type='set' from='a@b.com/browser' to='x@y.com' id='2'>
<req xmlns='urn:xmpp:http' method='PUT' resource='/api/users' version='1.1'>
<headers xmlns='http://jabber.org/protocol/shim'>
<header name='Host'>b.com</header>
<header name='Content-Type'>text/html</header>
<header name='Content-Length'>...</header>
</headers>
<data>
<text>...</text>
</data>
</req>
</iq>
"""
name = 'request'
namespace = 'urn:xmpp:http'
interfaces = set(['method', 'resource', 'version'])
plugin_attrib = 'req'
def get_method(self):
return self._get_attr('method', None)
def set_method(self, method):
self._set_attr('method', method)
def get_resource(self):
return self._get_attr('resource', None)
def set_resource(self, resource):
self._set_attr('resource', resource)
def get_version(self):
return self._get_attr('version', None)
def set_version(self, version='1.1'):
self._set_attr('version', version)

View File

@ -0,0 +1,61 @@
"""
SleekXMPP: The Sleek XMPP Library
Implementation of HTTP over XMPP transport
http://xmpp.org/extensions/xep-0332.html
Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class Response(ElementBase):
"""
When the HTTP Server responds, it does so by sending an `iq` stanza
response (type=`result`) back to the client containing the `resp` element.
Since response are asynchronous, and since multiple requests may be active
at the same time, responses may be returned in a different order than the
in which the original requests were made.
Examples:
<iq type='result' from='httpserver@clayster.com' to='httpclient@clayster.com/browser' id='2'>
<resp xmlns='urn:xmpp:http' version='1.1' statusCode='200' statusMessage='OK'>
<headers xmlns='http://jabber.org/protocol/shim'>
<header name='Date'>Fri, 03 May 2013 16:39:54GMT-4</header>
<header name='Server'>Clayster</header>
<header name='Content-Type'>text/turtle</header>
<header name='Content-Length'>...</header>
<header name='Connection'>Close</header>
</headers>
<data>
<text>
...
</text>
</data>
</resp>
</iq>
"""
name = 'response'
namespace = 'urn:xmpp:http'
interfaces = set(['code', 'message', 'version'])
plugin_attrib = 'resp'
def get_code(self):
code = self._get_attr('statusCode', None)
return int(code) if code is not None else code
def set_code(self, code):
self._set_attr('statusCode', str(code))
def get_message(self):
return self._get_attr('statusMessage', '')
def set_message(self, message):
self._set_attr('statusMessage', message)
def set_version(self, version='1.1'):
self._set_attr('version', version)