pyxmpp.clientstream

Package pyxmpp :: Module clientstream |
[hide private]

# Source Code for Module pyxmpp.clientstream

      1  #
      2  # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17  # pylint: disable-msg=W0221
     18
     19  """Client stream handling.
     20
     21  Normative reference:
     22    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
     23  """
     24
     25  __revision__="$Id: clientstream.py 714 2010-04-05 10:20:10Z jajcus $"
     26  __docformat__="restructuredtext en"
     27
     28  import logging
     29
     30  from pyxmpp.stream import Stream
     31  from pyxmpp.streambase import BIND_NS
     32  from pyxmpp.streamsasl import SASLNotAvailable,SASLMechanismNotAvailable
     33  from pyxmpp.jid import JID
     34  from pyxmpp.utils import to_utf8
     35  from pyxmpp.exceptions import StreamError,StreamAuthenticationError,FatalStreamError
     36  from pyxmpp.exceptions import ClientStreamError, FatalClientStreamError
     37
     38 -class ClientStream(Stream):
     39      """Handles XMPP-IM client connection stream.
     40
     41      Both client and server side of the connection is supported. This class handles
     42      client SASL authentication, authorisation and resource binding.
     43
     44      This class is not ready for handling of legacy Jabber servers, as it doesn't
     45      provide legacy authentication.
     46
     47      :Ivariables:
     48          - `my_jid`: requested local JID. Please notice that this may differ from
     49            `me`, which is actual authorized JID after the resource binding.
     50          - `server`: server to use.
     51          - `port`: port number to use.
     52          - `password`: user's password.
     53          - `auth_methods`: allowed authentication methods.
     54      :Types:
     55          - `my_jid`: `pyxmpp.JID`
     56          - `server`: `str`
     57          - `port`: `int`
     58          - `password`: `str`
     59          - `auth_methods`: `list` of `str`
     60      """
     61 -    def __init__(self, jid, password=None, server=None, port=None,
     62              auth_methods = ("sasl:DIGEST-MD5",),
     63              tls_settings = None, keepalive = 0, owner = None):
     64          """Initialize the ClientStream object.
     65
     66          :Parameters:
     67              - `jid`: local JID.
     68              - `password`: user's password.
     69              - `server`: server to use. If not given then address will be derived form the JID.
     70              - `port`: port number to use. If not given then address will be derived form the JID.
     71              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
     72                in the list should be prefixed with "sasl:" string.
     73              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
     74              - `keepalive`: keepalive output interval. 0 to disable.
     75              - `owner`: `Client`, `Component` or similar object "owning" this stream.
     76          :Types:
     77              - `jid`: `pyxmpp.JID`
     78              - `password`: `unicode`
     79              - `server`: `unicode`
     80              - `port`: `int`
     81              - `auth_methods`: sequence of `str`
     82              - `tls_settings`: `pyxmpp.TLSSettings`
     83              - `keepalive`: `int`
     84          """
     85          sasl_mechanisms=[]
     86          for m in auth_methods:
     87              if not m.startswith("sasl:"):
     88                  continue
     89              m=m[5:].upper()
     90              sasl_mechanisms.append(m)
     91          Stream.__init__(self, "jabber:client",
     92                      sasl_mechanisms = sasl_mechanisms,
     93                      tls_settings = tls_settings,
     94                      keepalive = keepalive,
     95                      owner = owner)
     96          self.server=server
     97          self.port=port
     98          self.password=password
     99          self.auth_methods=auth_methods
    100          self.my_jid=jid
    101          self.me = None
    102          self._auth_methods_left = None
    103          self.__logger=logging.getLogger("pyxmpp.ClientStream")
    104
    105 -    def _reset(self):
    106          """Reset `ClientStream` object state, making the object ready to handle
    107          new connections."""
    108          Stream._reset(self)
    109          self._auth_methods_left=[]
    110
    111 -    def connect(self,server=None,port=None):
    112          """Establish a client connection to a server.
    113
    114          [client only]
    115
    116          :Parameters:
    117              - `server`: name or address of the server to use. Not recommended -- proper value
    118                should be derived automatically from the JID.
    119              - `port`: port number of the server to use. Not recommended --
    120                proper value should be derived automatically from the JID.
    121
    122          :Types:
    123              - `server`: `unicode`
    124              - `port`: `int`"""
    125          self.lock.acquire()
    126          try:
    127              self._connect(server,port)
    128          finally:
    129              self.lock.release()
    130
    131 -    def _connect(self,server=None,port=None):
    132          """Same as `ClientStream.connect` but assume `self.lock` is acquired."""
    133          if not self.my_jid.node or not self.my_jid.resource:
    134              raise ClientStreamError,"Client JID must have username and resource"
    135          if not server:
    136              server=self.server
    137          if not port:
    138              port=self.port
    139          if server:
    140              self.__logger.debug("server: %r", (server,))
    141              service=None
    142          else:
    143              service="xmpp-client"
    144          if port is None:
    145              port=5222
    146          if server is None:
    147              server=self.my_jid.domain
    148          self.me=self.my_jid
    149          Stream._connect(self,server,port,service,self.my_jid.domain)
    150
    151 -    def accept(self,sock):
    152          """Accept an incoming client connection.
    153
    154          [server only]
    155
    156          :Parameters:
    157              - `sock`: a listening socket."""
    158          Stream.accept(self,sock,self.my_jid)
    159
    160 -    def _post_connect(self):
    161          """Initialize authentication when the connection is established
    162          and we are the initiator."""
    163          if self.initiator:
    164              self._auth_methods_left=list(self.auth_methods)
    165              self._try_auth()
    166
    167 -    def _try_auth(self):
    168          """Try to authenticate using the first one of allowed authentication
    169          methods left.
    170
    171          [client only]"""
    172          if not self.doc_out:
    173              self.__logger.debug("try_auth: disconnecting already?")
    174              return
    175          if self.authenticated:
    176              self.__logger.debug("try_auth: already authenticated")
    177              return
    178          self.__logger.debug("trying auth: %r", (self._auth_methods_left,))
    179          if not self._auth_methods_left:
    180              raise StreamAuthenticationError,"No allowed authentication methods available"
    181          method=self._auth_methods_left[0]
    182          if method.startswith("sasl:"):
    183              if self.version:
    184                  self._auth_methods_left.pop(0)
    185                  try:
    186                      self._sasl_authenticate(self.my_jid.node, None,
    187                              mechanism=method[5:].upper())
    188                  except (SASLMechanismNotAvailable,SASLNotAvailable):
    189                      self.__logger.debug("Skipping unavailable auth method: %s", (method,) )
    190                      return self._try_auth()
    191              else:
    192                  self._auth_methods_left.pop(0)
    193                  self.__logger.debug("Skipping auth method %s as legacy protocol is in use",
    194                          (method,) )
    195                  return self._try_auth()
    196          else:
    197              self._auth_methods_left.pop(0)
    198              self.__logger.debug("Skipping unknown auth method: %s", method)
    199              return self._try_auth()
    200
    201 -    def _get_stream_features(self):
    202          """Include resource binding feature in the stream features list.
    203
    204          [server only]"""
    205          features=Stream._get_stream_features(self)
    206          if self.peer_authenticated:
    207              bind=features.newChild(None,"bind",None)
    208              ns=bind.newNs(BIND_NS,None)
    209              bind.setNs(ns)
    210              self.set_iq_set_handler("bind",BIND_NS,self.do_bind)
    211          return features
    212
    213 -    def do_bind(self,stanza):
    214          """Do the resource binding requested by a client connected.
    215
    216          [server only]
    217
    218          :Parameters:
    219              - `stanza`: resource binding request stanza.
    220          :Types:
    221              - `stanza`: `pyxmpp.Iq`"""
    222          fr=stanza.get_from()
    223          if fr and fr!=self.peer:
    224              r=stanza.make_error_response("forbidden")
    225              self.send(r)
    226              r.free()
    227              return
    228          resource_n=stanza.xpath_eval("bind:bind/bind:resource",{"bind":BIND_NS})
    229          if resource_n:
    230              resource=resource_n[0].getContent()
    231          else:
    232              resource="auto"
    233          if not resource:
    234              r=stanza.make_error_response("bad-request")
    235          else:
    236              self.unset_iq_set_handler("bind",BIND_NS)
    237              r=stanza.make_result_response()
    238              self.peer.set_resource(resource)
    239              q=r.new_query(BIND_NS,"bind")
    240              q.newTextChild(None,"jid",to_utf8(self.peer.as_unicode()))
    241              self.state_change("authorized",self.peer)
    242          r.set_to(None)
    243          self.send(r)
    244          r.free()
    245
    246 -    def get_password(self, username, realm=None, acceptable_formats=("plain",)):
    247          """Get a user password for the SASL authentication.
    248
    249          :Parameters:
    250              - `username`: username used for authentication.
    251              - `realm`: realm used for authentication.
    252              - `acceptable_formats`: acceptable password encoding formats requested.
    253          :Types:
    254              - `username`: `unicode`
    255              - `realm`: `unicode`
    256              - `acceptable_formats`: `list` of `str`
    257
    258          :return: The password and the format name ('plain').
    259          :returntype: (`unicode`,`str`)"""
    260          _unused = realm
    261          if self.initiator and self.my_jid.node==username and "plain" in acceptable_formats:
    262              return self.password,"plain"
    263          else:
    264              return None,None
    265
    266 -    def get_realms(self):
    267          """Get realms available for client authentication.
    268
    269          [server only]
    270
    271          :return: list of realms.
    272          :returntype: `list` of `unicode`"""
    273          return [self.my_jid.domain]
    274
    275 -    def choose_realm(self,realm_list):
    276          """Choose authentication realm from the list provided by the server.
    277
    278          [client only]
    279
    280          Use domain of the own JID if no realm list was provided or the domain is on the list
    281          or the first realm on the list otherwise.
    282
    283          :Parameters:
    284              - `realm_list`: realm list provided by the server.
    285          :Types:
    286              - `realm_list`: `list` of `unicode`
    287
    288          :return: the realm chosen.
    289          :returntype: `unicode`"""
    290          if not realm_list:
    291              return self.my_jid.domain
    292          if self.my_jid.domain in realm_list:
    293              return self.my_jid.domain
    294          return realm_list[0]
    295
    296 -    def check_authzid(self,authzid,extra_info=None):
    297          """Check authorization id provided by the client.
    298
    299          [server only]
    300
    301          :Parameters:
    302              - `authzid`: authorization id provided.
    303              - `extra_info`: additional information about the user
    304                from the authentication backend. This mapping will
    305                usually contain at least 'username' item.
    306          :Types:
    307              - `authzid`: unicode
    308              - `extra_info`: mapping
    309
    310          :return: `True` if user is authorized to use that `authzid`.
    311          :returntype: `bool`"""
    312          if not extra_info:
    313              extra_info={}
    314          if not authzid:
    315              return 1
    316          if not self.initiator:
    317              jid=JID(authzid)
    318              if not extra_info.has_key("username"):
    319                  ret=0
    320              elif jid.node!=extra_info["username"]:
    321                  ret=0
    322              elif jid.domain!=self.my_jid.domain:
    323                  ret=0
    324              elif not jid.resource:
    325                  ret=0
    326              else:
    327                  ret=1
    328          else:
    329              ret=0
    330          return ret
    331
    332 -    def get_serv_type(self):
    333          """Get the server name for SASL authentication.
    334
    335          :return: 'xmpp'."""
    336          return "xmpp"
    337
    338 -    def get_serv_name(self):
    339          """Get the service name for SASL authentication.
    340
    341          :return: domain of the own JID."""
    342          return self.my_jid.domain
    343
    344 -    def get_serv_host(self):
    345          """Get the service host name for SASL authentication.
    346
    347          :return: domain of the own JID."""
    348          # FIXME: that should be the hostname choosen from SRV records found.
    349          return self.my_jid.domain
    350
    351 -    def fix_out_stanza(self,stanza):
    352          """Fix outgoing stanza.
    353
    354          On a client clear the sender JID. On a server set the sender
    355          address to the own JID if the address is not set yet."""
    356          if self.initiator:
    357              stanza.set_from(None)
    358          else:
    359              if not stanza.get_from():
    360                  stanza.set_from(self.my_jid)
    361
    362 -    def fix_in_stanza(self,stanza):
    363          """Fix an incoming stanza.
    364
    365          Ona server replace the sender address with authorized client JID."""
    366          if self.initiator:
    367              Stream.fix_in_stanza(self,stanza)
    368          else:
    369              stanza.set_from(self.peer)
    370
    371  # vi: sts=4 et sw=4
    372