client.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. ## client.py
  2. ##
  3. ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
  4. ##
  5. ## This program is free software; you can redistribute it and/or modify
  6. ## it under the terms of the GNU General Public License as published by
  7. ## the Free Software Foundation; either version 2, or (at your option)
  8. ## any later version.
  9. ##
  10. ## This program is distributed in the hope that it will be useful,
  11. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ## GNU General Public License for more details.
  14. # $Id$
  15. """
  16. Provides PlugIn class functionality to develop extentions for xmpppy.
  17. Also provides Client and Component classes implementations as the
  18. examples of xmpppy structures usage.
  19. These classes can be used for simple applications "AS IS" though.
  20. """
  21. import socket
  22. from . import debug
  23. Debug=debug
  24. Debug.DEBUGGING_IS_ON=1
  25. Debug.Debug.colors['socket']=debug.color_dark_gray
  26. Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray
  27. Debug.Debug.colors['nodebuilder']=debug.color_brown
  28. Debug.Debug.colors['client']=debug.color_cyan
  29. Debug.Debug.colors['component']=debug.color_cyan
  30. Debug.Debug.colors['dispatcher']=debug.color_green
  31. Debug.Debug.colors['browser']=debug.color_blue
  32. Debug.Debug.colors['auth']=debug.color_yellow
  33. Debug.Debug.colors['roster']=debug.color_magenta
  34. Debug.Debug.colors['ibb']=debug.color_yellow
  35. Debug.Debug.colors['down']=debug.color_brown
  36. Debug.Debug.colors['up']=debug.color_brown
  37. Debug.Debug.colors['data']=debug.color_brown
  38. Debug.Debug.colors['ok']=debug.color_green
  39. Debug.Debug.colors['warn']=debug.color_yellow
  40. Debug.Debug.colors['error']=debug.color_red
  41. Debug.Debug.colors['start']=debug.color_dark_gray
  42. Debug.Debug.colors['stop']=debug.color_dark_gray
  43. Debug.Debug.colors['sent']=debug.color_yellow
  44. Debug.Debug.colors['got']=debug.color_bright_cyan
  45. DBG_CLIENT='client'
  46. DBG_COMPONENT='component'
  47. class PlugIn:
  48. """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """
  49. def __init__(self):
  50. self._exported_methods=[]
  51. self.DBG_LINE=self.__class__.__name__.lower()
  52. def PlugIn(self,owner):
  53. """ Attach to main instance and register ourself and all our staff in it. """
  54. self._owner=owner
  55. if self.DBG_LINE not in owner.debug_flags:
  56. owner.debug_flags.append(self.DBG_LINE)
  57. self.DEBUG('Plugging %s into %s'%(self,self._owner),'start')
  58. if self.__class__.__name__ in owner.__dict__:
  59. return self.DEBUG('Plugging ignored: another instance already plugged.','error')
  60. self._old_owners_methods=[]
  61. for method in self._exported_methods:
  62. if method.__name__ in owner.__dict__:
  63. self._old_owners_methods.append(owner.__dict__[method.__name__])
  64. owner.__dict__[method.__name__]=method
  65. owner.__dict__[self.__class__.__name__]=self
  66. if 'plugin' in self.__class__.__dict__: return self.plugin(owner)
  67. def PlugOut(self):
  68. """ Unregister all our staff from main instance and detach from it. """
  69. self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop')
  70. ret = None
  71. if 'plugout' in self.__class__.__dict__: ret = self.plugout()
  72. self._owner.debug_flags.remove(self.DBG_LINE)
  73. for method in self._exported_methods: del self._owner.__dict__[method.__name__]
  74. for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method
  75. del self._owner.__dict__[self.__class__.__name__]
  76. return ret
  77. def DEBUG(self,text,severity='info'):
  78. """ Feed a provided debug line to main instance's debug facility along with our ID string. """
  79. self._owner.DEBUG(self.DBG_LINE,text,severity)
  80. import xmpp
  81. from xmpp import transports, dispatcher, roster
  82. class CommonClient:
  83. """ Base for Client and Component classes."""
  84. def __init__(self,server,port=5222,debug=['always', 'nodebuilder']):
  85. """ Caches server name and (optionally) port to connect to. "debug" parameter specifies
  86. the debug IDs that will go into debug output. You can either specifiy an "include"
  87. or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
  88. Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket',
  89. 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """
  90. if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT
  91. elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT
  92. self.defaultNamespace=self.Namespace
  93. self.disconnect_handlers=[]
  94. self.Server=server
  95. self.Port=port
  96. if debug and type(debug)!=list: debug=['always', 'nodebuilder']
  97. self._DEBUG=Debug.Debug(debug)
  98. self.DEBUG=self._DEBUG.Show
  99. self.debug_flags=self._DEBUG.debug_flags
  100. self.debug_flags.append(self.DBG)
  101. self._owner=self
  102. self._registered_name=None
  103. self.RegisterDisconnectHandler(self.DisconnectHandler)
  104. self.connected=''
  105. self._route=0
  106. def RegisterDisconnectHandler(self,handler):
  107. """ Register handler that will be called on disconnect."""
  108. self.disconnect_handlers.append(handler)
  109. def UnregisterDisconnectHandler(self,handler):
  110. """ Unregister handler that is called on disconnect."""
  111. self.disconnect_handlers.remove(handler)
  112. def disconnected(self):
  113. """ Called on disconnection. Calls disconnect handlers and cleans things up. """
  114. self.connected=''
  115. self.DEBUG(self.DBG,'Disconnect detected','stop')
  116. self.disconnect_handlers.reverse()
  117. for i in self.disconnect_handlers: i()
  118. self.disconnect_handlers.reverse()
  119. if 'TLS' in self.__dict__: self.TLS.PlugOut()
  120. def DisconnectHandler(self):
  121. """ Default disconnect handler. Just raises an IOError.
  122. If you choosed to use this class in your production client,
  123. override this method or at least unregister it. """
  124. raise IOError('Disconnected from server.')
  125. def event(self,eventName,args={}):
  126. """ Default event handler. To be overriden. """
  127. print(("Event: ",(eventName,args)))
  128. def isConnected(self):
  129. """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """
  130. return self.connected
  131. def reconnectAndReauth(self):
  132. """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """
  133. handlerssave=self.Dispatcher.dumpHandlers()
  134. if 'ComponentBind' in self.__dict__: self.ComponentBind.PlugOut()
  135. if 'Bind' in self.__dict__: self.Bind.PlugOut()
  136. self._route=0
  137. if 'NonSASL' in self.__dict__: self.NonSASL.PlugOut()
  138. if 'SASL' in self.__dict__: self.SASL.PlugOut()
  139. if 'TLS' in self.__dict__: self.TLS.PlugOut()
  140. self.Dispatcher.PlugOut()
  141. if 'HTTPPROXYsocket' in self.__dict__: self.HTTPPROXYsocket.PlugOut()
  142. if 'TCPsocket' in self.__dict__: self.TCPsocket.PlugOut()
  143. if not self.connect(server=self._Server,proxy=self._Proxy): return
  144. if not self.auth(self._User,self._Password,self._Resource): return
  145. self.Dispatcher.restoreHandlers(handlerssave)
  146. return self.connected
  147. def connect(self,server=None,proxy=None,ssl=None,use_srv=None,transport=None):
  148. """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
  149. Returns None or 'tcp' or 'tls', depending on the result."""
  150. if not server: server=(self.Server,self.Port)
  151. if transport:
  152. sock=transport
  153. elif proxy: sock=transports.HTTPPROXYsocket(proxy,server,use_srv)
  154. else: sock=transports.TCPsocket(server,use_srv)
  155. connected=sock.PlugIn(self)
  156. if not connected:
  157. sock.PlugOut()
  158. return
  159. self._Server,self._Proxy=server,proxy
  160. self.connected='tcp'
  161. if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl:
  162. try: # FIXME. This should be done in transports.py
  163. transports.TLS().PlugIn(self,now=1)
  164. self.connected='ssl'
  165. except socket.sslerror:
  166. return
  167. dispatcher.Dispatcher().PlugIn(self)
  168. while self.Dispatcher.Stream._document_attrs is None:
  169. if not self.Process(1): return
  170. if 'version' in self.Dispatcher.Stream._document_attrs and self.Dispatcher.Stream._document_attrs['version']=='1.0':
  171. while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
  172. return self.connected
  173. class Client(CommonClient):
  174. """ Example client class, based on CommonClient. """
  175. def connect(self,server=None,proxy=None,secure=None,use_srv=True, transport=None):
  176. """ Connect to XMPP server. If you want to specify different ip/port to connect to you can
  177. pass it as tuple as first parameter. If there is HTTP proxy between you and server
  178. specify it's address and credentials (if needed) in the second argument.
  179. If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443)
  180. If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1.
  181. If you want to disable tls/ssl support completely, set it to 0.
  182. Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'})
  183. Returns '' or 'tcp' or 'tls', depending on the result."""
  184. if not CommonClient.connect(self,server,proxy,secure,use_srv,transport) or secure!=None and not secure: return self.connected
  185. transports.TLS().PlugIn(self)
  186. if 'version' not in self.Dispatcher.Stream._document_attrs or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected
  187. while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
  188. if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server
  189. while not self.TLS.starttls and self.Process(1): pass
  190. if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected
  191. self.connected='tls'
  192. return self.connected
  193. def auth(self,user,password,resource='',sasl=1):
  194. """ Authenticate connnection and bind resource. If resource is not provided
  195. random one or library name used. """
  196. self._User,self._Password,self._Resource=user,password,resource
  197. while not self.Dispatcher.Stream._document_attrs and self.Process(1): pass
  198. if 'version' in self.Dispatcher.Stream._document_attrs and self.Dispatcher.Stream._document_attrs['version']=='1.0':
  199. while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented
  200. if sasl: xmpp.auth.SASL(user,password).PlugIn(self)
  201. if not sasl or self.SASL.startsasl=='not-supported':
  202. if not resource: resource='xmpppy'
  203. if xmpp.auth.NonSASL(user,password,resource).PlugIn(self):
  204. self.connected+='+old_auth'
  205. return 'old_auth'
  206. return
  207. self.SASL.auth()
  208. while self.SASL.startsasl=='in-process' and self.Process(1): pass
  209. if self.SASL.startsasl=='success':
  210. xmpp.auth.Bind().PlugIn(self)
  211. while self.Bind.bound is None and self.Process(1): pass
  212. if self.Bind.Bind(resource):
  213. self.connected+='+sasl'
  214. return 'sasl'
  215. else:
  216. if 'SASL' in self.__dict__: self.SASL.PlugOut()
  217. def getRoster(self):
  218. """ Return the Roster instance, previously plugging it in and
  219. requesting roster from server if needed. """
  220. if 'Roster' not in self.__dict__: roster.Roster().PlugIn(self)
  221. return self.Roster.getRoster()
  222. def sendInitPresence(self,requestRoster=1):
  223. """ Send roster request and initial <presence/>.
  224. You can disable the first by setting requestRoster argument to 0. """
  225. self.sendPresence(requestRoster=requestRoster)
  226. def sendPresence(self,jid=None,typ=None,requestRoster=0):
  227. """ Send some specific presence state.
  228. Can also request roster from server if according agrument is set."""
  229. if requestRoster: roster.Roster().PlugIn(self)
  230. self.send(dispatcher.Presence(to=jid, typ=typ))
  231. class Component(CommonClient):
  232. """ Component class. The only difference from CommonClient is ability to perform component authentication. """
  233. def __init__(self,transport,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,sasl=0,bind=0,route=0,xcp=0):
  234. """ Init function for Components.
  235. As components use a different auth mechanism which includes the namespace of the component.
  236. Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
  237. Jabberd2 uses jabber:client.
  238. 'transport' argument is a transport name that you are going to serve (f.e. "irc.localhost").
  239. 'port' can be specified if 'transport' resolves to correct IP. If it is not then you'll have to specify IP
  240. and port while calling "connect()".
  241. If you are going to serve several different domains with single Component instance - you must list them ALL
  242. in the 'domains' argument.
  243. For jabberd2 servers you should set typ='jabberd2' argument.
  244. """
  245. CommonClient.__init__(self,transport,port=port,debug=debug)
  246. self.typ=typ
  247. self.sasl=sasl
  248. self.bind=bind
  249. self.route=route
  250. self.xcp=xcp
  251. if domains:
  252. self.domains=domains
  253. else:
  254. self.domains=[transport]
  255. def connect(self,server=None,proxy=None):
  256. """ This will connect to the server, and if the features tag is found then set
  257. the namespace to be jabber:client as that is required for jabberd2.
  258. 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """
  259. if self.sasl:
  260. self.Namespace=xmpp.auth.NS_COMPONENT_1
  261. self.Server=server[0]
  262. CommonClient.connect(self,server=server,proxy=proxy)
  263. if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp):
  264. self.defaultNamespace=xmpp.auth.NS_CLIENT
  265. self.Dispatcher.RegisterNamespace(self.defaultNamespace)
  266. self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq)
  267. self.Dispatcher.RegisterProtocol('message',dispatcher.Message)
  268. self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence)
  269. return self.connected
  270. def dobind(self, sasl):
  271. # This has to be done before binding, because we can receive a route stanza before binding finishes
  272. self._route = self.route
  273. if self.bind:
  274. for domain in self.domains:
  275. xmpp.auth.ComponentBind(sasl).PlugIn(self)
  276. while self.ComponentBind.bound is None: self.Process(1)
  277. if (not self.ComponentBind.Bind(domain)):
  278. self.ComponentBind.PlugOut()
  279. return
  280. self.ComponentBind.PlugOut()
  281. def auth(self,name,password,dup=None):
  282. """ Authenticate component "name" with password "password"."""
  283. self._User,self._Password,self._Resource=name,password,''
  284. try:
  285. if self.sasl: xmpp.auth.SASL(name,password).PlugIn(self)
  286. if not self.sasl or self.SASL.startsasl=='not-supported':
  287. if xmpp.auth.NonSASL(name,password,'').PlugIn(self):
  288. self.dobind(sasl=False)
  289. self.connected+='+old_auth'
  290. return 'old_auth'
  291. return
  292. self.SASL.auth()
  293. while self.SASL.startsasl=='in-process' and self.Process(1): pass
  294. if self.SASL.startsasl=='success':
  295. self.dobind(sasl=True)
  296. self.connected+='+sasl'
  297. return 'sasl'
  298. else:
  299. raise xmpp.auth.NotAuthorized(self.SASL.startsasl)
  300. except:
  301. self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error')