Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Chat protocols.
|
||||
"""
|
||||
4074
venv/lib/python3.9/site-packages/twisted/words/protocols/irc.py
Normal file
4074
venv/lib/python3.9/site-packages/twisted/words/protocols/irc.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,8 @@
|
|||
# -*- test-case-name: twisted.words.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Twisted Jabber: Jabber Protocol Helpers
|
||||
"""
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
# -*- test-case-name: twisted.words.test.test_jabberclient -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from twisted.python.compat import _coercedUnicode, unicode
|
||||
from twisted.words.protocols.jabber import xmlstream, sasl, error
|
||||
from twisted.words.protocols.jabber.jid import JID
|
||||
from twisted.words.xish import domish, xpath, utility
|
||||
|
||||
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(object):
|
||||
"""
|
||||
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 = _coercedUnicode(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=unicode(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(object):
|
||||
"""
|
||||
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(unicode(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 L{jid} and
|
||||
L{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 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}
|
||||
|
||||
@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),
|
||||
]
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
# -*- 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.python.compat import _coercedUnicode, unicode
|
||||
from twisted.words.xish import domish
|
||||
from twisted.words.protocols.jabber import error, ijabber, jstrports, xmlstream
|
||||
from twisted.words.protocols.jabber.jid import internJID as JID
|
||||
|
||||
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(object):
|
||||
"""
|
||||
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,
|
||||
_coercedUnicode(xs.authenticator.password))
|
||||
hs.addContent(unicode(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(unicode(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,
|
||||
unicode(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(object):
|
||||
"""
|
||||
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("Routing to %s: %r" % (destination.full(), stanza.toXml()))
|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
# -*- test-case-name: twisted.words.test.test_jabbererror -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
XMPP Error support.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import copy
|
||||
|
||||
from twisted.python.compat import unicode
|
||||
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 = 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):
|
||||
message = "%s with condition %r" % (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 = unicode(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 = unicode(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
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
# 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).
|
||||
"""
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# -*- 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 twisted.python.compat import _PY3, unicode
|
||||
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep, resourceprep, nameprep
|
||||
|
||||
class InvalidFormat(Exception):
|
||||
"""
|
||||
The given string could not be parsed into a valid Jabber Identifier (JID).
|
||||
"""
|
||||
|
||||
def parse(jidstring):
|
||||
"""
|
||||
Parse given JID string into its respective parts and apply stringprep.
|
||||
|
||||
@param jidstring: string representation of a JID.
|
||||
@type jidstring: L{unicode}
|
||||
@return: tuple of (user, host, resource), each of type L{unicode} 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, host, resource):
|
||||
"""
|
||||
Perform stringprep on all JID fragments.
|
||||
|
||||
@param user: The user part of the JID.
|
||||
@type user: L{unicode}
|
||||
@param host: The host part of the JID.
|
||||
@type host: L{unicode}
|
||||
@param resource: The resource part of the JID.
|
||||
@type resource: L{unicode}
|
||||
@return: The given parts with stringprep applied.
|
||||
@rtype: L{tuple}
|
||||
"""
|
||||
|
||||
if user:
|
||||
try:
|
||||
user = nodeprep.prepare(unicode(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(unicode(host))
|
||||
except UnicodeError:
|
||||
raise InvalidFormat("Invalid character in hostname")
|
||||
|
||||
if resource:
|
||||
try:
|
||||
resource = resourceprep.prepare(unicode(resource))
|
||||
except UnicodeError:
|
||||
raise InvalidFormat("Invalid character in resource")
|
||||
else:
|
||||
resource = None
|
||||
|
||||
return (user, host, resource)
|
||||
|
||||
__internJIDs = {}
|
||||
|
||||
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(object):
|
||||
"""
|
||||
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=None, tuple=None):
|
||||
if not (str or tuple):
|
||||
raise RuntimeError("You must provide a value for either 'str' or "
|
||||
"'tuple' arguments.")
|
||||
|
||||
if str:
|
||||
user, host, res = parse(str)
|
||||
else:
|
||||
user, host, res = prep(*tuple)
|
||||
|
||||
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{unicode}
|
||||
"""
|
||||
if self.user:
|
||||
return u"%s@%s" % (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{unicode}
|
||||
"""
|
||||
if self.user:
|
||||
if self.resource:
|
||||
return u"%s@%s/%s" % (self.user, self.host, self.resource)
|
||||
else:
|
||||
return u"%s@%s" % (self.user, self.host)
|
||||
else:
|
||||
if self.resource:
|
||||
return u"%s/%s" % (self.host, self.resource)
|
||||
else:
|
||||
return self.host
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
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 __ne__(self, other):
|
||||
"""
|
||||
Inequality comparison.
|
||||
|
||||
This negates L{__eq__} for comparison with JIDs and uses the default
|
||||
comparison for other types.
|
||||
"""
|
||||
result = self.__eq__(other)
|
||||
if result is NotImplemented:
|
||||
return result
|
||||
else:
|
||||
return not result
|
||||
|
||||
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()
|
||||
|
||||
if _PY3:
|
||||
__str__ = __unicode__
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Get object representation.
|
||||
|
||||
Returns a string that would create a new JID object that compares equal
|
||||
to this one.
|
||||
"""
|
||||
return 'JID(%r)' % self.full()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- 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 __future__ import absolute_import, division
|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
XMPP-specific SASL profile.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
import re
|
||||
from twisted.internet import defer
|
||||
from twisted.python.compat import unicode
|
||||
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(unicode(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):
|
||||
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 u'=')
|
||||
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(unicode(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))
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
# -*- test-case-name: twisted.words.test.test_jabbersaslmechanisms -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Protocol agnostic implementations of SASL authentication mechanisms.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import binascii, random, time, os
|
||||
from hashlib import md5
|
||||
|
||||
from zope.interface import Interface, Attribute, implementer
|
||||
|
||||
from twisted.python.compat import iteritems, 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(object):
|
||||
"""
|
||||
Implements the ANONYMOUS SASL authentication mechanism.
|
||||
|
||||
This mechanism is defined in RFC 2245.
|
||||
"""
|
||||
name = 'ANONYMOUS'
|
||||
|
||||
def getInitialResponse(self):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@implementer(ISASLMechanism)
|
||||
class Plain(object):
|
||||
"""
|
||||
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 u''
|
||||
self.authcid = authcid or u''
|
||||
self.password = password or u''
|
||||
|
||||
|
||||
def getInitialResponse(self):
|
||||
return (self.authzid.encode('utf-8') + b"\x00" +
|
||||
self.authcid.encode('utf-8') + b"\x00" +
|
||||
self.password.encode('utf-8'))
|
||||
|
||||
|
||||
|
||||
@implementer(ISASLMechanism)
|
||||
class DigestMD5(object):
|
||||
"""
|
||||
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 username: 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 = u'%s/%s' % (serv_type, host)
|
||||
if serv_name is not None:
|
||||
self.digest_uri += u'/%s' % (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 iteritems(directives):
|
||||
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('%08x' % (1,)) # 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')
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,244 @@
|
|||
# -*- test-case-name: twisted.words.test.test_jabberxmppstringprep -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from encodings import idna
|
||||
from itertools import chain
|
||||
import stringprep
|
||||
|
||||
# We require Unicode version 3.2.
|
||||
from unicodedata import ucd_3_2_0 as unicodedata
|
||||
|
||||
from twisted.python.compat import unichr
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
from incremental import Version
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
|
||||
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 u"".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 = [unichr(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 = u'.'
|
||||
del labels[-1]
|
||||
else:
|
||||
trailing_dot = u''
|
||||
|
||||
for label in labels:
|
||||
result.append(self.nameprep(label))
|
||||
|
||||
return u".".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] == u'-':
|
||||
raise UnicodeError("Invalid leading hyphen-minus")
|
||||
if label[-1] == u'-':
|
||||
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([u'"', u'&', u"'", u'/',
|
||||
u':', u'<', u'>', u'@'])])
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue