Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -0,0 +1,6 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Chat protocols.
"""

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
# -*- test-case-name: twisted.words.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted Jabber: Jabber Protocol Helpers
"""

View file

@ -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),
]

View file

@ -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)

View file

@ -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

View file

@ -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).
"""

View file

@ -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()

View file

@ -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)

View file

@ -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))

View file

@ -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

View file

@ -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()