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