diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/words/protocols/jabber | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/words/protocols/jabber')
11 files changed, 3600 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/__init__.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/__init__.py new file mode 100644 index 0000000000..ad95b6853e --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/__init__.py @@ -0,0 +1,8 @@ +# -*- test-case-name: twisted.words.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" +Twisted Jabber: Jabber Protocol Helpers +""" diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/client.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/client.py new file mode 100644 index 0000000000..21de4774e2 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/client.py @@ -0,0 +1,394 @@ +# -*- test-case-name: twisted.words.test.test_jabberclient -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +from twisted.words.protocols.jabber import error, sasl, xmlstream +from twisted.words.protocols.jabber.jid import JID +from twisted.words.xish import domish, utility, xpath + +NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +NS_XMPP_BIND = "urn:ietf:params:xml:ns:xmpp-bind" +NS_XMPP_SESSION = "urn:ietf:params:xml:ns:xmpp-session" +NS_IQ_AUTH_FEATURE = "http://jabber.org/features/iq-auth" + +DigestAuthQry = xpath.internQuery("/iq/query/digest") +PlaintextAuthQry = xpath.internQuery("/iq/query/password") + + +def basicClientFactory(jid, secret): + a = BasicAuthenticator(jid, secret) + return xmlstream.XmlStreamFactory(a) + + +class IQ(domish.Element): + """ + Wrapper for a Info/Query packet. + + This provides the necessary functionality to send IQs and get notified when + a result comes back. It's a subclass from L{domish.Element}, so you can use + the standard DOM manipulation calls to add data to the outbound request. + + @type callbacks: L{utility.CallbackList} + @cvar callbacks: Callback list to be notified when response comes back + + """ + + def __init__(self, xmlstream, type="set"): + """ + @type xmlstream: L{xmlstream.XmlStream} + @param xmlstream: XmlStream to use for transmission of this IQ + + @type type: C{str} + @param type: IQ type identifier ('get' or 'set') + """ + + domish.Element.__init__(self, ("jabber:client", "iq")) + self.addUniqueId() + self["type"] = type + self._xmlstream = xmlstream + self.callbacks = utility.CallbackList() + + def addCallback(self, fn, *args, **kwargs): + """ + Register a callback for notification when the IQ result is available. + """ + + self.callbacks.addCallback(True, fn, *args, **kwargs) + + def send(self, to=None): + """ + Call this method to send this IQ request via the associated XmlStream. + + @param to: Jabber ID of the entity to send the request to + @type to: C{str} + + @returns: Callback list for this IQ. Any callbacks added to this list + will be fired when the result comes back. + """ + if to != None: + self["to"] = to + self._xmlstream.addOnetimeObserver( + "/iq[@id='%s']" % self["id"], self._resultEvent + ) + self._xmlstream.send(self) + + def _resultEvent(self, iq): + self.callbacks.callback(iq) + self.callbacks = None + + +class IQAuthInitializer: + """ + Non-SASL Authentication initializer for the initiating entity. + + This protocol is defined in + U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>} and mainly serves for + compatibility with pre-XMPP-1.0 server implementations. + + @cvar INVALID_USER_EVENT: Token to signal that authentication failed, due + to invalid username. + @type INVALID_USER_EVENT: L{str} + + @cvar AUTH_FAILED_EVENT: Token to signal that authentication failed, due to + invalid password. + @type AUTH_FAILED_EVENT: L{str} + """ + + INVALID_USER_EVENT = "//event/client/basicauth/invaliduser" + AUTH_FAILED_EVENT = "//event/client/basicauth/authfailed" + + def __init__(self, xs): + self.xmlstream = xs + + def initialize(self): + # Send request for auth fields + iq = xmlstream.IQ(self.xmlstream, "get") + iq.addElement(("jabber:iq:auth", "query")) + jid = self.xmlstream.authenticator.jid + iq.query.addElement("username", content=jid.user) + + d = iq.send() + d.addCallbacks(self._cbAuthQuery, self._ebAuthQuery) + return d + + def _cbAuthQuery(self, iq): + jid = self.xmlstream.authenticator.jid + password = self.xmlstream.authenticator.password + + # Construct auth request + reply = xmlstream.IQ(self.xmlstream, "set") + reply.addElement(("jabber:iq:auth", "query")) + reply.query.addElement("username", content=jid.user) + reply.query.addElement("resource", content=jid.resource) + + # Prefer digest over plaintext + if DigestAuthQry.matches(iq): + digest = xmlstream.hashPassword(self.xmlstream.sid, password) + reply.query.addElement("digest", content=str(digest)) + else: + reply.query.addElement("password", content=password) + + d = reply.send() + d.addCallbacks(self._cbAuth, self._ebAuth) + return d + + def _ebAuthQuery(self, failure): + failure.trap(error.StanzaError) + e = failure.value + if e.condition == "not-authorized": + self.xmlstream.dispatch(e.stanza, self.INVALID_USER_EVENT) + else: + self.xmlstream.dispatch(e.stanza, self.AUTH_FAILED_EVENT) + + return failure + + def _cbAuth(self, iq): + pass + + def _ebAuth(self, failure): + failure.trap(error.StanzaError) + self.xmlstream.dispatch(failure.value.stanza, self.AUTH_FAILED_EVENT) + return failure + + +class BasicAuthenticator(xmlstream.ConnectAuthenticator): + """ + Authenticates an XmlStream against a Jabber server as a Client. + + This only implements non-SASL authentication, per + U{JEP-0078<http://www.jabber.org/jeps/jep-0078.html>}. Additionally, this + authenticator provides the ability to perform inline registration, per + U{JEP-0077<http://www.jabber.org/jeps/jep-0077.html>}. + + Under normal circumstances, the BasicAuthenticator generates the + L{xmlstream.STREAM_AUTHD_EVENT} once the stream has authenticated. However, + it can also generate other events, such as: + - L{INVALID_USER_EVENT} : Authentication failed, due to invalid username + - L{AUTH_FAILED_EVENT} : Authentication failed, due to invalid password + - L{REGISTER_FAILED_EVENT} : Registration failed + + If authentication fails for any reason, you can attempt to register by + calling the L{registerAccount} method. If the registration succeeds, a + L{xmlstream.STREAM_AUTHD_EVENT} will be fired. Otherwise, one of the above + errors will be generated (again). + + + @cvar INVALID_USER_EVENT: See L{IQAuthInitializer.INVALID_USER_EVENT}. + @type INVALID_USER_EVENT: L{str} + + @cvar AUTH_FAILED_EVENT: See L{IQAuthInitializer.AUTH_FAILED_EVENT}. + @type AUTH_FAILED_EVENT: L{str} + + @cvar REGISTER_FAILED_EVENT: Token to signal that registration failed. + @type REGISTER_FAILED_EVENT: L{str} + + """ + + namespace = "jabber:client" + + INVALID_USER_EVENT = IQAuthInitializer.INVALID_USER_EVENT + AUTH_FAILED_EVENT = IQAuthInitializer.AUTH_FAILED_EVENT + REGISTER_FAILED_EVENT = "//event/client/basicauth/registerfailed" + + def __init__(self, jid, password): + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password + + def associateWithStream(self, xs): + xs.version = (0, 0) + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ + xmlstream.TLSInitiatingInitializer(xs, required=False), + IQAuthInitializer(xs), + ] + + # TODO: move registration into an Initializer? + + def registerAccount(self, username=None, password=None): + if username: + self.jid.user = username + if password: + self.password = password + + iq = IQ(self.xmlstream, "set") + iq.addElement(("jabber:iq:register", "query")) + iq.query.addElement("username", content=self.jid.user) + iq.query.addElement("password", content=self.password) + + iq.addCallback(self._registerResultEvent) + + iq.send() + + def _registerResultEvent(self, iq): + if iq["type"] == "result": + # Registration succeeded -- go ahead and auth + self.streamStarted() + else: + # Registration failed + self.xmlstream.dispatch(iq, self.REGISTER_FAILED_EVENT) + + +class CheckVersionInitializer: + """ + Initializer that checks if the minimum common stream version number is 1.0. + """ + + def __init__(self, xs): + self.xmlstream = xs + + def initialize(self): + if self.xmlstream.version < (1, 0): + raise error.StreamError("unsupported-version") + + +class BindInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Initializer that implements Resource Binding for the initiating entity. + + This protocol is documented in U{RFC 3920, section + 7<http://www.xmpp.org/specs/rfc3920.html#bind>}. + """ + + feature = (NS_XMPP_BIND, "bind") + + def start(self): + iq = xmlstream.IQ(self.xmlstream, "set") + bind = iq.addElement((NS_XMPP_BIND, "bind")) + resource = self.xmlstream.authenticator.jid.resource + if resource: + bind.addElement("resource", content=resource) + d = iq.send() + d.addCallback(self.onBind) + return d + + def onBind(self, iq): + if iq.bind: + self.xmlstream.authenticator.jid = JID(str(iq.bind.jid)) + + +class SessionInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Initializer that implements session establishment for the initiating + entity. + + This protocol is defined in U{RFC 3921, section + 3<http://www.xmpp.org/specs/rfc3921.html#session>}. + """ + + feature = (NS_XMPP_SESSION, "session") + + def start(self): + iq = xmlstream.IQ(self.xmlstream, "set") + iq.addElement((NS_XMPP_SESSION, "session")) + return iq.send() + + +def XMPPClientFactory(jid, password, configurationForTLS=None): + """ + Client factory for XMPP 1.0 (only). + + This returns a L{xmlstream.XmlStreamFactory} with an L{XMPPAuthenticator} + object to perform the stream initialization steps (such as authentication). + + @see: The notes at L{XMPPAuthenticator} describe how the C{jid} and + C{password} parameters are to be used. + + @param jid: Jabber ID to connect with. + @type jid: L{jid.JID} + + @param password: password to authenticate with. + @type password: L{unicode} + + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If L{None}, the default is + to verify the server certificate against the trust roots as provided by + the platform. See L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or L{None} + + @return: XML stream factory. + @rtype: L{xmlstream.XmlStreamFactory} + """ + a = XMPPAuthenticator(jid, password, configurationForTLS=configurationForTLS) + return xmlstream.XmlStreamFactory(a) + + +class XMPPAuthenticator(xmlstream.ConnectAuthenticator): + """ + Initializes an XmlStream connecting to an XMPP server as a Client. + + This authenticator performs the initialization steps needed to start + exchanging XML stanzas with an XMPP server as an XMPP client. It checks if + the server advertises XML stream version 1.0, negotiates TLS (when + available), performs SASL authentication, binds a resource and establishes + a session. + + Upon successful stream initialization, the L{xmlstream.STREAM_AUTHD_EVENT} + event will be dispatched through the XML stream object. Otherwise, the + L{xmlstream.INIT_FAILED_EVENT} event will be dispatched with a failure + object. + + After inspection of the failure, initialization can then be restarted by + calling L{ConnectAuthenticator.initializeStream}. For example, in case of + authentication failure, a user may be given the opportunity to input the + correct password. By setting the L{password} instance variable and restarting + initialization, the stream authentication step is then retried, and subsequent + steps are performed if successful. + + @ivar jid: Jabber ID to authenticate with. This may contain a resource + part, as a suggestion to the server for resource binding. A + server may override this, though. If the resource part is left + off, the server will generate a unique resource identifier. + The server will always return the full Jabber ID in the + resource binding step, and this is stored in this instance + variable. + @type jid: L{jid.JID} + + @ivar password: password to be used during SASL authentication. + @type password: L{unicode} + """ + + namespace = "jabber:client" + + def __init__(self, jid, password, configurationForTLS=None): + """ + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the + default is to verify the server certificate against the trust roots + as provided by the platform. See + L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or + C{None} + """ + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password + self._configurationForTLS = configurationForTLS + + def associateWithStream(self, xs): + """ + Register with the XML stream. + + Populates stream's list of initializers, along with their + requiredness. This list is used by + L{ConnectAuthenticator.initializeStream} to perform the initialization + steps. + """ + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ + CheckVersionInitializer(xs), + xmlstream.TLSInitiatingInitializer( + xs, required=True, configurationForTLS=self._configurationForTLS + ), + sasl.SASLInitiatingInitializer(xs, required=True), + BindInitializer(xs, required=True), + SessionInitializer(xs, required=False), + ] diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/component.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/component.py new file mode 100644 index 0000000000..d07c4ee9d7 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/component.py @@ -0,0 +1,456 @@ +# -*- test-case-name: twisted.words.test.test_jabbercomponent -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +External server-side components. + +Most Jabber server implementations allow for add-on components that act as a +separate entity on the Jabber network, but use the server-to-server +functionality of a regular Jabber IM server. These so-called 'external +components' are connected to the Jabber server using the Jabber Component +Protocol as defined in U{JEP-0114<http://www.jabber.org/jeps/jep-0114.html>}. + +This module allows for writing external server-side component by assigning one +or more services implementing L{ijabber.IService} up to L{ServiceManager}. The +ServiceManager connects to the Jabber server and is responsible for the +corresponding XML stream. +""" + +from zope.interface import implementer + +from twisted.application import service +from twisted.internet import defer +from twisted.python import log +from twisted.words.protocols.jabber import error, ijabber, jstrports, xmlstream +from twisted.words.protocols.jabber.jid import internJID as JID +from twisted.words.xish import domish + +NS_COMPONENT_ACCEPT = "jabber:component:accept" + + +def componentFactory(componentid, password): + """ + XML stream factory for external server-side components. + + @param componentid: JID of the component. + @type componentid: L{unicode} + @param password: password used to authenticate to the server. + @type password: C{str} + """ + a = ConnectComponentAuthenticator(componentid, password) + return xmlstream.XmlStreamFactory(a) + + +class ComponentInitiatingInitializer: + """ + External server-side component authentication initializer for the + initiating entity. + + @ivar xmlstream: XML stream between server and component. + @type xmlstream: L{xmlstream.XmlStream} + """ + + def __init__(self, xs): + self.xmlstream = xs + self._deferred = None + + def initialize(self): + xs = self.xmlstream + hs = domish.Element((self.xmlstream.namespace, "handshake")) + digest = xmlstream.hashPassword(xs.sid, xs.authenticator.password) + hs.addContent(str(digest)) + + # Setup observer to watch for handshake result + xs.addOnetimeObserver("/handshake", self._cbHandshake) + xs.send(hs) + self._deferred = defer.Deferred() + return self._deferred + + def _cbHandshake(self, _): + # we have successfully shaken hands and can now consider this + # entity to represent the component JID. + self.xmlstream.thisEntity = self.xmlstream.otherEntity + self._deferred.callback(None) + + +class ConnectComponentAuthenticator(xmlstream.ConnectAuthenticator): + """ + Authenticator to permit an XmlStream to authenticate against a Jabber + server as an external component (where the Authenticator is initiating the + stream). + """ + + namespace = NS_COMPONENT_ACCEPT + + def __init__(self, componentjid, password): + """ + @type componentjid: C{str} + @param componentjid: Jabber ID that this component wishes to bind to. + + @type password: C{str} + @param password: Password/secret this component uses to authenticate. + """ + # Note that we are sending 'to' our desired component JID. + xmlstream.ConnectAuthenticator.__init__(self, componentjid) + self.password = password + + def associateWithStream(self, xs): + xs.version = (0, 0) + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + xs.initializers = [ComponentInitiatingInitializer(xs)] + + +class ListenComponentAuthenticator(xmlstream.ListenAuthenticator): + """ + Authenticator for accepting components. + + @since: 8.2 + @ivar secret: The shared secret used to authorized incoming component + connections. + @type secret: C{unicode}. + """ + + namespace = NS_COMPONENT_ACCEPT + + def __init__(self, secret): + self.secret = secret + xmlstream.ListenAuthenticator.__init__(self) + + def associateWithStream(self, xs): + """ + Associate the authenticator with a stream. + + This sets the stream's version to 0.0, because the XEP-0114 component + protocol was not designed for XMPP 1.0. + """ + xs.version = (0, 0) + xmlstream.ListenAuthenticator.associateWithStream(self, xs) + + def streamStarted(self, rootElement): + """ + Called by the stream when it has started. + + This examines the default namespace of the incoming stream and whether + there is a requested hostname for the component. Then it generates a + stream identifier, sends a response header and adds an observer for + the first incoming element, triggering L{onElement}. + """ + + xmlstream.ListenAuthenticator.streamStarted(self, rootElement) + + if rootElement.defaultUri != self.namespace: + exc = error.StreamError("invalid-namespace") + self.xmlstream.sendStreamError(exc) + return + + # self.xmlstream.thisEntity is set to the address the component + # wants to assume. + if not self.xmlstream.thisEntity: + exc = error.StreamError("improper-addressing") + self.xmlstream.sendStreamError(exc) + return + + self.xmlstream.sendHeader() + self.xmlstream.addOnetimeObserver("/*", self.onElement) + + def onElement(self, element): + """ + Called on incoming XML Stanzas. + + The very first element received should be a request for handshake. + Otherwise, the stream is dropped with a 'not-authorized' error. If a + handshake request was received, the hash is extracted and passed to + L{onHandshake}. + """ + if (element.uri, element.name) == (self.namespace, "handshake"): + self.onHandshake(str(element)) + else: + exc = error.StreamError("not-authorized") + self.xmlstream.sendStreamError(exc) + + def onHandshake(self, handshake): + """ + Called upon receiving the handshake request. + + This checks that the given hash in C{handshake} is equal to a + calculated hash, responding with a handshake reply or a stream error. + If the handshake was ok, the stream is authorized, and XML Stanzas may + be exchanged. + """ + calculatedHash = xmlstream.hashPassword(self.xmlstream.sid, str(self.secret)) + if handshake != calculatedHash: + exc = error.StreamError("not-authorized", text="Invalid hash") + self.xmlstream.sendStreamError(exc) + else: + self.xmlstream.send("<handshake/>") + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + + +@implementer(ijabber.IService) +class Service(service.Service): + """ + External server-side component service. + """ + + def componentConnected(self, xs): + pass + + def componentDisconnected(self): + pass + + def transportConnected(self, xs): + pass + + def send(self, obj): + """ + Send data over service parent's XML stream. + + @note: L{ServiceManager} maintains a queue for data sent using this + method when there is no current established XML stream. This data is + then sent as soon as a new stream has been established and initialized. + Subsequently, L{componentConnected} will be called again. If this + queueing is not desired, use C{send} on the XmlStream object (passed to + L{componentConnected}) directly. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + + self.parent.send(obj) + + +class ServiceManager(service.MultiService): + """ + Business logic for a managed component connection to a Jabber router. + + This service maintains a single connection to a Jabber router and provides + facilities for packet routing and transmission. Business logic modules are + services implementing L{ijabber.IService} (like subclasses of L{Service}), + and added as sub-service. + """ + + def __init__(self, jid, password): + service.MultiService.__init__(self) + + # Setup defaults + self.jabberId = jid + self.xmlstream = None + + # Internal buffer of packets + self._packetQueue = [] + + # Setup the xmlstream factory + self._xsFactory = componentFactory(self.jabberId, password) + + # Register some lambda functions to keep the self.xmlstream var up to + # date + self._xsFactory.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self._connected) + self._xsFactory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self._authd) + self._xsFactory.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected) + + # Map addBootstrap and removeBootstrap to the underlying factory -- is + # this right? I have no clue...but it'll work for now, until i can + # think about it more. + self.addBootstrap = self._xsFactory.addBootstrap + self.removeBootstrap = self._xsFactory.removeBootstrap + + def getFactory(self): + return self._xsFactory + + def _connected(self, xs): + self.xmlstream = xs + for c in self: + if ijabber.IService.providedBy(c): + c.transportConnected(xs) + + def _authd(self, xs): + # Flush all pending packets + for p in self._packetQueue: + self.xmlstream.send(p) + self._packetQueue = [] + + # Notify all child services which implement the IService interface + for c in self: + if ijabber.IService.providedBy(c): + c.componentConnected(xs) + + def _disconnected(self, _): + self.xmlstream = None + + # Notify all child services which implement + # the IService interface + for c in self: + if ijabber.IService.providedBy(c): + c.componentDisconnected() + + def send(self, obj): + """ + Send data over the XML stream. + + When there is no established XML stream, the data is queued and sent + out when a new XML stream has been established and initialized. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + + if self.xmlstream != None: + self.xmlstream.send(obj) + else: + self._packetQueue.append(obj) + + +def buildServiceManager(jid, password, strport): + """ + Constructs a pre-built L{ServiceManager}, using the specified strport + string. + """ + + svc = ServiceManager(jid, password) + client_svc = jstrports.client(strport, svc.getFactory()) + client_svc.setServiceParent(svc) + return svc + + +class Router: + """ + XMPP Server's Router. + + A router connects the different components of the XMPP service and routes + messages between them based on the given routing table. + + Connected components are trusted to have correct addressing in the + stanzas they offer for routing. + + A route destination of L{None} adds a default route. Traffic for which no + specific route exists, will be routed to this default route. + + @since: 8.2 + @ivar routes: Routes based on the host part of JIDs. Maps host names to the + L{EventDispatcher<utility.EventDispatcher>}s that should + receive the traffic. A key of L{None} means the default + route. + @type routes: C{dict} + """ + + def __init__(self): + self.routes = {} + + def addRoute(self, destination, xs): + """ + Add a new route. + + The passed XML Stream C{xs} will have an observer for all stanzas + added to route its outgoing traffic. In turn, traffic for + C{destination} will be passed to this stream. + + @param destination: Destination of the route to be added as a host name + or L{None} for the default route. + @type destination: C{str} or L{None}. + @param xs: XML Stream to register the route for. + @type xs: L{EventDispatcher<utility.EventDispatcher>}. + """ + self.routes[destination] = xs + xs.addObserver("/*", self.route) + + def removeRoute(self, destination, xs): + """ + Remove a route. + + @param destination: Destination of the route that should be removed. + @type destination: C{str}. + @param xs: XML Stream to remove the route for. + @type xs: L{EventDispatcher<utility.EventDispatcher>}. + """ + xs.removeObserver("/*", self.route) + if xs == self.routes[destination]: + del self.routes[destination] + + def route(self, stanza): + """ + Route a stanza. + + @param stanza: The stanza to be routed. + @type stanza: L{domish.Element}. + """ + destination = JID(stanza["to"]) + + log.msg(f"Routing to {destination.full()}: {stanza.toXml()!r}") + + if destination.host in self.routes: + self.routes[destination.host].send(stanza) + else: + self.routes[None].send(stanza) + + +class XMPPComponentServerFactory(xmlstream.XmlStreamServerFactory): + """ + XMPP Component Server factory. + + This factory accepts XMPP external component connections and makes + the router service route traffic for a component's bound domain + to that component. + + @since: 8.2 + """ + + logTraffic = False + + def __init__(self, router, secret="secret"): + self.router = router + self.secret = secret + + def authenticatorFactory(): + return ListenComponentAuthenticator(self.secret) + + xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory) + self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT, self.onConnectionMade) + self.addBootstrap(xmlstream.STREAM_AUTHD_EVENT, self.onAuthenticated) + + self.serial = 0 + + def onConnectionMade(self, xs): + """ + Called when a component connection was made. + + This enables traffic debugging on incoming streams. + """ + xs.serial = self.serial + self.serial += 1 + + def logDataIn(buf): + log.msg("RECV (%d): %r" % (xs.serial, buf)) + + def logDataOut(buf): + log.msg("SEND (%d): %r" % (xs.serial, buf)) + + if self.logTraffic: + xs.rawDataInFn = logDataIn + xs.rawDataOutFn = logDataOut + + xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError) + + def onAuthenticated(self, xs): + """ + Called when a component has successfully authenticated. + + Add the component to the routing table and establish a handler + for a closed connection. + """ + destination = xs.thisEntity.host + + self.router.addRoute(destination, xs) + xs.addObserver( + xmlstream.STREAM_END_EVENT, self.onConnectionLost, 0, destination, xs + ) + + def onError(self, reason): + log.err(reason, "Stream Error") + + def onConnectionLost(self, destination, xs, reason): + self.router.removeRoute(destination, xs) diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/error.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/error.py new file mode 100644 index 0000000000..4d1644767d --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/error.py @@ -0,0 +1,323 @@ +# -*- test-case-name: twisted.words.test.test_jabbererror -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP Error support. +""" + + +import copy +from typing import Optional + +from twisted.words.xish import domish + +NS_XML = "http://www.w3.org/XML/1998/namespace" +NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" +NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" + +STANZA_CONDITIONS = { + "bad-request": {"code": "400", "type": "modify"}, + "conflict": {"code": "409", "type": "cancel"}, + "feature-not-implemented": {"code": "501", "type": "cancel"}, + "forbidden": {"code": "403", "type": "auth"}, + "gone": {"code": "302", "type": "modify"}, + "internal-server-error": {"code": "500", "type": "wait"}, + "item-not-found": {"code": "404", "type": "cancel"}, + "jid-malformed": {"code": "400", "type": "modify"}, + "not-acceptable": {"code": "406", "type": "modify"}, + "not-allowed": {"code": "405", "type": "cancel"}, + "not-authorized": {"code": "401", "type": "auth"}, + "payment-required": {"code": "402", "type": "auth"}, + "recipient-unavailable": {"code": "404", "type": "wait"}, + "redirect": {"code": "302", "type": "modify"}, + "registration-required": {"code": "407", "type": "auth"}, + "remote-server-not-found": {"code": "404", "type": "cancel"}, + "remote-server-timeout": {"code": "504", "type": "wait"}, + "resource-constraint": {"code": "500", "type": "wait"}, + "service-unavailable": {"code": "503", "type": "cancel"}, + "subscription-required": {"code": "407", "type": "auth"}, + "undefined-condition": {"code": "500", "type": None}, + "unexpected-request": {"code": "400", "type": "wait"}, +} + +CODES_TO_CONDITIONS = { + "302": ("gone", "modify"), + "400": ("bad-request", "modify"), + "401": ("not-authorized", "auth"), + "402": ("payment-required", "auth"), + "403": ("forbidden", "auth"), + "404": ("item-not-found", "cancel"), + "405": ("not-allowed", "cancel"), + "406": ("not-acceptable", "modify"), + "407": ("registration-required", "auth"), + "408": ("remote-server-timeout", "wait"), + "409": ("conflict", "cancel"), + "500": ("internal-server-error", "wait"), + "501": ("feature-not-implemented", "cancel"), + "502": ("service-unavailable", "wait"), + "503": ("service-unavailable", "cancel"), + "504": ("remote-server-timeout", "wait"), + "510": ("service-unavailable", "cancel"), +} + + +class BaseError(Exception): + """ + Base class for XMPP error exceptions. + + @cvar namespace: The namespace of the C{error} element generated by + C{getElement}. + @type namespace: C{str} + @ivar condition: The error condition. The valid values are defined by + subclasses of L{BaseError}. + @type contition: C{str} + @ivar text: Optional text message to supplement the condition or application + specific condition. + @type text: C{unicode} + @ivar textLang: Identifier of the language used for the message in C{text}. + Values are as described in RFC 3066. + @type textLang: C{str} + @ivar appCondition: Application specific condition element, supplementing + the error condition in C{condition}. + @type appCondition: object providing L{domish.IElement}. + """ + + namespace: Optional[str] = None + + def __init__(self, condition, text=None, textLang=None, appCondition=None): + Exception.__init__(self) + self.condition = condition + self.text = text + self.textLang = textLang + self.appCondition = appCondition + + def __str__(self) -> str: + message = "{} with condition {!r}".format( + self.__class__.__name__, self.condition + ) + + if self.text: + message += ": " + self.text + + return message + + def getElement(self): + """ + Get XML representation from self. + + The method creates an L{domish} representation of the + error data contained in this exception. + + @rtype: L{domish.Element} + """ + error = domish.Element((None, "error")) + error.addElement((self.namespace, self.condition)) + if self.text: + text = error.addElement((self.namespace, "text"), content=self.text) + if self.textLang: + text[(NS_XML, "lang")] = self.textLang + if self.appCondition: + error.addChild(self.appCondition) + return error + + +class StreamError(BaseError): + """ + Stream Error exception. + + Refer to RFC 3920, section 4.7.3, for the allowed values for C{condition}. + """ + + namespace = NS_XMPP_STREAMS + + def getElement(self): + """ + Get XML representation from self. + + Overrides the base L{BaseError.getElement} to make sure the returned + element is in the XML Stream namespace. + + @rtype: L{domish.Element} + """ + from twisted.words.protocols.jabber.xmlstream import NS_STREAMS + + error = BaseError.getElement(self) + error.uri = NS_STREAMS + return error + + +class StanzaError(BaseError): + """ + Stanza Error exception. + + Refer to RFC 3920, section 9.3, for the allowed values for C{condition} and + C{type}. + + @ivar type: The stanza error type. Gives a suggestion to the recipient + of the error on how to proceed. + @type type: C{str} + @ivar code: A numeric identifier for the error condition for backwards + compatibility with pre-XMPP Jabber implementations. + """ + + namespace = NS_XMPP_STANZAS + + def __init__( + self, condition, type=None, text=None, textLang=None, appCondition=None + ): + BaseError.__init__(self, condition, text, textLang, appCondition) + + if type is None: + try: + type = STANZA_CONDITIONS[condition]["type"] + except KeyError: + pass + self.type = type + + try: + self.code = STANZA_CONDITIONS[condition]["code"] + except KeyError: + self.code = None + + self.children = [] + self.iq = None + + def getElement(self): + """ + Get XML representation from self. + + Overrides the base L{BaseError.getElement} to make sure the returned + element has a C{type} attribute and optionally a legacy C{code} + attribute. + + @rtype: L{domish.Element} + """ + error = BaseError.getElement(self) + error["type"] = self.type + if self.code: + error["code"] = self.code + return error + + def toResponse(self, stanza): + """ + Construct error response stanza. + + The C{stanza} is transformed into an error response stanza by + swapping the C{to} and C{from} addresses and inserting an error + element. + + @note: This creates a shallow copy of the list of child elements of the + stanza. The child elements themselves are not copied themselves, + and references to their parent element will still point to the + original stanza element. + + The serialization of an element does not use the reference to + its parent, so the typical use case of immediately sending out + the constructed error response is not affected. + + @param stanza: the stanza to respond to + @type stanza: L{domish.Element} + """ + from twisted.words.protocols.jabber.xmlstream import toResponse + + response = toResponse(stanza, stanzaType="error") + response.children = copy.copy(stanza.children) + response.addChild(self.getElement()) + return response + + +def _parseError(error, errorNamespace): + """ + Parses an error element. + + @param error: The error element to be parsed + @type error: L{domish.Element} + @param errorNamespace: The namespace of the elements that hold the error + condition and text. + @type errorNamespace: C{str} + @return: Dictionary with extracted error information. If present, keys + C{condition}, C{text}, C{textLang} have a string value, + and C{appCondition} has an L{domish.Element} value. + @rtype: C{dict} + """ + condition = None + text = None + textLang = None + appCondition = None + + for element in error.elements(): + if element.uri == errorNamespace: + if element.name == "text": + text = str(element) + textLang = element.getAttribute((NS_XML, "lang")) + else: + condition = element.name + else: + appCondition = element + + return { + "condition": condition, + "text": text, + "textLang": textLang, + "appCondition": appCondition, + } + + +def exceptionFromStreamError(element): + """ + Build an exception object from a stream error. + + @param element: the stream error + @type element: L{domish.Element} + @return: the generated exception object + @rtype: L{StreamError} + """ + error = _parseError(element, NS_XMPP_STREAMS) + + exception = StreamError( + error["condition"], error["text"], error["textLang"], error["appCondition"] + ) + + return exception + + +def exceptionFromStanza(stanza): + """ + Build an exception object from an error stanza. + + @param stanza: the error stanza + @type stanza: L{domish.Element} + @return: the generated exception object + @rtype: L{StanzaError} + """ + children = [] + condition = text = textLang = appCondition = type = code = None + + for element in stanza.elements(): + if element.name == "error" and element.uri == stanza.uri: + code = element.getAttribute("code") + type = element.getAttribute("type") + error = _parseError(element, NS_XMPP_STANZAS) + condition = error["condition"] + text = error["text"] + textLang = error["textLang"] + appCondition = error["appCondition"] + + if not condition and code: + condition, type = CODES_TO_CONDITIONS[code] + text = str(stanza.error) + else: + children.append(element) + + if condition is None: + # TODO: raise exception instead? + return StanzaError(None) + + exception = StanzaError(condition, type, text, textLang, appCondition) + + exception.children = children + exception.stanza = stanza + + return exception diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/ijabber.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/ijabber.py new file mode 100644 index 0000000000..5408a9ae6c --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/ijabber.py @@ -0,0 +1,188 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Public Jabber Interfaces. +""" + +from zope.interface import Attribute, Interface + + +class IInitializer(Interface): + """ + Interface for XML stream initializers. + + Initializers perform a step in getting the XML stream ready to be + used for the exchange of XML stanzas. + """ + + +class IInitiatingInitializer(IInitializer): + """ + Interface for XML stream initializers for the initiating entity. + """ + + xmlstream = Attribute("""The associated XML stream""") + + def initialize(): + """ + Initiate the initialization step. + + May return a deferred when the initialization is done asynchronously. + """ + + +class IIQResponseTracker(Interface): + """ + IQ response tracker interface. + + The XMPP stanza C{iq} has a request-response nature that fits + naturally with deferreds. You send out a request and when the response + comes back a deferred is fired. + + The L{twisted.words.protocols.jabber.client.IQ} class implements a C{send} + method that returns a deferred. This deferred is put in a dictionary that + is kept in an L{XmlStream} object, keyed by the request stanzas C{id} + attribute. + + An object providing this interface (usually an instance of L{XmlStream}), + keeps the said dictionary and sets observers on the iq stanzas of type + C{result} and C{error} and lets the callback fire the associated deferred. + """ + + iqDeferreds = Attribute("Dictionary of deferreds waiting for an iq " "response") + + +class IXMPPHandler(Interface): + """ + Interface for XMPP protocol handlers. + + Objects that provide this interface can be added to a stream manager to + handle of (part of) an XMPP extension protocol. + """ + + parent = Attribute("""XML stream manager for this handler""") + xmlstream = Attribute("""The managed XML stream""") + + def setHandlerParent(parent): + """ + Set the parent of the handler. + + @type parent: L{IXMPPHandlerCollection} + """ + + def disownHandlerParent(parent): + """ + Remove the parent of the handler. + + @type parent: L{IXMPPHandlerCollection} + """ + + def makeConnection(xs): + """ + A connection over the underlying transport of the XML stream has been + established. + + At this point, no traffic has been exchanged over the XML stream + given in C{xs}. + + This should setup L{xmlstream} and call L{connectionMade}. + + @type xs: + L{twisted.words.protocols.jabber.xmlstream.XmlStream} + """ + + def connectionMade(): + """ + Called after a connection has been established. + + This method can be used to change properties of the XML Stream, its + authenticator or the stream manager prior to stream initialization + (including authentication). + """ + + def connectionInitialized(): + """ + The XML stream has been initialized. + + At this point, authentication was successful, and XML stanzas can be + exchanged over the XML stream L{xmlstream}. This method can be + used to setup observers for incoming stanzas. + """ + + def connectionLost(reason): + """ + The XML stream has been closed. + + Subsequent use of C{parent.send} will result in data being queued + until a new connection has been established. + + @type reason: L{twisted.python.failure.Failure} + """ + + +class IXMPPHandlerCollection(Interface): + """ + Collection of handlers. + + Contain several handlers and manage their connection. + """ + + def __iter__(): + """ + Get an iterator over all child handlers. + """ + + def addHandler(handler): + """ + Add a child handler. + + @type handler: L{IXMPPHandler} + """ + + def removeHandler(handler): + """ + Remove a child handler. + + @type handler: L{IXMPPHandler} + """ + + +class IService(Interface): + """ + External server-side component service interface. + + Services that provide this interface can be added to L{ServiceManager} to + implement (part of) the functionality of the server-side component. + """ + + def componentConnected(xs): + """ + Parent component has established a connection. + + At this point, authentication was successful, and XML stanzas + can be exchanged over the XML stream C{xs}. This method can be used + to setup observers for incoming stanzas. + + @param xs: XML Stream that represents the established connection. + @type xs: L{xmlstream.XmlStream} + """ + + def componentDisconnected(): + """ + Parent component has lost the connection to the Jabber server. + + Subsequent use of C{self.parent.send} will result in data being + queued until a new connection has been established. + """ + + def transportConnected(xs): + """ + Parent component has established a connection over the underlying + transport. + + At this point, no traffic has been exchanged over the XML stream. This + method can be used to change properties of the XML Stream (in C{xs}), + the service manager or it's authenticator prior to stream + initialization (including authentication). + """ diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jid.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jid.py new file mode 100644 index 0000000000..52e154fee4 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jid.py @@ -0,0 +1,259 @@ +# -*- test-case-name: twisted.words.test.test_jabberjid -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Jabber Identifier support. + +This module provides an object to represent Jabber Identifiers (JIDs) and +parse string representations into them with proper checking for illegal +characters, case folding and canonicalisation through +L{stringprep<twisted.words.protocols.jabber.xmpp_stringprep>}. +""" + +from typing import Dict, Tuple, Union + +from twisted.words.protocols.jabber.xmpp_stringprep import ( + nameprep, + nodeprep, + resourceprep, +) + + +class InvalidFormat(Exception): + """ + The given string could not be parsed into a valid Jabber Identifier (JID). + """ + + +def parse(jidstring: str) -> Tuple[Union[str, None], str, Union[str, None]]: + """ + Parse given JID string into its respective parts and apply stringprep. + + @param jidstring: string representation of a JID. + @type jidstring: L{str} + @return: tuple of (user, host, resource), each of type L{str} as + the parsed and stringprep'd parts of the given JID. If the + given string did not have a user or resource part, the respective + field in the tuple will hold L{None}. + @rtype: L{tuple} + """ + user = None + host = None + resource = None + + # Search for delimiters + user_sep = jidstring.find("@") + res_sep = jidstring.find("/") + + if user_sep == -1: + if res_sep == -1: + # host + host = jidstring + else: + # host/resource + host = jidstring[0:res_sep] + resource = jidstring[res_sep + 1 :] or None + else: + if res_sep == -1: + # user@host + user = jidstring[0:user_sep] or None + host = jidstring[user_sep + 1 :] + else: + if user_sep < res_sep: + # user@host/resource + user = jidstring[0:user_sep] or None + host = jidstring[user_sep + 1 : user_sep + (res_sep - user_sep)] + resource = jidstring[res_sep + 1 :] or None + else: + # host/resource (with an @ in resource) + host = jidstring[0:res_sep] + resource = jidstring[res_sep + 1 :] or None + + return prep(user, host, resource) + + +def prep( + user: Union[str, None], host: str, resource: Union[str, None] +) -> Tuple[Union[str, None], str, Union[str, None]]: + """ + Perform stringprep on all JID fragments. + + @param user: The user part of the JID. + @type user: L{str} + @param host: The host part of the JID. + @type host: L{str} + @param resource: The resource part of the JID. + @type resource: L{str} + @return: The given parts with stringprep applied. + @rtype: L{tuple} + """ + + if user: + try: + user = nodeprep.prepare(str(user)) + except UnicodeError: + raise InvalidFormat("Invalid character in username") + else: + user = None + + if not host: + raise InvalidFormat("Server address required.") + else: + try: + host = nameprep.prepare(str(host)) + except UnicodeError: + raise InvalidFormat("Invalid character in hostname") + + if resource: + try: + resource = resourceprep.prepare(str(resource)) + except UnicodeError: + raise InvalidFormat("Invalid character in resource") + else: + resource = None + + return (user, host, resource) + + +__internJIDs: Dict[str, "JID"] = {} + + +def internJID(jidstring): + """ + Return interned JID. + + @rtype: L{JID} + """ + + if jidstring in __internJIDs: + return __internJIDs[jidstring] + else: + j = JID(jidstring) + __internJIDs[jidstring] = j + return j + + +class JID: + """ + Represents a stringprep'd Jabber ID. + + JID objects are hashable so they can be used in sets and as keys in + dictionaries. + """ + + def __init__( + self, + str: Union[str, None] = None, + tuple: Union[Tuple[Union[str, None], str, Union[str, None]], None] = None, + ): + if str: + user, host, res = parse(str) + elif tuple: + user, host, res = prep(*tuple) + else: + raise RuntimeError( + "You must provide a value for either 'str' or 'tuple' arguments." + ) + + self.user = user + self.host = host + self.resource = res + + def userhost(self): + """ + Extract the bare JID as a unicode string. + + A bare JID does not have a resource part, so this returns either + C{user@host} or just C{host}. + + @rtype: L{str} + """ + if self.user: + return f"{self.user}@{self.host}" + else: + return self.host + + def userhostJID(self): + """ + Extract the bare JID. + + A bare JID does not have a resource part, so this returns a + L{JID} object representing either C{user@host} or just C{host}. + + If the object this method is called upon doesn't have a resource + set, it will return itself. Otherwise, the bare JID object will + be created, interned using L{internJID}. + + @rtype: L{JID} + """ + if self.resource: + return internJID(self.userhost()) + else: + return self + + def full(self): + """ + Return the string representation of this JID. + + @rtype: L{str} + """ + if self.user: + if self.resource: + return f"{self.user}@{self.host}/{self.resource}" + else: + return f"{self.user}@{self.host}" + else: + if self.resource: + return f"{self.host}/{self.resource}" + else: + return self.host + + def __eq__(self, other: object) -> bool: + """ + Equality comparison. + + L{JID}s compare equal if their user, host and resource parts all + compare equal. When comparing against instances of other types, it + uses the default comparison. + """ + if isinstance(other, JID): + return ( + self.user == other.user + and self.host == other.host + and self.resource == other.resource + ) + else: + return NotImplemented + + def __hash__(self): + """ + Calculate hash. + + L{JID}s with identical constituent user, host and resource parts have + equal hash values. In combination with the comparison defined on JIDs, + this allows for using L{JID}s in sets and as dictionary keys. + """ + return hash((self.user, self.host, self.resource)) + + def __unicode__(self): + """ + Get unicode representation. + + Return the string representation of this JID as a unicode string. + @see: L{full} + """ + + return self.full() + + __str__ = __unicode__ + + def __repr__(self) -> str: + """ + Get object representation. + + Returns a string that would create a new JID object that compares equal + to this one. + """ + return "JID(%r)" % self.full() diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jstrports.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jstrports.py new file mode 100644 index 0000000000..b564fe3512 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/jstrports.py @@ -0,0 +1,34 @@ +# -*- test-case-name: twisted.words.test -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +""" A temporary placeholder for client-capable strports, until we +sufficient use cases get identified """ + + +from twisted.internet.endpoints import _parse + + +def _parseTCPSSL(factory, domain, port): + """For the moment, parse TCP or SSL connections the same""" + return (domain, int(port), factory), {} + + +def _parseUNIX(factory, address): + return (address, factory), {} + + +_funcs = {"tcp": _parseTCPSSL, "unix": _parseUNIX, "ssl": _parseTCPSSL} + + +def parse(description, factory): + args, kw = _parse(description) + return (args[0].upper(),) + _funcs[args[0]](factory, *args[1:], **kw) + + +def client(description, factory): + from twisted.application import internet + + name, args, kw = parse(description, factory) + return getattr(internet, name + "Client")(*args, **kw) diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl.py new file mode 100644 index 0000000000..8bcf1a534a --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl.py @@ -0,0 +1,229 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP-specific SASL profile. +""" + + +import re +from base64 import b64decode, b64encode + +from twisted.internet import defer +from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream +from twisted.words.xish import domish + +NS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl" + + +def get_mechanisms(xs): + """ + Parse the SASL feature to extract the available mechanism names. + """ + mechanisms = [] + for element in xs.features[(NS_XMPP_SASL, "mechanisms")].elements(): + if element.name == "mechanism": + mechanisms.append(str(element)) + + return mechanisms + + +class SASLError(Exception): + """ + SASL base exception. + """ + + +class SASLNoAcceptableMechanism(SASLError): + """ + The server did not present an acceptable SASL mechanism. + """ + + +class SASLAuthError(SASLError): + """ + SASL Authentication failed. + """ + + def __init__(self, condition=None): + self.condition = condition + + def __str__(self) -> str: + return "SASLAuthError with condition %r" % self.condition + + +class SASLIncorrectEncodingError(SASLError): + """ + SASL base64 encoding was incorrect. + + RFC 3920 specifies that any characters not in the base64 alphabet + and padding characters present elsewhere than at the end of the string + MUST be rejected. See also L{fromBase64}. + + This exception is raised whenever the encoded string does not adhere + to these additional restrictions or when the decoding itself fails. + + The recommended behaviour for so-called receiving entities (like servers in + client-to-server connections, see RFC 3920 for terminology) is to fail the + SASL negotiation with a C{'incorrect-encoding'} condition. For initiating + entities, one should assume the receiving entity to be either buggy or + malevolent. The stream should be terminated and reconnecting is not + advised. + """ + + +base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$") + + +def fromBase64(s): + """ + Decode base64 encoded string. + + This helper performs regular decoding of a base64 encoded string, but also + rejects any characters that are not in the base64 alphabet and padding + occurring elsewhere from the last or last two characters, as specified in + section 14.9 of RFC 3920. This safeguards against various attack vectors + among which the creation of a covert channel that "leaks" information. + """ + + if base64Pattern.match(s) is None: + raise SASLIncorrectEncodingError() + + try: + return b64decode(s) + except Exception as e: + raise SASLIncorrectEncodingError(str(e)) + + +class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer): + """ + Stream initializer that performs SASL authentication. + + The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN} + and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set + on the authenticator, does not have a localpart (username), requesting an + anonymous session where the username is generated by the server. + Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order. + """ + + feature = (NS_XMPP_SASL, "mechanisms") + _deferred = None + + def setMechanism(self): + """ + Select and setup authentication mechanism. + + Uses the authenticator's C{jid} and C{password} attribute for the + authentication credentials. If no supported SASL mechanisms are + advertized by the receiving party, a failing deferred is returned with + a L{SASLNoAcceptableMechanism} exception. + """ + + jid = self.xmlstream.authenticator.jid + password = self.xmlstream.authenticator.password + + mechanisms = get_mechanisms(self.xmlstream) + if jid.user is not None: + if "DIGEST-MD5" in mechanisms: + self.mechanism = sasl_mechanisms.DigestMD5( + "xmpp", jid.host, None, jid.user, password + ) + elif "PLAIN" in mechanisms: + self.mechanism = sasl_mechanisms.Plain(None, jid.user, password) + else: + raise SASLNoAcceptableMechanism() + else: + if "ANONYMOUS" in mechanisms: + self.mechanism = sasl_mechanisms.Anonymous() + else: + raise SASLNoAcceptableMechanism() + + def start(self): + """ + Start SASL authentication exchange. + """ + + self.setMechanism() + self._deferred = defer.Deferred() + self.xmlstream.addObserver("/challenge", self.onChallenge) + self.xmlstream.addOnetimeObserver("/success", self.onSuccess) + self.xmlstream.addOnetimeObserver("/failure", self.onFailure) + self.sendAuth(self.mechanism.getInitialResponse()) + return self._deferred + + def sendAuth(self, data=None): + """ + Initiate authentication protocol exchange. + + If an initial client response is given in C{data}, it will be + sent along. + + @param data: initial client response. + @type data: C{str} or L{None}. + """ + + auth = domish.Element((NS_XMPP_SASL, "auth")) + auth["mechanism"] = self.mechanism.name + if data is not None: + auth.addContent(b64encode(data).decode("ascii") or "=") + self.xmlstream.send(auth) + + def sendResponse(self, data=b""): + """ + Send response to a challenge. + + @param data: client response. + @type data: L{bytes}. + """ + + response = domish.Element((NS_XMPP_SASL, "response")) + if data: + response.addContent(b64encode(data).decode("ascii")) + self.xmlstream.send(response) + + def onChallenge(self, element): + """ + Parse challenge and send response from the mechanism. + + @param element: the challenge protocol element. + @type element: L{domish.Element}. + """ + + try: + challenge = fromBase64(str(element)) + except SASLIncorrectEncodingError: + self._deferred.errback() + else: + self.sendResponse(self.mechanism.getResponse(challenge)) + + def onSuccess(self, success): + """ + Clean up observers, reset the XML stream and send a new header. + + @param success: the success protocol element. For now unused, but + could hold additional data. + @type success: L{domish.Element} + """ + + self.xmlstream.removeObserver("/challenge", self.onChallenge) + self.xmlstream.removeObserver("/failure", self.onFailure) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(xmlstream.Reset) + + def onFailure(self, failure): + """ + Clean up observers, parse the failure and errback the deferred. + + @param failure: the failure protocol element. Holds details on + the error condition. + @type failure: L{domish.Element} + """ + + self.xmlstream.removeObserver("/challenge", self.onChallenge) + self.xmlstream.removeObserver("/success", self.onSuccess) + try: + condition = failure.firstChildElement().name + except AttributeError: + condition = None + self._deferred.errback(SASLAuthError(condition)) diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl_mechanisms.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl_mechanisms.py new file mode 100644 index 0000000000..8d9c8fabcc --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl_mechanisms.py @@ -0,0 +1,307 @@ +# -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Protocol agnostic implementations of SASL authentication mechanisms. +""" + + +import binascii +import os +import random +import time +from hashlib import md5 + +from zope.interface import Attribute, Interface, implementer + +from twisted.python.compat import networkString + + +class ISASLMechanism(Interface): + name = Attribute("""Common name for the SASL Mechanism.""") + + def getInitialResponse(): + """ + Get the initial client response, if defined for this mechanism. + + @return: initial client response string. + @rtype: C{str}. + """ + + def getResponse(challenge): + """ + Get the response to a server challenge. + + @param challenge: server challenge. + @type challenge: C{str}. + @return: client response. + @rtype: C{str}. + """ + + +@implementer(ISASLMechanism) +class Anonymous: + """ + Implements the ANONYMOUS SASL authentication mechanism. + + This mechanism is defined in RFC 2245. + """ + + name = "ANONYMOUS" + + def getInitialResponse(self): + return None + + def getResponse(self, challenge): + # ISASLMechanism.getResponse + pass + + +@implementer(ISASLMechanism) +class Plain: + """ + Implements the PLAIN SASL authentication mechanism. + + The PLAIN SASL authentication mechanism is defined in RFC 2595. + """ + + name = "PLAIN" + + def __init__(self, authzid, authcid, password): + """ + @param authzid: The authorization identity. + @type authzid: L{unicode} + + @param authcid: The authentication identity. + @type authcid: L{unicode} + + @param password: The plain-text password. + @type password: L{unicode} + """ + + self.authzid = authzid or "" + self.authcid = authcid or "" + self.password = password or "" + + def getInitialResponse(self): + return ( + self.authzid.encode("utf-8") + + b"\x00" + + self.authcid.encode("utf-8") + + b"\x00" + + self.password.encode("utf-8") + ) + + def getResponse(self, challenge): + # ISASLMechanism.getResponse + pass + + +@implementer(ISASLMechanism) +class DigestMD5: + """ + Implements the DIGEST-MD5 SASL authentication mechanism. + + The DIGEST-MD5 SASL authentication mechanism is defined in RFC 2831. + """ + + name = "DIGEST-MD5" + + def __init__(self, serv_type, host, serv_name, username, password): + """ + @param serv_type: An indication of what kind of server authentication + is being attempted against. For example, C{u"xmpp"}. + @type serv_type: C{unicode} + + @param host: The authentication hostname. Also known as the realm. + This is used as a scope to help select the right credentials. + @type host: C{unicode} + + @param serv_name: An additional identifier for the server. + @type serv_name: C{unicode} + + @param username: The authentication username to use to respond to a + challenge. + @type username: C{unicode} + + @param password: The authentication password to use to respond to a + challenge. + @type password: C{unicode} + """ + self.username = username + self.password = password + self.defaultRealm = host + + self.digest_uri = f"{serv_type}/{host}" + if serv_name is not None: + self.digest_uri += f"/{serv_name}" + + def getInitialResponse(self): + return None + + def getResponse(self, challenge): + directives = self._parse(challenge) + + # Compat for implementations that do not send this along with + # a successful authentication. + if b"rspauth" in directives: + return b"" + + charset = directives[b"charset"].decode("ascii") + + try: + realm = directives[b"realm"] + except KeyError: + realm = self.defaultRealm.encode(charset) + + return self._genResponse(charset, realm, directives[b"nonce"]) + + def _parse(self, challenge): + """ + Parses the server challenge. + + Splits the challenge into a dictionary of directives with values. + + @return: challenge directives and their values. + @rtype: C{dict} of C{str} to C{str}. + """ + s = challenge + paramDict = {} + cur = 0 + remainingParams = True + while remainingParams: + # Parse a param. We can't just split on commas, because there can + # be some commas inside (quoted) param values, e.g.: + # qop="auth,auth-int" + + middle = s.index(b"=", cur) + name = s[cur:middle].lstrip() + middle += 1 + if s[middle : middle + 1] == b'"': + middle += 1 + end = s.index(b'"', middle) + value = s[middle:end] + cur = s.find(b",", end) + 1 + if cur == 0: + remainingParams = False + else: + end = s.find(b",", middle) + if end == -1: + value = s[middle:].rstrip() + remainingParams = False + else: + value = s[middle:end].rstrip() + cur = end + 1 + paramDict[name] = value + + for param in (b"qop", b"cipher"): + if param in paramDict: + paramDict[param] = paramDict[param].split(b",") + + return paramDict + + def _unparse(self, directives): + """ + Create message string from directives. + + @param directives: dictionary of directives (names to their values). + For certain directives, extra quotes are added, as + needed. + @type directives: C{dict} of C{str} to C{str} + @return: message string. + @rtype: C{str}. + """ + + directive_list = [] + for name, value in directives.items(): + if name in ( + b"username", + b"realm", + b"cnonce", + b"nonce", + b"digest-uri", + b"authzid", + b"cipher", + ): + directive = name + b"=" + value + else: + directive = name + b"=" + value + + directive_list.append(directive) + + return b",".join(directive_list) + + def _calculateResponse(self, cnonce, nc, nonce, username, password, realm, uri): + """ + Calculates response with given encoded parameters. + + @return: The I{response} field of a response to a Digest-MD5 challenge + of the given parameters. + @rtype: L{bytes} + """ + + def H(s): + return md5(s).digest() + + def HEX(n): + return binascii.b2a_hex(n) + + def KD(k, s): + return H(k + b":" + s) + + a1 = H(username + b":" + realm + b":" + password) + b":" + nonce + b":" + cnonce + a2 = b"AUTHENTICATE:" + uri + + response = HEX( + KD( + HEX(H(a1)), + nonce + b":" + nc + b":" + cnonce + b":" + b"auth" + b":" + HEX(H(a2)), + ) + ) + return response + + def _genResponse(self, charset, realm, nonce): + """ + Generate response-value. + + Creates a response to a challenge according to section 2.1.2.1 of + RFC 2831 using the C{charset}, C{realm} and C{nonce} directives + from the challenge. + """ + try: + username = self.username.encode(charset) + password = self.password.encode(charset) + digest_uri = self.digest_uri.encode(charset) + except UnicodeError: + # TODO - add error checking + raise + + nc = networkString(f"{1:08x}") # TODO: support subsequent auth. + cnonce = self._gen_nonce() + qop = b"auth" + + # TODO - add support for authzid + response = self._calculateResponse( + cnonce, nc, nonce, username, password, realm, digest_uri + ) + + directives = { + b"username": username, + b"realm": realm, + b"nonce": nonce, + b"cnonce": cnonce, + b"nc": nc, + b"qop": qop, + b"digest-uri": digest_uri, + b"response": response, + b"charset": charset.encode("ascii"), + } + + return self._unparse(directives) + + def _gen_nonce(self): + nonceString = "%f:%f:%d" % (random.random(), time.time(), os.getpid()) + nonceBytes = networkString(nonceString) + return md5(nonceBytes).hexdigest().encode("ascii") diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmlstream.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmlstream.py new file mode 100644 index 0000000000..601a879aa8 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmlstream.py @@ -0,0 +1,1145 @@ +# -*- test-case-name: twisted.words.test.test_jabberxmlstream -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +XMPP XML Streams + +Building blocks for setting up XML Streams, including helping classes for +doing authentication on either client or server side, and working with XML +Stanzas. + +@var STREAM_AUTHD_EVENT: Token dispatched by L{Authenticator} when the + stream has been completely initialized +@type STREAM_AUTHD_EVENT: L{str}. + +@var INIT_FAILED_EVENT: Token dispatched by L{Authenticator} when the + stream has failed to be initialized +@type INIT_FAILED_EVENT: L{str}. + +@var Reset: Token to signal that the XML stream has been reset. +@type Reset: Basic object. +""" + + +from binascii import hexlify +from hashlib import sha1 +from sys import intern +from typing import Optional, Tuple + +from zope.interface import directlyProvides, implementer + +from twisted.internet import defer, protocol +from twisted.internet.error import ConnectionLost +from twisted.python import failure, log, randbytes +from twisted.words.protocols.jabber import error, ijabber, jid +from twisted.words.xish import domish, xmlstream +from twisted.words.xish.xmlstream import ( + STREAM_CONNECTED_EVENT, + STREAM_END_EVENT, + STREAM_ERROR_EVENT, + STREAM_START_EVENT, +) + +try: + from twisted.internet import ssl as _ssl +except ImportError: + ssl = None +else: + if not _ssl.supported: + ssl = None + else: + ssl = _ssl + +STREAM_AUTHD_EVENT = intern("//event/stream/authd") +INIT_FAILED_EVENT = intern("//event/xmpp/initfailed") + +NS_STREAMS = "http://etherx.jabber.org/streams" +NS_XMPP_TLS = "urn:ietf:params:xml:ns:xmpp-tls" + +Reset = object() + + +def hashPassword(sid, password): + """ + Create a SHA1-digest string of a session identifier and password. + + @param sid: The stream session identifier. + @type sid: C{unicode}. + @param password: The password to be hashed. + @type password: C{unicode}. + """ + if not isinstance(sid, str): + raise TypeError("The session identifier must be a unicode object") + if not isinstance(password, str): + raise TypeError("The password must be a unicode object") + input = f"{sid}{password}" + return sha1(input.encode("utf-8")).hexdigest() + + +class Authenticator: + """ + Base class for business logic of initializing an XmlStream + + Subclass this object to enable an XmlStream to initialize and authenticate + to different types of stream hosts (such as clients, components, etc.). + + Rules: + 1. The Authenticator MUST dispatch a L{STREAM_AUTHD_EVENT} when the + stream has been completely initialized. + 2. The Authenticator SHOULD reset all state information when + L{associateWithStream} is called. + 3. The Authenticator SHOULD override L{streamStarted}, and start + initialization there. + + @type xmlstream: L{XmlStream} + @ivar xmlstream: The XmlStream that needs authentication + + @note: the term authenticator is historical. Authenticators perform + all steps required to prepare the stream for the exchange + of XML stanzas. + """ + + def __init__(self): + self.xmlstream = None + + def connectionMade(self): + """ + Called by the XmlStream when the underlying socket connection is + in place. + + This allows the Authenticator to send an initial root element, if it's + connecting, or wait for an inbound root from the peer if it's accepting + the connection. + + Subclasses can use self.xmlstream.send() to send any initial data to + the peer. + """ + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + A stream is considered to have started when the start tag of the root + element has been received. + + This examines C{rootElement} to see if there is a version attribute. + If absent, C{0.0} is assumed per RFC 3920. Subsequently, the + minimum of the version from the received stream header and the + value stored in L{xmlstream} is taken and put back in L{xmlstream}. + + Extensions of this method can extract more information from the + stream header and perform checks on them, optionally sending + stream errors and closing the stream. + """ + if rootElement.hasAttribute("version"): + version = rootElement["version"].split(".") + try: + version = (int(version[0]), int(version[1])) + except (IndexError, ValueError): + version = (0, 0) + else: + version = (0, 0) + + self.xmlstream.version = min(self.xmlstream.version, version) + + def associateWithStream(self, xmlstream): + """ + Called by the XmlStreamFactory when a connection has been made + to the requested peer, and an XmlStream object has been + instantiated. + + The default implementation just saves a handle to the new + XmlStream. + + @type xmlstream: L{XmlStream} + @param xmlstream: The XmlStream that will be passing events to this + Authenticator. + + """ + self.xmlstream = xmlstream + + +class ConnectAuthenticator(Authenticator): + """ + Authenticator for initiating entities. + """ + + namespace: Optional[str] = None + + def __init__(self, otherHost): + self.otherHost = otherHost + + def connectionMade(self): + self.xmlstream.namespace = self.namespace + self.xmlstream.otherEntity = jid.internJID(self.otherHost) + self.xmlstream.sendHeader() + + def initializeStream(self): + """ + Perform stream initialization procedures. + + An L{XmlStream} holds a list of initializer objects in its + C{initializers} attribute. This method calls these initializers in + order and dispatches the L{STREAM_AUTHD_EVENT} event when the list has + been successfully processed. Otherwise it dispatches the + C{INIT_FAILED_EVENT} event with the failure. + + Initializers may return the special L{Reset} object to halt the + initialization processing. It signals that the current initializer was + successfully processed, but that the XML Stream has been reset. An + example is the TLSInitiatingInitializer. + """ + + def remove_first(result): + self.xmlstream.initializers.pop(0) + + return result + + def do_next(result): + """ + Take the first initializer and process it. + + On success, the initializer is removed from the list and + then next initializer will be tried. + """ + + if result is Reset: + return None + + try: + init = self.xmlstream.initializers[0] + except IndexError: + self.xmlstream.dispatch(self.xmlstream, STREAM_AUTHD_EVENT) + return None + else: + d = defer.maybeDeferred(init.initialize) + d.addCallback(remove_first) + d.addCallback(do_next) + return d + + d = defer.succeed(None) + d.addCallback(do_next) + d.addErrback(self.xmlstream.dispatch, INIT_FAILED_EVENT) + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + This extends L{Authenticator.streamStarted} to extract further stream + headers from C{rootElement}, optionally wait for stream features being + received and then call C{initializeStream}. + """ + + Authenticator.streamStarted(self, rootElement) + + self.xmlstream.sid = rootElement.getAttribute("id") + + if rootElement.hasAttribute("from"): + self.xmlstream.otherEntity = jid.internJID(rootElement["from"]) + + # Setup observer for stream features, if applicable + if self.xmlstream.version >= (1, 0): + + def onFeatures(element): + features = {} + for feature in element.elements(): + features[(feature.uri, feature.name)] = feature + + self.xmlstream.features = features + self.initializeStream() + + self.xmlstream.addOnetimeObserver( + '/features[@xmlns="%s"]' % NS_STREAMS, onFeatures + ) + else: + self.initializeStream() + + +class ListenAuthenticator(Authenticator): + """ + Authenticator for receiving entities. + """ + + namespace: Optional[str] = None + + def associateWithStream(self, xmlstream): + """ + Called by the XmlStreamFactory when a connection has been made. + + Extend L{Authenticator.associateWithStream} to set the L{XmlStream} + to be non-initiating. + """ + Authenticator.associateWithStream(self, xmlstream) + self.xmlstream.initiating = False + + def streamStarted(self, rootElement): + """ + Called by the XmlStream when the stream has started. + + This extends L{Authenticator.streamStarted} to extract further + information from the stream headers from C{rootElement}. + """ + Authenticator.streamStarted(self, rootElement) + + self.xmlstream.namespace = rootElement.defaultUri + + if rootElement.hasAttribute("to"): + self.xmlstream.thisEntity = jid.internJID(rootElement["to"]) + + self.xmlstream.prefixes = {} + for prefix, uri in rootElement.localPrefixes.items(): + self.xmlstream.prefixes[uri] = prefix + + self.xmlstream.sid = hexlify(randbytes.secureRandom(8)).decode("ascii") + + +class FeatureNotAdvertized(Exception): + """ + Exception indicating a stream feature was not advertized, while required by + the initiating entity. + """ + + +@implementer(ijabber.IInitiatingInitializer) +class BaseFeatureInitiatingInitializer: + """ + Base class for initializers with a stream feature. + + This assumes the associated XmlStream represents the initiating entity + of the connection. + + @cvar feature: tuple of (uri, name) of the stream feature root element. + @type feature: tuple of (C{str}, C{str}) + + @ivar required: whether the stream feature is required to be advertized + by the receiving entity. + @type required: C{bool} + """ + + feature: Optional[Tuple[str, str]] = None + + def __init__(self, xs, required=False): + self.xmlstream = xs + self.required = required + + def initialize(self): + """ + Initiate the initialization. + + Checks if the receiving entity advertizes the stream feature. If it + does, the initialization is started. If it is not advertized, and the + C{required} instance variable is C{True}, it raises + L{FeatureNotAdvertized}. Otherwise, the initialization silently + succeeds. + """ + + if self.feature in self.xmlstream.features: + return self.start() + elif self.required: + raise FeatureNotAdvertized + else: + return None + + def start(self): + """ + Start the actual initialization. + + May return a deferred for asynchronous initialization. + """ + + +class TLSError(Exception): + """ + TLS base exception. + """ + + +class TLSFailed(TLSError): + """ + Exception indicating failed TLS negotiation + """ + + +class TLSRequired(TLSError): + """ + Exception indicating required TLS negotiation. + + This exception is raised when the receiving entity requires TLS + negotiation and the initiating does not desire to negotiate TLS. + """ + + +class TLSNotSupported(TLSError): + """ + Exception indicating missing TLS support. + + This exception is raised when the initiating entity wants and requires to + negotiate TLS when the OpenSSL library is not available. + """ + + +class TLSInitiatingInitializer(BaseFeatureInitiatingInitializer): + """ + TLS stream initializer for the initiating entity. + + It is strongly required to include this initializer in the list of + initializers for an XMPP stream. By default it will try to negotiate TLS. + An XMPP server may indicate that TLS is required. If TLS is not desired, + set the C{wanted} attribute to False instead of removing it from the list + of initializers, so a proper exception L{TLSRequired} can be raised. + + @ivar wanted: indicates if TLS negotiation is wanted. + @type wanted: C{bool} + """ + + feature = (NS_XMPP_TLS, "starttls") + wanted = True + _deferred = None + _configurationForTLS = None + + def __init__(self, xs, required=True, configurationForTLS=None): + """ + @param configurationForTLS: An object which creates appropriately + configured TLS connections. This is passed to C{startTLS} on the + transport and is preferably created using + L{twisted.internet.ssl.optionsForClientTLS}. If C{None}, the + default is to verify the server certificate against the trust roots + as provided by the platform. See + L{twisted.internet._sslverify.platformTrust}. + @type configurationForTLS: L{IOpenSSLClientConnectionCreator} or + C{None} + """ + super().__init__(xs, required=required) + self._configurationForTLS = configurationForTLS + + def onProceed(self, obj): + """ + Proceed with TLS negotiation and reset the XML stream. + """ + + self.xmlstream.removeObserver("/failure", self.onFailure) + if self._configurationForTLS: + ctx = self._configurationForTLS + else: + ctx = ssl.optionsForClientTLS(self.xmlstream.otherEntity.host) + self.xmlstream.transport.startTLS(ctx) + self.xmlstream.reset() + self.xmlstream.sendHeader() + self._deferred.callback(Reset) + + def onFailure(self, obj): + self.xmlstream.removeObserver("/proceed", self.onProceed) + self._deferred.errback(TLSFailed()) + + def start(self): + """ + Start TLS negotiation. + + This checks if the receiving entity requires TLS, the SSL library is + available and uses the C{required} and C{wanted} instance variables to + determine what to do in the various different cases. + + For example, if the SSL library is not available, and wanted and + required by the user, it raises an exception. However if it is not + required by both parties, initialization silently succeeds, moving + on to the next step. + """ + if self.wanted: + if ssl is None: + if self.required: + return defer.fail(TLSNotSupported()) + else: + return defer.succeed(None) + else: + pass + elif self.xmlstream.features[self.feature].required: + return defer.fail(TLSRequired()) + else: + return defer.succeed(None) + + self._deferred = defer.Deferred() + self.xmlstream.addOnetimeObserver("/proceed", self.onProceed) + self.xmlstream.addOnetimeObserver("/failure", self.onFailure) + self.xmlstream.send(domish.Element((NS_XMPP_TLS, "starttls"))) + return self._deferred + + +class XmlStream(xmlstream.XmlStream): + """ + XMPP XML Stream protocol handler. + + @ivar version: XML stream version as a tuple (major, minor). Initially, + this is set to the minimally supported version. Upon + receiving the stream header of the peer, it is set to the + minimum of that value and the version on the received + header. + @type version: (C{int}, C{int}) + @ivar namespace: default namespace URI for stream + @type namespace: C{unicode} + @ivar thisEntity: JID of this entity + @type thisEntity: L{JID} + @ivar otherEntity: JID of the peer entity + @type otherEntity: L{JID} + @ivar sid: session identifier + @type sid: C{unicode} + @ivar initiating: True if this is the initiating stream + @type initiating: C{bool} + @ivar features: map of (uri, name) to stream features element received from + the receiving entity. + @type features: C{dict} of (C{unicode}, C{unicode}) to L{domish.Element}. + @ivar prefixes: map of URI to prefixes that are to appear on stream + header. + @type prefixes: C{dict} of C{unicode} to C{unicode} + @ivar initializers: list of stream initializer objects + @type initializers: C{list} of objects that provide L{IInitializer} + @ivar authenticator: associated authenticator that uses C{initializers} to + initialize the XML stream. + """ + + version = (1, 0) + namespace = "invalid" + thisEntity = None + otherEntity = None + sid = None + initiating = True + + _headerSent = False # True if the stream header has been sent + + def __init__(self, authenticator): + xmlstream.XmlStream.__init__(self) + + self.prefixes = {NS_STREAMS: "stream"} + self.authenticator = authenticator + self.initializers = [] + self.features = {} + + # Reset the authenticator + authenticator.associateWithStream(self) + + def _callLater(self, *args, **kwargs): + from twisted.internet import reactor + + return reactor.callLater(*args, **kwargs) + + def reset(self): + """ + Reset XML Stream. + + Resets the XML Parser for incoming data. This is to be used after + successfully negotiating a new layer, e.g. TLS and SASL. Note that + registered event observers will continue to be in place. + """ + self._headerSent = False + self._initializeStream() + + def onStreamError(self, errelem): + """ + Called when a stream:error element has been received. + + Dispatches a L{STREAM_ERROR_EVENT} event with the error element to + allow for cleanup actions and drops the connection. + + @param errelem: The received error element. + @type errelem: L{domish.Element} + """ + self.dispatch( + failure.Failure(error.exceptionFromStreamError(errelem)), STREAM_ERROR_EVENT + ) + self.transport.loseConnection() + + def sendHeader(self): + """ + Send stream header. + """ + # set up optional extra namespaces + localPrefixes = {} + for uri, prefix in self.prefixes.items(): + if uri != NS_STREAMS: + localPrefixes[prefix] = uri + + rootElement = domish.Element( + (NS_STREAMS, "stream"), self.namespace, localPrefixes=localPrefixes + ) + + if self.otherEntity: + rootElement["to"] = self.otherEntity.userhost() + + if self.thisEntity: + rootElement["from"] = self.thisEntity.userhost() + + if not self.initiating and self.sid: + rootElement["id"] = self.sid + + if self.version >= (1, 0): + rootElement["version"] = "%d.%d" % self.version + + self.send(rootElement.toXml(prefixes=self.prefixes, closeElement=0)) + self._headerSent = True + + def sendFooter(self): + """ + Send stream footer. + """ + self.send("</stream:stream>") + + def sendStreamError(self, streamError): + """ + Send stream level error. + + If we are the receiving entity, and haven't sent the header yet, + we sent one first. + + After sending the stream error, the stream is closed and the transport + connection dropped. + + @param streamError: stream error instance + @type streamError: L{error.StreamError} + """ + if not self._headerSent and not self.initiating: + self.sendHeader() + + if self._headerSent: + self.send(streamError.getElement()) + self.sendFooter() + + self.transport.loseConnection() + + def send(self, obj): + """ + Send data over the stream. + + This overrides L{xmlstream.XmlStream.send} to use the default namespace + of the stream header when serializing L{domish.IElement}s. It is + assumed that if you pass an object that provides L{domish.IElement}, + it represents a direct child of the stream's root element. + """ + if domish.IElement.providedBy(obj): + obj = obj.toXml( + prefixes=self.prefixes, + defaultUri=self.namespace, + prefixesInScope=list(self.prefixes.values()), + ) + + xmlstream.XmlStream.send(self, obj) + + def connectionMade(self): + """ + Called when a connection is made. + + Notifies the authenticator when a connection has been made. + """ + xmlstream.XmlStream.connectionMade(self) + self.authenticator.connectionMade() + + def onDocumentStart(self, rootElement): + """ + Called when the stream header has been received. + + Extracts the header's C{id} and C{version} attributes from the root + element. The C{id} attribute is stored in our C{sid} attribute and the + C{version} attribute is parsed and the minimum of the version we sent + and the parsed C{version} attribute is stored as a tuple (major, minor) + in this class' C{version} attribute. If no C{version} attribute was + present, we assume version 0.0. + + If appropriate (we are the initiating stream and the minimum of our and + the other party's version is at least 1.0), a one-time observer is + registered for getting the stream features. The registered function is + C{onFeatures}. + + Ultimately, the authenticator's C{streamStarted} method will be called. + + @param rootElement: The root element. + @type rootElement: L{domish.Element} + """ + xmlstream.XmlStream.onDocumentStart(self, rootElement) + + # Setup observer for stream errors + self.addOnetimeObserver("/error[@xmlns='%s']" % NS_STREAMS, self.onStreamError) + + self.authenticator.streamStarted(rootElement) + + +class XmlStreamFactory(xmlstream.XmlStreamFactory): + """ + Factory for Jabber XmlStream objects as a reconnecting client. + + Note that this differs from L{xmlstream.XmlStreamFactory} in that + it generates Jabber specific L{XmlStream} instances that have + authenticators. + """ + + protocol = XmlStream + + def __init__(self, authenticator): + xmlstream.XmlStreamFactory.__init__(self, authenticator) + self.authenticator = authenticator + + +class XmlStreamServerFactory(xmlstream.BootstrapMixin, protocol.ServerFactory): + """ + Factory for Jabber XmlStream objects as a server. + + @since: 8.2. + @ivar authenticatorFactory: Factory callable that takes no arguments, to + create a fresh authenticator to be associated + with the XmlStream. + """ + + # Type is wrong. See: https://twistedmatrix.com/trac/ticket/10007#ticket + protocol = XmlStream # type: ignore[assignment] + + def __init__(self, authenticatorFactory): + xmlstream.BootstrapMixin.__init__(self) + self.authenticatorFactory = authenticatorFactory + + def buildProtocol(self, addr): + """ + Create an instance of XmlStream. + + A new authenticator instance will be created and passed to the new + XmlStream. Registered bootstrap event observers are installed as well. + """ + authenticator = self.authenticatorFactory() + xs = self.protocol(authenticator) + xs.factory = self + self.installBootstraps(xs) + return xs + + +class TimeoutError(Exception): + """ + Exception raised when no IQ response has been received before the + configured timeout. + """ + + +def upgradeWithIQResponseTracker(xs): + """ + Enhances an XmlStream for iq response tracking. + + This makes an L{XmlStream} object provide L{IIQResponseTracker}. When a + response is an error iq stanza, the deferred has its errback invoked with a + failure that holds a L{StanzaError<error.StanzaError>} that is + easier to examine. + """ + + def callback(iq): + """ + Handle iq response by firing associated deferred. + """ + if getattr(iq, "handled", False): + return + + try: + d = xs.iqDeferreds[iq["id"]] + except KeyError: + pass + else: + del xs.iqDeferreds[iq["id"]] + iq.handled = True + if iq["type"] == "error": + d.errback(error.exceptionFromStanza(iq)) + else: + d.callback(iq) + + def disconnected(_): + """ + Make sure deferreds do not linger on after disconnect. + + This errbacks all deferreds of iq's for which no response has been + received with a L{ConnectionLost} failure. Otherwise, the deferreds + will never be fired. + """ + iqDeferreds = xs.iqDeferreds + xs.iqDeferreds = {} + for d in iqDeferreds.values(): + d.errback(ConnectionLost()) + + xs.iqDeferreds = {} + xs.iqDefaultTimeout = getattr(xs, "iqDefaultTimeout", None) + xs.addObserver(xmlstream.STREAM_END_EVENT, disconnected) + xs.addObserver('/iq[@type="result"]', callback) + xs.addObserver('/iq[@type="error"]', callback) + directlyProvides(xs, ijabber.IIQResponseTracker) + + +class IQ(domish.Element): + """ + Wrapper for an iq stanza. + + Iq stanzas are used for communications with a request-response behaviour. + Each iq request is associated with an XML stream and has its own unique id + to be able to track the response. + + @ivar timeout: if set, a timeout period after which the deferred returned + by C{send} will have its errback called with a + L{TimeoutError} failure. + @type timeout: C{float} + """ + + timeout = None + + def __init__(self, xmlstream, stanzaType="set"): + """ + @type xmlstream: L{xmlstream.XmlStream} + @param xmlstream: XmlStream to use for transmission of this IQ + + @type stanzaType: C{str} + @param stanzaType: IQ type identifier ('get' or 'set') + """ + domish.Element.__init__(self, (None, "iq")) + self.addUniqueId() + self["type"] = stanzaType + self._xmlstream = xmlstream + + def send(self, to=None): + """ + Send out this iq. + + Returns a deferred that is fired when an iq response with the same id + is received. Result responses will be passed to the deferred callback. + Error responses will be transformed into a + L{StanzaError<error.StanzaError>} and result in the errback of the + deferred being invoked. + + @rtype: L{defer.Deferred} + """ + if to is not None: + self["to"] = to + + if not ijabber.IIQResponseTracker.providedBy(self._xmlstream): + upgradeWithIQResponseTracker(self._xmlstream) + + d = defer.Deferred() + self._xmlstream.iqDeferreds[self["id"]] = d + + timeout = self.timeout or self._xmlstream.iqDefaultTimeout + if timeout is not None: + + def onTimeout(): + del self._xmlstream.iqDeferreds[self["id"]] + d.errback(TimeoutError("IQ timed out")) + + call = self._xmlstream._callLater(timeout, onTimeout) + + def cancelTimeout(result): + if call.active(): + call.cancel() + + return result + + d.addBoth(cancelTimeout) + + self._xmlstream.send(self) + return d + + +def toResponse(stanza, stanzaType=None): + """ + Create a response stanza from another stanza. + + This takes the addressing and id attributes from a stanza to create a (new, + empty) response stanza. The addressing attributes are swapped and the id + copied. Optionally, the stanza type of the response can be specified. + + @param stanza: the original stanza + @type stanza: L{domish.Element} + @param stanzaType: optional response stanza type + @type stanzaType: C{str} + @return: the response stanza. + @rtype: L{domish.Element} + """ + + toAddr = stanza.getAttribute("from") + fromAddr = stanza.getAttribute("to") + stanzaID = stanza.getAttribute("id") + + response = domish.Element((None, stanza.name)) + if toAddr: + response["to"] = toAddr + if fromAddr: + response["from"] = fromAddr + if stanzaID: + response["id"] = stanzaID + if stanzaType: + response["type"] = stanzaType + + return response + + +@implementer(ijabber.IXMPPHandler) +class XMPPHandler: + """ + XMPP protocol handler. + + Classes derived from this class implement (part of) one or more XMPP + extension protocols, and are referred to as a subprotocol implementation. + """ + + def __init__(self): + self.parent = None + self.xmlstream = None + + def setHandlerParent(self, parent): + self.parent = parent + self.parent.addHandler(self) + + def disownHandlerParent(self, parent): + self.parent.removeHandler(self) + self.parent = None + + def makeConnection(self, xs): + self.xmlstream = xs + self.connectionMade() + + def connectionMade(self): + """ + Called after a connection has been established. + + Can be overridden to perform work before stream initialization. + """ + + def connectionInitialized(self): + """ + The XML stream has been initialized. + + Can be overridden to perform work after stream initialization, e.g. to + set up observers and start exchanging XML stanzas. + """ + + def connectionLost(self, reason): + """ + The XML stream has been closed. + + This method can be extended to inspect the C{reason} argument and + act on it. + """ + self.xmlstream = None + + def send(self, obj): + """ + Send data over the managed XML stream. + + @note: The stream manager maintains a queue for data sent using this + method when there is no current initialized XML stream. This + data is then sent as soon as a new stream has been established + and initialized. Subsequently, L{connectionInitialized} will be + called again. If this queueing is not desired, use C{send} on + C{self.xmlstream}. + + @param obj: data to be sent over the XML stream. This is usually an + object providing L{domish.IElement}, or serialized XML. See + L{xmlstream.XmlStream} for details. + """ + self.parent.send(obj) + + +@implementer(ijabber.IXMPPHandlerCollection) +class XMPPHandlerCollection: + """ + Collection of XMPP subprotocol handlers. + + This allows for grouping of subprotocol handlers, but is not an + L{XMPPHandler} itself, so this is not recursive. + + @ivar handlers: List of protocol handlers. + @type handlers: C{list} of objects providing + L{IXMPPHandler} + """ + + def __init__(self): + self.handlers = [] + + def __iter__(self): + """ + Act as a container for handlers. + """ + return iter(self.handlers) + + def addHandler(self, handler): + """ + Add protocol handler. + + Protocol handlers are expected to provide L{ijabber.IXMPPHandler}. + """ + self.handlers.append(handler) + + def removeHandler(self, handler): + """ + Remove protocol handler. + """ + self.handlers.remove(handler) + + +class StreamManager(XMPPHandlerCollection): + """ + Business logic representing a managed XMPP connection. + + This maintains a single XMPP connection and provides facilities for packet + routing and transmission. Business logic modules are objects providing + L{ijabber.IXMPPHandler} (like subclasses of L{XMPPHandler}), and added + using L{addHandler}. + + @ivar xmlstream: currently managed XML stream + @type xmlstream: L{XmlStream} + @ivar logTraffic: if true, log all traffic. + @type logTraffic: C{bool} + @ivar _initialized: Whether the stream represented by L{xmlstream} has + been initialized. This is used when caching outgoing + stanzas. + @type _initialized: C{bool} + @ivar _packetQueue: internal buffer of unsent data. See L{send} for details. + @type _packetQueue: C{list} + """ + + logTraffic = False + + def __init__(self, factory): + XMPPHandlerCollection.__init__(self) + self.xmlstream = None + self._packetQueue = [] + self._initialized = False + + factory.addBootstrap(STREAM_CONNECTED_EVENT, self._connected) + factory.addBootstrap(STREAM_AUTHD_EVENT, self._authd) + factory.addBootstrap(INIT_FAILED_EVENT, self.initializationFailed) + factory.addBootstrap(STREAM_END_EVENT, self._disconnected) + self.factory = factory + + def addHandler(self, handler): + """ + Add protocol handler. + + When an XML stream has already been established, the handler's + C{connectionInitialized} will be called to get it up to speed. + """ + XMPPHandlerCollection.addHandler(self, handler) + + # get protocol handler up to speed when a connection has already + # been established + if self.xmlstream and self._initialized: + handler.makeConnection(self.xmlstream) + handler.connectionInitialized() + + def _connected(self, xs): + """ + Called when the transport connection has been established. + + Here we optionally set up traffic logging (depending on L{logTraffic}) + and call each handler's C{makeConnection} method with the L{XmlStream} + instance. + """ + + def logDataIn(buf): + log.msg("RECV: %r" % buf) + + def logDataOut(buf): + log.msg("SEND: %r" % buf) + + if self.logTraffic: + xs.rawDataInFn = logDataIn + xs.rawDataOutFn = logDataOut + + self.xmlstream = xs + + for e in self: + e.makeConnection(xs) + + def _authd(self, xs): + """ + Called when the stream has been initialized. + + Send out cached stanzas and call each handler's + C{connectionInitialized} method. + """ + # Flush all pending packets + for p in self._packetQueue: + xs.send(p) + self._packetQueue = [] + self._initialized = True + + # Notify all child services which implement + # the IService interface + for e in self: + e.connectionInitialized() + + def initializationFailed(self, reason): + """ + Called when stream initialization has failed. + + Stream initialization has halted, with the reason indicated by + C{reason}. It may be retried by calling the authenticator's + C{initializeStream}. See the respective authenticators for details. + + @param reason: A failure instance indicating why stream initialization + failed. + @type reason: L{failure.Failure} + """ + + def _disconnected(self, reason): + """ + Called when the stream has been closed. + + From this point on, the manager doesn't interact with the + L{XmlStream} anymore and notifies each handler that the connection + was lost by calling its C{connectionLost} method. + """ + self.xmlstream = None + self._initialized = False + + # Notify all child services which implement + # the IService interface + for e in self: + e.connectionLost(reason) + + def send(self, obj): + """ + Send data over the XML stream. + + When there is no established XML stream, the data is queued and sent + out when a new XML stream has been established and initialized. + + @param obj: data to be sent over the XML stream. See + L{xmlstream.XmlStream.send} for details. + """ + if self._initialized: + self.xmlstream.send(obj) + else: + self._packetQueue.append(obj) + + +__all__ = [ + "Authenticator", + "BaseFeatureInitiatingInitializer", + "ConnectAuthenticator", + "FeatureNotAdvertized", + "INIT_FAILED_EVENT", + "IQ", + "ListenAuthenticator", + "NS_STREAMS", + "NS_XMPP_TLS", + "Reset", + "STREAM_AUTHD_EVENT", + "STREAM_CONNECTED_EVENT", + "STREAM_END_EVENT", + "STREAM_ERROR_EVENT", + "STREAM_START_EVENT", + "StreamManager", + "TLSError", + "TLSFailed", + "TLSInitiatingInitializer", + "TLSNotSupported", + "TLSRequired", + "TimeoutError", + "XMPPHandler", + "XMPPHandlerCollection", + "XmlStream", + "XmlStreamFactory", + "XmlStreamServerFactory", + "hashPassword", + "toResponse", + "upgradeWithIQResponseTracker", +] diff --git a/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmpp_stringprep.py b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmpp_stringprep.py new file mode 100644 index 0000000000..4ffafa7060 --- /dev/null +++ b/contrib/python/Twisted/py3/twisted/words/protocols/jabber/xmpp_stringprep.py @@ -0,0 +1,257 @@ +# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +import stringprep +from encodings import idna +from itertools import chain + +# We require Unicode version 3.2. +from unicodedata import ucd_3_2_0 as unicodedata + +from zope.interface import Interface, implementer + +from incremental import Version + +from twisted.python.deprecate import deprecatedModuleAttribute + +crippled = False +deprecatedModuleAttribute( + Version("Twisted", 13, 1, 0), "crippled is always False", __name__, "crippled" +) + + +class ILookupTable(Interface): + """ + Interface for character lookup classes. + """ + + def lookup(c): + """ + Return whether character is in this table. + """ + + +class IMappingTable(Interface): + """ + Interface for character mapping classes. + """ + + def map(c): + """ + Return mapping for character. + """ + + +@implementer(ILookupTable) +class LookupTableFromFunction: + def __init__(self, in_table_function): + self.lookup = in_table_function + + +@implementer(ILookupTable) +class LookupTable: + def __init__(self, table): + self._table = table + + def lookup(self, c): + return c in self._table + + +@implementer(IMappingTable) +class MappingTableFromFunction: + def __init__(self, map_table_function): + self.map = map_table_function + + +@implementer(IMappingTable) +class EmptyMappingTable: + def __init__(self, in_table_function): + self._in_table_function = in_table_function + + def map(self, c): + if self._in_table_function(c): + return None + else: + return c + + +class Profile: + def __init__( + self, + mappings=[], + normalize=True, + prohibiteds=[], + check_unassigneds=True, + check_bidi=True, + ): + self.mappings = mappings + self.normalize = normalize + self.prohibiteds = prohibiteds + self.do_check_unassigneds = check_unassigneds + self.do_check_bidi = check_bidi + + def prepare(self, string): + result = self.map(string) + if self.normalize: + result = unicodedata.normalize("NFKC", result) + self.check_prohibiteds(result) + if self.do_check_unassigneds: + self.check_unassigneds(result) + if self.do_check_bidi: + self.check_bidirectionals(result) + return result + + def map(self, string): + result = [] + + for c in string: + result_c = c + + for mapping in self.mappings: + result_c = mapping.map(c) + if result_c != c: + break + + if result_c is not None: + result.append(result_c) + + return "".join(result) + + def check_prohibiteds(self, string): + for c in string: + for table in self.prohibiteds: + if table.lookup(c): + raise UnicodeError("Invalid character %s" % repr(c)) + + def check_unassigneds(self, string): + for c in string: + if stringprep.in_table_a1(c): + raise UnicodeError("Unassigned code point %s" % repr(c)) + + def check_bidirectionals(self, string): + found_LCat = False + found_RandALCat = False + + for c in string: + if stringprep.in_table_d1(c): + found_RandALCat = True + if stringprep.in_table_d2(c): + found_LCat = True + + if found_LCat and found_RandALCat: + raise UnicodeError("Violation of BIDI Requirement 2") + + if found_RandALCat and not ( + stringprep.in_table_d1(string[0]) and stringprep.in_table_d1(string[-1]) + ): + raise UnicodeError("Violation of BIDI Requirement 3") + + +class NamePrep: + """Implements preparation of internationalized domain names. + + This class implements preparing internationalized domain names using the + rules defined in RFC 3491, section 4 (Conversion operations). + + We do not perform step 4 since we deal with unicode representations of + domain names and do not convert from or to ASCII representations using + punycode encoding. When such a conversion is needed, the C{idna} standard + library provides the C{ToUnicode()} and C{ToASCII()} functions. Note that + C{idna} itself assumes UseSTD3ASCIIRules to be false. + + The following steps are performed by C{prepare()}: + + - Split the domain name in labels at the dots (RFC 3490, 3.1) + - Apply nameprep proper on each label (RFC 3491) + - Enforce the restrictions on ASCII characters in host names by + assuming STD3ASCIIRules to be true. (STD 3) + - Rejoin the labels using the label separator U+002E (full stop). + + """ + + # Prohibited characters. + prohibiteds = [ + chr(n) + for n in chain( + range(0x00, 0x2C + 1), + range(0x2E, 0x2F + 1), + range(0x3A, 0x40 + 1), + range(0x5B, 0x60 + 1), + range(0x7B, 0x7F + 1), + ) + ] + + def prepare(self, string): + result = [] + + labels = idna.dots.split(string) + + if labels and len(labels[-1]) == 0: + trailing_dot = "." + del labels[-1] + else: + trailing_dot = "" + + for label in labels: + result.append(self.nameprep(label)) + + return ".".join(result) + trailing_dot + + def check_prohibiteds(self, string): + for c in string: + if c in self.prohibiteds: + raise UnicodeError("Invalid character %s" % repr(c)) + + def nameprep(self, label): + label = idna.nameprep(label) + self.check_prohibiteds(label) + if label[0] == "-": + raise UnicodeError("Invalid leading hyphen-minus") + if label[-1] == "-": + raise UnicodeError("Invalid trailing hyphen-minus") + return label + + +C_11 = LookupTableFromFunction(stringprep.in_table_c11) +C_12 = LookupTableFromFunction(stringprep.in_table_c12) +C_21 = LookupTableFromFunction(stringprep.in_table_c21) +C_22 = LookupTableFromFunction(stringprep.in_table_c22) +C_3 = LookupTableFromFunction(stringprep.in_table_c3) +C_4 = LookupTableFromFunction(stringprep.in_table_c4) +C_5 = LookupTableFromFunction(stringprep.in_table_c5) +C_6 = LookupTableFromFunction(stringprep.in_table_c6) +C_7 = LookupTableFromFunction(stringprep.in_table_c7) +C_8 = LookupTableFromFunction(stringprep.in_table_c8) +C_9 = LookupTableFromFunction(stringprep.in_table_c9) + +B_1 = EmptyMappingTable(stringprep.in_table_b1) +B_2 = MappingTableFromFunction(stringprep.map_table_b2) + +nodeprep = Profile( + mappings=[B_1, B_2], + prohibiteds=[ + C_11, + C_12, + C_21, + C_22, + C_3, + C_4, + C_5, + C_6, + C_7, + C_8, + C_9, + LookupTable(['"', "&", "'", "/", ":", "<", ">", "@"]), + ], +) + +resourceprep = Profile( + mappings=[ + B_1, + ], + prohibiteds=[C_12, C_21, C_22, C_3, C_4, C_5, C_6, C_7, C_8, C_9], +) + +nameprep = NamePrep() |