pyxmpp.clientstream

Package pyxmpp :: Module clientstream |
[hide private]

# Source Code for Module pyxmpp.clientstream

      1  #
      2  # (C) Copyright 2003-2006 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 652 2006-08-27 19:41:15Z 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 self.authenticated:
    173              self.__logger.debug("try_auth: already authenticated")
    174              return
    175          self.__logger.debug("trying auth: %r", (self._auth_methods_left,))
    176          if not self._auth_methods_left:
    177              raise StreamAuthenticationError,"No allowed authentication methods available"
    178          method=self._auth_methods_left[0]
    179          if method.startswith("sasl:"):
    180              if self.version:
    181                  self._auth_methods_left.pop(0)
    182                  try:
    183                      self._sasl_authenticate(self.my_jid.node, None,
    184                              mechanism=method[5:].upper())
    185                  except (SASLMechanismNotAvailable,SASLNotAvailable):
    186                      self.__logger.debug("Skipping unavailable auth method: %s", (method,) )
    187                      return self._try_auth()
    188              else:
    189                  self._auth_methods_left.pop(0)
    190                  self.__logger.debug("Skipping auth method %s as legacy protocol is in use",
    191                          (method,) )
    192                  return self._try_auth()
    193          else:
    194              self._auth_methods_left.pop(0)
    195              self.__logger.debug("Skipping unknown auth method: %s", method)
    196              return self._try_auth()
    197
    198 -    def _get_stream_features(self):
    199          """Include resource binding feature in the stream features list.
    200
    201          [server only]"""
    202          features=Stream._get_stream_features(self)
    203          if self.peer_authenticated:
    204              bind=features.newChild(None,"bind",None)
    205              ns=bind.newNs(BIND_NS,None)
    206              bind.setNs(ns)
    207              self.set_iq_set_handler("bind",BIND_NS,self.do_bind)
    208          return features
    209
    210 -    def do_bind(self,stanza):
    211          """Do the resource binding requested by a client connected.
    212
    213          [server only]
    214
    215          :Parameters:
    216              - `stanza`: resource binding request stanza.
    217          :Types:
    218              - `stanza`: `pyxmpp.Iq`"""
    219          fr=stanza.get_from()
    220          if fr and fr!=self.peer:
    221              r=stanza.make_error_response("forbidden")
    222              self.send(r)
    223              r.free()
    224              return
    225          resource_n=stanza.xpath_eval("bind:bind/bind:resource",{"bind":BIND_NS})
    226          if resource_n:
    227              resource=resource_n[0].getContent()
    228          else:
    229              resource="auto"
    230          if not resource:
    231              r=stanza.make_error_response("bad-request")
    232          else:
    233              self.unset_iq_set_handler("bind",BIND_NS)
    234              r=stanza.make_result_response()
    235              self.peer.set_resource(resource)
    236              q=r.new_query(BIND_NS,"bind")
    237              q.newTextChild(None,"jid",to_utf8(self.peer.as_unicode()))
    238              self.state_change("authorized",self.peer)
    239          r.set_to(None)
    240          self.send(r)
    241          r.free()
    242
    243 -    def get_password(self, username, realm=None, acceptable_formats=("plain",)):
    244          """Get a user password for the SASL authentication.
    245
    246          :Parameters:
    247              - `username`: username used for authentication.
    248              - `realm`: realm used for authentication.
    249              - `acceptable_formats`: acceptable password encoding formats requested.
    250          :Types:
    251              - `username`: `unicode`
    252              - `realm`: `unicode`
    253              - `acceptable_formats`: `list` of `str`
    254
    255          :return: The password and the format name ('plain').
    256          :returntype: (`unicode`,`str`)"""
    257          _unused = realm
    258          if self.initiator and self.my_jid.node==username and "plain" in acceptable_formats:
    259              return self.password,"plain"
    260          else:
    261              return None,None
    262
    263 -    def get_realms(self):
    264          """Get realms available for client authentication.
    265
    266          [server only]
    267
    268          :return: list of realms.
    269          :returntype: `list` of `unicode`"""
    270          return [self.my_jid.domain]
    271
    272 -    def choose_realm(self,realm_list):
    273          """Choose authentication realm from the list provided by the server.
    274
    275          [client only]
    276
    277          Use domain of the own JID if no realm list was provided or the domain is on the list
    278          or the first realm on the list otherwise.
    279
    280          :Parameters:
    281              - `realm_list`: realm list provided by the server.
    282          :Types:
    283              - `realm_list`: `list` of `unicode`
    284
    285          :return: the realm chosen.
    286          :returntype: `unicode`"""
    287          if not realm_list:
    288              return self.my_jid.domain
    289          if self.my_jid.domain in realm_list:
    290              return self.my_jid.domain
    291          return realm_list[0]
    292
    293 -    def check_authzid(self,authzid,extra_info=None):
    294          """Check authorization id provided by the client.
    295
    296          [server only]
    297
    298          :Parameters:
    299              - `authzid`: authorization id provided.
    300              - `extra_info`: additional information about the user
    301                from the authentication backend. This mapping will
    302                usually contain at least 'username' item.
    303          :Types:
    304              - `authzid`: unicode
    305              - `extra_info`: mapping
    306
    307          :return: `True` if user is authorized to use that `authzid`.
    308          :returntype: `bool`"""
    309          if not extra_info:
    310              extra_info={}
    311          if not authzid:
    312              return 1
    313          if not self.initiator:
    314              jid=JID(authzid)
    315              if not extra_info.has_key("username"):
    316                  ret=0
    317              elif jid.node!=extra_info["username"]:
    318                  ret=0
    319              elif jid.domain!=self.my_jid.domain:
    320                  ret=0
    321              elif not jid.resource:
    322                  ret=0
    323              else:
    324                  ret=1
    325          else:
    326              ret=0
    327          return ret
    328
    329 -    def get_serv_type(self):
    330          """Get the server name for SASL authentication.
    331
    332          :return: 'xmpp'."""
    333          return "xmpp"
    334
    335 -    def get_serv_name(self):
    336          """Get the service name for SASL authentication.
    337
    338          :return: domain of the own JID."""
    339          return self.my_jid.domain
    340
    341 -    def get_serv_host(self):
    342          """Get the service host name for SASL authentication.
    343
    344          :return: domain of the own JID."""
    345          # FIXME: that should be the hostname choosen from SRV records found.
    346          return self.my_jid.domain
    347
    348 -    def fix_out_stanza(self,stanza):
    349          """Fix outgoing stanza.
    350
    351          On a client clear the sender JID. On a server set the sender
    352          address to the own JID if the address is not set yet."""
    353          if self.initiator:
    354              stanza.set_from(None)
    355          else:
    356              if not stanza.get_from():
    357                  stanza.set_from(self.my_jid)
    358
    359 -    def fix_in_stanza(self,stanza):
    360          """Fix an incoming stanza.
    361
    362          Ona server replace the sender address with authorized client JID."""
    363          if self.initiator:
    364              Stream.fix_in_stanza(self,stanza)
    365          else:
    366              stanza.set_from(self.peer)
    367
    368  # vi: sts=4 et sw=4
    369