diff options
Diffstat (limited to 'xmpp/transports.py')
-rw-r--r-- | xmpp/transports.py | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/xmpp/transports.py b/xmpp/transports.py new file mode 100644 index 0000000..0a12a74 --- /dev/null +++ b/xmpp/transports.py @@ -0,0 +1,403 @@ +## transports.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2, or (at your option) +## any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +# $Id: transports.py, v1.36 2013/11/03 alkorgun Exp $ + +""" +This module contains the low-level implementations of xmpppy connect methods or +(in other words) transports for xmpp-stanzas. +Currently here is three transports: +direct TCP connect - TCPsocket class +proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies) +TLS connection - TLS class. Can be used for SSL connections also. + +Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport. + +Also exception 'error' is defined to allow capture of this module specific exceptions. +""" + +import sys +import socket +import dispatcher + +from base64 import encodestring +from select import select +from simplexml import ustr +from plugin import PlugIn +from protocol import * + +# http://pydns.sourceforge.net +try: + import dns +except ImportError: + dns = None + +DATA_RECEIVED = 'DATA RECEIVED' +DATA_SENT = 'DATA SENT' +DBG_CONNECT_PROXY = 'CONNECTproxy' + +BUFLEN = 1024 + +class error: + """ + An exception to be raised in case of low-level errors in methods of 'transports' module. + """ + def __init__(self, comment): + """ + Cache the descriptive string. + """ + self._comment = comment + + def __str__(self): + """ + Serialize exception into pre-cached descriptive string. + """ + return self._comment + +class TCPsocket(PlugIn): + """ + This class defines direct TCP connection method. + """ + def __init__(self, server=None, use_srv=True): + """ + Cache connection point 'server'. 'server' is the tuple of (host, port) + absolutely the same as standard tcp socket uses. However library will lookup for + ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is) + server instead. + """ + PlugIn.__init__(self) + self.DBG_LINE = "socket" + self._exported_methods = [self.send, self.disconnect] + self._server, self.use_srv = server, use_srv + + def srv_lookup(self, server): + """ + SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair. + """ + if dns: + query = "_xmpp-client._tcp.%s" % server[0] + try: + dns.DiscoverNameServers() + dns__ = dns.Request() + response = dns__.req(query, qtype="SRV") + if response.answers: + (port, host) = response.answers[0]["data"][2:] + server = str(host), int(port) + except dns.DNSError: + self.DEBUG("An error occurred while looking up %s." % query, "warn") + return server + + def plugin(self, owner): + """ + Fire up connection. Return non-empty string on success. + Also registers self.disconnected method in the owner's dispatcher. + Called internally. + """ + if not self._server: + self._server = (self._owner.Server, 5222) + if self.use_srv: + server = self.srv_lookup(self._server) + else: + server = self._server + if not self.connect(server): + return None + self._owner.Connection = self + self._owner.RegisterDisconnectHandler(self.disconnected) + return "ok" + + def getHost(self): + """ + Returns the 'host' value that is connection is [will be] made to. + """ + return self._server[0] + + def getPort(self): + """ + Returns the 'port' value that is connection is [will be] made to. + """ + return self._server[1] + + def connect(self, server=None): + """ + Try to connect to the given host/port. Does not lookup for SRV record. + Returns non-empty string on success. + """ + if not server: + server = self._server + host, port = server + server = (host, int(port)) + if ":" in host: + sock = socket.AF_INET6 + server = server.__add__((0, 0)) + else: + sock = socket.AF_INET + try: + self._sock = socket.socket(sock, socket.SOCK_STREAM) + self._sock.connect(server) + self._send = self._sock.sendall + self._recv = self._sock.recv + except socket.error, (errno, strerror): + self.DEBUG("Failed to connect to remote host %s: %s (%s)" % (repr(server), strerror, errno), "error") + except: + pass + else: + self.DEBUG("Successfully connected to remote host %s." % repr(server), "start") + return "ok" + + def plugout(self): + """ + Disconnect from the remote server and unregister self.disconnected method from + the owner's dispatcher. + """ + self._sock.close() + if hasattr(self._owner, "Connection"): + del self._owner.Connection + self._owner.UnregisterDisconnectHandler(self.disconnected) + + def receive(self): + """ + Reads all pending incoming data. + In case of disconnection calls owner's disconnected() method and then raises IOError exception. + """ + try: + data = self._recv(BUFLEN) + except socket.sslerror as e: + self._seen_data = 0 + if e[0] in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): + return "" + self.DEBUG("Socket error while receiving data.", "error") + sys.exc_clear() + self._owner.disconnected() + raise IOError("Disconnected!") + except: + data = "" + while self.pending_data(0): + try: + add = self._recv(BUFLEN) + except: + break + if not add: + break + data += add + if data: + self._seen_data = 1 + self.DEBUG(data, "got") + if hasattr(self._owner, "Dispatcher"): + self._owner.Dispatcher.Event("", DATA_RECEIVED, data) + else: + self.DEBUG("Socket error while receiving data.", "error") + sys.exc_clear() + self._owner.disconnected() + raise IOError("Disconnected!") + return data + + def send(self, data): + """ + Writes raw outgoing data. Blocks until done. + If supplied data is unicode string, encodes it to utf-8 before send. + """ + if isinstance(data, unicode): + data = data.encode("utf-8") + elif not isinstance(data, str): + data = ustr(data).encode("utf-8") + try: + self._send(data) + except: + self.DEBUG("Socket error while sending data.", "error") + self._owner.disconnected() + else: + if not data.strip(): + data = repr(data) + self.DEBUG(data, "sent") + if hasattr(self._owner, "Dispatcher"): + self._owner.Dispatcher.Event("", DATA_SENT, data) + + def pending_data(self, timeout=0): + """ + Returns true if there is a data ready to be read. + """ + return select([self._sock], [], [], timeout)[0] + + def disconnect(self): + """ + Closes the socket. + """ + self.DEBUG("Closing socket.", "stop") + self._sock.close() + + def disconnected(self): + """ + Called when a Network Error or disconnection occurs. + Designed to be overidden. + """ + self.DEBUG("Socket operation failed.", "error") + +class HTTPPROXYsocket(TCPsocket): + """ + HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class + redefines only connect method. Allows to use HTTP proxies like squid with + (optionally) simple authentication (using login and password). + """ + def __init__(self, proxy, server, use_srv=True): + """ + Caches proxy and target addresses. + 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) + and optional keys 'user' and 'password' to use for authentication. + 'server' argument is a tuple of host and port - just like TCPsocket uses. + """ + TCPsocket.__init__(self, server, use_srv) + self.DBG_LINE = DBG_CONNECT_PROXY + self._proxy = proxy + + def plugin(self, owner): + """ + Starts connection. Used interally. Returns non-empty string on success. + """ + owner.debug_flags.append(DBG_CONNECT_PROXY) + return TCPsocket.plugin(self, owner) + + def connect(self, dupe=None): + """ + Starts connection. Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. Returns non-empty sting on success. + """ + if not TCPsocket.connect(self, (self._proxy["host"], self._proxy["port"])): + return None + self.DEBUG("Proxy server contacted, performing authentification.", "start") + connector = [ + "CONNECT %s:%s HTTP/1.0" % self._server, + "Proxy-Connection: Keep-Alive", + "Pragma: no-cache", + "Host: %s:%s" % self._server, + "User-Agent: HTTPPROXYsocket/v0.1" + ] + if "user" in self._proxy and "password" in self._proxy: + credentials = "%s:%s" % (self._proxy["user"], self._proxy["password"]) + credentials = encodestring(credentials).strip() + connector.append("Proxy-Authorization: Basic " + credentials) + connector.append("\r\n") + self.send("\r\n".join(connector)) + try: + reply = self.receive().replace("\r", "") + except IOError: + self.DEBUG("Proxy suddenly disconnected.", "error") + self._owner.disconnected() + return None + try: + proto, code, desc = reply.split("\n")[0].split(" ", 2) + except: + raise error("Invalid proxy reply") + if code != "200": + self.DEBUG("Invalid proxy reply: %s %s %s" % (proto, code, desc), "error") + self._owner.disconnected() + return None + while reply.find("\n\n") == -1: + try: + reply += self.receive().replace("\r", "") + except IOError: + self.DEBUG("Proxy suddenly disconnected.", "error") + self._owner.disconnected() + return None + self.DEBUG("Authentification successfull. Jabber server contacted.", "ok") + return "ok" + + def DEBUG(self, text, severity): + """ + Overwrites DEBUG tag to allow debug output be presented as 'CONNECTproxy'. + """ + return self._owner.DEBUG(DBG_CONNECT_PROXY, text, severity) + +class TLS(PlugIn): + """ + TLS connection used to encrypts already estabilished tcp connection. + """ + def PlugIn(self, owner, now=0): + """ + If the 'now' argument is true then starts using encryption immidiatedly. + If 'now' in false then starts encryption as soon as TLS feature is + declared by the server (if it were already declared - it is ok). + """ + if hasattr(owner, "TLS"): + return None + PlugIn.PlugIn(self, owner) + DBG_LINE = "TLS" + if now: + return self._startSSL() + if self._owner.Dispatcher.Stream.features: + try: + self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) + except NodeProcessed: + pass + else: + self._owner.RegisterHandlerOnce("features", self.FeaturesHandler, xmlns=NS_STREAMS) + self.starttls = None + + def plugout(self, now=0): + """ + Unregisters TLS handler's from owner's dispatcher. Take note that encription + can not be stopped once started. You can only break the connection and start over. + """ + self._owner.UnregisterHandler("features", self.FeaturesHandler, xmlns=NS_STREAMS) + self._owner.UnregisterHandler("proceed", self.StartTLSHandler, xmlns=NS_TLS) + self._owner.UnregisterHandler("failure", self.StartTLSHandler, xmlns=NS_TLS) + + def FeaturesHandler(self, conn, feats): + """ + Used to analyse server <features/> tag for TLS support. + If TLS is supported starts the encryption negotiation. Used internally. + """ + if not feats.getTag("starttls", namespace=NS_TLS): + self.DEBUG("TLS unsupported by remote server.", "warn") + return None + self.DEBUG("TLS supported by remote server. Requesting TLS start.", "ok") + self._owner.RegisterHandlerOnce("proceed", self.StartTLSHandler, xmlns=NS_TLS) + self._owner.RegisterHandlerOnce("failure", self.StartTLSHandler, xmlns=NS_TLS) + self._owner.Connection.send("<starttls xmlns=\"%s\"/>" % NS_TLS) + raise NodeProcessed() + + def pending_data(self, timeout=0): + """ + Returns true if there possible is a data ready to be read. + """ + return self._tcpsock._seen_data or select([self._tcpsock._sock], [], [], timeout)[0] + + def _startSSL(self): + tcpsock = self._owner.Connection + tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) + tcpsock._sslIssuer = tcpsock._sslObj.issuer() + tcpsock._sslServer = tcpsock._sslObj.server() + tcpsock._recv = tcpsock._sslObj.read + tcpsock._send = tcpsock._sslObj.write + tcpsock._seen_data = 1 + self._tcpsock = tcpsock + tcpsock.pending_data = self.pending_data + tcpsock._sock.setblocking(0) + self.starttls = "success" + + def StartTLSHandler(self, conn, starttls): + """ + Handle server reply if TLS is allowed to process. Behaves accordingly. + Used internally. + """ + if starttls.getNamespace() != NS_TLS: + return None + self.starttls = starttls.getName() + if self.starttls == "failure": + self.DEBUG("Got starttls response: " + self.starttls, "error") + return None + self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...", "ok") + self._startSSL() + self._owner.Dispatcher.PlugOut() + dispatcher.Dispatcher().PlugIn(self._owner) |