# HG changeset patch # User Goffi # Date 1565949603 -7200 # Node ID a58610ab2983fdf0966903d0d016bb030e889f7a # Parent c56a728412f1b252bd38c83cd539a558d3159305 removed old code: - removed gateway which was an HTTP gateway inherited from Idavoll and which is not used is SàT Pubsub - removed memory_storage which has not been maintained since Idavoll and which is expected to be replaced by a sqlite-based backend - simplejson dependency is not used anymore diff -r c56a728412f1 -r a58610ab2983 sat_pubsub/gateway.py --- a/sat_pubsub/gateway.py Fri Aug 16 12:00:02 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,899 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- - -# Copyright (c) 2003-2011 Ralph Meijer -# Copyright (c) 2012-2019 Jérôme Poisson - - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -- - -# This program is based on Idavoll (http://idavoll.ik.nu/), -# originaly written by Ralph Meijer (http://ralphm.net/blog/) -# It is sublicensed under AGPL v3 (or any later version) as allowed by the original -# license. - -# -- - -# Here is a copy of the original license: - -# Copyright (c) 2003-2011 Ralph Meijer - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -""" -Web resources and client for interacting with pubsub services. -""" - -import mimetools -from time import gmtime, strftime -from StringIO import StringIO -import urllib -import urlparse - -import simplejson - -from twisted.application import service -from twisted.internet import defer, reactor -from twisted.python import log -from twisted.web import client, http, resource, server -from twisted.web.error import Error -from twisted.words.protocols.jabber.jid import JID -from twisted.words.protocols.jabber.error import StanzaError -from twisted.words.xish import domish - -from wokkel.generic import parseXml -from wokkel.pubsub import Item -from wokkel.pubsub import PubSubClient - -from sat_pubsub import error - -NS_ATOM = 'http://www.w3.org/2005/Atom' -MIME_ATOM_ENTRY = b'application/atom+xml;type=entry' -MIME_ATOM_FEED = b'application/atom+xml;type=feed' -MIME_JSON = b'application/json' - -class XMPPURIParseError(ValueError): - """ - Raised when a given XMPP URI couldn't be properly parsed. - """ - - - -def getServiceAndNode(uri): - """ - Given an XMPP URI, extract the publish subscribe service JID and node ID. - """ - - try: - scheme, rest = uri.split(':', 1) - except ValueError: - raise XMPPURIParseError("No URI scheme component") - - if scheme != 'xmpp': - raise XMPPURIParseError("Unknown URI scheme") - - if rest.startswith("//"): - raise XMPPURIParseError("Unexpected URI authority component") - - try: - entity, query = rest.split('?', 1) - except ValueError: - entity, query = rest, '' - - if not entity: - raise XMPPURIParseError("Empty URI path component") - - try: - service = JID(entity) - except Exception, e: - raise XMPPURIParseError("Invalid JID: %s" % e) - - params = urlparse.parse_qs(query) - - try: - nodeIdentifier = params['node'][0] - except (KeyError, ValueError): - nodeIdentifier = '' - - return service, nodeIdentifier - - - -def getXMPPURI(service, nodeIdentifier): - """ - Construct an XMPP URI from a service JID and node identifier. - """ - return "xmpp:%s?;node=%s" % (service.full(), nodeIdentifier or '') - - - -def _parseContentType(header): - """ - Parse a Content-Type header value to a L{mimetools.Message}. - - L{mimetools.Message} parses a Content-Type header and makes the - components available with its C{getmaintype}, C{getsubtype}, C{gettype}, - C{getplist} and C{getparam} methods. - """ - return mimetools.Message(StringIO(b'Content-Type: ' + header)) - - - -def _asyncResponse(render): - """ - """ - def wrapped(self, request): - def eb(failure): - if failure.check(Error): - err = failure.value - else: - log.err(failure) - err = Error(500) - request.setResponseCode(err.status, err.message) - return err.response - - def finish(result): - if result is server.NOT_DONE_YET: - return - - if result: - request.write(result) - request.finish() - - d = defer.maybeDeferred(render, self, request) - d.addErrback(eb) - d.addCallback(finish) - - return server.NOT_DONE_YET - - return wrapped - - - -class CreateResource(resource.Resource): - """ - A resource to create a publish-subscribe node. - """ - def __init__(self, backend, serviceJID, owner): - self.backend = backend - self.serviceJID = serviceJID - self.owner = owner - - - http_GET = None - - - @_asyncResponse - def render_POST(self, request): - """ - Respond to a POST request to create a new node. - """ - - def toResponse(nodeIdentifier): - uri = getXMPPURI(self.serviceJID, nodeIdentifier) - body = simplejson.dumps({'uri': uri}) - request.setHeader(b'Content-Type', MIME_JSON) - return body - - d = self.backend.createNode(None, self.owner) - d.addCallback(toResponse) - return d - - - -class DeleteResource(resource.Resource): - """ - A resource to create a publish-subscribe node. - """ - def __init__(self, backend, serviceJID, owner): - self.backend = backend - self.serviceJID = serviceJID - self.owner = owner - - - render_GET = None - - - @_asyncResponse - def render_POST(self, request): - """ - Respond to a POST request to create a new node. - """ - def toResponse(result): - request.setResponseCode(http.NO_CONTENT) - - def trapNotFound(failure): - failure.trap(error.NodeNotFound) - raise Error(http.NOT_FOUND, "Node not found") - - if not request.args.get('uri'): - raise Error(http.BAD_REQUEST, "No URI given") - - try: - jid, nodeIdentifier = getServiceAndNode(request.args['uri'][0]) - except XMPPURIParseError, e: - raise Error(http.BAD_REQUEST, "Malformed XMPP URI: %s" % e) - - - data = request.content.read() - if data: - params = simplejson.loads(data) - redirectURI = params.get('redirect_uri', None) - else: - redirectURI = None - - d = self.backend.deleteNode(nodeIdentifier, self.owner, - redirectURI) - d.addCallback(toResponse) - d.addErrback(trapNotFound) - return d - - - -class PublishResource(resource.Resource): - """ - A resource to publish to a publish-subscribe node. - """ - - def __init__(self, backend, serviceJID, owner): - self.backend = backend - self.serviceJID = serviceJID - self.owner = owner - - - render_GET = None - - - def checkMediaType(self, request): - ctype = request.getHeader(b'content-type') - - if not ctype: - request.setResponseCode(http.BAD_REQUEST) - - raise Error(http.BAD_REQUEST, b"No specified Media Type") - - message = _parseContentType(ctype) - if (message.maintype != b'application' or - message.subtype != b'atom+xml' or - message.getparam(b'type') != b'entry' or - (message.getparam(b'charset') or b'utf-8') != b'utf-8'): - raise Error(http.UNSUPPORTED_MEDIA_TYPE, - b"Unsupported Media Type: %s" % ctype) - - - @_asyncResponse - def render_POST(self, request): - """ - Respond to a POST request to create a new item. - """ - - def toResponse(nodeIdentifier): - uri = getXMPPURI(self.serviceJID, nodeIdentifier) - body = simplejson.dumps({'uri': uri}) - request.setHeader(b'Content-Type', MIME_JSON) - return body - - def gotNode(nodeIdentifier, payload): - item = Item(id='current', payload=payload) - d = self.backend.publish(nodeIdentifier, [item], self.owner) - d.addCallback(lambda _: nodeIdentifier) - return d - - def getNode(): - if request.args.get('uri'): - jid, nodeIdentifier = getServiceAndNode(request.args['uri'][0]) - return defer.succeed(nodeIdentifier) - else: - return self.backend.createNode(None, self.owner) - - def trapNotFound(failure): - failure.trap(error.NodeNotFound) - raise Error(http.NOT_FOUND, "Node not found") - - def trapXMPPURIParseError(failure): - failure.trap(XMPPURIParseError) - raise Error(http.BAD_REQUEST, - "Malformed XMPP URI: %s" % failure.value) - - self.checkMediaType(request) - payload = parseXml(request.content.read()) - d = getNode() - d.addCallback(gotNode, payload) - d.addCallback(toResponse) - d.addErrback(trapNotFound) - d.addErrback(trapXMPPURIParseError) - return d - - - -class ListResource(resource.Resource): - def __init__(self, service): - self.service = service - - - @_asyncResponse - def render_GET(self, request): - def responseFromNodes(nodeIdentifiers): - body = simplejson.dumps(nodeIdentifiers) - request.setHeader(b'Content-Type', MIME_JSON) - return body - - d = self.service.getNodes() - d.addCallback(responseFromNodes) - return d - - - -# Service for subscribing to remote XMPP Pubsub nodes and web resources - -def extractAtomEntries(items): - """ - Extract atom entries from a list of publish-subscribe items. - - @param items: List of L{domish.Element}s that represent publish-subscribe - items. - @type items: C{list} - """ - - atomEntries = [] - - for item in items: - # ignore non-items (i.e. retractions) - if item.name != 'item': - continue - - atomEntry = None - for element in item.elements(): - # extract the first element that is an atom entry - if element.uri == NS_ATOM and element.name == 'entry': - atomEntry = element - break - - if atomEntry: - atomEntries.append(atomEntry) - - return atomEntries - - - -def constructFeed(service, nodeIdentifier, entries, title): - nodeURI = getXMPPURI(service, nodeIdentifier) - now = strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) - - # Collect the received entries in a feed - feed = domish.Element((NS_ATOM, 'feed')) - feed.addElement('title', content=title) - feed.addElement('id', content=nodeURI) - feed.addElement('updated', content=now) - - for entry in entries: - feed.addChild(entry) - - return feed - - - -class RemoteSubscriptionService(service.Service, PubSubClient): - """ - Service for subscribing to remote XMPP Publish-Subscribe nodes. - - Subscriptions are created with a callback HTTP URI that is POSTed - to with the received items in notifications. - """ - - def __init__(self, jid, storage): - self.jid = jid - self.storage = storage - - - def trapNotFound(self, failure): - failure.trap(StanzaError) - - if failure.value.condition == 'item-not-found': - raise error.NodeNotFound() - else: - return failure - - - def subscribeCallback(self, jid, nodeIdentifier, callback): - """ - Subscribe a callback URI. - - This registers a callback URI to be called when a notification is - received for the given node. - - If this is the first callback registered for this node, the gateway - will subscribe to the node. Otherwise, the most recently published item - for this node is retrieved and, if present, the newly registered - callback will be called with that item. - """ - - def callbackForLastItem(items): - atomEntries = extractAtomEntries(items) - - if not atomEntries: - return - - self._postTo([callback], jid, nodeIdentifier, atomEntries[0], - MIME_ATOM_ENTRY) - - def subscribeOrItems(hasCallbacks): - if hasCallbacks: - if not nodeIdentifier: - return None - d = self.items(jid, nodeIdentifier, 1) - d.addCallback(callbackForLastItem) - else: - d = self.subscribe(jid, nodeIdentifier, self.jid) - - d.addErrback(self.trapNotFound) - return d - - d = self.storage.hasCallbacks(jid, nodeIdentifier) - d.addCallback(subscribeOrItems) - d.addCallback(lambda _: self.storage.addCallback(jid, nodeIdentifier, - callback)) - return d - - - def unsubscribeCallback(self, jid, nodeIdentifier, callback): - """ - Unsubscribe a callback. - - If this was the last registered callback for this node, the - gateway will unsubscribe from node. - """ - - def cb(last): - if last: - return self.unsubscribe(jid, nodeIdentifier, self.jid) - - d = self.storage.removeCallback(jid, nodeIdentifier, callback) - d.addCallback(cb) - return d - - - def itemsReceived(self, event): - """ - Fire up HTTP client to do callback - """ - - atomEntries = extractAtomEntries(event.items) - service = event.sender - nodeIdentifier = event.nodeIdentifier - headers = event.headers - - # Don't notify if there are no atom entries - if not atomEntries: - return - - if len(atomEntries) == 1: - contentType = MIME_ATOM_ENTRY - payload = atomEntries[0] - else: - contentType = MIME_ATOM_FEED - payload = constructFeed(service, nodeIdentifier, atomEntries, - title='Received item collection') - - self.callCallbacks(service, nodeIdentifier, payload, contentType) - - if 'Collection' in headers: - for collection in headers['Collection']: - nodeIdentifier = collection or '' - self.callCallbacks(service, nodeIdentifier, payload, - contentType) - - - def deleteReceived(self, event): - """ - Fire up HTTP client to do callback - """ - - service = event.sender - nodeIdentifier = event.nodeIdentifier - redirectURI = event.redirectURI - self.callCallbacks(service, nodeIdentifier, eventType='DELETED', - redirectURI=redirectURI) - - - def _postTo(self, callbacks, service, nodeIdentifier, - payload=None, contentType=None, eventType=None, - redirectURI=None): - - if not callbacks: - return - - postdata = None - nodeURI = getXMPPURI(service, nodeIdentifier) - headers = {'Referer': nodeURI.encode('utf-8'), - 'PubSub-Service': service.full().encode('utf-8')} - - if payload: - postdata = payload.toXml().encode('utf-8') - if contentType: - headers['Content-Type'] = "%s;charset=utf-8" % contentType - - if eventType: - headers['Event'] = eventType - - if redirectURI: - headers['Link'] = '<%s>; rel=alternate' % ( - redirectURI.encode('utf-8'), - ) - - def postNotification(callbackURI): - f = getPageWithFactory(str(callbackURI), - method='POST', - postdata=postdata, - headers=headers) - d = f.deferred - d.addErrback(log.err) - - for callbackURI in callbacks: - reactor.callLater(0, postNotification, callbackURI) - - - def callCallbacks(self, service, nodeIdentifier, - payload=None, contentType=None, eventType=None, - redirectURI=None): - - def eb(failure): - failure.trap(error.NoCallbacks) - - # No callbacks were registered for this node. Unsubscribe? - - d = self.storage.getCallbacks(service, nodeIdentifier) - d.addCallback(self._postTo, service, nodeIdentifier, payload, - contentType, eventType, redirectURI) - d.addErrback(eb) - d.addErrback(log.err) - - - -class RemoteSubscribeBaseResource(resource.Resource): - """ - Base resource for remote pubsub node subscription and unsubscription. - - This resource accepts POST request with a JSON document that holds - a dictionary with the keys C{uri} and C{callback} that respectively map - to the XMPP URI of the publish-subscribe node and the callback URI. - - This class should be inherited with L{serviceMethod} overridden. - - @cvar serviceMethod: The name of the method to be called with - the JID of the pubsub service, the node identifier - and the callback URI as received in the HTTP POST - request to this resource. - """ - serviceMethod = None - errorMap = { - error.NodeNotFound: - (http.FORBIDDEN, "Node not found"), - error.NotSubscribed: - (http.FORBIDDEN, "No such subscription found"), - error.SubscriptionExists: - (http.FORBIDDEN, "Subscription already exists"), - } - - def __init__(self, service): - self.service = service - self.params = None - - - render_GET = None - - - @_asyncResponse - def render_POST(self, request): - def trapNotFound(failure): - err = failure.trap(*self.errorMap.keys()) - status, message = self.errorMap[err] - raise Error(status, message) - - def toResponse(result): - request.setResponseCode(http.NO_CONTENT) - return b'' - - def trapXMPPURIParseError(failure): - failure.trap(XMPPURIParseError) - raise Error(http.BAD_REQUEST, - "Malformed XMPP URI: %s" % failure.value) - - data = request.content.read() - self.params = simplejson.loads(data) - - uri = self.params['uri'] - callback = self.params['callback'] - - jid, nodeIdentifier = getServiceAndNode(uri) - method = getattr(self.service, self.serviceMethod) - d = method(jid, nodeIdentifier, callback) - d.addCallback(toResponse) - d.addErrback(trapNotFound) - d.addErrback(trapXMPPURIParseError) - return d - - - -class RemoteSubscribeResource(RemoteSubscribeBaseResource): - """ - Resource to subscribe to a remote publish-subscribe node. - - The passed C{uri} is the XMPP URI of the node to subscribe to and the - C{callback} is the callback URI. Upon receiving notifications from the - node, a POST request will be perfomed on the callback URI. - """ - serviceMethod = 'subscribeCallback' - - - -class RemoteUnsubscribeResource(RemoteSubscribeBaseResource): - """ - Resource to unsubscribe from a remote publish-subscribe node. - - The passed C{uri} is the XMPP URI of the node to unsubscribe from and the - C{callback} is the callback URI that was registered for it. - """ - serviceMethod = 'unsubscribeCallback' - - - -class RemoteItemsResource(resource.Resource): - """ - Resource for retrieving items from a remote pubsub node. - """ - - def __init__(self, service): - self.service = service - - - @_asyncResponse - def render_GET(self, request): - try: - maxItems = int(request.args.get('max_items', [0])[0]) or None - except ValueError: - raise Error(http.BAD_REQUEST, - "The argument max_items has an invalid value.") - - try: - uri = request.args['uri'][0] - except KeyError: - raise Error(http.BAD_REQUEST, - "No URI for the remote node provided.") - - try: - jid, nodeIdentifier = getServiceAndNode(uri) - except XMPPURIParseError: - raise Error(http.BAD_REQUEST, - "Malformed XMPP URI: %s" % uri) - - def toResponse(items): - """ - Create a feed out the retrieved items. - """ - atomEntries = extractAtomEntries(items) - feed = constructFeed(jid, nodeIdentifier, atomEntries, - "Retrieved item collection") - body = feed.toXml().encode('utf-8') - request.setHeader(b'Content-Type', MIME_ATOM_FEED) - return body - - def trapNotFound(failure): - failure.trap(StanzaError) - if not failure.value.condition == 'item-not-found': - raise failure - raise Error(http.NOT_FOUND, "Node not found") - - d = self.service.items(jid, nodeIdentifier, maxItems) - d.addCallback(toResponse) - d.addErrback(trapNotFound) - return d - - - -# Client side code to interact with a service as provided above - -def getPageWithFactory(url, contextFactory=None, *args, **kwargs): - """Download a web page. - - Download a page. Return the factory that holds a deferred, which will - callback with a page (as a string) or errback with a description of the - error. - - See HTTPClientFactory to see what extra args can be passed. - """ - - factory = client.HTTPClientFactory(url, *args, **kwargs) - factory.protocol.handleStatus_204 = lambda self: self.handleStatus_200() - - if factory.scheme == 'https': - from twisted.internet import ssl - if contextFactory is None: - contextFactory = ssl.ClientContextFactory() - reactor.connectSSL(factory.host, factory.port, factory, contextFactory) - else: - reactor.connectTCP(factory.host, factory.port, factory) - return factory - - - -class CallbackResource(resource.Resource): - """ - Web resource for retrieving gateway notifications. - """ - - def __init__(self, callback): - self.callback = callback - - - http_GET = None - - - def render_POST(self, request): - if request.requestHeaders.hasHeader(b'Event'): - payload = None - else: - payload = parseXml(request.content.read()) - - self.callback(payload, request.requestHeaders) - - request.setResponseCode(http.NO_CONTENT) - return b'' - - - - -class GatewayClient(service.Service): - """ - Service that provides client access to the HTTP Gateway into Idavoll. - """ - - agent = "Idavoll HTTP Gateway Client" - - def __init__(self, baseURI, callbackHost=None, callbackPort=None): - self.baseURI = baseURI - self.callbackHost = callbackHost or 'localhost' - self.callbackPort = callbackPort or 8087 - root = resource.Resource() - root.putChild('callback', CallbackResource( - lambda *args, **kwargs: self.callback(*args, **kwargs))) - self.site = server.Site(root) - - - def startService(self): - self.port = reactor.listenTCP(self.callbackPort, - self.site) - - - def stopService(self): - return self.port.stopListening() - - - def _makeURI(self, verb, query=None): - uriComponents = urlparse.urlparse(self.baseURI) - uri = urlparse.urlunparse((uriComponents[0], - uriComponents[1], - uriComponents[2] + verb, - '', - query and urllib.urlencode(query) or '', - '')) - return uri - - - def callback(self, data, headers): - pass - - - def ping(self): - f = getPageWithFactory(self._makeURI(''), - method='HEAD', - agent=self.agent) - return f.deferred - - - def create(self): - f = getPageWithFactory(self._makeURI('create'), - method='POST', - agent=self.agent) - return f.deferred.addCallback(simplejson.loads) - - - def delete(self, xmppURI, redirectURI=None): - query = {'uri': xmppURI} - - if redirectURI: - params = {'redirect_uri': redirectURI} - postdata = simplejson.dumps(params) - headers = {'Content-Type': MIME_JSON} - else: - postdata = None - headers = None - - f = getPageWithFactory(self._makeURI('delete', query), - method='POST', - postdata=postdata, - headers=headers, - agent=self.agent) - return f.deferred - - - def publish(self, entry, xmppURI=None): - query = xmppURI and {'uri': xmppURI} - - f = getPageWithFactory(self._makeURI('publish', query), - method='POST', - postdata=entry.toXml().encode('utf-8'), - headers={'Content-Type': MIME_ATOM_ENTRY}, - agent=self.agent) - return f.deferred.addCallback(simplejson.loads) - - - def listNodes(self): - f = getPageWithFactory(self._makeURI('list'), - method='GET', - agent=self.agent) - return f.deferred.addCallback(simplejson.loads) - - - def subscribe(self, xmppURI): - params = {'uri': xmppURI, - 'callback': 'http://%s:%s/callback' % (self.callbackHost, - self.callbackPort)} - f = getPageWithFactory(self._makeURI('subscribe'), - method='POST', - postdata=simplejson.dumps(params), - headers={'Content-Type': MIME_JSON}, - agent=self.agent) - return f.deferred - - - def unsubscribe(self, xmppURI): - params = {'uri': xmppURI, - 'callback': 'http://%s:%s/callback' % (self.callbackHost, - self.callbackPort)} - f = getPageWithFactory(self._makeURI('unsubscribe'), - method='POST', - postdata=simplejson.dumps(params), - headers={'Content-Type': MIME_JSON}, - agent=self.agent) - return f.deferred - - - def items(self, xmppURI, maxItems=None): - query = {'uri': xmppURI} - if maxItems is not None: - query['max_items'] = int(maxItems) - f = getPageWithFactory(self._makeURI('items', query), - method='GET', - agent=self.agent) - return f.deferred diff -r c56a728412f1 -r a58610ab2983 sat_pubsub/memory_storage.py --- a/sat_pubsub/memory_storage.py Fri Aug 16 12:00:02 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,380 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- - -# Copyright (c) 2003-2011 Ralph Meijer -# Copyright (c) 2012-2019 Jérôme Poisson - - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -- - -# This program is based on Idavoll (http://idavoll.ik.nu/), -# originaly written by Ralph Meijer (http://ralphm.net/blog/) -# It is sublicensed under AGPL v3 (or any later version) as allowed by the original -# license. - -# -- - -# Here is a copy of the original license: - -# Copyright (c) 2003-2011 Ralph Meijer - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -import copy -from zope.interface import implements -from twisted.internet import defer -from twisted.words.protocols.jabber import jid - -from wokkel.pubsub import Subscription - -from sat_pubsub import error, iidavoll - -class Storage: - - implements(iidavoll.IStorage) - - defaultConfig = { - 'leaf': { - "pubsub#persist_items": True, - "pubsub#deliver_payloads": True, - "pubsub#send_last_published_item": 'on_sub', - }, - 'collection': { - "pubsub#deliver_payloads": True, - "pubsub#send_last_published_item": 'on_sub', - } - } - - def __init__(self): - rootNode = CollectionNode('', jid.JID('localhost'), - copy.copy(self.defaultConfig['collection'])) - self._nodes = {'': rootNode} - - - def getNode(self, nodeIdentifier): - try: - node = self._nodes[nodeIdentifier] - except KeyError: - return defer.fail(error.NodeNotFound()) - - return defer.succeed(node) - - - def getNodeIds(self): - return defer.succeed(self._nodes.keys()) - - - def createNode(self, nodeIdentifier, owner, config): - if nodeIdentifier in self._nodes: - return defer.fail(error.NodeExists()) - - if config['pubsub#node_type'] != 'leaf': - raise error.NoCollections() - - node = LeafNode(nodeIdentifier, owner, config) - self._nodes[nodeIdentifier] = node - - return defer.succeed(None) - - - def deleteNode(self, nodeIdentifier): - try: - del self._nodes[nodeIdentifier] - except KeyError: - return defer.fail(error.NodeNotFound()) - - return defer.succeed(None) - - - def getAffiliations(self, entity): - entity = entity.userhost() - return defer.succeed([(node.nodeIdentifier, node._affiliations[entity]) - for name, node in self._nodes.iteritems() - if entity in node._affiliations]) - - - def getSubscriptions(self, entity): - subscriptions = [] - for node in self._nodes.itervalues(): - for subscriber, subscription in node._subscriptions.iteritems(): - subscriber = jid.internJID(subscriber) - if subscriber.userhostJID() == entity.userhostJID(): - subscriptions.append(subscription) - - return defer.succeed(subscriptions) - - - def getDefaultConfiguration(self, nodeType): - if nodeType == 'collection': - raise error.NoCollections() - - return self.defaultConfig[nodeType] - - -class Node: - - implements(iidavoll.INode) - - def __init__(self, nodeIdentifier, owner, config): - self.nodeIdentifier = nodeIdentifier - self._affiliations = {owner.userhost(): 'owner'} - self._subscriptions = {} - self._config = copy.copy(config) - - - def getType(self): - return self.nodeType - - - def getConfiguration(self): - return self._config - - - def getMetaData(self): - config = copy.copy(self._config) - config["pubsub#node_type"] = self.nodeType - return config - - - def setConfiguration(self, options): - for option in options: - if option in self._config: - self._config[option] = options[option] - - return defer.succeed(None) - - - def getAffiliation(self, entity): - return defer.succeed(self._affiliations.get(entity.userhost())) - - - def getSubscription(self, subscriber): - try: - subscription = self._subscriptions[subscriber.full()] - except KeyError: - return defer.succeed(None) - else: - return defer.succeed(subscription) - - - def getSubscriptions(self, state=None): - return defer.succeed( - [subscription - for subscription in self._subscriptions.itervalues() - if state is None or subscription.state == state]) - - - - def addSubscription(self, subscriber, state, options): - if self._subscriptions.get(subscriber.full()): - return defer.fail(error.SubscriptionExists()) - - subscription = Subscription(self.nodeIdentifier, subscriber, state, - options) - self._subscriptions[subscriber.full()] = subscription - return defer.succeed(None) - - - def removeSubscription(self, subscriber): - try: - del self._subscriptions[subscriber.full()] - except KeyError: - return defer.fail(error.NotSubscribed()) - - return defer.succeed(None) - - - def isSubscribed(self, entity): - for subscriber, subscription in self._subscriptions.iteritems(): - if jid.internJID(subscriber).userhost() == entity.userhost() and \ - subscription.state == 'subscribed': - return defer.succeed(True) - - return defer.succeed(False) - - - def getAffiliations(self): - affiliations = [(jid.internJID(entity), affiliation) for entity, affiliation - in self._affiliations.iteritems()] - - return defer.succeed(affiliations) - - - -class PublishedItem(object): - """ - A published item. - - This represent an item as it was published by an entity. - - @ivar element: The DOM representation of the item that was published. - @type element: L{Element} - @ivar publisher: The entity that published the item. - @type publisher: L{JID} - """ - - def __init__(self, element, publisher): - self.element = element - self.publisher = publisher - - - -class LeafNode(Node): - - implements(iidavoll.ILeafNode) - - nodeType = 'leaf' - - def __init__(self, nodeIdentifier, owner, config): - Node.__init__(self, nodeIdentifier, owner, config) - self._items = {} - self._itemlist = [] - - - def storeItems(self, item_data, publisher): - for access_model, item_config, element in item_data: - item = PublishedItem(element, publisher) - itemIdentifier = element["id"] - if itemIdentifier in self._items: - self._itemlist.remove(self._items[itemIdentifier]) - self._items[itemIdentifier] = item - self._itemlist.append(item) - - return defer.succeed(None) - - - def removeItems(self, itemIdentifiers): - deleted = [] - - for itemIdentifier in itemIdentifiers: - try: - item = self._items[itemIdentifier] - except KeyError: - pass - else: - self._itemlist.remove(item) - del self._items[itemIdentifier] - deleted.append(itemIdentifier) - - return defer.succeed(deleted) - - - def getItems(self, authorized_groups, unrestricted, maxItems=None): - if maxItems is not None: - itemList = self._itemlist[-maxItems:] - else: - itemList = self._itemlist - return defer.succeed([item.element for item in itemList]) - - - def getItemsById(self, authorized_groups, unrestricted, itemIdentifiers): - items = [] - for itemIdentifier in itemIdentifiers: - try: - item = self._items[itemIdentifier] - except KeyError: - pass - else: - items.append(item.element) - return defer.succeed(items) - - - def purge(self): - self._items = {} - self._itemlist = [] - - return defer.succeed(None) - - - def filterItemsWithPublisher(self, itemIdentifiers, requestor): - filteredItems = [] - for itemIdentifier in itemIdentifiers: - try: - if self._items[itemIdentifier].publisher.userhost() == requestor.userhost(): - filteredItems.append(self.items[itemIdentifier]) - except KeyError, AttributeError: - pass - return defer.succeed(filteredItems) - - -class CollectionNode(Node): - nodeType = 'collection' - - - -class GatewayStorage(object): - """ - Memory based storage facility for the XMPP-HTTP gateway. - """ - - def __init__(self): - self.callbacks = {} - - - def addCallback(self, service, nodeIdentifier, callback): - try: - callbacks = self.callbacks[service, nodeIdentifier] - except KeyError: - callbacks = {callback} - self.callbacks[service, nodeIdentifier] = callbacks - else: - callbacks.add(callback) - pass - - return defer.succeed(None) - - - def removeCallback(self, service, nodeIdentifier, callback): - try: - callbacks = self.callbacks[service, nodeIdentifier] - callbacks.remove(callback) - except KeyError: - return defer.fail(error.NotSubscribed()) - else: - if not callbacks: - del self.callbacks[service, nodeIdentifier] - - return defer.succeed(not callbacks) - - - def getCallbacks(self, service, nodeIdentifier): - try: - callbacks = self.callbacks[service, nodeIdentifier] - except KeyError: - return defer.fail(error.NoCallbacks()) - else: - return defer.succeed(callbacks) - - - def hasCallbacks(self, service, nodeIdentifier): - return defer.succeed((service, nodeIdentifier) in self.callbacks) diff -r c56a728412f1 -r a58610ab2983 sat_pubsub/pgsql_storage.py --- a/sat_pubsub/pgsql_storage.py Fri Aug 16 12:00:02 2019 +0200 +++ b/sat_pubsub/pgsql_storage.py Fri Aug 16 12:00:03 2019 +0200 @@ -1300,80 +1300,3 @@ class CollectionNode(Node): nodeType = 'collection' - - - -class GatewayStorage(object): - """ - Memory based storage facility for the XMPP-HTTP gateway. - """ - - def __init__(self, dbpool): - self.dbpool = dbpool - - def _countCallbacks(self, cursor, service, nodeIdentifier): - """ - Count number of callbacks registered for a node. - """ - cursor.execute("""SELECT count(*) FROM callbacks - WHERE service=%s and node=%s""", - (service.full(), - nodeIdentifier)) - results = cursor.fetchall() - return results[0][0] - - def addCallback(self, service, nodeIdentifier, callback): - def interaction(cursor): - cursor.execute("""SELECT 1 as bool FROM callbacks - WHERE service=%s and node=%s and uri=%s""", - (service.full(), - nodeIdentifier, - callback)) - if cursor.fetchall(): - return - - cursor.execute("""INSERT INTO callbacks - (service, node, uri) VALUES - (%s, %s, %s)""", - (service.full(), - nodeIdentifier, - callback)) - - return self.dbpool.runInteraction(interaction) - - def removeCallback(self, service, nodeIdentifier, callback): - def interaction(cursor): - cursor.execute("""DELETE FROM callbacks - WHERE service=%s and node=%s and uri=%s""", - (service.full(), - nodeIdentifier, - callback)) - - if cursor.rowcount != 1: - raise error.NotSubscribed() - - last = not self._countCallbacks(cursor, service, nodeIdentifier) - return last - - return self.dbpool.runInteraction(interaction) - - def getCallbacks(self, service, nodeIdentifier): - def interaction(cursor): - cursor.execute("""SELECT uri FROM callbacks - WHERE service=%s and node=%s""", - (service.full(), - nodeIdentifier)) - results = cursor.fetchall() - - if not results: - raise error.NoCallbacks() - - return [result[0] for result in results] - - return self.dbpool.runInteraction(interaction) - - def hasCallbacks(self, service, nodeIdentifier): - def interaction(cursor): - return bool(self._countCallbacks(cursor, service, nodeIdentifier)) - - return self.dbpool.runInteraction(interaction) diff -r c56a728412f1 -r a58610ab2983 sat_pubsub/test/test_gateway.py --- a/sat_pubsub/test/test_gateway.py Fri Aug 16 12:00:02 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,822 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- - -# Copyright (c) 2003-2011 Ralph Meijer -# Copyright (c) 2012-2019 Jérôme Poisson - - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. - -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -- - -# This program is based on Idavoll (http://idavoll.ik.nu/), -# originaly written by Ralph Meijer (http://ralphm.net/blog/) -# It is sublicensed under AGPL v3 (or any later version) as allowed by the original -# license. - -# -- - -# Here is a copy of the original license: - -# Copyright (c) 2003-2011 Ralph Meijer - -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: - -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -""" -Tests for L{idavoll.gateway}. - -Note that some tests are functional tests that require a running idavoll -service. -""" - -from StringIO import StringIO - -import simplejson - -from twisted.internet import defer -from twisted.trial import unittest -from twisted.web import error, http, http_headers, server -from twisted.web.test import requesthelper -from twisted.words.xish import domish -from twisted.words.protocols.jabber.jid import JID - -from sat_pubsub import gateway -from sat_pubsub.backend import BackendService -from sat_pubsub.memory_storage import Storage - -AGENT = "Idavoll Test Script" -NS_ATOM = "http://www.w3.org/2005/Atom" - -TEST_ENTRY = domish.Element((NS_ATOM, 'entry')) -TEST_ENTRY.addElement("id", - content="urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a") -TEST_ENTRY.addElement("title", content="Atom-Powered Robots Run Amok") -TEST_ENTRY.addElement("author").addElement("name", content="John Doe") -TEST_ENTRY.addElement("content", content="Some text.") - -baseURI = "http://localhost:8086/" -component = "pubsub" -componentJID = JID(component) -ownerJID = JID('owner@example.org') - -def _render(resource, request): - result = resource.render(request) - if isinstance(result, str): - request.write(result) - request.finish() - return defer.succeed(None) - elif result is server.NOT_DONE_YET: - if request.finished: - return defer.succeed(None) - else: - return request.notifyFinish() - else: - raise ValueError("Unexpected return value: %r" % (result,)) - - -class DummyRequest(requesthelper.DummyRequest): - - def __init__(self, *args, **kwargs): - requesthelper.DummyRequest.__init__(self, *args, **kwargs) - self.requestHeaders = http_headers.Headers() - - - -class GetServiceAndNodeTest(unittest.TestCase): - """ - Tests for {gateway.getServiceAndNode}. - """ - - def test_basic(self): - """ - getServiceAndNode parses an XMPP URI with node parameter. - """ - uri = b'xmpp:pubsub.example.org?;node=test' - service, nodeIdentifier = gateway.getServiceAndNode(uri) - self.assertEqual(JID(u'pubsub.example.org'), service) - self.assertEqual(u'test', nodeIdentifier) - - - def test_schemeEmpty(self): - """ - If the URI scheme is empty, an exception is raised. - """ - uri = b'pubsub.example.org' - self.assertRaises(gateway.XMPPURIParseError, - gateway.getServiceAndNode, uri) - - - def test_schemeNotXMPP(self): - """ - If the URI scheme is not 'xmpp', an exception is raised. - """ - uri = b'mailto:test@example.org' - self.assertRaises(gateway.XMPPURIParseError, - gateway.getServiceAndNode, uri) - - - def test_authorityPresent(self): - """ - If the URI has an authority component, an exception is raised. - """ - uri = b'xmpp://pubsub.example.org/' - self.assertRaises(gateway.XMPPURIParseError, - gateway.getServiceAndNode, uri) - - - def test_queryEmpty(self): - """ - If there is no query component, the nodeIdentifier is empty. - """ - uri = b'xmpp:pubsub.example.org' - service, nodeIdentifier = gateway.getServiceAndNode(uri) - - self.assertEqual(JID(u'pubsub.example.org'), service) - self.assertEqual(u'', nodeIdentifier) - - - def test_jidInvalid(self): - """ - If the JID from the path component is invalid, an exception is raised. - """ - uri = b'xmpp:@@pubsub.example.org?;node=test' - self.assertRaises(gateway.XMPPURIParseError, - gateway.getServiceAndNode, uri) - - - def test_pathEmpty(self): - """ - If there is no path component, an exception is raised. - """ - uri = b'xmpp:?node=test' - self.assertRaises(gateway.XMPPURIParseError, - gateway.getServiceAndNode, uri) - - - def test_nodeAbsent(self): - """ - If the node parameter is missing, the nodeIdentifier is empty. - """ - uri = b'xmpp:pubsub.example.org?' - service, nodeIdentifier = gateway.getServiceAndNode(uri) - - self.assertEqual(JID(u'pubsub.example.org'), service) - self.assertEqual(u'', nodeIdentifier) - - - -class GetXMPPURITest(unittest.TestCase): - """ - Tests for L{gateway.getXMPPURITest}. - """ - - def test_basic(self): - uri = gateway.getXMPPURI(JID(u'pubsub.example.org'), u'test') - self.assertEqual('xmpp:pubsub.example.org?;node=test', uri) - - -class CreateResourceTest(unittest.TestCase): - """ - Tests for L{gateway.CreateResource}. - """ - - def setUp(self): - self.backend = BackendService(Storage()) - self.resource = gateway.CreateResource(self.backend, componentJID, - ownerJID) - - - def test_get(self): - """ - The method GET is not supported. - """ - request = DummyRequest([b'']) - self.assertRaises(error.UnsupportedMethod, - _render, self.resource, request) - - - def test_post(self): - """ - Upon a POST, a new node is created and the URI returned. - """ - request = DummyRequest([b'']) - request.method = 'POST' - - def gotNodes(nodeIdentifiers, uri): - service, nodeIdentifier = gateway.getServiceAndNode(uri) - self.assertIn(nodeIdentifier, nodeIdentifiers) - - def rendered(result): - self.assertEqual('application/json', - request.outgoingHeaders['content-type']) - payload = simplejson.loads(b''.join(request.written)) - self.assertIn('uri', payload) - d = self.backend.getNodes() - d.addCallback(gotNodes, payload['uri']) - return d - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - -class DeleteResourceTest(unittest.TestCase): - """ - Tests for L{gateway.DeleteResource}. - """ - - def setUp(self): - self.backend = BackendService(Storage()) - self.resource = gateway.DeleteResource(self.backend, componentJID, - ownerJID) - - - def test_get(self): - """ - The method GET is not supported. - """ - request = DummyRequest([b'']) - self.assertRaises(error.UnsupportedMethod, - _render, self.resource, request) - - - def test_post(self): - """ - Upon a POST, a new node is created and the URI returned. - """ - request = DummyRequest([b'']) - request.method = b'POST' - - def rendered(result): - self.assertEqual(http.NO_CONTENT, request.responseCode) - - def nodeCreated(nodeIdentifier): - uri = gateway.getXMPPURI(componentJID, nodeIdentifier) - request.args[b'uri'] = [uri] - request.content = StringIO(b'') - - return _render(self.resource, request) - - d = self.backend.createNode(u'test', ownerJID) - d.addCallback(nodeCreated) - d.addCallback(rendered) - return d - - - def test_postWithRedirect(self): - """ - Upon a POST, a new node is created and the URI returned. - """ - request = DummyRequest([b'']) - request.method = b'POST' - otherNodeURI = b'xmpp:pubsub.example.org?node=other' - - def rendered(result): - self.assertEqual(http.NO_CONTENT, request.responseCode) - self.assertEqual(1, len(deletes)) - nodeIdentifier, owner, redirectURI = deletes[-1] - self.assertEqual(otherNodeURI, redirectURI) - - def nodeCreated(nodeIdentifier): - uri = gateway.getXMPPURI(componentJID, nodeIdentifier) - request.args[b'uri'] = [uri] - payload = {b'redirect_uri': otherNodeURI} - body = simplejson.dumps(payload) - request.content = StringIO(body) - return _render(self.resource, request) - - def deleteNode(nodeIdentifier, owner, redirectURI): - deletes.append((nodeIdentifier, owner, redirectURI)) - return defer.succeed(nodeIdentifier) - - deletes = [] - self.patch(self.backend, 'deleteNode', deleteNode) - d = self.backend.createNode(u'test', ownerJID) - d.addCallback(nodeCreated) - d.addCallback(rendered) - return d - - - def test_postUnknownNode(self): - """ - If the node to be deleted is unknown, 404 Not Found is returned. - """ - request = DummyRequest([b'']) - request.method = b'POST' - - def rendered(result): - self.assertEqual(http.NOT_FOUND, request.responseCode) - - uri = gateway.getXMPPURI(componentJID, u'unknown') - request.args[b'uri'] = [uri] - request.content = StringIO(b'') - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - def test_postMalformedXMPPURI(self): - """ - If the XMPP URI is malformed, Bad Request is returned. - """ - request = DummyRequest([b'']) - request.method = b'POST' - - def rendered(result): - self.assertEqual(http.BAD_REQUEST, request.responseCode) - - uri = 'xmpp:@@@@' - request.args[b'uri'] = [uri] - request.content = StringIO(b'') - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - def test_postURIMissing(self): - """ - If no URI is passed, 400 Bad Request is returned. - """ - request = DummyRequest([b'']) - request.method = b'POST' - - def rendered(result): - self.assertEqual(http.BAD_REQUEST, request.responseCode) - - request.content = StringIO(b'') - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - -class CallbackResourceTest(unittest.TestCase): - """ - Tests for L{gateway.CallbackResource}. - """ - - def setUp(self): - self.callbackEvents = [] - self.resource = gateway.CallbackResource(self._callback) - - - def _callback(self, payload, headers): - self.callbackEvents.append((payload, headers)) - - - def test_get(self): - """ - The method GET is not supported. - """ - request = DummyRequest([b'']) - self.assertRaises(error.UnsupportedMethod, - _render, self.resource, request) - - - def test_post(self): - """ - The body posted is passed to the callback. - """ - request = DummyRequest([b'']) - request.method = 'POST' - request.content = StringIO(b'') - - def rendered(result): - self.assertEqual(1, len(self.callbackEvents)) - payload, headers = self.callbackEvents[-1] - self.assertEqual('root', payload.name) - - self.assertEqual(http.NO_CONTENT, request.responseCode) - self.assertFalse(b''.join(request.written)) - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - def test_postEvent(self): - """ - If the Event header is set, the payload is empty and the header passed. - """ - request = DummyRequest([b'']) - request.method = 'POST' - request.requestHeaders.addRawHeader(b'Event', b'DELETE') - request.content = StringIO(b'') - - def rendered(result): - self.assertEqual(1, len(self.callbackEvents)) - payload, headers = self.callbackEvents[-1] - self.assertIdentical(None, payload) - self.assertEqual(['DELETE'], headers.getRawHeaders(b'Event')) - self.assertFalse(b''.join(request.written)) - - d = _render(self.resource, request) - d.addCallback(rendered) - return d - - - -class GatewayTest(unittest.TestCase): - timeout = 2 - - def setUp(self): - self.client = gateway.GatewayClient(baseURI) - self.client.startService() - self.addCleanup(self.client.stopService) - - def trapConnectionRefused(failure): - from twisted.internet.error import ConnectionRefusedError - failure.trap(ConnectionRefusedError) - raise unittest.SkipTest("Gateway to test against is not available") - - def trapNotFound(failure): - from twisted.web.error import Error - failure.trap(Error) - - d = self.client.ping() - d.addErrback(trapConnectionRefused) - d.addErrback(trapNotFound) - return d - - - def tearDown(self): - return self.client.stopService() - - - def test_create(self): - - def cb(response): - self.assertIn('uri', response) - - d = self.client.create() - d.addCallback(cb) - return d - - def test_publish(self): - - def cb(response): - self.assertIn('uri', response) - - d = self.client.publish(TEST_ENTRY) - d.addCallback(cb) - return d - - def test_publishExistingNode(self): - - def cb2(response, xmppURI): - self.assertEquals(xmppURI, response['uri']) - - def cb1(response): - xmppURI = response['uri'] - d = self.client.publish(TEST_ENTRY, xmppURI) - d.addCallback(cb2, xmppURI) - return d - - d = self.client.create() - d.addCallback(cb1) - return d - - def test_publishNonExisting(self): - def cb(err): - self.assertEqual('404', err.status) - - d = self.client.publish(TEST_ENTRY, 'xmpp:%s?node=test' % component) - self.assertFailure(d, error.Error) - d.addCallback(cb) - return d - - def test_delete(self): - def cb(response): - xmppURI = response['uri'] - d = self.client.delete(xmppURI) - return d - - d = self.client.create() - d.addCallback(cb) - return d - - def test_deleteWithRedirect(self): - def cb(response): - xmppURI = response['uri'] - redirectURI = 'xmpp:%s?node=test' % component - d = self.client.delete(xmppURI, redirectURI) - return d - - d = self.client.create() - d.addCallback(cb) - return d - - def test_deleteNotification(self): - def onNotification(data, headers): - try: - self.assertTrue(headers.hasHeader('Event')) - self.assertEquals(['DELETED'], headers.getRawHeaders('Event')) - self.assertFalse(headers.hasHeader('Link')) - except: - self.client.deferred.errback() - else: - self.client.deferred.callback(None) - - def cb(response): - xmppURI = response['uri'] - d = self.client.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = self.client.delete(xmppURI) - return d - - self.client.callback = onNotification - self.client.deferred = defer.Deferred() - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - return defer.gatherResults([d, self.client.deferred]) - - def test_deleteNotificationWithRedirect(self): - redirectURI = 'xmpp:%s?node=test' % component - - def onNotification(data, headers): - try: - self.assertTrue(headers.hasHeader('Event')) - self.assertEquals(['DELETED'], headers.getRawHeaders('Event')) - self.assertEquals(['<%s>; rel=alternate' % redirectURI], - headers.getRawHeaders('Link')) - except: - self.client.deferred.errback() - else: - self.client.deferred.callback(None) - - def cb(response): - xmppURI = response['uri'] - d = self.client.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = self.client.delete(xmppURI, redirectURI) - return d - - self.client.callback = onNotification - self.client.deferred = defer.Deferred() - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - return defer.gatherResults([d, self.client.deferred]) - - def test_list(self): - d = self.client.listNodes() - return d - - def test_subscribe(self): - def cb(response): - xmppURI = response['uri'] - d = self.client.subscribe(xmppURI) - return d - - d = self.client.create() - d.addCallback(cb) - return d - - def test_subscribeGetNotification(self): - - def onNotification(data, headers): - self.client.deferred.callback(None) - - def cb(response): - xmppURI = response['uri'] - d = self.client.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = self.client.publish(TEST_ENTRY, xmppURI) - return d - - - self.client.callback = onNotification - self.client.deferred = defer.Deferred() - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - return defer.gatherResults([d, self.client.deferred]) - - - def test_subscribeTwiceGetNotification(self): - - def onNotification1(data, headers): - d = client1.stopService() - d.chainDeferred(client1.deferred) - - def onNotification2(data, headers): - d = client2.stopService() - d.chainDeferred(client2.deferred) - - def cb(response): - xmppURI = response['uri'] - d = client1.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = client2.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb3(xmppURI): - d = self.client.publish(TEST_ENTRY, xmppURI) - return d - - - client1 = gateway.GatewayClient(baseURI, callbackPort=8088) - client1.startService() - client1.callback = onNotification1 - client1.deferred = defer.Deferred() - client2 = gateway.GatewayClient(baseURI, callbackPort=8089) - client2.startService() - client2.callback = onNotification2 - client2.deferred = defer.Deferred() - - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - d.addCallback(cb3) - dl = defer.gatherResults([d, client1.deferred, client2.deferred]) - return dl - - - def test_subscribeGetDelayedNotification(self): - - def onNotification(data, headers): - self.client.deferred.callback(None) - - def cb(response): - xmppURI = response['uri'] - self.assertNot(self.client.deferred.called) - d = self.client.publish(TEST_ENTRY, xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = self.client.subscribe(xmppURI) - return d - - - self.client.callback = onNotification - self.client.deferred = defer.Deferred() - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - return defer.gatherResults([d, self.client.deferred]) - - def test_subscribeGetDelayedNotification2(self): - """ - Test that subscribing as second results in a notification being sent. - """ - - def onNotification1(data, headers): - client1.deferred.callback(None) - client1.stopService() - - def onNotification2(data, headers): - client2.deferred.callback(None) - client2.stopService() - - def cb(response): - xmppURI = response['uri'] - self.assertNot(client1.deferred.called) - self.assertNot(client2.deferred.called) - d = self.client.publish(TEST_ENTRY, xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - d = client1.subscribe(xmppURI) - d.addCallback(lambda _: xmppURI) - return d - - def cb3(xmppURI): - d = client2.subscribe(xmppURI) - return d - - client1 = gateway.GatewayClient(baseURI, callbackPort=8088) - client1.startService() - client1.callback = onNotification1 - client1.deferred = defer.Deferred() - client2 = gateway.GatewayClient(baseURI, callbackPort=8089) - client2.startService() - client2.callback = onNotification2 - client2.deferred = defer.Deferred() - - - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - d.addCallback(cb3) - dl = defer.gatherResults([d, client1.deferred, client2.deferred]) - return dl - - - def test_subscribeNonExisting(self): - def cb(err): - self.assertEqual('403', err.status) - - d = self.client.subscribe('xmpp:%s?node=test' % component) - self.assertFailure(d, error.Error) - d.addCallback(cb) - return d - - - def test_subscribeRootGetNotification(self): - - def clean(rootNode): - return self.client.unsubscribe(rootNode) - - def onNotification(data, headers): - self.client.deferred.callback(None) - - def cb(response): - xmppURI = response['uri'] - jid, nodeIdentifier = gateway.getServiceAndNode(xmppURI) - rootNode = gateway.getXMPPURI(jid, '') - - d = self.client.subscribe(rootNode) - d.addCallback(lambda _: self.addCleanup(clean, rootNode)) - d.addCallback(lambda _: xmppURI) - return d - - def cb2(xmppURI): - return self.client.publish(TEST_ENTRY, xmppURI) - - - self.client.callback = onNotification - self.client.deferred = defer.Deferred() - d = self.client.create() - d.addCallback(cb) - d.addCallback(cb2) - return defer.gatherResults([d, self.client.deferred]) - - - def test_unsubscribeNonExisting(self): - def cb(err): - self.assertEqual('403', err.status) - - d = self.client.unsubscribe('xmpp:%s?node=test' % component) - self.assertFailure(d, error.Error) - d.addCallback(cb) - return d - - - def test_items(self): - def cb(response): - xmppURI = response['uri'] - d = self.client.items(xmppURI) - return d - - d = self.client.publish(TEST_ENTRY) - d.addCallback(cb) - return d - - - def test_itemsMaxItems(self): - def cb(response): - xmppURI = response['uri'] - d = self.client.items(xmppURI, 2) - return d - - d = self.client.publish(TEST_ENTRY) - d.addCallback(cb) - return d diff -r c56a728412f1 -r a58610ab2983 setup.py --- a/setup.py Fri Aug 16 12:00:02 2019 +0200 +++ b/setup.py Fri Aug 16 12:00:03 2019 +0200 @@ -26,9 +26,8 @@ install_requires = [ 'wokkel >= 0.7.1', 'psycopg2', - 'simplejson', + 'sat_tmp', 'uuid', - 'sat_tmp', ] diff -r c56a728412f1 -r a58610ab2983 twisted/plugins/pubsub.py --- a/twisted/plugins/pubsub.py Fri Aug 16 12:00:02 2019 +0200 +++ b/twisted/plugins/pubsub.py Fri Aug 16 12:00:03 2019 +0200 @@ -166,7 +166,7 @@ def postOptions(self): if self['backend'] not in ['pgsql', 'memory']: - raise usage.UsageError, "Unknown backend!" + raise usage.UsageError("Unknown backend!") if self['backend'] == 'memory': raise NotImplementedError('memory backend is not available at the moment') @@ -211,8 +211,7 @@ ) st = Storage(dbpool) elif config['backend'] == 'memory': - from sat_pubsub.memory_storage import Storage - st = Storage() + raise NotImplementedError('memory backend is not available at the moment') bs = BackendService(st, config) bs.setName('backend')